Fixes #11872 - Add authorization for Usage APIs (#11873)

This commit is contained in:
Suresh Srinivas 2023-06-02 15:50:29 -07:00 committed by GitHub
parent b4d56bb559
commit d978580703
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 95 additions and 18 deletions

View File

@ -41,6 +41,7 @@ import org.slf4j.LoggerFactory;
public class CatalogGenericExceptionMapper implements ExceptionMapper<Throwable> { public class CatalogGenericExceptionMapper implements ExceptionMapper<Throwable> {
@Override @Override
public Response toResponse(Throwable ex) { public Response toResponse(Throwable ex) {
ex.printStackTrace();
LOG.debug(ex.getMessage()); LOG.debug(ex.getMessage());
if (ex instanceof ProcessingException if (ex instanceof ProcessingException
|| ex instanceof IllegalArgumentException || ex instanceof IllegalArgumentException

View File

@ -88,10 +88,10 @@ public class UsageRepository {
} }
@Transaction @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 // Validate data entity for which usage is being collected
Entity.getEntityReferenceById(entityType, UUID.fromString(id), Include.NON_DELETED); Entity.getEntityReferenceById(entityType, id, Include.NON_DELETED);
return addUsage(PUT, entityType, id, usage); return addUsage(PUT, entityType, id.toString(), usage);
} }
@Transaction @Transaction

View File

@ -494,7 +494,6 @@ public class SearchResource {
@Operation( @Operation(
operationId = "getAllReindexBatchJobs", operationId = "getAllReindexBatchJobs",
summary = "Get all reindex batch jobs", summary = "Get all reindex batch jobs",
tags = "search",
description = "Get all reindex batch jobs", description = "Get all reindex batch jobs",
responses = { responses = {
@ApiResponse(responseCode = "200", description = "Success"), @ApiResponse(responseCode = "200", description = "Success"),

View File

@ -22,6 +22,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
import java.io.IOException; import java.io.IOException;
import java.util.Date; import java.util.Date;
import java.util.Objects; import java.util.Objects;
import java.util.UUID;
import javax.validation.Valid; import javax.validation.Valid;
import javax.ws.rs.Consumes; import javax.ws.rs.Consumes;
import javax.ws.rs.GET; 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.Context;
import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import javax.ws.rs.core.SecurityContext;
import javax.ws.rs.core.UriInfo; import javax.ws.rs.core.UriInfo;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.openmetadata.schema.type.DailyCount; import org.openmetadata.schema.type.DailyCount;
import org.openmetadata.schema.type.EntityUsage; import org.openmetadata.schema.type.EntityUsage;
import org.openmetadata.schema.type.MetadataOperation;
import org.openmetadata.service.Entity; import org.openmetadata.service.Entity;
import org.openmetadata.service.jdbi3.CollectionDAO; import org.openmetadata.service.jdbi3.CollectionDAO;
import org.openmetadata.service.jdbi3.UsageRepository; import org.openmetadata.service.jdbi3.UsageRepository;
import org.openmetadata.service.resources.Collection; import org.openmetadata.service.resources.Collection;
import org.openmetadata.service.resources.EntityResource;
import org.openmetadata.service.security.Authorizer; 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; import org.openmetadata.service.util.RestUtil;
@Slf4j @Slf4j
@ -53,9 +59,11 @@ import org.openmetadata.service.util.RestUtil;
@Collection(name = "usage") @Collection(name = "usage")
public class UsageResource { public class UsageResource {
private final UsageRepository dao; private final UsageRepository dao;
private final Authorizer authorizer;
public UsageResource(CollectionDAO dao, Authorizer authorizer) { public UsageResource(CollectionDAO dao, Authorizer authorizer) {
Objects.requireNonNull(dao, "UsageRepository must not be null"); Objects.requireNonNull(dao, "UsageRepository must not be null");
this.authorizer = authorizer;
this.dao = new UsageRepository(dao); this.dao = new UsageRepository(dao);
} }
@ -75,6 +83,7 @@ public class UsageResource {
}) })
public EntityUsage get( public EntityUsage get(
@Context UriInfo uriInfo, @Context UriInfo uriInfo,
@Context SecurityContext securityContext,
@Parameter( @Parameter(
description = "Entity type for which usage is requested", description = "Entity type for which usage is requested",
required = true, required = true,
@ -93,7 +102,10 @@ public class UsageResource {
@QueryParam("date") @QueryParam("date")
String date) String date)
throws IOException { 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); int actualDays = Math.min(Math.max(days, 1), 30);
String actualDate = date == null ? RestUtil.DATE_FORMAT.format(new Date()) : date; String actualDate = date == null ? RestUtil.DATE_FORMAT.format(new Date()) : date;
return addHref(uriInfo, dao.get(entity, id, actualDate, actualDays)); return addHref(uriInfo, dao.get(entity, id, actualDate, actualDays));
@ -115,6 +127,7 @@ public class UsageResource {
}) })
public EntityUsage getByName( public EntityUsage getByName(
@Context UriInfo uriInfo, @Context UriInfo uriInfo,
@Context SecurityContext securityContext,
@Parameter( @Parameter(
description = "Entity type for which usage is requested", description = "Entity type for which usage is requested",
required = true, required = true,
@ -135,8 +148,12 @@ public class UsageResource {
description = description =
"Usage for number of days going back from this date in ISO 8601 format " + "(default = currentDate)") "Usage for number of days going back from this date in ISO 8601 format " + "(default = currentDate)")
@QueryParam("date") @QueryParam("date")
String date) { String date)
// TODO add href 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); int actualDays = Math.min(Math.max(days, 1), 30);
String actualDate = date == null ? RestUtil.DATE_FORMAT.format(new Date()) : date; String actualDate = date == null ? RestUtil.DATE_FORMAT.format(new Date()) : date;
return addHref(uriInfo, dao.getByName(entity, fqn, actualDate, actualDays)); return addHref(uriInfo, dao.getByName(entity, fqn, actualDate, actualDays));
@ -159,6 +176,7 @@ public class UsageResource {
}) })
public Response create( public Response create(
@Context UriInfo uriInfo, @Context UriInfo uriInfo,
@Context SecurityContext securityContext,
@Parameter( @Parameter(
description = "Entity type for which usage is reported", description = "Entity type for which usage is reported",
required = true, required = true,
@ -169,6 +187,10 @@ public class UsageResource {
String id, String id,
@Parameter(description = "Usage information a given date") @Valid DailyCount usage) @Parameter(description = "Usage information a given date") @Valid DailyCount usage)
throws IOException { 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(); return dao.create(entity, id, usage).toResponse();
} }
@ -189,6 +211,7 @@ public class UsageResource {
}) })
public Response createOrUpdate( public Response createOrUpdate(
@Context UriInfo uriInfo, @Context UriInfo uriInfo,
@Context SecurityContext securityContext,
@Parameter( @Parameter(
description = "Entity type for which usage is reported", description = "Entity type for which usage is reported",
required = true, required = true,
@ -196,9 +219,13 @@ public class UsageResource {
@PathParam("entity") @PathParam("entity")
String entity, String entity,
@Parameter(description = "Entity id", required = true, schema = @Schema(type = "string")) @PathParam("id") @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) @Parameter(description = "Usage information a given date") @Valid DailyCount usage)
throws IOException { 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(); return dao.createOrUpdate(entity, id, usage).toResponse();
} }
@ -219,6 +246,7 @@ public class UsageResource {
}) })
public Response createByName( public Response createByName(
@Context UriInfo uriInfo, @Context UriInfo uriInfo,
@Context SecurityContext securityContext,
@Parameter( @Parameter(
description = "Entity type for which usage is reported", description = "Entity type for which usage is reported",
required = true, required = true,
@ -233,6 +261,10 @@ public class UsageResource {
String fullyQualifiedName, String fullyQualifiedName,
@Parameter(description = "Usage information a given date") @Valid DailyCount usage) @Parameter(description = "Usage information a given date") @Valid DailyCount usage)
throws IOException { 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(); return dao.createByName(entity, fullyQualifiedName, usage).toResponse();
} }
@ -253,6 +285,7 @@ public class UsageResource {
}) })
public Response createOrUpdateByName( public Response createOrUpdateByName(
@Context UriInfo uriInfo, @Context UriInfo uriInfo,
@Context SecurityContext securityContext,
@Parameter( @Parameter(
description = "Entity type for which usage is reported", description = "Entity type for which usage is reported",
required = true, required = true,
@ -267,6 +300,10 @@ public class UsageResource {
String fullyQualifiedName, String fullyQualifiedName,
@Parameter(description = "Usage information a given date") @Valid DailyCount usage) @Parameter(description = "Usage information a given date") @Valid DailyCount usage)
throws IOException { 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(); return dao.createOrUpdateByName(entity, fullyQualifiedName, usage).toResponse();
} }
@ -283,6 +320,7 @@ public class UsageResource {
}) })
public Response computePercentile( public Response computePercentile(
@Context UriInfo uriInfo, @Context UriInfo uriInfo,
@Context SecurityContext securityContext,
@Parameter( @Parameter(
description = "Entity name for which usage is requested", description = "Entity name for which usage is requested",
schema = @Schema(type = "string", example = "table, report, metrics, or dashboard")) 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", description = "ISO 8601 format date to compute percentile on",
schema = @Schema(type = "string", example = "2021-01-28")) schema = @Schema(type = "string", example = "2021-01-28"))
@PathParam("date") @PathParam("date")
String date) { String date)
// TODO delete this? 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); dao.computePercentile(entity, date);
return Response.status(Response.Status.CREATED).build(); return Response.status(Response.Status.CREATED).build();
} }

View File

@ -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 javax.ws.rs.core.Response.Status.NOT_FOUND;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.openmetadata.common.utils.CommonUtil.getDateStringByOffset; 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.Entity.TABLE;
import static org.openmetadata.service.exception.CatalogExceptionMessage.entityNotFound; import static org.openmetadata.service.exception.CatalogExceptionMessage.entityNotFound;
import static org.openmetadata.service.exception.CatalogExceptionMessage.entityTypeNotFound; 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.ADMIN_AUTH_HEADERS;
import static org.openmetadata.service.util.TestUtils.NON_EXISTENT_ENTITY; 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 static org.openmetadata.service.util.TestUtils.assertResponse;
import java.io.IOException; import java.io.IOException;
@ -34,6 +39,7 @@ import java.util.Random;
import java.util.UUID; import java.util.UUID;
import javax.ws.rs.client.WebTarget; import javax.ws.rs.client.WebTarget;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import lombok.SneakyThrows; import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.http.client.HttpResponseException; 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.entity.data.Table;
import org.openmetadata.schema.type.DailyCount; import org.openmetadata.schema.type.DailyCount;
import org.openmetadata.schema.type.EntityUsage; import org.openmetadata.schema.type.EntityUsage;
import org.openmetadata.schema.type.MetadataOperation;
import org.openmetadata.schema.type.UsageDetails; import org.openmetadata.schema.type.UsageDetails;
import org.openmetadata.service.Entity; import org.openmetadata.service.Entity;
import org.openmetadata.service.OpenMetadataApplicationTest; import org.openmetadata.service.OpenMetadataApplicationTest;
import org.openmetadata.service.exception.CatalogExceptionMessage;
import org.openmetadata.service.resources.databases.DatabaseResourceTest; import org.openmetadata.service.resources.databases.DatabaseResourceTest;
import org.openmetadata.service.resources.databases.TableResourceTest; import org.openmetadata.service.resources.databases.TableResourceTest;
import org.openmetadata.service.util.RestUtil; import org.openmetadata.service.util.RestUtil;
@ -148,25 +156,51 @@ class UsageResourceTest extends OpenMetadataApplicationTest {
} }
@Test @Test
void post_validUsageByName_200_OK(TestInfo test) { void post_validUsageByNameAsAdmin_200(TestInfo test) {
testValidUsageByName(test, POST); testValidUsageByName(test, POST, ADMIN_AUTH_HEADERS);
} }
@Test @Test
void put_validUsageByName_200_OK(TestInfo test) { void post_validUsageByNameAsIngestionBot_200(TestInfo test) {
testValidUsageByName(test, PUT); 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 @SneakyThrows
void testValidUsageByName(TestInfo test, String methodType) { void testValidUsageByName(TestInfo test, String methodType, Map<String, String> authHeaders) {
TableResourceTest tableResourceTest = new TableResourceTest(); TableResourceTest tableResourceTest = new TableResourceTest();
Table table = tableResourceTest.createEntity(tableResourceTest.createRequest(test), ADMIN_AUTH_HEADERS); Table table = tableResourceTest.createEntity(tableResourceTest.createRequest(test), ADMIN_AUTH_HEADERS);
DailyCount usageReport = usageReport().withCount(100).withDate(RestUtil.DATE_FORMAT.format(new Date())); 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 // a put request updates the data again
if (methodType.equals(PUT)) { if (methodType.equals(PUT)) {
reportUsageByNamePut(TABLE, table.getFullyQualifiedName(), usageReport, ADMIN_AUTH_HEADERS); reportUsageByNamePut(TABLE, table.getFullyQualifiedName(), usageReport, authHeaders);
checkUsageByName(usageReport.getDate(), TABLE, table.getFullyQualifiedName(), 200, 200, 200, ADMIN_AUTH_HEADERS); checkUsageByName(usageReport.getDate(), TABLE, table.getFullyQualifiedName(), 200, 200, 200, authHeaders);
} }
} }

View File

@ -38,6 +38,7 @@
"EditTeams", "EditTeams",
"EditTier", "EditTier",
"EditTests", "EditTests",
"EditUsage",
"EditUsers" "EditUsers"
] ]
} }