Add Role entity (#1970)

This commit is contained in:
Matt 2021-12-30 08:30:12 -08:00 committed by GitHub
parent ab4c9ede25
commit 485e661430
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 906 additions and 1 deletions

View File

@ -0,0 +1,15 @@
CREATE TABLE IF NOT EXISTS role_entity (
id VARCHAR(36) GENERATED ALWAYS AS (json ->> '$.id') STORED NOT NULL,
name VARCHAR(256) GENERATED ALWAYS AS (json ->> '$.name') NOT NULL,
json JSON NOT NULL,
updatedAt TIMESTAMP GENERATED ALWAYS AS (TIMESTAMP(STR_TO_DATE(json ->> '$.updatedAt', '%Y-%m-%dT%T.%fZ'))) NOT NULL,
updatedBy VARCHAR(256) GENERATED ALWAYS AS (json ->> '$.updatedBy') NOT NULL,
PRIMARY KEY (id),
UNIQUE KEY unique_name(name),
INDEX (updatedBy),
INDEX (updatedAt)
);
ALTER TABLE role_entity
ADD COLUMN deleted BOOLEAN GENERATED ALWAYS AS (JSON_EXTRACT(json, '$.deleted')),
ADD INDEX (deleted);

View File

@ -68,8 +68,9 @@ public final class Entity {
public static final String POLICY = "policy";
//
// Team/user
// Role, team and user
//
public static final String ROLE = "role";
public static final String USER = "user";
public static final String TEAM = "team";

View File

@ -44,6 +44,7 @@ import org.openmetadata.catalog.entity.services.DatabaseService;
import org.openmetadata.catalog.entity.services.MessagingService;
import org.openmetadata.catalog.entity.services.PipelineService;
import org.openmetadata.catalog.entity.services.StorageService;
import org.openmetadata.catalog.entity.teams.Role;
import org.openmetadata.catalog.entity.teams.Team;
import org.openmetadata.catalog.entity.teams.User;
import org.openmetadata.catalog.jdbi3.BotsRepository.BotsEntityInterface;
@ -62,6 +63,7 @@ import org.openmetadata.catalog.jdbi3.PipelineRepository.PipelineEntityInterface
import org.openmetadata.catalog.jdbi3.PipelineServiceRepository.PipelineServiceEntityInterface;
import org.openmetadata.catalog.jdbi3.PolicyRepository.PolicyEntityInterface;
import org.openmetadata.catalog.jdbi3.ReportRepository.ReportEntityInterface;
import org.openmetadata.catalog.jdbi3.RoleRepository.RoleEntityInterface;
import org.openmetadata.catalog.jdbi3.StorageServiceRepository.StorageServiceEntityInterface;
import org.openmetadata.catalog.jdbi3.TableRepository.TableEntityInterface;
import org.openmetadata.catalog.jdbi3.TeamRepository.TeamEntityInterface;
@ -89,6 +91,9 @@ public interface CollectionDAO {
@CreateSqlObject
EntityExtensionDAO entityExtensionDAO();
@CreateSqlObject
RoleDAO roleDAO();
@CreateSqlObject
UserDAO userDAO();
@ -894,6 +899,28 @@ public interface CollectionDAO {
}
}
interface RoleDAO extends EntityDAO<Role> {
@Override
default String getTableName() {
return "role_entity";
}
@Override
default Class<Role> getEntityClass() {
return Role.class;
}
@Override
default String getNameColumn() {
return "name";
}
@Override
default EntityReference getEntityReference(Role entity) {
return new RoleEntityInterface(entity).getEntityReference();
}
}
interface TeamDAO extends EntityDAO<Team> {
@Override
default String getTableName() {

View File

@ -0,0 +1,230 @@
/*
* Copyright 2021 Collate
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.openmetadata.catalog.jdbi3;
import java.io.IOException;
import java.net.URI;
import java.util.Date;
import java.util.List;
import java.util.UUID;
import org.openmetadata.catalog.Entity;
import org.openmetadata.catalog.entity.teams.Role;
import org.openmetadata.catalog.exception.CatalogExceptionMessage;
import org.openmetadata.catalog.resources.teams.RoleResource;
import org.openmetadata.catalog.type.ChangeDescription;
import org.openmetadata.catalog.type.EntityReference;
import org.openmetadata.catalog.type.TagLabel;
import org.openmetadata.catalog.util.EntityInterface;
import org.openmetadata.catalog.util.EntityUtil.Fields;
import org.openmetadata.catalog.util.JsonUtils;
public class RoleRepository extends EntityRepository<Role> {
static final Fields ROLE_UPDATE_FIELDS = new Fields(RoleResource.FIELD_LIST, null);
static final Fields ROLE_PATCH_FIELDS = new Fields(RoleResource.FIELD_LIST, null);
private final CollectionDAO dao;
public RoleRepository(CollectionDAO dao) {
super(
RoleResource.COLLECTION_PATH,
Entity.ROLE,
Role.class,
dao.roleDAO(),
dao,
ROLE_PATCH_FIELDS,
ROLE_UPDATE_FIELDS);
this.dao = dao;
}
@Override
public Role setFields(Role role, Fields fields) throws IOException {
// Nothing to set.
return role;
}
@Override
public void restorePatchAttributes(Role original, Role updated) {
// Patch can't make changes to following fields. Ignore the changes
updated.withName(original.getName()).withId(original.getId());
}
@Override
public EntityInterface<Role> getEntityInterface(Role entity) {
return new RoleEntityInterface(entity);
}
@Override
public void prepare(Role role) throws IOException {}
@Override
public void storeEntity(Role role, boolean update) throws IOException {
// Don't store href as JSON. Build it on the fly based on relationships
role.withHref(null);
if (update) {
dao.roleDAO().update(role.getId(), JsonUtils.pojoToJson(role));
} else {
dao.roleDAO().insert(role);
}
}
@Override
public void storeRelationships(Role role) {}
@Override
public EntityUpdater getUpdater(Role original, Role updated, boolean patchOperation) {
return new RoleUpdater(original, updated, patchOperation);
}
public static class RoleEntityInterface implements EntityInterface<Role> {
private final Role entity;
public RoleEntityInterface(Role entity) {
this.entity = entity;
}
@Override
public UUID getId() {
return entity.getId();
}
@Override
public String getDescription() {
return entity.getDescription();
}
@Override
public String getDisplayName() {
return entity.getDisplayName();
}
@Override
public EntityReference getOwner() {
return null;
}
@Override
public String getFullyQualifiedName() {
return entity.getName();
}
@Override
public List<TagLabel> getTags() {
return null;
}
@Override
public Double getVersion() {
return entity.getVersion();
}
@Override
public String getUpdatedBy() {
return entity.getUpdatedBy();
}
@Override
public Date getUpdatedAt() {
return entity.getUpdatedAt();
}
@Override
public URI getHref() {
return entity.getHref();
}
@Override
public List<EntityReference> getFollowers() {
throw new UnsupportedOperationException("Role does not support followers");
}
@Override
public EntityReference getEntityReference() {
return new EntityReference()
.withId(getId())
.withName(getFullyQualifiedName())
.withDescription(getDescription())
.withDisplayName(getDisplayName())
.withType(Entity.ROLE)
.withHref(getHref());
}
@Override
public Role getEntity() {
return entity;
}
@Override
public void setId(UUID id) {
entity.setId(id);
}
@Override
public void setDescription(String description) {
entity.setDescription(description);
}
@Override
public void setDisplayName(String displayName) {
entity.setDisplayName(displayName);
}
@Override
public void setUpdateDetails(String updatedBy, Date updatedAt) {
entity.setUpdatedBy(updatedBy);
entity.setUpdatedAt(updatedAt);
}
@Override
public void setChangeDescription(Double newVersion, ChangeDescription changeDescription) {
entity.setVersion(newVersion);
entity.setChangeDescription(changeDescription);
}
@Override
public void setOwner(EntityReference owner) {}
@Override
public void setDeleted(boolean flag) {
entity.setDeleted(flag);
}
@Override
public Role withHref(URI href) {
return entity.withHref(href);
}
@Override
public ChangeDescription getChangeDescription() {
return entity.getChangeDescription();
}
@Override
public void setTags(List<TagLabel> tags) {}
}
/** Handles entity updated from PUT and POST operation. */
public class RoleUpdater extends EntityUpdater {
public RoleUpdater(Role original, Role updated, boolean patchOperation) {
super(original, updated, patchOperation);
}
@Override
public void entitySpecificUpdate() throws IOException {
// Update operation cannot undelete a role.
if (updated.getEntity().getDeleted() != original.getEntity().getDeleted()) {
throw new IllegalArgumentException(CatalogExceptionMessage.readOnlyAttribute("Role", "deleted"));
}
}
}
}

View File

@ -0,0 +1,326 @@
/*
* Copyright 2021 Collate
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.openmetadata.catalog.resources.teams;
import com.google.inject.Inject;
import io.dropwizard.jersey.PATCH;
import io.swagger.annotations.Api;
import io.swagger.v3.oas.annotations.ExternalDocumentation;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.ExampleObject;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.security.GeneralSecurityException;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
import javax.json.JsonPatch;
import javax.validation.Valid;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
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 org.openmetadata.catalog.api.teams.CreateRole;
import org.openmetadata.catalog.entity.teams.Role;
import org.openmetadata.catalog.jdbi3.CollectionDAO;
import org.openmetadata.catalog.jdbi3.RoleRepository;
import org.openmetadata.catalog.resources.Collection;
import org.openmetadata.catalog.security.Authorizer;
import org.openmetadata.catalog.security.SecurityUtil;
import org.openmetadata.catalog.type.EntityHistory;
import org.openmetadata.catalog.util.EntityUtil;
import org.openmetadata.catalog.util.RestUtil;
import org.openmetadata.catalog.util.RestUtil.PatchResponse;
import org.openmetadata.catalog.util.ResultList;
@Path("/v1/roles")
@Api(value = "Roles collection", tags = "Roles collection")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@Collection(name = "roles")
public class RoleResource {
public static final String COLLECTION_PATH = "/v1/roles/";
private final RoleRepository dao;
private final Authorizer authorizer;
@Inject
public RoleResource(CollectionDAO dao, Authorizer authorizer) {
Objects.requireNonNull(dao, "RoleRepository must not be null");
this.dao = new RoleRepository(dao);
this.authorizer = authorizer;
}
public static class RoleList extends ResultList<Role> {
@SuppressWarnings("unused") /* Required for tests */
RoleList() {}
public RoleList(List<Role> roles, String beforeCursor, String afterCursor, int total)
throws GeneralSecurityException, UnsupportedEncodingException {
super(roles, beforeCursor, afterCursor, total);
}
}
// No additional fields supported for role entity at the moment.
public static final List<String> FIELD_LIST = new ArrayList<>();
@GET
@Valid
@Operation(
summary = "List roles",
tags = "roles",
description =
"Get a list of roles. Use cursor-based pagination to limit the number of entries in the list using `limit`"
+ " and `before` or `after` query params.",
responses = {
@ApiResponse(
responseCode = "200",
description = "List of roles",
content = @Content(mediaType = "application/json", schema = @Schema(implementation = RoleList.class)))
})
public ResultList<Role> list(
@Context UriInfo uriInfo,
@Context SecurityContext securityContext,
@Parameter(description = "Limit the number tables returned. (1 to 1000000, default = 10)")
@DefaultValue("10")
@Min(1)
@Max(1000000)
@QueryParam("limit")
int limitParam,
@Parameter(description = "Returns list of tables before this cursor", schema = @Schema(type = "string"))
@QueryParam("before")
String before,
@Parameter(description = "Returns list of tables after this cursor", schema = @Schema(type = "string"))
@QueryParam("after")
String after)
throws IOException, GeneralSecurityException, ParseException {
RestUtil.validateCursors(before, after);
EntityUtil.Fields fields = new EntityUtil.Fields(FIELD_LIST, null);
ResultList<Role> roles;
if (before != null) { // Reverse paging
roles = dao.listBefore(uriInfo, fields, null, limitParam, before); // Ask for one extra entry
} else { // Forward paging or first page
roles = dao.listAfter(uriInfo, fields, null, limitParam, after);
}
return roles;
}
@GET
@Path("/{id}/versions")
@Operation(
summary = "List role versions",
tags = "roles",
description = "Get a list of all the versions of a role identified by `id`",
responses = {
@ApiResponse(
responseCode = "200",
description = "List of role versions",
content = @Content(mediaType = "application/json", schema = @Schema(implementation = EntityHistory.class)))
})
public EntityHistory listVersions(
@Context UriInfo uriInfo,
@Context SecurityContext securityContext,
@Parameter(description = "role Id", schema = @Schema(type = "string")) @PathParam("id") String id)
throws IOException, ParseException {
return dao.listVersions(id);
}
@GET
@Valid
@Path("/{id}")
@Operation(
summary = "Get a role",
tags = "roles",
description = "Get a role by `id`.",
responses = {
@ApiResponse(
responseCode = "200",
description = "The role",
content = @Content(mediaType = "application/json", schema = @Schema(implementation = Role.class))),
@ApiResponse(responseCode = "404", description = "Role for instance {id} is not found")
})
public Role get(@Context UriInfo uriInfo, @Context SecurityContext securityContext, @PathParam("id") String id)
throws IOException, ParseException {
EntityUtil.Fields fields = new EntityUtil.Fields(FIELD_LIST, null);
return dao.get(uriInfo, id, fields);
}
@GET
@Valid
@Path("/name/{name}")
@Operation(
summary = "Get a role by name",
tags = "roles",
description = "Get a role by `name`.",
responses = {
@ApiResponse(
responseCode = "200",
description = "The role",
content = @Content(mediaType = "application/json", schema = @Schema(implementation = Role.class))),
@ApiResponse(responseCode = "404", description = "Role for instance {name} is not found")
})
public Role getByName(
@Context UriInfo uriInfo, @Context SecurityContext securityContext, @PathParam("name") String name)
throws IOException, ParseException {
EntityUtil.Fields fields = new EntityUtil.Fields(FIELD_LIST, null);
return dao.getByName(uriInfo, name, fields);
}
@GET
@Path("/{id}/versions/{version}")
@Operation(
summary = "Get a version of the role",
tags = "roles",
description = "Get a version of the role by given `id`",
responses = {
@ApiResponse(
responseCode = "200",
description = "role",
content = @Content(mediaType = "application/json", schema = @Schema(implementation = Role.class))),
@ApiResponse(
responseCode = "404",
description = "Role for instance {id} and version {version} is " + "not found")
})
public Role getVersion(
@Context UriInfo uriInfo,
@Context SecurityContext securityContext,
@Parameter(description = "Role Id", schema = @Schema(type = "string")) @PathParam("id") String id,
@Parameter(
description = "Role version number in the form `major`.`minor`",
schema = @Schema(type = "string", example = "0.1 or 1.1"))
@PathParam("version")
String version)
throws IOException, ParseException {
return dao.getVersion(id, version);
}
@POST
@Operation(
summary = "Create a role",
tags = "roles",
description = "Create a new role.",
responses = {
@ApiResponse(
responseCode = "200",
description = "The role",
content = @Content(mediaType = "application/json", schema = @Schema(implementation = CreateRole.class))),
@ApiResponse(responseCode = "400", description = "Bad request")
})
public Response create(
@Context UriInfo uriInfo, @Context SecurityContext securityContext, @Valid CreateRole createRole)
throws IOException {
SecurityUtil.checkAdminOrBotRole(authorizer, securityContext);
Role role = getRole(createRole, securityContext);
role = dao.create(uriInfo, role);
return Response.created(role.getHref()).entity(role).build();
}
@PUT
@Operation(
summary = "Create or Update a role",
tags = "roles",
description = "Create or Update a role.",
responses = {
@ApiResponse(
responseCode = "200",
description = "The role ",
content = @Content(mediaType = "application/json", schema = @Schema(implementation = CreateRole.class))),
@ApiResponse(responseCode = "400", description = "Bad request")
})
public Response createOrUpdateRole(
@Context UriInfo uriInfo, @Context SecurityContext securityContext, @Valid CreateRole createRole)
throws IOException, ParseException {
SecurityUtil.checkAdminOrBotRole(authorizer, securityContext);
Role role = getRole(createRole, securityContext);
RestUtil.PutResponse<Role> response = dao.createOrUpdate(uriInfo, role);
return response.toResponse();
}
@PATCH
@Path("/{id}")
@Consumes(MediaType.APPLICATION_JSON_PATCH_JSON)
@Operation(
summary = "Update a role",
tags = "roles",
description = "Update an existing role with JsonPatch.",
externalDocs = @ExternalDocumentation(description = "JsonPatch RFC", url = "https://tools.ietf.org/html/rfc6902"))
public Response patch(
@Context UriInfo uriInfo,
@Context SecurityContext securityContext,
@PathParam("id") String id,
@RequestBody(
description = "JsonPatch with array of operations",
content =
@Content(
mediaType = MediaType.APPLICATION_JSON_PATCH_JSON,
examples = {
@ExampleObject("[" + "{op:remove, path:/a}," + "{op:add, path: /b, value: val}" + "]")
}))
JsonPatch patch)
throws IOException, ParseException {
SecurityUtil.checkAdminOrBotRole(authorizer, securityContext);
PatchResponse<Role> response =
dao.patch(uriInfo, UUID.fromString(id), securityContext.getUserPrincipal().getName(), patch);
return response.toResponse();
}
@DELETE
@Path("/{id}")
@Operation(
summary = "Delete a role",
tags = "roles",
description = "Delete a role by given `id`.",
responses = {
@ApiResponse(responseCode = "200", description = "OK"),
@ApiResponse(responseCode = "404", description = "Role for instance {id} is not found")
})
public Response delete(@Context UriInfo uriInfo, @Context SecurityContext securityContext, @PathParam("id") String id)
throws IOException {
SecurityUtil.checkAdminOrBotRole(authorizer, securityContext);
dao.delete(UUID.fromString(id));
return Response.ok().build();
}
private Role getRole(CreateRole ct, SecurityContext securityContext) {
return new Role()
.withId(UUID.randomUUID())
.withName(ct.getName())
.withDescription(ct.getDescription())
.withDisplayName(ct.getDisplayName())
.withUpdatedBy(securityContext.getUserPrincipal().getName())
.withUpdatedAt(new Date());
}
}

View File

@ -0,0 +1,21 @@
{
"$id": "https://github.com/open-metadata/OpenMetadata/blob/main/catalog-rest-service/src/main/resources/json/schema/api/teams/createRole.json",
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Create role entity request",
"description": "Role entity",
"type": "object",
"properties" : {
"name": {
"$ref": "../../entity/teams/role.json#/definitions/roleName"
},
"displayName": {
"description": "Optional name used for display purposes. Example 'Data Consumer'",
"type": "string"
},
"description": {
"description": "Optional description of the role",
"type": "string"
}
},
"required": ["name"]
}

View File

@ -0,0 +1,61 @@
{
"$id": "https://open-metadata.org/schema/entity/teams/role.json",
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Role",
"description": "This schema defines the Role entity. A Role has access to zero or more data assets",
"type": "object",
"definitions" : {
"roleName" : {
"description": "A unique name of the role.",
"type": "string",
"minLength": 1,
"maxLength": 128
}
},
"properties" : {
"id": {
"$ref": "../../type/basic.json#/definitions/uuid"
},
"name": {
"$ref": "#/definitions/roleName"
},
"displayName": {
"description": "Name used for display purposes. Example 'Data Consumer'.",
"type": "string"
},
"description": {
"description": "Description of the role.",
"type": "string"
},
"version" : {
"description": "Metadata version of the entity.",
"$ref": "../../type/entityHistory.json#/definitions/entityVersion"
},
"updatedAt" : {
"description": "Last update time corresponding to the new version of the entity.",
"$ref": "../../type/basic.json#/definitions/dateTime"
},
"updatedBy" : {
"description": "User who made the update.",
"type": "string"
},
"href": {
"description": "Link to the resource corresponding to this entity.",
"$ref": "../../type/basic.json#/definitions/href"
},
"changeDescription": {
"description" : "Change that lead to this version of the entity.",
"$ref": "../../type/entityHistory.json#/definitions/changeDescription"
},
"deleted" : {
"description": "When `true` indicates the entity has been soft deleted.",
"type": "boolean",
"default" : false
}
},
"required" : [
"id",
"name"
],
"additionalProperties": false
}

View File

@ -0,0 +1,224 @@
/*
* Copyright 2021 Collate
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.openmetadata.catalog.resources.teams;
import static javax.ws.rs.core.Response.Status.BAD_REQUEST;
import static javax.ws.rs.core.Response.Status.FORBIDDEN;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.openmetadata.catalog.security.SecurityUtil.authHeaders;
import static org.openmetadata.catalog.util.TestUtils.adminAuthHeaders;
import static org.openmetadata.catalog.util.TestUtils.assertListNotNull;
import static org.openmetadata.catalog.util.TestUtils.assertResponse;
import com.fasterxml.jackson.core.JsonProcessingException;
import java.io.IOException;
import java.util.Map;
import java.util.UUID;
import javax.json.JsonPatch;
import javax.ws.rs.client.WebTarget;
import org.apache.http.client.HttpResponseException;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInfo;
import org.openmetadata.catalog.CatalogApplicationTest;
import org.openmetadata.catalog.Entity;
import org.openmetadata.catalog.api.teams.CreateRole;
import org.openmetadata.catalog.entity.teams.Role;
import org.openmetadata.catalog.exception.CatalogExceptionMessage;
import org.openmetadata.catalog.jdbi3.RoleRepository.RoleEntityInterface;
import org.openmetadata.catalog.resources.EntityResourceTest;
import org.openmetadata.catalog.resources.teams.RoleResource.RoleList;
import org.openmetadata.catalog.type.EntityReference;
import org.openmetadata.catalog.util.EntityInterface;
import org.openmetadata.catalog.util.JsonUtils;
import org.openmetadata.catalog.util.TestUtils;
import org.openmetadata.common.utils.JsonSchemaUtil;
public class RoleResourceTest extends EntityResourceTest<Role> {
public RoleResourceTest() {
super(Entity.ROLE, Role.class, RoleList.class, "roles", null, false, false, false);
}
@Test
public void post_validRoles_as_admin_200_OK(TestInfo test) throws IOException {
// Create role with different optional fields
CreateRole create = create(test, 1);
createAndCheckEntity(create, adminAuthHeaders());
create = create(test, 2).withDisplayName("displayName");
createAndCheckEntity(create, adminAuthHeaders());
create = create(test, 3).withDescription("description");
createAndCheckEntity(create, adminAuthHeaders());
create = create(test, 4).withDisplayName("displayName").withDescription("description");
createAndCheckEntity(create, adminAuthHeaders());
}
@Test
public void post_validRoles_as_non_admin_401(TestInfo test) {
// Create role with different optional fields
Map<String, String> authHeaders = authHeaders("test@open-metadata.org");
CreateRole create = create(test, 1);
HttpResponseException exception =
assertThrows(HttpResponseException.class, () -> createAndCheckEntity(create, authHeaders));
assertResponse(exception, FORBIDDEN, "Principal: CatalogPrincipal{name='test'} is not admin");
}
/**
* @see EntityResourceTest#put_addDeleteFollower_200 for tests related getting role with entities owned by the role
*/
@Test
public void delete_validRole_200_OK(TestInfo test) throws IOException {
CreateRole create = create(test);
Role role = createAndCheckEntity(create, adminAuthHeaders());
deleteEntity(role.getId(), adminAuthHeaders());
}
@Test
public void delete_validRole_as_non_admin_401(TestInfo test) throws IOException {
CreateRole create = create(test);
Role role = createAndCheckEntity(create, adminAuthHeaders());
HttpResponseException exception =
assertThrows(
HttpResponseException.class, () -> deleteEntity(role.getId(), authHeaders("test@open-metadata.org")));
assertResponse(exception, FORBIDDEN, "Principal: CatalogPrincipal{name='test'} is not admin");
}
@Test
public void patch_roleDeletedDisallowed_400(TestInfo test) throws HttpResponseException, JsonProcessingException {
// Ensure role deleted attribute can't be changed using patch
Role role = createRole(create(test), adminAuthHeaders());
String roleJson = JsonUtils.pojoToJson(role);
role.setDeleted(true);
HttpResponseException exception =
assertThrows(HttpResponseException.class, () -> patchRole(roleJson, role, adminAuthHeaders()));
assertResponse(exception, BAD_REQUEST, CatalogExceptionMessage.readOnlyAttribute("Role", "deleted"));
}
@Test
public void patch_roleAttributes_as_non_admin_403(TestInfo test)
throws HttpResponseException, JsonProcessingException {
// Create table without any attributes
Role role = createRole(create(test), adminAuthHeaders());
// Patching as a non-admin should is disallowed
String originalJson = JsonUtils.pojoToJson(role);
role.setDisplayName("newDisplayName");
HttpResponseException exception =
assertThrows(
HttpResponseException.class,
() -> patchRole(role.getId(), originalJson, role, authHeaders("test@open-metadata.org")));
assertResponse(exception, FORBIDDEN, "Principal: CatalogPrincipal{name='test'} is not admin");
}
public static Role createRole(CreateRole create, Map<String, String> authHeaders) throws HttpResponseException {
return TestUtils.post(CatalogApplicationTest.getResource("roles"), create, Role.class, authHeaders);
}
public static Role getRole(UUID id, String fields, Map<String, String> authHeaders) throws HttpResponseException {
WebTarget target = CatalogApplicationTest.getResource("roles/" + id);
target = fields != null ? target.queryParam("fields", fields) : target;
return TestUtils.get(target, Role.class, authHeaders);
}
public static Role getRoleByName(String name, String fields, Map<String, String> authHeaders)
throws HttpResponseException {
WebTarget target = CatalogApplicationTest.getResource("roles/name/" + name);
target = fields != null ? target.queryParam("fields", fields) : target;
return TestUtils.get(target, Role.class, authHeaders);
}
private static void validateRole(
Role role, String expectedDescription, String expectedDisplayName, String expectedUpdatedBy) {
assertListNotNull(role.getId(), role.getHref());
assertEquals(expectedDescription, role.getDescription());
assertEquals(expectedUpdatedBy, role.getUpdatedBy());
assertEquals(expectedDisplayName, role.getDisplayName());
}
/** Validate returned fields GET .../roles/{id}?fields="..." or GET .../roles/name/{name}?fields="..." */
@Override
public void validateGetWithDifferentFields(Role expectedRole, boolean byName) throws HttpResponseException {
String updatedBy = TestUtils.getPrincipal(adminAuthHeaders());
// Role does not have any supported additional fields yet.
// .../roles
Role getRole =
byName
? getRoleByName(expectedRole.getName(), null, adminAuthHeaders())
: getRole(expectedRole.getId(), null, adminAuthHeaders());
validateRole(getRole, expectedRole.getDescription(), expectedRole.getDisplayName(), updatedBy);
}
private Role patchRole(UUID roleId, String originalJson, Role updated, Map<String, String> authHeaders)
throws JsonProcessingException, HttpResponseException {
String updatedJson = JsonUtils.pojoToJson(updated);
JsonPatch patch = JsonSchemaUtil.getJsonPatch(originalJson, updatedJson);
return TestUtils.patch(CatalogApplicationTest.getResource("roles/" + roleId), patch, Role.class, authHeaders);
}
private Role patchRole(String originalJson, Role updated, Map<String, String> authHeaders)
throws JsonProcessingException, HttpResponseException {
return patchRole(updated.getId(), originalJson, updated, authHeaders);
}
CreateRole create(TestInfo test, int index) {
return new CreateRole().withName(getEntityName(test) + index);
}
public CreateRole create(TestInfo test) {
return create(getEntityName(test));
}
public CreateRole create(String entityName) {
return new CreateRole().withName(entityName);
}
@Override
public Object createRequest(String name, String description, String displayName, EntityReference owner) {
return create(name).withDescription(description).withDisplayName(displayName);
}
@Override
public void validateCreatedEntity(Role role, Object request, Map<String, String> authHeaders) {
CreateRole createRequest = (CreateRole) request;
validateCommonEntityFields(
getEntityInterface(role), createRequest.getDescription(), TestUtils.getPrincipal(authHeaders), null);
assertEquals(createRequest.getDisplayName(), role.getDisplayName());
}
@Override
public void validateUpdatedEntity(Role updatedEntity, Object request, Map<String, String> authHeaders) {
validateCreatedEntity(updatedEntity, request, authHeaders);
}
@Override
public void compareEntities(Role expected, Role updated, Map<String, String> authHeaders) {
validateCommonEntityFields(
getEntityInterface(updated), expected.getDescription(), TestUtils.getPrincipal(authHeaders), null);
assertEquals(expected.getDisplayName(), updated.getDisplayName());
}
@Override
public EntityInterface<Role> getEntityInterface(Role entity) {
return new RoleEntityInterface(entity);
}
@Override
public void assertFieldChange(String fieldName, Object expected, Object actual) throws IOException {
assertCommonFieldChange(fieldName, expected, actual);
}
}