diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/Constants.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/Constants.java index 5f555b45d3b09..0924dbc0c0a6d 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/Constants.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/Constants.java @@ -21,6 +21,7 @@ private Constants() {} public static final String PROPERTIES_SCHEMA_FILE = "properties.graphql"; public static final String FORMS_SCHEMA_FILE = "forms.graphql"; public static final String INCIDENTS_SCHEMA_FILE = "incident.graphql"; + public static final String CONNECTIONS_SCHEMA_FILE = "connection.graphql"; public static final String BROWSE_PATH_DELIMITER = "/"; public static final String BROWSE_PATH_V2_DELIMITER = "␟"; public static final String VERSION_STAMP_FIELD_NAME = "versionStamp"; 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 38c40dbfd83e9..1fb01e9ed0d52 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 @@ -48,6 +48,7 @@ import com.linkedin.datahub.graphql.generated.DashboardStatsSummary; import com.linkedin.datahub.graphql.generated.DashboardUserUsageCounts; import com.linkedin.datahub.graphql.generated.DataFlow; +import com.linkedin.datahub.graphql.generated.DataHubConnection; import com.linkedin.datahub.graphql.generated.DataHubView; import com.linkedin.datahub.graphql.generated.DataJob; import com.linkedin.datahub.graphql.generated.DataJobInputOutput; @@ -129,6 +130,7 @@ import com.linkedin.datahub.graphql.resolvers.chart.BrowseV2Resolver; import com.linkedin.datahub.graphql.resolvers.chart.ChartStatsSummaryResolver; import com.linkedin.datahub.graphql.resolvers.config.AppConfigResolver; +import com.linkedin.datahub.graphql.resolvers.connection.UpsertConnectionResolver; import com.linkedin.datahub.graphql.resolvers.container.ContainerEntitiesResolver; import com.linkedin.datahub.graphql.resolvers.container.ParentContainersResolver; import com.linkedin.datahub.graphql.resolvers.dashboard.DashboardStatsSummaryResolver; @@ -306,6 +308,7 @@ import com.linkedin.datahub.graphql.types.chart.ChartType; import com.linkedin.datahub.graphql.types.common.mappers.OperationMapper; import com.linkedin.datahub.graphql.types.common.mappers.UrnToEntityMapper; +import com.linkedin.datahub.graphql.types.connection.DataHubConnectionType; import com.linkedin.datahub.graphql.types.container.ContainerType; import com.linkedin.datahub.graphql.types.corpgroup.CorpGroupType; import com.linkedin.datahub.graphql.types.corpuser.CorpUserType; @@ -355,6 +358,7 @@ import com.linkedin.metadata.config.ViewsConfiguration; import com.linkedin.metadata.config.VisualConfiguration; import com.linkedin.metadata.config.telemetry.TelemetryConfiguration; +import com.linkedin.metadata.connection.ConnectionService; import com.linkedin.metadata.entity.EntityService; import com.linkedin.metadata.graph.GraphClient; import com.linkedin.metadata.graph.SiblingGraphService; @@ -439,6 +443,7 @@ public class GmsGraphQLEngine { private final ERModelRelationshipService erModelRelationshipService; private final FormService formService; private final RestrictedService restrictedService; + private ConnectionService connectionService; private final BusinessAttributeService businessAttributeService; private final FeatureFlags featureFlags; @@ -472,6 +477,7 @@ public class GmsGraphQLEngine { private final GlossaryTermType glossaryTermType; private final GlossaryNodeType glossaryNodeType; private final AspectType aspectType; + private final DataHubConnectionType connectionType; private final ContainerType containerType; private final DomainType domainType; private final NotebookType notebookType; @@ -558,6 +564,7 @@ public GmsGraphQLEngine(final GmsGraphQLEngineArgs args) { this.dataProductService = args.dataProductService; this.formService = args.formService; this.restrictedService = args.restrictedService; + this.connectionService = args.connectionService; this.businessAttributeService = args.businessAttributeService; this.ingestionConfiguration = Objects.requireNonNull(args.ingestionConfiguration); @@ -588,6 +595,7 @@ public GmsGraphQLEngine(final GmsGraphQLEngineArgs args) { this.glossaryTermType = new GlossaryTermType(entityClient); this.glossaryNodeType = new GlossaryNodeType(entityClient); this.aspectType = new AspectType(entityClient); + this.connectionType = new DataHubConnectionType(entityClient, secretService); this.containerType = new ContainerType(entityClient); this.domainType = new DomainType(entityClient); this.notebookType = new NotebookType(entityClient); @@ -636,6 +644,7 @@ public GmsGraphQLEngine(final GmsGraphQLEngineArgs args) { dataJobType, glossaryTermType, glossaryNodeType, + connectionType, containerType, notebookType, domainType, @@ -753,6 +762,7 @@ public void configureRuntimeWiring(final RuntimeWiring.Builder builder) { configureRoleResolvers(builder); configureBusinessAttributeResolver(builder); configureBusinessAttributeAssociationResolver(builder); + configureConnectionResolvers(builder); } private void configureOrganisationRoleResolvers(RuntimeWiring.Builder builder) { @@ -803,6 +813,7 @@ public GraphQLEngine.Builder builder() { .addSchema(fileBasedSchema(LINEAGE_SCHEMA_FILE)) .addSchema(fileBasedSchema(PROPERTIES_SCHEMA_FILE)) .addSchema(fileBasedSchema(FORMS_SCHEMA_FILE)) + .addSchema(fileBasedSchema(CONNECTIONS_SCHEMA_FILE)) .addSchema(fileBasedSchema(INCIDENTS_SCHEMA_FILE)); for (GmsGraphQLPlugin plugin : this.graphQLPlugins) { @@ -3015,4 +3026,29 @@ private void configureBusinessAttributeAssociationResolver(final RuntimeWiring.B .getBusinessAttribute() .getUrn()))); } + + private void configureConnectionResolvers(final RuntimeWiring.Builder builder) { + builder.type( + "Mutation", + typeWiring -> + typeWiring.dataFetcher( + "upsertConnection", + new UpsertConnectionResolver(connectionService, secretService))); + builder.type( + "Query", + typeWiring -> typeWiring.dataFetcher("connection", getResolver(this.connectionType))); + builder.type( + "DataHubConnection", + typeWiring -> + typeWiring.dataFetcher( + "platform", + new LoadableTypeResolver<>( + this.dataPlatformType, + (env) -> { + final DataHubConnection connection = env.getSource(); + return connection.getPlatform() != null + ? connection.getPlatform().getUrn() + : null; + }))); + } } diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/GmsGraphQLEngineArgs.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/GmsGraphQLEngineArgs.java index 2077a674abd68..d4d4d592d6bca 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/GmsGraphQLEngineArgs.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/GmsGraphQLEngineArgs.java @@ -19,6 +19,7 @@ import com.linkedin.metadata.config.ViewsConfiguration; import com.linkedin.metadata.config.VisualConfiguration; import com.linkedin.metadata.config.telemetry.TelemetryConfiguration; +import com.linkedin.metadata.connection.ConnectionService; import com.linkedin.metadata.entity.EntityService; import com.linkedin.metadata.graph.GraphClient; import com.linkedin.metadata.graph.SiblingGraphService; @@ -84,6 +85,7 @@ public class GmsGraphQLEngineArgs { int graphQLQueryDepthLimit; boolean graphQLQueryIntrospectionEnabled; BusinessAttributeService businessAttributeService; + ConnectionService connectionService; // any fork specific args should go below this line } diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/connection/ConnectionMapper.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/connection/ConnectionMapper.java new file mode 100644 index 0000000000000..a4ad332d5946d --- /dev/null +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/connection/ConnectionMapper.java @@ -0,0 +1,104 @@ +package com.linkedin.datahub.graphql.resolvers.connection; + +import com.linkedin.common.DataPlatformInstance; +import com.linkedin.common.urn.Urn; +import com.linkedin.data.DataMap; +import com.linkedin.datahub.graphql.QueryContext; +import com.linkedin.datahub.graphql.generated.DataHubConnection; +import com.linkedin.datahub.graphql.generated.DataHubConnectionDetails; +import com.linkedin.datahub.graphql.generated.DataHubJsonConnection; +import com.linkedin.datahub.graphql.generated.DataPlatform; +import com.linkedin.datahub.graphql.generated.EntityType; +import com.linkedin.entity.EntityResponse; +import com.linkedin.entity.EnvelopedAspect; +import com.linkedin.entity.EnvelopedAspectMap; +import com.linkedin.metadata.Constants; +import io.datahubproject.metadata.services.SecretService; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +public class ConnectionMapper { + /** + * Maps a GMS encrypted connection details object into the decrypted form returned by the GraphQL + * API. + * + *

Returns null if the Entity does not have the required aspects: dataHubConnectionDetails or + * dataPlatformInstance. + */ + @Nullable + public static DataHubConnection map( + @Nonnull final QueryContext context, + @Nonnull final EntityResponse entityResponse, + @Nonnull final SecretService secretService) { + // If the connection does not exist, simply return null + if (!hasAspects(entityResponse)) { + return null; + } + + final DataHubConnection result = new DataHubConnection(); + final Urn entityUrn = entityResponse.getUrn(); + final EnvelopedAspectMap aspects = entityResponse.getAspects(); + + result.setUrn(entityUrn.toString()); + result.setType(EntityType.DATAHUB_CONNECTION); + + final EnvelopedAspect envelopedAssertionInfo = + aspects.get(Constants.DATAHUB_CONNECTION_DETAILS_ASPECT_NAME); + if (envelopedAssertionInfo != null) { + result.setDetails( + mapConnectionDetails( + context, + new com.linkedin.connection.DataHubConnectionDetails( + envelopedAssertionInfo.getValue().data()), + secretService)); + } + final EnvelopedAspect envelopedPlatformInstance = + aspects.get(Constants.DATA_PLATFORM_INSTANCE_ASPECT_NAME); + if (envelopedPlatformInstance != null) { + final DataMap data = envelopedPlatformInstance.getValue().data(); + result.setPlatform(mapPlatform(new DataPlatformInstance(data))); + } + return result; + } + + private static DataHubConnectionDetails mapConnectionDetails( + @Nonnull final QueryContext context, + @Nonnull final com.linkedin.connection.DataHubConnectionDetails gmsDetails, + @Nonnull final SecretService secretService) { + final DataHubConnectionDetails result = new DataHubConnectionDetails(); + result.setType( + com.linkedin.datahub.graphql.generated.DataHubConnectionDetailsType.valueOf( + gmsDetails.getType().toString())); + if (gmsDetails.hasJson() && ConnectionUtils.canManageConnections(context)) { + result.setJson(mapJsonConnectionDetails(gmsDetails.getJson(), secretService)); + } + if (gmsDetails.hasName()) { + result.setName(gmsDetails.getName()); + } + return result; + } + + private static DataHubJsonConnection mapJsonConnectionDetails( + @Nonnull final com.linkedin.connection.DataHubJsonConnection gmsJsonConnection, + @Nonnull final SecretService secretService) { + final DataHubJsonConnection result = new DataHubJsonConnection(); + // Decrypt the BLOB! + result.setBlob(secretService.decrypt(gmsJsonConnection.getEncryptedBlob())); + return result; + } + + private static DataPlatform mapPlatform(final DataPlatformInstance platformInstance) { + // Set dummy platform to be resolved. + final DataPlatform partialPlatform = new DataPlatform(); + partialPlatform.setUrn(platformInstance.getPlatform().toString()); + return partialPlatform; + } + + private static boolean hasAspects(@Nonnull final EntityResponse response) { + return response.hasAspects() + && response.getAspects().containsKey(Constants.DATAHUB_CONNECTION_DETAILS_ASPECT_NAME) + && response.getAspects().containsKey(Constants.DATA_PLATFORM_INSTANCE_ASPECT_NAME); + } + + private ConnectionMapper() {} +} diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/connection/ConnectionUtils.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/connection/ConnectionUtils.java new file mode 100644 index 0000000000000..bcdd6460ae75e --- /dev/null +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/connection/ConnectionUtils.java @@ -0,0 +1,23 @@ +package com.linkedin.datahub.graphql.resolvers.connection; + +import com.datahub.authorization.AuthUtil; +import com.linkedin.datahub.graphql.QueryContext; +import com.linkedin.metadata.authorization.PoliciesConfig; +import javax.annotation.Nonnull; + +/** Utilities for working with DataHub Connections. */ +public class ConnectionUtils { + + /** + * Returns true if the user is able to read and or write connection between DataHub and external + * platforms. + */ + public static boolean canManageConnections(@Nonnull QueryContext context) { + return AuthUtil.isAuthorized( + context.getAuthorizer(), + context.getActorUrn(), + PoliciesConfig.MANAGE_CONNECTIONS_PRIVILEGE); + } + + private ConnectionUtils() {} +} diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/connection/UpsertConnectionResolver.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/connection/UpsertConnectionResolver.java new file mode 100644 index 0000000000000..3aae612b8cb78 --- /dev/null +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/connection/UpsertConnectionResolver.java @@ -0,0 +1,78 @@ +package com.linkedin.datahub.graphql.resolvers.connection; + +import static com.linkedin.datahub.graphql.resolvers.ResolverUtils.*; + +import com.datahub.authentication.Authentication; +import com.linkedin.common.urn.Urn; +import com.linkedin.common.urn.UrnUtils; +import com.linkedin.connection.DataHubConnectionDetailsType; +import com.linkedin.connection.DataHubJsonConnection; +import com.linkedin.datahub.graphql.QueryContext; +import com.linkedin.datahub.graphql.exception.AuthorizationException; +import com.linkedin.datahub.graphql.generated.DataHubConnection; +import com.linkedin.datahub.graphql.generated.UpsertDataHubConnectionInput; +import com.linkedin.entity.EntityResponse; +import com.linkedin.metadata.connection.ConnectionService; +import graphql.schema.DataFetcher; +import graphql.schema.DataFetchingEnvironment; +import io.datahubproject.metadata.services.SecretService; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import javax.annotation.Nonnull; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class UpsertConnectionResolver implements DataFetcher> { + + private final ConnectionService _connectionService; + private final SecretService _secretService; + + public UpsertConnectionResolver( + @Nonnull final ConnectionService connectionService, + @Nonnull final SecretService secretService) { + _connectionService = + Objects.requireNonNull(connectionService, "connectionService cannot be null"); + _secretService = Objects.requireNonNull(secretService, "secretService cannot be null"); + } + + @Override + public CompletableFuture get(final DataFetchingEnvironment environment) + throws Exception { + + final QueryContext context = environment.getContext(); + final UpsertDataHubConnectionInput input = + bindArgument(environment.getArgument("input"), UpsertDataHubConnectionInput.class); + final Authentication authentication = context.getAuthentication(); + + return CompletableFuture.supplyAsync( + () -> { + if (!ConnectionUtils.canManageConnections(context)) { + throw new AuthorizationException( + "Unauthorized to upsert Connection. Please contact your DataHub administrator for more information."); + } + + try { + final Urn connectionUrn = + _connectionService.upsertConnection( + context.getOperationContext(), + input.getId(), + UrnUtils.getUrn(input.getPlatformUrn()), + DataHubConnectionDetailsType.valueOf(input.getType().toString()), + input.getJson() != null + // Encrypt payload + ? new DataHubJsonConnection() + .setEncryptedBlob(_secretService.encrypt(input.getJson().getBlob())) + : null, + input.getName()); + + final EntityResponse connectionResponse = + _connectionService.getConnectionEntityResponse( + context.getOperationContext(), connectionUrn); + return ConnectionMapper.map(context, connectionResponse, _secretService); + } catch (Exception e) { + throw new RuntimeException( + String.format("Failed to upsert a Connection from input %s", input), e); + } + }); + } +} diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/connection/DataHubConnectionType.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/connection/DataHubConnectionType.java new file mode 100644 index 0000000000000..0a62d224c6513 --- /dev/null +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/connection/DataHubConnectionType.java @@ -0,0 +1,87 @@ +package com.linkedin.datahub.graphql.types.connection; + +import com.google.common.collect.ImmutableSet; +import com.linkedin.common.urn.Urn; +import com.linkedin.common.urn.UrnUtils; +import com.linkedin.datahub.graphql.QueryContext; +import com.linkedin.datahub.graphql.generated.DataHubConnection; +import com.linkedin.datahub.graphql.generated.Entity; +import com.linkedin.datahub.graphql.generated.EntityType; +import com.linkedin.datahub.graphql.resolvers.connection.ConnectionMapper; +import com.linkedin.entity.EntityResponse; +import com.linkedin.entity.client.EntityClient; +import com.linkedin.metadata.Constants; +import graphql.execution.DataFetcherResult; +import io.datahubproject.metadata.services.SecretService; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; +import javax.annotation.Nonnull; + +public class DataHubConnectionType + implements com.linkedin.datahub.graphql.types.EntityType { + + static final Set ASPECTS_TO_FETCH = + ImmutableSet.of( + Constants.DATAHUB_CONNECTION_DETAILS_ASPECT_NAME, + Constants.DATA_PLATFORM_INSTANCE_ASPECT_NAME); + private final EntityClient _entityClient; + private final SecretService _secretService; + + public DataHubConnectionType( + @Nonnull final EntityClient entityClient, @Nonnull final SecretService secretService) { + _entityClient = Objects.requireNonNull(entityClient, "entityClient must not be null"); + _secretService = Objects.requireNonNull(secretService, "secretService must not be null"); + } + + @Override + public EntityType type() { + return EntityType.DATAHUB_CONNECTION; + } + + @Override + public Function getKeyProvider() { + return Entity::getUrn; + } + + @Override + public Class objectClass() { + return DataHubConnection.class; + } + + @Override + public List> batchLoad( + @Nonnull List urns, @Nonnull QueryContext context) throws Exception { + final List connectionUrns = + urns.stream().map(UrnUtils::getUrn).collect(Collectors.toList()); + try { + final Map entities = + _entityClient.batchGetV2( + context.getOperationContext(), + Constants.DATAHUB_CONNECTION_ENTITY_NAME, + new HashSet<>(connectionUrns), + ASPECTS_TO_FETCH); + + final List gmsResults = new ArrayList<>(); + for (Urn urn : connectionUrns) { + gmsResults.add(entities.getOrDefault(urn, null)); + } + return gmsResults.stream() + .map( + gmsResult -> + gmsResult == null + ? null + : DataFetcherResult.newResult() + .data(ConnectionMapper.map(context, gmsResult, _secretService)) + .build()) + .collect(Collectors.toList()); + } catch (Exception e) { + throw new RuntimeException("Failed to batch load Connections", e); + } + } +} diff --git a/datahub-graphql-core/src/main/resources/connection.graphql b/datahub-graphql-core/src/main/resources/connection.graphql new file mode 100644 index 0000000000000..1a7249485e69d --- /dev/null +++ b/datahub-graphql-core/src/main/resources/connection.graphql @@ -0,0 +1,130 @@ +# DataHub Connections-specific GraphQL types + +extend type Query { + """ + Get a set of connection details by URN. + This requires the 'Manage Connections' platform privilege. + Returns null if a connection with the provided urn does not exist. + """ + connection(urn: String!): DataHubConnection +} + +extend type Mutation { + """ + Upsert a particular connection. + This requires the 'Manage Connections' platform privilege. + """ + upsertConnection(input: UpsertDataHubConnectionInput!): DataHubConnection! +} + +""" +A connection between DataHub and an external Platform. +""" +type DataHubConnection implements Entity { + """ + The urn of the connection + """ + urn: String! + + """ + The standard Entity Type field + """ + type: EntityType! + + """ + The connection details + """ + details: DataHubConnectionDetails! + + """ + The external Data Platform associated with the connection + """ + platform: DataPlatform! + + """ + Not implemented! + """ + relationships(input: RelationshipsInput!): EntityRelationshipsResult +} + + +""" +The details of the Connection +""" +type DataHubConnectionDetails { + """ + The type or format of connection + """ + type: DataHubConnectionDetailsType! + + """ + A JSON-encoded connection. Present when type is JSON. + """ + json: DataHubJsonConnection + + """ + The name for this DataHub connection + """ + name: String +} + +""" +The type of a DataHub connection +""" +enum DataHubConnectionDetailsType { + """ + A json-encoded set of connection details. + """ + JSON +} + +""" +The details of a JSON Connection +""" +type DataHubJsonConnection { + """ + The JSON blob containing the specific connection details. + """ + blob: String! +} + +""" +Input required to upsert a new DataHub connection. +""" +input UpsertDataHubConnectionInput { + """ + An optional ID to use when creating the URN of the connection. If none is provided, + a random UUID will be generated automatically. + """ + id: String + + """ + The type or format of connection + """ + type: DataHubConnectionDetailsType! + + """ + Urn of the associated platform + """ + platformUrn: String! + + """ + A JSON-encoded connection. This must be present when type is JSON. + """ + json: DataHubJsonConnectionInput + + """ + An optional name for this connection entity + """ + name: String +} + +""" +The details of a JSON Connection +""" +input DataHubJsonConnectionInput { + """ + The JSON blob containing the specific connection details. + """ + blob: String! +} \ No newline at end of file diff --git a/datahub-graphql-core/src/main/resources/entity.graphql b/datahub-graphql-core/src/main/resources/entity.graphql index 2315d6f8767d9..2afb42c649fec 100644 --- a/datahub-graphql-core/src/main/resources/entity.graphql +++ b/datahub-graphql-core/src/main/resources/entity.graphql @@ -1143,6 +1143,11 @@ enum EntityType { """ CUSTOM_OWNERSHIP_TYPE + """ + A connection to an external source. + """ + DATAHUB_CONNECTION + """ A DataHub incident - SaaS only """ diff --git a/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/connection/UpsertConnectionResolverTest.java b/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/connection/UpsertConnectionResolverTest.java new file mode 100644 index 0000000000000..5bc5332e711fd --- /dev/null +++ b/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/connection/UpsertConnectionResolverTest.java @@ -0,0 +1,128 @@ +package com.linkedin.datahub.graphql.resolvers.connection; + +import static com.linkedin.datahub.graphql.TestUtils.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; +import static org.testng.Assert.assertThrows; + +import com.google.common.collect.ImmutableMap; +import com.linkedin.common.DataPlatformInstance; +import com.linkedin.common.urn.Urn; +import com.linkedin.common.urn.UrnUtils; +import com.linkedin.connection.DataHubConnectionDetails; +import com.linkedin.connection.DataHubJsonConnection; +import com.linkedin.datahub.graphql.QueryContext; +import com.linkedin.datahub.graphql.generated.DataHubConnection; +import com.linkedin.datahub.graphql.generated.DataHubConnectionDetailsType; +import com.linkedin.datahub.graphql.generated.DataHubJsonConnectionInput; +import com.linkedin.datahub.graphql.generated.EntityType; +import com.linkedin.datahub.graphql.generated.UpsertDataHubConnectionInput; +import com.linkedin.entity.Aspect; +import com.linkedin.entity.EntityResponse; +import com.linkedin.entity.EnvelopedAspect; +import com.linkedin.entity.EnvelopedAspectMap; +import com.linkedin.metadata.Constants; +import com.linkedin.metadata.connection.ConnectionService; +import graphql.schema.DataFetchingEnvironment; +import io.datahubproject.metadata.context.OperationContext; +import io.datahubproject.metadata.services.SecretService; +import java.util.concurrent.CompletionException; +import org.mockito.Mockito; +import org.testng.Assert; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +public class UpsertConnectionResolverTest { + + private ConnectionService connectionService; + private SecretService secretService; + private UpsertConnectionResolver resolver; + + @BeforeMethod + public void setUp() { + connectionService = Mockito.mock(ConnectionService.class); + secretService = Mockito.mock(SecretService.class); + Mockito.when(secretService.encrypt("{}")).thenReturn("encrypted"); + Mockito.when(secretService.decrypt("encrypted")).thenReturn("{}"); + resolver = new UpsertConnectionResolver(connectionService, secretService); + } + + @Test + public void testGetAuthorized() throws Exception { + // Mock inputs + Urn connectionUrn = UrnUtils.getUrn("urn:li:dataHubConnection:test-id"); + Urn platformUrn = UrnUtils.getUrn("urn:li:dataPlatform:slack"); + + final UpsertDataHubConnectionInput input = new UpsertDataHubConnectionInput(); + input.setId(connectionUrn.getId()); + input.setPlatformUrn(platformUrn.toString()); + input.setType(DataHubConnectionDetailsType.JSON); + input.setName("test-name"); + input.setJson(new DataHubJsonConnectionInput("{}")); + + QueryContext mockContext = getMockAllowContext(); + DataFetchingEnvironment mockEnv = Mockito.mock(DataFetchingEnvironment.class); + Mockito.when(mockEnv.getArgument(Mockito.eq("input"))).thenReturn(input); + Mockito.when(mockEnv.getContext()).thenReturn(mockContext); + + final DataHubConnectionDetails details = + new DataHubConnectionDetails() + .setType(com.linkedin.connection.DataHubConnectionDetailsType.JSON) + .setJson(new DataHubJsonConnection().setEncryptedBlob("encrypted")); + + final DataPlatformInstance platformInstance = + new DataPlatformInstance().setPlatform(platformUrn); + + when(connectionService.upsertConnection( + any(OperationContext.class), + Mockito.eq(input.getId()), + Mockito.eq(platformUrn), + Mockito.eq(details.getType()), + Mockito.eq(details.getJson()), + Mockito.any(String.class))) + .thenReturn(connectionUrn); + when(connectionService.getConnectionEntityResponse( + any(OperationContext.class), Mockito.eq(connectionUrn))) + .thenReturn( + new EntityResponse() + .setUrn(connectionUrn) + .setEntityName(Constants.DATAHUB_CONNECTION_ENTITY_NAME) + .setAspects( + new EnvelopedAspectMap( + ImmutableMap.of( + Constants.DATAHUB_CONNECTION_DETAILS_ASPECT_NAME, + new EnvelopedAspect() + .setName(Constants.DATAHUB_CONNECTION_DETAILS_ASPECT_NAME) + .setValue(new Aspect(details.data())), + Constants.DATA_PLATFORM_INSTANCE_ASPECT_NAME, + new EnvelopedAspect() + .setName(Constants.DATA_PLATFORM_INSTANCE_ASPECT_NAME) + .setValue(new Aspect(platformInstance.data())))))); + + DataHubConnection actual = resolver.get(mockEnv).get(); + + Assert.assertEquals(actual.getType(), EntityType.DATAHUB_CONNECTION); + Assert.assertEquals(actual.getUrn(), connectionUrn.toString()); + Assert.assertEquals(actual.getPlatform().getUrn(), platformUrn.toString()); + Assert.assertEquals(actual.getDetails().getType(), input.getType()); + Assert.assertEquals(actual.getDetails().getJson().getBlob(), input.getJson().getBlob()); + } + + @Test + public void testGetUnAuthorized() { + // Mock inputs + Urn connectionUrn = UrnUtils.getUrn("urn:li:dataHubConnection:test-id"); + + final UpsertDataHubConnectionInput input = new UpsertDataHubConnectionInput(); + input.setId(connectionUrn.getId()); + input.setPlatformUrn(connectionUrn.toString()); + input.setType(DataHubConnectionDetailsType.JSON); + + QueryContext mockContext = getMockAllowContext(); + DataFetchingEnvironment mockEnv = Mockito.mock(DataFetchingEnvironment.class); + Mockito.when(mockEnv.getArgument(Mockito.eq("input"))).thenReturn(input); + Mockito.when(mockEnv.getContext()).thenReturn(mockContext); + + assertThrows(CompletionException.class, () -> resolver.get(mockEnv).join()); + } +} diff --git a/datahub-web-react/src/graphql/connection.graphql b/datahub-web-react/src/graphql/connection.graphql new file mode 100644 index 0000000000000..02f87f08c519f --- /dev/null +++ b/datahub-web-react/src/graphql/connection.graphql @@ -0,0 +1,29 @@ +mutation upsertConnection($input: UpsertDataHubConnectionInput!) { + upsertConnection(input: $input) { + urn + details { + type + json { + blob + } + } + platform { + ...platformFields + } + } +} + +query connection($urn: String!) { + connection(urn: $urn) { + urn + details { + type + json { + blob + } + } + platform { + ...platformFields + } + } +} diff --git a/docs-website/graphql/generateGraphQLSchema.sh b/docs-website/graphql/generateGraphQLSchema.sh index c6d7ec528b613..da14fbc337f90 100755 --- a/docs-website/graphql/generateGraphQLSchema.sh +++ b/docs-website/graphql/generateGraphQLSchema.sh @@ -17,4 +17,5 @@ cat ../../datahub-graphql-core/src/main/resources/timeline.graphql >> combined.g cat ../../datahub-graphql-core/src/main/resources/step.graphql >> combined.graphql cat ../../datahub-graphql-core/src/main/resources/lineage.graphql >> combined.graphql cat ../../datahub-graphql-core/src/main/resources/properties.graphql >> combined.graphql -cat ../../datahub-graphql-core/src/main/resources/forms.graphql >> combined.graphql \ No newline at end of file +cat ../../datahub-graphql-core/src/main/resources/forms.graphql >> combined.graphql +cat ../../datahub-graphql-core/src/main/resources/connection.graphql >> combined.graphql \ No newline at end of file diff --git a/li-utils/src/main/java/com/linkedin/metadata/Constants.java b/li-utils/src/main/java/com/linkedin/metadata/Constants.java index c200a4bc30d19..66ed48a428a21 100644 --- a/li-utils/src/main/java/com/linkedin/metadata/Constants.java +++ b/li-utils/src/main/java/com/linkedin/metadata/Constants.java @@ -358,6 +358,10 @@ public class Constants { public static final String GLOBAL_SETTINGS_INFO_ASPECT_NAME = "globalSettingsInfo"; public static final Urn GLOBAL_SETTINGS_URN = Urn.createFromTuple(GLOBAL_SETTINGS_ENTITY_NAME, 0); + // Connection + public static final String DATAHUB_CONNECTION_ENTITY_NAME = "dataHubConnection"; + public static final String DATAHUB_CONNECTION_DETAILS_ASPECT_NAME = "dataHubConnectionDetails"; + // Relationships public static final String IS_MEMBER_OF_GROUP_RELATIONSHIP_NAME = "IsMemberOfGroup"; public static final String IS_MEMBER_OF_NATIVE_GROUP_RELATIONSHIP_NAME = "IsMemberOfNativeGroup"; diff --git a/metadata-io/src/main/java/com/linkedin/metadata/connection/ConnectionService.java b/metadata-io/src/main/java/com/linkedin/metadata/connection/ConnectionService.java new file mode 100644 index 0000000000000..f044ea52a251a --- /dev/null +++ b/metadata-io/src/main/java/com/linkedin/metadata/connection/ConnectionService.java @@ -0,0 +1,129 @@ +package com.linkedin.metadata.connection; + +import com.google.common.collect.ImmutableSet; +import com.linkedin.common.DataPlatformInstance; +import com.linkedin.common.urn.Urn; +import com.linkedin.connection.DataHubConnectionDetails; +import com.linkedin.connection.DataHubConnectionDetailsType; +import com.linkedin.connection.DataHubJsonConnection; +import com.linkedin.data.template.SetMode; +import com.linkedin.entity.EntityResponse; +import com.linkedin.entity.client.EntityClient; +import com.linkedin.metadata.Constants; +import com.linkedin.metadata.entity.AspectUtils; +import com.linkedin.metadata.key.DataHubConnectionKey; +import com.linkedin.metadata.utils.EntityKeyUtils; +import com.linkedin.mxe.MetadataChangeProposal; +import io.datahubproject.metadata.context.OperationContext; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.UUID; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@RequiredArgsConstructor +public class ConnectionService { + + private final EntityClient _entityClient; + + /** + * Upserts a DataHub connection. If the connection with the provided ID already exists, then it + * will be overwritten. + * + *

This method assumes that authorization has already been verified at the calling layer. + * + * @return the URN of the new connection. + */ + public Urn upsertConnection( + @Nonnull OperationContext opContext, + @Nullable final String id, + @Nonnull final Urn platformUrn, + @Nonnull final DataHubConnectionDetailsType type, + @Nullable final DataHubJsonConnection json, + @Nullable final String name) { + Objects.requireNonNull(platformUrn, "platformUrn must not be null"); + Objects.requireNonNull(type, "type must not be null"); + Objects.requireNonNull(opContext, "opContext must not be null"); + + // 1. Optionally generate new connection id + final String connectionId = id != null ? id : UUID.randomUUID().toString(); + final DataHubConnectionKey key = new DataHubConnectionKey().setId(connectionId); + final Urn connectionUrn = + EntityKeyUtils.convertEntityKeyToUrn(key, Constants.DATAHUB_CONNECTION_ENTITY_NAME); + + // 2. Build Connection Details + final DataHubConnectionDetails details = new DataHubConnectionDetails(); + details.setType(type); + // default set name as ID if it exists, otherwise use name if it exists + details.setName(id, SetMode.IGNORE_NULL); + details.setName(name, SetMode.IGNORE_NULL); + + if (DataHubConnectionDetailsType.JSON.equals(details.getType())) { + if (json != null) { + details.setJson(json); + } else { + throw new IllegalArgumentException( + "Connections with type JSON must provide the field 'json'."); + } + } + + // 3. Build platform instance + final DataPlatformInstance platformInstance = new DataPlatformInstance(); + platformInstance.setPlatform(platformUrn); + + // 4. Write changes to GMS + try { + final List aspectsToIngest = new ArrayList<>(); + aspectsToIngest.add( + AspectUtils.buildMetadataChangeProposal( + connectionUrn, Constants.DATAHUB_CONNECTION_DETAILS_ASPECT_NAME, details)); + aspectsToIngest.add( + AspectUtils.buildMetadataChangeProposal( + connectionUrn, Constants.DATA_PLATFORM_INSTANCE_ASPECT_NAME, platformInstance)); + _entityClient.batchIngestProposals(opContext, aspectsToIngest, false); + } catch (Exception e) { + throw new RuntimeException( + String.format("Failed to upsert Connection with urn %s", connectionUrn), e); + } + return connectionUrn; + } + + @Nullable + public DataHubConnectionDetails getConnectionDetails( + @Nonnull OperationContext opContext, @Nonnull final Urn connectionUrn) { + Objects.requireNonNull(connectionUrn, "connectionUrn must not be null"); + final EntityResponse response = getConnectionEntityResponse(opContext, connectionUrn); + if (response != null + && response.getAspects().containsKey(Constants.DATAHUB_CONNECTION_DETAILS_ASPECT_NAME)) { + return new DataHubConnectionDetails( + response + .getAspects() + .get(Constants.DATAHUB_CONNECTION_DETAILS_ASPECT_NAME) + .getValue() + .data()); + } + // No aspect found + return null; + } + + @Nullable + public EntityResponse getConnectionEntityResponse( + @Nonnull OperationContext opContext, @Nonnull final Urn connectionUrn) { + try { + return _entityClient.getV2( + opContext, + Constants.DATAHUB_CONNECTION_ENTITY_NAME, + connectionUrn, + ImmutableSet.of( + Constants.DATAHUB_CONNECTION_DETAILS_ASPECT_NAME, + Constants.DATA_PLATFORM_INSTANCE_ASPECT_NAME)); + } catch (Exception e) { + throw new RuntimeException( + String.format("Failed to retrieve Connection with urn %s", connectionUrn), e); + } + } +} diff --git a/metadata-io/src/test/java/com/linkedin/metadata/connection/ConnectionServiceTest.java b/metadata-io/src/test/java/com/linkedin/metadata/connection/ConnectionServiceTest.java new file mode 100644 index 0000000000000..658c66807ccf1 --- /dev/null +++ b/metadata-io/src/test/java/com/linkedin/metadata/connection/ConnectionServiceTest.java @@ -0,0 +1,147 @@ +package com.linkedin.metadata.connection; + +import static org.mockito.Mockito.*; +import static org.testng.Assert.*; + +import com.datahub.authentication.Authentication; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.linkedin.common.DataPlatformInstance; +import com.linkedin.common.urn.Urn; +import com.linkedin.common.urn.UrnUtils; +import com.linkedin.connection.DataHubConnectionDetails; +import com.linkedin.connection.DataHubConnectionDetailsType; +import com.linkedin.connection.DataHubJsonConnection; +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.metadata.Constants; +import com.linkedin.metadata.entity.AspectUtils; +import com.linkedin.metadata.key.DataHubConnectionKey; +import com.linkedin.metadata.utils.EntityKeyUtils; +import io.datahubproject.metadata.context.OperationContext; +import org.mockito.Mockito; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +public class ConnectionServiceTest { + + private EntityClient entityClient; + private Authentication systemAuthentication; + private ConnectionService connectionService; + + @BeforeMethod + public void setUp() { + entityClient = Mockito.mock(EntityClient.class); + systemAuthentication = Mockito.mock(Authentication.class); + connectionService = new ConnectionService(entityClient); + } + + @Test + public void testUpsertConnection() throws Exception { + final String id = "testId"; + final Urn platformUrn = UrnUtils.getUrn("urn:li:dataPlatform:slack"); + final DataHubConnectionDetailsType type = DataHubConnectionDetailsType.JSON; + final DataHubJsonConnection json = new DataHubJsonConnection().setEncryptedBlob("blob"); + final Authentication authentication = Mockito.mock(Authentication.class); + final DataHubConnectionKey key = new DataHubConnectionKey().setId(id); + final Urn connectionUrn = + EntityKeyUtils.convertEntityKeyToUrn(key, Constants.DATAHUB_CONNECTION_ENTITY_NAME); + + // Execute and assert + Urn result = + connectionService.upsertConnection( + mock(OperationContext.class), id, platformUrn, type, json, null); + + DataHubConnectionDetails expectedDetails = mockConnectionDetails(id); + DataPlatformInstance expectedDataPlatformInstance = mockPlatformInstance(platformUrn); + + verify(entityClient) + .batchIngestProposals( + any(OperationContext.class), + Mockito.eq( + ImmutableList.of( + AspectUtils.buildMetadataChangeProposal( + connectionUrn, + Constants.DATAHUB_CONNECTION_DETAILS_ASPECT_NAME, + expectedDetails), + AspectUtils.buildMetadataChangeProposal( + connectionUrn, + Constants.DATA_PLATFORM_INSTANCE_ASPECT_NAME, + expectedDataPlatformInstance))), + Mockito.eq(false)); + assertEquals(result, connectionUrn); + } + + @Test + public void testGetConnectionDetails() throws Exception { + final Urn connectionUrn = Mockito.mock(Urn.class); + + final DataHubConnectionDetails connectionDetails = mockConnectionDetails("testId"); + final DataPlatformInstance platformInstance = + mockPlatformInstance(UrnUtils.getUrn("urn:li:dataPlatform:slack")); + + EntityResponse response = + new EntityResponse() + .setEntityName(Constants.DATAHUB_CONNECTION_ENTITY_NAME) + .setUrn(connectionUrn) + .setAspects( + new EnvelopedAspectMap( + ImmutableMap.of( + Constants.DATAHUB_CONNECTION_DETAILS_ASPECT_NAME, + new EnvelopedAspect() + .setName(Constants.DATAHUB_CONNECTION_ENTITY_NAME) + .setValue(new Aspect(connectionDetails.data())), + Constants.DATA_PLATFORM_INSTANCE_ASPECT_NAME, + new EnvelopedAspect() + .setName(Constants.DATA_PLATFORM_INSTANCE_ASPECT_NAME) + .setValue(new Aspect(platformInstance.data()))))); + when(entityClient.getV2( + any(OperationContext.class), + Mockito.eq(Constants.DATAHUB_CONNECTION_ENTITY_NAME), + Mockito.eq(connectionUrn), + Mockito.eq( + ImmutableSet.of( + Constants.DATAHUB_CONNECTION_DETAILS_ASPECT_NAME, + Constants.DATA_PLATFORM_INSTANCE_ASPECT_NAME)))) + .thenReturn(response); + + // Execute and assert + DataHubConnectionDetails details = + connectionService.getConnectionDetails(mock(OperationContext.class), connectionUrn); + assertEquals(details, connectionDetails); + } + + @Test + public void testGetConnectionEntityResponse() throws Exception { + final Urn connectionUrn = Mockito.mock(Urn.class); + EntityResponse response = Mockito.mock(EntityResponse.class); + when(entityClient.getV2( + any(OperationContext.class), + Mockito.eq(Constants.DATAHUB_CONNECTION_ENTITY_NAME), + Mockito.eq(connectionUrn), + Mockito.eq( + ImmutableSet.of( + Constants.DATAHUB_CONNECTION_DETAILS_ASPECT_NAME, + Constants.DATA_PLATFORM_INSTANCE_ASPECT_NAME)))) + .thenReturn(response); + // Execute and assert + assertEquals( + connectionService.getConnectionEntityResponse(mock(OperationContext.class), connectionUrn), + response); + } + + private DataHubConnectionDetails mockConnectionDetails(String id) { + return new DataHubConnectionDetails() + .setType(DataHubConnectionDetailsType.JSON) + .setName(id) + .setJson(new DataHubJsonConnection().setEncryptedBlob("blob")); + } + + private DataPlatformInstance mockPlatformInstance(Urn platformUrn) { + return new DataPlatformInstance().setPlatform(platformUrn); + } +} diff --git a/metadata-models/src/main/pegasus/com/linkedin/connection/DataHubConnectionDetails.pdl b/metadata-models/src/main/pegasus/com/linkedin/connection/DataHubConnectionDetails.pdl new file mode 100644 index 0000000000000..81f57abf2dac4 --- /dev/null +++ b/metadata-models/src/main/pegasus/com/linkedin/connection/DataHubConnectionDetails.pdl @@ -0,0 +1,38 @@ +namespace com.linkedin.connection + +import com.linkedin.common.Urn + +/** + * Information about a connection to an external platform. + */ +@Aspect = { + "name": "dataHubConnectionDetails" +} +record DataHubConnectionDetails { + /** + * The type of the connection. This defines the schema / encoding of the connection details. + */ + @Searchable = {} + type: enum DataHubConnectionDetailsType { + /** + * A json-encoded set of connection details + */ + JSON + } + + /** + * Display name of the connection + */ + @Searchable = { + "fieldType": "TEXT_PARTIAL", + "enableAutocomplete": true, + "boostScore": 10.0 + } + name: optional string + + /** + * An JSON payload containing raw connection details. + * This will be present if the type is JSON. + */ + json: optional DataHubJsonConnection +} \ No newline at end of file diff --git a/metadata-models/src/main/pegasus/com/linkedin/connection/DataHubJsonConnection.pdl b/metadata-models/src/main/pegasus/com/linkedin/connection/DataHubJsonConnection.pdl new file mode 100644 index 0000000000000..996e2a3238bd5 --- /dev/null +++ b/metadata-models/src/main/pegasus/com/linkedin/connection/DataHubJsonConnection.pdl @@ -0,0 +1,11 @@ +namespace com.linkedin.connection + +/** + * A set of connection details consisting of an encrypted JSON blob. + */ +record DataHubJsonConnection { + /** + * The encrypted JSON connection details. + */ + encryptedBlob: string +} \ No newline at end of file diff --git a/metadata-models/src/main/pegasus/com/linkedin/metadata/key/DataHubConnectionKey.pdl b/metadata-models/src/main/pegasus/com/linkedin/metadata/key/DataHubConnectionKey.pdl new file mode 100644 index 0000000000000..cd851d8382759 --- /dev/null +++ b/metadata-models/src/main/pegasus/com/linkedin/metadata/key/DataHubConnectionKey.pdl @@ -0,0 +1,15 @@ +namespace com.linkedin.metadata.key + +/** + * Key for a Connection + */ +@Aspect = { + "name": "dataHubConnectionKey" +} +record DataHubConnectionKey { + /** + * A unique identifier for the connection. + */ + @Searchable = {} + id: string +} \ No newline at end of file diff --git a/metadata-models/src/main/resources/entity-registry.yml b/metadata-models/src/main/resources/entity-registry.yml index a9301076d4e82..60ef05ea55b2c 100644 --- a/metadata-models/src/main/resources/entity-registry.yml +++ b/metadata-models/src/main/resources/entity-registry.yml @@ -570,6 +570,12 @@ entities: - formInfo - dynamicFormAssignment - ownership + - name: dataHubConnection + category: internal + keyAspect: dataHubConnectionKey + aspects: + - dataHubConnectionDetails + - dataPlatformInstance events: plugins: aspectPayloadValidators: diff --git a/metadata-service/factories/src/main/java/com/linkedin/gms/factory/connection/ConnectionServiceFactory.java b/metadata-service/factories/src/main/java/com/linkedin/gms/factory/connection/ConnectionServiceFactory.java new file mode 100644 index 0000000000000..07cc59722e91f --- /dev/null +++ b/metadata-service/factories/src/main/java/com/linkedin/gms/factory/connection/ConnectionServiceFactory.java @@ -0,0 +1,19 @@ +package com.linkedin.gms.factory.connection; + +import com.linkedin.entity.client.SystemEntityClient; +import com.linkedin.metadata.connection.ConnectionService; +import javax.annotation.Nonnull; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class ConnectionServiceFactory { + @Bean(name = "connectionService") + @Nonnull + protected ConnectionService getInstance( + @Qualifier("systemEntityClient") final SystemEntityClient systemEntityClient) + throws Exception { + return new ConnectionService(systemEntityClient); + } +} diff --git a/metadata-service/factories/src/main/java/com/linkedin/gms/factory/graphql/GraphQLEngineFactory.java b/metadata-service/factories/src/main/java/com/linkedin/gms/factory/graphql/GraphQLEngineFactory.java index 678d442396d0f..1ac6010be92e5 100644 --- a/metadata-service/factories/src/main/java/com/linkedin/gms/factory/graphql/GraphQLEngineFactory.java +++ b/metadata-service/factories/src/main/java/com/linkedin/gms/factory/graphql/GraphQLEngineFactory.java @@ -21,6 +21,7 @@ import com.linkedin.gms.factory.entityregistry.EntityRegistryFactory; import com.linkedin.gms.factory.recommendation.RecommendationServiceFactory; import com.linkedin.metadata.client.UsageStatsJavaClient; +import com.linkedin.metadata.connection.ConnectionService; import com.linkedin.metadata.entity.EntityService; import com.linkedin.metadata.graph.GraphClient; import com.linkedin.metadata.graph.GraphService; @@ -181,6 +182,10 @@ public class GraphQLEngineFactory { @Qualifier("businessAttributeService") private BusinessAttributeService businessAttributeService; + @Autowired + @Qualifier("connectionService") + private ConnectionService _connectionService; + @Bean(name = "graphQLEngine") @Nonnull protected GraphQLEngine graphQLEngine( @@ -233,6 +238,7 @@ protected GraphQLEngine graphQLEngine( configProvider.getGraphQL().getQuery().getComplexityLimit()); args.setGraphQLQueryDepthLimit(configProvider.getGraphQL().getQuery().getDepthLimit()); args.setBusinessAttributeService(businessAttributeService); + args.setConnectionService(_connectionService); return new GmsGraphQLEngine(args).builder().build(); } } diff --git a/metadata-service/war/src/main/java/com/linkedin/gms/servlet/GraphQLServletConfig.java b/metadata-service/war/src/main/java/com/linkedin/gms/servlet/GraphQLServletConfig.java index 64ec11f58c60d..42413df0757e6 100644 --- a/metadata-service/war/src/main/java/com/linkedin/gms/servlet/GraphQLServletConfig.java +++ b/metadata-service/war/src/main/java/com/linkedin/gms/servlet/GraphQLServletConfig.java @@ -18,7 +18,8 @@ "com.linkedin.gms.factory.query", "com.linkedin.gms.factory.ermodelrelation", "com.linkedin.gms.factory.dataproduct", - "com.linkedin.gms.factory.businessattribute" + "com.linkedin.gms.factory.businessattribute", + "com.linkedin.gms.factory.connection" }) @Configuration public class GraphQLServletConfig {} 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 342c492b01b2e..ea8f52925b5b3 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 @@ -142,6 +142,10 @@ public class PoliciesConfig { "Manage Business Attribute", "Create, update, delete Business Attribute"); + public static final Privilege MANAGE_CONNECTIONS_PRIVILEGE = + Privilege.of( + "MANAGE_CONNECTIONS", "Manage Connections", "Manage connections to external platforms."); + public static final List PLATFORM_PRIVILEGES = ImmutableList.of( MANAGE_POLICIES_PRIVILEGE, @@ -164,7 +168,8 @@ public class PoliciesConfig { MANAGE_GLOBAL_VIEWS, MANAGE_GLOBAL_OWNERSHIP_TYPES, CREATE_BUSINESS_ATTRIBUTE_PRIVILEGE, - MANAGE_BUSINESS_ATTRIBUTE_PRIVILEGE); + MANAGE_BUSINESS_ATTRIBUTE_PRIVILEGE, + MANAGE_CONNECTIONS_PRIVILEGE); // Resource Privileges //