diff --git a/elide-core/src/main/java/com/yahoo/elide/core/RequestScope.java b/elide-core/src/main/java/com/yahoo/elide/core/RequestScope.java index 57ae75d33d..f6ef4036fa 100644 --- a/elide-core/src/main/java/com/yahoo/elide/core/RequestScope.java +++ b/elide-core/src/main/java/com/yahoo/elide/core/RequestScope.java @@ -183,7 +183,7 @@ public RequestScope(String baseUrlEndPoint, Function permissionExecutorGenerator = elideSettings.getPermissionExecutor(); this.permissionExecutor = new MultiplexPermissionExecutor( - dictionary.getPermissionExecutors(this), + dictionary.buildPermissionExecutors(this), (permissionExecutorGenerator == null) ? new ActivePermissionExecutor(this) : permissionExecutorGenerator.apply(this), diff --git a/elide-core/src/main/java/com/yahoo/elide/core/dictionary/EntityDictionary.java b/elide-core/src/main/java/com/yahoo/elide/core/dictionary/EntityDictionary.java index 8bce3b9be7..8618342035 100644 --- a/elide-core/src/main/java/com/yahoo/elide/core/dictionary/EntityDictionary.java +++ b/elide-core/src/main/java/com/yahoo/elide/core/dictionary/EntityDictionary.java @@ -109,6 +109,7 @@ public class EntityDictionary { protected final ConcurrentHashMap, Type> bindJsonApiToEntity = new ConcurrentHashMap<>(); protected final ConcurrentHashMap, EntityBinding> entityBindings = new ConcurrentHashMap<>(); + @Getter protected final ConcurrentHashMap, Function> entityPermissionExecutor = new ConcurrentHashMap<>(); protected final CopyOnWriteArrayList> bindEntityRoots = new CopyOnWriteArrayList<>(); @@ -1088,7 +1089,7 @@ public void bindPermissionExecutor(Type clz, * @param scope - request scope to generate permission executor. * @return Map of bound model type to its permission executor object. */ - public Map, PermissionExecutor> getPermissionExecutors(RequestScope scope) { + public Map, PermissionExecutor> buildPermissionExecutors(RequestScope scope) { return entityPermissionExecutor.entrySet().stream() .collect(Collectors.toMap( Map.Entry::getKey, 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 index 9f5ac69696..66a351dda4 100644 --- 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 @@ -100,7 +100,7 @@ public ExpressionResult checkSpecificFieldPermissionsDefe ChangeSpec changeSpec, Class annotationClass, String field) { - throw new UnsupportedOperationException(); + return checkSpecificFieldPermissions(resource, changeSpec, annotationClass, field); } /** 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 d4605d9dcc..1fde503a50 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 @@ -366,6 +366,8 @@ private Expression buildAnyFieldOnlyExpression(final PermissionCondition conditi OrExpression allFieldsExpression = new OrExpression(FAILURE, null); List fields = entityDictionary.getAllFields(resourceClass); + boolean fieldExpressionUsed = false; + for (String field : fields) { if (requestedFields != null && !requestedFields.contains(field)) { continue; @@ -377,10 +379,15 @@ private Expression buildAnyFieldOnlyExpression(final PermissionCondition conditi if (fieldExpression == null) { return SUCCESSFUL_EXPRESSION; } + fieldExpressionUsed = true; allFieldsExpression = new OrExpression(allFieldsExpression, fieldExpression); } + if (!fieldExpressionUsed) { + return SUCCESSFUL_EXPRESSION; + } + return new AnyFieldExpression(condition, allFieldsExpression); } diff --git a/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/AggregationDataStore.java b/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/AggregationDataStore.java index 3fa2112723..45efc525b1 100644 --- a/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/AggregationDataStore.java +++ b/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/AggregationDataStore.java @@ -6,14 +6,16 @@ package com.yahoo.elide.datastores.aggregation; import com.yahoo.elide.annotation.ReadPermission; +import com.yahoo.elide.core.RequestScope; import com.yahoo.elide.core.datastore.DataStore; import com.yahoo.elide.core.datastore.DataStoreTransaction; import com.yahoo.elide.core.dictionary.ArgumentType; import com.yahoo.elide.core.dictionary.EntityDictionary; +import com.yahoo.elide.core.security.PermissionExecutor; import com.yahoo.elide.core.security.checks.Check; import com.yahoo.elide.core.security.checks.FilterExpressionCheck; import com.yahoo.elide.core.security.checks.UserCheck; -import com.yahoo.elide.core.security.executors.ActivePermissionExecutor; +import com.yahoo.elide.core.security.executors.AggregationStorePermissionExecutor; import com.yahoo.elide.core.type.ClassType; import com.yahoo.elide.core.type.Type; import com.yahoo.elide.core.utils.ClassScanner; @@ -41,6 +43,7 @@ import java.util.List; import java.util.Objects; import java.util.Set; +import java.util.function.Function; import java.util.function.Predicate; /** @@ -54,6 +57,9 @@ public class AggregationDataStore implements DataStore { private final Set> dynamicCompiledClasses; private final QueryLogger queryLogger; + private final Function aggPermissionExecutor = + AggregationStorePermissionExecutor::new; + /** * These are the classes the Aggregation Store manages. */ @@ -71,14 +77,14 @@ public void populateEntityDictionary(EntityDictionary dictionary) { dynamicCompiledClasses.forEach(dynamicLoadedClass -> { dictionary.bindEntity(dynamicLoadedClass, Collections.singleton(Join.class)); validateModelExpressionChecks(dictionary, dynamicLoadedClass); - dictionary.bindPermissionExecutor(dynamicLoadedClass, ActivePermissionExecutor::new); + dictionary.bindPermissionExecutor(dynamicLoadedClass, aggPermissionExecutor); }); } ClassScanner.getAnnotatedClasses(AGGREGATION_STORE_CLASSES).forEach(cls -> { dictionary.bindEntity(cls, Collections.singleton(Join.class)); validateModelExpressionChecks(dictionary, ClassType.of(cls)); - dictionary.bindPermissionExecutor(cls, ActivePermissionExecutor::new); + dictionary.bindPermissionExecutor(cls, aggPermissionExecutor); } ); diff --git a/elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/checks/VideoGameFilterCheck.java b/elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/checks/VideoGameFilterCheck.java new file mode 100644 index 0000000000..6f7e1b00f4 --- /dev/null +++ b/elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/checks/VideoGameFilterCheck.java @@ -0,0 +1,28 @@ +/* + * Copyright 2021, Yahoo Inc. + * Licensed under the Apache License, Version 2.0 + * See LICENSE file in project root for terms. + */ +package com.yahoo.elide.datastores.aggregation.checks; + +import com.yahoo.elide.annotation.SecurityCheck; +import com.yahoo.elide.core.Path; +import com.yahoo.elide.core.filter.expression.FilterExpression; +import com.yahoo.elide.core.filter.predicates.PostfixPredicate; +import com.yahoo.elide.core.security.RequestScope; +import com.yahoo.elide.core.security.checks.FilterExpressionCheck; +import com.yahoo.elide.core.type.Type; +import com.yahoo.elide.datastores.aggregation.example.VideoGame; + +/** + * Filter Expression Check for video game + */ +@SecurityCheck(VideoGameFilterCheck.NAME_FILTER) +public class VideoGameFilterCheck extends FilterExpressionCheck { + public static final String NAME_FILTER = "player name filter"; + @Override + public FilterExpression getFilterExpression(Type entityClass, RequestScope requestScope) { + Path path = super.getFieldPath(entityClass, requestScope, "getPlayerName", "playerName"); + return new PostfixPredicate(path, "Doe"); + } +} diff --git a/elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/example/VideoGame.java b/elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/example/VideoGame.java index 74955088ad..a1a59ff9dd 100644 --- a/elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/example/VideoGame.java +++ b/elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/example/VideoGame.java @@ -6,6 +6,7 @@ package com.yahoo.elide.datastores.aggregation.example; import com.yahoo.elide.annotation.Include; +import com.yahoo.elide.annotation.ReadPermission; import com.yahoo.elide.datastores.aggregation.annotation.DimensionFormula; import com.yahoo.elide.datastores.aggregation.annotation.Join; import com.yahoo.elide.datastores.aggregation.annotation.JoinType; @@ -21,6 +22,7 @@ */ @Include @FromTable(name = "videoGames", dbConnectionName = "mycon") +@ReadPermission(expression = "admin.user or player name filter") public class VideoGame { @Setter private Long id; @@ -82,6 +84,7 @@ public Float getTimeSpentPerSession() { return timeSpentPerSession; } + @ReadPermission(expression = "operator") @MetricFormula("{{timeSpentPerSession}} / 100") public Float getTimeSpentPerGame() { return timeSpentPerGame; diff --git a/elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/integration/AggregationDataStoreIntegrationTest.java b/elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/integration/AggregationDataStoreIntegrationTest.java index 3e731aa7db..8876eff112 100644 --- a/elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/integration/AggregationDataStoreIntegrationTest.java +++ b/elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/integration/AggregationDataStoreIntegrationTest.java @@ -30,6 +30,7 @@ import com.yahoo.elide.core.security.checks.prefab.Role; import com.yahoo.elide.datastores.aggregation.AggregationDataStore; import com.yahoo.elide.datastores.aggregation.checks.OperatorCheck; +import com.yahoo.elide.datastores.aggregation.checks.VideoGameFilterCheck; import com.yahoo.elide.datastores.aggregation.example.PlayerStats; import com.yahoo.elide.datastores.aggregation.framework.AggregationDataStoreTestHarness; import com.yahoo.elide.datastores.aggregation.framework.SQLUnitTest; @@ -106,6 +107,7 @@ public SecurityHjsonIntegrationTestResourceConfig() { protected void configure() { Map> map = new HashMap<>(TestCheckMappings.MAPPINGS); map.put(OperatorCheck.OPERTOR_CHECK, OperatorCheck.class); + map.put(VideoGameFilterCheck.NAME_FILTER, VideoGameFilterCheck.class); EntityDictionary dictionary = new EntityDictionary(map); VALIDATOR.getElideSecurityConfig().getRoles().forEach(role -> @@ -293,16 +295,47 @@ public void metricFormulaTest() throws Exception { field("playerName", "Jon Doe") ), selections( - field("timeSpent", 200), - field("sessions", 10), - field("timeSpentPerSession", 20.0), + field("timeSpent", 350), + field("sessions", 25), + field("timeSpentPerSession", 14.0), field("playerName", "Jane Doe") + ), + selections( + field("timeSpent", 300), + field("sessions", 10), + field("timeSpentPerSession", 30.0), + field("playerName", "Han") ) ) ) ).toResponse(); runQueryWithExpectedResult(graphQLRequest, expected); + + //When admin = false + + when(securityContextMock.isUserInRole("admin.user")).thenReturn(false); + + expected = document( + selections( + field( + "videoGame", + selections( + field("timeSpent", 720), + field("sessions", 60), + field("timeSpentPerSession", 12.0), + field("playerName", "Jon Doe") + ), + selections( + field("timeSpent", 350), + field("sessions", 25), + field("timeSpentPerSession", 14.0), + field("playerName", "Jane Doe") + ) + ) + ) + ).toResponse(); + runQueryWithExpectedResult(graphQLRequest, expected); } /** @@ -1635,4 +1668,67 @@ public void testUpsertWithDynamicModel() throws IOException { runQueryWithExpectedError(graphQLRequest, expected); } + + + //Security + @Test + public void testPermissionFilters() throws IOException { + when(securityContextMock.isUserInRole("admin.user")).thenReturn(false); + + String graphQLRequest = document( + selection( + field( + "videoGame", + arguments( + argument("sort", "\"timeSpentPerSession\"") + ), + selections( + field("timeSpent"), + field("sessions"), + field("timeSpentPerSession") + ) + ) + ) + ).toQuery(); + + //Records for Jon Doe and Jane Doe will only be aggregated. + String expected = document( + selections( + field( + "videoGame", + selections( + field("timeSpent", 1070), + field("sessions", 85), + field("timeSpentPerSession", 12.588235) + ) + ) + ) + ).toResponse(); + + runQueryWithExpectedResult(graphQLRequest, expected); + + } + + @Test + public void testFieldPermissions() throws IOException { + when(securityContextMock.isUserInRole("operator")).thenReturn(false); + String graphQLRequest = document( + selection( + field( + "videoGame", + selections( + field("timeSpent"), + field("sessions"), + field("timeSpentPerSession"), + field("timeSpentPerGame") + ) + ) + ) + ).toQuery(); + + String expected = "Exception while fetching data (/videoGame/edges[0]/node/timeSpentPerGame) : ReadPermission Denied"; + + runQueryWithExpectedError(graphQLRequest, expected); + + } } diff --git a/elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/integration/MetaDataStoreIntegrationTest.java b/elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/integration/MetaDataStoreIntegrationTest.java index 049503371a..ca74d43296 100644 --- a/elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/integration/MetaDataStoreIntegrationTest.java +++ b/elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/integration/MetaDataStoreIntegrationTest.java @@ -30,6 +30,7 @@ import com.yahoo.elide.core.security.checks.Check; import com.yahoo.elide.core.security.checks.prefab.Role; import com.yahoo.elide.datastores.aggregation.checks.OperatorCheck; +import com.yahoo.elide.datastores.aggregation.checks.VideoGameFilterCheck; import com.yahoo.elide.datastores.aggregation.framework.AggregationDataStoreTestHarness; import com.yahoo.elide.datastores.aggregation.queryengines.sql.ConnectionDetails; import com.yahoo.elide.datastores.aggregation.queryengines.sql.dialects.SQLDialect; @@ -70,6 +71,7 @@ public SecurityHjsonIntegrationTestResourceConfig() { protected void configure() { Map> map = new HashMap<>(TestCheckMappings.MAPPINGS); map.put(OperatorCheck.OPERTOR_CHECK, OperatorCheck.class); + map.put(VideoGameFilterCheck.NAME_FILTER, VideoGameFilterCheck.class); EntityDictionary dictionary = new EntityDictionary(map); VALIDATOR.getElideSecurityConfig().getRoles().forEach(role -> diff --git a/elide-datastore/elide-datastore-aggregation/src/test/resources/prepare_tables.sql b/elide-datastore/elide-datastore-aggregation/src/test/resources/prepare_tables.sql index add39f747c..60c42a2252 100644 --- a/elide-datastore/elide-datastore-aggregation/src/test/resources/prepare_tables.sql +++ b/elide-datastore/elide-datastore-aggregation/src/test/resources/prepare_tables.sql @@ -63,6 +63,8 @@ INSERT INTO videoGames VALUES (10, 50, 1); INSERT INTO videoGames VALUES (20, 150, 1); INSERT INTO videoGames VALUES (30, 520, 1); INSERT INTO videoGames VALUES (10, 200, 2); +INSERT INTO videoGames VALUES (15, 150, 2); +INSERT INTO videoGames VALUES (10, 300, 3); CREATE TABLE IF NOT EXISTS continents diff --git a/elide-datastore/elide-datastore-multiplex/src/main/java/com/yahoo/elide/datastores/multiplex/MultiplexManager.java b/elide-datastore/elide-datastore-multiplex/src/main/java/com/yahoo/elide/datastores/multiplex/MultiplexManager.java index 6d5dfd655a..f1d4f526f1 100644 --- a/elide-datastore/elide-datastore-multiplex/src/main/java/com/yahoo/elide/datastores/multiplex/MultiplexManager.java +++ b/elide-datastore/elide-datastore-multiplex/src/main/java/com/yahoo/elide/datastores/multiplex/MultiplexManager.java @@ -5,10 +5,12 @@ */ package com.yahoo.elide.datastores.multiplex; +import com.yahoo.elide.core.RequestScope; import com.yahoo.elide.core.datastore.DataStore; import com.yahoo.elide.core.datastore.DataStoreTransaction; import com.yahoo.elide.core.dictionary.EntityBinding; import com.yahoo.elide.core.dictionary.EntityDictionary; +import com.yahoo.elide.core.security.PermissionExecutor; import com.yahoo.elide.core.type.Type; import lombok.AccessLevel; @@ -16,7 +18,9 @@ import java.util.Arrays; import java.util.List; +import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; /** * Allows multiple database handlers to each process their own beans while keeping the main @@ -68,6 +72,12 @@ public void populateEntityDictionary(EntityDictionary dictionary) { // bind to multiplex dictionary dictionary.bindEntity(binding); } + + for (Map.Entry, Function> entry + : subordinateDictionary.getEntityPermissionExecutor().entrySet()) { + dictionary.bindPermissionExecutor(entry.getKey(), entry.getValue()); + + } } }