diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/exception/CatalogGenericExceptionMapper.java b/openmetadata-service/src/main/java/org/openmetadata/service/exception/CatalogGenericExceptionMapper.java index a4c7ae6c536..bed6c9e6221 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/exception/CatalogGenericExceptionMapper.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/exception/CatalogGenericExceptionMapper.java @@ -41,6 +41,7 @@ import org.slf4j.LoggerFactory; public class CatalogGenericExceptionMapper implements ExceptionMapper { @Override public Response toResponse(Throwable ex) { + ex.printStackTrace(); LOG.debug(ex.getMessage()); if (ex instanceof ProcessingException || ex instanceof IllegalArgumentException diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/UsageRepository.java b/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/UsageRepository.java index ca797410cb8..55c2e43e09b 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/UsageRepository.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/UsageRepository.java @@ -88,10 +88,10 @@ public class UsageRepository { } @Transaction - public RestUtil.PutResponse createOrUpdate(String entityType, String id, DailyCount usage) throws IOException { + public RestUtil.PutResponse createOrUpdate(String entityType, UUID id, DailyCount usage) throws IOException { // Validate data entity for which usage is being collected - Entity.getEntityReferenceById(entityType, UUID.fromString(id), Include.NON_DELETED); - return addUsage(PUT, entityType, id, usage); + Entity.getEntityReferenceById(entityType, id, Include.NON_DELETED); + return addUsage(PUT, entityType, id.toString(), usage); } @Transaction diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/search/SearchResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/search/SearchResource.java index 3bef2910781..a5158a15eb6 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/search/SearchResource.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/search/SearchResource.java @@ -494,7 +494,6 @@ public class SearchResource { @Operation( operationId = "getAllReindexBatchJobs", summary = "Get all reindex batch jobs", - tags = "search", description = "Get all reindex batch jobs", responses = { @ApiResponse(responseCode = "200", description = "Success"), diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/usage/UsageResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/usage/UsageResource.java index 3ee8eb33952..3d64f32a42b 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/usage/UsageResource.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/usage/UsageResource.java @@ -22,6 +22,7 @@ import io.swagger.v3.oas.annotations.tags.Tag; import java.io.IOException; import java.util.Date; import java.util.Objects; +import java.util.UUID; import javax.validation.Valid; import javax.ws.rs.Consumes; import javax.ws.rs.GET; @@ -34,15 +35,20 @@ import javax.ws.rs.QueryParam; import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; +import javax.ws.rs.core.SecurityContext; import javax.ws.rs.core.UriInfo; import lombok.extern.slf4j.Slf4j; import org.openmetadata.schema.type.DailyCount; import org.openmetadata.schema.type.EntityUsage; +import org.openmetadata.schema.type.MetadataOperation; import org.openmetadata.service.Entity; import org.openmetadata.service.jdbi3.CollectionDAO; import org.openmetadata.service.jdbi3.UsageRepository; import org.openmetadata.service.resources.Collection; +import org.openmetadata.service.resources.EntityResource; import org.openmetadata.service.security.Authorizer; +import org.openmetadata.service.security.policyevaluator.OperationContext; +import org.openmetadata.service.security.policyevaluator.ResourceContext; import org.openmetadata.service.util.RestUtil; @Slf4j @@ -53,9 +59,11 @@ import org.openmetadata.service.util.RestUtil; @Collection(name = "usage") public class UsageResource { private final UsageRepository dao; + private final Authorizer authorizer; public UsageResource(CollectionDAO dao, Authorizer authorizer) { Objects.requireNonNull(dao, "UsageRepository must not be null"); + this.authorizer = authorizer; this.dao = new UsageRepository(dao); } @@ -75,6 +83,7 @@ public class UsageResource { }) public EntityUsage get( @Context UriInfo uriInfo, + @Context SecurityContext securityContext, @Parameter( description = "Entity type for which usage is requested", required = true, @@ -93,7 +102,10 @@ public class UsageResource { @QueryParam("date") String date) throws IOException { - // TODO add href + OperationContext operationContext = new OperationContext(entity, MetadataOperation.VIEW_USAGE); + ResourceContext resourceContext = + EntityResource.getResourceContext(entity, Entity.getEntityRepository(entity)).build(); + authorizer.authorize(securityContext, operationContext, resourceContext); int actualDays = Math.min(Math.max(days, 1), 30); String actualDate = date == null ? RestUtil.DATE_FORMAT.format(new Date()) : date; return addHref(uriInfo, dao.get(entity, id, actualDate, actualDays)); @@ -115,6 +127,7 @@ public class UsageResource { }) public EntityUsage getByName( @Context UriInfo uriInfo, + @Context SecurityContext securityContext, @Parameter( description = "Entity type for which usage is requested", required = true, @@ -135,8 +148,12 @@ public class UsageResource { description = "Usage for number of days going back from this date in ISO 8601 format " + "(default = currentDate)") @QueryParam("date") - String date) { - // TODO add href + String date) + throws IOException { + OperationContext operationContext = new OperationContext(entity, MetadataOperation.VIEW_USAGE); + ResourceContext resourceContext = + EntityResource.getResourceContext(entity, Entity.getEntityRepository(entity)).name(fqn).build(); + authorizer.authorize(securityContext, operationContext, resourceContext); int actualDays = Math.min(Math.max(days, 1), 30); String actualDate = date == null ? RestUtil.DATE_FORMAT.format(new Date()) : date; return addHref(uriInfo, dao.getByName(entity, fqn, actualDate, actualDays)); @@ -159,6 +176,7 @@ public class UsageResource { }) public Response create( @Context UriInfo uriInfo, + @Context SecurityContext securityContext, @Parameter( description = "Entity type for which usage is reported", required = true, @@ -169,6 +187,10 @@ public class UsageResource { String id, @Parameter(description = "Usage information a given date") @Valid DailyCount usage) throws IOException { + OperationContext operationContext = new OperationContext(entity, MetadataOperation.EDIT_USAGE); + ResourceContext resourceContext = + EntityResource.getResourceContext(entity, Entity.getEntityRepository(entity)).build(); + authorizer.authorize(securityContext, operationContext, resourceContext); return dao.create(entity, id, usage).toResponse(); } @@ -189,6 +211,7 @@ public class UsageResource { }) public Response createOrUpdate( @Context UriInfo uriInfo, + @Context SecurityContext securityContext, @Parameter( description = "Entity type for which usage is reported", required = true, @@ -196,9 +219,13 @@ public class UsageResource { @PathParam("entity") String entity, @Parameter(description = "Entity id", required = true, schema = @Schema(type = "string")) @PathParam("id") - String id, + UUID id, @Parameter(description = "Usage information a given date") @Valid DailyCount usage) throws IOException { + OperationContext operationContext = new OperationContext(entity, MetadataOperation.EDIT_USAGE); + ResourceContext resourceContext = + EntityResource.getResourceContext(entity, Entity.getEntityRepository(entity)).id(id).build(); + authorizer.authorize(securityContext, operationContext, resourceContext); return dao.createOrUpdate(entity, id, usage).toResponse(); } @@ -219,6 +246,7 @@ public class UsageResource { }) public Response createByName( @Context UriInfo uriInfo, + @Context SecurityContext securityContext, @Parameter( description = "Entity type for which usage is reported", required = true, @@ -233,6 +261,10 @@ public class UsageResource { String fullyQualifiedName, @Parameter(description = "Usage information a given date") @Valid DailyCount usage) throws IOException { + OperationContext operationContext = new OperationContext(entity, MetadataOperation.EDIT_USAGE); + ResourceContext resourceContext = + EntityResource.getResourceContext(entity, Entity.getEntityRepository(entity)).name(fullyQualifiedName).build(); + authorizer.authorize(securityContext, operationContext, resourceContext); return dao.createByName(entity, fullyQualifiedName, usage).toResponse(); } @@ -253,6 +285,7 @@ public class UsageResource { }) public Response createOrUpdateByName( @Context UriInfo uriInfo, + @Context SecurityContext securityContext, @Parameter( description = "Entity type for which usage is reported", required = true, @@ -267,6 +300,10 @@ public class UsageResource { String fullyQualifiedName, @Parameter(description = "Usage information a given date") @Valid DailyCount usage) throws IOException { + OperationContext operationContext = new OperationContext(entity, MetadataOperation.EDIT_USAGE); + ResourceContext resourceContext = + EntityResource.getResourceContext(entity, Entity.getEntityRepository(entity)).name(fullyQualifiedName).build(); + authorizer.authorize(securityContext, operationContext, resourceContext); return dao.createOrUpdateByName(entity, fullyQualifiedName, usage).toResponse(); } @@ -283,6 +320,7 @@ public class UsageResource { }) public Response computePercentile( @Context UriInfo uriInfo, + @Context SecurityContext securityContext, @Parameter( description = "Entity name for which usage is requested", schema = @Schema(type = "string", example = "table, report, metrics, or dashboard")) @@ -292,8 +330,12 @@ public class UsageResource { description = "ISO 8601 format date to compute percentile on", schema = @Schema(type = "string", example = "2021-01-28")) @PathParam("date") - String date) { - // TODO delete this? + String date) + throws IOException { + OperationContext operationContext = new OperationContext(entity, MetadataOperation.EDIT_USAGE); + ResourceContext resourceContext = + EntityResource.getResourceContext(entity, Entity.getEntityRepository(entity)).build(); + authorizer.authorize(securityContext, operationContext, resourceContext); dao.computePercentile(entity, date); return Response.status(Response.Status.CREATED).build(); } diff --git a/openmetadata-service/src/test/java/org/openmetadata/service/resources/usage/UsageResourceTest.java b/openmetadata-service/src/test/java/org/openmetadata/service/resources/usage/UsageResourceTest.java index 223cfaa653e..c002f8cb30b 100644 --- a/openmetadata-service/src/test/java/org/openmetadata/service/resources/usage/UsageResourceTest.java +++ b/openmetadata-service/src/test/java/org/openmetadata/service/resources/usage/UsageResourceTest.java @@ -17,11 +17,16 @@ import static javax.ws.rs.core.Response.Status.BAD_REQUEST; import static javax.ws.rs.core.Response.Status.NOT_FOUND; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.openmetadata.common.utils.CommonUtil.getDateStringByOffset; +import static org.openmetadata.common.utils.CommonUtil.listOf; +import static org.openmetadata.service.Entity.INGESTION_BOT_NAME; import static org.openmetadata.service.Entity.TABLE; import static org.openmetadata.service.exception.CatalogExceptionMessage.entityNotFound; import static org.openmetadata.service.exception.CatalogExceptionMessage.entityTypeNotFound; +import static org.openmetadata.service.security.SecurityUtil.authHeaders; import static org.openmetadata.service.util.TestUtils.ADMIN_AUTH_HEADERS; import static org.openmetadata.service.util.TestUtils.NON_EXISTENT_ENTITY; +import static org.openmetadata.service.util.TestUtils.TEST_AUTH_HEADERS; +import static org.openmetadata.service.util.TestUtils.TEST_USER_NAME; import static org.openmetadata.service.util.TestUtils.assertResponse; import java.io.IOException; @@ -34,6 +39,7 @@ import java.util.Random; import java.util.UUID; import javax.ws.rs.client.WebTarget; import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import org.apache.http.client.HttpResponseException; @@ -49,9 +55,11 @@ import org.openmetadata.schema.entity.data.Database; import org.openmetadata.schema.entity.data.Table; import org.openmetadata.schema.type.DailyCount; import org.openmetadata.schema.type.EntityUsage; +import org.openmetadata.schema.type.MetadataOperation; import org.openmetadata.schema.type.UsageDetails; import org.openmetadata.service.Entity; import org.openmetadata.service.OpenMetadataApplicationTest; +import org.openmetadata.service.exception.CatalogExceptionMessage; import org.openmetadata.service.resources.databases.DatabaseResourceTest; import org.openmetadata.service.resources.databases.TableResourceTest; import org.openmetadata.service.util.RestUtil; @@ -148,25 +156,51 @@ class UsageResourceTest extends OpenMetadataApplicationTest { } @Test - void post_validUsageByName_200_OK(TestInfo test) { - testValidUsageByName(test, POST); + void post_validUsageByNameAsAdmin_200(TestInfo test) { + testValidUsageByName(test, POST, ADMIN_AUTH_HEADERS); } @Test - void put_validUsageByName_200_OK(TestInfo test) { - testValidUsageByName(test, PUT); + void post_validUsageByNameAsIngestionBot_200(TestInfo test) { + testValidUsageByName(test, POST, authHeaders(INGESTION_BOT_NAME)); + } + + @Test + void post_validUsageByNameAsNonPrivilegedUser_401(TestInfo test) { + assertResponse( + () -> testValidUsageByName(test, POST, TEST_AUTH_HEADERS), + Status.FORBIDDEN, + CatalogExceptionMessage.permissionNotAllowed(TEST_USER_NAME, listOf(MetadataOperation.EDIT_USAGE))); + } + + @Test + void put_validUsageByNameAsAdmin_200(TestInfo test) { + testValidUsageByName(test, PUT, ADMIN_AUTH_HEADERS); + } + + @Test + void put_validUsageByNameAsIngestionBot_200(TestInfo test) { + testValidUsageByName(test, PUT, authHeaders(INGESTION_BOT_NAME)); + } + + @Test + void put_validUsageByNameAsNonPrivilegedUser_401(TestInfo test) { + assertResponse( + () -> testValidUsageByName(test, PUT, TEST_AUTH_HEADERS), + Status.FORBIDDEN, + CatalogExceptionMessage.permissionNotAllowed(TEST_USER_NAME, listOf(MetadataOperation.EDIT_USAGE))); } @SneakyThrows - void testValidUsageByName(TestInfo test, String methodType) { + void testValidUsageByName(TestInfo test, String methodType, Map authHeaders) { TableResourceTest tableResourceTest = new TableResourceTest(); Table table = tableResourceTest.createEntity(tableResourceTest.createRequest(test), ADMIN_AUTH_HEADERS); DailyCount usageReport = usageReport().withCount(100).withDate(RestUtil.DATE_FORMAT.format(new Date())); - reportUsageByNameAndCheckPut(TABLE, table.getFullyQualifiedName(), usageReport, 100, 100, ADMIN_AUTH_HEADERS); + reportUsageByNameAndCheckPut(TABLE, table.getFullyQualifiedName(), usageReport, 100, 100, authHeaders); // a put request updates the data again if (methodType.equals(PUT)) { - reportUsageByNamePut(TABLE, table.getFullyQualifiedName(), usageReport, ADMIN_AUTH_HEADERS); - checkUsageByName(usageReport.getDate(), TABLE, table.getFullyQualifiedName(), 200, 200, 200, ADMIN_AUTH_HEADERS); + reportUsageByNamePut(TABLE, table.getFullyQualifiedName(), usageReport, authHeaders); + checkUsageByName(usageReport.getDate(), TABLE, table.getFullyQualifiedName(), 200, 200, 200, authHeaders); } } diff --git a/openmetadata-spec/src/main/resources/json/schema/entity/policies/accessControl/resourceDescriptor.json b/openmetadata-spec/src/main/resources/json/schema/entity/policies/accessControl/resourceDescriptor.json index cabf82a28ee..80abcd34e14 100644 --- a/openmetadata-spec/src/main/resources/json/schema/entity/policies/accessControl/resourceDescriptor.json +++ b/openmetadata-spec/src/main/resources/json/schema/entity/policies/accessControl/resourceDescriptor.json @@ -38,6 +38,7 @@ "EditTeams", "EditTier", "EditTests", + "EditUsage", "EditUsers" ] }