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",