Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(policies): Allow policies to be applied to resources based on tags #9684

Merged
merged 2 commits into from
Jan 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,7 @@ public enum EntityFieldType {
/** Groups of which the entity (only applies to corpUser) is a member */
GROUP_MEMBERSHIP,
/** Data platform instance of resource */
DATA_PLATFORM_INSTANCE
DATA_PLATFORM_INSTANCE,
/** Tags of the entity */
TAG,
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import com.datahub.authorization.fieldresolverprovider.EntityUrnFieldResolverProvider;
import com.datahub.authorization.fieldresolverprovider.GroupMembershipFieldResolverProvider;
import com.datahub.authorization.fieldresolverprovider.OwnerFieldResolverProvider;
import com.datahub.authorization.fieldresolverprovider.TagFieldResolverProvider;
import com.google.common.collect.ImmutableList;
import com.linkedin.entity.client.EntityClient;
import com.linkedin.util.Pair;
Expand All @@ -26,7 +27,8 @@ public DefaultEntitySpecResolver(Authentication systemAuthentication, EntityClie
new DomainFieldResolverProvider(entityClient, systemAuthentication),
new OwnerFieldResolverProvider(entityClient, systemAuthentication),
new DataPlatformInstanceFieldResolverProvider(entityClient, systemAuthentication),
new GroupMembershipFieldResolverProvider(entityClient, systemAuthentication));
new GroupMembershipFieldResolverProvider(entityClient, systemAuthentication),
new TagFieldResolverProvider(entityClient, systemAuthentication));
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an inefficient design. The urns and their aspect reads should be batched and not fetching each aspect one by one for each urn. I know this isn't part of the scope for this PR, but authentication causes latency on practically every api call so it has a high impact. Even if we don't batch urns, we should definitely batch the aspects There should be a batched resolver for domain, owner, data platform, group membership, and tags. All these aspects should be fetched in batch using the entity client's same getV2 method just using the collection as something other then a single item Collections.singleton(

This is a perfect opportunity to address the tech debt here by adding the extra aspect and improve performance.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After reading the adjacent code, I don't think we are sequentially performing each of these resolvers for each request. Rather in getFieldResolvers (in this file) we are creating a dictionary of what query to run based on the EntityFieldType that an incoming request has in their filter.

See

return fieldResolvers.get(entityFieldType).getFieldValuesFuture().join().getValues();

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That line is executing a request per field meaning separate requests for each aspect. It is using futures, but not batching.

Copy link
Collaborator Author

@pedro93 pedro93 Jan 22, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fieldResolvers.get(entityFieldType) is only getting 1 resolver and generating 1 future, which depends on the filter being analysed.

I think this is the line responsible for multiple possible requests:

return filter.getCriteria().stream().allMatch(criterion -> checkCriterion(criterion, resource));

I think any performance improvement here would come from policies that have multiple filter conditions like:
contains tag X AND is part of Domain Y AND is OwnedBy z, here 3 calls would happen sequentially (one of the them, domain, is out of scope). The remaining filterResolvers are either not used or make no call to the entity service.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, if a single policy operates on 3 fields it would benefit from fetching all three at the same time. If there are multiple policies which depend on 3 different fields, it would also benefit from fetching all three at the same time. The general idea is to fetch the required aspects in 1 call for the policies being evaluated rather then 3 subsequent calls. I am assuming the same resource is being passed to each policy and that we would not be fetching the same aspect for each policy already today.

Copy link
Collaborator Author

@pedro93 pedro93 Jan 22, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At this point in the code, only a single Policy’s filter gets evaluated.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Going down the route of evaluating all policies to check whether they apply for a given resource request is a large refactor of the policy engine. I would suggest a design document before doing anything else.

@RyanHolstien @david-leifker do we have metrics on how much the PolicyEngine is a performance bottleneck?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens in this case if no policies are authored with any tag predicates on them? Does the code performance stay the same as today or degrade?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code performs the same as today. The field resolver for tags is not invoked.

}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package com.datahub.authorization.fieldresolverprovider;

import com.datahub.authentication.Authentication;
import com.datahub.authorization.EntityFieldType;
import com.datahub.authorization.EntitySpec;
import com.datahub.authorization.FieldResolver;
import com.linkedin.common.GlobalTags;
import com.linkedin.common.urn.Urn;
import com.linkedin.common.urn.UrnUtils;
import com.linkedin.entity.EntityResponse;
import com.linkedin.entity.EnvelopedAspect;
import com.linkedin.entity.client.EntityClient;
import com.linkedin.metadata.Constants;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

/** Provides field resolver for owners given entitySpec */
@Slf4j
@RequiredArgsConstructor
public class TagFieldResolverProvider implements EntityFieldResolverProvider {

private final EntityClient _entityClient;
private final Authentication _systemAuthentication;

@Override
public List<EntityFieldType> getFieldTypes() {
return Collections.singletonList(EntityFieldType.TAG);
}

@Override
public FieldResolver getFieldResolver(EntitySpec entitySpec) {
return FieldResolver.getResolverFromFunction(entitySpec, this::getTags);
}

private FieldResolver.FieldValue getTags(EntitySpec entitySpec) {
Urn entityUrn = UrnUtils.getUrn(entitySpec.getEntity());
EnvelopedAspect globalTagsAspect;
try {
EntityResponse response =
_entityClient.getV2(
entityUrn.getEntityType(),
entityUrn,
Collections.singleton(Constants.GLOBAL_TAGS_ASPECT_NAME),
_systemAuthentication);
if (response == null
|| !response.getAspects().containsKey(Constants.GLOBAL_TAGS_ASPECT_NAME)) {
return FieldResolver.emptyFieldValue();
}
globalTagsAspect = response.getAspects().get(Constants.GLOBAL_TAGS_ASPECT_NAME);
} catch (Exception e) {
log.error("Error while retrieving tags aspect for urn {}", entityUrn, e);
return FieldResolver.emptyFieldValue();
}
GlobalTags globalTags = new GlobalTags(globalTagsAspect.getValue().data());
return FieldResolver.FieldValue.builder()
.values(
globalTags.getTags().stream()
.map(tag -> tag.getTag().toString())
.collect(Collectors.toSet()))
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ public class PolicyEngineTest {
private static final String AUTHORIZED_GROUP = "urn:li:corpGroup:authorizedGroup";
private static final String RESOURCE_URN = "urn:li:dataset:test";
private static final String DOMAIN_URN = "urn:li:domain:domain1";
private static final String TAG_URN = "urn:li:tag:allowed";
private static final String OWNERSHIP_TYPE_URN = "urn:li:ownershipType:__system__technical_owner";
private static final String OTHER_OWNERSHIP_TYPE_URN =
"urn:li:ownershipType:__system__data_steward";
Expand All @@ -69,7 +70,8 @@ public void setupTest() throws Exception {
AUTHORIZED_PRINCIPAL,
Collections.emptySet(),
Collections.emptySet(),
Collections.singleton(AUTHORIZED_GROUP));
Collections.singleton(AUTHORIZED_GROUP),
Collections.emptySet());
unauthorizedUserUrn = Urn.createFromString(UNAUTHORIZED_PRINCIPAL);
resolvedUnauthorizedUserSpec =
buildEntityResolvers(CORP_USER_ENTITY_NAME, UNAUTHORIZED_PRINCIPAL);
Expand Down Expand Up @@ -595,6 +597,7 @@ public void testEvaluatePolicyActorFilterUserResourceOwnersMatch() throws Except
RESOURCE_URN,
ImmutableSet.of(AUTHORIZED_PRINCIPAL),
Collections.emptySet(),
Collections.emptySet(),
Collections.emptySet());
// Assert authorized user can edit entity tags, because he is a user owner.
PolicyEngine.PolicyEvaluationResult result1 =
Expand Down Expand Up @@ -653,6 +656,7 @@ public void testEvaluatePolicyActorFilterUserResourceOwnersTypeMatch() throws Ex
RESOURCE_URN,
ImmutableSet.of(AUTHORIZED_PRINCIPAL),
Collections.emptySet(),
Collections.emptySet(),
Collections.emptySet());

PolicyEngine.PolicyEvaluationResult result1 =
Expand Down Expand Up @@ -712,6 +716,7 @@ public void testEvaluatePolicyActorFilterUserResourceOwnersTypeNoMatch() throws
RESOURCE_URN,
ImmutableSet.of(AUTHORIZED_PRINCIPAL),
Collections.emptySet(),
Collections.emptySet(),
Collections.emptySet());

PolicyEngine.PolicyEvaluationResult result1 =
Expand Down Expand Up @@ -767,6 +772,7 @@ public void testEvaluatePolicyActorFilterGroupResourceOwnersMatch() throws Excep
RESOURCE_URN,
ImmutableSet.of(AUTHORIZED_GROUP),
Collections.emptySet(),
Collections.emptySet(),
Collections.emptySet());
// Assert authorized user can edit entity tags, because he is a user owner.
PolicyEngine.PolicyEvaluationResult result1 =
Expand Down Expand Up @@ -1037,6 +1043,7 @@ public void testEvaluatePolicyResourceFilterSpecificResourceMatchDomain() throws
RESOURCE_URN,
Collections.emptySet(),
Collections.singleton(DOMAIN_URN),
Collections.emptySet(),
Collections.emptySet());
PolicyEngine.PolicyEvaluationResult result =
_policyEngine.evaluatePolicy(
Expand Down Expand Up @@ -1082,6 +1089,7 @@ public void testEvaluatePolicyResourceFilterSpecificResourceNoMatchDomain() thro
RESOURCE_URN,
Collections.emptySet(),
Collections.singleton("urn:li:domain:domain2"),
Collections.emptySet(),
Collections.emptySet()); // Domain doesn't match
PolicyEngine.PolicyEvaluationResult result =
_policyEngine.evaluatePolicy(
Expand All @@ -1095,6 +1103,52 @@ public void testEvaluatePolicyResourceFilterSpecificResourceNoMatchDomain() thro
verify(_entityClient, times(0)).batchGetV2(any(), any(), any(), any());
}

@Test
public void testEvaluatePolicyResourceFilterSpecificResourceMatchTag() throws Exception {
final DataHubPolicyInfo dataHubPolicyInfo = new DataHubPolicyInfo();
dataHubPolicyInfo.setType(METADATA_POLICY_TYPE);
dataHubPolicyInfo.setState(ACTIVE_POLICY_STATE);
dataHubPolicyInfo.setPrivileges(new StringArray("VIEW_ENTITY_PAGE"));
dataHubPolicyInfo.setDisplayName("Tag-based policy");
dataHubPolicyInfo.setDescription("Allow viewing entity pages based on tags");
dataHubPolicyInfo.setEditable(true);

final DataHubActorFilter actorFilter = new DataHubActorFilter();
actorFilter.setResourceOwners(true);
actorFilter.setAllUsers(true);
actorFilter.setAllGroups(true);
dataHubPolicyInfo.setActors(actorFilter);

final DataHubResourceFilter resourceFilter = new DataHubResourceFilter();
resourceFilter.setFilter(
FilterUtils.newFilter(
ImmutableMap.of(
EntityFieldType.TYPE,
Collections.singletonList("dataset"),
EntityFieldType.TAG,
Collections.singletonList(TAG_URN))));
dataHubPolicyInfo.setResources(resourceFilter);

ResolvedEntitySpec resourceSpec =
buildEntityResolvers(
"dataset",
RESOURCE_URN,
Collections.emptySet(),
Collections.emptySet(),
Collections.emptySet(),
Collections.singleton(TAG_URN));
PolicyEngine.PolicyEvaluationResult result =
_policyEngine.evaluatePolicy(
dataHubPolicyInfo,
resolvedAuthorizedUserSpec,
"VIEW_ENTITY_PAGE",
Optional.of(resourceSpec));
assertTrue(result.isGranted());

// Verify no network calls
verify(_entityClient, times(0)).batchGetV2(any(), any(), any(), any());
}

@Test
public void testGetGrantedPrivileges() throws Exception {
// Policy 1, match dataset type and domain
Expand Down Expand Up @@ -1180,6 +1234,7 @@ public void testGetGrantedPrivileges() throws Exception {
RESOURCE_URN,
Collections.emptySet(),
Collections.singleton(DOMAIN_URN),
Collections.emptySet(),
Collections.emptySet()); // Everything matches
assertEquals(
_policyEngine.getGrantedPrivileges(
Expand All @@ -1192,6 +1247,7 @@ public void testGetGrantedPrivileges() throws Exception {
RESOURCE_URN,
Collections.emptySet(),
Collections.singleton("urn:li:domain:domain2"),
Collections.emptySet(),
Collections.emptySet()); // Domain doesn't match
assertEquals(
_policyEngine.getGrantedPrivileges(
Expand All @@ -1204,6 +1260,7 @@ public void testGetGrantedPrivileges() throws Exception {
"urn:li:dataset:random",
Collections.emptySet(),
Collections.singleton(DOMAIN_URN),
Collections.emptySet(),
Collections.emptySet()); // Resource doesn't match
assertEquals(
_policyEngine.getGrantedPrivileges(
Expand All @@ -1228,6 +1285,7 @@ public void testGetGrantedPrivileges() throws Exception {
RESOURCE_URN,
Collections.singleton(AUTHORIZED_PRINCIPAL),
Collections.singleton(DOMAIN_URN),
Collections.emptySet(),
Collections.emptySet()); // Is owner
assertEquals(
_policyEngine.getGrantedPrivileges(
Expand All @@ -1240,6 +1298,7 @@ public void testGetGrantedPrivileges() throws Exception {
RESOURCE_URN,
Collections.singleton(AUTHORIZED_PRINCIPAL),
Collections.singleton(DOMAIN_URN),
Collections.emptySet(),
Collections.emptySet()); // Resource type doesn't match
assertEquals(
_policyEngine.getGrantedPrivileges(
Expand Down Expand Up @@ -1289,6 +1348,7 @@ public void testGetMatchingActorsResourceMatch() throws Exception {
RESOURCE_URN,
ImmutableSet.of(AUTHORIZED_PRINCIPAL, AUTHORIZED_GROUP),
Collections.emptySet(),
Collections.emptySet(),
Collections.emptySet());
PolicyEngine.PolicyActors actors =
_policyEngine.getMatchingActors(dataHubPolicyInfo, Optional.of(resourceSpec));
Expand Down Expand Up @@ -1406,6 +1466,7 @@ public void testGetMatchingActorsByRoleResourceMatch() throws Exception {
RESOURCE_URN,
ImmutableSet.of(),
Collections.emptySet(),
Collections.emptySet(),
Collections.emptySet());

PolicyEngine.PolicyActors actors =
Expand Down Expand Up @@ -1506,6 +1567,7 @@ public static ResolvedEntitySpec buildEntityResolvers(String entityType, String
entityUrn,
Collections.emptySet(),
Collections.emptySet(),
Collections.emptySet(),
Collections.emptySet());
}

Expand All @@ -1514,7 +1576,8 @@ public static ResolvedEntitySpec buildEntityResolvers(
String entityUrn,
Set<String> owners,
Set<String> domains,
Set<String> groups) {
Set<String> groups,
Set<String> tags) {
return new ResolvedEntitySpec(
new EntitySpec(entityType, entityUrn),
ImmutableMap.of(
Expand All @@ -1527,6 +1590,8 @@ public static ResolvedEntitySpec buildEntityResolvers(
EntityFieldType.DOMAIN,
FieldResolver.getResolverFromValues(domains),
EntityFieldType.GROUP_MEMBERSHIP,
FieldResolver.getResolverFromValues(groups)));
FieldResolver.getResolverFromValues(groups),
EntityFieldType.TAG,
FieldResolver.getResolverFromValues(tags)));
}
}
Loading
Loading