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> {
@Override
public Response toResponse(Throwable ex) {
ex.printStackTrace();
LOG.debug(ex.getMessage());
if (ex instanceof ProcessingException
|| ex instanceof IllegalArgumentException

View File

@ -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

View File

@ -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"),

View File

@ -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();
}

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 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<String, String> 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);
}
}

View File

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