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 //