diff --git a/datahub-graphql-core/src/main/resources/entity.graphql b/datahub-graphql-core/src/main/resources/entity.graphql index a2e2fe9163f53..fd112c9524ac9 100644 --- a/datahub-graphql-core/src/main/resources/entity.graphql +++ b/datahub-graphql-core/src/main/resources/entity.graphql @@ -9157,6 +9157,10 @@ enum PolicyMatchCondition { Whether the field matches the value """ EQUALS + """ + Whether the field value starts with the value + """ + STARTS_WITH } """ diff --git a/datahub-web-react/src/app/permissions/policy/PolicyDetailsModal.tsx b/datahub-web-react/src/app/permissions/policy/PolicyDetailsModal.tsx index 88a1388ba9589..37349585fa4c9 100644 --- a/datahub-web-react/src/app/permissions/policy/PolicyDetailsModal.tsx +++ b/datahub-web-react/src/app/permissions/policy/PolicyDetailsModal.tsx @@ -3,9 +3,14 @@ import { Link } from 'react-router-dom'; import { Button, Divider, Modal, Tag, Typography } from 'antd'; import styled from 'styled-components'; import { useEntityRegistry } from '../../useEntityRegistry'; -import { Maybe, Policy, PolicyState, PolicyType } from '../../../types.generated'; +import { Maybe, Policy, PolicyMatchCondition, PolicyState, PolicyType } from '../../../types.generated'; import { useAppConfig } from '../../useAppConfig'; -import { convertLegacyResourceFilter, getFieldValues, mapResourceTypeToDisplayName } from './policyUtils'; +import { + convertLegacyResourceFilter, + getFieldValues, + getFieldCondition, + mapResourceTypeToDisplayName, +} from './policyUtils'; import AvatarsGroup from '../AvatarsGroup'; type PrivilegeOptionType = { @@ -70,6 +75,7 @@ export default function PolicyDetailsModal({ policy, open, onClose, privileges } const resourceTypes = getFieldValues(resources?.filter, 'TYPE') || []; const dataPlatformInstances = getFieldValues(resources?.filter, 'DATA_PLATFORM_INSTANCE') || []; const resourceEntities = getFieldValues(resources?.filter, 'URN') || []; + const resourceFilterCondition = getFieldCondition(resources?.filter, 'URN') || PolicyMatchCondition.Equals; const domains = getFieldValues(resources?.filter, 'DOMAIN') || []; const { @@ -104,6 +110,10 @@ export default function PolicyDetailsModal({ policy, open, onClose, privileges } ); }; + const getWildcardUrnTag = (criterionValue) => { + return {criterionValue.value}*; + }; + const resourceOwnersField = (actors) => { if (!actors?.resourceOwners) { return No; @@ -166,7 +176,10 @@ export default function PolicyDetailsModal({ policy, open, onClose, privileges } return ( // eslint-disable-next-line react/no-array-index-key - {getEntityTag(value)} + {resourceFilterCondition && + resourceFilterCondition === PolicyMatchCondition.StartsWith + ? getWildcardUrnTag(value) + : getEntityTag(value)} ); })) || All} diff --git a/datahub-web-react/src/app/permissions/policy/policyUtils.ts b/datahub-web-react/src/app/permissions/policy/policyUtils.ts index 725e39d82d62e..b71a38f80fc25 100644 --- a/datahub-web-react/src/app/permissions/policy/policyUtils.ts +++ b/datahub-web-react/src/app/permissions/policy/policyUtils.ts @@ -118,6 +118,10 @@ export const getFieldValues = (filter: Maybe | undefined, res return filter?.criteria?.find((criterion) => criterion.field === resourceFieldType)?.values || []; }; +export const getFieldCondition = (filter: Maybe | undefined, resourceFieldType: string) => { + return filter?.criteria?.find((criterion) => criterion.field === resourceFieldType)?.condition || null; +}; + export const getFieldValuesOfTags = (filter: Maybe | undefined, resourceFieldType: string) => { return filter?.criteria?.find((criterion) => criterion.field === resourceFieldType)?.values || []; }; diff --git a/metadata-models/src/main/pegasus/com/linkedin/policy/PolicyMatchCondition.pdl b/metadata-models/src/main/pegasus/com/linkedin/policy/PolicyMatchCondition.pdl index 0c51e7072ebd2..074d00e05692c 100644 --- a/metadata-models/src/main/pegasus/com/linkedin/policy/PolicyMatchCondition.pdl +++ b/metadata-models/src/main/pegasus/com/linkedin/policy/PolicyMatchCondition.pdl @@ -8,4 +8,9 @@ enum PolicyMatchCondition { * Whether the field matches the value */ EQUALS + + /** + * Whether the field value starts with the value + */ + STARTS_WITH } \ No newline at end of file diff --git a/metadata-service/auth-impl/src/main/java/com/datahub/authorization/PolicyEngine.java b/metadata-service/auth-impl/src/main/java/com/datahub/authorization/PolicyEngine.java index 028e9a9b7eb57..e1d2e20de2157 100644 --- a/metadata-service/auth-impl/src/main/java/com/datahub/authorization/PolicyEngine.java +++ b/metadata-service/auth-impl/src/main/java/com/datahub/authorization/PolicyEngine.java @@ -252,11 +252,15 @@ private boolean checkCriterion( private boolean checkCondition( Set fieldValues, String filterValue, PolicyMatchCondition condition) { - if (condition == PolicyMatchCondition.EQUALS) { - return fieldValues.contains(filterValue); + switch (condition) { + case EQUALS: + return fieldValues.contains(filterValue); + case STARTS_WITH: + return fieldValues.stream().anyMatch(v -> v.startsWith(filterValue)); + default: + log.error("Unsupported condition {}", condition); + return false; } - log.error("Unsupported condition {}", condition); - return false; } /** diff --git a/metadata-service/auth-impl/src/test/java/com/datahub/authorization/PolicyEngineTest.java b/metadata-service/auth-impl/src/test/java/com/datahub/authorization/PolicyEngineTest.java index ae0192ee9a738..da4e632113307 100644 --- a/metadata-service/auth-impl/src/test/java/com/datahub/authorization/PolicyEngineTest.java +++ b/metadata-service/auth-impl/src/test/java/com/datahub/authorization/PolicyEngineTest.java @@ -23,9 +23,7 @@ import com.linkedin.entity.client.EntityClient; import com.linkedin.identity.RoleMembership; import com.linkedin.metadata.Constants; -import com.linkedin.policy.DataHubActorFilter; -import com.linkedin.policy.DataHubPolicyInfo; -import com.linkedin.policy.DataHubResourceFilter; +import com.linkedin.policy.*; import io.datahubproject.metadata.context.OperationContext; import io.datahubproject.test.metadata.context.TestOperationContexts; import java.net.URISyntaxException; @@ -1043,6 +1041,92 @@ public void testEvaluatePolicyResourceFilterSpecificResourceNoMatch() throws Exc verify(_entityClient, times(0)).batchGetV2(any(), any(), any(), any()); } + @Test + public void testEvaluatePolicyResourceFilterResourceUrnStartsWithMatch() throws Exception { + final DataHubPolicyInfo dataHubPolicyInfo = new DataHubPolicyInfo(); + dataHubPolicyInfo.setType(METADATA_POLICY_TYPE); + dataHubPolicyInfo.setState(ACTIVE_POLICY_STATE); + dataHubPolicyInfo.setPrivileges(new StringArray("EDIT_ENTITY_TAGS")); + dataHubPolicyInfo.setDisplayName("My Test Display"); + dataHubPolicyInfo.setDescription("My test display!"); + 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(); + PolicyMatchCriterion policyMatchCriterion = + FilterUtils.newCriterion( + EntityFieldType.URN, + Collections.singletonList("urn:li:dataset:te"), + PolicyMatchCondition.STARTS_WITH); + + resourceFilter.setFilter( + new PolicyMatchFilter() + .setCriteria( + new PolicyMatchCriterionArray(Collections.singleton(policyMatchCriterion)))); + dataHubPolicyInfo.setResources(resourceFilter); + + ResolvedEntitySpec resourceSpec = buildEntityResolvers("dataset", RESOURCE_URN); + PolicyEngine.PolicyEvaluationResult result = + _policyEngine.evaluatePolicy( + systemOperationContext, + dataHubPolicyInfo, + resolvedAuthorizedUserSpec, + "EDIT_ENTITY_TAGS", + Optional.of(resourceSpec)); + assertTrue(result.isGranted()); + + // Verify no network calls + verify(_entityClient, times(0)).batchGetV2(any(), any(), any(), any()); + } + + @Test + public void testEvaluatePolicyResourceFilterResourceUrnStartsWithNoMatch() throws Exception { + final DataHubPolicyInfo dataHubPolicyInfo = new DataHubPolicyInfo(); + dataHubPolicyInfo.setType(METADATA_POLICY_TYPE); + dataHubPolicyInfo.setState(ACTIVE_POLICY_STATE); + dataHubPolicyInfo.setPrivileges(new StringArray("EDIT_ENTITY_TAGS")); + dataHubPolicyInfo.setDisplayName("My Test Display"); + dataHubPolicyInfo.setDescription("My test display!"); + 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(); + PolicyMatchCriterion policyMatchCriterion = + FilterUtils.newCriterion( + EntityFieldType.URN, + Collections.singletonList("urn:li:dataset:other"), + PolicyMatchCondition.STARTS_WITH); + + resourceFilter.setFilter( + new PolicyMatchFilter() + .setCriteria( + new PolicyMatchCriterionArray(Collections.singleton(policyMatchCriterion)))); + dataHubPolicyInfo.setResources(resourceFilter); + + ResolvedEntitySpec resourceSpec = buildEntityResolvers("dataset", RESOURCE_URN); + PolicyEngine.PolicyEvaluationResult result = + _policyEngine.evaluatePolicy( + systemOperationContext, + dataHubPolicyInfo, + resolvedAuthorizedUserSpec, + "EDIT_ENTITY_TAGS", + Optional.of(resourceSpec)); + assertFalse(result.isGranted()); + + // Verify no network calls + verify(_entityClient, times(0)).batchGetV2(any(), any(), any(), any()); + } + @Test public void testEvaluatePolicyResourceFilterSpecificResourceMatchDomain() throws Exception { final DataHubPolicyInfo dataHubPolicyInfo = new DataHubPolicyInfo(); diff --git a/metadata-service/restli-api/src/main/snapshot/com.linkedin.entity.entities.snapshot.json b/metadata-service/restli-api/src/main/snapshot/com.linkedin.entity.entities.snapshot.json index 982a409ef8e4b..8ff0aa930770c 100644 --- a/metadata-service/restli-api/src/main/snapshot/com.linkedin.entity.entities.snapshot.json +++ b/metadata-service/restli-api/src/main/snapshot/com.linkedin.entity.entities.snapshot.json @@ -5478,9 +5478,10 @@ "type" : "enum", "name" : "PolicyMatchCondition", "doc" : "The matching condition in a filter criterion", - "symbols" : [ "EQUALS" ], + "symbols" : [ "EQUALS", "STARTS_WITH" ], "symbolDocs" : { - "EQUALS" : "Whether the field matches the value" + "EQUALS" : "Whether the field matches the value", + "STARTS_WITH" : "Whether the field value starts with the value" } }, "doc" : "The condition for the criterion", diff --git a/metadata-service/restli-api/src/main/snapshot/com.linkedin.platform.platform.snapshot.json b/metadata-service/restli-api/src/main/snapshot/com.linkedin.platform.platform.snapshot.json index 1a35b52474e4f..226279e176229 100644 --- a/metadata-service/restli-api/src/main/snapshot/com.linkedin.platform.platform.snapshot.json +++ b/metadata-service/restli-api/src/main/snapshot/com.linkedin.platform.platform.snapshot.json @@ -5472,9 +5472,10 @@ "type" : "enum", "name" : "PolicyMatchCondition", "doc" : "The matching condition in a filter criterion", - "symbols" : [ "EQUALS" ], + "symbols" : [ "EQUALS", "STARTS_WITH" ], "symbolDocs" : { - "EQUALS" : "Whether the field matches the value" + "EQUALS" : "Whether the field matches the value", + "STARTS_WITH" : "Whether the field value starts with the value" } }, "doc" : "The condition for the criterion",