Add STARTS_WITH policy condition to allow for URN-wildcard-based policies (#11441)

Co-authored-by: Hendrik Richert <hendrik.richert@swisscom.com>
This commit is contained in:
Hendrik Richert 2024-09-20 17:33:14 +02:00 committed by GitHub
parent 9eefedfdc0
commit b607a66c05
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 130 additions and 14 deletions

View File

@ -9157,6 +9157,10 @@ enum PolicyMatchCondition {
Whether the field matches the value Whether the field matches the value
""" """
EQUALS EQUALS
"""
Whether the field value starts with the value
"""
STARTS_WITH
} }
""" """

View File

@ -3,9 +3,14 @@ import { Link } from 'react-router-dom';
import { Button, Divider, Modal, Tag, Typography } from 'antd'; import { Button, Divider, Modal, Tag, Typography } from 'antd';
import styled from 'styled-components'; import styled from 'styled-components';
import { useEntityRegistry } from '../../useEntityRegistry'; 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 { useAppConfig } from '../../useAppConfig';
import { convertLegacyResourceFilter, getFieldValues, mapResourceTypeToDisplayName } from './policyUtils'; import {
convertLegacyResourceFilter,
getFieldValues,
getFieldCondition,
mapResourceTypeToDisplayName,
} from './policyUtils';
import AvatarsGroup from '../AvatarsGroup'; import AvatarsGroup from '../AvatarsGroup';
type PrivilegeOptionType = { type PrivilegeOptionType = {
@ -70,6 +75,7 @@ export default function PolicyDetailsModal({ policy, open, onClose, privileges }
const resourceTypes = getFieldValues(resources?.filter, 'TYPE') || []; const resourceTypes = getFieldValues(resources?.filter, 'TYPE') || [];
const dataPlatformInstances = getFieldValues(resources?.filter, 'DATA_PLATFORM_INSTANCE') || []; const dataPlatformInstances = getFieldValues(resources?.filter, 'DATA_PLATFORM_INSTANCE') || [];
const resourceEntities = getFieldValues(resources?.filter, 'URN') || []; const resourceEntities = getFieldValues(resources?.filter, 'URN') || [];
const resourceFilterCondition = getFieldCondition(resources?.filter, 'URN') || PolicyMatchCondition.Equals;
const domains = getFieldValues(resources?.filter, 'DOMAIN') || []; const domains = getFieldValues(resources?.filter, 'DOMAIN') || [];
const { const {
@ -104,6 +110,10 @@ export default function PolicyDetailsModal({ policy, open, onClose, privileges }
); );
}; };
const getWildcardUrnTag = (criterionValue) => {
return <Typography.Text>{criterionValue.value}*</Typography.Text>;
};
const resourceOwnersField = (actors) => { const resourceOwnersField = (actors) => {
if (!actors?.resourceOwners) { if (!actors?.resourceOwners) {
return <PoliciesTag>No</PoliciesTag>; return <PoliciesTag>No</PoliciesTag>;
@ -166,7 +176,10 @@ export default function PolicyDetailsModal({ policy, open, onClose, privileges }
return ( return (
// eslint-disable-next-line react/no-array-index-key // eslint-disable-next-line react/no-array-index-key
<PoliciesTag key={`resource-${value.value}-${key}`}> <PoliciesTag key={`resource-${value.value}-${key}`}>
{getEntityTag(value)} {resourceFilterCondition &&
resourceFilterCondition === PolicyMatchCondition.StartsWith
? getWildcardUrnTag(value)
: getEntityTag(value)}
</PoliciesTag> </PoliciesTag>
); );
})) || <PoliciesTag>All</PoliciesTag>} })) || <PoliciesTag>All</PoliciesTag>}

View File

@ -118,6 +118,10 @@ export const getFieldValues = (filter: Maybe<PolicyMatchFilter> | undefined, res
return filter?.criteria?.find((criterion) => criterion.field === resourceFieldType)?.values || []; return filter?.criteria?.find((criterion) => criterion.field === resourceFieldType)?.values || [];
}; };
export const getFieldCondition = (filter: Maybe<PolicyMatchFilter> | undefined, resourceFieldType: string) => {
return filter?.criteria?.find((criterion) => criterion.field === resourceFieldType)?.condition || null;
};
export const getFieldValuesOfTags = (filter: Maybe<PolicyMatchFilter> | undefined, resourceFieldType: string) => { export const getFieldValuesOfTags = (filter: Maybe<PolicyMatchFilter> | undefined, resourceFieldType: string) => {
return filter?.criteria?.find((criterion) => criterion.field === resourceFieldType)?.values || []; return filter?.criteria?.find((criterion) => criterion.field === resourceFieldType)?.values || [];
}; };

View File

@ -8,4 +8,9 @@ enum PolicyMatchCondition {
* Whether the field matches the value * Whether the field matches the value
*/ */
EQUALS EQUALS
/**
* Whether the field value starts with the value
*/
STARTS_WITH
} }

View File

@ -252,12 +252,16 @@ public class PolicyEngine {
private boolean checkCondition( private boolean checkCondition(
Set<String> fieldValues, String filterValue, PolicyMatchCondition condition) { Set<String> fieldValues, String filterValue, PolicyMatchCondition condition) {
if (condition == PolicyMatchCondition.EQUALS) { switch (condition) {
case EQUALS:
return fieldValues.contains(filterValue); return fieldValues.contains(filterValue);
} case STARTS_WITH:
return fieldValues.stream().anyMatch(v -> v.startsWith(filterValue));
default:
log.error("Unsupported condition {}", condition); log.error("Unsupported condition {}", condition);
return false; return false;
} }
}
/** /**
* Returns true if the actor portion of a DataHub policy matches the actor being evaluated, false * Returns true if the actor portion of a DataHub policy matches the actor being evaluated, false

View File

@ -23,9 +23,7 @@ import com.linkedin.entity.EnvelopedAspectMap;
import com.linkedin.entity.client.EntityClient; import com.linkedin.entity.client.EntityClient;
import com.linkedin.identity.RoleMembership; import com.linkedin.identity.RoleMembership;
import com.linkedin.metadata.Constants; import com.linkedin.metadata.Constants;
import com.linkedin.policy.DataHubActorFilter; import com.linkedin.policy.*;
import com.linkedin.policy.DataHubPolicyInfo;
import com.linkedin.policy.DataHubResourceFilter;
import io.datahubproject.metadata.context.OperationContext; import io.datahubproject.metadata.context.OperationContext;
import io.datahubproject.test.metadata.context.TestOperationContexts; import io.datahubproject.test.metadata.context.TestOperationContexts;
import java.net.URISyntaxException; import java.net.URISyntaxException;
@ -1043,6 +1041,92 @@ public class PolicyEngineTest {
verify(_entityClient, times(0)).batchGetV2(any(), any(), any(), any()); 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 @Test
public void testEvaluatePolicyResourceFilterSpecificResourceMatchDomain() throws Exception { public void testEvaluatePolicyResourceFilterSpecificResourceMatchDomain() throws Exception {
final DataHubPolicyInfo dataHubPolicyInfo = new DataHubPolicyInfo(); final DataHubPolicyInfo dataHubPolicyInfo = new DataHubPolicyInfo();

View File

@ -5478,9 +5478,10 @@
"type" : "enum", "type" : "enum",
"name" : "PolicyMatchCondition", "name" : "PolicyMatchCondition",
"doc" : "The matching condition in a filter criterion", "doc" : "The matching condition in a filter criterion",
"symbols" : [ "EQUALS" ], "symbols" : [ "EQUALS", "STARTS_WITH" ],
"symbolDocs" : { "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", "doc" : "The condition for the criterion",

View File

@ -5472,9 +5472,10 @@
"type" : "enum", "type" : "enum",
"name" : "PolicyMatchCondition", "name" : "PolicyMatchCondition",
"doc" : "The matching condition in a filter criterion", "doc" : "The matching condition in a filter criterion",
"symbols" : [ "EQUALS" ], "symbols" : [ "EQUALS", "STARTS_WITH" ],
"symbolDocs" : { "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", "doc" : "The condition for the criterion",