Skip to content

Commit ac20d06

Browse files
ani-malgariAnirudh Reddy Malgari
andauthored
feat: Allow users to add multiple Applications to entities (#15160)
Co-authored-by: Anirudh Reddy Malgari <amalgari@Anirudhs-MacBook-Pro.local>
1 parent 9fbbf20 commit ac20d06

File tree

32 files changed

+1039
-105
lines changed

32 files changed

+1039
-105
lines changed

datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/GmsGraphQLEngine.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import com.linkedin.datahub.graphql.resolvers.MeResolver;
2929
import com.linkedin.datahub.graphql.resolvers.ResolverUtils;
3030
import com.linkedin.datahub.graphql.resolvers.application.BatchSetApplicationResolver;
31+
import com.linkedin.datahub.graphql.resolvers.application.BatchUnsetApplicationResolver;
3132
import com.linkedin.datahub.graphql.resolvers.application.CreateApplicationResolver;
3233
import com.linkedin.datahub.graphql.resolvers.application.DeleteApplicationResolver;
3334
import com.linkedin.datahub.graphql.resolvers.assertion.AssertionRunEventResolver;
@@ -1394,6 +1395,9 @@ private void configureMutationResolvers(final RuntimeWiring.Builder builder) {
13941395
new DeleteApplicationResolver(this.entityClient, this.applicationService))
13951396
.dataFetcher(
13961397
"batchSetApplication", new BatchSetApplicationResolver(this.applicationService))
1398+
.dataFetcher(
1399+
"batchUnsetApplication",
1400+
new BatchUnsetApplicationResolver(this.applicationService))
13971401
.dataFetcher(
13981402
"createOwnershipType", new CreateOwnershipTypeResolver(this.ownershipTypeService))
13991403
.dataFetcher(

datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/application/ApplicationAuthorizationUtils.java

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,16 @@
44
import static com.linkedin.metadata.authorization.ApiOperation.MANAGE;
55

66
import com.datahub.authorization.AuthUtil;
7+
import com.datahub.authorization.ConjunctivePrivilegeGroup;
8+
import com.datahub.authorization.DisjunctivePrivilegeGroup;
9+
import com.google.common.collect.ImmutableList;
10+
import com.linkedin.common.urn.Urn;
11+
import com.linkedin.common.urn.UrnUtils;
712
import com.linkedin.datahub.graphql.QueryContext;
13+
import com.linkedin.datahub.graphql.authorization.AuthorizationUtils;
14+
import com.linkedin.datahub.graphql.exception.AuthorizationException;
15+
import com.linkedin.metadata.authorization.PoliciesConfig;
16+
import com.linkedin.metadata.service.ApplicationService;
817
import java.util.List;
918
import javax.annotation.Nonnull;
1019
import lombok.extern.slf4j.Slf4j;
@@ -22,4 +31,53 @@ public static boolean canManageApplications(@Nonnull QueryContext context) {
2231
return AuthUtil.isAuthorizedEntityType(
2332
context.getOperationContext(), MANAGE, List.of(APPLICATION_ENTITY_NAME));
2433
}
34+
35+
/**
36+
* Verifies that the current user is authorized to edit applications on a specific resource
37+
* entity.
38+
*
39+
* @throws AuthorizationException if the user is not authorized
40+
*/
41+
public static void verifyEditApplicationsAuthorization(
42+
@Nonnull Urn resourceUrn, @Nonnull QueryContext context) {
43+
if (!AuthorizationUtils.isAuthorized(
44+
context,
45+
resourceUrn.getEntityType(),
46+
resourceUrn.toString(),
47+
new DisjunctivePrivilegeGroup(
48+
ImmutableList.of(
49+
new ConjunctivePrivilegeGroup(
50+
ImmutableList.of(PoliciesConfig.EDIT_ENTITY_APPLICATIONS_PRIVILEGE.getType())),
51+
new ConjunctivePrivilegeGroup(
52+
ImmutableList.of(PoliciesConfig.EDIT_ENTITY_PRIVILEGE.getType())))))) {
53+
throw new AuthorizationException(
54+
"Unauthorized to perform this action. Please contact your DataHub administrator.");
55+
}
56+
}
57+
58+
/**
59+
* Verifies that all resources exist and that the current user is authorized to edit applications
60+
* on them.
61+
*
62+
* @param resources List of resource URN strings to verify
63+
* @param applicationService Service to verify entity existence
64+
* @param context Query context with operation context and authorization info
65+
* @param operationName Name of the operation being performed (for error messages)
66+
* @throws RuntimeException if any resource does not exist
67+
* @throws AuthorizationException if the user is not authorized for any resource
68+
*/
69+
public static void verifyResourcesExistAndAuthorized(
70+
@Nonnull List<String> resources,
71+
@Nonnull ApplicationService applicationService,
72+
@Nonnull QueryContext context,
73+
@Nonnull String operationName) {
74+
for (String resource : resources) {
75+
Urn resourceUrn = UrnUtils.getUrn(resource);
76+
if (!applicationService.verifyEntityExists(context.getOperationContext(), resourceUrn)) {
77+
throw new RuntimeException(
78+
String.format("Failed to %s, %s in resources does not exist", operationName, resource));
79+
}
80+
verifyEditApplicationsAuthorization(resourceUrn, context);
81+
}
82+
}
2583
}

datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/application/BatchSetApplicationResolver.java

Lines changed: 2 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,13 @@
11
package com.linkedin.datahub.graphql.resolvers.application;
22

33
import static com.linkedin.datahub.graphql.resolvers.ResolverUtils.bindArgument;
4+
import static com.linkedin.datahub.graphql.resolvers.application.ApplicationAuthorizationUtils.verifyResourcesExistAndAuthorized;
45

5-
import com.datahub.authorization.ConjunctivePrivilegeGroup;
6-
import com.datahub.authorization.DisjunctivePrivilegeGroup;
7-
import com.google.common.collect.ImmutableList;
86
import com.linkedin.common.urn.Urn;
97
import com.linkedin.common.urn.UrnUtils;
108
import com.linkedin.datahub.graphql.QueryContext;
11-
import com.linkedin.datahub.graphql.authorization.AuthorizationUtils;
129
import com.linkedin.datahub.graphql.concurrency.GraphQLConcurrencyUtils;
13-
import com.linkedin.datahub.graphql.exception.AuthorizationException;
1410
import com.linkedin.datahub.graphql.generated.BatchSetApplicationInput;
15-
import com.linkedin.metadata.authorization.PoliciesConfig;
1611
import com.linkedin.metadata.service.ApplicationService;
1712
import graphql.schema.DataFetcher;
1813
import graphql.schema.DataFetchingEnvironment;
@@ -63,29 +58,7 @@ public CompletableFuture<Boolean> get(DataFetchingEnvironment environment) throw
6358
}
6459

6560
private void verifyResources(List<String> resources, QueryContext context) {
66-
for (String resource : resources) {
67-
if (!applicationService.verifyEntityExists(
68-
context.getOperationContext(), UrnUtils.getUrn(resource))) {
69-
throw new RuntimeException(
70-
String.format(
71-
"Failed to batch set Application, %s in resources does not exist", resource));
72-
}
73-
Urn resourceUrn = UrnUtils.getUrn(resource);
74-
if (!AuthorizationUtils.isAuthorized(
75-
context,
76-
resourceUrn.getEntityType(),
77-
resourceUrn.toString(),
78-
new DisjunctivePrivilegeGroup(
79-
ImmutableList.of(
80-
new ConjunctivePrivilegeGroup(
81-
ImmutableList.of(
82-
PoliciesConfig.EDIT_ENTITY_APPLICATIONS_PRIVILEGE.getType())),
83-
new ConjunctivePrivilegeGroup(
84-
ImmutableList.of(PoliciesConfig.EDIT_ENTITY_PRIVILEGE.getType())))))) {
85-
throw new AuthorizationException(
86-
"Unauthorized to perform this action. Please contact your DataHub administrator.");
87-
}
88-
}
61+
verifyResourcesExistAndAuthorized(resources, applicationService, context, "set_application");
8962
}
9063

9164
private void verifyApplication(String maybeApplicationUrn, QueryContext context) {
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
package com.linkedin.datahub.graphql.resolvers.application;
2+
3+
import static com.linkedin.datahub.graphql.resolvers.ResolverUtils.bindArgument;
4+
import static com.linkedin.datahub.graphql.resolvers.application.ApplicationAuthorizationUtils.verifyResourcesExistAndAuthorized;
5+
6+
import com.linkedin.common.urn.Urn;
7+
import com.linkedin.common.urn.UrnUtils;
8+
import com.linkedin.datahub.graphql.QueryContext;
9+
import com.linkedin.datahub.graphql.concurrency.GraphQLConcurrencyUtils;
10+
import com.linkedin.datahub.graphql.generated.BatchUnsetApplicationInput;
11+
import com.linkedin.metadata.service.ApplicationService;
12+
import graphql.schema.DataFetcher;
13+
import graphql.schema.DataFetchingEnvironment;
14+
import java.util.List;
15+
import java.util.concurrent.CompletableFuture;
16+
import java.util.stream.Collectors;
17+
import javax.annotation.Nonnull;
18+
import lombok.RequiredArgsConstructor;
19+
import lombok.extern.slf4j.Slf4j;
20+
21+
@Slf4j
22+
@RequiredArgsConstructor
23+
public class BatchUnsetApplicationResolver implements DataFetcher<CompletableFuture<Boolean>> {
24+
25+
private final ApplicationService applicationService;
26+
27+
@Override
28+
public CompletableFuture<Boolean> get(DataFetchingEnvironment environment) throws Exception {
29+
final QueryContext context = environment.getContext();
30+
final BatchUnsetApplicationInput input =
31+
bindArgument(environment.getArgument("input"), BatchUnsetApplicationInput.class);
32+
final String applicationUrn = input.getApplicationUrn();
33+
final List<String> resources = input.getResourceUrns();
34+
35+
return GraphQLConcurrencyUtils.supplyAsync(
36+
() -> {
37+
verifyResources(resources, context);
38+
verifyApplication(applicationUrn, context);
39+
40+
try {
41+
List<Urn> resourceUrns =
42+
resources.stream().map(UrnUtils::getUrn).collect(Collectors.toList());
43+
batchUnsetApplication(applicationUrn, resourceUrns, context);
44+
return true;
45+
} catch (Exception e) {
46+
log.error("Failed to perform update against input {}, {}", input, e.getMessage());
47+
throw new RuntimeException(
48+
String.format("Failed to perform update against input %s", input), e);
49+
}
50+
},
51+
this.getClass().getSimpleName(),
52+
"get");
53+
}
54+
55+
private void verifyResources(List<String> resources, QueryContext context) {
56+
verifyResourcesExistAndAuthorized(resources, applicationService, context, "unset_application");
57+
}
58+
59+
private void verifyApplication(String applicationUrn, QueryContext context) {
60+
if (!applicationService.verifyEntityExists(
61+
context.getOperationContext(), UrnUtils.getUrn(applicationUrn))) {
62+
throw new RuntimeException(
63+
String.format(
64+
"Failed to batch unset Application, Application urn %s does not exist",
65+
applicationUrn));
66+
}
67+
}
68+
69+
private void batchUnsetApplication(
70+
@Nonnull String applicationUrn, List<Urn> resources, QueryContext context) {
71+
try {
72+
applicationService.batchUnsetApplication(
73+
context.getOperationContext(),
74+
UrnUtils.getUrn(applicationUrn),
75+
resources,
76+
UrnUtils.getUrn(context.getActorUrn()));
77+
} catch (Exception e) {
78+
throw new RuntimeException(
79+
String.format(
80+
"Failed to batch unset Application %s from resources with urns %s!",
81+
applicationUrn, resources),
82+
e);
83+
}
84+
}
85+
}

datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/application/ApplicationAssociationMapper.java

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
import com.linkedin.datahub.graphql.generated.Application;
77
import com.linkedin.datahub.graphql.generated.ApplicationAssociation;
88
import com.linkedin.datahub.graphql.generated.EntityType;
9+
import java.util.List;
10+
import java.util.stream.Collectors;
911
import javax.annotation.Nonnull;
1012
import javax.annotation.Nullable;
1113

@@ -25,6 +27,13 @@ public static ApplicationAssociation map(
2527
return INSTANCE.apply(context, applications, entityUrn);
2628
}
2729

30+
public static List<ApplicationAssociation> mapList(
31+
@Nullable final QueryContext context,
32+
@Nonnull final com.linkedin.application.Applications applications,
33+
@Nonnull final String entityUrn) {
34+
return INSTANCE.applyList(context, applications, entityUrn);
35+
}
36+
2837
public ApplicationAssociation apply(
2938
@Nullable final QueryContext context,
3039
@Nonnull final com.linkedin.application.Applications applications,
@@ -43,4 +52,26 @@ public ApplicationAssociation apply(
4352
}
4453
return null;
4554
}
55+
56+
public List<ApplicationAssociation> applyList(
57+
@Nullable final QueryContext context,
58+
@Nonnull final com.linkedin.application.Applications applications,
59+
@Nonnull final String entityUrn) {
60+
return applications.getApplications().stream()
61+
.filter(
62+
applicationUrn ->
63+
context == null || canView(context.getOperationContext(), applicationUrn))
64+
.map(
65+
applicationUrn -> {
66+
ApplicationAssociation association = new ApplicationAssociation();
67+
association.setApplication(
68+
Application.builder()
69+
.setType(EntityType.APPLICATION)
70+
.setUrn(applicationUrn.toString())
71+
.build());
72+
association.setAssociatedUrn(entityUrn);
73+
return association;
74+
})
75+
.collect(Collectors.toList());
76+
}
4677
}

datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/chart/mappers/ChartMapper.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,12 @@ private static void mapDomains(
314314
private static void mapApplicationAssociation(
315315
@Nullable final QueryContext context, @Nonnull Chart chart, @Nonnull DataMap dataMap) {
316316
final Applications applications = new Applications(dataMap);
317-
chart.setApplication(ApplicationAssociationMapper.map(context, applications, chart.getUrn()));
317+
final java.util.List<com.linkedin.datahub.graphql.generated.ApplicationAssociation>
318+
applicationAssociations =
319+
ApplicationAssociationMapper.mapList(context, applications, chart.getUrn());
320+
chart.setApplications(applicationAssociations);
321+
if (applicationAssociations != null && !applicationAssociations.isEmpty()) {
322+
chart.setApplication(applicationAssociations.get(0));
323+
}
318324
}
319325
}

datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/dashboard/mappers/DashboardMapper.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -308,7 +308,12 @@ private static void mapApplicationAssociation(
308308
@Nonnull Dashboard dashboard,
309309
@Nonnull DataMap dataMap) {
310310
final Applications applications = new Applications(dataMap);
311-
dashboard.setApplication(
312-
ApplicationAssociationMapper.map(context, applications, dashboard.getUrn()));
311+
final java.util.List<com.linkedin.datahub.graphql.generated.ApplicationAssociation>
312+
applicationAssociations =
313+
ApplicationAssociationMapper.mapList(context, applications, dashboard.getUrn());
314+
dashboard.setApplications(applicationAssociations);
315+
if (applicationAssociations != null && !applicationAssociations.isEmpty()) {
316+
dashboard.setApplication(applicationAssociations.get(0));
317+
}
313318
}
314319
}

datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/dataflow/mappers/DataFlowMapper.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,12 @@ private static void mapDomains(
240240
private static void mapApplicationAssociation(
241241
@Nullable final QueryContext context, @Nonnull DataFlow dataFlow, @Nonnull DataMap dataMap) {
242242
final Applications applications = new Applications(dataMap);
243-
dataFlow.setApplication(
244-
ApplicationAssociationMapper.map(context, applications, dataFlow.getUrn()));
243+
final java.util.List<com.linkedin.datahub.graphql.generated.ApplicationAssociation>
244+
applicationAssociations =
245+
ApplicationAssociationMapper.mapList(context, applications, dataFlow.getUrn());
246+
dataFlow.setApplications(applicationAssociations);
247+
if (applicationAssociations != null && !applicationAssociations.isEmpty()) {
248+
dataFlow.setApplication(applicationAssociations.get(0));
249+
}
245250
}
246251
}

datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/datajob/mappers/DataJobMapper.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -236,8 +236,13 @@ private void mapApplicationAssociation(
236236
if (aspectMap.containsKey(APPLICATION_MEMBERSHIP_ASPECT_NAME)) {
237237
final Applications applications =
238238
new Applications(aspectMap.get(APPLICATION_MEMBERSHIP_ASPECT_NAME).getValue().data());
239-
dataJob.setApplication(
240-
ApplicationAssociationMapper.map(context, applications, dataJob.getUrn()));
239+
final java.util.List<com.linkedin.datahub.graphql.generated.ApplicationAssociation>
240+
applicationAssociations =
241+
ApplicationAssociationMapper.mapList(context, applications, dataJob.getUrn());
242+
dataJob.setApplications(applicationAssociations);
243+
if (applicationAssociations != null && !applicationAssociations.isEmpty()) {
244+
dataJob.setApplication(applicationAssociations.get(0));
245+
}
241246
}
242247
}
243248
}

datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/dataproduct/mappers/DataProductMapper.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,12 @@ private static void mapApplicationAssociation(
164164
@Nonnull DataProduct dataProduct,
165165
@Nonnull DataMap dataMap) {
166166
final Applications applications = new Applications(dataMap);
167-
dataProduct.setApplication(
168-
ApplicationAssociationMapper.map(context, applications, dataProduct.getUrn()));
167+
final java.util.List<com.linkedin.datahub.graphql.generated.ApplicationAssociation>
168+
applicationAssociations =
169+
ApplicationAssociationMapper.mapList(context, applications, dataProduct.getUrn());
170+
dataProduct.setApplications(applicationAssociations);
171+
if (applicationAssociations != null && !applicationAssociations.isEmpty()) {
172+
dataProduct.setApplication(applicationAssociations.get(0));
173+
}
169174
}
170175
}

0 commit comments

Comments
 (0)