diff --git a/elide-core/src/main/java/com/yahoo/elide/core/security/executors/AbstractPermissionExecutor.java b/elide-core/src/main/java/com/yahoo/elide/core/security/executors/AbstractPermissionExecutor.java new file mode 100644 index 0000000000..b67c1da010 --- /dev/null +++ b/elide-core/src/main/java/com/yahoo/elide/core/security/executors/AbstractPermissionExecutor.java @@ -0,0 +1,264 @@ +/* + * Copyright 2021, Yahoo Inc. + * Licensed under the Apache License, Version 2.0 + * See LICENSE file in project root for terms. + */ + +package com.yahoo.elide.core.security.executors; + +import static com.yahoo.elide.core.security.permissions.ExpressionResult.DEFERRED; +import static com.yahoo.elide.core.security.permissions.ExpressionResult.FAIL; +import static com.yahoo.elide.core.security.permissions.ExpressionResult.PASS; + +import com.yahoo.elide.annotation.DeletePermission; +import com.yahoo.elide.annotation.ReadPermission; +import com.yahoo.elide.core.RequestScope; +import com.yahoo.elide.core.exceptions.ForbiddenAccessException; +import com.yahoo.elide.core.security.PermissionExecutor; +import com.yahoo.elide.core.security.permissions.ExpressionResult; +import com.yahoo.elide.core.security.permissions.ExpressionResultCache; +import com.yahoo.elide.core.security.permissions.PermissionExpressionBuilder; +import com.yahoo.elide.core.security.permissions.expressions.Expression; +import com.yahoo.elide.core.type.Type; + +import com.google.common.collect.ImmutableSet; + +import org.apache.commons.lang3.tuple.Triple; +import org.slf4j.Logger; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.lang.annotation.Annotation; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.Queue; +import java.util.Set; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.function.Function; +import java.util.function.Supplier; + +/** + * Abstract Permission Executor with common permission executor functionalities. + */ +public abstract class AbstractPermissionExecutor implements PermissionExecutor { + private final Logger log; + protected final Queue commitCheckQueue = new LinkedBlockingQueue<>(); + + protected final RequestScope requestScope; + protected final PermissionExpressionBuilder expressionBuilder; + protected final Map, Type, ImmutableSet>, ExpressionResult> + userPermissionCheckCache; + protected final Map checkStats; + + public AbstractPermissionExecutor(Logger log, RequestScope requestScope) { + ExpressionResultCache cache = new ExpressionResultCache(); + this.log = log; + this.requestScope = requestScope; + this.expressionBuilder = new PermissionExpressionBuilder(cache, requestScope.getDictionary()); + userPermissionCheckCache = new HashMap<>(); + checkStats = new HashMap<>(); + } + + /** + * Execute commmit checks. + */ + @Override + public void executeCommitChecks() { + commitCheckQueue.forEach((expr) -> { + Expression expression = expr.getExpression(); + ExpressionResult result = expression.evaluate(Expression.EvaluationMode.ALL_CHECKS); + if (result == FAIL) { + ForbiddenAccessException e = new ForbiddenAccessException( + expr.getAnnotationClass(), expression, Expression.EvaluationMode.ALL_CHECKS); + if (log.isTraceEnabled()) { + log.trace("{}", e.getLoggedMessage()); + } + throw e; + } + }); + commitCheckQueue.clear(); + } + + /** + * First attempts to check user permissions (by looking in the cache and if not present by executing user + * permissions). If user permissions don't short circuit the check, run the provided expression executor. + * + * @param type parameter + * @param resourceClass Resource class + * @param annotationClass Annotation class + * @param fields Set of all field names that is being accessed + * @param expressionSupplier Builds a permission expression. + * @param expressionExecutor Evaluates the expression (post user check evaluation) + */ + protected ExpressionResult checkPermissions( + Type resourceClass, + Class annotationClass, + Set fields, + Supplier expressionSupplier, + Optional> expressionExecutor) { + + // If the user check has already been evaluated before, return the result directly and save the building cost + ImmutableSet immutableFields = fields == null ? null : ImmutableSet.copyOf(fields); + ExpressionResult expressionResult + = userPermissionCheckCache.get(Triple.of(annotationClass, resourceClass, immutableFields)); + + if (expressionResult == PASS) { + return expressionResult; + } + + Expression expression = expressionSupplier.get(); + + if (expressionResult == null) { + expressionResult = executeExpressions( + expression, + annotationClass, + Expression.EvaluationMode.USER_CHECKS_ONLY); + + userPermissionCheckCache.put( + Triple.of(annotationClass, resourceClass, immutableFields), expressionResult); + + if (expressionResult == PASS) { + return expressionResult; + } + } + + return expressionExecutor + .map(executor -> executor.apply(expression)) + .orElse(expressionResult); + } + + /** + * Only executes user permissions. + * + * @param type parameter + * @param resourceClass Resource class + * @param annotationClass Annotation class + * @param fields Set of all field names that is being accessed + * @param expressionSupplier Builds a permission expression. + */ + protected ExpressionResult checkOnlyUserPermissions( + Type resourceClass, + Class annotationClass, + Set fields, + Supplier expressionSupplier) { + + return checkPermissions( + resourceClass, + annotationClass, + fields, + expressionSupplier, + Optional.empty() + ); + } + + /** + * First attempts to check user permissions (by looking in the cache and if not present by executing user + * permissions). If user permissions don't short circuit the check, run the provided expression executor. + * + * @param type parameter + * @param resourceClass Resource class + * @param annotationClass Annotation class + * @param fields Set of all field names that is being accessed + * @param expressionSupplier Builds a permission expression. + * @param expressionExecutor Evaluates the expression (post user check evaluation) + */ + protected ExpressionResult checkPermissions( + Type resourceClass, + Class annotationClass, + Set fields, + Supplier expressionSupplier, + Function expressionExecutor) { + + return checkPermissions( + resourceClass, + annotationClass, + fields, + expressionSupplier, + Optional.of(expressionExecutor) + ); + } + + /** + * Execute expressions. + * + * @param expression The expression to evaluate. + * @param annotationClass The permission associated with the expression. + * @param mode The evaluation mode of the expression. + */ + protected ExpressionResult executeExpressions(final Expression expression, + final Class annotationClass, + Expression.EvaluationMode mode) { + + ExpressionResult result = expression.evaluate(mode); + + // Record the check + if (log.isTraceEnabled()) { + String checkKey = expression.toString(); + Long checkOccurrences = checkStats.getOrDefault(checkKey, 0L) + 1; + checkStats.put(checkKey, checkOccurrences); + } + + if (result == DEFERRED) { + + /* + * Checking user checks only are an optimization step. We don't need to defer these checks because + * INLINE_ONLY checks will be evaluated later. Also, the user checks don't have + * the correct context to evaluate as COMMIT checks later. + */ + if (mode == Expression.EvaluationMode.USER_CHECKS_ONLY) { + return DEFERRED; + } + + + if (isInlineOnlyCheck(annotationClass)) { + // Force evaluation of checks that can only be executed inline. + result = expression.evaluate(Expression.EvaluationMode.ALL_CHECKS); + if (result == FAIL) { + ForbiddenAccessException e = new ForbiddenAccessException( + annotationClass, + expression, + Expression.EvaluationMode.ALL_CHECKS); + if (log.isTraceEnabled()) { + log.trace("{}", e.getLoggedMessage()); + } + throw e; + } + return result; + } + commitCheckQueue.add(new AbstractPermissionExecutor.QueuedCheck(expression, annotationClass)); + return DEFERRED; + } + if (result == FAIL) { + ForbiddenAccessException e = new ForbiddenAccessException(annotationClass, expression, mode); + if (log.isTraceEnabled()) { + log.trace("{}", e.getLoggedMessage()); + } + throw e; + } + + return result; + } + + /** + * Check whether or not this check can only be run inline or not. + * + * @param annotationClass annotation class + * @return True if check can only be run inline, false otherwise. + */ + private boolean isInlineOnlyCheck(final Class annotationClass) { + return ReadPermission.class.isAssignableFrom(annotationClass) + || DeletePermission.class.isAssignableFrom(annotationClass); + } + + /** + * Information container about queued checks. + */ + @AllArgsConstructor + private static class QueuedCheck { + @Getter + private final Expression expression; + @Getter private final Class annotationClass; + } +} diff --git a/elide-core/src/main/java/com/yahoo/elide/core/security/executors/AggregationStorePermissionExecutor.java b/elide-core/src/main/java/com/yahoo/elide/core/security/executors/AggregationStorePermissionExecutor.java new file mode 100644 index 0000000000..9f5ac69696 --- /dev/null +++ b/elide-core/src/main/java/com/yahoo/elide/core/security/executors/AggregationStorePermissionExecutor.java @@ -0,0 +1,171 @@ +/* + * Copyright 2021, Yahoo Inc. + * Licensed under the Apache License, Version 2.0 + * See LICENSE file in project root for terms. + */ + +package com.yahoo.elide.core.security.executors; + +import com.yahoo.elide.annotation.ReadPermission; +import com.yahoo.elide.core.PersistentResource; +import com.yahoo.elide.core.RequestScope; +import com.yahoo.elide.core.filter.expression.FilterExpression; +import com.yahoo.elide.core.security.ChangeSpec; +import com.yahoo.elide.core.security.permissions.ExpressionResult; +import com.yahoo.elide.core.security.permissions.expressions.Expression; +import com.yahoo.elide.core.type.Type; + +import lombok.extern.slf4j.Slf4j; + +import java.lang.annotation.Annotation; +import java.util.Collections; +import java.util.Optional; +import java.util.Set; +import java.util.function.Supplier; + +/** + * Permission Executor for all models managed by aggregation datastore. + */ +@Slf4j +public class AggregationStorePermissionExecutor extends AbstractPermissionExecutor { + + public AggregationStorePermissionExecutor(RequestScope requestScope) { + super(log, requestScope); + } + + /** + * Checks user checks for the requested fields. + * expression = (field1Rule OR field2Rule ... OR fieldNRule) + * @param annotationClass annotation class + * @param resource resource + * @param requestedFields the list of requested fields + * @param + * @return ExpressionResult - result of the above any field expression + */ + @Override + public ExpressionResult checkPermission(Class annotationClass, + PersistentResource resource, + Set requestedFields) { + if (!annotationClass.equals(ReadPermission.class)) { + return ExpressionResult.FAIL; + } + + Supplier expressionSupplier = () -> + expressionBuilder.buildUserCheckAnyFieldOnlyExpression( + resource.getResourceType(), + annotationClass, + requestedFields, + requestScope); + + return checkOnlyUserPermissions( + resource.getResourceType(), + annotationClass, + requestedFields, + expressionSupplier); + } + + + /** + * Evaluates user check permission on specific field + * Aggregation Datastore model can only have user checks at field level permission expression. + * @param resource resource + * @param changeSpec changepsec + * @param annotationClass annotation class + * @param field field to check + * @param + * @return + */ + @Override + public ExpressionResult checkSpecificFieldPermissions(PersistentResource resource, + ChangeSpec changeSpec, + Class annotationClass, + String field) { + if (!annotationClass.equals(ReadPermission.class)) { + return ExpressionResult.FAIL; + } + return checkUserPermissions(resource.getResourceType(), annotationClass, field); + } + + /** + * Not supported in aggregation datastore. + * @param resource resource + * @param changeSpec changepsec + * @param annotationClass annotation class + * @param field field to check + * @param + * @return + */ + @Override + public ExpressionResult checkSpecificFieldPermissionsDeferred(PersistentResource resource, + ChangeSpec changeSpec, + Class annotationClass, + String field) { + throw new UnsupportedOperationException(); + } + + /** + * Check strictly user permissions on an entity and any of the requested field. Evaluates + * expression = (entityRule AND (field1Rule OR field2Rule ... OR fieldNRule)) + * + * @param type parameter + * @param resourceClass Resource class + * @param annotationClass Annotation class + */ + @Override + public ExpressionResult checkUserPermissions(Type resourceClass, + Class annotationClass, + Set requestedFields) { + if (!annotationClass.equals(ReadPermission.class)) { + return ExpressionResult.FAIL; + } + + Expression expression = expressionBuilder.buildUserCheckEntityAndAnyFieldExpression( + resourceClass, + annotationClass, + requestedFields, + requestScope); + + // Skips userPermissionCheckCache and directly executes the expression. + // Cache is used by checkPermission() which is called for every resource returned. + // Cache cannot be shared b/w checkUserPermissions and checkPermissions because of difference in computation. + return executeExpressions(expression, annotationClass, Expression.EvaluationMode.USER_CHECKS_ONLY); + } + + /** + * Check strictly user permissions on an entity field. + * + * @param type parameter + * @param resourceClass Resource class + * @param annotationClass Annotation class + * @param field The entity field + */ + @Override + public ExpressionResult checkUserPermissions(Type resourceClass, + Class annotationClass, + String field) { + if (!annotationClass.equals(ReadPermission.class)) { + return ExpressionResult.FAIL; + } + + Supplier expressionSupplier = () -> + expressionBuilder.buildUserCheckFieldExpressions( + resourceClass, + requestScope, + annotationClass, + field, + false); + + + return checkOnlyUserPermissions( + resourceClass, + annotationClass, + Collections.singleton(field), + expressionSupplier); + } + + @Override + public Optional getReadPermissionFilter(Type resourceClass, Set requestedFields) { + FilterExpression filterExpression = expressionBuilder.buildEntityFilterExpression(resourceClass, requestScope); + return Optional.ofNullable(filterExpression); + } +} diff --git a/elide-core/src/main/java/com/yahoo/elide/core/security/permissions/PermissionExpressionBuilder.java b/elide-core/src/main/java/com/yahoo/elide/core/security/permissions/PermissionExpressionBuilder.java index 514fc7eeba..d4605d9dcc 100644 --- a/elide-core/src/main/java/com/yahoo/elide/core/security/permissions/PermissionExpressionBuilder.java +++ b/elide-core/src/main/java/com/yahoo/elide/core/security/permissions/PermissionExpressionBuilder.java @@ -18,6 +18,7 @@ import com.yahoo.elide.core.security.ChangeSpec; import com.yahoo.elide.core.security.CheckInstantiator; import com.yahoo.elide.core.security.checks.Check; +import com.yahoo.elide.core.security.permissions.expressions.AndExpression; import com.yahoo.elide.core.security.permissions.expressions.AnyFieldExpression; import com.yahoo.elide.core.security.permissions.expressions.CheckExpression; import com.yahoo.elide.core.security.permissions.expressions.Expression; @@ -81,7 +82,8 @@ public Expression buildSpecificFieldExpressions(final Per final Function, Expression> buildExpressionFn = (checkFn) -> buildSpecificFieldExpression( PermissionCondition.create(annotationClass, resource, field, changeSpec), - checkFn + checkFn, + true ); return buildExpressionFn.apply(leafBuilderFn); @@ -136,6 +138,27 @@ public Expression buildUserCheckFieldExpressions(final Ty final RequestScope scope, final Class annotationClass, final String field) { + return buildUserCheckFieldExpressions(resourceClass, scope, annotationClass, field, true); + } + + /** + * Build an expression that strictly evaluates UserCheck's and ignores other checks for a specific field. + *

+ * NOTE: This method returns _NO_ commit checks. + * + * @param resourceClass Resource Class + * @param scope The request scope. + * @param annotationClass Annotation class + * @param field Field to check (if null only check entity-level) + * @param includeEntityPermission whether entity permission needs to be evaluated in the absence of field permission + * @param type parameter + * @return User check expression to evaluate + */ + public Expression buildUserCheckFieldExpressions(final Type resourceClass, + final RequestScope scope, + final Class annotationClass, + final String field, + final boolean includeEntityPermission) { if (!entityDictionary.entityHasChecksForPermission(resourceClass, annotationClass)) { return SUCCESSFUL_EXPRESSION; } @@ -144,11 +167,12 @@ public Expression buildUserCheckFieldExpressions(final Ty new CheckExpression(check, null, scope, null, cache); return buildSpecificFieldExpression(new PermissionCondition(annotationClass, resourceClass, field), - leafBuilderFn); + leafBuilderFn, includeEntityPermission); } /** * Build an expression that strictly evaluates UserCheck's and ignores other checks for an entity. + * expression = (field1Rule OR field2Rule ... OR fieldNRule) *

* NOTE: This method returns _NO_ commit checks. * @@ -171,24 +195,90 @@ public Expression buildUserCheckAnyExpression(final Type< requestedFields, requestScope); } + /** + * Build an expression that strictly evaluates UserCheck's and ignores other checks for an entity. + * expression = (field1Rule OR field2Rule ... OR fieldNRule) + *

+ * NOTE: This method returns _NO_ commit checks. + * + * @param resourceClass Resource class + * @param annotationClass Annotation class + * @param requestScope Request scope + * @param type parameter + * @return User check expression to evaluate + */ + public Expression buildUserCheckAnyFieldOnlyExpression(final Type resourceClass, + final Class annotationClass, + Set requestedFields, + final RequestScope requestScope) { + + final Function leafBuilderFn = (check) -> + new CheckExpression(check, null, requestScope, null, cache); + + return buildAnyFieldOnlyExpression( + new PermissionCondition(annotationClass, resourceClass), leafBuilderFn, requestedFields); + } + + /** + * Build an expression that strictly evaluates UserCheck's and ignores other checks for an entity. + * expression = (entityRule AND (field1Rule OR field2Rule ... OR fieldNRule)) + *

+ * NOTE: This method returns _NO_ commit checks. + * + * @param resourceClass Resource class + * @param annotationClass Annotation class + * @param scope Request scope + * @param type parameter + * @return User check expression to evaluate + */ + public Expression buildUserCheckEntityAndAnyFieldExpression(final Type resourceClass, + final Class annotationClass, + Set requestedFields, + final RequestScope scope) { + + final Function leafBuilderFn = (check) -> + new CheckExpression(check, null, scope, null, cache); + + ParseTree classPermissions = entityDictionary.getPermissionsForClass(resourceClass, annotationClass); + Expression entityExpression = normalizedExpressionFromParseTree(classPermissions, leafBuilderFn); + + Expression anyFieldExpression = buildAnyFieldOnlyExpression( + new PermissionCondition(annotationClass, resourceClass), leafBuilderFn, + requestedFields); + + if (entityExpression == null) { + return anyFieldExpression; + } + + return new AndExpression(entityExpression, anyFieldExpression); + } + /** * Builder for specific field expressions. * * @param condition The condition which triggered this permission expression check * @param checkFn Operation check function + * @param includeEntityPermission whether entity permission needs to be evaluated in the absence of field permission * @return Expressions representing specific field */ private Expression buildSpecificFieldExpression(final PermissionCondition condition, - final Function checkFn) { + final Function checkFn, + boolean includeEntityPermission) { Type resourceClass = condition.getEntityClass(); Class annotationClass = condition.getPermission(); String field = condition.getField().orElse(null); - ParseTree classPermissions = entityDictionary.getPermissionsForClass(resourceClass, annotationClass); ParseTree fieldPermissions = entityDictionary.getPermissionsForField(resourceClass, field, annotationClass); + if (includeEntityPermission) { + ParseTree classPermissions = entityDictionary.getPermissionsForClass(resourceClass, annotationClass); + return new SpecificFieldExpression(condition, + normalizedExpressionFromParseTree(classPermissions, checkFn), + normalizedExpressionFromParseTree(fieldPermissions, checkFn) + ); + } return new SpecificFieldExpression(condition, - normalizedExpressionFromParseTree(classPermissions, checkFn), + null, normalizedExpressionFromParseTree(fieldPermissions, checkFn) ); } @@ -258,6 +348,43 @@ private Expression buildAnyFieldExpression(final PermissionCondition condition, return new AnyFieldExpression(condition, allFieldsExpression); } + /** + * Builds disjunction of permission expression of all requested fields. + * If the field permission is null, then return default SUCCESSFUL_EXPRESSION. + * expression = (field1Rule OR field2Rule ... OR fieldNRule) + * @param condition The condition which triggered this permission expression check + * @param checkFn check function + * @param requestedFields The list of requested fields + * @return Expression + */ + private Expression buildAnyFieldOnlyExpression(final PermissionCondition condition, + final Function checkFn, + final Set requestedFields) { + Type resourceClass = condition.getEntityClass(); + Class annotationClass = condition.getPermission(); + + OrExpression allFieldsExpression = new OrExpression(FAILURE, null); + List fields = entityDictionary.getAllFields(resourceClass); + + for (String field : fields) { + if (requestedFields != null && !requestedFields.contains(field)) { + continue; + } + + ParseTree fieldPermissions = entityDictionary.getPermissionsForField(resourceClass, field, annotationClass); + Expression fieldExpression = normalizedExpressionFromParseTree(fieldPermissions, checkFn); + + if (fieldExpression == null) { + return SUCCESSFUL_EXPRESSION; + } + + allFieldsExpression = new OrExpression(allFieldsExpression, fieldExpression); + } + + return new AnyFieldExpression(condition, allFieldsExpression); + } + + /** * Build an expression representing any field on an entity. * @@ -320,6 +447,24 @@ public FilterExpression buildAnyFieldFilterExpression( return allFieldsFilterExpression; } + /** + * Build a filter expression for entity permission alone + * @param forType Resource class + * @param requestScope Request Scope + * @return + */ + public FilterExpression buildEntityFilterExpression(Type forType, RequestScope requestScope) { + ParseTree classPermissions = entityDictionary.getPermissionsForClass(forType, ReadPermission.class); + FilterExpression entityFilter = filterExpressionFromParseTree(classPermissions, forType, requestScope); + //case where the permissions does not have ANY filterExpressionCheck + if (entityFilter == FALSE_USER_CHECK_EXPRESSION + || entityFilter == NO_EVALUATION_EXPRESSION + || entityFilter == TRUE_USER_CHECK_EXPRESSION) { + return null; + } + return entityFilter; + } + private Expression normalizedExpressionFromParseTree(ParseTree permissions, Function checkFn) { if (permissions == null) { return null; diff --git a/elide-core/src/test/java/com/yahoo/elide/core/security/AggregationStorePermissionExecutorTest.java b/elide-core/src/test/java/com/yahoo/elide/core/security/AggregationStorePermissionExecutorTest.java new file mode 100644 index 0000000000..c2d69fff1f --- /dev/null +++ b/elide-core/src/test/java/com/yahoo/elide/core/security/AggregationStorePermissionExecutorTest.java @@ -0,0 +1,199 @@ +/* + * Copyright 2021, Yahoo Inc. + * Licensed under the Apache License, Version 2.0 + * See LICENSE file in project root for terms. + */ + +package com.yahoo.elide.core.security; + +import static com.yahoo.elide.core.dictionary.EntityDictionary.NO_VERSION; + +import com.yahoo.elide.ElideSettings; +import com.yahoo.elide.ElideSettingsBuilder; +import com.yahoo.elide.annotation.Include; +import com.yahoo.elide.annotation.ReadPermission; +import com.yahoo.elide.core.Path; +import com.yahoo.elide.core.PersistentResource; +import com.yahoo.elide.core.dictionary.EntityDictionary; +import com.yahoo.elide.core.dictionary.TestDictionary; +import com.yahoo.elide.core.exceptions.ForbiddenAccessException; +import com.yahoo.elide.core.filter.expression.FilterExpression; +import com.yahoo.elide.core.filter.predicates.NotNullPredicate; +import com.yahoo.elide.core.security.checks.Check; +import com.yahoo.elide.core.security.checks.FilterExpressionCheck; +import com.yahoo.elide.core.security.checks.prefab.Role; +import com.yahoo.elide.core.security.executors.AggregationStorePermissionExecutor; +import com.yahoo.elide.core.security.permissions.ExpressionResult; +import com.yahoo.elide.core.type.ClassType; +import com.yahoo.elide.core.type.Type; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; + +import lombok.Value; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.UUID; + +import javax.persistence.Entity; + +/** + * test Aggregation Store Permission Executor + */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +public class AggregationStorePermissionExecutorTest { + private EntityDictionary dictionary; + private ElideSettings elideSettings; + + + public static class FilterCheck extends FilterExpressionCheck { + + @Override + public FilterExpression getFilterExpression(Type entityClass, com.yahoo.elide.core.security.RequestScope requestScope) { + Path path = super.getFieldPath(entityClass, requestScope, "getFilterDim", "filterDim"); + return new NotNullPredicate(path); + } + } + + @BeforeAll + public void setup() { + Map> checks = new HashMap<>(); + checks.put("user all", Role.ALL.class); + checks.put("user none", Role.NONE.class); + checks.put("filter check", FilterCheck.class); + dictionary = TestDictionary.getTestDictionary(checks); + elideSettings = new ElideSettingsBuilder(null) + .withEntityDictionary(dictionary) + .build(); + } + + @Test + public void testUserPermissions() { + @Entity + @Include + @Value + @ReadPermission(expression = "user none or filter check") + class Model { + String filterDim; + long metric; + long metric2; + + @ReadPermission(expression = "user all") + public String getFilterDim() { + return filterDim; + } + + @ReadPermission(expression = "user none") + public long getMetric() { + return metric; + } + } + + com.yahoo.elide.core.RequestScope scope = bindAndgetRequestScope(Model.class); + PermissionExecutor executor = scope.getPermissionExecutor(); + + // evaluated expression = user all + Assertions.assertEquals( + ExpressionResult.PASS, + executor.checkUserPermissions(ClassType.of(Model.class), ReadPermission.class, "filterDim")); + + + // evaluated expression = user none -> ForbiddenAccess + Assertions.assertThrows( + ForbiddenAccessException.class, + () -> executor.checkUserPermissions(ClassType.of(Model.class), ReadPermission.class, "metric")); + + + // evaluated expression = null -> false + Assertions.assertEquals( + ExpressionResult.PASS, + executor.checkSpecificFieldPermissions( + new PersistentResource(new Model("dim1", 0, 1), "1", scope), + null, ReadPermission.class, "metric2")); + + + // evaluated expression = (user none or filter check) AND (user all OR user none) + Assertions.assertEquals( + ExpressionResult.DEFERRED, + executor.checkUserPermissions(ClassType.of(Model.class), ReadPermission.class, new HashSet<>(Arrays.asList("filterDim", "metric")))); + + // evaluated expression = (user all OR user none) + Assertions.assertEquals( + ExpressionResult.PASS, + executor.checkPermission(ReadPermission.class, + new PersistentResource(new Model("dim1", 0, 1), "1", scope), + new HashSet<>(Arrays.asList("filterDim", "metric")))); + + // evaluated expression = (user none OR null) + Assertions.assertEquals( + ExpressionResult.PASS, + executor.checkPermission(ReadPermission.class, + new PersistentResource(new Model("dim1", 0, 1), "1", scope), + new HashSet<>(Arrays.asList("metric", "metric2")))); + + + } + + @Test + public void filterTest() { + @Entity + @Include + @Value + @ReadPermission(expression = "user none or filter check") + class Model1 { + String filterDim; + long metric; + } + + com.yahoo.elide.core.RequestScope scope = bindAndgetRequestScope(Model1.class); + PermissionExecutor executor = scope.getPermissionExecutor(); + + FilterExpression expression = executor.getReadPermissionFilter(ClassType.of(Model1.class), + new HashSet<>(Arrays.asList("filterDim", "metric"))) + .orElse(null); + Assertions.assertNotNull(expression); + Assertions.assertEquals("model1.filterDim NOTNULL []", expression.toString()); + + + @Entity + @Include + @Value + @ReadPermission(expression = "user none and filter check") + class Model2 { + String filterDim; + long metric; + } + + scope = bindAndgetRequestScope(Model2.class); + executor = scope.getPermissionExecutor(); + + expression = executor.getReadPermissionFilter(ClassType.of(Model2.class), new HashSet<>(Arrays.asList("filterDim", "metric"))).orElse(null); + Assertions.assertNull(expression); + + @Entity + @Include + @Value + @ReadPermission(expression = "user all or filter check") + class Model3 { + String filterDim; + long metric; + } + + scope = bindAndgetRequestScope(Model3.class); + executor = scope.getPermissionExecutor(); + + expression = executor.getReadPermissionFilter(ClassType.of(Model3.class), new HashSet<>(Arrays.asList("filterDim", "metric"))).orElse(null); + Assertions.assertNull(expression); + } + + private com.yahoo.elide.core.RequestScope bindAndgetRequestScope(Class clz) { + dictionary.bindEntity(clz); + dictionary.bindPermissionExecutor(clz, AggregationStorePermissionExecutor::new); + return new com.yahoo.elide.core.RequestScope(null, null, NO_VERSION, null, null, null, null, null, UUID.randomUUID(), elideSettings); + } +}