feat(GraphQL): Support for Deleting Domains, Tags via GraphQL API (#5272)

This commit is contained in:
John Joyce 2022-06-28 17:57:22 -04:00 committed by GitHub
parent be0bc0f0f0
commit fdee7a787b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 615 additions and 48 deletions

View File

@ -84,6 +84,7 @@ import com.linkedin.datahub.graphql.resolvers.container.ParentContainersResolver
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.AutoCompleteResolver;
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 @@ public class GmsGraphQLEngine {
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 @@ public class GmsGraphQLEngine {
.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))

View File

@ -5,14 +5,24 @@ import com.datahub.authorization.AuthorizationResult;
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 class AuthorizationUtils {
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 class AuthorizationUtils {
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);
}

View File

@ -5,6 +5,7 @@ import com.datahub.authorization.AuthorizationResult;
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 class MeResolver implements DataFetcher<CompletableFuture<AuthenticatedUs
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();

View File

@ -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 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 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<CompletableFuture<String>> {
private final EntityClient _entityClient;
private final EntityService _entityService;
@Override
public CompletableFuture<String> get(DataFetchingEnvironment environment) throws Exception {
@ -42,12 +40,10 @@ public class CreateDomainResolver implements DataFetcher<CompletableFuture<Strin
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 class CreateDomainResolver implements DataFetcher<CompletableFuture<Strin
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 class CreateDomainResolver implements DataFetcher<CompletableFuture<Strin
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 @@ public class CreateDomainResolver implements DataFetcher<CompletableFuture<Strin
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);
}
}

View File

@ -0,0 +1,55 @@
package com.linkedin.datahub.graphql.resolvers.domain;
import com.linkedin.common.urn.Urn;
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<CompletableFuture<Boolean>> {
private final EntityClient _entityClient;
public DeleteDomainResolver(final EntityClient entityClient) {
_entityClient = entityClient;
}
@Override
public CompletableFuture<Boolean> 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(urn, 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.");
});
}
}

View File

@ -45,7 +45,7 @@ public class ListDomainsResolver implements DataFetcher<CompletableFuture<ListDo
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();

View File

@ -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<CompletableFuture<String>> {
private final EntityService _entityService;
@Override
public CompletableFuture<String> 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;
}
}

View File

@ -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<CompletableFuture<Boolean>> {
private final EntityClient _entityClient;
public DeleteTagResolver(final EntityClient entityClient) {
_entityClient = entityClient;
}
@Override
public CompletableFuture<Boolean> 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.");
});
}
}

View File

@ -42,7 +42,6 @@ public class TagUpdateInputMapper implements InputModelMapper<TagUpdateInput, Co
auditStamp.setActor(actor, SetMode.IGNORE_NULL);
auditStamp.setTime(System.currentTimeMillis());
// Creator is the owner.
final Ownership ownership = new Ownership();
final Owner owner = new Owner();

View File

@ -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!
}
"""

View File

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

View File

@ -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 class CreateDomainResolverTest {
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 class CreateDomainResolverTest {
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);

View File

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

View File

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

View File

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

View File

@ -3084,6 +3084,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,
},
},
},
@ -3303,4 +3314,7 @@ export const platformPrivileges: PlatformPrivileges = {
manageTests: true,
manageGlossaries: true,
manageUserCredentials: true,
manageTags: true,
createTags: true,
createDomains: true,
};

View File

@ -31,6 +31,9 @@ query getMe {
manageTests
manageGlossaries
manageUserCredentials
manageTags
createDomains
createTags
}
}
}

View File

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

View File

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

View File

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