From d9db3a632408228f5edc60415ac60c9f8653256e Mon Sep 17 00:00:00 2001 From: John Joyce Date: Mon, 27 Jun 2022 18:15:36 -0700 Subject: [PATCH 1/5] Support for deleting tags, domains --- .../datahub/graphql/GmsGraphQLEngine.java | 8 +- .../authorization/AuthorizationUtils.java | 54 +++++++++ .../domain/CreateDomainResolver.java | 34 ++---- .../domain/DeleteDomainResolver.java | 56 ++++++++++ .../resolvers/domain/ListDomainsResolver.java | 2 +- .../resolvers/tag/CreateTagResolver.java | 80 ++++++++++++++ .../resolvers/tag/DeleteTagResolver.java | 58 ++++++++++ .../tag/mappers/TagUpdateInputMapper.java | 1 - .../src/main/resources/entity.graphql | 45 +++++++- .../domain/CreateDomainResolverTest.java | 34 +++--- .../domain/DeleteDomainResolverTest.java | 56 ++++++++++ .../resolvers/tag/CreateTagResolverTest.java | 103 ++++++++++++++++++ .../resolvers/tag/DeleteTagResolverTest.java | 56 ++++++++++ .../war/src/main/resources/boot/policies.json | 6 +- .../authorization/PoliciesConfig.java | 30 ++++- 15 files changed, 576 insertions(+), 47 deletions(-) create mode 100644 datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/domain/DeleteDomainResolver.java create mode 100644 datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/tag/CreateTagResolver.java create mode 100644 datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/tag/DeleteTagResolver.java create mode 100644 datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/domain/DeleteDomainResolverTest.java create mode 100644 datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/tag/CreateTagResolverTest.java create mode 100644 datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/tag/DeleteTagResolverTest.java diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/GmsGraphQLEngine.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/GmsGraphQLEngine.java index 1996d3a577b50..4d47e7ce60ef6 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/GmsGraphQLEngine.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/GmsGraphQLEngine.java @@ -84,6 +84,7 @@ import com.linkedin.datahub.graphql.resolvers.dataset.DatasetHealthResolver; import com.linkedin.datahub.graphql.resolvers.deprecation.UpdateDeprecationResolver; import com.linkedin.datahub.graphql.resolvers.domain.CreateDomainResolver; +import com.linkedin.datahub.graphql.resolvers.domain.DeleteDomainResolver; import com.linkedin.datahub.graphql.resolvers.domain.DomainEntitiesResolver; import com.linkedin.datahub.graphql.resolvers.domain.ListDomainsResolver; import com.linkedin.datahub.graphql.resolvers.domain.SetDomainResolver; @@ -153,6 +154,8 @@ import com.linkedin.datahub.graphql.resolvers.search.SearchAcrossEntitiesResolver; import com.linkedin.datahub.graphql.resolvers.search.SearchAcrossLineageResolver; import com.linkedin.datahub.graphql.resolvers.search.SearchResolver; +import com.linkedin.datahub.graphql.resolvers.tag.CreateTagResolver; +import com.linkedin.datahub.graphql.resolvers.tag.DeleteTagResolver; import com.linkedin.datahub.graphql.resolvers.tag.SetTagColorResolver; import com.linkedin.datahub.graphql.resolvers.test.CreateTestResolver; import com.linkedin.datahub.graphql.resolvers.test.DeleteTestResolver; @@ -672,8 +675,10 @@ private String getUrnField(DataFetchingEnvironment env) { private void configureMutationResolvers(final RuntimeWiring.Builder builder) { builder.type("Mutation", typeWiring -> typeWiring .dataFetcher("updateDataset", new MutableTypeResolver<>(datasetType)) + .dataFetcher("createTag", new CreateTagResolver(entityService)) .dataFetcher("updateTag", new MutableTypeResolver<>(tagType)) .dataFetcher("setTagColor", new SetTagColorResolver(entityClient, entityService)) + .dataFetcher("deleteTag", new DeleteTagResolver(entityClient)) .dataFetcher("updateChart", new MutableTypeResolver<>(chartType)) .dataFetcher("updateDashboard", new MutableTypeResolver<>(dashboardType)) .dataFetcher("updateNotebook", new MutableTypeResolver<>(notebookType)) @@ -702,7 +707,8 @@ private void configureMutationResolvers(final RuntimeWiring.Builder builder) { .dataFetcher("removeUser", new RemoveUserResolver(this.entityClient)) .dataFetcher("removeGroup", new RemoveGroupResolver(this.entityClient)) .dataFetcher("updateUserStatus", new UpdateUserStatusResolver(this.entityClient)) - .dataFetcher("createDomain", new CreateDomainResolver(this.entityClient)) + .dataFetcher("createDomain", new CreateDomainResolver(this.entityService)) + .dataFetcher("deleteDomain", new DeleteDomainResolver(entityClient)) .dataFetcher("setDomain", new SetDomainResolver(this.entityClient, this.entityService)) .dataFetcher("updateDeprecation", new UpdateDeprecationResolver(this.entityClient, this.entityService)) .dataFetcher("unsetDomain", new UnsetDomainResolver(this.entityClient, this.entityService)) diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/authorization/AuthorizationUtils.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/authorization/AuthorizationUtils.java index 0ff396374e1ed..d7a5940f9840c 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/authorization/AuthorizationUtils.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/authorization/AuthorizationUtils.java @@ -5,14 +5,24 @@ import com.datahub.authorization.Authorizer; import com.datahub.authorization.ResourceSpec; import com.google.common.collect.ImmutableList; +import com.linkedin.common.AuditStamp; +import com.linkedin.common.urn.Urn; +import com.linkedin.common.urn.UrnUtils; import com.linkedin.datahub.graphql.QueryContext; import com.linkedin.metadata.authorization.PoliciesConfig; +import java.time.Clock; import java.util.Optional; import javax.annotation.Nonnull; public class AuthorizationUtils { + private static final Clock CLOCK = Clock.systemUTC(); + + public static AuditStamp createAuditStamp(@Nonnull QueryContext context) { + return new AuditStamp().setTime(CLOCK.millis()).setActor(UrnUtils.getUrn(context.getActorUrn())); + } + public static boolean canManageUsersAndGroups(@Nonnull QueryContext context) { return isAuthorized(context, Optional.empty(), PoliciesConfig.MANAGE_USERS_AND_GROUPS_PRIVILEGE); } @@ -25,6 +35,24 @@ public static boolean canManageTokens(@Nonnull QueryContext context) { return isAuthorized(context, Optional.empty(), PoliciesConfig.MANAGE_ACCESS_TOKENS); } + /** + * Returns true if the current used is able to create Domains. This is true if the user has the 'Manage Domains' or 'Create Domains' platform privilege. + */ + public static boolean canCreateDomains(@Nonnull QueryContext context) { + final DisjunctivePrivilegeGroup orPrivilegeGroups = new DisjunctivePrivilegeGroup( + ImmutableList.of( + new ConjunctivePrivilegeGroup(ImmutableList.of( + PoliciesConfig.CREATE_DOMAINS_PRIVILEGE.getType())), + new ConjunctivePrivilegeGroup(ImmutableList.of( + PoliciesConfig.MANAGE_DOMAINS_PRIVILEGE.getType())) + )); + + return AuthorizationUtils.isAuthorized( + context.getAuthorizer(), + context.getActorUrn(), + orPrivilegeGroups); + } + public static boolean canManageDomains(@Nonnull QueryContext context) { return isAuthorized(context, Optional.empty(), PoliciesConfig.MANAGE_DOMAINS_PRIVILEGE); } @@ -33,6 +61,32 @@ public static boolean canManageGlossaries(@Nonnull QueryContext context) { return isAuthorized(context, Optional.empty(), PoliciesConfig.MANAGE_GLOSSARIES_PRIVILEGE); } + /** + * Returns true if the current used is able to create Tags. This is true if the user has the 'Manage Tags' or 'Create Tags' platform privilege. + */ + public static boolean canCreateTags(@Nonnull QueryContext context) { + final DisjunctivePrivilegeGroup orPrivilegeGroups = new DisjunctivePrivilegeGroup( + ImmutableList.of( + new ConjunctivePrivilegeGroup(ImmutableList.of( + PoliciesConfig.CREATE_TAGS_PRIVILEGE.getType())), + new ConjunctivePrivilegeGroup(ImmutableList.of( + PoliciesConfig.MANAGE_TAGS_PRIVILEGE.getType())) + )); + + return AuthorizationUtils.isAuthorized( + context.getAuthorizer(), + context.getActorUrn(), + orPrivilegeGroups); + } + + public static boolean canManageTags(@Nonnull QueryContext context) { + return isAuthorized(context, Optional.empty(), PoliciesConfig.MANAGE_TAGS_PRIVILEGE); + } + + public static boolean canDeleteEntity(@Nonnull Urn entityUrn, @Nonnull QueryContext context) { + return isAuthorized(context, Optional.of(new ResourceSpec(entityUrn.getEntityType(), entityUrn.toString())), PoliciesConfig.DELETE_ENTITY_PRIVILEGE); + } + public static boolean canManageUserCredentials(@Nonnull QueryContext context) { return isAuthorized(context, Optional.empty(), PoliciesConfig.MANAGE_USER_CREDENTIALS_PRIVILEGE); } diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/domain/CreateDomainResolver.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/domain/CreateDomainResolver.java index 3883a4b6f5769..4a435a8bfc9ef 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/domain/CreateDomainResolver.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/domain/CreateDomainResolver.java @@ -1,19 +1,16 @@ package com.linkedin.datahub.graphql.resolvers.domain; -import com.google.common.collect.ImmutableList; import com.linkedin.data.template.SetMode; import com.linkedin.datahub.graphql.QueryContext; import com.linkedin.datahub.graphql.authorization.AuthorizationUtils; -import com.linkedin.datahub.graphql.authorization.ConjunctivePrivilegeGroup; -import com.linkedin.datahub.graphql.authorization.DisjunctivePrivilegeGroup; import com.linkedin.datahub.graphql.exception.AuthorizationException; import com.linkedin.datahub.graphql.generated.CreateDomainInput; import com.linkedin.domain.DomainProperties; -import com.linkedin.entity.client.EntityClient; import com.linkedin.events.metadata.ChangeType; import com.linkedin.metadata.Constants; -import com.linkedin.metadata.authorization.PoliciesConfig; +import com.linkedin.metadata.entity.EntityService; import com.linkedin.metadata.key.DomainKey; +import com.linkedin.metadata.utils.EntityKeyUtils; import com.linkedin.metadata.utils.GenericRecordUtils; import com.linkedin.mxe.MetadataChangeProposal; import graphql.schema.DataFetcher; @@ -23,16 +20,17 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import static com.linkedin.datahub.graphql.authorization.AuthorizationUtils.*; import static com.linkedin.datahub.graphql.resolvers.ResolverUtils.*; /** - * Resolver used for creating a new Domain on DataHub. Requires the MANAGE_DOMAINS privilege. + * Resolver used for creating a new Domain on DataHub. Requires the CREATE_DOMAINS or MANAGE_DOMAINS privilege. */ @Slf4j @RequiredArgsConstructor public class CreateDomainResolver implements DataFetcher> { - private final EntityClient _entityClient; + private final EntityService _entityService; @Override public CompletableFuture get(DataFetchingEnvironment environment) throws Exception { @@ -42,12 +40,10 @@ public CompletableFuture get(DataFetchingEnvironment environment) throws return CompletableFuture.supplyAsync(() -> { - if (!isAuthorizedToCreateDomain(context)) { + if (!AuthorizationUtils.canCreateDomains(context)) { throw new AuthorizationException("Unauthorized to perform this action. Please contact your DataHub administrator."); } - // TODO: Add exists check. Currently this can override previously created domains. - try { // Create the Domain Key final DomainKey key = new DomainKey(); @@ -56,6 +52,10 @@ public CompletableFuture get(DataFetchingEnvironment environment) throws final String id = input.getId() != null ? input.getId() : UUID.randomUUID().toString(); key.setId(id); + if (_entityService.exists(EntityKeyUtils.convertEntityKeyToUrn(key, Constants.DOMAIN_ENTITY_NAME))) { + throw new IllegalArgumentException("This Domain already exists!"); + } + // Create the MCP final MetadataChangeProposal proposal = new MetadataChangeProposal(); proposal.setEntityKeyAspect(GenericRecordUtils.serializeAspect(key)); @@ -63,7 +63,8 @@ public CompletableFuture get(DataFetchingEnvironment environment) throws proposal.setAspectName(Constants.DOMAIN_PROPERTIES_ASPECT_NAME); proposal.setAspect(GenericRecordUtils.serializeAspect(mapDomainProperties(input))); proposal.setChangeType(ChangeType.UPSERT); - return _entityClient.ingestProposal(proposal, context.getAuthentication()); + + return _entityService.ingestProposal(proposal, createAuditStamp(context)).getUrn().toString(); } catch (Exception e) { log.error("Failed to create Domain with id: {}, name: {}: {}", input.getId(), input.getName(), e.getMessage()); throw new RuntimeException(String.format("Failed to create Domain with id: %s, name: %s", input.getId(), input.getName()), e); @@ -77,15 +78,4 @@ private DomainProperties mapDomainProperties(final CreateDomainInput input) { result.setDescription(input.getDescription(), SetMode.IGNORE_NULL); return result; } - - private boolean isAuthorizedToCreateDomain(final QueryContext context) { - final DisjunctivePrivilegeGroup orPrivilegeGroups = new DisjunctivePrivilegeGroup(ImmutableList.of( - new ConjunctivePrivilegeGroup(ImmutableList.of(PoliciesConfig.MANAGE_DOMAINS_PRIVILEGE.getType())) - )); - - return AuthorizationUtils.isAuthorized( - context.getAuthorizer(), - context.getActorUrn(), - orPrivilegeGroups); - } } \ No newline at end of file diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/domain/DeleteDomainResolver.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/domain/DeleteDomainResolver.java new file mode 100644 index 0000000000000..10d35a4e0e53e --- /dev/null +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/domain/DeleteDomainResolver.java @@ -0,0 +1,56 @@ +package com.linkedin.datahub.graphql.resolvers.domain; + +import com.linkedin.common.urn.Urn; +import com.linkedin.common.urn.UrnUtils; +import com.linkedin.datahub.graphql.QueryContext; +import com.linkedin.datahub.graphql.authorization.AuthorizationUtils; +import com.linkedin.datahub.graphql.exception.AuthorizationException; +import com.linkedin.entity.client.EntityClient; +import com.linkedin.r2.RemoteInvocationException; +import graphql.schema.DataFetcher; +import graphql.schema.DataFetchingEnvironment; +import java.util.concurrent.CompletableFuture; +import lombok.extern.slf4j.Slf4j; + + +/** + * Resolver responsible for hard deleting a particular DataHub Corp Group + */ +@Slf4j +public class DeleteDomainResolver implements DataFetcher> { + + private final EntityClient _entityClient; + + public DeleteDomainResolver(final EntityClient entityClient) { + _entityClient = entityClient; + } + + @Override + public CompletableFuture get(final DataFetchingEnvironment environment) throws Exception { + final QueryContext context = environment.getContext(); + final String domainUrn = environment.getArgument("urn"); + final Urn urn = Urn.createFromString(domainUrn); + return CompletableFuture.supplyAsync(() -> { + + if (AuthorizationUtils.canManageDomains(context) || AuthorizationUtils.canDeleteEntity(UrnUtils.getUrn(domainUrn), context)) { + try { + _entityClient.deleteEntity(urn, context.getAuthentication()); + + // Asynchronously Delete all references to the entity (to return quickly) + CompletableFuture.runAsync(() -> { + try { + _entityClient.deleteEntityReferences(urn, context.getAuthentication()); + } catch (RemoteInvocationException e) { + log.error(String.format("Caught exception while attempting to clear all entity references for Domain with urn %s", urn), e); + } + }); + + return true; + } catch (Exception e) { + throw new RuntimeException(String.format("Failed to perform delete against domain with urn %s", domainUrn), e); + } + } + throw new AuthorizationException("Unauthorized to perform this action. Please contact your DataHub administrator."); + }); + } +} diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/domain/ListDomainsResolver.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/domain/ListDomainsResolver.java index 076387449816a..0cf6d70e6909e 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/domain/ListDomainsResolver.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/domain/ListDomainsResolver.java @@ -45,7 +45,7 @@ public CompletableFuture get(final DataFetchingEnvironment en return CompletableFuture.supplyAsync(() -> { - if (AuthorizationUtils.canManageDomains(context)) { + if (AuthorizationUtils.canCreateDomains(context)) { final ListDomainsInput input = bindArgument(environment.getArgument("input"), ListDomainsInput.class); final Integer start = input.getStart() == null ? DEFAULT_START : input.getStart(); final Integer count = input.getCount() == null ? DEFAULT_COUNT : input.getCount(); diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/tag/CreateTagResolver.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/tag/CreateTagResolver.java new file mode 100644 index 0000000000000..22e3f9187505d --- /dev/null +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/tag/CreateTagResolver.java @@ -0,0 +1,80 @@ +package com.linkedin.datahub.graphql.resolvers.tag; + +import com.linkedin.data.template.SetMode; +import com.linkedin.datahub.graphql.QueryContext; +import com.linkedin.datahub.graphql.authorization.AuthorizationUtils; +import com.linkedin.datahub.graphql.exception.AuthorizationException; +import com.linkedin.datahub.graphql.generated.CreateTagInput; +import com.linkedin.events.metadata.ChangeType; +import com.linkedin.metadata.Constants; +import com.linkedin.metadata.entity.EntityService; +import com.linkedin.metadata.key.TagKey; +import com.linkedin.metadata.utils.EntityKeyUtils; +import com.linkedin.metadata.utils.GenericRecordUtils; +import com.linkedin.mxe.MetadataChangeProposal; +import com.linkedin.tag.TagProperties; +import graphql.schema.DataFetcher; +import graphql.schema.DataFetchingEnvironment; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import static com.linkedin.datahub.graphql.authorization.AuthorizationUtils.*; +import static com.linkedin.datahub.graphql.resolvers.ResolverUtils.*; + +/** + * Resolver used for creating a new Tag on DataHub. Requires the CREATE_TAG or MANAGE_TAGS privilege. + */ +@Slf4j +@RequiredArgsConstructor +public class CreateTagResolver implements DataFetcher> { + + private final EntityService _entityService; + + @Override + public CompletableFuture get(DataFetchingEnvironment environment) throws Exception { + + final QueryContext context = environment.getContext(); + final CreateTagInput input = bindArgument(environment.getArgument("input"), CreateTagInput.class); + + return CompletableFuture.supplyAsync(() -> { + + if (!AuthorizationUtils.canCreateTags(context)) { + throw new AuthorizationException("Unauthorized to perform this action. Please contact your DataHub administrator."); + } + + try { + // Create the Tag Key + final TagKey key = new TagKey(); + + // Take user provided id OR generate a random UUID for the Tag. + final String id = input.getId() != null ? input.getId() : UUID.randomUUID().toString(); + key.setName(id); + + if (_entityService.exists(EntityKeyUtils.convertEntityKeyToUrn(key, Constants.TAG_ENTITY_NAME))) { + throw new IllegalArgumentException("This Tag already exists!"); + } + + // Create the MCP + final MetadataChangeProposal proposal = new MetadataChangeProposal(); + proposal.setEntityKeyAspect(GenericRecordUtils.serializeAspect(key)); + proposal.setEntityType(Constants.TAG_ENTITY_NAME); + proposal.setAspectName(Constants.TAG_PROPERTIES_ASPECT_NAME); + proposal.setAspect(GenericRecordUtils.serializeAspect(mapTagProperties(input))); + proposal.setChangeType(ChangeType.UPSERT); + return _entityService.ingestProposal(proposal, createAuditStamp(context)).getUrn().toString(); + } catch (Exception e) { + log.error("Failed to create Domain with id: {}, name: {}: {}", input.getId(), input.getName(), e.getMessage()); + throw new RuntimeException(String.format("Failed to create Domain with id: %s, name: %s", input.getId(), input.getName()), e); + } + }); + } + + private TagProperties mapTagProperties(final CreateTagInput input) { + final TagProperties result = new TagProperties(); + result.setName(input.getName()); + result.setDescription(input.getDescription(), SetMode.IGNORE_NULL); + return result; + } +} \ No newline at end of file diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/tag/DeleteTagResolver.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/tag/DeleteTagResolver.java new file mode 100644 index 0000000000000..72b95935838ef --- /dev/null +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/tag/DeleteTagResolver.java @@ -0,0 +1,58 @@ +package com.linkedin.datahub.graphql.resolvers.tag; + +import com.linkedin.common.urn.Urn; +import com.linkedin.common.urn.UrnUtils; +import com.linkedin.datahub.graphql.QueryContext; +import com.linkedin.datahub.graphql.authorization.AuthorizationUtils; +import com.linkedin.datahub.graphql.exception.AuthorizationException; +import com.linkedin.entity.client.EntityClient; +import com.linkedin.r2.RemoteInvocationException; +import graphql.schema.DataFetcher; +import graphql.schema.DataFetchingEnvironment; +import java.util.concurrent.CompletableFuture; +import lombok.extern.slf4j.Slf4j; + + +/** + * Resolver responsible for hard deleting a particular DataHub Corp Group + */ +@Slf4j +public class DeleteTagResolver implements DataFetcher> { + + private final EntityClient _entityClient; + + public DeleteTagResolver(final EntityClient entityClient) { + _entityClient = entityClient; + } + + @Override + public CompletableFuture get(final DataFetchingEnvironment environment) throws Exception { + final QueryContext context = environment.getContext(); + final String tagUrn = environment.getArgument("urn"); + final Urn urn = Urn.createFromString(tagUrn); + + return CompletableFuture.supplyAsync(() -> { + + if (AuthorizationUtils.canManageTags(context) || AuthorizationUtils.canDeleteEntity(UrnUtils.getUrn(tagUrn), context)) { + try { + _entityClient.deleteEntity(urn, context.getAuthentication()); + + // Asynchronously Delete all references to the entity (to return quickly) + CompletableFuture.runAsync(() -> { + try { + _entityClient.deleteEntityReferences(urn, context.getAuthentication()); + } catch (RemoteInvocationException e) { + log.error(String.format( + "Caught exception while attempting to clear all entity references for Tag with urn %s", urn), e); + } + }); + + return true; + } catch (Exception e) { + throw new RuntimeException(String.format("Failed to perform delete against domain with urn %s", tagUrn), e); + } + } + throw new AuthorizationException("Unauthorized to perform this action. Please contact your DataHub administrator."); + }); + } +} diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/tag/mappers/TagUpdateInputMapper.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/tag/mappers/TagUpdateInputMapper.java index 264f4041f88cf..b666bf5c60ed5 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/tag/mappers/TagUpdateInputMapper.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/tag/mappers/TagUpdateInputMapper.java @@ -42,7 +42,6 @@ public Collection apply( auditStamp.setActor(actor, SetMode.IGNORE_NULL); auditStamp.setTime(System.currentTimeMillis()); - // Creator is the owner. final Ownership ownership = new Ownership(); final Owner owner = new Owner(); diff --git a/datahub-graphql-core/src/main/resources/entity.graphql b/datahub-graphql-core/src/main/resources/entity.graphql index ddfdc8e17222d..f8941f681a8af 100644 --- a/datahub-graphql-core/src/main/resources/entity.graphql +++ b/datahub-graphql-core/src/main/resources/entity.graphql @@ -210,11 +210,26 @@ type Mutation { """ updateDataJob(urn: String!, input: DataJobUpdateInput!): DataJob + """ + Create a new tag. Requires the 'Manage Tags' or 'Create Tags' Platform Privilege. If a Tag with the provided ID already exists, + it will be overwritten. + """ + createTag( + "Inputs required to create a new Tag." + input: CreateTagInput!): String + """ Update the information about a particular Entity Tag """ updateTag(urn: String!, input: TagUpdateInput!): Tag + """ + Delete a Tag + """ + deleteTag( + "The urn of the Tag to delete" + urn: String!): Boolean + """ Set the hex color associated with an existing Tag """ @@ -326,11 +341,19 @@ type Mutation { createGroup(input: CreateGroupInput!): String """ - Create a new Domain. Returns the urn of the newly created Domain. Requires the Manage Domains privilege. If a domain with the provided ID already exists, + Create a new Domain. Returns the urn of the newly created Domain. Requires the 'Create Domains' or 'Manage Domains' Platform Privilege. If a Domain with the provided ID already exists, it will be overwritten. """ createDomain(input: CreateDomainInput!): String + """ + Delete a Domain + """ + deleteDomain( + "The urn of the Domain to delete" + urn: String!): Boolean + + """ Sets the Domain for a Dataset, Chart, Dashboard, Data Flow (Pipeline), or Data Job (Task). Returns true if the Domain was successfully added, or already exists. Requires the Edit Domains privilege for the Entity. """ @@ -3615,6 +3638,26 @@ input TagUpdateInput { ownership: OwnershipUpdate } +""" +Input required to create a new Tag +""" +input CreateTagInput { + """ + Optional! A custom id to use as the primary key identifier for the Tag. If not provided, a random UUID will be generated as the id. + """ + id: String + + """ + Display name for the Tag + """ + name: String! + + """ + Optional description for the Tag + """ + description: String +} + """ An update for the ownership information for a Metadata Entity """ diff --git a/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/domain/CreateDomainResolverTest.java b/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/domain/CreateDomainResolverTest.java index 8fce428519c4b..a1dbd4ae064dd 100644 --- a/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/domain/CreateDomainResolverTest.java +++ b/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/domain/CreateDomainResolverTest.java @@ -1,16 +1,16 @@ package com.linkedin.datahub.graphql.resolvers.domain; -import com.datahub.authentication.Authentication; +import com.linkedin.common.AuditStamp; +import com.linkedin.common.urn.UrnUtils; import com.linkedin.datahub.graphql.QueryContext; import com.linkedin.datahub.graphql.generated.CreateDomainInput; import com.linkedin.domain.DomainProperties; -import com.linkedin.entity.client.EntityClient; import com.linkedin.events.metadata.ChangeType; import com.linkedin.metadata.Constants; +import com.linkedin.metadata.entity.EntityService; import com.linkedin.metadata.key.DomainKey; import com.linkedin.metadata.utils.GenericRecordUtils; import com.linkedin.mxe.MetadataChangeProposal; -import com.linkedin.r2.RemoteInvocationException; import graphql.schema.DataFetchingEnvironment; import java.util.concurrent.CompletionException; import org.mockito.Mockito; @@ -31,8 +31,12 @@ public class CreateDomainResolverTest { @Test public void testGetSuccess() throws Exception { // Create resolver - EntityClient mockClient = Mockito.mock(EntityClient.class); - CreateDomainResolver resolver = new CreateDomainResolver(mockClient); + EntityService mockService = Mockito.mock(EntityService.class); + Mockito.when(mockService.ingestProposal(Mockito.any(MetadataChangeProposal.class), Mockito.any(AuditStamp.class))) + .thenReturn(new EntityService.IngestProposalResult(UrnUtils.getUrn( + String.format("urn:li:tag:%s", + TEST_INPUT.getId())), true)); + CreateDomainResolver resolver = new CreateDomainResolver(mockService); // Execute resolver QueryContext mockContext = getMockAllowContext(); @@ -55,17 +59,17 @@ public void testGetSuccess() throws Exception { proposal.setChangeType(ChangeType.UPSERT); // Not ideal to match against "any", but we don't know the auto-generated execution request id - Mockito.verify(mockClient, Mockito.times(1)).ingestProposal( + Mockito.verify(mockService, Mockito.times(1)).ingestProposal( Mockito.eq(proposal), - Mockito.any(Authentication.class) + Mockito.any(AuditStamp.class) ); } @Test public void testGetUnauthorized() throws Exception { // Create resolver - EntityClient mockClient = Mockito.mock(EntityClient.class); - CreateDomainResolver resolver = new CreateDomainResolver(mockClient); + EntityService mockService = Mockito.mock(EntityService.class); + CreateDomainResolver resolver = new CreateDomainResolver(mockService); // Execute resolver DataFetchingEnvironment mockEnv = Mockito.mock(DataFetchingEnvironment.class); @@ -74,19 +78,19 @@ public void testGetUnauthorized() throws Exception { Mockito.when(mockEnv.getContext()).thenReturn(mockContext); assertThrows(CompletionException.class, () -> resolver.get(mockEnv).join()); - Mockito.verify(mockClient, Mockito.times(0)).ingestProposal( + Mockito.verify(mockService, Mockito.times(0)).ingestProposal( Mockito.any(), - Mockito.any(Authentication.class)); + Mockito.any(AuditStamp.class)); } @Test public void testGetEntityClientException() throws Exception { // Create resolver - EntityClient mockClient = Mockito.mock(EntityClient.class); - Mockito.doThrow(RemoteInvocationException.class).when(mockClient).ingestProposal( + EntityService mockService = Mockito.mock(EntityService.class); + Mockito.doThrow(RuntimeException.class).when(mockService).ingestProposal( Mockito.any(), - Mockito.any(Authentication.class)); - CreateDomainResolver resolver = new CreateDomainResolver(mockClient); + Mockito.any(AuditStamp.class)); + CreateDomainResolver resolver = new CreateDomainResolver(mockService); // Execute resolver DataFetchingEnvironment mockEnv = Mockito.mock(DataFetchingEnvironment.class); diff --git a/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/domain/DeleteDomainResolverTest.java b/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/domain/DeleteDomainResolverTest.java new file mode 100644 index 0000000000000..1c450b0e85424 --- /dev/null +++ b/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/domain/DeleteDomainResolverTest.java @@ -0,0 +1,56 @@ +package com.linkedin.datahub.graphql.resolvers.domain; + +import com.datahub.authentication.Authentication; +import com.linkedin.common.urn.Urn; +import com.linkedin.datahub.graphql.QueryContext; +import com.linkedin.entity.client.EntityClient; +import graphql.schema.DataFetchingEnvironment; +import java.util.concurrent.CompletionException; +import org.mockito.Mockito; +import org.testng.annotations.Test; + +import static com.linkedin.datahub.graphql.TestUtils.*; +import static org.testng.Assert.*; + + +public class DeleteDomainResolverTest { + + private static final String TEST_URN = "urn:li:domain:test-id"; + + @Test + public void testGetSuccess() throws Exception { + EntityClient mockClient = Mockito.mock(EntityClient.class); + DeleteDomainResolver resolver = new DeleteDomainResolver(mockClient); + + // Execute resolver + QueryContext mockContext = getMockAllowContext(); + DataFetchingEnvironment mockEnv = Mockito.mock(DataFetchingEnvironment.class); + Mockito.when(mockEnv.getArgument(Mockito.eq("urn"))).thenReturn(TEST_URN); + Mockito.when(mockEnv.getContext()).thenReturn(mockContext); + + assertTrue(resolver.get(mockEnv).get()); + + Mockito.verify(mockClient, Mockito.times(1)).deleteEntity( + Mockito.eq(Urn.createFromString(TEST_URN)), + Mockito.any(Authentication.class) + ); + } + + @Test + public void testGetUnauthorized() throws Exception { + // Create resolver + EntityClient mockClient = Mockito.mock(EntityClient.class); + DeleteDomainResolver resolver = new DeleteDomainResolver(mockClient); + + // Execute resolver + DataFetchingEnvironment mockEnv = Mockito.mock(DataFetchingEnvironment.class); + Mockito.when(mockEnv.getArgument(Mockito.eq("urn"))).thenReturn(TEST_URN); + QueryContext mockContext = getMockDenyContext(); + Mockito.when(mockEnv.getContext()).thenReturn(mockContext); + + assertThrows(CompletionException.class, () -> resolver.get(mockEnv).join()); + Mockito.verify(mockClient, Mockito.times(0)).deleteEntity( + Mockito.any(), + Mockito.any(Authentication.class)); + } +} \ No newline at end of file diff --git a/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/tag/CreateTagResolverTest.java b/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/tag/CreateTagResolverTest.java new file mode 100644 index 0000000000000..91217dfc1e2e4 --- /dev/null +++ b/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/tag/CreateTagResolverTest.java @@ -0,0 +1,103 @@ +package com.linkedin.datahub.graphql.resolvers.tag; + +import com.linkedin.common.AuditStamp; +import com.linkedin.common.urn.UrnUtils; +import com.linkedin.datahub.graphql.QueryContext; +import com.linkedin.datahub.graphql.generated.CreateTagInput; +import com.linkedin.tag.TagProperties; +import com.linkedin.events.metadata.ChangeType; +import com.linkedin.metadata.Constants; +import com.linkedin.metadata.entity.EntityService; +import com.linkedin.metadata.key.TagKey; +import com.linkedin.metadata.utils.GenericRecordUtils; +import com.linkedin.mxe.MetadataChangeProposal; +import graphql.schema.DataFetchingEnvironment; +import java.util.concurrent.CompletionException; +import org.mockito.Mockito; +import org.testng.annotations.Test; + +import static com.linkedin.datahub.graphql.TestUtils.*; +import static org.testng.Assert.*; + + +public class CreateTagResolverTest { + + private static final CreateTagInput TEST_INPUT = new CreateTagInput( + "test-id", + "test-name", + "test-description" + ); + + @Test + public void testGetSuccess() throws Exception { + // Create resolver + EntityService mockService = Mockito.mock(EntityService.class); + Mockito.when(mockService.ingestProposal(Mockito.any(MetadataChangeProposal.class), Mockito.any(AuditStamp.class))) + .thenReturn(new EntityService.IngestProposalResult(UrnUtils.getUrn( + String.format("urn:li:tag:%s", + TEST_INPUT.getId())), true)); + CreateTagResolver resolver = new CreateTagResolver(mockService); + + // Execute resolver + QueryContext mockContext = getMockAllowContext(); + DataFetchingEnvironment mockEnv = Mockito.mock(DataFetchingEnvironment.class); + Mockito.when(mockEnv.getArgument(Mockito.eq("input"))).thenReturn(TEST_INPUT); + Mockito.when(mockEnv.getContext()).thenReturn(mockContext); + + resolver.get(mockEnv).get(); + + final TagKey key = new TagKey(); + key.setName("test-id"); + final MetadataChangeProposal proposal = new MetadataChangeProposal(); + proposal.setEntityKeyAspect(GenericRecordUtils.serializeAspect(key)); + proposal.setEntityType(Constants.TAG_ENTITY_NAME); + TagProperties props = new TagProperties(); + props.setDescription("test-description"); + props.setName("test-name"); + proposal.setAspectName(Constants.TAG_PROPERTIES_ASPECT_NAME); + proposal.setAspect(GenericRecordUtils.serializeAspect(props)); + proposal.setChangeType(ChangeType.UPSERT); + + // Not ideal to match against "any", but we don't know the auto-generated execution request id + Mockito.verify(mockService, Mockito.times(1)).ingestProposal( + Mockito.eq(proposal), + Mockito.any(AuditStamp.class) + ); + } + + @Test + public void testGetUnauthorized() throws Exception { + // Create resolver + EntityService mockService = Mockito.mock(EntityService.class); + CreateTagResolver resolver = new CreateTagResolver(mockService); + + // Execute resolver + DataFetchingEnvironment mockEnv = Mockito.mock(DataFetchingEnvironment.class); + QueryContext mockContext = getMockDenyContext(); + Mockito.when(mockEnv.getArgument(Mockito.eq("input"))).thenReturn(TEST_INPUT); + Mockito.when(mockEnv.getContext()).thenReturn(mockContext); + + assertThrows(CompletionException.class, () -> resolver.get(mockEnv).join()); + Mockito.verify(mockService, Mockito.times(0)).ingestProposal( + Mockito.any(), + Mockito.any(AuditStamp.class)); + } + + @Test + public void testGetEntityClientException() throws Exception { + // Create resolver + EntityService mockService = Mockito.mock(EntityService.class); + Mockito.doThrow(RuntimeException.class).when(mockService).ingestProposal( + Mockito.any(), + Mockito.any(AuditStamp.class)); + CreateTagResolver resolver = new CreateTagResolver(mockService); + + // Execute resolver + DataFetchingEnvironment mockEnv = Mockito.mock(DataFetchingEnvironment.class); + QueryContext mockContext = getMockAllowContext(); + Mockito.when(mockEnv.getArgument(Mockito.eq("input"))).thenReturn(TEST_INPUT); + Mockito.when(mockEnv.getContext()).thenReturn(mockContext); + + assertThrows(CompletionException.class, () -> resolver.get(mockEnv).join()); + } +} \ No newline at end of file diff --git a/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/tag/DeleteTagResolverTest.java b/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/tag/DeleteTagResolverTest.java new file mode 100644 index 0000000000000..b01ac1a9b14ae --- /dev/null +++ b/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/tag/DeleteTagResolverTest.java @@ -0,0 +1,56 @@ +package com.linkedin.datahub.graphql.resolvers.tag; + +import com.datahub.authentication.Authentication; +import com.linkedin.common.urn.Urn; +import com.linkedin.datahub.graphql.QueryContext; +import com.linkedin.entity.client.EntityClient; +import graphql.schema.DataFetchingEnvironment; +import java.util.concurrent.CompletionException; +import org.mockito.Mockito; +import org.testng.annotations.Test; + +import static com.linkedin.datahub.graphql.TestUtils.*; +import static org.testng.Assert.*; + + +public class DeleteTagResolverTest { + + private static final String TEST_URN = "urn:li:tag:test-id"; + + @Test + public void testGetSuccess() throws Exception { + EntityClient mockClient = Mockito.mock(EntityClient.class); + DeleteTagResolver resolver = new DeleteTagResolver(mockClient); + + // Execute resolver + QueryContext mockContext = getMockAllowContext(); + DataFetchingEnvironment mockEnv = Mockito.mock(DataFetchingEnvironment.class); + Mockito.when(mockEnv.getArgument(Mockito.eq("urn"))).thenReturn(TEST_URN); + Mockito.when(mockEnv.getContext()).thenReturn(mockContext); + + assertTrue(resolver.get(mockEnv).get()); + + Mockito.verify(mockClient, Mockito.times(1)).deleteEntity( + Mockito.eq(Urn.createFromString(TEST_URN)), + Mockito.any(Authentication.class) + ); + } + + @Test + public void testGetUnauthorized() throws Exception { + // Create resolver + EntityClient mockClient = Mockito.mock(EntityClient.class); + DeleteTagResolver resolver = new DeleteTagResolver(mockClient); + + // Execute resolver + DataFetchingEnvironment mockEnv = Mockito.mock(DataFetchingEnvironment.class); + Mockito.when(mockEnv.getArgument(Mockito.eq("urn"))).thenReturn(TEST_URN); + QueryContext mockContext = getMockDenyContext(); + Mockito.when(mockEnv.getContext()).thenReturn(mockContext); + + assertThrows(CompletionException.class, () -> resolver.get(mockEnv).join()); + Mockito.verify(mockClient, Mockito.times(0)).deleteEntity( + Mockito.any(), + Mockito.any(Authentication.class)); + } +} \ No newline at end of file diff --git a/metadata-service/war/src/main/resources/boot/policies.json b/metadata-service/war/src/main/resources/boot/policies.json index ae154bd132e2b..643f7a5e882a8 100644 --- a/metadata-service/war/src/main/resources/boot/policies.json +++ b/metadata-service/war/src/main/resources/boot/policies.json @@ -21,7 +21,8 @@ "MANAGE_DOMAINS", "MANAGE_TESTS", "MANAGE_GLOSSARIES", - "MANAGE_USER_CREDENTIALS" + "MANAGE_USER_CREDENTIALS", + "MANAGE_TAGS" ], "displayName":"Root User - All Platform Privileges", "description":"Grants full platform privileges to root datahub super user.", @@ -84,7 +85,8 @@ "GENERATE_PERSONAL_ACCESS_TOKENS", "MANAGE_DOMAINS", "MANAGE_TESTS", - "MANAGE_GLOSSARIES" + "MANAGE_GLOSSARIES", + "MANAGE_TAGS" ], "displayName":"All Users - All Platform Privileges", "description":"Grants full platform privileges to ALL users of DataHub. Change this policy to alter that behavior.", diff --git a/metadata-utils/src/main/java/com/linkedin/metadata/authorization/PoliciesConfig.java b/metadata-utils/src/main/java/com/linkedin/metadata/authorization/PoliciesConfig.java index 20b2074a16064..7c870e5183410 100644 --- a/metadata-utils/src/main/java/com/linkedin/metadata/authorization/PoliciesConfig.java +++ b/metadata-utils/src/main/java/com/linkedin/metadata/authorization/PoliciesConfig.java @@ -52,7 +52,6 @@ public class PoliciesConfig { "Generate Personal Access Tokens", "Generate personal access tokens for use with DataHub APIs."); - public static final Privilege MANAGE_ACCESS_TOKENS = Privilege.of( "MANAGE_ACCESS_TOKENS", "Manage All Access Tokens", @@ -79,6 +78,21 @@ public class PoliciesConfig { Privilege.of("MANAGE_USER_CREDENTIALS", "Manage User Credentials", "Manage credentials for native DataHub users, including inviting new users and resetting passwords"); + public static final Privilege MANAGE_TAGS_PRIVILEGE = Privilege.of( + "MANAGE_TAGS", + "Manage Tags", + "Create and remove Tags."); + + public static final Privilege CREATE_TAGS_PRIVILEGE = Privilege.of( + "CREATE_TAGS", + "Create Tags", + "Create new Tags."); + + public static final Privilege CREATE_DOMAINS_PRIVILEGE = Privilege.of( + "CREATE_DOMAINS", + "Create Domains", + "Create new Domains."); + public static final List PLATFORM_PRIVILEGES = ImmutableList.of( MANAGE_POLICIES_PRIVILEGE, MANAGE_USERS_AND_GROUPS_PRIVILEGE, @@ -90,7 +104,10 @@ public class PoliciesConfig { MANAGE_ACCESS_TOKENS, MANAGE_TESTS_PRIVILEGE, MANAGE_GLOSSARIES_PRIVILEGE, - MANAGE_USER_CREDENTIALS_PRIVILEGE + MANAGE_USER_CREDENTIALS_PRIVILEGE, + MANAGE_TAGS_PRIVILEGE, + CREATE_TAGS_PRIVILEGE, + CREATE_DOMAINS_PRIVILEGE ); // Resource Privileges // @@ -155,6 +172,11 @@ public class PoliciesConfig { "Edit All", "The ability to edit any information about an entity. Super user privileges."); + public static final Privilege DELETE_ENTITY_PRIVILEGE = Privilege.of( + "DELETE_ENTITY", + "Delete", + "The ability to delete the delete this entity."); + public static final List COMMON_ENTITY_PRIVILEGES = ImmutableList.of( VIEW_ENTITY_PAGE_PRIVILEGE, EDIT_ENTITY_TAGS_PRIVILEGE, @@ -283,7 +305,7 @@ public class PoliciesConfig { "Tags", "Tags indexed by DataHub", ImmutableList.of(VIEW_ENTITY_PAGE_PRIVILEGE, EDIT_ENTITY_OWNERS_PRIVILEGE, EDIT_TAG_COLOR_PRIVILEGE, - EDIT_ENTITY_DOCS_PRIVILEGE, EDIT_ENTITY_PRIVILEGE) + EDIT_ENTITY_DOCS_PRIVILEGE, EDIT_ENTITY_PRIVILEGE, DELETE_ENTITY_PRIVILEGE) ); // Container Privileges @@ -300,7 +322,7 @@ public class PoliciesConfig { "Domains", "Domains created on DataHub", ImmutableList.of(VIEW_ENTITY_PAGE_PRIVILEGE, EDIT_ENTITY_OWNERS_PRIVILEGE, EDIT_ENTITY_DOCS_PRIVILEGE, - EDIT_ENTITY_DOC_LINKS_PRIVILEGE, EDIT_ENTITY_PRIVILEGE) + EDIT_ENTITY_DOC_LINKS_PRIVILEGE, EDIT_ENTITY_PRIVILEGE, DELETE_ENTITY_PRIVILEGE) ); // Glossary Term Privileges From 45c24ac57758f4ef41400b6ad262ec17a8b8b029 Mon Sep 17 00:00:00 2001 From: John Joyce Date: Tue, 28 Jun 2022 10:09:34 -0700 Subject: [PATCH 2/5] Adding me resolver --- .../datahub/graphql/resolvers/MeResolver.java | 4 ++++ .../resolvers/domain/DeleteDomainResolver.java | 3 +-- .../src/main/resources/app.graphql | 15 +++++++++++++++ docs/policies.md | 5 ++++- 4 files changed, 24 insertions(+), 3 deletions(-) diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/MeResolver.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/MeResolver.java index ecd6a422903b8..35ad96193263a 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/MeResolver.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/MeResolver.java @@ -5,6 +5,7 @@ import com.datahub.authorization.Authorizer; import com.linkedin.common.urn.Urn; import com.linkedin.datahub.graphql.QueryContext; +import com.linkedin.datahub.graphql.authorization.AuthorizationUtils; import com.linkedin.datahub.graphql.generated.AuthenticatedUser; import com.linkedin.datahub.graphql.generated.CorpUser; import com.linkedin.datahub.graphql.generated.PlatformPrivileges; @@ -65,6 +66,9 @@ public CompletableFuture get(DataFetchingEnvironment environm platformPrivileges.setManageTests(canManageTests(context)); platformPrivileges.setManageGlossaries(canManageGlossaries(context)); platformPrivileges.setManageUserCredentials(canManageUserCredentials(context)); + platformPrivileges.setCreateDomains(AuthorizationUtils.canCreateDomains(context)); + platformPrivileges.setCreateTags(AuthorizationUtils.canCreateTags(context)); + platformPrivileges.setManageTags(AuthorizationUtils.canManageTags(context)); // Construct and return authenticated user object. final AuthenticatedUser authUser = new AuthenticatedUser(); diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/domain/DeleteDomainResolver.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/domain/DeleteDomainResolver.java index 10d35a4e0e53e..e3f0ffd1ac7b1 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/domain/DeleteDomainResolver.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/domain/DeleteDomainResolver.java @@ -1,7 +1,6 @@ package com.linkedin.datahub.graphql.resolvers.domain; import com.linkedin.common.urn.Urn; -import com.linkedin.common.urn.UrnUtils; import com.linkedin.datahub.graphql.QueryContext; import com.linkedin.datahub.graphql.authorization.AuthorizationUtils; import com.linkedin.datahub.graphql.exception.AuthorizationException; @@ -32,7 +31,7 @@ public CompletableFuture get(final DataFetchingEnvironment environment) final Urn urn = Urn.createFromString(domainUrn); return CompletableFuture.supplyAsync(() -> { - if (AuthorizationUtils.canManageDomains(context) || AuthorizationUtils.canDeleteEntity(UrnUtils.getUrn(domainUrn), context)) { + if (AuthorizationUtils.canManageDomains(context) || AuthorizationUtils.canDeleteEntity(urn, context)) { try { _entityClient.deleteEntity(urn, context.getAuthentication()); diff --git a/datahub-graphql-core/src/main/resources/app.graphql b/datahub-graphql-core/src/main/resources/app.graphql index c183bd6700600..8f5e5e7a03f86 100644 --- a/datahub-graphql-core/src/main/resources/app.graphql +++ b/datahub-graphql-core/src/main/resources/app.graphql @@ -52,6 +52,11 @@ type PlatformPrivileges { """ generatePersonalAccessTokens: Boolean! + """ + Whether the user should be able to create new Domains + """ + createDomains: Boolean! + """ Whether the user should be able to manage Domains """ @@ -86,6 +91,16 @@ type PlatformPrivileges { Whether the user is able to manage user credentials """ manageUserCredentials: Boolean! + + """ + Whether the user should be able to create new Tags + """ + createTags: Boolean! + + """ + Whether the user should be able to create and delete all Tags + """ + manageTags: Boolean! } """ diff --git a/docs/policies.md b/docs/policies.md index da2078e3c840f..32a89aca49384 100644 --- a/docs/policies.md +++ b/docs/policies.md @@ -77,11 +77,14 @@ We currently support the following: | Manage Secrets | Allow actor to create & remove secrets stored inside DataHub. | | Manage Users & Groups | Allow actor to create, remove, and update users and groups on DataHub. | | Manage All Access Tokens | Allow actor to create, remove, and list access tokens for all users on DataHub. | -| Manage Domains | Allow actor to create and remove Asset Domains. | +| Create Domains | Allow the actor to create new Domains | +| Manage Domains | Allow actor to create and remove any Domains. | | View Analytics | Allow the actor access to the DataHub analytics dashboard. | | Generate Personal Access Tokens | Allow the actor to generate access tokens for personal use with DataHub APIs. | | Manage User Credentials | Allow the actor to generate invite links for new native DataHub users, and password reset links for existing native users. | | Manage Glossaries | Allow the actor to create, edit, move, and delete Glossary Terms and Term Groups | +| Create Tags | Allow the actor to create new Tags | +| Manage Tags | Allow the actor to create and remove any Tags | **Common metadata privileges** to view & modify any entity within DataHub. From 5cdc322e2194b64a40ce3c269d04e8bb2386ba08 Mon Sep 17 00:00:00 2001 From: John Joyce Date: Tue, 28 Jun 2022 10:51:33 -0700 Subject: [PATCH 3/5] Adding projects --- datahub-web-react/src/graphql/me.graphql | 3 +++ 1 file changed, 3 insertions(+) diff --git a/datahub-web-react/src/graphql/me.graphql b/datahub-web-react/src/graphql/me.graphql index 7f1d426f3d6a7..d28b1a60cd504 100644 --- a/datahub-web-react/src/graphql/me.graphql +++ b/datahub-web-react/src/graphql/me.graphql @@ -31,6 +31,9 @@ query getMe { manageTests manageGlossaries manageUserCredentials + manageTags + createDomains + createTags } } } From fe738d92271e4a82d0c0a3bb531a84c1c51bfd07 Mon Sep 17 00:00:00 2001 From: John Joyce Date: Tue, 28 Jun 2022 13:09:57 -0700 Subject: [PATCH 4/5] Updating mocks --- datahub-web-react/src/Mocks.tsx | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/datahub-web-react/src/Mocks.tsx b/datahub-web-react/src/Mocks.tsx index 84dbb303b63fd..e3911d00a74c9 100644 --- a/datahub-web-react/src/Mocks.tsx +++ b/datahub-web-react/src/Mocks.tsx @@ -3083,6 +3083,17 @@ export const mocks = [ viewAnalytics: true, managePolicies: true, manageIdentities: true, + manageDomains: true, + manageTags: true, + createDomains: true, + createTags: true, + manageUserCredentials: true, + manageGlossaries: true, + manageTests: true, + manageTokens: true, + manageSecrets: true, + manageIngestion: true, + generatePersonalAccessTokens: true, }, }, }, From 827b7fc282e3c0ab5fadce93c732e9276ae178c2 Mon Sep 17 00:00:00 2001 From: John Joyce Date: Tue, 28 Jun 2022 14:36:14 -0700 Subject: [PATCH 5/5] Fix mocks --- datahub-web-react/src/Mocks.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/datahub-web-react/src/Mocks.tsx b/datahub-web-react/src/Mocks.tsx index 890b2e5ab9ae1..a1b65f3e2b6ac 100644 --- a/datahub-web-react/src/Mocks.tsx +++ b/datahub-web-react/src/Mocks.tsx @@ -3314,4 +3314,7 @@ export const platformPrivileges: PlatformPrivileges = { manageTests: true, manageGlossaries: true, manageUserCredentials: true, + manageTags: true, + createTags: true, + createDomains: true, };