Skip to content

Commit b607a66

Browse files
githendrikhrichert
andauthoredSep 20, 2024
Add STARTS_WITH policy condition to allow for URN-wildcard-based policies (datahub-project#11441)
Co-authored-by: Hendrik Richert <[email protected]>
1 parent 9eefedf commit b607a66

File tree

8 files changed

+130
-14
lines changed

8 files changed

+130
-14
lines changed
 

‎datahub-graphql-core/src/main/resources/entity.graphql

+4
Original file line numberDiff line numberDiff line change
@@ -9157,6 +9157,10 @@ enum PolicyMatchCondition {
91579157
Whether the field matches the value
91589158
"""
91599159
EQUALS
9160+
"""
9161+
Whether the field value starts with the value
9162+
"""
9163+
STARTS_WITH
91609164
}
91619165

91629166
"""

‎datahub-web-react/src/app/permissions/policy/PolicyDetailsModal.tsx

+16-3
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,14 @@ import { Link } from 'react-router-dom';
33
import { Button, Divider, Modal, Tag, Typography } from 'antd';
44
import styled from 'styled-components';
55
import { useEntityRegistry } from '../../useEntityRegistry';
6-
import { Maybe, Policy, PolicyState, PolicyType } from '../../../types.generated';
6+
import { Maybe, Policy, PolicyMatchCondition, PolicyState, PolicyType } from '../../../types.generated';
77
import { useAppConfig } from '../../useAppConfig';
8-
import { convertLegacyResourceFilter, getFieldValues, mapResourceTypeToDisplayName } from './policyUtils';
8+
import {
9+
convertLegacyResourceFilter,
10+
getFieldValues,
11+
getFieldCondition,
12+
mapResourceTypeToDisplayName,
13+
} from './policyUtils';
914
import AvatarsGroup from '../AvatarsGroup';
1015

1116
type PrivilegeOptionType = {
@@ -70,6 +75,7 @@ export default function PolicyDetailsModal({ policy, open, onClose, privileges }
7075
const resourceTypes = getFieldValues(resources?.filter, 'TYPE') || [];
7176
const dataPlatformInstances = getFieldValues(resources?.filter, 'DATA_PLATFORM_INSTANCE') || [];
7277
const resourceEntities = getFieldValues(resources?.filter, 'URN') || [];
78+
const resourceFilterCondition = getFieldCondition(resources?.filter, 'URN') || PolicyMatchCondition.Equals;
7379
const domains = getFieldValues(resources?.filter, 'DOMAIN') || [];
7480

7581
const {
@@ -104,6 +110,10 @@ export default function PolicyDetailsModal({ policy, open, onClose, privileges }
104110
);
105111
};
106112

113+
const getWildcardUrnTag = (criterionValue) => {
114+
return <Typography.Text>{criterionValue.value}*</Typography.Text>;
115+
};
116+
107117
const resourceOwnersField = (actors) => {
108118
if (!actors?.resourceOwners) {
109119
return <PoliciesTag>No</PoliciesTag>;
@@ -166,7 +176,10 @@ export default function PolicyDetailsModal({ policy, open, onClose, privileges }
166176
return (
167177
// eslint-disable-next-line react/no-array-index-key
168178
<PoliciesTag key={`resource-${value.value}-${key}`}>
169-
{getEntityTag(value)}
179+
{resourceFilterCondition &&
180+
resourceFilterCondition === PolicyMatchCondition.StartsWith
181+
? getWildcardUrnTag(value)
182+
: getEntityTag(value)}
170183
</PoliciesTag>
171184
);
172185
})) || <PoliciesTag>All</PoliciesTag>}

‎datahub-web-react/src/app/permissions/policy/policyUtils.ts

+4
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,10 @@ export const getFieldValues = (filter: Maybe<PolicyMatchFilter> | undefined, res
118118
return filter?.criteria?.find((criterion) => criterion.field === resourceFieldType)?.values || [];
119119
};
120120

121+
export const getFieldCondition = (filter: Maybe<PolicyMatchFilter> | undefined, resourceFieldType: string) => {
122+
return filter?.criteria?.find((criterion) => criterion.field === resourceFieldType)?.condition || null;
123+
};
124+
121125
export const getFieldValuesOfTags = (filter: Maybe<PolicyMatchFilter> | undefined, resourceFieldType: string) => {
122126
return filter?.criteria?.find((criterion) => criterion.field === resourceFieldType)?.values || [];
123127
};

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

+5
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,9 @@ enum PolicyMatchCondition {
88
* Whether the field matches the value
99
*/
1010
EQUALS
11+
12+
/**
13+
* Whether the field value starts with the value
14+
*/
15+
STARTS_WITH
1116
}

‎metadata-service/auth-impl/src/main/java/com/datahub/authorization/PolicyEngine.java

+8-4
Original file line numberDiff line numberDiff line change
@@ -252,11 +252,15 @@ private boolean checkCriterion(
252252

253253
private boolean checkCondition(
254254
Set<String> fieldValues, String filterValue, PolicyMatchCondition condition) {
255-
if (condition == PolicyMatchCondition.EQUALS) {
256-
return fieldValues.contains(filterValue);
255+
switch (condition) {
256+
case EQUALS:
257+
return fieldValues.contains(filterValue);
258+
case STARTS_WITH:
259+
return fieldValues.stream().anyMatch(v -> v.startsWith(filterValue));
260+
default:
261+
log.error("Unsupported condition {}", condition);
262+
return false;
257263
}
258-
log.error("Unsupported condition {}", condition);
259-
return false;
260264
}
261265

262266
/**

‎metadata-service/auth-impl/src/test/java/com/datahub/authorization/PolicyEngineTest.java

+87-3
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,7 @@
2323
import com.linkedin.entity.client.EntityClient;
2424
import com.linkedin.identity.RoleMembership;
2525
import com.linkedin.metadata.Constants;
26-
import com.linkedin.policy.DataHubActorFilter;
27-
import com.linkedin.policy.DataHubPolicyInfo;
28-
import com.linkedin.policy.DataHubResourceFilter;
26+
import com.linkedin.policy.*;
2927
import io.datahubproject.metadata.context.OperationContext;
3028
import io.datahubproject.test.metadata.context.TestOperationContexts;
3129
import java.net.URISyntaxException;
@@ -1043,6 +1041,92 @@ public void testEvaluatePolicyResourceFilterSpecificResourceNoMatch() throws Exc
10431041
verify(_entityClient, times(0)).batchGetV2(any(), any(), any(), any());
10441042
}
10451043

1044+
@Test
1045+
public void testEvaluatePolicyResourceFilterResourceUrnStartsWithMatch() throws Exception {
1046+
final DataHubPolicyInfo dataHubPolicyInfo = new DataHubPolicyInfo();
1047+
dataHubPolicyInfo.setType(METADATA_POLICY_TYPE);
1048+
dataHubPolicyInfo.setState(ACTIVE_POLICY_STATE);
1049+
dataHubPolicyInfo.setPrivileges(new StringArray("EDIT_ENTITY_TAGS"));
1050+
dataHubPolicyInfo.setDisplayName("My Test Display");
1051+
dataHubPolicyInfo.setDescription("My test display!");
1052+
dataHubPolicyInfo.setEditable(true);
1053+
1054+
final DataHubActorFilter actorFilter = new DataHubActorFilter();
1055+
actorFilter.setResourceOwners(true);
1056+
actorFilter.setAllUsers(true);
1057+
actorFilter.setAllGroups(true);
1058+
dataHubPolicyInfo.setActors(actorFilter);
1059+
1060+
final DataHubResourceFilter resourceFilter = new DataHubResourceFilter();
1061+
PolicyMatchCriterion policyMatchCriterion =
1062+
FilterUtils.newCriterion(
1063+
EntityFieldType.URN,
1064+
Collections.singletonList("urn:li:dataset:te"),
1065+
PolicyMatchCondition.STARTS_WITH);
1066+
1067+
resourceFilter.setFilter(
1068+
new PolicyMatchFilter()
1069+
.setCriteria(
1070+
new PolicyMatchCriterionArray(Collections.singleton(policyMatchCriterion))));
1071+
dataHubPolicyInfo.setResources(resourceFilter);
1072+
1073+
ResolvedEntitySpec resourceSpec = buildEntityResolvers("dataset", RESOURCE_URN);
1074+
PolicyEngine.PolicyEvaluationResult result =
1075+
_policyEngine.evaluatePolicy(
1076+
systemOperationContext,
1077+
dataHubPolicyInfo,
1078+
resolvedAuthorizedUserSpec,
1079+
"EDIT_ENTITY_TAGS",
1080+
Optional.of(resourceSpec));
1081+
assertTrue(result.isGranted());
1082+
1083+
// Verify no network calls
1084+
verify(_entityClient, times(0)).batchGetV2(any(), any(), any(), any());
1085+
}
1086+
1087+
@Test
1088+
public void testEvaluatePolicyResourceFilterResourceUrnStartsWithNoMatch() throws Exception {
1089+
final DataHubPolicyInfo dataHubPolicyInfo = new DataHubPolicyInfo();
1090+
dataHubPolicyInfo.setType(METADATA_POLICY_TYPE);
1091+
dataHubPolicyInfo.setState(ACTIVE_POLICY_STATE);
1092+
dataHubPolicyInfo.setPrivileges(new StringArray("EDIT_ENTITY_TAGS"));
1093+
dataHubPolicyInfo.setDisplayName("My Test Display");
1094+
dataHubPolicyInfo.setDescription("My test display!");
1095+
dataHubPolicyInfo.setEditable(true);
1096+
1097+
final DataHubActorFilter actorFilter = new DataHubActorFilter();
1098+
actorFilter.setResourceOwners(true);
1099+
actorFilter.setAllUsers(true);
1100+
actorFilter.setAllGroups(true);
1101+
dataHubPolicyInfo.setActors(actorFilter);
1102+
1103+
final DataHubResourceFilter resourceFilter = new DataHubResourceFilter();
1104+
PolicyMatchCriterion policyMatchCriterion =
1105+
FilterUtils.newCriterion(
1106+
EntityFieldType.URN,
1107+
Collections.singletonList("urn:li:dataset:other"),
1108+
PolicyMatchCondition.STARTS_WITH);
1109+
1110+
resourceFilter.setFilter(
1111+
new PolicyMatchFilter()
1112+
.setCriteria(
1113+
new PolicyMatchCriterionArray(Collections.singleton(policyMatchCriterion))));
1114+
dataHubPolicyInfo.setResources(resourceFilter);
1115+
1116+
ResolvedEntitySpec resourceSpec = buildEntityResolvers("dataset", RESOURCE_URN);
1117+
PolicyEngine.PolicyEvaluationResult result =
1118+
_policyEngine.evaluatePolicy(
1119+
systemOperationContext,
1120+
dataHubPolicyInfo,
1121+
resolvedAuthorizedUserSpec,
1122+
"EDIT_ENTITY_TAGS",
1123+
Optional.of(resourceSpec));
1124+
assertFalse(result.isGranted());
1125+
1126+
// Verify no network calls
1127+
verify(_entityClient, times(0)).batchGetV2(any(), any(), any(), any());
1128+
}
1129+
10461130
@Test
10471131
public void testEvaluatePolicyResourceFilterSpecificResourceMatchDomain() throws Exception {
10481132
final DataHubPolicyInfo dataHubPolicyInfo = new DataHubPolicyInfo();

‎metadata-service/restli-api/src/main/snapshot/com.linkedin.entity.entities.snapshot.json

+3-2
Original file line numberDiff line numberDiff line change
@@ -5478,9 +5478,10 @@
54785478
"type" : "enum",
54795479
"name" : "PolicyMatchCondition",
54805480
"doc" : "The matching condition in a filter criterion",
5481-
"symbols" : [ "EQUALS" ],
5481+
"symbols" : [ "EQUALS", "STARTS_WITH" ],
54825482
"symbolDocs" : {
5483-
"EQUALS" : "Whether the field matches the value"
5483+
"EQUALS" : "Whether the field matches the value",
5484+
"STARTS_WITH" : "Whether the field value starts with the value"
54845485
}
54855486
},
54865487
"doc" : "The condition for the criterion",

‎metadata-service/restli-api/src/main/snapshot/com.linkedin.platform.platform.snapshot.json

+3-2
Original file line numberDiff line numberDiff line change
@@ -5472,9 +5472,10 @@
54725472
"type" : "enum",
54735473
"name" : "PolicyMatchCondition",
54745474
"doc" : "The matching condition in a filter criterion",
5475-
"symbols" : [ "EQUALS" ],
5475+
"symbols" : [ "EQUALS", "STARTS_WITH" ],
54765476
"symbolDocs" : {
5477-
"EQUALS" : "Whether the field matches the value"
5477+
"EQUALS" : "Whether the field matches the value",
5478+
"STARTS_WITH" : "Whether the field value starts with the value"
54785479
}
54795480
},
54805481
"doc" : "The condition for the criterion",

0 commit comments

Comments
 (0)
Please sign in to comment.