Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ private Constants() {}
public static final String LOGICAL_SCHEMA_FILE = "logical.graphql";
public static final String SETTINGS_SCHEMA_FILE = "settings.graphql";
public static final String FILES_SCHEMA_FILE = "files.graphql";
public static final String KNOWLEDGE_SCHEMA_FILE = "knowledge.graphql";

public static final String QUERY_SCHEMA_FILE = "query.graphql";
public static final String TEMPLATE_SCHEMA_FILE = "template.graphql";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,7 @@
import com.linkedin.datahub.graphql.types.incident.IncidentType;
import com.linkedin.datahub.graphql.types.ingestion.ExecutionRequestType;
import com.linkedin.datahub.graphql.types.ingestion.IngestionSourceType;
import com.linkedin.datahub.graphql.types.knowledge.DocumentType;
import com.linkedin.datahub.graphql.types.mlmodel.MLFeatureTableType;
import com.linkedin.datahub.graphql.types.mlmodel.MLFeatureType;
import com.linkedin.datahub.graphql.types.mlmodel.MLModelGroupType;
Expand Down Expand Up @@ -340,6 +341,7 @@
import com.linkedin.metadata.service.BusinessAttributeService;
import com.linkedin.metadata.service.DataHubFileService;
import com.linkedin.metadata.service.DataProductService;
import com.linkedin.metadata.service.DocumentService;
import com.linkedin.metadata.service.ERModelRelationshipService;
import com.linkedin.metadata.service.FormService;
import com.linkedin.metadata.service.LineageService;
Expand Down Expand Up @@ -423,6 +425,7 @@ public class GmsGraphQLEngine {
private final RestrictedService restrictedService;
private ConnectionService connectionService;
private AssertionService assertionService;
private final DocumentService documentService;
private final EntityVersioningService entityVersioningService;
private final ApplicationService applicationService;
private final PageTemplateService pageTemplateService;
Expand Down Expand Up @@ -469,6 +472,7 @@ public class GmsGraphQLEngine {
private final DataHubConnectionType connectionType;
private final ContainerType containerType;
private final DomainType domainType;
private final DocumentType documentType;
private final NotebookType notebookType;
private final AssertionType assertionType;
private final VersionedDatasetType versionedDatasetType;
Expand Down Expand Up @@ -573,6 +577,7 @@ public GmsGraphQLEngine(final GmsGraphQLEngineArgs args) {
this.restrictedService = args.restrictedService;
this.connectionService = args.connectionService;
this.assertionService = args.assertionService;
this.documentService = args.documentService;
this.entityVersioningService = args.entityVersioningService;

this.businessAttributeService = args.businessAttributeService;
Expand Down Expand Up @@ -612,6 +617,7 @@ public GmsGraphQLEngine(final GmsGraphQLEngineArgs args) {
this.connectionType = new DataHubConnectionType(entityClient, secretService);
this.containerType = new ContainerType(entityClient);
this.domainType = new DomainType(entityClient);
this.documentType = new DocumentType(entityClient);
this.notebookType = new NotebookType(entityClient);
this.assertionType = new AssertionType(entityClient);
this.versionedDatasetType = new VersionedDatasetType(entityClient);
Expand Down Expand Up @@ -671,6 +677,7 @@ public GmsGraphQLEngine(final GmsGraphQLEngineArgs args) {
containerType,
notebookType,
domainType,
documentType,
assertionType,
versionedDatasetType,
dataPlatformInstanceType,
Expand Down Expand Up @@ -774,6 +781,7 @@ public void configureRuntimeWiring(final RuntimeWiring.Builder builder) {
configureOrganisationRoleResolvers(builder);
configureGlossaryNodeResolvers(builder);
configureDomainResolvers(builder);
configureDocumentResolvers(builder);
configureDataProductResolvers(builder);
configureApplicationResolvers(builder);
configureAssertionResolvers(builder);
Expand Down Expand Up @@ -872,7 +880,8 @@ public GraphQLEngine.Builder builder() {
.addSchema(fileBasedSchema(MODULE_SCHEMA_FILE))
.addSchema(fileBasedSchema(PATCH_SCHEMA_FILE))
.addSchema(fileBasedSchema(SETTINGS_SCHEMA_FILE))
.addSchema(fileBasedSchema(FILES_SCHEMA_FILE));
.addSchema(fileBasedSchema(FILES_SCHEMA_FILE))
.addSchema(fileBasedSchema(KNOWLEDGE_SCHEMA_FILE));

for (GmsGraphQLPlugin plugin : this.graphQLPlugins) {
List<String> pluginSchemaFiles = plugin.getSchemaFiles();
Expand Down Expand Up @@ -2952,6 +2961,20 @@ private void configureDomainResolvers(final RuntimeWiring.Builder builder) {
.getUrn())));
}

private void configureDocumentResolvers(final RuntimeWiring.Builder builder) {
// Delegate Knowledge Article wiring to consolidated resolver class
new com.linkedin.datahub.graphql.resolvers.knowledge.DocumentResolvers(
this.documentService,
entityTypes,
documentType,
entityClient,
this.entityService,
this.graphClient,
entityRegistry,
this.timelineService)
.configureResolvers(builder);
}

private void configureFormResolvers(final RuntimeWiring.Builder builder) {
builder.type(
"FormAssociation",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import com.linkedin.metadata.service.BusinessAttributeService;
import com.linkedin.metadata.service.DataHubFileService;
import com.linkedin.metadata.service.DataProductService;
import com.linkedin.metadata.service.DocumentService;
import com.linkedin.metadata.service.ERModelRelationshipService;
import com.linkedin.metadata.service.FormService;
import com.linkedin.metadata.service.LineageService;
Expand Down Expand Up @@ -94,6 +95,7 @@ public class GmsGraphQLEngineArgs {
ChromeExtensionConfiguration chromeExtensionConfiguration;
ConnectionService connectionService;
AssertionService assertionService;
DocumentService documentService;
EntityVersioningService entityVersioningService;
ApplicationService applicationService;
PageTemplateService pageTemplateService;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,82 @@ public static boolean canManageHomePageTemplates(@Nonnull QueryContext context)
context.getOperationContext(), PoliciesConfig.MANAGE_HOME_PAGE_TEMPLATES_PRIVILEGE);
}

/**
* Returns true if the current user is able to create Knowledge Articles. This is true if the user
* has the 'Create Entity' privilege for Knowledge Articles or 'Manage Knowledge Articles'
* platform privilege.
*/
public static boolean canCreateDocument(@Nonnull QueryContext context) {
final DisjunctivePrivilegeGroup orPrivilegeGroups =
new DisjunctivePrivilegeGroup(
ImmutableList.of(
new ConjunctivePrivilegeGroup(
ImmutableList.of(PoliciesConfig.MANAGE_DOCUMENTS_PRIVILEGE.getType()))));

return AuthUtil.isAuthorized(context.getOperationContext(), orPrivilegeGroups, null);
}

/**
* Returns true if the current user is able to edit a specific Document. This is true if the user
* has the 'Edit Entity Docs' or 'Edit Entity' metadata privilege on the document, or the 'Manage
* Documents' platform privilege.
*/
public static boolean canEditDocument(@Nonnull Urn documentUrn, @Nonnull QueryContext context) {
final DisjunctivePrivilegeGroup orPrivilegeGroups =
new DisjunctivePrivilegeGroup(
ImmutableList.of(
new ConjunctivePrivilegeGroup(
ImmutableList.of(PoliciesConfig.EDIT_ENTITY_DOCS_PRIVILEGE.getType())),
new ConjunctivePrivilegeGroup(
ImmutableList.of(PoliciesConfig.EDIT_ENTITY_PRIVILEGE.getType())),
new ConjunctivePrivilegeGroup(
ImmutableList.of(PoliciesConfig.MANAGE_DOCUMENTS_PRIVILEGE.getType()))));

return isAuthorized(
context, documentUrn.getEntityType(), documentUrn.toString(), orPrivilegeGroups);
}

/**
* Returns true if the current user is able to read a specific Document. This is true if the user
* has the 'Get Entity' metadata privilege on the document or the 'Manage Documents' platform
* privilege.
*/
public static boolean canGetDocument(@Nonnull Urn documentUrn, @Nonnull QueryContext context) {
final DisjunctivePrivilegeGroup orPrivilegeGroups =
new DisjunctivePrivilegeGroup(
ImmutableList.of(
new ConjunctivePrivilegeGroup(
ImmutableList.of(PoliciesConfig.VIEW_ENTITY_PAGE_PRIVILEGE.getType())),
new ConjunctivePrivilegeGroup(
ImmutableList.of(PoliciesConfig.MANAGE_DOCUMENTS_PRIVILEGE.getType()))));

return isAuthorized(
context, documentUrn.getEntityType(), documentUrn.toString(), orPrivilegeGroups);
}

/**
* Returns true if the current user is able to delete a specific Document. This is true if the
* user has the delete entity authorization on the document or the 'Manage Documents' platform
* privilege.
*/
public static boolean canDeleteDocument(@Nonnull Urn documentUrn, @Nonnull QueryContext context) {
// Check if user can delete entity using standard delete authorization
if (AuthUtil.isAuthorizedEntityUrns(
context.getOperationContext(), DELETE, List.of(documentUrn))) {
return true;
}

// Fallback to document-specific management privilege
final DisjunctivePrivilegeGroup orPrivilegeGroups =
new DisjunctivePrivilegeGroup(
ImmutableList.of(
new ConjunctivePrivilegeGroup(
ImmutableList.of(PoliciesConfig.MANAGE_DOCUMENTS_PRIVILEGE.getType()))));

return isAuthorized(
context, documentUrn.getEntityType(), documentUrn.toString(), orPrivilegeGroups);
}

public static boolean isAuthorized(
@Nonnull QueryContext context,
@Nonnull String resourceType,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
package com.linkedin.datahub.graphql.resolvers.knowledge;

import static com.linkedin.datahub.graphql.resolvers.ResolverUtils.bindArgument;

import com.linkedin.common.Owner;
import com.linkedin.common.OwnershipType;
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.concurrency.GraphQLConcurrencyUtils;
import com.linkedin.datahub.graphql.exception.AuthorizationException;
import com.linkedin.datahub.graphql.generated.CreateDocumentInput;
import com.linkedin.datahub.graphql.generated.OwnerInput;
import com.linkedin.metadata.entity.EntityService;
import com.linkedin.metadata.service.DocumentService;
import graphql.schema.DataFetcher;
import graphql.schema.DataFetchingEnvironment;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

/**
* Resolver used for creating a new Document on DataHub. Requires the CREATE_ENTITY privilege for
* Documents or MANAGE_DOCUMENTS privilege.
*/
@Slf4j
@RequiredArgsConstructor
public class CreateDocumentResolver implements DataFetcher<CompletableFuture<String>> {

private final DocumentService _documentService;
private final EntityService _entityService;

@Override
public CompletableFuture<String> get(DataFetchingEnvironment environment) throws Exception {

final QueryContext context = environment.getContext();
final CreateDocumentInput input =
bindArgument(environment.getArgument("input"), CreateDocumentInput.class);

return GraphQLConcurrencyUtils.supplyAsync(
() -> {
if (!AuthorizationUtils.canCreateDocument(context)) {
throw new AuthorizationException(
"Unauthorized to perform this action. Please contact your DataHub administrator.");
}

try {
// Extract content text
final String content = input.getContents().getText();

// Extract related URNs
final Urn parentDocumentUrn =
input.getParentDocument() != null
? UrnUtils.getUrn(input.getParentDocument())
: null;
final List<Urn> relatedAssetUrns =
input.getRelatedAssets() != null
? input.getRelatedAssets().stream()
.map(UrnUtils::getUrn)
.collect(Collectors.toList())
: null;
final List<Urn> relatedDocumentUrns =
input.getRelatedDocuments() != null
? input.getRelatedDocuments().stream()
.map(UrnUtils::getUrn)
.collect(Collectors.toList())
: null;

// Map GraphQL state enum to PDL enum if provided. If draftFor is provided, force
// UNPUBLISHED for the draft document.
com.linkedin.knowledge.DocumentState pdlState =
input.getState() != null
? com.linkedin.knowledge.DocumentState.valueOf(input.getState().name())
: null;
final String draftForUrn = input.getDraftFor();
if (draftForUrn != null) {
pdlState = com.linkedin.knowledge.DocumentState.UNPUBLISHED;
}

// Automatically create source with NATIVE type - users cannot set this via API
// (reserved for ingestion from external systems)
final com.linkedin.knowledge.DocumentSource source =
new com.linkedin.knowledge.DocumentSource();
source.setSourceType(com.linkedin.knowledge.DocumentSourceType.NATIVE);

// Convert single subType to list for subTypes aspect
final List<String> subTypes =
input.getSubType() != null
? java.util.Collections.singletonList(input.getSubType())
: null;

// Create document using service (draftFor parameter will handle draft logic)
final Urn draftForUrnParsed = draftForUrn != null ? UrnUtils.getUrn(draftForUrn) : null;
final Urn documentUrn =
_documentService.createDocument(
context.getOperationContext(),
input.getId(),
subTypes,
input.getTitle(),
source,
pdlState,
content,
parentDocumentUrn,
relatedAssetUrns,
relatedDocumentUrns,
draftForUrnParsed,
UrnUtils.getUrn(context.getActorUrn()));

// Set ownership
final Urn actorUrn = UrnUtils.getUrn(context.getActorUrn());
if (input.getOwners() != null && !input.getOwners().isEmpty()) {
// Use provided owners
final List<Owner> owners = mapOwnerInputsToOwners(input.getOwners());
_documentService.setDocumentOwnership(
context.getOperationContext(), documentUrn, owners, actorUrn);
} else {
// Default to adding the creator as owner
final Owner creatorOwner = new Owner();
creatorOwner.setOwner(actorUrn);
creatorOwner.setType(OwnershipType.TECHNICAL_OWNER);
_documentService.setDocumentOwnership(
context.getOperationContext(),
documentUrn,
java.util.Collections.singletonList(creatorOwner),
actorUrn);
}

return documentUrn.toString();
} catch (Exception e) {
log.error(
"Failed to create Document with id: {}, subType: {}: {}",
input.getId(),
input.getSubType(),
e.getMessage());
throw new RuntimeException(
String.format("Failed to create Document: %s", e.getMessage()), e);
}
},
this.getClass().getSimpleName(),
"get");
}

/** Maps GraphQL OwnerInputs to PDL Owner objects. */
private List<Owner> mapOwnerInputsToOwners(List<OwnerInput> ownerInputs) {
List<Owner> owners = new ArrayList<>();
for (OwnerInput ownerInput : ownerInputs) {
Owner owner = new Owner();
owner.setOwner(UrnUtils.getUrn(ownerInput.getOwnerUrn()));

// Map ownership type
if (ownerInput.getOwnershipTypeUrn() != null) {
// Custom ownership type URN
owner.setTypeUrn(UrnUtils.getUrn(ownerInput.getOwnershipTypeUrn()));
} else if (ownerInput.getType() != null) {
// Standard ownership type enum
owner.setType(OwnershipType.valueOf(ownerInput.getType().name()));
} else {
// Default to NONE
owner.setType(OwnershipType.NONE);
}

owners.add(owner);
}
return owners;
}
}
Loading
Loading