From 5dd56aa78161a38f226894d330eb427f91c4ed5d Mon Sep 17 00:00:00 2001 From: Sourabh Jain Date: Thu, 14 Mar 2024 07:58:18 +0530 Subject: [PATCH 01/33] first draft --- .../Authorization/AuthorizationResolver.cs | 5 ++ src/Core/Models/GraphQLFilterParsers.cs | 22 ++--- src/Core/Parsers/ODataASTVisitorForCosmos.cs | 77 ++++++++++++++++ .../Resolvers/AuthorizationPolicyHelpers.cs | 88 +++++++++++++++++++ src/Core/Resolvers/BaseQueryStructure.cs | 7 ++ src/Core/Resolvers/CosmosQueryBuilder.cs | 23 ++++- src/Core/Resolvers/CosmosQueryStructure.cs | 20 +++++ .../BaseSqlQueryStructure.cs | 6 -- src/Core/Services/GraphQLSchemaCreator.cs | 1 + .../CosmosSqlMetadataProvider.cs | 76 ++++++++++++++++ .../Directives/ContainerDirective.cs | 30 +++++++ .../CosmosTests/CosmosTestHelper.cs | 3 +- .../CosmosTests/QueryFilterTests.cs | 30 +++++++ src/Service.Tests/CosmosTests/TestBase.cs | 23 ++--- .../dab-config.CosmosDb_NoSql.json | 51 ++++++++++- .../Azure.DataApiBuilder.Service.csproj | 5 -- 16 files changed, 430 insertions(+), 37 deletions(-) create mode 100644 src/Core/Parsers/ODataASTVisitorForCosmos.cs create mode 100644 src/Service.GraphQLBuilder/Directives/ContainerDirective.cs diff --git a/src/Core/Authorization/AuthorizationResolver.cs b/src/Core/Authorization/AuthorizationResolver.cs index 64785de703..295a51087c 100644 --- a/src/Core/Authorization/AuthorizationResolver.cs +++ b/src/Core/Authorization/AuthorizationResolver.cs @@ -198,6 +198,11 @@ public string ProcessDBPolicy(string entityName, string roleName, EntityActionOp /// public string GetDBPolicyForRequest(string entityName, string roleName, EntityActionOperation operation) { + if (!EntityPermissionsMap.ContainsKey(entityName)) + { + return string.Empty; + } + if (!EntityPermissionsMap[entityName].RoleToOperationMap.TryGetValue(roleName, out RoleMetadata? roleMetadata)) { return string.Empty; diff --git a/src/Core/Models/GraphQLFilterParsers.cs b/src/Core/Models/GraphQLFilterParsers.cs index 1e07c20481..6c2584febc 100644 --- a/src/Core/Models/GraphQLFilterParsers.cs +++ b/src/Core/Models/GraphQLFilterParsers.cs @@ -30,8 +30,6 @@ public class GQLFilterParser private readonly RuntimeConfigProvider _configProvider; private readonly IMetadataProviderFactory _metadataProviderFactory; - private IncrementingInteger? _tableCounter; - /// /// Constructor for GQLFilterParser /// @@ -41,7 +39,6 @@ public GQLFilterParser(RuntimeConfigProvider runtimeConfigProvider, IMetadataPro { _configProvider = runtimeConfigProvider; _metadataProviderFactory = metadataProviderFactory; - _tableCounter = new IncrementingInteger(); } /// @@ -312,7 +309,7 @@ private void HandleNestedFilterForCosmos( if (string.IsNullOrEmpty(tableAlias)) { - tableAlias = $"table{_tableCounter?.Next()}"; + tableAlias = queryStructure.GetTableAlias(); } // Validate that the field referenced in the nested input filter can be accessed. @@ -344,13 +341,13 @@ private void HandleNestedFilterForCosmos( comosQueryStructure.SourceAlias = tableAlias; comosQueryStructure.EntityName = entityName; - PredicateOperand joinpredicate = new( + PredicateOperand joinPredicate = new( Parse( ctx: ctx, filterArgumentSchema: filterField, fields: subfields, queryStructure: comosQueryStructure)); - predicates.Push(joinpredicate); + predicates.Push(joinPredicate); queryStructure.Joins ??= new Stack(); if (comosQueryStructure.Joins is not null and { Count: > 0 }) @@ -358,11 +355,14 @@ private void HandleNestedFilterForCosmos( queryStructure.Joins = comosQueryStructure.Joins; } - queryStructure - .Joins - .Push(new CosmosJoinStructure( - DbObject: new DatabaseTable(schemaName: queryStructure.SourceAlias, tableName: columnName), - TableAlias: tableAlias)); + if (!queryStructure.Joins.ToList().Any(j => j.DbObject.Name == columnName)) + { + queryStructure + .Joins + .Push(new CosmosJoinStructure( + DbObject: new DatabaseTable(schemaName: queryStructure.SourceAlias, tableName: columnName), + TableAlias: tableAlias)); + } // Add all parameters from the exists subquery to the main queryStructure. foreach ((string key, DbConnectionParam value) in comosQueryStructure.Parameters) diff --git a/src/Core/Parsers/ODataASTVisitorForCosmos.cs b/src/Core/Parsers/ODataASTVisitorForCosmos.cs new file mode 100644 index 0000000000..df510e447d --- /dev/null +++ b/src/Core/Parsers/ODataASTVisitorForCosmos.cs @@ -0,0 +1,77 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.OData.UriParser; + +namespace Azure.DataApiBuilder.Core.Parsers +{ + internal class ODataASTVisitorForCosmos : QueryNodeVisitor + { + private string _prefix; + + public ODataASTVisitorForCosmos(string prefix) + { + this._prefix = prefix; + } + + public override string Visit(SingleValuePropertyAccessNode nodeIn) + { + return ($"{_prefix}.{nodeIn.Property.Name}"); + } + + public override string Visit(BinaryOperatorNode nodeIn) + { + string left = nodeIn.Left.Accept(this); + string right = nodeIn.Right.Accept(this); + + return $"({left} {GetFilterPredicateOperator(nodeIn.OperatorKind)} {right})"; + } + + public override string Visit(ConvertNode nodeIn) + { + return nodeIn.Source.Accept(this); + } + + public override string Visit(ConstantNode nodeIn) + { + if (nodeIn.TypeReference is null) + { + // Represents a NULL value, we support NULL in queries so return "NULL" here + return "NULL"; + } + + return $"'{nodeIn.Value}'"; + } + + /// + /// Return the correct string for the binary operator that will be a part of the filter predicates + /// that will make up the filter of the query. + /// + /// The op we will translate. + /// The string which is a translation of the op. + private static string GetFilterPredicateOperator(BinaryOperatorKind op) + { + switch (op) + { + case BinaryOperatorKind.Equal: + return "="; + case BinaryOperatorKind.GreaterThan: + return ">"; + case BinaryOperatorKind.GreaterThanOrEqual: + return ">="; + case BinaryOperatorKind.LessThan: + return "<"; + case BinaryOperatorKind.LessThanOrEqual: + return "<="; + case BinaryOperatorKind.NotEqual: + return "!="; + case BinaryOperatorKind.And: + return "AND"; + case BinaryOperatorKind.Or: + return "OR"; + default: + throw new ArgumentException($"Uknown Predicate Operation of {op}"); + } + } + } +} diff --git a/src/Core/Resolvers/AuthorizationPolicyHelpers.cs b/src/Core/Resolvers/AuthorizationPolicyHelpers.cs index f73a02d4b4..274c655387 100644 --- a/src/Core/Resolvers/AuthorizationPolicyHelpers.cs +++ b/src/Core/Resolvers/AuthorizationPolicyHelpers.cs @@ -2,14 +2,19 @@ // Licensed under the MIT License. using Azure.DataApiBuilder.Auth; +using Azure.DataApiBuilder.Config.DatabasePrimitives; using Azure.DataApiBuilder.Config.ObjectModel; using Azure.DataApiBuilder.Core.Authorization; using Azure.DataApiBuilder.Core.Parsers; using Azure.DataApiBuilder.Core.Services; +using Azure.DataApiBuilder.Core.Services.MetadataProviders; using Azure.DataApiBuilder.Service.Exceptions; +using HotChocolate.Language; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Primitives; +using Microsoft.OData.Edm; using Microsoft.OData.UriParser; +using static Azure.DataApiBuilder.Core.Resolvers.CosmosQueryStructure; namespace Azure.DataApiBuilder.Core.Resolvers { @@ -65,6 +70,89 @@ public static void ProcessAuthorizationPolicies( } } + public static void ProcessAuthorizationPoliciesForCosmos( + EntityActionOperation operationType, + CosmosQueryStructure queryStructure, + HttpContext context, + IAuthorizationResolver authorizationResolver, + CosmosSqlMetadataProvider cosmosMetadataProvider) + { + if(cosmosMetadataProvider is null) + { + return; + } + + CosmosSqlMetadataProvider metadataProvider = (CosmosSqlMetadataProvider)cosmosMetadataProvider; + if (!context.Request.Headers.TryGetValue(AuthorizationResolver.CLIENT_ROLE_HEADER, out StringValues roleHeaderValue)) + { + throw new DataApiBuilderException( + message: "No ClientRoleHeader found in request context.", + statusCode: System.Net.HttpStatusCode.Forbidden, + subStatusCode: DataApiBuilderException.SubStatusCodes.AuthorizationCheckFailed); + } + + IEnumerable? entitySets = metadataProvider.EdmModel?.EntityContainer?.EntitySets(); + if (entitySets == null) + { + return; + } + + Dictionary filterClauses = new(); + foreach (IEdmEntitySet entitySet in entitySets) + { + string dbQueryPolicy = authorizationResolver.ProcessDBPolicy( + entitySet.Name, + roleHeaderValue.ToString(), + operationType, + context); + + if(string.IsNullOrEmpty(dbQueryPolicy)) + { + continue; + } + + Uri relativeUri = new(entitySet.Name + "/?$filter=" + dbQueryPolicy, UriKind.Relative); + ODataUriParser parser = new(metadataProvider.EdmModel, relativeUri); + filterClauses.Add(entitySet.Name, parser.ParseFilter()); + } + + Dictionary> pathConfigMap = cosmosMetadataProvider.EntityPaths; + + string? filterString = null; + foreach ((string key, FilterClause filterClause) in filterClauses) + { + foreach (dynamic pathConfig in pathConfigMap[key]) + { + string configPath = pathConfig.path; + if (pathConfig.type is ListTypeNode listTypeNode) + { + string tableAlias = queryStructure.GetTableAlias(); + queryStructure.Joins ??= new Stack(); + queryStructure.Joins.Push(new CosmosJoinStructure( + DbObject: new DatabaseTable(schemaName: pathConfig.path, tableName: pathConfig.entityName), + TableAlias: tableAlias)); + + configPath = tableAlias; + } + else + { + configPath+= "." + pathConfig.entityName; + } + + if (filterString is null) + { + filterString = filterClause.Expression.Accept(new ODataASTVisitorForCosmos(configPath)); + } + else + { + filterString += " AND " + filterClause.Expression.Accept(new ODataASTVisitorForCosmos(configPath)); + } + } + } + + queryStructure.DbPolicyPredicatesForOperations[operationType] = filterString; + } + /// /// Given a dbPolicyClause string, appends the string formatting needed to be processed by ODataParser /// diff --git a/src/Core/Resolvers/BaseQueryStructure.cs b/src/Core/Resolvers/BaseQueryStructure.cs index e8cb912770..a97e3eea9b 100644 --- a/src/Core/Resolvers/BaseQueryStructure.cs +++ b/src/Core/Resolvers/BaseQueryStructure.cs @@ -3,6 +3,7 @@ using Azure.DataApiBuilder.Auth; using Azure.DataApiBuilder.Config.DatabasePrimitives; +using Azure.DataApiBuilder.Config.ObjectModel; using Azure.DataApiBuilder.Core.Models; using Azure.DataApiBuilder.Core.Services; using Azure.DataApiBuilder.Service.GraphQLBuilder; @@ -67,6 +68,12 @@ public class BaseQueryStructure /// public IAuthorizationResolver AuthorizationResolver { get; } + /// + /// DbPolicyPredicates is a string that represents the filter portion of our query + /// in the WHERE Clause added by virtue of the database policy. + /// + public Dictionary DbPolicyPredicatesForOperations { get; set; } = new(); + public const string PARAM_NAME_PREFIX = "@"; public BaseQueryStructure( diff --git a/src/Core/Resolvers/CosmosQueryBuilder.cs b/src/Core/Resolvers/CosmosQueryBuilder.cs index 158b470b48..dc485d20bc 100644 --- a/src/Core/Resolvers/CosmosQueryBuilder.cs +++ b/src/Core/Resolvers/CosmosQueryBuilder.cs @@ -3,6 +3,7 @@ using System.Text; using Azure.DataApiBuilder.Config.DatabasePrimitives; +using Azure.DataApiBuilder.Config.ObjectModel; using Azure.DataApiBuilder.Core.Models; using static Azure.DataApiBuilder.Core.Resolvers.CosmosQueryStructure; @@ -22,16 +23,34 @@ public string Build(CosmosQueryStructure structure) StringBuilder queryStringBuilder = new(); queryStringBuilder.Append($"SELECT {WrappedColumns(structure)}" + $" FROM {_containerAlias}"); - string predicateString = Build(structure.Predicates); if (structure.Joins != null && structure.Joins.Count > 0) { queryStringBuilder.Append($" {Build(structure.Joins)}"); } + string predicateString = Build(structure.Predicates); + + structure.DbPolicyPredicatesForOperations.TryGetValue(EntityActionOperation.Read, out string? policy); + + if (!string.IsNullOrEmpty(predicateString) || !string.IsNullOrEmpty(policy)) + { + queryStringBuilder.Append(" WHERE "); + } + if (!string.IsNullOrEmpty(predicateString)) + { + queryStringBuilder.Append($" {predicateString}"); + } + + if (!string.IsNullOrEmpty(predicateString) && !string.IsNullOrEmpty(policy)) + { + queryStringBuilder.Append(" AND "); + } + + if (!string.IsNullOrEmpty(policy)) { - queryStringBuilder.Append($" WHERE {predicateString}"); + queryStringBuilder.Append($"{policy}"); } if (structure.OrderByColumns.Count > 0) diff --git a/src/Core/Resolvers/CosmosQueryStructure.cs b/src/Core/Resolvers/CosmosQueryStructure.cs index 881d2b50eb..a49158c7b8 100644 --- a/src/Core/Resolvers/CosmosQueryStructure.cs +++ b/src/Core/Resolvers/CosmosQueryStructure.cs @@ -4,13 +4,16 @@ using System.Diagnostics.CodeAnalysis; using Azure.DataApiBuilder.Auth; using Azure.DataApiBuilder.Config.DatabasePrimitives; +using Azure.DataApiBuilder.Config.ObjectModel; using Azure.DataApiBuilder.Core.Models; using Azure.DataApiBuilder.Core.Services; +using Azure.DataApiBuilder.Core.Services.MetadataProviders; using Azure.DataApiBuilder.Service.GraphQLBuilder; using Azure.DataApiBuilder.Service.GraphQLBuilder.GraphQLTypes; using Azure.DataApiBuilder.Service.GraphQLBuilder.Queries; using HotChocolate.Language; using HotChocolate.Resolvers; +using Microsoft.AspNetCore.Http; namespace Azure.DataApiBuilder.Core.Resolvers { @@ -18,6 +21,7 @@ public class CosmosQueryStructure : BaseQueryStructure { private readonly IMiddlewareContext _context; private readonly string _containerAlias = "c"; + private IncrementingInteger _tableCounter = new(); public override string SourceAlias { get => base.SourceAlias; set => base.SourceAlias = value; } @@ -40,6 +44,11 @@ public class CosmosQueryStructure : BaseQueryStructure /// The alias of the table that is joined with. public record CosmosJoinStructure(DatabaseObject DbObject, string TableAlias); + public string GetTableAlias() + { + return $"table{_tableCounter.Next()}"; + } + public CosmosQueryStructure( IMiddlewareContext context, IDictionary parameters, @@ -138,6 +147,17 @@ private void Init(IDictionary queryParams) Container = MetadataProvider.GetDatabaseObjectName(entityName); } + HttpContext httpContext = GraphQLFilterParser.GetHttpContextFromMiddlewareContext(_context); + if (httpContext is not null) + { + AuthorizationPolicyHelpers.ProcessAuthorizationPoliciesForCosmos( + EntityActionOperation.Read, + this, + httpContext, + AuthorizationResolver, + (CosmosSqlMetadataProvider)MetadataProvider); + } + // first and after will not be part of query parameters. They will be going into headers instead. // TODO: Revisit 'first' while adding support for TOP queries if (queryParams.ContainsKey(QueryBuilder.PAGE_START_ARGUMENT_NAME)) diff --git a/src/Core/Resolvers/Sql Query Structures/BaseSqlQueryStructure.cs b/src/Core/Resolvers/Sql Query Structures/BaseSqlQueryStructure.cs index b90bb01958..16e13fd3bc 100644 --- a/src/Core/Resolvers/Sql Query Structures/BaseSqlQueryStructure.cs +++ b/src/Core/Resolvers/Sql Query Structures/BaseSqlQueryStructure.cs @@ -42,12 +42,6 @@ public abstract class BaseSqlQueryStructure : BaseQueryStructure /// public string? FilterPredicates { get; set; } - /// - /// DbPolicyPredicates is a string that represents the filter portion of our query - /// in the WHERE Clause added by virtue of the database policy. - /// - public Dictionary DbPolicyPredicatesForOperations { get; set; } = new(); - /// /// Collection of all the fields referenced in the database policy for create action. /// The fields referenced in the database policy should be a subset of the fields that are being inserted via the insert statement, diff --git a/src/Core/Services/GraphQLSchemaCreator.cs b/src/Core/Services/GraphQLSchemaCreator.cs index 76ba3218c8..4bd9193de5 100644 --- a/src/Core/Services/GraphQLSchemaCreator.cs +++ b/src/Core/Services/GraphQLSchemaCreator.cs @@ -90,6 +90,7 @@ private ISchemaBuilder Parse( .AddDocument(root) .AddAuthorizeDirectiveType() // Add our custom directives + .AddDirectiveType() .AddDirectiveType() .AddDirectiveType() .AddDirectiveType() diff --git a/src/Core/Services/MetadataProviders/CosmosSqlMetadataProvider.cs b/src/Core/Services/MetadataProviders/CosmosSqlMetadataProvider.cs index 8503dfd62d..e00805b314 100644 --- a/src/Core/Services/MetadataProviders/CosmosSqlMetadataProvider.cs +++ b/src/Core/Services/MetadataProviders/CosmosSqlMetadataProvider.cs @@ -11,6 +11,7 @@ using Azure.DataApiBuilder.Service.Exceptions; using Azure.DataApiBuilder.Service.GraphQLBuilder; using HotChocolate.Language; +using Microsoft.OData.Edm; namespace Azure.DataApiBuilder.Core.Services.MetadataProviders { @@ -22,6 +23,9 @@ public class CosmosSqlMetadataProvider : ISqlMetadataProvider private readonly RuntimeConfig _runtimeConfig; private Dictionary _partitionKeyPaths = new(); + public EdmModel EdmModel { get; set; } = new(); + public Dictionary> EntityPaths { get; set; } = new(); + /// public Dictionary GraphQLStoredProcedureExposedNameToEntityNameMap { get; set; } = new(); @@ -65,6 +69,78 @@ public CosmosSqlMetadataProvider(RuntimeConfigProvider runtimeConfigProvider, IF } ParseSchemaGraphQLFieldsForGraphQLType(); + GenerateEdmModel(); + ExtractPathsFromSchema(); + } + + private void GenerateEdmModel() + { + String NAMESPACE = "DEFAULT_NAMESPACE"; + + EdmEntityContainer container = new(NAMESPACE, "DEFAULT_CONTAINER_NAME"); + + foreach (ObjectTypeDefinitionNode typeDefinition in GraphQLSchemaRoot.Definitions) + { + EdmEntityType edmEntity = new(NAMESPACE, typeDefinition.Name.Value); + foreach (FieldDefinitionNode field in typeDefinition.Fields) + { + edmEntity.AddStructuralProperty(field.Name.Value, EdmPrimitiveTypeKind.String, false); + } + + container.AddEntitySet(name: typeDefinition.Name.Value, edmEntity); + } + + EdmModel.AddElement(container); + } + + private void ExtractPathsFromSchema() + { + Dictionary schemaDefinitions = new(); + foreach (ObjectTypeDefinitionNode typeDefinition in GraphQLSchemaRoot.Definitions) + { + schemaDefinitions.Add(typeDefinition.Name.Value, typeDefinition); + } + + foreach (IDefinitionNode typeDefinition in GraphQLSchemaRoot.Definitions) + { + if (typeDefinition is ObjectTypeDefinitionNode objectType) + { + if (objectType.Directives.Any(a => a.Name.Value == "container")) + { + string currentPath = "c"; + + EntityPaths.Add(objectType.Name.Value, new List { new { path = currentPath, type = "" } }); + + ProcessSchema(objectType.Fields, schemaDefinitions, currentPath, _runtimeConfig.Entities); + } + } + } + } + + private void ProcessSchema(IReadOnlyList fields, + Dictionary schemaDocument, + string currentPath, + RuntimeEntities entities) + { + // Traverse the fields and add them to the path + foreach (FieldDefinitionNode field in fields) + { + string entityType = field.Type.NamedType().Name.Value; + + if (entities.Entities.ContainsKey(entityType)) + { + if (EntityPaths.ContainsKey(entityType)) + { + EntityPaths[entityType].Add(new { path = currentPath, entityName = field.Name.Value, type = field.Type }); + } + else + { + EntityPaths.Add(entityType, new List { new { path = currentPath, entityName = field.Name.Value, type = field.Type } }); + } + + ProcessSchema(schemaDocument[entityType].Fields, schemaDocument, $"{currentPath}.{field.Name.Value}", entities); + } + } } /// diff --git a/src/Service.GraphQLBuilder/Directives/ContainerDirective.cs b/src/Service.GraphQLBuilder/Directives/ContainerDirective.cs new file mode 100644 index 0000000000..f39387e6f9 --- /dev/null +++ b/src/Service.GraphQLBuilder/Directives/ContainerDirective.cs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using HotChocolate.Language; +using HotChocolate.Types; +using DirectiveLocation = HotChocolate.Types.DirectiveLocation; + +namespace Azure.DataApiBuilder.Service.GraphQLBuilder.Directives +{ + public class ContainerDirectiveType : DirectiveType + { + public static string DirectiveName { get; } = "container"; + + protected override void Configure(IDirectiveTypeDescriptor descriptor) + { + descriptor.Name(DirectiveName) + .Description("A directive to indicate the container level entity") + .Location(DirectiveLocation.Object); + } + + /// + /// Retrieves the relationship directive defined on the given field definition node. + /// + public static DirectiveNode? GetDirective(FieldDefinitionNode field) + { + DirectiveNode? directive = field.Directives.FirstOrDefault(d => d.Name.Value == DirectiveName); + return directive; + } + } +} diff --git a/src/Service.Tests/CosmosTests/CosmosTestHelper.cs b/src/Service.Tests/CosmosTests/CosmosTestHelper.cs index 94e232c72c..2f8a06583d 100644 --- a/src/Service.Tests/CosmosTests/CosmosTestHelper.cs +++ b/src/Service.Tests/CosmosTests/CosmosTestHelper.cs @@ -169,7 +169,8 @@ public static object GetItem(string id, string name = null, int numericVal = 4) earth = new { id = id, - name = "blue earth" + name = "blue earth" + numericVal, + type = "earth" + numericVal }, additionalAttributes = new[] { diff --git a/src/Service.Tests/CosmosTests/QueryFilterTests.cs b/src/Service.Tests/CosmosTests/QueryFilterTests.cs index 4f75b5b0b5..905cbb05a8 100644 --- a/src/Service.Tests/CosmosTests/QueryFilterTests.cs +++ b/src/Service.Tests/CosmosTests/QueryFilterTests.cs @@ -881,6 +881,36 @@ public async Task TestFilterWithEntityNameAlias() await ExecuteAndValidateResult(_graphQLQueryName, gqlQuery, dbQuery); } + [TestMethod] + public async Task TestQueryFilterFieldAuth_Only_AuthorizedArrayItem() + { + string gqlQuery = @"{ + planets(first: 1, " + QueryBuilder.FILTER_FIELD_NAME + @" : { character: {type: {eq: ""Mars""}}}) + { + items { + id + } + } + }"; + + // MoonAdditionalAttributes (array inside moon object which is an array in container): "@item.name eq 'moonattr0'" + // Earth(object in object): "@item.type eq 'earth0'" + // AdditionalAttribute (array in container): "@item.type eq 'volcano0'" + + // Now get the item with item level permission + string clientRoleHeader = "item-level-permission-role"; + JsonElement actual = await ExecuteGraphQLRequestAsync( + queryName: _graphQLQueryName, + query: gqlQuery, + authToken: AuthTestHelper.CreateStaticWebAppsEasyAuthToken(specificRole: clientRoleHeader), + clientRoleHeader: clientRoleHeader); + + string dbQuery = $"SELECT top 1 c.id FROM c where c.earth.id = \"{_idList[0]}\""; + JsonDocument expected = await ExecuteCosmosRequestAsync(dbQuery, _pageSize, null, _containerName); + // Validate the result contains the GraphQL authorization error code. + ValidateResults(actual.GetProperty("items"), expected.RootElement, false); + } + #region Field Level Auth /// /// Tests that the field level query filter succeeds requests when filter fields are authorized diff --git a/src/Service.Tests/CosmosTests/TestBase.cs b/src/Service.Tests/CosmosTests/TestBase.cs index c4d4a1031b..5c24ac0eb6 100644 --- a/src/Service.Tests/CosmosTests/TestBase.cs +++ b/src/Service.Tests/CosmosTests/TestBase.cs @@ -33,7 +33,7 @@ public class TestBase // Intentionally removed name attibute from Planet model to test scenario where the 'name' attribute // is not explicitly added in the schema internal const string GRAPHQL_SCHEMA = @" -type Character @model(name:""Character"") { +type Character { id : ID, name : String, type: String, @@ -42,7 +42,7 @@ type Character @model(name:""Character"") { star: Star } -type Planet @model { +type Planet @model @container { id : ID!, name : String, character: Character, @@ -56,47 +56,48 @@ type Planet @model { suns: [Sun] } -type Star @model(name:""StarAlias"") { +type Star { id : ID, name : String, tag: Tag } -type Tag @model(name:""TagAlias"") { +type Tag { id : ID, name : String } -type Moon @model(name:""Moon"") @authorize(policy: ""Crater"") { +type Moon @authorize(policy: ""Crater"") { id : ID, name : String, details : String, moonAdditionalAttributes: [MoonAdditionalAttribute] } -type Earth @model(name:""Earth"") { +type Earth { id : ID, name : String, type: String @authorize(roles: [""authenticated""]) } -type Sun @model(name:""Sun"") { +type Sun { id : ID, name : String } -type AdditionalAttribute @model(name:""AdditionalAttribute"") { +type AdditionalAttribute { id : ID, - name : String + name : String, + type: String } -type MoonAdditionalAttribute @model(name:""MoonAdditionalAttribute"") { +type MoonAdditionalAttribute { id : ID, name : String, moreAttributes: [MoreAttribute!] } -type MoreAttribute @model(name:""MoreAttrAlias"") { +type MoreAttribute { id : ID, name : String, type: String @authorize(roles: [""authenticated""]) diff --git a/src/Service.Tests/dab-config.CosmosDb_NoSql.json b/src/Service.Tests/dab-config.CosmosDb_NoSql.json index c9f429929e..91ca1aadad 100644 --- a/src/Service.Tests/dab-config.CosmosDb_NoSql.json +++ b/src/Service.Tests/dab-config.CosmosDb_NoSql.json @@ -96,6 +96,14 @@ "action": "read" } ] + }, + { + "role": "item-level-permission-role", + "actions": [ + { + "action": "read" + } + ] } ] }, @@ -130,6 +138,14 @@ "action": "delete" } ] + }, + { + "role": "item-level-permission-role", + "actions": [ + { + "action": "read" + } + ] } ] }, @@ -421,6 +437,17 @@ "action": "delete" } ] + }, + { + "role": "item-level-permission-role", + "actions": [ + { + "action": "read", + "policy": { + "database": "@item.type eq 'earth0'" + } + } + ] } ] }, @@ -488,6 +515,17 @@ "action": "*" } ] + }, + { + "role": "item-level-permission-role", + "actions": [ + { + "action": "read", + "policy": { + "database": "@item.type eq 'volcano0'" + } + } + ] } ] }, @@ -513,6 +551,17 @@ "action": "*" } ] + }, + { + "role": "item-level-permission-role", + "actions": [ + { + "action": "read", + "policy": { + "database": "@item.name eq 'moonattr0'" + } + } + ] } ] }, @@ -589,4 +638,4 @@ ] } } -} \ No newline at end of file +} diff --git a/src/Service/Azure.DataApiBuilder.Service.csproj b/src/Service/Azure.DataApiBuilder.Service.csproj index d4bf2601db..613af5eb39 100644 --- a/src/Service/Azure.DataApiBuilder.Service.csproj +++ b/src/Service/Azure.DataApiBuilder.Service.csproj @@ -17,10 +17,6 @@ - - - -