Skip to content

Commit 70b1423

Browse files
anshbansalpedro93
andauthored
feat(access): Experimental policy debugger (datahub-project#9833)
Co-authored-by: Pedro Silva <[email protected]>
1 parent 23e9e94 commit 70b1423

File tree

5 files changed

+318
-2
lines changed

5 files changed

+318
-2
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -975,6 +975,7 @@ private void configureQueryResolvers(final RuntimeWiring.Builder builder) {
975975
.dataFetcher(
976976
"getAccessTokenMetadata",
977977
new GetAccessTokenMetadataResolver(statefulTokenService, this.entityClient))
978+
.dataFetcher("debugAccess", new DebugAccessResolver(this.entityClient, graphClient))
978979
.dataFetcher("container", getResolver(containerType))
979980
.dataFetcher("listDomains", new ListDomainsResolver(this.entityClient))
980981
.dataFetcher("listSecrets", new ListSecretsResolver(this.entityClient))
Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
package com.linkedin.datahub.graphql.resolvers.auth;
2+
3+
import static com.linkedin.metadata.Constants.*;
4+
5+
import com.google.common.collect.ImmutableSet;
6+
import com.linkedin.common.EntityRelationship;
7+
import com.linkedin.common.EntityRelationships;
8+
import com.linkedin.common.urn.Urn;
9+
import com.linkedin.data.template.StringArray;
10+
import com.linkedin.datahub.graphql.QueryContext;
11+
import com.linkedin.datahub.graphql.authorization.AuthorizationUtils;
12+
import com.linkedin.datahub.graphql.exception.AuthorizationException;
13+
import com.linkedin.datahub.graphql.generated.DebugAccessResult;
14+
import com.linkedin.entity.EntityResponse;
15+
import com.linkedin.entity.client.EntityClient;
16+
import com.linkedin.metadata.Constants;
17+
import com.linkedin.metadata.graph.GraphClient;
18+
import com.linkedin.metadata.query.filter.Condition;
19+
import com.linkedin.metadata.query.filter.ConjunctiveCriterion;
20+
import com.linkedin.metadata.query.filter.ConjunctiveCriterionArray;
21+
import com.linkedin.metadata.query.filter.Criterion;
22+
import com.linkedin.metadata.query.filter.CriterionArray;
23+
import com.linkedin.metadata.query.filter.Filter;
24+
import com.linkedin.metadata.query.filter.RelationshipDirection;
25+
import com.linkedin.metadata.query.filter.SortCriterion;
26+
import com.linkedin.metadata.query.filter.SortOrder;
27+
import com.linkedin.metadata.search.SearchEntity;
28+
import com.linkedin.policy.DataHubPolicyInfo;
29+
import com.linkedin.r2.RemoteInvocationException;
30+
import graphql.schema.DataFetcher;
31+
import graphql.schema.DataFetchingEnvironment;
32+
import java.net.URISyntaxException;
33+
import java.util.ArrayList;
34+
import java.util.Arrays;
35+
import java.util.HashSet;
36+
import java.util.List;
37+
import java.util.Map;
38+
import java.util.Objects;
39+
import java.util.Set;
40+
import java.util.concurrent.CompletableFuture;
41+
import java.util.stream.Collectors;
42+
43+
public class DebugAccessResolver implements DataFetcher<CompletableFuture<DebugAccessResult>> {
44+
45+
private static final String LAST_UPDATED_AT_FIELD = "lastUpdatedTimestamp";
46+
private final EntityClient _entityClient;
47+
private final GraphClient _graphClient;
48+
49+
public DebugAccessResolver(EntityClient entityClient, GraphClient graphClient) {
50+
_entityClient = entityClient;
51+
_graphClient = graphClient;
52+
}
53+
54+
@Override
55+
public CompletableFuture<DebugAccessResult> get(DataFetchingEnvironment environment)
56+
throws Exception {
57+
return CompletableFuture.supplyAsync(
58+
() -> {
59+
final QueryContext context = environment.getContext();
60+
61+
if (!AuthorizationUtils.canManageUsersAndGroups(context)) {
62+
throw new AuthorizationException(
63+
"Unauthorized to perform this action. Please contact your DataHub administrator.");
64+
}
65+
final String userUrn = environment.getArgument("userUrn");
66+
67+
return populateDebugAccessResult(userUrn, context);
68+
});
69+
}
70+
71+
public DebugAccessResult populateDebugAccessResult(String userUrn, QueryContext context) {
72+
73+
try {
74+
final String actorUrn = context.getActorUrn();
75+
final DebugAccessResult result = new DebugAccessResult();
76+
final List<String> types =
77+
Arrays.asList("IsMemberOfRole", "IsMemberOfGroup", "IsMemberOfNativeGroup");
78+
EntityRelationships entityRelationships = getEntityRelationships(userUrn, types, actorUrn);
79+
80+
List<String> roles =
81+
getUrnsFromEntityRelationships(entityRelationships, Constants.DATAHUB_ROLE_ENTITY_NAME);
82+
83+
List<String> groups =
84+
getUrnsFromEntityRelationships(entityRelationships, Constants.CORP_GROUP_ENTITY_NAME);
85+
List<String> groupsWithRoles = new ArrayList<>();
86+
87+
Set<String> rolesViaGroups = new HashSet<>();
88+
groups.forEach(
89+
groupUrn -> {
90+
EntityRelationships groupRelationships =
91+
getEntityRelationships(groupUrn, List.of("IsMemberOfRole"), actorUrn);
92+
List<String> rolesOfGroup =
93+
getUrnsFromEntityRelationships(
94+
groupRelationships, Constants.DATAHUB_ROLE_ENTITY_NAME);
95+
if (rolesOfGroup.isEmpty()) {
96+
return;
97+
}
98+
groupsWithRoles.add(groupUrn);
99+
rolesViaGroups.addAll(rolesOfGroup);
100+
});
101+
Set<String> allRoles = new HashSet<>(roles);
102+
allRoles.addAll(rolesViaGroups);
103+
104+
result.setRoles(roles);
105+
result.setGroups(groups);
106+
result.setGroupsWithRoles(groupsWithRoles);
107+
result.setRolesViaGroups(new ArrayList<>(rolesViaGroups));
108+
result.setAllRoles(new ArrayList<>(allRoles));
109+
110+
Set<Urn> policyUrns = getPoliciesFor(context, userUrn, groups, result.getAllRoles());
111+
112+
// List of Policy that apply to this user directly or indirectly.
113+
result.setPolicies(policyUrns.stream().map(Urn::toString).collect(Collectors.toList()));
114+
115+
// List of privileges that this user has directly or indirectly.
116+
result.setPrivileges(new ArrayList<>(getPrivileges(context, policyUrns)));
117+
118+
return result;
119+
} catch (Exception e) {
120+
throw new RuntimeException(e);
121+
}
122+
}
123+
124+
private Set<String> getPrivileges(final QueryContext context, Set<Urn> policyUrns) {
125+
try {
126+
final Map<Urn, EntityResponse> policies =
127+
_entityClient.batchGetV2(
128+
Constants.POLICY_ENTITY_NAME,
129+
policyUrns,
130+
ImmutableSet.of(Constants.DATAHUB_POLICY_INFO_ASPECT_NAME),
131+
context.getAuthentication());
132+
133+
return policies.keySet().stream()
134+
.filter(Objects::nonNull)
135+
.filter(key -> policies.get(key) != null)
136+
.filter(key -> policies.get(key).hasAspects())
137+
.map(key -> policies.get(key).getAspects())
138+
.filter(aspectMap -> aspectMap.containsKey(DATAHUB_POLICY_INFO_ASPECT_NAME))
139+
.map(
140+
aspectMap ->
141+
new DataHubPolicyInfo(
142+
aspectMap.get(DATAHUB_POLICY_INFO_ASPECT_NAME).getValue().data()))
143+
.map(DataHubPolicyInfo::getPrivileges)
144+
.flatMap(List::stream)
145+
.collect(Collectors.toSet());
146+
} catch (URISyntaxException | RemoteInvocationException e) {
147+
throw new RuntimeException("Failed to retrieve privileges from GMS", e);
148+
}
149+
}
150+
151+
private List<String> getUrnsFromEntityRelationships(
152+
EntityRelationships entityRelationships, String entityName) {
153+
return entityRelationships.getRelationships().stream()
154+
.map(EntityRelationship::getEntity)
155+
.filter(entity -> entityName.equals(entity.getEntityType()))
156+
.map(Urn::toString)
157+
.distinct()
158+
.collect(Collectors.toList());
159+
}
160+
161+
private EntityRelationships getEntityRelationships(
162+
final String urn, final List<String> types, final String actor) {
163+
return _graphClient.getRelatedEntities(
164+
urn, types, RelationshipDirection.OUTGOING, 0, 100, actor);
165+
}
166+
167+
private Set<Urn> getPoliciesFor(
168+
final QueryContext context,
169+
final String user,
170+
final List<String> groups,
171+
final List<String> roles)
172+
throws RemoteInvocationException {
173+
final SortCriterion sortCriterion =
174+
new SortCriterion().setField(LAST_UPDATED_AT_FIELD).setOrder(SortOrder.DESCENDING);
175+
return _entityClient
176+
.search(
177+
context.getOperationContext().withSearchFlags(flags -> flags.setFulltext(true)),
178+
Constants.POLICY_ENTITY_NAME,
179+
"",
180+
buildFilterToGetPolicies(user, groups, roles),
181+
sortCriterion,
182+
0,
183+
10000)
184+
.getEntities()
185+
.stream()
186+
.map(SearchEntity::getEntity)
187+
.collect(Collectors.toSet());
188+
}
189+
190+
private Filter buildFilterToGetPolicies(
191+
final String user, final List<String> groups, final List<String> roles) {
192+
193+
// setOr(array(andArray(user), andArray(groups), andArray(roles), andArray(allUsers),
194+
// andArray(allGroups))
195+
ConjunctiveCriterionArray conjunctiveCriteria = new ConjunctiveCriterionArray();
196+
197+
final CriterionArray allUsersAndArray = new CriterionArray();
198+
allUsersAndArray.add(
199+
new Criterion().setField("allUsers").setValue("true").setCondition(Condition.EQUAL));
200+
conjunctiveCriteria.add(new ConjunctiveCriterion().setAnd(allUsersAndArray));
201+
202+
final CriterionArray allGroupsAndArray = new CriterionArray();
203+
allGroupsAndArray.add(
204+
new Criterion().setField("allGroups").setValue("true").setCondition(Condition.EQUAL));
205+
conjunctiveCriteria.add(new ConjunctiveCriterion().setAnd(allGroupsAndArray));
206+
207+
if (user != null && !user.isEmpty()) {
208+
final CriterionArray userAndArray = new CriterionArray();
209+
userAndArray.add(
210+
new Criterion().setField("users").setValue(user).setCondition(Condition.EQUAL));
211+
conjunctiveCriteria.add(new ConjunctiveCriterion().setAnd(userAndArray));
212+
}
213+
214+
if (groups != null && !groups.isEmpty()) {
215+
final CriterionArray groupsAndArray = new CriterionArray();
216+
groupsAndArray.add(
217+
new Criterion()
218+
.setField("groups")
219+
.setValue("")
220+
.setValues(new StringArray(groups))
221+
.setCondition(Condition.EQUAL));
222+
conjunctiveCriteria.add(new ConjunctiveCriterion().setAnd(groupsAndArray));
223+
}
224+
225+
if (roles != null && !roles.isEmpty()) {
226+
final CriterionArray rolesAndArray = new CriterionArray();
227+
rolesAndArray.add(
228+
new Criterion()
229+
.setField("roles")
230+
.setValue("")
231+
.setValues(new StringArray(roles))
232+
.setCondition(Condition.EQUAL));
233+
conjunctiveCriteria.add(new ConjunctiveCriterion().setAnd(rolesAndArray));
234+
}
235+
return new Filter().setOr(conjunctiveCriteria);
236+
}
237+
}

datahub-graphql-core/src/main/resources/auth.graphql

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,13 @@ extend type Query {
1717
This is useful to debug when you have a raw token but don't know the actor.
1818
"""
1919
getAccessTokenMetadata(token: String!): AccessTokenMetadata!
20+
21+
"""
22+
Experimental API to debug Access for users.
23+
Backward incompatible changes will be made without notice in the future.
24+
Do not build on top of this API.
25+
"""
26+
debugAccess(userUrn: String!): DebugAccessResult!
2027
}
2128

2229
extend type Mutation {
@@ -280,3 +287,46 @@ type EntityPrivileges {
280287
"""
281288
canEditProperties: Boolean
282289
}
290+
291+
"""
292+
Experimental API result to debug Access for users.
293+
Backward incompatible changes will be made without notice in the future.
294+
"""
295+
type DebugAccessResult {
296+
"""
297+
Roles that the user has.
298+
"""
299+
roles: [String!]!
300+
301+
"""
302+
Groups that the user belongs to.
303+
"""
304+
groups: [String!]!
305+
306+
"""
307+
List of groups that the user is assigned to AND where the group has a role.
308+
This is a subset of the groups property.
309+
"""
310+
groupsWithRoles: [String!]!
311+
312+
"""
313+
Final set of roles that are coming through groups.
314+
If not role assigned to groups, then this would be empty.
315+
"""
316+
rolesViaGroups: [String!]!
317+
318+
"""
319+
Union of `roles` + `rolesViaGroups` that the user has.
320+
"""
321+
allRoles: [String!]!
322+
323+
"""
324+
List of Policy that apply to this user directly or indirectly.
325+
"""
326+
policies: [String!]!
327+
328+
"""
329+
List of privileges that this user has directly or indirectly.
330+
"""
331+
privileges: [String!]!
332+
}

datahub-upgrade/src/main/java/com/linkedin/datahub/upgrade/system/policyfields/BackfillPolicyFieldsStep.java

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@
4545
*/
4646
@Slf4j
4747
public class BackfillPolicyFieldsStep implements UpgradeStep {
48-
private static final String UPGRADE_ID = "BackfillPolicyFieldsStep";
48+
private static final String UPGRADE_ID = "BackfillPolicyFieldsStep_V2";
4949
private static final Urn UPGRADE_ID_URN = BootstrapStep.getUpgradeUrn(UPGRADE_ID);
5050

5151
private final OperationContext opContext;
@@ -174,13 +174,20 @@ private String backfillPolicies(AuditStamp auditStamp, String scrollId) {
174174
}
175175

176176
private Filter backfillPolicyFieldFilter() {
177-
// Condition: Does not have at least 1 of: `privileges`, `editable`, `state` or `type`
177+
// Condition: Does not have at least 1 of: `privileges`, `editable`, `state`, `type`, `users`,
178+
// `groups`, `allUsers`
179+
// `allGroups` or `roles`
178180
ConjunctiveCriterionArray conjunctiveCriterionArray = new ConjunctiveCriterionArray();
179181

180182
conjunctiveCriterionArray.add(getCriterionForMissingField("privilege"));
181183
conjunctiveCriterionArray.add(getCriterionForMissingField("editable"));
182184
conjunctiveCriterionArray.add(getCriterionForMissingField("state"));
183185
conjunctiveCriterionArray.add(getCriterionForMissingField("type"));
186+
conjunctiveCriterionArray.add(getCriterionForMissingField("users"));
187+
conjunctiveCriterionArray.add(getCriterionForMissingField("groups"));
188+
conjunctiveCriterionArray.add(getCriterionForMissingField("roles"));
189+
conjunctiveCriterionArray.add(getCriterionForMissingField("allUsers"));
190+
conjunctiveCriterionArray.add(getCriterionForMissingField("allGroups"));
184191

185192
Filter filter = new Filter();
186193
filter.setOr(conjunctiveCriterionArray);

metadata-models/src/main/pegasus/com/linkedin/policy/DataHubActorFilter.pdl

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,21 @@ record DataHubActorFilter {
1111
/**
1212
* A specific set of users to apply the policy to (disjunctive)
1313
*/
14+
@Searchable = {
15+
"/*": {
16+
"fieldType": "URN",
17+
}
18+
}
1419
users: optional array[Urn]
1520

1621
/**
1722
* A specific set of groups to apply the policy to (disjunctive)
1823
*/
24+
@Searchable = {
25+
"/*": {
26+
"fieldType": "URN",
27+
}
28+
}
1929
groups: optional array[Urn]
2030

2131
/**
@@ -32,11 +42,17 @@ record DataHubActorFilter {
3242
/**
3343
* Whether the filter should apply to all users.
3444
*/
45+
@Searchable = {
46+
"fieldType": "BOOLEAN"
47+
}
3548
allUsers: boolean = false
3649

3750
/**
3851
* Whether the filter should apply to all groups.
3952
*/
53+
@Searchable = {
54+
"fieldType": "BOOLEAN"
55+
}
4056
allGroups: boolean = false
4157

4258
/**
@@ -48,5 +64,10 @@ record DataHubActorFilter {
4864
"entityTypes": [ "dataHubRole" ]
4965
}
5066
}
67+
@Searchable = {
68+
"/*": {
69+
"fieldType": "URN",
70+
}
71+
}
5172
roles: optional array[Urn]
5273
}

0 commit comments

Comments
 (0)