Fixes #10679 - Add policy functions inAnyTeam and hasAnyRole (#10680)

This commit is contained in:
Suresh Srinivas 2023-03-21 07:24:41 -07:00 committed by GitHub
parent 55fde2d775
commit b8e0ae489a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 350 additions and 227 deletions

View File

@ -24,7 +24,6 @@ import lombok.extern.slf4j.Slf4j;
import org.openmetadata.schema.entity.events.EventFilterRule;
import org.openmetadata.schema.entity.events.EventSubscription;
import org.openmetadata.schema.entity.events.SubscriptionStatus;
import org.openmetadata.schema.type.EntityReference;
import org.openmetadata.service.Entity;
import org.openmetadata.service.events.EventPubSub;
import org.openmetadata.service.events.subscription.AlertUtil;
@ -75,18 +74,11 @@ public class EventSubscriptionRepository extends EntityRepository<EventSubscript
@Override
public void storeEntity(EventSubscription entity, boolean update) throws IOException {
EntityReference owner = entity.getOwner();
// Don't store owner, database, href and tags as JSON. Build it on the fly based on relationships
entity.withOwner(null).withHref(null);
store(entity, update);
// Restore the relationships
entity.withOwner(owner);
}
@Override
public void storeRelationships(EventSubscription entity) {
// store owner
storeOwner(entity, entity.getOwner());
}

View File

@ -13,7 +13,6 @@
package org.openmetadata.service.jdbi3;
import static org.openmetadata.common.utils.CommonUtil.listOf;
import static org.openmetadata.common.utils.CommonUtil.listOrEmpty;
import static org.openmetadata.common.utils.CommonUtil.nullOrEmpty;
import static org.openmetadata.csv.CsvUtil.addEntityReferences;
@ -601,7 +600,7 @@ public class TeamRepository extends EntityRepository<Team> {
continue; // Parent is being created by CSV import
}
// Else the parent should already exist
if (!SubjectCache.getInstance().isInTeam(team.getName(), listOf(parentRef))) {
if (!SubjectCache.getInstance().isInTeam(team.getName(), parentRef)) {
importFailure(printer, invalidTeam(4, team.getName(), importedTeam.getName(), parentRef.getName()), record);
processRecord = false;
}

View File

@ -13,7 +13,6 @@
package org.openmetadata.service.jdbi3;
import static org.openmetadata.common.utils.CommonUtil.listOf;
import static org.openmetadata.common.utils.CommonUtil.listOrEmpty;
import static org.openmetadata.common.utils.CommonUtil.nullOrEmpty;
import static org.openmetadata.csv.CsvUtil.addEntityReferences;
@ -401,7 +400,7 @@ public class UserRepository extends EntityRepository<User> {
continue; // Team is same as the team to which CSV is being imported, then it is in the same hierarchy
}
// Else the parent should already exist
if (!SubjectCache.getInstance().isInTeam(team.getName(), listOf(teamRef))) {
if (!SubjectCache.getInstance().isInTeam(team.getName(), teamRef)) {
importFailure(printer, invalidTeam(6, team.getName(), user, teamRef.getName()), record);
processRecord = false;
}

View File

@ -61,12 +61,6 @@ public final class CollectionRegistry {
/** Map of class name to list of functions exposed for writing conditions */
private final Map<Class<?>, List<org.openmetadata.schema.type.Function>> functionMap = new ConcurrentHashMap<>();
/**
* Some functions are used for capturing resource based rules where policies are applied based on resource being
* accessed and team hierarchy the resource belongs to instead of the subject.
*/
@Getter private final List<String> resourceBasedFunctions = new ArrayList<>();
/** Resources used only for testing */
@VisibleForTesting private final List<Object> testResources = new ArrayList<>();
@ -137,10 +131,6 @@ public final class CollectionRegistry {
.withParameterInputType(annotation.paramInputType());
functionList.add(function);
functionList.sort(Comparator.comparing(org.openmetadata.schema.type.Function::getName));
if (annotation.resourceBased()) {
resourceBasedFunctions.add(annotation.name());
}
LOG.info("Initialized for {} function {}\n", method.getDeclaringClass().getSimpleName(), function);
}
}

View File

@ -7,7 +7,6 @@ import com.fasterxml.jackson.annotation.JsonIgnore;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.openmetadata.schema.entity.policies.accessControl.Rule;
import org.openmetadata.schema.type.MetadataOperation;
@ -15,7 +14,6 @@ import org.openmetadata.schema.type.Permission;
import org.openmetadata.schema.type.Permission.Access;
import org.openmetadata.schema.type.ResourcePermission;
import org.openmetadata.service.exception.CatalogExceptionMessage;
import org.openmetadata.service.resources.CollectionRegistry;
import org.openmetadata.service.security.AuthorizationException;
import org.openmetadata.service.security.policyevaluator.SubjectContext.PolicyContext;
import org.springframework.expression.Expression;
@ -27,7 +25,6 @@ import org.springframework.expression.spel.support.StandardEvaluationContext;
public class CompiledRule extends Rule {
private static final SpelExpressionParser EXPRESSION_PARSER = new SpelExpressionParser();
@JsonIgnore private Expression expression;
@JsonIgnore @Getter private boolean resourceBased = false;
public CompiledRule(Rule rule) {
super();
@ -72,13 +69,6 @@ public class CompiledRule extends Rule {
}
if (expression == null) {
expression = parseExpression(getCondition());
List<String> resourceBasedFunctions = CollectionRegistry.getInstance().getResourceBasedFunctions();
for (String function : resourceBasedFunctions) {
if (getCondition().contains(function)) {
resourceBased = true;
break;
}
}
}
return expression;
}

View File

@ -50,7 +50,7 @@ public class PolicyCache {
public static void initialize() {
if (!INITIALIZED) {
POLICY_CACHE =
CacheBuilder.newBuilder().maximumSize(100).expireAfterWrite(3, TimeUnit.MINUTES).build(new PolicyLoader());
CacheBuilder.newBuilder().maximumSize(1000).expireAfterWrite(3, TimeUnit.MINUTES).build(new PolicyLoader());
POLICY_REPOSITORY = (PolicyRepository) Entity.getEntityRepository(Entity.POLICY);
FIELDS = POLICY_REPOSITORY.getFields("rules");
INITIALIZED = true;

View File

@ -63,18 +63,12 @@ public class PolicyEvaluator {
@NonNull ResourceContextInterface resourceContext,
@NonNull OperationContext operationContext)
throws IOException {
// First run through all the DENY policies based on the user
// First run through all the DENY policies
evaluateDenySubjectPolicies(subjectContext, resourceContext, operationContext);
// Next run through all the DENY policies based on the resource
evaluateDenyResourcePolicies(subjectContext, resourceContext, operationContext);
// Next run through all the ALLOW policies based on the user
evaluateAllowSubjectPolicies(subjectContext, resourceContext, operationContext);
// Next run through all the ALLOW policies based on the resource
evaluateAllowResourcePolicies(subjectContext, resourceContext, operationContext);
if (!operationContext.getOperations().isEmpty()) { // Some operations have not been allowed
throw new AuthorizationException(
CatalogExceptionMessage.permissionNotAllowed(
@ -83,33 +77,17 @@ public class PolicyEvaluator {
}
private static void evaluateDenySubjectPolicies(
SubjectContext subjectContext, ResourceContextInterface resourceContext, OperationContext operationContext) {
evaluatePolicies(subjectContext.getPolicies(), subjectContext, resourceContext, operationContext, true, false);
SubjectContext subjectContext, ResourceContextInterface resourceContext, OperationContext operationContext)
throws IOException {
Iterator<PolicyContext> policyIterator = subjectContext.getPolicies(resourceContext.getOwner());
evaluatePolicies(policyIterator, subjectContext, resourceContext, operationContext, true);
}
private static void evaluateAllowSubjectPolicies(
SubjectContext subjectContext, ResourceContextInterface resourceContext, OperationContext operationContext) {
evaluatePolicies(subjectContext.getPolicies(), subjectContext, resourceContext, operationContext, false, false);
}
private static void evaluateDenyResourcePolicies(
SubjectContext subjectContext, ResourceContextInterface resourceContext, OperationContext operationContext)
throws IOException {
if (resourceContext == null || resourceContext.getOwner() == null) {
return; // No owner for a resource. No need to walk the hierarchy of user and teams that are resource owners
}
Iterator<PolicyContext> resourcePolicies = subjectContext.getResourcePolicies(resourceContext.getOwner());
evaluatePolicies(resourcePolicies, subjectContext, resourceContext, operationContext, true, true);
}
private static void evaluateAllowResourcePolicies(
SubjectContext subjectContext, ResourceContextInterface resourceContext, OperationContext operationContext)
throws IOException {
if (resourceContext == null || resourceContext.getOwner() == null) {
return; // No owner for a resource. No need to walk the hierarchy of user and teams that are resource owners
}
Iterator<PolicyContext> resourcePolicies = subjectContext.getResourcePolicies(resourceContext.getOwner());
evaluatePolicies(resourcePolicies, subjectContext, resourceContext, operationContext, false, true);
Iterator<PolicyContext> policyIterator = subjectContext.getPolicies(resourceContext.getOwner());
evaluatePolicies(policyIterator, subjectContext, resourceContext, operationContext, false);
}
private static void evaluatePolicies(
@ -117,16 +95,12 @@ public class PolicyEvaluator {
SubjectContext subjectContext,
ResourceContextInterface resourceContext,
OperationContext operationContext,
boolean evaluateDeny,
boolean evaluateResourcePolicies) {
boolean evaluateDeny) {
// When an operation is allowed by a rule, it is removed from operation context
// When list of operations is empty in the operation context, all operations have been allowed
while (policies.hasNext() && !operationContext.getOperations().isEmpty()) {
PolicyContext context = policies.next();
for (CompiledRule rule : context.getRules()) {
if (evaluateResourcePolicies && !rule.isResourceBased()) {
continue; // Only evaluate resource based rules
}
LOG.debug(
"evaluating policy for {} {}:{}:{}",
evaluateDeny ? "deny" : "allow",
@ -146,7 +120,7 @@ public class PolicyEvaluator {
public static List<ResourcePermission> listPermission(@NonNull SubjectContext subjectContext) {
Map<String, ResourcePermission> resourcePermissionMap = initResourcePermissions();
Iterator<PolicyContext> policies = subjectContext.getPolicies();
Iterator<PolicyContext> policies = subjectContext.getPolicies(null);
while (policies.hasNext()) {
PolicyContext policyContext = policies.next();
for (CompiledRule rule : policyContext.getRules()) {
@ -177,7 +151,7 @@ public class PolicyEvaluator {
ResourcePermission resourcePermission = getResourcePermission(resourceType, Access.NOT_ALLOW);
// Iterate through policies and set the permissions to DENY, ALLOW, CONDITIONAL_DENY, or CONDITIONAL_ALLOW
Iterator<PolicyContext> policies = subjectContext.getPolicies();
Iterator<PolicyContext> policies = subjectContext.getPolicies(null);
while (policies.hasNext()) {
PolicyContext policyContext = policies.next();
for (CompiledRule rule : policyContext.getRules()) {
@ -194,7 +168,7 @@ public class PolicyEvaluator {
ResourcePermission resourcePermission = getResourcePermission(resourceContext.getResource(), Access.NOT_ALLOW);
// Iterate through policies and set the permissions to DENY, ALLOW, CONDITIONAL_DENY, or CONDITIONAL_ALLOW
Iterator<PolicyContext> policies = subjectContext.getPolicies();
Iterator<PolicyContext> policies = subjectContext.getPolicies(resourceContext.getOwner());
while (policies.hasNext()) {
PolicyContext policyContext = policies.next();
for (CompiledRule rule : policyContext.getRules()) {
@ -202,27 +176,6 @@ public class PolicyEvaluator {
rule.evaluatePermission(subjectContext, resourceContext, resourcePermission, policyContext);
}
}
// Iterate through policies and set the permissions to DENY, ALLOW, CONDITIONAL_DENY, or CONDITIONAL_ALLOW
if (resourceContext == null || resourceContext.getOwner() == null) {
return resourcePermission; // No owner - No need to walk the hierarchy of user and teams that are resource owners
}
Iterator<PolicyContext> resourcePolicies = subjectContext.getResourcePolicies(resourceContext.getOwner());
while (resourcePolicies.hasNext()) {
PolicyContext policyContext = resourcePolicies.next();
for (CompiledRule rule : policyContext.getRules()) {
rule.getExpression();
if (rule.isResourceBased() == false) {
continue;
}
LOG.debug(
"evaluating resource policies {}:{}:{}\n",
policyContext.getRoleName(),
policyContext.getPolicyName(),
rule.getName());
rule.evaluatePermission(subjectContext, resourceContext, resourcePermission, policyContext);
}
}
return resourcePermission;
}

View File

@ -39,8 +39,7 @@ public class RuleEvaluator {
name = "isOwner",
input = "none",
description = "Returns true if the logged in user is the owner of the entity being accessed",
examples = {"isOwner()", "!isOwner", "noOwner() || isOwner()"},
resourceBased = true)
examples = {"isOwner()", "!isOwner", "noOwner() || isOwner()"})
public boolean isOwner() throws IOException {
return subjectContext != null && subjectContext.isOwner(resourceContext.getOwner());
}
@ -49,8 +48,7 @@ public class RuleEvaluator {
name = "matchAllTags",
input = "List of comma separated tag or glossary fully qualified names",
description = "Returns true if the entity being accessed has all the tags given as input",
examples = {"matchAllTags('PersonalData.Personal', 'Tier.Tier1', 'Business Glossary.Clothing')"},
resourceBased = true)
examples = {"matchAllTags('PersonalData.Personal', 'Tier.Tier1', 'Business Glossary.Clothing')"})
public boolean matchAllTags(String... tagFQNs) throws IOException {
if (resourceContext == null) {
return false;
@ -70,8 +68,7 @@ public class RuleEvaluator {
name = "matchAnyTag",
input = "List of comma separated tag or glossary fully qualified names",
description = "Returns true if the entity being accessed has at least one of the tags given as input",
examples = {"matchAnyTag('PersonalData.Personal', 'Tier.Tier1', 'Business Glossary.Clothing')"},
resourceBased = true)
examples = {"matchAnyTag('PersonalData.Personal', 'Tier.Tier1', 'Business Glossary.Clothing')"})
public boolean matchAnyTag(String... tagFQNs) throws IOException {
if (resourceContext == null) {
return false;
@ -93,16 +90,48 @@ public class RuleEvaluator {
description =
"Returns true if the user and the resource belongs to the team hierarchy where this policy is"
+ "attached. This allows restricting permissions to a resource to the members of the team hierarchy.",
examples = {"matchTeam()"},
resourceBased = true)
examples = {"matchTeam()"})
public boolean matchTeam() throws IOException {
if (resourceContext == null || resourceContext.getOwner() == null) {
return true; // No ownership information
return false; // No ownership information
}
if (policyContext == null || !policyContext.getEntityType().equals(Entity.TEAM)) {
return true; // Policy must be attached to a team for this function to work
return false; // Policy must be attached to a team for this function to work
}
return subjectContext.isTeamAsset(policyContext.getEntityName(), resourceContext.getOwner())
&& subjectContext.isUserUnderTeam(policyContext.getEntityName());
}
@Function(
name = "inAnyTeam",
input = "List of comma separated team names",
description = "Returns true if the user belongs under the hierarchy of any of the teams in the given team list.",
examples = {"inAnyTeam('marketing')"})
public boolean inAnyTeam(String... teams) {
for (String team : teams) {
if (subjectContext.isUserUnderTeam(team)) {
LOG.debug("inAnyTeam - User {} is under the team {}", subjectContext.getUser().getName(), team);
return true;
}
LOG.debug("inAnyTeam - User {} is not under the team {}", subjectContext.getUser().getName(), team);
}
return false;
}
@Function(
name = "hasAnyRole",
input = "List of comma separated roles",
description =
"Returns true if the user (either direct or inherited from the parent teams) has one or more roles "
+ "from the list.",
examples = {"hasAnyRole('DataSteward', 'DataEngineer')"})
public boolean hasAnyRole(String... roles) {
for (String role : roles) {
if (subjectContext.hasAnyRole(role)) {
LOG.debug("hasAnyRole - User {} has the role {}", subjectContext.getUser().getName(), role);
return true;
}
}
return false;
}
}

View File

@ -96,6 +96,14 @@ public class SubjectCache {
}
}
public User getUser(String userName) throws EntityNotFoundException {
try {
return USER_CACHE.get(userName).getUser();
} catch (ExecutionException | UncheckedExecutionException ex) {
return null;
}
}
public Team getTeam(UUID teamId) throws EntityNotFoundException {
try {
return TEAM_CACHE.get(teamId);
@ -105,19 +113,41 @@ public class SubjectCache {
}
/** Return true if given list of teams is part of the hierarchy of parentTeam */
public boolean isInTeam(String parentTeam, List<EntityReference> teams) {
public boolean isInTeam(String parentTeam, EntityReference team) {
Stack<EntityReference> stack = new Stack<>();
listOrEmpty(teams).forEach(stack::push);
stack.push(team); // Start with team and see if the parent matches
while (!stack.empty()) {
Team parent = getTeam(stack.pop().getId());
if (parent.getName().equals(parentTeam)) {
return true;
}
listOrEmpty(parent.getParents()).forEach(stack::push);
listOrEmpty(parent.getParents()).forEach(stack::push); // Continue to go up the chain of parents
}
return false;
}
/** Return true if the given user has any roles the list of roles */
public boolean hasRole(User user, String role) {
Stack<EntityReference> stack = new Stack<>();
// If user has one of the roles directly assigned then return true
if (hasRole(user.getRoles(), role)) {
return true;
}
listOrEmpty(user.getTeams()).forEach(stack::push); // Continue to go up the chain of parents
while (!stack.empty()) {
Team parent = getTeam(stack.pop().getId());
if (hasRole(parent.getDefaultRoles(), role)) {
return true;
}
listOrEmpty(parent.getParents()).forEach(stack::push); // Continue to go up the chain of parents
}
return false;
}
private static boolean hasRole(List<EntityReference> userRoles, String expectedRole) {
return listOrEmpty(userRoles).stream().anyMatch(userRole -> userRole.getName().equals(expectedRole));
}
public static void cleanUp() {
LOG.info("Subject cache is cleaned up");
USER_CACHE.invalidateAll();

View File

@ -16,7 +16,6 @@ package org.openmetadata.service.security.policyevaluator;
import static org.openmetadata.common.utils.CommonUtil.listOrEmpty;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;
@ -64,7 +63,12 @@ public class SubjectContext {
/** Returns true if the user of this SubjectContext is under the team hierarchy of parentTeam */
public boolean isUserUnderTeam(String parentTeam) {
return isInTeam(parentTeam, user.getTeams());
for (EntityReference userTeam : user.getTeams()) {
if (isInTeam(parentTeam, userTeam)) {
return true;
}
}
return false;
}
/** Returns true if the given resource owner is under the team hierarchy of parentTeam */
@ -74,38 +78,30 @@ public class SubjectContext {
return subjectContext.isUserUnderTeam(parentTeam);
} else if (owner.getType().equals(Entity.TEAM)) {
Team team = SubjectCache.getInstance().getTeam(owner.getId());
return isInTeam(parentTeam, List.of(team.getEntityReference()));
return isInTeam(parentTeam, team.getEntityReference());
}
return false;
}
/** Return true if given list of teams is part of the hierarchy of parentTeam */
private boolean isInTeam(String parentTeam, List<EntityReference> teams) {
return SubjectCache.getInstance().isInTeam(parentTeam, teams);
/** Return true if the team is part of the hierarchy of parentTeam */
private boolean isInTeam(String parentTeam, EntityReference team) {
return SubjectCache.getInstance().isInTeam(parentTeam, team);
}
// Iterate over all the policies of the team hierarchy the user belongs to
public Iterator<PolicyContext> getPolicies() {
return new UserPolicyIterator(user, new ArrayList<>());
}
// Iterate over all the policies of the team hierarchy the resource belongs to
public Iterator<PolicyContext> getResourcePolicies(EntityReference owner) {
if (owner.getType().equals(Entity.USER)) {
SubjectContext subjectContext = SubjectCache.getInstance().getSubjectContext(owner.getName());
return subjectContext.getPolicies();
} else if (owner.getType().equals(Entity.TEAM)) {
Team team = SubjectCache.getInstance().getTeam(owner.getId());
List<UUID> teamsVisited = new ArrayList<>();
return new TeamPolicyIterator(team.getId(), teamsVisited);
}
return Collections.emptyIterator();
public Iterator<PolicyContext> getPolicies(EntityReference resourceOwner) {
return new UserPolicyIterator(user, resourceOwner, new ArrayList<>());
}
public List<EntityReference> getTeams() {
return user.getTeams();
}
/** Returns true if the user has any of the roles (either direct or inherited roles) */
public boolean hasAnyRole(String roles) {
return SubjectCache.getInstance().hasRole(getUser(), roles);
}
@Getter
static class PolicyContext {
private final String entityType;
@ -123,7 +119,7 @@ public class SubjectContext {
}
}
/** PolicyIterator goes over policies in a set of policies one by one. */
/** PolicyIterator goes over policies from a set of policies one by one. */
static class PolicyIterator implements Iterator<PolicyContext> {
// When executing roles from a policy, entity type User or Team to which the Role is attached to.
@ -223,7 +219,7 @@ public class SubjectContext {
private final List<Iterator<PolicyContext>> iterators = new ArrayList<>();
/** Policy iterator for a user */
UserPolicyIterator(User user, List<UUID> teamsVisited) {
UserPolicyIterator(User user, EntityReference resourceOwner, List<UUID> teamsVisited) {
this.user = user;
// Iterate over policies in user role
@ -231,13 +227,19 @@ public class SubjectContext {
iterators.add(new RolePolicyIterator(Entity.USER, user.getName(), user.getRoles()));
}
// Next, iterate over policies of teams to which the user belongs to
// Note that ** Bots don't inherit policies or default roles from teams **
if (!Boolean.TRUE.equals(user.getIsBot())) {
// Finally, iterate over policies of teams to which the user belongs to
// Note that ** Bots don't inherit policies or default roles from teams **
for (EntityReference team : user.getTeams()) {
iterators.add(new TeamPolicyIterator(team.getId(), teamsVisited));
iterators.add(new TeamPolicyIterator(team.getId(), teamsVisited, false));
}
}
// Finally, iterate over policies of teams that own the resource
if (resourceOwner != null && resourceOwner.getType().equals(Entity.TEAM)) {
Team team = SubjectCache.getInstance().getTeam(resourceOwner.getId());
iterators.add(new TeamPolicyIterator(team.getId(), teamsVisited, true));
}
}
@Override
@ -248,7 +250,7 @@ public class SubjectContext {
}
iteratorIndex++;
}
LOG.debug("Subject {} policy iteration done" + user.getName());
LOG.debug("Subject {} policy iteration done", user.getName());
return false;
}
@ -270,21 +272,21 @@ public class SubjectContext {
private final List<Iterator<PolicyContext>> iterators = new ArrayList<>();
/** Policy iterator for a team */
TeamPolicyIterator(UUID teamId, List<UUID> teamsVisited) {
TeamPolicyIterator(UUID teamId, List<UUID> teamsVisited, boolean skipRoles) {
Team team = SubjectCache.getInstance().getTeam(teamId);
// If a team is already visited (because user can belong to multiple teams
// and a team can belong to multiple teams) then don't visit the roles/policies of that team
if (!teamsVisited.contains(teamId)) {
teamsVisited.add(teamId);
if (team.getDefaultRoles() != null) {
if (!skipRoles && team.getDefaultRoles() != null) {
iterators.add(new RolePolicyIterator(Entity.TEAM, team.getName(), team.getDefaultRoles()));
}
if (team.getPolicies() != null) {
iterators.add(new PolicyIterator(Entity.TEAM, team.getName(), null, team.getPolicies()));
}
for (EntityReference parentTeam : listOrEmpty(team.getParents())) {
iterators.add(new TeamPolicyIterator(parentTeam.getId(), teamsVisited));
iterators.add(new TeamPolicyIterator(parentTeam.getId(), teamsVisited, skipRoles));
}
}
}

View File

@ -3,39 +3,52 @@ package org.openmetadata.service.security.policyevaluator;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.openmetadata.common.utils.CommonUtil.listOf;
import static org.openmetadata.common.utils.CommonUtil.listOrEmpty;
import static org.openmetadata.service.security.policyevaluator.CompiledRule.parseExpression;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.mockito.stubbing.Answer;
import org.openmetadata.schema.entity.data.Table;
import org.openmetadata.schema.entity.teams.Role;
import org.openmetadata.schema.entity.teams.Team;
import org.openmetadata.schema.entity.teams.User;
import org.openmetadata.schema.type.EntityReference;
import org.openmetadata.schema.type.TagLabel;
import org.openmetadata.service.Entity;
import org.openmetadata.service.jdbi3.CollectionDAO.RoleDAO;
import org.openmetadata.service.jdbi3.CollectionDAO.TableDAO;
import org.openmetadata.service.jdbi3.CollectionDAO.TeamDAO;
import org.openmetadata.service.jdbi3.CollectionDAO.UserDAO;
import org.openmetadata.service.jdbi3.RoleRepository;
import org.openmetadata.service.jdbi3.TableRepository;
import org.openmetadata.service.jdbi3.TeamRepository;
import org.openmetadata.service.jdbi3.UserRepository;
import org.openmetadata.service.security.policyevaluator.SubjectContext.PolicyContext;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.spel.support.StandardEvaluationContext;
class RuleEvaluatorTest {
private static Table table = new Table().withName("table");
private static final Table table = new Table().withName("table");
private static User user;
private static EvaluationContext evaluationContext;
private static SubjectContext subjectContext;
private static ResourceContext resourceContext;
@BeforeAll
public static void setup() {
Entity.registerEntity(User.class, Entity.USER, Mockito.mock(UserDAO.class), Mockito.mock(UserRepository.class));
Entity.registerEntity(Team.class, Entity.TEAM, Mockito.mock(TeamDAO.class), Mockito.mock(TeamRepository.class));
Entity.registerEntity(Role.class, Entity.ROLE, Mockito.mock(RoleDAO.class), Mockito.mock(RoleRepository.class));
SubjectCache.initialize();
RoleCache.initialize();
TableRepository tableRepository = Mockito.mock(TableRepository.class);
Mockito.when(tableRepository.getAllTags(any()))
@ -43,18 +56,24 @@ class RuleEvaluatorTest {
Entity.registerEntity(Table.class, Entity.TABLE, Mockito.mock(TableDAO.class), tableRepository);
user = new User().withId(UUID.randomUUID()).withName("user");
ResourceContext resourceContext =
resourceContext =
ResourceContext.builder()
.resource("table")
.entity(table)
.entityRepository(Mockito.mock(TableRepository.class))
.build();
SubjectContext subjectContext = new SubjectContext(user);
subjectContext = new SubjectContext(user);
RuleEvaluator ruleEvaluator = new RuleEvaluator(null, subjectContext, resourceContext);
evaluationContext = new StandardEvaluationContext(ruleEvaluator);
}
@AfterAll
public static void cleanup() {
SubjectCache.cleanUp();
RoleCache.cleanUp();
}
@Test
void test_noOwner() {
// Set no owner to the entity and test noOwner method
@ -133,6 +152,99 @@ class RuleEvaluatorTest {
assertTrue(evaluateExpression("!matchAnyTag('tag4')"));
}
@Test
void test_matchTeam() {
// Create a team hierarchy
Team team1 = createTeam("team1", null);
Team team11 = createTeam("team11", "team1");
Team team12 = createTeam("team12", "team1");
Team team111 = createTeam("team111", "team11");
// Resource belongs to team111 and the Policy executed is coming from team111
table.setOwner(team111.getEntityReference());
updatePolicyContext("team111");
for (Team team : listOf(team111)) { // For users in team111 hierarchy matchTeam is true
user.setTeams(listOf(team.getEntityReference()));
assertTrue(evaluateExpression("matchTeam()"));
}
for (Team team : listOf(team1, team12, team11)) { // For users not in team111 hierarchy matchTeam is false
user.setTeams(listOf(team.getEntityReference()));
assertFalse(evaluateExpression("matchTeam()"), "Failed for team " + team.getName());
}
// Resource belongs to team111 and the Policy executed is coming from team11
updatePolicyContext("team11");
for (Team team : listOf(team11, team111)) { // For users in team11 hierarchy matchTeam is true
user.setTeams(listOf(team.getEntityReference()));
assertTrue(evaluateExpression("matchTeam()"));
}
for (Team team : listOf(team1, team12)) { // For users not in team11 hierarchy matchTeam is false
user.setTeams(listOf(team.getEntityReference()));
assertFalse(evaluateExpression("matchTeam()"), "Failed for team " + team.getName());
}
// Resource belongs to team111 and the Policy executed is coming from team1
updatePolicyContext("team1");
for (Team team : listOf(team1, team11, team111, team12)) { // For users in team1 hierarchy matchTeam is true
user.setTeams(listOf(team.getEntityReference()));
assertTrue(evaluateExpression("matchTeam()"));
}
}
@Test
void test_inAnyTeam() {
// Create a team hierarchy
Team team1 = createTeam("team1", null);
createTeam("team11", "team1");
Team team12 = createTeam("team12", "team1");
Team team111 = createTeam("team111", "team11");
// User in team111 - that means user is also in parent teams team11 and team1
user.setTeams(listOf(team111.getEntityReference()));
assertTrue(evaluateExpression("inAnyTeam('team1')"));
assertTrue(evaluateExpression("inAnyTeam('team11')"));
assertTrue(evaluateExpression("inAnyTeam('team111')"));
assertFalse(evaluateExpression("inAnyTeam('team12')"));
// User in team12 - that means user is also in parent team team1
user.setTeams(listOf(team12.getEntityReference()));
assertTrue(evaluateExpression("inAnyTeam('team1')"));
assertTrue(evaluateExpression("inAnyTeam('team12')"));
assertFalse(evaluateExpression("inAnyTeam('team111', 'team11')"));
// User in team1 with no parents
user.setTeams(listOf(team1.getEntityReference()));
assertTrue(evaluateExpression("inAnyTeam('team1')"));
assertFalse(evaluateExpression("inAnyTeam('team12', 'team11', 'team111')"));
}
@Test
void test_hasAnyRole() {
// Create a team hierarchy
Team team1 = createTeamWithRole("team1", null);
Team team11 = createTeamWithRole("team11", "team1");
Team team111 = createTeamWithRole("team111", "team11");
user.setRoles(listOf(createRole("user").getEntityReference()));
// User in team111 inherits all roles
user.setTeams(listOf(team111.getEntityReference()));
for (String role : listOf("user", "team111", "team11", "team1")) {
assertTrue(evaluateExpression(String.format("hasAnyRole('%s')", role)));
}
// User in team11 inherits all roles except team111
user.setTeams(listOf(team11.getEntityReference()));
for (String role : listOf("user", "team11", "team1")) {
assertTrue(evaluateExpression(String.format("hasAnyRole('%s')", role)));
}
// User in team1 does not have parent team to inherit from
user.setTeams(listOf(team1.getEntityReference()));
for (String role : listOf("user", "team1")) {
assertTrue(evaluateExpression(String.format("hasAnyRole('%s')", role)));
}
}
private Boolean evaluateExpression(String condition) {
return parseExpression(condition).getValue(evaluationContext, Boolean.class);
}
@ -144,4 +256,42 @@ class RuleEvaluatorTest {
}
return tagLabels;
}
private Team createTeam(String teamName, String parentName) {
UUID teamId = UUID.nameUUIDFromBytes(teamName.getBytes(StandardCharsets.UTF_8));
Team team = new Team().withName(teamName).withId(teamId);
if (parentName != null) {
UUID parentId = UUID.nameUUIDFromBytes(parentName.getBytes(StandardCharsets.UTF_8));
Team parentTeam = SubjectCache.getInstance().getTeam(parentId);
team.setParents(listOf(parentTeam.getEntityReference()));
}
SubjectCache.TEAM_CACHE.put(team.getId(), team);
return team;
}
private Team createTeamWithRole(String teamName, String parentName) {
Team team = createTeam(teamName, parentName);
Role role = createRole(teamName); // Create a role with same name as the teamName
team.setDefaultRoles(listOf(role.getEntityReference()));
team.setInheritedRoles(new ArrayList<>());
for (EntityReference parent : listOrEmpty(team.getParents())) {
Team parentTeam = SubjectCache.getInstance().getTeam(parent.getId());
team.getInheritedRoles().addAll(listOrEmpty(parentTeam.getDefaultRoles()));
team.getInheritedRoles().addAll(listOrEmpty(parentTeam.getInheritedRoles()));
}
return team;
}
private Role createRole(String roleName) {
UUID roleId = UUID.nameUUIDFromBytes(roleName.getBytes(StandardCharsets.UTF_8));
Role role = new Role().withName(roleName).withId(roleId);
RoleCache.ROLE_CACHE.put(role.getId(), role);
return role;
}
private void updatePolicyContext(String team) {
PolicyContext policyContext = new PolicyContext(Entity.TEAM, team, null, null, null);
RuleEvaluator ruleEvaluator = new RuleEvaluator(policyContext, subjectContext, resourceContext);
evaluationContext = new StandardEvaluationContext(ruleEvaluator);
}
}

View File

@ -15,6 +15,7 @@ package org.openmetadata.service.security.policyevaluator;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.openmetadata.common.utils.CommonUtil.listOrEmpty;
import java.util.ArrayList;
import java.util.Iterator;
@ -45,19 +46,25 @@ import org.openmetadata.service.security.policyevaluator.SubjectContext.PolicyCo
public class SubjectContextTest {
private static List<Role> team1Roles;
private static List<Policy> team1Policies;
private static Team team1;
private static List<Role> team11Roles;
private static List<Policy> team11Policies;
private static Team team11;
private static List<Role> team12Roles;
private static List<Policy> team12Policies;
private static List<Role> team13Roles;
private static List<Policy> team13Policies;
private static Team team13;
private static List<Role> team111Roles;
private static List<Policy> team111Policies;
private static Team team111;
private static List<Role> team131Roles;
private static List<Policy> team131Policies;
private static Team team131;
private static List<Role> userRoles;
private static User user;
@ -73,32 +80,40 @@ public class SubjectContextTest {
SubjectCache.initialize();
// Create team hierarchy:
// team1, has team11, team12, team13 as children
// team11 has team111 as children
// team111 has user as children
// Each with 3 roles and 3 policies
team1Roles = getRoles("team1", 3);
team1Policies = getPolicies("team1", 3);
team1 = createTeam("team1", team1Roles, team1Policies, null);
// team1
// / | \
// team11 team12 team13
// / / \
// team111 / team131
// \ /
// user
// Each team has 3 roles and 3 policies
team1Roles = getRoles("team1");
team1Policies = getPolicies("team1");
Team team1 = createTeam("team1", team1Roles, team1Policies, null);
team11Roles = getRoles("team11", 3);
team11Policies = getPolicies("team11", 3);
team11 = createTeam("team11", team11Roles, team11Policies, List.of(team1));
team11Roles = getRoles("team11");
team11Policies = getPolicies("team11");
Team team11 = createTeam("team11", team11Roles, team11Policies, List.of(team1));
team12Roles = getRoles("team12", 3);
team12Policies = getPolicies("team12", 3);
team12Roles = getRoles("team12");
team12Policies = getPolicies("team12");
Team team12 = createTeam("team12", team12Roles, team12Policies, List.of(team1));
List<Role> team13Roles = getRoles("team13", 3);
List<Policy> team13Policies = getPolicies("team13", 3);
createTeam("team13", team13Roles, team13Policies, List.of(team1));
team13Roles = getRoles("team13");
team13Policies = getPolicies("team13");
team13 = createTeam("team13", team13Roles, team13Policies, List.of(team1));
team111Roles = getRoles("team111", 3);
team111Policies = getPolicies("team111", 3);
team111Roles = getRoles("team111");
team111Policies = getPolicies("team111");
team111 = createTeam("team111", team111Roles, team111Policies, List.of(team11, team12));
team131Roles = getRoles("team131");
team131Policies = getPolicies("team131");
team131 = createTeam("team131", team131Roles, team131Policies, List.of(team13));
// Add user to team111
userRoles = getRoles("user", 3);
userRoles = getRoles("user");
List<EntityReference> userRolesRef = toEntityReferences(userRoles);
user = new User().withName("user").withRoles(userRolesRef).withTeams(List.of(team111.getEntityReference()));
SubjectCache.USER_CACHE.put("user", new SubjectContext(user));
@ -113,12 +128,36 @@ public class SubjectContextTest {
@Test
void testPolicyIterator() {
//
// Check iteration order of the policies
//
// Check iteration order of the policies without resourceOwner
SubjectContext subjectContext = SubjectCache.getInstance().getSubjectContext(user.getName());
Iterator<PolicyContext> policyContextIterator = subjectContext.getPolicies();
assertUserPolicyIterator(policyContextIterator);
Iterator<PolicyContext> policyContextIterator = subjectContext.getPolicies(null);
List<String> expectedUserPolicyOrder = new ArrayList<>();
expectedUserPolicyOrder.addAll(getPolicyListFromRoles(userRoles)); // First polices associated with user roles
expectedUserPolicyOrder.addAll(getAllTeamPolicies(team111Roles, team111Policies)); // Next parent team111 policies
expectedUserPolicyOrder.addAll(
getAllTeamPolicies(team11Roles, team11Policies)); // Next team111 parent team11 policies
expectedUserPolicyOrder.addAll(getAllTeamPolicies(team1Roles, team1Policies)); // Next team11 parent team1 policies
expectedUserPolicyOrder.addAll(
getAllTeamPolicies(team12Roles, team12Policies)); // Next team111 parent team12 policies
assertPolicyIterator(expectedUserPolicyOrder, policyContextIterator);
// Check iteration order of policies with team13 as the resource owner
subjectContext = SubjectCache.getInstance().getSubjectContext(user.getName());
policyContextIterator = subjectContext.getPolicies(team13.getEntityReference());
List<String> expectedUserAndTeam13PolicyOrder = new ArrayList<>();
expectedUserAndTeam13PolicyOrder.addAll(expectedUserPolicyOrder);
expectedUserAndTeam13PolicyOrder.addAll(getAllTeamPolicies(null, team13Policies));
assertPolicyIterator(expectedUserAndTeam13PolicyOrder, policyContextIterator);
// Check iteration order of policies with team131 as the resource owner
subjectContext = SubjectCache.getInstance().getSubjectContext(user.getName());
policyContextIterator = subjectContext.getPolicies(team131.getEntityReference());
// Roles & policies are inherited from resource owner team131
List<String> expectedUserAndTeam131PolicyOrder = new ArrayList<>();
expectedUserAndTeam131PolicyOrder.addAll(expectedUserPolicyOrder);
expectedUserAndTeam131PolicyOrder.addAll(getAllTeamPolicies(null, team131Policies));
expectedUserAndTeam131PolicyOrder.addAll(getAllTeamPolicies(null, team13Policies));
assertPolicyIterator(expectedUserAndTeam131PolicyOrder, policyContextIterator);
}
@Test
@ -160,44 +199,12 @@ public class SubjectContextTest {
}
@Test
void testResourcePolicyIterator() {
// A resource with user as owner and make sure all policies from user's hierarchy is in the iterator
EntityReference userOwner = user.getEntityReference();
SubjectContext subjectContext = SubjectCache.getInstance().getSubjectContext(user.getName());
Iterator<PolicyContext> actualPolicyIterator = subjectContext.getResourcePolicies(userOwner);
assertUserPolicyIterator(actualPolicyIterator);
// A resource with team1 as owner and make sure all policies from user's hierarchy is in the iterator
EntityReference team1Owner = team1.getEntityReference();
actualPolicyIterator = subjectContext.getResourcePolicies(team1Owner);
// add policies from team1
List<String> expectedPolicyOrder = new ArrayList<>(getAllTeamPolicies(team1Roles, team1Policies));
assertPolicyIterator(expectedPolicyOrder, actualPolicyIterator);
// A resource with team11 as owner and make sure all policies from user's hierarchy is in the iterator
EntityReference team11Owner = team11.getEntityReference();
actualPolicyIterator = subjectContext.getResourcePolicies(team11Owner);
List<String> list = new ArrayList<>(getAllTeamPolicies(team11Roles, team11Policies)); // add policies from team11
list.addAll(expectedPolicyOrder); // Add all policies from parent team1 previously setup
expectedPolicyOrder = list;
assertPolicyIterator(expectedPolicyOrder, actualPolicyIterator);
// A resource with team11 as owner and make sure all policies from user's hierarchy is in the iterator
EntityReference team111Owner = team111.getEntityReference();
actualPolicyIterator = subjectContext.getResourcePolicies(team111Owner);
list = new ArrayList<>(getAllTeamPolicies(team111Roles, team111Policies)); // add policies from team111
list.addAll(expectedPolicyOrder); // Add all policies form team11 and team1 previously setup
list.addAll(getPolicyListFromRoles(team12Roles)); // add policies from team12 roles
list.addAll(getPolicyList(team12Policies)); // add team12 policies
assertPolicyIterator(list, actualPolicyIterator);
}
private static List<Role> getRoles(String prefix, int count) {
private static List<Role> getRoles(String prefix) {
// Create roles with 3 policies each and each policy with 3 rules
List<Role> roles = new ArrayList<>(count);
for (int i = 1; i <= count; i++) {
List<Role> roles = new ArrayList<>(3);
for (int i = 1; i <= 3; i++) {
String name = prefix + "_role_" + i;
List<EntityReference> policies = toEntityReferences(getPolicies(name, 3));
List<EntityReference> policies = toEntityReferences(getPolicies(name));
Role role = new Role().withName(name).withId(UUID.randomUUID()).withPolicies(policies);
RoleCache.ROLE_CACHE.put(role.getId(), role);
roles.add(role);
@ -205,21 +212,21 @@ public class SubjectContextTest {
return roles;
}
private static List<Policy> getPolicies(String prefix, int count) {
List<Policy> policies = new ArrayList<>(count);
for (int i = 1; i <= count; i++) {
private static List<Policy> getPolicies(String prefix) {
List<Policy> policies = new ArrayList<>(3);
for (int i = 1; i <= 3; i++) {
String name = prefix + "_policy_" + i;
Policy policy = new Policy().withName(name).withId(UUID.randomUUID()).withRules(getRules(name, 3));
Policy policy = new Policy().withName(name).withId(UUID.randomUUID()).withRules(getRules(name));
policies.add(policy);
PolicyCache.POLICY_CACHE.put(policy.getId(), PolicyCache.getInstance().getRules(policy));
}
return policies;
}
private static List<Rule> getRules(String prefix, int count) {
List<Rule> rules = new ArrayList<>(count);
for (int i = 1; i <= count; i++) {
rules.add(new Rule().withName(prefix + "rule" + count));
private static List<Rule> getRules(String prefix) {
List<Rule> rules = new ArrayList<>(3);
for (int i = 1; i <= 3; i++) {
rules.add(new Rule().withName(prefix + "rule" + 3));
}
return rules;
}
@ -234,14 +241,14 @@ public class SubjectContextTest {
private static List<String> getAllTeamPolicies(List<Role> roles, List<Policy> policies) {
List<String> list = new ArrayList<>();
list.addAll(getPolicyListFromRoles(roles));
list.addAll(getPolicyList(policies));
listOrEmpty(list).addAll(getPolicyListFromRoles(roles));
listOrEmpty(list).addAll(getPolicyList(policies));
return list;
}
private static List<String> getPolicyListFromRoles(List<Role> roles) {
List<String> list = new ArrayList<>();
roles.forEach(r -> list.addAll(getPolicyRefList(r.getPolicies())));
listOrEmpty(roles).forEach(r -> list.addAll(getPolicyRefList(r.getPolicies())));
return list;
}
@ -270,19 +277,6 @@ public class SubjectContextTest {
return team;
}
void assertUserPolicyIterator(Iterator<PolicyContext> actualPolicyIterator) {
//
// Check iteration order of the policies
//
List<String> expectedPolicyOrder = new ArrayList<>();
expectedPolicyOrder.addAll(getPolicyListFromRoles(userRoles)); // First polices associated with user roles
expectedPolicyOrder.addAll(getAllTeamPolicies(team111Roles, team111Policies)); // Next parent team111 policies
expectedPolicyOrder.addAll(getAllTeamPolicies(team11Roles, team11Policies)); // Next team111 parent team11 policies
expectedPolicyOrder.addAll(getAllTeamPolicies(team1Roles, team1Policies)); // Next team11 parent team1 policies
expectedPolicyOrder.addAll(getAllTeamPolicies(team12Roles, team12Policies)); // Next team111 parent team12 policies
assertPolicyIterator(expectedPolicyOrder, actualPolicyIterator);
}
void assertPolicyIterator(List<String> expectedPolicyOrder, Iterator<PolicyContext> actualPolicyIterator) {
int count = 0;
while (actualPolicyIterator.hasNext()) {

View File

@ -18,9 +18,4 @@ public @interface Function {
String[] examples();
ParameterType paramInputType() default ParameterType.NOT_REQUIRED;
/**
* Some functions are used for capturing resource based rules where policies are applied based on resource being
* accessed and team hierarchy the resource belongs to instead of the subject.
*/
boolean resourceBased() default false;
}