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 ad6809439b434..51021bf2a299b 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 @@ -49,6 +49,7 @@ import com.linkedin.datahub.graphql.resolvers.browse.BrowsePathsResolver; import com.linkedin.datahub.graphql.resolvers.browse.BrowseResolver; import com.linkedin.datahub.graphql.resolvers.config.AppConfigResolver; +import com.linkedin.datahub.graphql.resolvers.deprecation.UpdateDeprecationResolver; import com.linkedin.datahub.graphql.resolvers.domain.CreateDomainResolver; import com.linkedin.datahub.graphql.resolvers.domain.DomainEntitiesResolver; import com.linkedin.datahub.graphql.resolvers.domain.ListDomainsResolver; @@ -613,6 +614,7 @@ private void configureMutationResolvers(final RuntimeWiring.Builder builder) { .dataFetcher("updateUserStatus", new UpdateUserStatusResolver(this.entityClient)) .dataFetcher("createDomain", new CreateDomainResolver(this.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)) .dataFetcher("createSecret", new CreateSecretResolver(this.entityClient, this.secretService)) .dataFetcher("deleteSecret", new DeleteSecretResolver(this.entityClient)) diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/AuthUtils.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/AuthUtils.java index e442aba8fb10e..081a56bebb229 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/AuthUtils.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/AuthUtils.java @@ -1,5 +1,8 @@ package com.linkedin.datahub.graphql.resolvers; +import com.google.common.collect.ImmutableList; +import com.linkedin.datahub.graphql.authorization.ConjunctivePrivilegeGroup; +import com.linkedin.metadata.authorization.PoliciesConfig; import java.util.List; import java.util.Optional; import com.datahub.authorization.AuthorizationRequest; @@ -8,6 +11,10 @@ public class AuthUtils { + public static final ConjunctivePrivilegeGroup ALL_PRIVILEGES_GROUP = new ConjunctivePrivilegeGroup(ImmutableList.of( + PoliciesConfig.EDIT_ENTITY_PRIVILEGE.getType() + )); + public static boolean isAuthorized( String principal, List privilegeGroup, diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/config/AppConfigResolver.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/config/AppConfigResolver.java index d31670ad3bf8c..f74aa4df5b0f6 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/config/AppConfigResolver.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/config/AppConfigResolver.java @@ -108,6 +108,12 @@ private EntityType mapResourceTypeToEntityType(final String resourceType) { return EntityType.DATA_JOB; } else if (com.linkedin.metadata.authorization.PoliciesConfig.TAG_PRIVILEGES.getResourceType().equals(resourceType)) { return EntityType.TAG; + } else if (com.linkedin.metadata.authorization.PoliciesConfig.GLOSSARY_TERM_PRIVILEGES.getResourceType().equals(resourceType)) { + return EntityType.GLOSSARY_TERM; + } else if (com.linkedin.metadata.authorization.PoliciesConfig.DOMAIN_PRIVILEGES.getResourceType().equals(resourceType)) { + return EntityType.DOMAIN; + } else if (com.linkedin.metadata.authorization.PoliciesConfig.CONTAINER_PRIVILEGES.getResourceType().equals(resourceType)) { + return EntityType.CONTAINER; } else { return null; } diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/deprecation/UpdateDeprecationResolver.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/deprecation/UpdateDeprecationResolver.java new file mode 100644 index 0000000000000..a797c05a14fa8 --- /dev/null +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/deprecation/UpdateDeprecationResolver.java @@ -0,0 +1,129 @@ +package com.linkedin.datahub.graphql.resolvers.deprecation; + +import com.google.common.collect.ImmutableList; +import com.linkedin.common.Deprecation; +import com.linkedin.common.urn.Urn; +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.UpdateDeprecationInput; +import com.linkedin.datahub.graphql.resolvers.AuthUtils; +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.utils.GenericAspectUtils; +import com.linkedin.mxe.MetadataChangeProposal; +import graphql.schema.DataFetcher; +import graphql.schema.DataFetchingEnvironment; +import java.net.URISyntaxException; +import java.util.concurrent.CompletableFuture; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import static com.linkedin.datahub.graphql.resolvers.ResolverUtils.*; +import static com.linkedin.datahub.graphql.resolvers.mutate.MutationUtils.*; + + +/** + * Resolver used for updating the Domain associated with a Metadata Asset. Requires the EDIT_DOMAINS privilege for a particular asset. + */ +@Slf4j +@RequiredArgsConstructor +public class UpdateDeprecationResolver implements DataFetcher> { + + private static final String EMPTY_STRING = ""; + private final EntityClient _entityClient; + private final EntityService _entityService; // TODO: Remove this when 'exists' added to EntityClient + + @Override + public CompletableFuture get(DataFetchingEnvironment environment) throws Exception { + + final QueryContext context = environment.getContext(); + final UpdateDeprecationInput input = bindArgument(environment.getArgument("input"), UpdateDeprecationInput.class); + final Urn entityUrn = Urn.createFromString(input.getUrn()); + + return CompletableFuture.supplyAsync(() -> { + + if (!isAuthorizedToUpdateDeprecationForEntity(environment.getContext(), entityUrn)) { + throw new AuthorizationException("Unauthorized to perform this action. Please contact your DataHub administrator."); + } + validateUpdateDeprecationInput( + entityUrn, + _entityService + ); + try { + Deprecation deprecation = (Deprecation) getAspectFromEntity( + entityUrn.toString(), + Constants.DEPRECATION_ASPECT_NAME, + _entityService, + new Deprecation()); + updateDeprecation(deprecation, input, context); + + // Create the Deprecation aspect + final MetadataChangeProposal proposal = new MetadataChangeProposal(); + proposal.setEntityUrn(entityUrn); + proposal.setEntityType(entityUrn.getEntityType()); + proposal.setAspectName(Constants.DEPRECATION_ASPECT_NAME); + proposal.setAspect(GenericAspectUtils.serializeAspect(deprecation)); + proposal.setChangeType(ChangeType.UPSERT); + _entityClient.ingestProposal(proposal, context.getAuthentication()); + return true; + } catch (Exception e) { + log.error("Failed to update Deprecation for resource with entity urn {}: {}", entityUrn, e.getMessage()); + throw new RuntimeException(String.format("Failed to update Deprecation for resource with entity urn %s", entityUrn), e); + } + }); + } + + private boolean isAuthorizedToUpdateDeprecationForEntity(final QueryContext context, final Urn entityUrn) { + final DisjunctivePrivilegeGroup orPrivilegeGroups = new DisjunctivePrivilegeGroup(ImmutableList.of( + AuthUtils.ALL_PRIVILEGES_GROUP, + new ConjunctivePrivilegeGroup(ImmutableList.of(PoliciesConfig.EDIT_ENTITY_DEPRECATION_PRIVILEGE.getType())) + )); + + return AuthorizationUtils.isAuthorized( + context.getAuthorizer(), + context.getActorUrn(), + entityUrn.getEntityType(), + entityUrn.toString(), + orPrivilegeGroups); + } + + public static Boolean validateUpdateDeprecationInput( + Urn entityUrn, + EntityService entityService + ) { + + if (!entityService.exists(entityUrn)) { + throw new IllegalArgumentException( + String.format("Failed to update deprecation for Entity %s. Entity does not exist.", entityUrn)); + } + + return true; + } + + private static void updateDeprecation(Deprecation deprecation, UpdateDeprecationInput input, QueryContext context) { + deprecation.setDeprecated(input.getDeprecated()); + deprecation.setDecommissionTime(input.getDecommissionTime(), SetMode.REMOVE_IF_NULL); + if (input.getNote() != null) { + deprecation.setNote(input.getNote()); + } else { + // Note is required field in GMS. Set to empty string if not provided. + deprecation.setNote(EMPTY_STRING); + } + try { + deprecation.setActor(Urn.createFromString(context.getActorUrn())); + } catch (URISyntaxException e) { + // Should never happen. + throw new RuntimeException( + String.format("Failed to convert authorized actor into an Urn. actor urn: %s", + context.getActorUrn()), + e); + } + } +} \ No newline at end of file diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/chart/ChartType.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/chart/ChartType.java index 044719a13616a..e760e99595ce1 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/chart/ChartType.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/chart/ChartType.java @@ -70,7 +70,8 @@ public class ChartType implements SearchableEntityType, BrowsableEntityTy GLOSSARY_TERMS_ASPECT_NAME, STATUS_ASPECT_NAME, CONTAINER_ASPECT_NAME, - DOMAINS_ASPECT_NAME + DOMAINS_ASPECT_NAME, + DEPRECATION_ASPECT_NAME ); private static final Set FACET_FIELDS = ImmutableSet.of("access", "queryType", "tool", "type"); diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/chart/mappers/ChartMapper.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/chart/mappers/ChartMapper.java index 212609c788c21..76fd47a63697a 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/chart/mappers/ChartMapper.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/chart/mappers/ChartMapper.java @@ -1,6 +1,7 @@ package com.linkedin.datahub.graphql.types.chart.mappers; import com.linkedin.chart.EditableChartProperties; +import com.linkedin.common.Deprecation; import com.linkedin.common.GlobalTags; import com.linkedin.common.GlossaryTerms; import com.linkedin.common.InstitutionalMemory; @@ -21,6 +22,7 @@ import com.linkedin.datahub.graphql.generated.Domain; import com.linkedin.datahub.graphql.generated.EntityType; import com.linkedin.datahub.graphql.types.common.mappers.AuditStampMapper; +import com.linkedin.datahub.graphql.types.common.mappers.DeprecationMapper; import com.linkedin.datahub.graphql.types.common.mappers.InstitutionalMemoryMapper; import com.linkedin.datahub.graphql.types.common.mappers.OwnershipMapper; import com.linkedin.datahub.graphql.types.common.mappers.StatusMapper; @@ -104,6 +106,8 @@ public Chart apply(@Nonnull final EntityResponse entityResponse) { .setType(EntityType.DOMAIN) .setUrn(domains.getDomains().get(0).toString()).build()); } + } else if (DEPRECATION_ASPECT_NAME.equals(name)) { + result.setDeprecation(DeprecationMapper.map(new Deprecation(data))); } }); diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/container/ContainerType.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/container/ContainerType.java index b70530c75047f..6a31c3e5a22f2 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/container/ContainerType.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/container/ContainerType.java @@ -33,7 +33,8 @@ public class ContainerType implements com.linkedin.datahub.graphql.types.EntityT Constants.GLOBAL_TAGS_ASPECT_NAME, Constants.GLOSSARY_TERMS_ASPECT_NAME, Constants.CONTAINER_ASPECT_NAME, - Constants.DOMAINS_ASPECT_NAME + Constants.DOMAINS_ASPECT_NAME, + Constants.DEPRECATION_ASPECT_NAME ); private final EntityClient _entityClient; diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/container/mappers/ContainerMapper.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/container/mappers/ContainerMapper.java index dce901bb5e35e..53beee6da93e1 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/container/mappers/ContainerMapper.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/container/mappers/ContainerMapper.java @@ -1,6 +1,7 @@ package com.linkedin.datahub.graphql.types.container.mappers; import com.linkedin.common.DataPlatformInstance; +import com.linkedin.common.Deprecation; import com.linkedin.common.GlobalTags; import com.linkedin.common.GlossaryTerms; import com.linkedin.common.InstitutionalMemory; @@ -13,6 +14,7 @@ import com.linkedin.datahub.graphql.generated.DataPlatform; import com.linkedin.datahub.graphql.generated.Domain; import com.linkedin.datahub.graphql.generated.EntityType; +import com.linkedin.datahub.graphql.types.common.mappers.DeprecationMapper; import com.linkedin.datahub.graphql.types.common.mappers.InstitutionalMemoryMapper; import com.linkedin.datahub.graphql.types.common.mappers.OwnershipMapper; import com.linkedin.datahub.graphql.types.common.mappers.StringMapMapper; @@ -102,6 +104,11 @@ public static Container map(final EntityResponse entityResponse) { } } + final EnvelopedAspect envelopedDeprecation = aspects.get(Constants.DEPRECATION_ASPECT_NAME); + if (envelopedDeprecation != null) { + result.setDeprecation(DeprecationMapper.map(new Deprecation(envelopedDeprecation.getValue().data()))); + } + return result; } diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/dashboard/DashboardType.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/dashboard/DashboardType.java index 3953e5966c03a..b371e20b1b9ab 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/dashboard/DashboardType.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/dashboard/DashboardType.java @@ -70,7 +70,8 @@ public class DashboardType implements SearchableEntityType, Browsable GLOSSARY_TERMS_ASPECT_NAME, STATUS_ASPECT_NAME, CONTAINER_ASPECT_NAME, - DOMAINS_ASPECT_NAME + DOMAINS_ASPECT_NAME, + DEPRECATION_ASPECT_NAME ); private static final Set FACET_FIELDS = ImmutableSet.of("access", "tool"); diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/dashboard/mappers/DashboardMapper.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/dashboard/mappers/DashboardMapper.java index 1c71deca39358..3974e97ad188e 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/dashboard/mappers/DashboardMapper.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/dashboard/mappers/DashboardMapper.java @@ -1,5 +1,6 @@ package com.linkedin.datahub.graphql.types.dashboard.mappers; +import com.linkedin.common.Deprecation; import com.linkedin.common.GlobalTags; import com.linkedin.common.GlossaryTerms; import com.linkedin.common.InstitutionalMemory; @@ -18,6 +19,7 @@ import com.linkedin.datahub.graphql.generated.Domain; import com.linkedin.datahub.graphql.generated.EntityType; import com.linkedin.datahub.graphql.types.common.mappers.AuditStampMapper; +import com.linkedin.datahub.graphql.types.common.mappers.DeprecationMapper; import com.linkedin.datahub.graphql.types.common.mappers.InstitutionalMemoryMapper; import com.linkedin.datahub.graphql.types.common.mappers.OwnershipMapper; import com.linkedin.datahub.graphql.types.common.mappers.StatusMapper; @@ -97,6 +99,8 @@ public Dashboard apply(@Nonnull final EntityResponse entityResponse) { .setType(EntityType.DOMAIN) .setUrn(domains.getDomains().get(0).toString()).build()); } + } else if (DEPRECATION_ASPECT_NAME.equals(name)) { + result.setDeprecation(DeprecationMapper.map(new Deprecation(data))); } }); diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/dataflow/DataFlowType.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/dataflow/DataFlowType.java index e48569150be14..eced20364f094 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/dataflow/DataFlowType.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/dataflow/DataFlowType.java @@ -68,7 +68,8 @@ public class DataFlowType implements SearchableEntityType, BrowsableEn GLOBAL_TAGS_ASPECT_NAME, GLOSSARY_TERMS_ASPECT_NAME, STATUS_ASPECT_NAME, - DOMAINS_ASPECT_NAME + DOMAINS_ASPECT_NAME, + DEPRECATION_ASPECT_NAME ); private static final Set FACET_FIELDS = ImmutableSet.of("orchestrator", "cluster"); private final EntityClient _entityClient; diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/dataflow/mappers/DataFlowMapper.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/dataflow/mappers/DataFlowMapper.java index f634f77367c46..f408a527034ea 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/dataflow/mappers/DataFlowMapper.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/dataflow/mappers/DataFlowMapper.java @@ -1,5 +1,6 @@ package com.linkedin.datahub.graphql.types.dataflow.mappers; +import com.linkedin.common.Deprecation; import com.linkedin.common.GlobalTags; import com.linkedin.common.GlossaryTerms; import com.linkedin.common.InstitutionalMemory; @@ -13,6 +14,7 @@ import com.linkedin.datahub.graphql.generated.DataPlatform; import com.linkedin.datahub.graphql.generated.Domain; import com.linkedin.datahub.graphql.generated.EntityType; +import com.linkedin.datahub.graphql.types.common.mappers.DeprecationMapper; import com.linkedin.datahub.graphql.types.common.mappers.InstitutionalMemoryMapper; import com.linkedin.datahub.graphql.types.common.mappers.OwnershipMapper; import com.linkedin.datahub.graphql.types.common.mappers.StatusMapper; @@ -86,6 +88,8 @@ public DataFlow apply(@Nonnull final EntityResponse entityResponse) { .setType(EntityType.DOMAIN) .setUrn(domains.getDomains().get(0).toString()).build()); } + } else if (DEPRECATION_ASPECT_NAME.equals(name)) { + result.setDeprecation(DeprecationMapper.map(new Deprecation(data))); } }); diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/datajob/DataJobType.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/datajob/DataJobType.java index 673da8786a47d..3844b962b8808 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/datajob/DataJobType.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/datajob/DataJobType.java @@ -69,7 +69,8 @@ public class DataJobType implements SearchableEntityType, BrowsableEnti GLOBAL_TAGS_ASPECT_NAME, GLOSSARY_TERMS_ASPECT_NAME, STATUS_ASPECT_NAME, - DOMAINS_ASPECT_NAME + DOMAINS_ASPECT_NAME, + DEPRECATION_ASPECT_NAME ); private static final Set FACET_FIELDS = ImmutableSet.of("flow"); private final EntityClient _entityClient; diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/datajob/mappers/DataJobMapper.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/datajob/mappers/DataJobMapper.java index 37b42dcba4629..310a9e565dde1 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/datajob/mappers/DataJobMapper.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/datajob/mappers/DataJobMapper.java @@ -1,6 +1,7 @@ package com.linkedin.datahub.graphql.types.datajob.mappers; import com.google.common.collect.ImmutableList; +import com.linkedin.common.Deprecation; import com.linkedin.common.GlobalTags; import com.linkedin.common.GlossaryTerms; import com.linkedin.common.InstitutionalMemory; @@ -16,6 +17,7 @@ import com.linkedin.datahub.graphql.generated.Dataset; import com.linkedin.datahub.graphql.generated.Domain; import com.linkedin.datahub.graphql.generated.EntityType; +import com.linkedin.datahub.graphql.types.common.mappers.DeprecationMapper; import com.linkedin.datahub.graphql.types.common.mappers.InstitutionalMemoryMapper; import com.linkedin.datahub.graphql.types.common.mappers.OwnershipMapper; import com.linkedin.datahub.graphql.types.common.mappers.StatusMapper; @@ -85,6 +87,8 @@ public DataJob apply(@Nonnull final EntityResponse entityResponse) { .setType(EntityType.DOMAIN) .setUrn(domains.getDomains().get(0).toString()).build()); } + } else if (DEPRECATION_ASPECT_NAME.equals(name)) { + result.setDeprecation(DeprecationMapper.map(new Deprecation(data))); } }); diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/dataset/DatasetType.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/dataset/DatasetType.java index ef8b1ff4ddff8..e5fc30d66f9f5 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/dataset/DatasetType.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/dataset/DatasetType.java @@ -62,7 +62,8 @@ public class DatasetType implements SearchableEntityType, BrowsableEnti DATASET_KEY_ASPECT_NAME, DATASET_PROPERTIES_ASPECT_NAME, EDITABLE_DATASET_PROPERTIES_ASPECT_NAME, - DATASET_DEPRECATION_ASPECT_NAME, + DATASET_DEPRECATION_ASPECT_NAME, // This aspect is deprecated. + DEPRECATION_ASPECT_NAME, DATASET_UPSTREAM_LINEAGE_ASPECT_NAME, UPSTREAM_LINEAGE_ASPECT_NAME, EDITABLE_SCHEMA_METADATA_ASPECT_NAME, diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/dataset/mappers/DatasetMapper.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/dataset/mappers/DatasetMapper.java index 2290c41177763..1becfd50b0466 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/dataset/mappers/DatasetMapper.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/dataset/mappers/DatasetMapper.java @@ -1,5 +1,6 @@ package com.linkedin.datahub.graphql.types.dataset.mappers; +import com.linkedin.common.Deprecation; import com.linkedin.common.GlobalTags; import com.linkedin.common.GlossaryTerms; import com.linkedin.common.InstitutionalMemory; @@ -13,6 +14,7 @@ import com.linkedin.datahub.graphql.generated.Domain; import com.linkedin.datahub.graphql.generated.EntityType; import com.linkedin.datahub.graphql.generated.FabricType; +import com.linkedin.datahub.graphql.types.common.mappers.DeprecationMapper; import com.linkedin.datahub.graphql.types.common.mappers.InstitutionalMemoryMapper; import com.linkedin.datahub.graphql.types.common.mappers.OwnershipMapper; import com.linkedin.datahub.graphql.types.common.mappers.StatusMapper; @@ -126,6 +128,13 @@ public Dataset apply(@Nonnull final EntityResponse entityResponse) { .setType(EntityType.DOMAIN) .setUrn(domains.getDomains().get(0).toString()).build()); } + } else if (DEPRECATION_ASPECT_NAME.equals(name)) { + if (result.getDeprecation() == null) { + // If deprecation has not already been populated by the legacy + // 'datasetDeprecation' aspect, set it. If it's already been set, + // use the new Deprecation aspect. + result.setDeprecation(DeprecationMapper.map(new Deprecation(data))); + } } }); return result; diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/glossary/GlossaryTermType.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/glossary/GlossaryTermType.java index c67bd339237a1..961b943461885 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/glossary/GlossaryTermType.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/glossary/GlossaryTermType.java @@ -49,7 +49,8 @@ public class GlossaryTermType implements SearchableEntityType, Bro GLOSSARY_RELATED_TERM_ASPECT_NAME, OWNERSHIP_ASPECT_NAME, STATUS_ASPECT_NAME, - BROWSE_PATHS_ASPECT_NAME + BROWSE_PATHS_ASPECT_NAME, + DEPRECATION_ASPECT_NAME ); private final EntityClient _entityClient; diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/glossary/mappers/GlossaryTermMapper.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/glossary/mappers/GlossaryTermMapper.java index d5f2d80c5b4f0..94d5a4722a57f 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/glossary/mappers/GlossaryTermMapper.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/glossary/mappers/GlossaryTermMapper.java @@ -1,9 +1,11 @@ package com.linkedin.datahub.graphql.types.glossary.mappers; +import com.linkedin.common.Deprecation; import com.linkedin.common.Ownership; import com.linkedin.data.DataMap; import com.linkedin.datahub.graphql.generated.EntityType; import com.linkedin.datahub.graphql.generated.GlossaryTerm; +import com.linkedin.datahub.graphql.types.common.mappers.DeprecationMapper; import com.linkedin.datahub.graphql.types.common.mappers.OwnershipMapper; import com.linkedin.datahub.graphql.types.glossary.GlossaryTermUtils; import com.linkedin.datahub.graphql.types.mappers.ModelMapper; @@ -42,6 +44,8 @@ public GlossaryTerm apply(@Nonnull final EntityResponse entityResponse) { result.setGlossaryTermInfo(GlossaryTermInfoMapper.map(new GlossaryTermInfo(data))); } else if (OWNERSHIP_ASPECT_NAME.equals(name)) { result.setOwnership(OwnershipMapper.map(new Ownership(data))); + } else if (DEPRECATION_ASPECT_NAME.equals(name)) { + result.setDeprecation(DeprecationMapper.map(new Deprecation(data))); } }); return result; diff --git a/datahub-graphql-core/src/main/resources/entity.graphql b/datahub-graphql-core/src/main/resources/entity.graphql index 5ad532f2b8ed1..8b8ddc5c5516a 100644 --- a/datahub-graphql-core/src/main/resources/entity.graphql +++ b/datahub-graphql-core/src/main/resources/entity.graphql @@ -261,6 +261,13 @@ type Mutation { """ unsetDomain(entityUrn: String!): Boolean + """ + Sets the Deprecation status for a Metadata Entity. Requires the Edit Deprecation status privilege for an entity. + """ + updateDeprecation( + "Input required to set deprecation for an Entity." + input: UpdateDeprecationInput!): Boolean + """ Update a particular Corp User's editable properties """ @@ -570,7 +577,7 @@ type Dataset implements EntityWithRelationships & Entity { ownership: Ownership """ - The deprecation status + The deprecation status of the dataset """ deprecation: Deprecation @@ -816,6 +823,11 @@ type GlossaryTerm implements Entity { """ glossaryTermInfo: GlossaryTermInfo + """ + The deprecation status of the Glossary Term + """ + deprecation: Deprecation + """ Edges extending from this entity """ @@ -1188,6 +1200,11 @@ type Container implements Entity { """ domain: Domain + """ + The deprecation status of the dashboard + """ + deprecation: Deprecation + """ Children entities inside of the Container """ @@ -2945,6 +2962,11 @@ type Dashboard implements EntityWithRelationships & Entity { """ status: Status + """ + The deprecation status of the dashboard + """ + deprecation: Deprecation + """ The tags associated with the dashboard """ @@ -3158,6 +3180,11 @@ type Chart implements EntityWithRelationships & Entity { """ status: Status + """ + The deprecation status of the chart + """ + deprecation: Deprecation + """ The tags associated with the chart """ @@ -3475,6 +3502,11 @@ type DataFlow implements Entity { """ status: Status + """ + The deprecation status of the Data Flow + """ + deprecation: Deprecation + """ References to internal resources related to the dashboard """ @@ -3631,6 +3663,11 @@ type DataJob implements EntityWithRelationships & Entity { """ status: Status + """ + The deprecation status of the Data Flow + """ + deprecation: Deprecation + """ References to internal resources related to the dashboard """ @@ -4133,7 +4170,7 @@ type Deprecation { """ Additional information about the entity deprecation plan """ - note: String! + note: String """ The user who will be credited for modifying this deprecation content @@ -4311,6 +4348,31 @@ input DescriptionUpdateInput { subResource: String } +""" +Input provided when setting the Deprecation status for an Entity. +""" +input UpdateDeprecationInput { + """ + The urn of the Entity to set deprecation for. + """ + urn: String! + + """ + Whether the Entity is marked as deprecated. + """ + deprecated: Boolean! + + """ + Optional - The time user plan to decommission this entity + """ + decommissionTime: Long + + """ + Optional - Additional information about the entity deprecation plan + """ + note: String +} + """ Input provided when creating or updating an Access Policy """ diff --git a/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/domain/DomainTestUtils.java b/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/TestUtils.java similarity index 90% rename from datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/domain/DomainTestUtils.java rename to datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/TestUtils.java index d37d24bcae6f8..ef0cc566c575e 100644 --- a/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/domain/DomainTestUtils.java +++ b/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/TestUtils.java @@ -1,13 +1,12 @@ -package com.linkedin.datahub.graphql.resolvers.domain; +package com.linkedin.datahub.graphql; import com.datahub.authentication.Authentication; import com.datahub.authorization.AuthorizationResult; import com.datahub.authorization.Authorizer; -import com.linkedin.datahub.graphql.QueryContext; import org.mockito.Mockito; -public class DomainTestUtils { +public class TestUtils { public static QueryContext getMockAllowContext() { QueryContext mockContext = Mockito.mock(QueryContext.class); @@ -37,5 +36,5 @@ public static QueryContext getMockDenyContext() { return mockContext; } - private DomainTestUtils() { } + private TestUtils() { } } diff --git a/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/deprecation/UpdateDeprecationResolverTest.java b/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/deprecation/UpdateDeprecationResolverTest.java new file mode 100644 index 0000000000000..6d7000e393679 --- /dev/null +++ b/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/deprecation/UpdateDeprecationResolverTest.java @@ -0,0 +1,218 @@ +package com.linkedin.datahub.graphql.resolvers.deprecation; + +import com.datahub.authentication.Authentication; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.linkedin.common.Deprecation; +import com.linkedin.common.urn.CorpuserUrn; +import com.linkedin.common.urn.Urn; +import com.linkedin.datahub.graphql.QueryContext; +import com.linkedin.datahub.graphql.generated.UpdateDeprecationInput; +import com.linkedin.entity.Aspect; +import com.linkedin.entity.EntityResponse; +import com.linkedin.entity.EnvelopedAspect; +import com.linkedin.entity.EnvelopedAspectMap; +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.utils.GenericAspectUtils; +import com.linkedin.mxe.MetadataChangeProposal; +import com.linkedin.r2.RemoteInvocationException; +import graphql.schema.DataFetchingEnvironment; +import java.util.Collections; +import java.util.HashSet; +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 UpdateDeprecationResolverTest { + + private static final String TEST_ENTITY_URN = "urn:li:dataset:(urn:li:dataPlatform:mysql,my-test,PROD)"; + private static final UpdateDeprecationInput TEST_DEPRECATION_INPUT = new UpdateDeprecationInput( + TEST_ENTITY_URN, + true, + 0L, + "Test note" + ); + private static final CorpuserUrn TEST_ACTOR_URN = new CorpuserUrn("test"); + + @Test + public void testGetSuccessNoExistingDeprecation() throws Exception { + // Create resolver + EntityClient mockClient = Mockito.mock(EntityClient.class); + + Mockito.when(mockClient.batchGetV2( + Mockito.eq(Constants.DATASET_ENTITY_NAME), + Mockito.eq(new HashSet<>(ImmutableSet.of(Urn.createFromString(TEST_ENTITY_URN)))), + Mockito.eq(ImmutableSet.of(Constants.DEPRECATION_ASPECT_NAME)), + Mockito.any(Authentication.class))) + .thenReturn(ImmutableMap.of(Urn.createFromString(TEST_ENTITY_URN), + new EntityResponse() + .setEntityName(Constants.DATASET_ENTITY_NAME) + .setUrn(Urn.createFromString(TEST_ENTITY_URN)) + .setAspects(new EnvelopedAspectMap(Collections.emptyMap())))); + + EntityService mockService = Mockito.mock(EntityService.class); + Mockito.when(mockService.exists(Urn.createFromString(TEST_ENTITY_URN))).thenReturn(true); + + UpdateDeprecationResolver resolver = new UpdateDeprecationResolver(mockClient, mockService); + + // Execute resolver + QueryContext mockContext = getMockAllowContext(); + Mockito.when(mockContext.getActorUrn()).thenReturn(TEST_ACTOR_URN.toString()); + DataFetchingEnvironment mockEnv = Mockito.mock(DataFetchingEnvironment.class); + Mockito.when(mockEnv.getArgument(Mockito.eq("input"))).thenReturn(TEST_DEPRECATION_INPUT); + Mockito.when(mockEnv.getContext()).thenReturn(mockContext); + resolver.get(mockEnv).get(); + + final Deprecation newDeprecation = new Deprecation().setDeprecated(true).setDecommissionTime(0L).setNote("Test note").setActor(TEST_ACTOR_URN); + final MetadataChangeProposal proposal = new MetadataChangeProposal(); + proposal.setEntityUrn(Urn.createFromString(TEST_ENTITY_URN)); + proposal.setEntityType(Constants.DATASET_ENTITY_NAME); + proposal.setAspectName(Constants.DEPRECATION_ASPECT_NAME); + proposal.setAspect(GenericAspectUtils.serializeAspect(newDeprecation)); + proposal.setChangeType(ChangeType.UPSERT); + + Mockito.verify(mockClient, Mockito.times(1)).ingestProposal( + Mockito.eq(proposal), + Mockito.any(Authentication.class) + ); + + Mockito.verify(mockService, Mockito.times(1)).exists( + Mockito.eq(Urn.createFromString(TEST_ENTITY_URN)) + ); + } + + @Test + public void testGetSuccessExistingDeprecation() throws Exception { + Deprecation originalDeprecation = new Deprecation().setDeprecated(false).setDecommissionTime(1L).setActor(TEST_ACTOR_URN).setNote(""); + + // Create resolver + EntityClient mockClient = Mockito.mock(EntityClient.class); + + Mockito.when(mockClient.batchGetV2( + Mockito.eq(Constants.DATASET_ENTITY_NAME), + Mockito.eq(new HashSet<>(ImmutableSet.of(Urn.createFromString(TEST_ENTITY_URN)))), + Mockito.eq(ImmutableSet.of(Constants.DEPRECATION_ASPECT_NAME)), + Mockito.any(Authentication.class))) + .thenReturn(ImmutableMap.of(Urn.createFromString(TEST_ENTITY_URN), + new EntityResponse() + .setEntityName(Constants.DATASET_ENTITY_NAME) + .setUrn(Urn.createFromString(TEST_ENTITY_URN)) + .setAspects(new EnvelopedAspectMap(ImmutableMap.of( + Constants.DEPRECATION_ASPECT_NAME, + new EnvelopedAspect().setValue(new Aspect(originalDeprecation.data())) + ))))); + + EntityService mockService = Mockito.mock(EntityService.class); + Mockito.when(mockService.exists(Urn.createFromString(TEST_ENTITY_URN))).thenReturn(true); + + UpdateDeprecationResolver resolver = new UpdateDeprecationResolver(mockClient, mockService); + + // Execute resolver + QueryContext mockContext = getMockAllowContext(); + Mockito.when(mockContext.getActorUrn()).thenReturn(TEST_ACTOR_URN.toString()); + DataFetchingEnvironment mockEnv = Mockito.mock(DataFetchingEnvironment.class); + Mockito.when(mockEnv.getArgument(Mockito.eq("input"))).thenReturn(TEST_DEPRECATION_INPUT); + Mockito.when(mockEnv.getContext()).thenReturn(mockContext); + resolver.get(mockEnv).get(); + + final Deprecation newDeprecation = new Deprecation() + .setDeprecated(true) + .setDecommissionTime(0L) + .setNote("Test note") + .setActor(TEST_ACTOR_URN); + final MetadataChangeProposal proposal = new MetadataChangeProposal(); + proposal.setEntityUrn(Urn.createFromString(TEST_ENTITY_URN)); + proposal.setEntityType(Constants.DATASET_ENTITY_NAME); + proposal.setAspectName(Constants.DEPRECATION_ASPECT_NAME); + proposal.setAspect(GenericAspectUtils.serializeAspect(newDeprecation)); + proposal.setChangeType(ChangeType.UPSERT); + + Mockito.verify(mockClient, Mockito.times(1)).ingestProposal( + Mockito.eq(proposal), + Mockito.any(Authentication.class) + ); + + Mockito.verify(mockService, Mockito.times(1)).exists( + Mockito.eq(Urn.createFromString(TEST_ENTITY_URN)) + ); + + } + + @Test + public void testGetFailureEntityDoesNotExist() throws Exception { + // Create resolver + EntityClient mockClient = Mockito.mock(EntityClient.class); + + Mockito.when(mockClient.batchGetV2( + Mockito.eq(Constants.DATASET_ENTITY_NAME), + Mockito.eq(new HashSet<>(ImmutableSet.of(Urn.createFromString(TEST_ENTITY_URN)))), + Mockito.eq(ImmutableSet.of(Constants.DEPRECATION_ASPECT_NAME)), + Mockito.any(Authentication.class))) + .thenReturn(ImmutableMap.of(Urn.createFromString(TEST_ENTITY_URN), + new EntityResponse() + .setEntityName(Constants.DEPRECATION_ASPECT_NAME) + .setUrn(Urn.createFromString(TEST_ENTITY_URN)) + .setAspects(new EnvelopedAspectMap(Collections.emptyMap())))); + + EntityService mockService = Mockito.mock(EntityService.class); + Mockito.when(mockService.exists(Urn.createFromString(TEST_ENTITY_URN))).thenReturn(false); + + UpdateDeprecationResolver resolver = new UpdateDeprecationResolver(mockClient, mockService); + + // Execute resolver + QueryContext mockContext = getMockAllowContext(); + Mockito.when(mockContext.getActorUrn()).thenReturn(TEST_ACTOR_URN.toString()); + DataFetchingEnvironment mockEnv = Mockito.mock(DataFetchingEnvironment.class); + Mockito.when(mockEnv.getArgument(Mockito.eq("input"))).thenReturn(TEST_DEPRECATION_INPUT); + Mockito.when(mockEnv.getContext()).thenReturn(mockContext); + + assertThrows(CompletionException.class, () -> resolver.get(mockEnv).join()); + Mockito.verify(mockClient, Mockito.times(0)).ingestProposal( + Mockito.any(), + Mockito.any(Authentication.class)); + } + + @Test + public void testGetUnauthorized() throws Exception { + // Create resolver + EntityClient mockClient = Mockito.mock(EntityClient.class); + EntityService mockService = Mockito.mock(EntityService.class); + UpdateDeprecationResolver resolver = new UpdateDeprecationResolver(mockClient, mockService); + + // Execute resolver + DataFetchingEnvironment mockEnv = Mockito.mock(DataFetchingEnvironment.class); + Mockito.when(mockEnv.getArgument(Mockito.eq("input"))).thenReturn(TEST_DEPRECATION_INPUT); + QueryContext mockContext = getMockDenyContext(); + Mockito.when(mockEnv.getContext()).thenReturn(mockContext); + + assertThrows(CompletionException.class, () -> resolver.get(mockEnv).join()); + Mockito.verify(mockClient, Mockito.times(0)).ingestProposal( + Mockito.any(), + Mockito.any(Authentication.class)); + } + + @Test + public void testGetEntityClientException() throws Exception { + EntityClient mockClient = Mockito.mock(EntityClient.class); + EntityService mockService = Mockito.mock(EntityService.class); + Mockito.doThrow(RemoteInvocationException.class).when(mockClient).ingestProposal( + Mockito.any(), + Mockito.any(Authentication.class)); + UpdateDeprecationResolver resolver = new UpdateDeprecationResolver(mockClient, mockService); + + // Execute resolver + DataFetchingEnvironment mockEnv = Mockito.mock(DataFetchingEnvironment.class); + QueryContext mockContext = getMockAllowContext(); + Mockito.when(mockEnv.getArgument(Mockito.eq("input"))).thenReturn(TEST_DEPRECATION_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/domain/CreateDomainResolverTest.java b/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/domain/CreateDomainResolverTest.java index 3cfcc177438c9..75cf66f1351c6 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 @@ -16,7 +16,7 @@ import org.mockito.Mockito; import org.testng.annotations.Test; -import static com.linkedin.datahub.graphql.resolvers.domain.DomainTestUtils.*; +import static com.linkedin.datahub.graphql.TestUtils.*; import static org.testng.Assert.*; diff --git a/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/domain/ListDomainsResolverTest.java b/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/domain/ListDomainsResolverTest.java index c2cef0ecbe1f5..cf0cfef218259 100644 --- a/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/domain/ListDomainsResolverTest.java +++ b/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/domain/ListDomainsResolverTest.java @@ -16,7 +16,7 @@ import org.mockito.Mockito; import org.testng.annotations.Test; -import static com.linkedin.datahub.graphql.resolvers.domain.DomainTestUtils.*; +import static com.linkedin.datahub.graphql.TestUtils.*; import static org.testng.Assert.*; diff --git a/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/domain/SetDomainResolverTest.java b/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/domain/SetDomainResolverTest.java index 842516057834f..1455b7ca78ff6 100644 --- a/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/domain/SetDomainResolverTest.java +++ b/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/domain/SetDomainResolverTest.java @@ -26,7 +26,7 @@ import org.mockito.Mockito; import org.testng.annotations.Test; -import static com.linkedin.datahub.graphql.resolvers.domain.DomainTestUtils.*; +import static com.linkedin.datahub.graphql.TestUtils.*; import static org.testng.Assert.*; diff --git a/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/domain/UnsetDomainResolverTest.java b/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/domain/UnsetDomainResolverTest.java index 29939eabc321d..916ae30ac02be 100644 --- a/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/domain/UnsetDomainResolverTest.java +++ b/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/domain/UnsetDomainResolverTest.java @@ -26,7 +26,7 @@ import org.mockito.Mockito; import org.testng.annotations.Test; -import static com.linkedin.datahub.graphql.resolvers.domain.DomainTestUtils.*; +import static com.linkedin.datahub.graphql.TestUtils.*; import static org.testng.Assert.*; diff --git a/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/types/domain/DomainTypeTest.java b/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/types/domain/DomainTypeTest.java index 2e3bfec32841b..48c23f436f875 100644 --- a/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/types/domain/DomainTypeTest.java +++ b/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/types/domain/DomainTypeTest.java @@ -33,7 +33,7 @@ import org.testng.annotations.Test; -import static com.linkedin.datahub.graphql.resolvers.domain.DomainTestUtils.*; +import static com.linkedin.datahub.graphql.TestUtils.*; import static org.testng.Assert.*; public class DomainTypeTest { diff --git a/metadata-models/src/main/pegasus/com/linkedin/common/Deprecation.pdl b/metadata-models/src/main/pegasus/com/linkedin/common/Deprecation.pdl index 4988c4d37f289..f96b9da45cdf7 100644 --- a/metadata-models/src/main/pegasus/com/linkedin/common/Deprecation.pdl +++ b/metadata-models/src/main/pegasus/com/linkedin/common/Deprecation.pdl @@ -28,7 +28,7 @@ record Deprecation { note: string /** - * The corpuser URN which will be credited for modifying this deprecation content. + * The user URN which will be credited for modifying this deprecation content. */ - actor: CorpuserUrn + actor: Urn } \ No newline at end of file diff --git a/metadata-models/src/main/pegasus/com/linkedin/common/Status.pdl b/metadata-models/src/main/pegasus/com/linkedin/common/Status.pdl index 74043a96919ad..a3fb9eac870b5 100644 --- a/metadata-models/src/main/pegasus/com/linkedin/common/Status.pdl +++ b/metadata-models/src/main/pegasus/com/linkedin/common/Status.pdl @@ -1,16 +1,15 @@ namespace com.linkedin.common /** - * The status metadata of an entity, e.g. dataset, metric, feature, etc. + * The lifecycle status metadata of an entity, e.g. dataset, metric, feature, etc. * This aspect is used to represent soft deletes conventionally. */ @Aspect = { "name": "status" } record Status { - /** - * whether the entity is removed or not + * Whether the entity has been removed (soft-deleted). */ @Searchable = { "fieldType": "BOOLEAN" diff --git a/metadata-models/src/main/pegasus/com/linkedin/dataset/DatasetDeprecation.pdl b/metadata-models/src/main/pegasus/com/linkedin/dataset/DatasetDeprecation.pdl index c5c9033212476..853baec56c906 100644 --- a/metadata-models/src/main/pegasus/com/linkedin/dataset/DatasetDeprecation.pdl +++ b/metadata-models/src/main/pegasus/com/linkedin/dataset/DatasetDeprecation.pdl @@ -4,10 +4,12 @@ import com.linkedin.common.Urn /** * Dataset deprecation status + * Deprecated! This aspect is deprecated in favor of the more-general-purpose 'Deprecation' aspect. */ @Aspect = { "name": "datasetDeprecation" } +@Deprecated record DatasetDeprecation { /** diff --git a/metadata-models/src/main/resources/entity-registry.yml b/metadata-models/src/main/resources/entity-registry.yml index 70c89f0dec8ce..cd0d74bb1e994 100644 --- a/metadata-models/src/main/resources/entity-registry.yml +++ b/metadata-models/src/main/resources/entity-registry.yml @@ -13,6 +13,7 @@ entities: - status - container - assertionRunEvent + - deprecation - name: dataHubPolicy doc: DataHub Policies represent access policies granted to users or groups on metadata operations like edit, view etc. keyAspect: dataHubPolicyKey @@ -24,20 +25,24 @@ entities: - datahubIngestionRunSummary - datahubIngestionCheckpoint - domains + - deprecation - name: dataFlow keyAspect: dataFlowKey aspects: - domains + - deprecation - name: chart keyAspect: chartKey aspects: - domains - container + - deprecation - name: dashboard keyAspect: dashboardKey aspects: - domains - container + - deprecation - name: corpuser doc: CorpUser represents an identity of a person (or an account) in the enterprise. keyAspect: corpUserKey @@ -61,10 +66,12 @@ entities: - browsePaths # unclear if this will be used - status - domains + - deprecation - name: glossaryTerm keyAspect: glossaryTermKey aspects: - schemaMetadata + - deprecation - name: domain doc: A data domain within an organization. keyAspect: domainKey diff --git a/metadata-service/restli-api/src/main/snapshot/com.linkedin.entity.aspects.snapshot.json b/metadata-service/restli-api/src/main/snapshot/com.linkedin.entity.aspects.snapshot.json index 10ddf1ba30a79..2fcd7010882c6 100644 --- a/metadata-service/restli-api/src/main/snapshot/com.linkedin.entity.aspects.snapshot.json +++ b/metadata-service/restli-api/src/main/snapshot/com.linkedin.entity.aspects.snapshot.json @@ -1084,11 +1084,11 @@ "type" : "record", "name" : "Status", "namespace" : "com.linkedin.common", - "doc" : "The status metadata of an entity, e.g. dataset, metric, feature, etc.\nThis aspect is used to represent soft deletes conventionally.", + "doc" : "The lifecycle status metadata of an entity, e.g. dataset, metric, feature, etc.\nThis aspect is used to represent soft deletes conventionally.", "fields" : [ { "name" : "removed", "type" : "boolean", - "doc" : "whether the entity is removed or not", + "doc" : "Whether the entity has been removed (soft-deleted).", "default" : false, "Searchable" : { "fieldType" : "BOOLEAN" @@ -1358,7 +1358,7 @@ "type" : "record", "name" : "DatasetDeprecation", "namespace" : "com.linkedin.dataset", - "doc" : "Dataset deprecation status", + "doc" : "Dataset deprecation status\nDeprecated! This aspect is deprecated in favor of the more-general-purpose 'Deprecation' aspect.", "fields" : [ { "name" : "deprecated", "type" : "boolean", @@ -1386,7 +1386,8 @@ } ], "Aspect" : { "name" : "datasetDeprecation" - } + }, + "Deprecated" : true }, { "type" : "enum", "name" : "DatasetLineageType", @@ -1521,6 +1522,7 @@ "name" : "GlossaryTermInfo", "namespace" : "com.linkedin.glossary", "doc" : "Properties associated with a GlossaryTerm", + "include" : [ "com.linkedin.common.CustomProperties" ], "fields" : [ { "name" : "definition", "type" : "string", @@ -1551,14 +1553,6 @@ "type" : "com.linkedin.common.Url", "doc" : "The abstracted URL such as https://spec.edmcouncil.org/fibo/ontology/FBC/FinancialInstruments/FinancialInstruments/CashInstrument.", "optional" : true - }, { - "name" : "customProperties", - "type" : { - "type" : "map", - "values" : "string" - }, - "doc" : "A key-value map to capture any other non-standardized properties for the glossary term", - "default" : { } }, { "name" : "rawSchema", "type" : "string", diff --git a/metadata-service/restli-api/src/main/snapshot/com.linkedin.entity.entities.snapshot.json b/metadata-service/restli-api/src/main/snapshot/com.linkedin.entity.entities.snapshot.json index 3496452a34bba..fadc939f9efe7 100644 --- a/metadata-service/restli-api/src/main/snapshot/com.linkedin.entity.entities.snapshot.json +++ b/metadata-service/restli-api/src/main/snapshot/com.linkedin.entity.entities.snapshot.json @@ -710,8 +710,8 @@ "doc" : "Additional information about the entity deprecation plan, such as the wiki, doc, RB." }, { "name" : "actor", - "type" : "CorpuserUrn", - "doc" : "The corpuser URN which will be credited for modifying this deprecation content." + "type" : "Urn", + "doc" : "The user URN which will be credited for modifying this deprecation content." } ], "Aspect" : { "name" : "deprecation" @@ -1110,11 +1110,11 @@ "type" : "record", "name" : "Status", "namespace" : "com.linkedin.common", - "doc" : "The status metadata of an entity, e.g. dataset, metric, feature, etc.\nThis aspect is used to represent soft deletes conventionally.", + "doc" : "The lifecycle status metadata of an entity, e.g. dataset, metric, feature, etc.\nThis aspect is used to represent soft deletes conventionally.", "fields" : [ { "name" : "removed", "type" : "boolean", - "doc" : "whether the entity is removed or not", + "doc" : "Whether the entity has been removed (soft-deleted).", "default" : false, "Searchable" : { "fieldType" : "BOOLEAN" @@ -1546,7 +1546,7 @@ "type" : "record", "name" : "DatasetDeprecation", "namespace" : "com.linkedin.dataset", - "doc" : "Dataset deprecation status", + "doc" : "Dataset deprecation status\nDeprecated! This aspect is deprecated in favor of the more-general-purpose 'Deprecation' aspect.", "fields" : [ { "name" : "deprecated", "type" : "boolean", @@ -1574,7 +1574,8 @@ } ], "Aspect" : { "name" : "datasetDeprecation" - } + }, + "Deprecated" : true }, { "type" : "record", "name" : "DatasetFieldMapping", @@ -4128,6 +4129,7 @@ "name" : "GlossaryTermInfo", "namespace" : "com.linkedin.glossary", "doc" : "Properties associated with a GlossaryTerm", + "include" : [ "com.linkedin.common.CustomProperties" ], "fields" : [ { "name" : "definition", "type" : "string", @@ -4158,14 +4160,6 @@ "type" : "com.linkedin.common.Url", "doc" : "The abstracted URL such as https://spec.edmcouncil.org/fibo/ontology/FBC/FinancialInstruments/FinancialInstruments/CashInstrument.", "optional" : true - }, { - "name" : "customProperties", - "type" : { - "type" : "map", - "values" : "string" - }, - "doc" : "A key-value map to capture any other non-standardized properties for the glossary term", - "default" : { } }, { "name" : "rawSchema", "type" : "string", diff --git a/metadata-service/restli-api/src/main/snapshot/com.linkedin.entity.runs.snapshot.json b/metadata-service/restli-api/src/main/snapshot/com.linkedin.entity.runs.snapshot.json index ab5006e3bc189..b8f7949451150 100644 --- a/metadata-service/restli-api/src/main/snapshot/com.linkedin.entity.runs.snapshot.json +++ b/metadata-service/restli-api/src/main/snapshot/com.linkedin.entity.runs.snapshot.json @@ -854,11 +854,11 @@ "type" : "record", "name" : "Status", "namespace" : "com.linkedin.common", - "doc" : "The status metadata of an entity, e.g. dataset, metric, feature, etc.\nThis aspect is used to represent soft deletes conventionally.", + "doc" : "The lifecycle status metadata of an entity, e.g. dataset, metric, feature, etc.\nThis aspect is used to represent soft deletes conventionally.", "fields" : [ { "name" : "removed", "type" : "boolean", - "doc" : "whether the entity is removed or not", + "doc" : "Whether the entity has been removed (soft-deleted).", "default" : false, "Searchable" : { "fieldType" : "BOOLEAN" @@ -1128,7 +1128,7 @@ "type" : "record", "name" : "DatasetDeprecation", "namespace" : "com.linkedin.dataset", - "doc" : "Dataset deprecation status", + "doc" : "Dataset deprecation status\nDeprecated! This aspect is deprecated in favor of the more-general-purpose 'Deprecation' aspect.", "fields" : [ { "name" : "deprecated", "type" : "boolean", @@ -1156,7 +1156,8 @@ } ], "Aspect" : { "name" : "datasetDeprecation" - } + }, + "Deprecated" : true }, { "type" : "enum", "name" : "DatasetLineageType", @@ -1278,6 +1279,7 @@ "name" : "GlossaryTermInfo", "namespace" : "com.linkedin.glossary", "doc" : "Properties associated with a GlossaryTerm", + "include" : [ "com.linkedin.common.CustomProperties" ], "fields" : [ { "name" : "definition", "type" : "string", @@ -1308,14 +1310,6 @@ "type" : "com.linkedin.common.Url", "doc" : "The abstracted URL such as https://spec.edmcouncil.org/fibo/ontology/FBC/FinancialInstruments/FinancialInstruments/CashInstrument.", "optional" : true - }, { - "name" : "customProperties", - "type" : { - "type" : "map", - "values" : "string" - }, - "doc" : "A key-value map to capture any other non-standardized properties for the glossary term", - "default" : { } }, { "name" : "rawSchema", "type" : "string", diff --git a/metadata-service/war/src/main/resources/boot/policies.json b/metadata-service/war/src/main/resources/boot/policies.json index f12ef64c76105..b001640e2762f 100644 --- a/metadata-service/war/src/main/resources/boot/policies.json +++ b/metadata-service/war/src/main/resources/boot/policies.json @@ -221,5 +221,30 @@ "type":"METADATA", "editable":false } + }, + { + "urn": "urn:li:dataHubPolicy:9", + "info": { + "actors":{ + "resourceOwners":false, + "allUsers":false, + "allGroups":false, + "users":[ + "urn:li:corpuser:datahub" + ] + }, + "privileges":[ + "EDIT_ENTITY" + ], + "displayName":"Root User - Edit All Glossary Terms", + "resources":{ + "type":"glossaryTerm", + "allResources":true + }, + "description":"Grants full edit privileges for Glossary Terms to root 'datahub' root user.", + "state":"ACTIVE", + "type":"METADATA", + "editable":false + } } ] diff --git a/metadata-utils/src/main/java/com/linkedin/metadata/Constants.java b/metadata-utils/src/main/java/com/linkedin/metadata/Constants.java index 27a21e0399e32..548d871c674ad 100644 --- a/metadata-utils/src/main/java/com/linkedin/metadata/Constants.java +++ b/metadata-utils/src/main/java/com/linkedin/metadata/Constants.java @@ -42,6 +42,7 @@ public class Constants { public static final String GLOSSARY_TERMS_ASPECT_NAME = "glossaryTerms"; public static final String STATUS_ASPECT_NAME = "status"; public static final String SUB_TYPES_ASPECT_NAME = "subTypes"; + public static final String DEPRECATION_ASPECT_NAME = "deprecation"; // User public static final String CORP_USER_KEY_ASPECT_NAME = "corpUserKey"; 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 ca71d77164f7c..f94ac66bdba9b 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 @@ -106,6 +106,11 @@ public class PoliciesConfig { "Edit Domain", "The ability to edit the Domain of an entity."); + public static final Privilege EDIT_ENTITY_DEPRECATION_PRIVILEGE = Privilege.of( + "EDIT_DEPRECATION_PRIVILEGE", + "Edit Deprecation", + "The ability to edit the Deprecation status of an entity."); + public static final Privilege EDIT_ENTITY_PRIVILEGE = Privilege.of( "EDIT_ENTITY", "Edit All", @@ -119,6 +124,7 @@ public class PoliciesConfig { EDIT_ENTITY_DOC_LINKS_PRIVILEGE, EDIT_ENTITY_STATUS_PRIVILEGE, EDIT_ENTITY_DOMAINS_PRIVILEGE, + EDIT_ENTITY_DEPRECATION_PRIVILEGE, EDIT_ENTITY_PRIVILEGE ); @@ -207,6 +213,19 @@ public class PoliciesConfig { ImmutableList.of(EDIT_ENTITY_OWNERS_PRIVILEGE, EDIT_ENTITY_DOCS_PRIVILEGE, EDIT_ENTITY_DOC_LINKS_PRIVILEGE, EDIT_ENTITY_PRIVILEGE) ); + // Glossary Term Privileges + public static final ResourcePrivileges GLOSSARY_TERM_PRIVILEGES = ResourcePrivileges.of( + "glossaryTerm", + "Glossary Terms", + "Glossary Terms created on DataHub", + ImmutableList.of( + EDIT_ENTITY_OWNERS_PRIVILEGE, + EDIT_ENTITY_DOCS_PRIVILEGE, + EDIT_ENTITY_DOC_LINKS_PRIVILEGE, + EDIT_ENTITY_DEPRECATION_PRIVILEGE, + EDIT_ENTITY_PRIVILEGE) + ); + public static final List RESOURCE_PRIVILEGES = ImmutableList.of( DATASET_PRIVILEGES, DASHBOARD_PRIVILEGES, @@ -215,7 +234,8 @@ public class PoliciesConfig { DATA_JOB_PRIVILEGES, TAG_PRIVILEGES, CONTAINER_PRIVILEGES, - DOMAIN_PRIVILEGES + DOMAIN_PRIVILEGES, + GLOSSARY_TERM_PRIVILEGES ); @Data diff --git a/smoke-test/tests/deprecation/__init__.py b/smoke-test/tests/deprecation/__init__.py new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/smoke-test/tests/deprecation/data.json b/smoke-test/tests/deprecation/data.json new file mode 100644 index 0000000000000..2a0d059ae71dd --- /dev/null +++ b/smoke-test/tests/deprecation/data.json @@ -0,0 +1,24 @@ +[ + { + "auditHeader": null, + "proposedSnapshot": { + "com.linkedin.pegasus2avro.metadata.snapshot.DatasetSnapshot": { + "urn": "urn:li:dataset:(urn:li:dataPlatform:kafka,test-tags-terms-sample-kafka,PROD)", + "aspects": [ + { + "com.linkedin.pegasus2avro.dataset.DatasetProperties": { + "description": null, + "uri": null, + "tags": [], + "customProperties": { + "prop1": "fakeprop", + "prop2": "pikachu" + } + } + } + ] + } + }, + "proposedDelta": null + } +] \ No newline at end of file diff --git a/smoke-test/tests/deprecation/deprecation_test.py b/smoke-test/tests/deprecation/deprecation_test.py new file mode 100644 index 0000000000000..1173ca8d6327d --- /dev/null +++ b/smoke-test/tests/deprecation/deprecation_test.py @@ -0,0 +1,151 @@ +import pytest +import time +from tests.utils import FRONTEND_ENDPOINT +from tests.utils import GMS_ENDPOINT +from tests.utils import ingest_file_via_rest +from tests.utils import delete_urns_from_file + +@pytest.fixture(scope="module", autouse=True) +def ingest_cleanup_data(request): + print("ingesting deprecation test data") + ingest_file_via_rest("tests/deprecation/data.json") + yield + print("removing deprecation test data") + delete_urns_from_file("tests/deprecation/data.json") + +@pytest.mark.dependency() +def test_healthchecks(wait_for_healthchecks): + # Call to wait_for_healthchecks fixture will do the actual functionality. + pass + +@pytest.mark.dependency(depends=["test_healthchecks"]) +def test_update_deprecation_all_fields(frontend_session): + dataset_urn = "urn:li:dataset:(urn:li:dataPlatform:kafka,test-tags-terms-sample-kafka,PROD)" + + dataset_json = { + "query": """query getDataset($urn: String!) {\n + dataset(urn: $urn) {\n + deprecation {\n + deprecated\n + decommissionTime\n + note\n + actor\n + }\n + }\n + }""", + "variables": { + "urn": dataset_urn + } + } + + # Fetch tags + response = frontend_session.post( + f"{FRONTEND_ENDPOINT}/api/v2/graphql", json=dataset_json + ) + response.raise_for_status() + res_data = response.json() + + assert res_data + assert res_data["data"] + assert res_data["data"]["dataset"] + assert res_data["data"]["dataset"]["deprecation"] is None + + update_deprecation_json = { + "query": """mutation updateDeprecation($input: UpdateDeprecationInput!) {\n + updateDeprecation(input: $input) + }""", + "variables": { + "input": { + "urn": dataset_urn, + "deprecated": True, + "note": "My test note", + "decommissionTime": 0 + } + } + } + + response = frontend_session.post( + f"{FRONTEND_ENDPOINT}/api/v2/graphql", json=update_deprecation_json + ) + response.raise_for_status() + res_data = response.json() + + assert res_data + assert res_data["data"] + assert res_data["data"]["updateDeprecation"] is True + + # Refetch the dataset + response = frontend_session.post( + f"{FRONTEND_ENDPOINT}/api/v2/graphql", json=dataset_json + ) + response.raise_for_status() + res_data = response.json() + + assert res_data + assert res_data["data"] + assert res_data["data"]["dataset"] + assert res_data["data"]["dataset"]["deprecation"] == { + 'deprecated': True, + 'decommissionTime': 0, + 'note': 'My test note', + 'actor': 'urn:li:corpuser:datahub' + } + +@pytest.mark.dependency(depends=["test_healthchecks", "test_update_deprecation_all_fields"]) +def test_update_deprecation_partial_fields(frontend_session, ingest_cleanup_data): + dataset_urn = "urn:li:dataset:(urn:li:dataPlatform:kafka,test-tags-terms-sample-kafka,PROD)" + + update_deprecation_json = { + "query": """mutation updateDeprecation($input: UpdateDeprecationInput!) {\n + updateDeprecation(input: $input) + }""", + "variables": { + "input": { + "urn": dataset_urn, + "deprecated": False + } + } + } + + response = frontend_session.post( + f"{FRONTEND_ENDPOINT}/api/v2/graphql", json=update_deprecation_json + ) + response.raise_for_status() + res_data = response.json() + + assert res_data + assert res_data["data"] + assert res_data["data"]["updateDeprecation"] is True + + # Refetch the dataset + dataset_json = { + "query": """query getDataset($urn: String!) {\n + dataset(urn: $urn) {\n + deprecation {\n + deprecated\n + decommissionTime\n + note\n + actor\n + }\n + }\n + }""", + "variables": { + "urn": dataset_urn + } + } + + response = frontend_session.post( + f"{FRONTEND_ENDPOINT}/api/v2/graphql", json=dataset_json + ) + response.raise_for_status() + res_data = response.json() + + assert res_data + assert res_data["data"] + assert res_data["data"]["dataset"] + assert res_data["data"]["dataset"]["deprecation"] == { + 'deprecated': False, + 'note': '', + 'actor': 'urn:li:corpuser:datahub', + 'decommissionTime': None + } \ No newline at end of file