diff --git a/config-generators/cosmosdb_nosql-commands.txt b/config-generators/cosmosdb_nosql-commands.txt
index a04795671a..42e7a6330d 100644
--- a/config-generators/cosmosdb_nosql-commands.txt
+++ b/config-generators/cosmosdb_nosql-commands.txt
@@ -1,33 +1,48 @@
init --config "dab-config.CosmosDb_NoSql.json" --database-type "cosmosdb_nosql" --connection-string "AccountEndpoint=https://localhost:8081/;AccountKey=C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==" --cosmosdb_nosql-database "graphqldb" --cosmosdb_nosql-container "planet" --graphql-schema "schema.gql" --host-mode Development --cors-origin "http://localhost:5000"
-add Planet --config "dab-config.CosmosDb_NoSql.json" --source "graphqldb.planet" --permissions "anonymous:create,read,update,delete" --graphql "Planet:Planets"
-update Planet --config "dab-config.CosmosDb_NoSql.json" --permissions "anonymous:read" --fields.include "*"
-update Planet --config "dab-config.CosmosDb_NoSql.json" --permissions "authenticated:create,read,update,delete"
-update Planet --config "dab-config.CosmosDb_NoSql.json" --permissions "limited-read-role:read"
+add PlanetAlias --config "dab-config.CosmosDb_NoSql.json" --source "graphqldb.planet" --permissions "anonymous:create,read,update,delete" --graphql "Planet:Planets"
+update PlanetAlias --config "dab-config.CosmosDb_NoSql.json" --permissions "anonymous:read" --fields.include "*"
+update PlanetAlias --config "dab-config.CosmosDb_NoSql.json" --permissions "authenticated:create,read,update,delete"
+update PlanetAlias --config "dab-config.CosmosDb_NoSql.json" --permissions "limited-read-role:read"
+update PlanetAlias --config "dab-config.CosmosDb_NoSql.json" --permissions "item-level-permission-role:read"
add Character --config "dab-config.CosmosDb_NoSql.json" --source "graphqldb.planet" --permissions "anonymous:create,read,update,delete" --graphql "Character:Characters"
-add StarAlias --config "dab-config.CosmosDb_NoSql.json" --source "graphqldb.planet" --permissions "anonymous:create,read,update,delete" --graphql "Star:Stars"
-update StarAlias --config "dab-config.CosmosDb_NoSql.json" --source "graphqldb.planet" --permissions "authenticated:create,read,update,delete"
-add TagAlias --config "dab-config.CosmosDb_NoSql.json" --source "graphqldb.planet" --permissions "anonymous:create,read,update,delete" --graphql "Tag:Tags"
+update Character --config "dab-config.CosmosDb_NoSql.json" --permissions "item-level-permission-role:read"
+add Star --config "dab-config.CosmosDb_NoSql.json" --source "graphqldb.planet" --permissions "anonymous:create,read,update,delete" --graphql "Star:Stars"
+update Star --config "dab-config.CosmosDb_NoSql.json" --source "graphqldb.planet" --permissions "authenticated:create,read,update,delete"
+add Tag --config "dab-config.CosmosDb_NoSql.json" --source "graphqldb.planet" --permissions "anonymous:create,read,update,delete" --graphql "Tag:Tags"
add Moon --config "dab-config.CosmosDb_NoSql.json" --source "graphqldb.planet" --permissions "anonymous:create,read,update,delete" --graphql true
update Moon --config "dab-config.CosmosDb_NoSql.json" --source "graphqldb.planet" --permissions "authenticated:create,read,update,delete"
add Earth --config "dab-config.CosmosDb_NoSql.json" --source "graphqldb.planet" --permissions "field-mutation-with-read-permission:read" --graphql "Earth:Earths"
-update Earth --config "dab-config.CosmosDb_NoSql.json" --permissions "field-mutation-with-read-permission:create" --fields.include "id" --fields.exclude "name"
-update Earth --config "dab-config.CosmosDb_NoSql.json" --permissions "field-mutation-with-read-permission:delete" --fields.include "id,type" --fields.exclude "name"
-update Earth --config "dab-config.CosmosDb_NoSql.json" --permissions "field-mutation-with-read-permission:update" --fields.include "id,type" --fields.exclude "name"
+update Earth --config "dab-config.CosmosDb_NoSql.json" --permissions "anonymous:create,update,delete"
+update Earth --config "dab-config.CosmosDb_NoSql.json" --permissions "anonymous:read" --fields.include "*" --fields.exclude "*"
update Earth --config "dab-config.CosmosDb_NoSql.json" --permissions "authenticated:create,read,update,delete"
update Earth --config "dab-config.CosmosDb_NoSql.json" --permissions "limited-read-role:read" --fields.include "id,type" --fields.exclude "name"
-update Earth --config "dab-config.CosmosDb_NoSql.json" --permissions "wildcard-exclude-fields-role:create" --fields.exclude "*"
-update Earth --config "dab-config.CosmosDb_NoSql.json" --permissions "wildcard-exclude-fields-role:update" --fields.exclude "*"
-update Earth --config "dab-config.CosmosDb_NoSql.json" --permissions "wildcard-exclude-fields-role:delete" --fields.exclude "*"
-update Earth --config "dab-config.CosmosDb_NoSql.json" --permissions "wildcard-exclude-fields-role:read" --fields.exclude "*"
-update Earth --config "dab-config.CosmosDb_NoSql.json" --permissions "only-create-role:create"
-update Earth --config "dab-config.CosmosDb_NoSql.json" --permissions "only-update-role:update"
-update Earth --config "dab-config.CosmosDb_NoSql.json" --permissions "only-delete-role:delete"
+update Earth --config "dab-config.CosmosDb_NoSql.json" --permissions "item-level-permission-role:read" --policy-database "@item.type eq 'earth0'"
add Sun --config "dab-config.CosmosDb_NoSql.json" --source "graphqldb.planet" --permissions "anonymous:create,update,delete" --graphql true
update Sun --config "dab-config.CosmosDb_NoSql.json" --permissions "anonymous:read" --fields.include "*" --fields.exclude "name"
add AdditionalAttribute --config "dab-config.CosmosDb_NoSql.json" --source "graphqldb.planet" --permissions "anonymous:*" --graphql "AdditionalAttribute:AdditionalAttributes"
+update AdditionalAttribute --config "dab-config.CosmosDb_NoSql.json" --permissions "item-level-permission-role:read" --policy-database "@item.name eq 'volcano0'"
add MoonAdditionalAttribute --config "dab-config.CosmosDb_NoSql.json" --source "graphqldb.planet" --permissions "anonymous:*" --graphql "MoonAdditionalAttribute:MoonAdditionalAttributes"
-add MoreAttrAlias --config "dab-config.CosmosDb_NoSql.json" --source "graphqldb.planet" --permissions "anonymous:delete" --graphql "MoreAttribute:MoreAttributes"
-update MoreAttrAlias --config "dab-config.CosmosDb_NoSql.json" --permissions "anonymous:create" --fields.include "id" --fields.exclude "name"
-update MoreAttrAlias --config "dab-config.CosmosDb_NoSql.json" --permissions "anonymous:read" --fields.include "id" --fields.exclude "name"
-update MoreAttrAlias --config "dab-config.CosmosDb_NoSql.json" --permissions "anonymous:update" --fields.exclude "*"
-update MoreAttrAlias --config "dab-config.CosmosDb_NoSql.json" --permissions "authenticated:create,read,update,delete"
+update MoonAdditionalAttribute --config "dab-config.CosmosDb_NoSql.json" --permissions "item-level-permission-role:read" --policy-database "@item.name eq 'moonattr0'"
+add MoreAttribute --config "dab-config.CosmosDb_NoSql.json" --source "graphqldb.planet" --permissions "anonymous:delete" --graphql "MoreAttribute:MoreAttributes"
+update MoreAttribute --config "dab-config.CosmosDb_NoSql.json" --permissions "anonymous:create" --fields.include "id" --fields.exclude "name"
+update MoreAttribute --config "dab-config.CosmosDb_NoSql.json" --permissions "anonymous:read" --fields.include "id" --fields.exclude "name"
+update MoreAttribute --config "dab-config.CosmosDb_NoSql.json" --permissions "anonymous:update" --fields.exclude "*"
+update MoreAttribute --config "dab-config.CosmosDb_NoSql.json" --permissions "authenticated:create,read,update,delete"
+add PlanetAgain --config "dab-config.CosmosDb_NoSql.json" --source "graphqldb.newcontainer" --permissions "field-mutation-with-read-permission:read" --graphql "PlanetAgain:PlanetAgains"
+update PlanetAgain --config "dab-config.CosmosDb_NoSql.json" --permissions "field-mutation-with-read-permission:create" --fields.include "id" --fields.exclude "name"
+update PlanetAgain --config "dab-config.CosmosDb_NoSql.json" --permissions "field-mutation-with-read-permission:delete" --fields.include "id,type" --fields.exclude "name"
+update PlanetAgain --config "dab-config.CosmosDb_NoSql.json" --permissions "field-mutation-with-read-permission:update" --fields.include "id,type" --fields.exclude "name"
+update PlanetAgain --config "dab-config.CosmosDb_NoSql.json" --permissions "authenticated:create,read,update,delete"
+update PlanetAgain --config "dab-config.CosmosDb_NoSql.json" --permissions "limited-read-role:read" --fields.include "id,type" --fields.exclude "name"
+update PlanetAgain --config "dab-config.CosmosDb_NoSql.json" --permissions "wildcard-exclude-fields-role:create" --fields.exclude "*"
+update PlanetAgain --config "dab-config.CosmosDb_NoSql.json" --permissions "wildcard-exclude-fields-role:update" --fields.exclude "*"
+update PlanetAgain --config "dab-config.CosmosDb_NoSql.json" --permissions "wildcard-exclude-fields-role:delete" --fields.exclude "*"
+update PlanetAgain --config "dab-config.CosmosDb_NoSql.json" --permissions "wildcard-exclude-fields-role:read" --fields.exclude "*"
+update PlanetAgain --config "dab-config.CosmosDb_NoSql.json" --permissions "only-create-role:create"
+update PlanetAgain --config "dab-config.CosmosDb_NoSql.json" --permissions "only-update-role:update"
+update PlanetAgain --config "dab-config.CosmosDb_NoSql.json" --permissions "only-delete-role:delete"
+add InvalidAuthModel --config "dab-config.CosmosDb_NoSql.json" --source "graphqldb.invalidAuthModelContainer" --permissions "anonymous:delete" --graphql "InvalidAuthModel:InvalidAuthModels"
+update InvalidAuthModel --config "dab-config.CosmosDb_NoSql.json" --permissions "anonymous:create" --fields.include "id" --fields.exclude "name"
+update InvalidAuthModel --config "dab-config.CosmosDb_NoSql.json" --permissions "anonymous:read" --fields.include "id" --fields.exclude "name"
+update InvalidAuthModel --config "dab-config.CosmosDb_NoSql.json" --permissions "anonymous:update" --fields.exclude "*"
+update InvalidAuthModel --config "dab-config.CosmosDb_NoSql.json" --permissions "authenticated:create,read,update,delete"
diff --git a/src/Config/ObjectModel/EntityDbPolicyCosmosModel.cs b/src/Config/ObjectModel/EntityDbPolicyCosmosModel.cs
new file mode 100644
index 0000000000..42ca0de8dc
--- /dev/null
+++ b/src/Config/ObjectModel/EntityDbPolicyCosmosModel.cs
@@ -0,0 +1,53 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+namespace Azure.DataApiBuilder.Config.ObjectModel
+{
+ ///
+ /// It contains Entity information along with Pre-Generated JOIN statements for all the entities using configured DB policy.
+ /// So that, it can be used for generating CosmosDB SQL queries.
+ ///
+ public record EntityDbPolicyCosmosModel
+ {
+ ///
+ /// Path to the given entity with "." delimiter, it will be used as prefix while generating conditions for CosmosDB SQL queries.
+ /// e.g. c.stars, c.earth.type
+ ///
+ public string Path { get; }
+
+ ///
+ /// Column name representation of the entity
+ ///
+ public string? ColumnName { get; }
+
+ ///
+ /// If entity is of array type then we would have a generated alias of the entity
+ /// e.g table0, table1 etc
+ ///
+ public string? Alias { get; }
+
+ ///
+ /// Entity Name
+ ///
+ public string? EntityName { get; }
+
+ ///
+ /// Pre-generated (If define DB policies are available and entity type is array) JOIN statement for the entity
+ ///
+ public string? JoinStatement { get; set; }
+
+ public EntityDbPolicyCosmosModel(string Path, string? EntityName, string? ColumnName = null, string? Alias = null)
+ {
+ this.Path = Path;
+ this.ColumnName = ColumnName;
+ this.Alias = Alias;
+ this.EntityName = EntityName;
+
+ // Generate JOIN statement only when Alias is there
+ if (!string.IsNullOrEmpty(Alias))
+ {
+ this.JoinStatement = $" {Alias} IN {Path}.{ColumnName}";
+ }
+ }
+ }
+}
diff --git a/src/Core/Models/GraphQLFilterParsers.cs b/src/Core/Models/GraphQLFilterParsers.cs
index 1e07c20481..b0a7df7ee4 100644
--- a/src/Core/Models/GraphQLFilterParsers.cs
+++ b/src/Core/Models/GraphQLFilterParsers.cs
@@ -16,7 +16,6 @@
using HotChocolate.Resolvers;
using Microsoft.AspNetCore.Http;
using static Azure.DataApiBuilder.Core.Authorization.AuthorizationResolver;
-using static Azure.DataApiBuilder.Core.Resolvers.CosmosQueryStructure;
namespace Azure.DataApiBuilder.Core.Models;
@@ -272,12 +271,12 @@ public Predicate Parse(
}
///
- /// For CosmosDB, a nested filter represents a join between
- /// the parent entity being filtered and the related entity representing the
- /// non-scalar filter input. This function:
- /// 1. Recursively parses any more(possibly nested) filters.
- /// 2. Adds join predicates between the related entities.
- /// 3. Adds the subquery to the existing list of predicates.
+ /// For CosmosDB, a nested filter represents an EXISTS clause with a subquery.
+ /// This function:
+ /// 1. Defines the Exists Query structure
+ /// 2. Recursively parses any more(possibly nested) filters on the Exists sub query.
+ /// 3. Adds join predicates between the related entities to the Exists sub query.
+ /// 4. Adds the Exists subquery to the existing list of predicates.
///
/// The middleware context.
/// The nested filter field.
@@ -285,7 +284,9 @@ public Predicate Parse(
/// Current Column Name
/// Current Entity Type
/// The predicates parsed so far.
- /// The query structure of the entity being filtered.
+ /// The query structure of the entity being filtered, it would be modified to contain EXIST predicates
+ /// Cosmos Metadata Provider, to get metadata information for a given entity
+ ///
private void HandleNestedFilterForCosmos(
IMiddlewareContext ctx,
IInputField filterField,
@@ -296,28 +297,9 @@ private void HandleNestedFilterForCosmos(
CosmosQueryStructure queryStructure,
ISqlMetadataProvider metadataProvider)
{
- string entityName = metadataProvider.GetEntityName(entityType);
-
- HashSet? jstruct = queryStructure.Joins?.ToHashSet();
-
- string? tableAlias = null;
- foreach (CosmosJoinStructure join in jstruct ?? new HashSet())
- {
- if (join.DbObject.Name == columnName)
- {
- tableAlias = join.TableAlias;
- break;
- }
- }
-
- if (string.IsNullOrEmpty(tableAlias))
- {
- tableAlias = $"table{_tableCounter?.Next()}";
- }
-
// Validate that the field referenced in the nested input filter can be accessed.
bool entityAccessPermitted = queryStructure.AuthorizationResolver.AreRoleAndOperationDefinedForEntity(
- entityIdentifier: entityName,
+ entityIdentifier: entityType,
roleName: GetHttpContextFromMiddlewareContext(ctx).Request.Headers[CLIENT_ROLE_HEADER],
operation: EntityActionOperation.Read);
@@ -329,43 +311,39 @@ private void HandleNestedFilterForCosmos(
subStatusCode: DataApiBuilderException.SubStatusCodes.AuthorizationCheckFailed);
}
- IDictionary subParameters = new Dictionary();
- CosmosQueryStructure comosQueryStructure =
- new(
- context: ctx,
- parameters: subParameters,
- metadataProvider: metadataProvider,
- authorizationResolver: queryStructure.AuthorizationResolver,
- gQLFilterParser: this,
- counter: queryStructure.Counter);
-
- comosQueryStructure.DatabaseObject.SchemaName = queryStructure.SourceAlias;
- comosQueryStructure.DatabaseObject.Name = tableAlias;
- comosQueryStructure.SourceAlias = tableAlias;
- comosQueryStructure.EntityName = entityName;
-
- PredicateOperand joinpredicate = new(
- Parse(
- ctx: ctx,
- filterArgumentSchema: filterField,
- fields: subfields,
- queryStructure: comosQueryStructure));
- predicates.Push(joinpredicate);
-
- queryStructure.Joins ??= new Stack();
- if (comosQueryStructure.Joins is not null and { Count: > 0 })
- {
- queryStructure.Joins = comosQueryStructure.Joins;
- }
+ List predicatesForExistsQuery = new();
+ CosmosExistsQueryStructure existsQuery = new(
+ ctx,
+ new Dictionary(),
+ metadataProvider,
+ queryStructure.AuthorizationResolver,
+ this,
+ queryStructure.Counter,
+ predicatesForExistsQuery);
- queryStructure
- .Joins
- .Push(new CosmosJoinStructure(
- DbObject: new DatabaseTable(schemaName: queryStructure.SourceAlias, tableName: columnName),
- TableAlias: tableAlias));
+ existsQuery.DatabaseObject.SchemaName = $"{queryStructure.SourceAlias}.{columnName}";
+ existsQuery.DatabaseObject.Name = existsQuery.SourceAlias;
+ existsQuery.EntityName = metadataProvider.GetEntityName(entityType);
+
+ // Recursively parse and obtain the predicates for the Exists clause subquery
+ Predicate existsQueryFilterPredicate = Parse(ctx,
+ filterField,
+ subfields,
+ existsQuery);
+
+ predicatesForExistsQuery.Push(existsQueryFilterPredicate);
+
+ // The right operand is the SqlExistsQueryStructure.
+ PredicateOperand right = new(existsQuery);
+
+ // Create a new unary Exists Predicate
+ Predicate existsPredicate = new(left: null, PredicateOperation.EXISTS, right);
+
+ // Add it to the rest of the existing predicates.
+ predicates.Push(new PredicateOperand(existsPredicate));
// Add all parameters from the exists subquery to the main queryStructure.
- foreach ((string key, DbConnectionParam value) in comosQueryStructure.Parameters)
+ foreach ((string key, DbConnectionParam value) in existsQuery.Parameters)
{
queryStructure.Parameters.Add(key, value);
}
@@ -541,7 +519,7 @@ private Predicate ParseAndOr(
BaseQueryStructure baseQuery,
PredicateOperation op)
{
- if (fields.Count == 0 && (baseQuery is CosmosQueryStructure cosmosQueryStructure && cosmosQueryStructure.Joins?.Count == 0))
+ if (fields.Count == 0)
{
return Predicate.MakeFalsePredicate();
}
diff --git a/src/Core/Models/SqlQueryStructures.cs b/src/Core/Models/SqlQueryStructures.cs
index a86f810e84..f922cd4345 100644
--- a/src/Core/Models/SqlQueryStructures.cs
+++ b/src/Core/Models/SqlQueryStructures.cs
@@ -165,7 +165,7 @@ public class PredicateOperand
///
private readonly Predicate? _predicateOperand;
- private readonly BaseSqlQueryStructure? _queryStructure;
+ private readonly BaseQueryStructure? _queryStructure;
///
/// Initialize operand as Column
@@ -186,7 +186,7 @@ public PredicateOperand(Column? column)
///
/// Initialize operand as a query structure.
///
- public PredicateOperand(BaseSqlQueryStructure? queryStructure)
+ public PredicateOperand(BaseQueryStructure? queryStructure)
{
if (queryStructure == null)
{
@@ -260,11 +260,29 @@ public PredicateOperand(Predicate? predicate)
/// Resolve operand as a BaseSqlQueryStructure
///
/// null if operand is not intialized as BaseSqlQueryStructure
- public BaseSqlQueryStructure? AsSqlQueryStructure()
+ public BaseQueryStructure? AsBaseQueryStructure()
{
return _queryStructure;
}
+ ///
+ /// Resolve operand as a BaseSqlQueryStructure
+ ///
+ /// null if operand is not intialized as BaseSqlQueryStructure
+ public BaseSqlQueryStructure? AsSqlQueryStructure()
+ {
+ return _queryStructure as BaseSqlQueryStructure;
+ }
+
+ ///
+ /// Resolve operand as a CosmosQueryStructure
+ ///
+ /// null if operand is not intialized as CosmosQueryStructure
+ public CosmosQueryStructure? AsCosmosQueryStructure()
+ {
+ return _queryStructure as CosmosQueryStructure;
+ }
+
///
/// Used to check if the predicate operand is a predicate itself
///
diff --git a/src/Core/Parsers/EdmModelBuilder.cs b/src/Core/Parsers/EdmModelBuilder.cs
index 8180e58db0..63dabb3616 100644
--- a/src/Core/Parsers/EdmModelBuilder.cs
+++ b/src/Core/Parsers/EdmModelBuilder.cs
@@ -4,6 +4,7 @@
using Azure.DataApiBuilder.Config.DatabasePrimitives;
using Azure.DataApiBuilder.Config.ObjectModel;
using Azure.DataApiBuilder.Core.Services;
+using HotChocolate.Language;
using Microsoft.OData.Edm;
namespace Azure.DataApiBuilder.Core.Parsers
@@ -37,6 +38,41 @@ public EdmModelBuilder BuildModel(ISqlMetadataProvider sqlMetadataProvider)
.BuildEntitySets(sqlMetadataProvider);
}
+ ///
+ /// Build the model from the provided schema.
+ ///
+ /// holds the whole schema.
+ /// An EdmModelBuilder that can be used to get a model.
+ public EdmModelBuilder BuildModel(DocumentNode graphQLSchemaRoot)
+ {
+ return BuildEdmModelsForCosmos(graphQLSchemaRoot);
+ }
+
+ private EdmModelBuilder BuildEdmModelsForCosmos(DocumentNode graphQLSchemaRoot)
+ {
+ EdmEntityContainer container = new(DEFAULT_NAMESPACE, DEFAULT_CONTAINER_NAME);
+
+ foreach (ObjectTypeDefinitionNode typeDefinition in graphQLSchemaRoot.Definitions)
+ {
+ EdmEntityType edmEntity = new(DEFAULT_NAMESPACE, typeDefinition.Name.Value);
+ foreach (FieldDefinitionNode field in typeDefinition.Fields)
+ {
+ edmEntity.AddStructuralProperty(
+ name: field.Name.Value,
+ type: TypeHelper.GetEdmPrimitiveTypeFromITypeNode(field.Type),
+ isNullable: !field.Type.IsNonNullType());
+ }
+
+ container.AddEntitySet(
+ name: typeDefinition.Name.Value,
+ elementType: edmEntity);
+ }
+
+ _model.AddElement(container);
+
+ return this;
+ }
+
///
/// Build EdmEntityType objects for runtime config defined entities and add the created objects to the EdmModel.
///
diff --git a/src/Core/Parsers/FilterParser.cs b/src/Core/Parsers/FilterParser.cs
index 498e2c682d..ec765e26a6 100644
--- a/src/Core/Parsers/FilterParser.cs
+++ b/src/Core/Parsers/FilterParser.cs
@@ -4,6 +4,7 @@
using System.Net;
using Azure.DataApiBuilder.Core.Services;
using Azure.DataApiBuilder.Service.Exceptions;
+using HotChocolate.Language;
using Microsoft.OData;
using Microsoft.OData.Edm;
using Microsoft.OData.UriParser;
@@ -26,6 +27,12 @@ public void BuildModel(ISqlMetadataProvider sqlMetadataProvider)
_model = builder.BuildModel(sqlMetadataProvider).GetModel();
}
+ public void BuildModel(DocumentNode graphQLSchemaRoot)
+ {
+ EdmModelBuilder builder = new();
+ _model = builder.BuildModel(graphQLSchemaRoot).GetModel();
+ }
+
///
/// Parses a given filter part of a query string.
///
diff --git a/src/Core/Parsers/ODataASTCosmosVisitor.cs b/src/Core/Parsers/ODataASTCosmosVisitor.cs
new file mode 100644
index 0000000000..1fca34d624
--- /dev/null
+++ b/src/Core/Parsers/ODataASTCosmosVisitor.cs
@@ -0,0 +1,177 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using Microsoft.OData.UriParser;
+
+///
+/// This class is a visitor for an AST generated when parsing a $filter query string
+/// with the OData Uri Parser for Cosmos DB Db Policies
+///
+namespace Azure.DataApiBuilder.Core.Parsers
+{
+ internal class ODataASTCosmosVisitor : QueryNodeVisitor
+ {
+ private string _prefix;
+
+ ///
+ /// Constructor for the visitor to append prefix to the column names which would be the path from container to the column
+ ///
+ ///
+ public ODataASTCosmosVisitor(string prefix)
+ {
+ this._prefix = prefix;
+ }
+
+ ///
+ /// Represents visiting a SingleValuePropertyAccessNode, which is what
+ /// holds an exposed field name in the AST.
+ ///
+ /// The node visited.
+ /// String representing the Field name
+ public override string Visit(SingleValuePropertyAccessNode nodeIn)
+ {
+ return ($"{_prefix}.{nodeIn.Property.Name}");
+ }
+
+ ///
+ /// Represents visiting a BinaryOperatorNode, which will hold either
+ /// a Predicate operation (eq, gt, lt, etc), or a Logical operation (And, Or).
+ ///
+ /// The node visited.
+ /// String concatenation of (left op right).
+ public override string Visit(BinaryOperatorNode nodeIn)
+ {
+ string left = nodeIn.Left.Accept(this);
+ string right = nodeIn.Right.Accept(this);
+
+ if (left.Equals("NULL", StringComparison.OrdinalIgnoreCase) || right.Equals("NULL", StringComparison.OrdinalIgnoreCase))
+ {
+ return CreateNullResult(nodeIn.OperatorKind, left, right);
+ }
+
+ return $"({left} {GetFilterPredicateOperator(nodeIn.OperatorKind)} {right})";
+ }
+
+ ///
+ /// Create the correctly formed response with NULLs.
+ ///
+ /// The binary operation
+ /// The value representing a field.
+ /// The correct format for a NULL given the op and left hand side.
+ private static string CreateNullResult(BinaryOperatorKind op, string left, string right)
+ {
+ switch (op)
+ {
+ case BinaryOperatorKind.Equal:
+ if (!left.Equals("NULL", StringComparison.OrdinalIgnoreCase))
+ {
+ return $"({left} IS NULL)";
+ }
+
+ return $"({right} IS NULL)";
+ case BinaryOperatorKind.NotEqual:
+ if (!left.Equals("NULL", StringComparison.OrdinalIgnoreCase))
+ {
+ return $"({left} IS NOT NULL)";
+ }
+
+ return $"({right} IS NOT NULL)";
+ case BinaryOperatorKind.GreaterThan:
+ case BinaryOperatorKind.GreaterThanOrEqual:
+ case BinaryOperatorKind.LessThan:
+ case BinaryOperatorKind.LessThanOrEqual:
+ return $"({left} {GetFilterPredicateOperator(op)} {right})";
+ default:
+ throw new NotSupportedException($"{op} is not supported with {left} and {right}");
+ }
+ }
+
+ ///
+ /// Represents visiting a UnaryNode, which is what holds unary
+ /// operators such as NOT.
+ ///
+ /// The node visited.
+ /// String concatenation of (op children)
+ public override string Visit(UnaryOperatorNode nodeIn)
+ {
+ string child = nodeIn.Operand.Accept(this);
+ return $"({GetFilterPredicateOperator(nodeIn.OperatorKind)} {child} )";
+ }
+
+ ///
+ /// Represents visiting a ConvertNode, which holds
+ /// some other node as its source.
+ ///
+ /// The node visited.
+ ///
+ public override string Visit(ConvertNode nodeIn)
+ {
+ return nodeIn.Source.Accept(this);
+ }
+
+ ///
+ /// Represents visiting a ConstantNode, which is what
+ /// holds a value in the AST.
+ ///
+ /// The node visited.
+ /// String representing param that holds given value.
+ 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($"Unknown Predicate Operation of {op}");
+ }
+ }
+
+ ///
+ /// Return the correct string for the unary 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(UnaryOperatorKind op)
+ {
+ switch (op)
+ {
+ case UnaryOperatorKind.Not:
+ return "NOT";
+ default:
+ throw new ArgumentException($"Unknown Predicate Operation of {op}");
+ }
+ }
+ }
+}
diff --git a/src/Core/Resolvers/AuthorizationPolicyHelpers.cs b/src/Core/Resolvers/AuthorizationPolicyHelpers.cs
index f73a02d4b4..58f4d1461d 100644
--- a/src/Core/Resolvers/AuthorizationPolicyHelpers.cs
+++ b/src/Core/Resolvers/AuthorizationPolicyHelpers.cs
@@ -2,10 +2,12 @@
// 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 Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Primitives;
@@ -25,14 +27,14 @@ public static class AuthorizationPolicyHelpers
/// Then, the OData clause is processed for the passed in SqlQueryStructure
/// by calling OData visitor helpers.
///
- /// Action to provide the authorizationResolver during policy lookup.
+ /// Action to provide the authorizationResolver during policy lookup.
/// SqlQueryStructure object, could be a subQueryStructure which is of the same type.
/// The GraphQL Middleware context with request metadata like HttpContext.
/// Used to lookup authorization policies.
/// Provides helper method to process ODataFilterClause.
public static void ProcessAuthorizationPolicies(
EntityActionOperation operationType,
- BaseSqlQueryStructure queryStructure,
+ BaseQueryStructure queryStructure,
HttpContext context,
IAuthorizationResolver authorizationResolver,
ISqlMetadataProvider sqlMetadataProvider)
@@ -46,23 +48,134 @@ public static void ProcessAuthorizationPolicies(
}
string clientRoleHeader = roleHeaderValue.ToString();
- List elementalOperations = ResolveCompoundOperationToElementalOperations(operationType);
+ List? elementalOperations = ResolveCompoundOperationToElementalOperations(operationType);
+ Dictionary entitiesToProcess = new();
+ if (queryStructure is BaseSqlQueryStructure baseSqlQueryStructure)
+ {
+ ProcessFilter(
+ context: context,
+ authorizationResolver: authorizationResolver,
+ sqlMetadataProvider: sqlMetadataProvider,
+ clientRoleHeader: clientRoleHeader,
+ elementalOperations: elementalOperations,
+ entityName: queryStructure.EntityName,
+ entityDBObject: queryStructure.DatabaseObject,
+ postProcessCallback: (filterClause, elementalOperation) =>
+ {
+ baseSqlQueryStructure.ProcessOdataClause(filterClause, elementalOperation);
+ });
+ ;
+ }
+ else if (sqlMetadataProvider is CosmosSqlMetadataProvider cosmosSqlMetadataProvider &&
+ queryStructure is CosmosQueryStructure cosmosQueryStructure)
+ {
+ Dictionary> entityPaths = cosmosSqlMetadataProvider.EntityWithJoins;
+
+ foreach (KeyValuePair> entity in entityPaths)
+ {
+ ProcessFilter(
+ context: context,
+ authorizationResolver: authorizationResolver,
+ sqlMetadataProvider: cosmosSqlMetadataProvider,
+ clientRoleHeader: clientRoleHeader,
+ elementalOperations: elementalOperations,
+ entityName: entity.Key,
+ entityDBObject: null,
+ postProcessCallback: (filterClause, _) =>
+ {
+ if (filterClause is null)
+ {
+ return;
+ }
+
+ foreach (EntityDbPolicyCosmosModel pathConfig in entity.Value)
+ {
+ string? existQuery = null;
+ string? fromClause = string.Empty;
+ string? predicates = string.Empty;
+
+ if (pathConfig.Alias is not null)
+ {
+ if (pathConfig.ColumnName is null || pathConfig.EntityName is null)
+ {
+ continue;
+ }
+ //Increment Table counter with the new JOIN so that we can have unique alias for each join
+ cosmosQueryStructure.TableCounter.Next();
+
+ fromClause = pathConfig.JoinStatement;
+ predicates = filterClause?.Expression.Accept(new ODataASTCosmosVisitor(pathConfig.Alias));
+
+ existQuery = CosmosQueryBuilder.BuildExistsQueryForCosmos(fromClause, predicates);
+ }
+ else
+ {
+ predicates = filterClause?.Expression.Accept(new ODataASTCosmosVisitor($"{pathConfig.Path}.{pathConfig.ColumnName}"));
+ }
+
+ if (pathConfig.EntityName == entity.Key)
+ {
+ if (!cosmosQueryStructure.DbPolicyPredicatesForOperations.TryGetValue(operationType, out string? _))
+ {
+ cosmosQueryStructure.DbPolicyPredicatesForOperations[operationType]
+ = existQuery ?? predicates;
+ }
+ else
+ {
+ cosmosQueryStructure.DbPolicyPredicatesForOperations[operationType]
+ += $" AND {existQuery ?? predicates}";
+ }
+ }
+ }
+ });
+ }
+ }
+ }
+
+ ///
+ /// Read the DB policy from the config file and process it to generate OData Filter Clause.
+ /// Here, we are processing the DB policy for each elemental operation and then calling the postProcessCallback.
+ /// PostProcessCallback is a callback function which can be used to generate filter clause according to specific database.
+ ///
+ /// HttpContext, provides information related to http request
+ /// Required to read DB policy from config file
+ /// Metadata Provider
+ /// User Role
+ /// Operation to be made
+ /// Entity Name
+ /// Contains entity information.
+ /// Call back to be called after DB policy information is fetched.
+ /// OData Filter Clause
+ private static List ProcessFilter(
+ HttpContext context,
+ IAuthorizationResolver authorizationResolver,
+ ISqlMetadataProvider sqlMetadataProvider,
+ string clientRoleHeader,
+ List elementalOperations,
+ string entityName,
+ DatabaseObject? entityDBObject,
+ Action postProcessCallback)
+ {
+ List filterClauses = new();
foreach (EntityActionOperation elementalOperation in elementalOperations)
{
string dbQueryPolicy = authorizationResolver.ProcessDBPolicy(
- queryStructure.EntityName,
+ entityName,
clientRoleHeader,
elementalOperation,
context);
FilterClause? filterClause = GetDBPolicyClauseForQueryStructure(
dbQueryPolicy,
- entityName: queryStructure.EntityName,
- resourcePath: queryStructure.DatabaseObject.FullName,
+ entityName: entityName,
+ resourcePath: (entityDBObject is not null) ? $"{entityName}.{entityDBObject.FullName}" : entityName,
sqlMetadataProvider);
- queryStructure.ProcessOdataClause(filterClause, elementalOperation);
+
+ postProcessCallback(filterClause, elementalOperation);
}
+
+ return filterClauses;
}
///
@@ -89,7 +202,7 @@ public static void ProcessAuthorizationPolicies(
// FilterClauseInDbPolicy is an Abstract Syntax Tree representing the parsed policy text.
return sqlMetadataProvider.GetODataParser().GetFilterClause(
filterQueryString: dbPolicyClause,
- resourcePath: $"{entityName}.{resourcePath}",
+ resourcePath: resourcePath,
customResolver: new ClaimsTypeDataUriResolver());
}
diff --git a/src/Core/Resolvers/BaseQueryStructure.cs b/src/Core/Resolvers/BaseQueryStructure.cs
index e8cb912770..cfe4a050ec 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;
@@ -30,7 +31,7 @@ public class BaseQueryStructure
///
/// The DatabaseObject associated with the entity, represents the
- /// databse object to be queried.
+ /// database object to be queried.
///
public DatabaseObject DatabaseObject { get; protected set; } = null!;
@@ -57,7 +58,7 @@ public class BaseQueryStructure
public List Predicates { get; }
///
- /// Used for parsing graphql filter arguments.
+ /// Used for parsing GraphQL filter arguments.
///
public GQLFilterParser GraphQLFilterParser { get; protected set; }
@@ -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/CosmosExistsQueryStructure.cs b/src/Core/Resolvers/CosmosExistsQueryStructure.cs
new file mode 100644
index 0000000000..798a85dbce
--- /dev/null
+++ b/src/Core/Resolvers/CosmosExistsQueryStructure.cs
@@ -0,0 +1,34 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using Azure.DataApiBuilder.Auth;
+using Azure.DataApiBuilder.Core.Models;
+using Azure.DataApiBuilder.Core.Services;
+using HotChocolate.Resolvers;
+
+namespace Azure.DataApiBuilder.Core.Resolvers
+{
+ public class CosmosExistsQueryStructure : CosmosQueryStructure
+ {
+ ///
+ /// Constructor for Exists query.
+ ///
+ public CosmosExistsQueryStructure(IMiddlewareContext context,
+ IDictionary parameters,
+ ISqlMetadataProvider metadataProvider,
+ IAuthorizationResolver authorizationResolver,
+ GQLFilterParser gQLFilterParser,
+ IncrementingInteger? counter = null,
+ List? predicates = null)
+ : base(context,
+ parameters,
+ metadataProvider,
+ authorizationResolver,
+ gQLFilterParser,
+ counter,
+ predicates)
+ {
+ SourceAlias = CreateTableAlias();
+ }
+ }
+}
diff --git a/src/Core/Resolvers/CosmosQueryBuilder.cs b/src/Core/Resolvers/CosmosQueryBuilder.cs
index 158b470b48..e6f835429f 100644
--- a/src/Core/Resolvers/CosmosQueryBuilder.cs
+++ b/src/Core/Resolvers/CosmosQueryBuilder.cs
@@ -2,9 +2,8 @@
// Licensed under the MIT License.
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;
namespace Azure.DataApiBuilder.Core.Resolvers
{
@@ -24,14 +23,15 @@ public string Build(CosmosQueryStructure structure)
+ $" FROM {_containerAlias}");
string predicateString = Build(structure.Predicates);
- if (structure.Joins != null && structure.Joins.Count > 0)
+ structure.DbPolicyPredicatesForOperations.TryGetValue(EntityActionOperation.Read, out string? policy);
+ // If there is a predicate or policy, add a WHERE clause
+ if (!string.IsNullOrEmpty(predicateString) || !string.IsNullOrEmpty(policy))
{
- queryStringBuilder.Append($" {Build(structure.Joins)}");
- }
-
- if (!string.IsNullOrEmpty(predicateString))
- {
- queryStringBuilder.Append($" WHERE {predicateString}");
+ queryStringBuilder
+ .Append(" WHERE ")
+ .Append(string.IsNullOrEmpty(predicateString) || string.IsNullOrEmpty(policy)
+ ? predicateString + policy
+ : string.Join(" AND ", predicateString, policy));
}
if (structure.OrderByColumns.Count > 0)
@@ -61,7 +61,7 @@ protected override string Build(KeysetPaginationPredicate? predicate)
public override string QuoteIdentifier(string ident)
{
- throw new System.NotImplementedException();
+ return ident;
}
///
@@ -108,6 +108,8 @@ protected override string Build(PredicateOperation op)
return "";
case PredicateOperation.IS_NOT:
return "NOT";
+ case PredicateOperation.EXISTS:
+ return "EXISTS";
case PredicateOperation.ARRAY_CONTAINS:
return "ARRAY_CONTAINS";
case PredicateOperation.NOT_ARRAY_CONTAINS:
@@ -129,17 +131,26 @@ protected override string Build(Predicate? predicate)
}
string predicateString;
- if (predicate.Op == PredicateOperation.ARRAY_CONTAINS || predicate.Op == PredicateOperation.NOT_ARRAY_CONTAINS)
+ if (predicate.Left is not null)
{
- predicateString = $" {Build(predicate.Op)} ( {ResolveOperand(predicate.Left)}, {ResolveOperand(predicate.Right)})";
- }
- else if (ResolveOperand(predicate.Right).Equals(GQLFilterParser.NullStringValue))
- {
- predicateString = $" {Build(predicate.Op)} IS_NULL({ResolveOperand(predicate.Left)})";
+ if (predicate.Op == PredicateOperation.ARRAY_CONTAINS || predicate.Op == PredicateOperation.NOT_ARRAY_CONTAINS)
+ {
+ predicateString = $" {Build(predicate.Op)} ( {ResolveOperand(predicate.Left)}, {ResolveOperand(predicate.Right)})";
+ }
+ else if (ResolveOperand(predicate.Right).Equals(GQLFilterParser.NullStringValue))
+ {
+ // For Binary predicates:
+ predicateString = $" {Build(predicate.Op)} IS_NULL({ResolveOperand(predicate.Left)})";
+ }
+ else
+ {
+ predicateString = $"{ResolveOperand(predicate.Left)} {Build(predicate.Op)} {ResolveOperand(predicate.Right)} ";
+ }
}
else
{
- predicateString = $"{ResolveOperand(predicate.Left)} {Build(predicate.Op)} {ResolveOperand(predicate.Right)} ";
+ // For Unary predicates, there is always a parenthesis around the operand.
+ predicateString = $"{Build(predicate.Op)} ({ResolveOperand(predicate.Right)})";
}
if (predicate.AddParenthesis)
@@ -153,29 +164,79 @@ protected override string Build(Predicate? predicate)
}
///
- /// Build JOIN statements which will be used in the query.
- /// It makes sure that the same table is not joined multiple times by maintaining a set of table names.
+ /// Resolves the operand either as a column, another predicate,
+ /// a SqlQueryStructure or returns it directly as string
///
- ///
- ///
- private static string Build(Stack joinstructure)
+ protected new string ResolveOperand(PredicateOperand? operand)
{
- StringBuilder joinBuilder = new();
-
- HashSet tableNames = new();
- foreach (CosmosJoinStructure structure in joinstructure)
+ if (operand == null)
{
- if (tableNames.Contains(structure.DbObject))
- {
- continue;
- }
+ throw new ArgumentNullException(nameof(operand));
+ }
- joinBuilder.Append($" JOIN {structure.TableAlias} IN {structure.DbObject.FullName}");
- tableNames.Add(structure.DbObject);
+ Column? column;
+ string? stringType;
+ Predicate? predicate;
+ BaseQueryStructure? sqlQueryStructure;
+ if ((column = operand.AsColumn()) != null)
+ {
+ return Build(column);
+ }
+ else if ((stringType = operand.AsString()) != null)
+ {
+ return stringType;
+ }
+ else if ((predicate = operand.AsPredicate()) != null)
+ {
+ return Build(predicate);
}
+ else if ((sqlQueryStructure = operand.AsCosmosQueryStructure()) is not null
+ && sqlQueryStructure is CosmosExistsQueryStructure cosmosExistsQueryStructure)
+ {
+ return Build(cosmosExistsQueryStructure);
+ }
+ else if ((sqlQueryStructure = operand.AsCosmosQueryStructure()) is not null
+ && sqlQueryStructure is CosmosQueryStructure cosmosQueryStructure)
+ {
+ return Build(cosmosQueryStructure);
+ }
+ else
+ {
+ throw new ArgumentException("Cannot get a value from PredicateOperand to build.");
+ }
+ }
+
+ ///
+ public virtual string Build(CosmosExistsQueryStructure structure)
+ {
+ string query = $"SELECT 1 " +
+ $"FROM {QuoteIdentifier(structure.SourceAlias)} IN {QuoteIdentifier(structure.DatabaseObject.SchemaName)} " +
+ $"WHERE {Build(structure.Predicates)}";
- return joinBuilder.ToString();
+ return query;
}
+ ///
+ /// Generate Cosmos DB Query for the given fromClause and predicates.
+ ///
+ /// Use to generate FROM part in sql along with table and JOINS
+ /// Query Conditions
+ /// CosmosDB Exist Query
+ public static string BuildExistsQueryForCosmos(string? fromClause, string? predicates)
+ {
+ string? existQuery = $"EXISTS " +
+ $"(SELECT VALUE 1 " +
+ $"FROM {fromClause} ";
+ if (!string.IsNullOrEmpty(predicates))
+ {
+ existQuery += $"WHERE {predicates})";
+ }
+ else
+ {
+ existQuery += ")";
+ }
+
+ return existQuery;
+ }
}
}
diff --git a/src/Core/Resolvers/CosmosQueryStructure.cs b/src/Core/Resolvers/CosmosQueryStructure.cs
index 881d2b50eb..bd84e3fba5 100644
--- a/src/Core/Resolvers/CosmosQueryStructure.cs
+++ b/src/Core/Resolvers/CosmosQueryStructure.cs
@@ -4,20 +4,30 @@
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
{
public class CosmosQueryStructure : BaseQueryStructure
{
private readonly IMiddlewareContext _context;
- private readonly string _containerAlias = "c";
+
+ ///
+ /// For any CosmosDB Query, the default alias for the container is 'c'
+ ///
+ public const string COSMOSDB_CONTAINER_DEFAULT_ALIAS = "c";
+
+ private readonly string _containerAlias = COSMOSDB_CONTAINER_DEFAULT_ALIAS;
+ public IncrementingInteger TableCounter { get; internal set; } = new();
public override string SourceAlias { get => base.SourceAlias; set => base.SourceAlias = value; }
@@ -29,16 +39,11 @@ public class CosmosQueryStructure : BaseQueryStructure
public int? MaxItemCount { get; internal set; }
public string? PartitionKeyValue { get; internal set; }
public List OrderByColumns { get; internal set; }
- // Order of the join matters
- public Stack? Joins { get; internal set; }
- ///
- /// A simple class that is used to hold the information about joins that
- /// are part of a Cosmos query.
- ///
- /// The name of the database object containing table metadata like joined tables.
- /// 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,
@@ -46,8 +51,9 @@ public CosmosQueryStructure(
ISqlMetadataProvider metadataProvider,
IAuthorizationResolver authorizationResolver,
GQLFilterParser gQLFilterParser,
- IncrementingInteger? counter = null)
- : base(metadataProvider, authorizationResolver, gQLFilterParser, entityName: string.Empty, counter: counter)
+ IncrementingInteger? counter = null,
+ List? predicates = null)
+ : base(metadataProvider, authorizationResolver, gQLFilterParser, predicates: predicates, entityName: string.Empty, counter: counter)
{
_context = context;
SourceAlias = _containerAlias;
@@ -138,6 +144,17 @@ private void Init(IDictionary queryParams)
Container = MetadataProvider.GetDatabaseObjectName(entityName);
}
+ HttpContext httpContext = GraphQLFilterParser.GetHttpContextFromMiddlewareContext(_context);
+ if (httpContext is not null)
+ {
+ AuthorizationPolicyHelpers.ProcessAuthorizationPolicies(
+ 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/MetadataProviders/CosmosSqlMetadataProvider.cs b/src/Core/Services/MetadataProviders/CosmosSqlMetadataProvider.cs
index 8503dfd62d..3c7d9fc42e 100644
--- a/src/Core/Services/MetadataProviders/CosmosSqlMetadataProvider.cs
+++ b/src/Core/Services/MetadataProviders/CosmosSqlMetadataProvider.cs
@@ -6,22 +6,37 @@
using Azure.DataApiBuilder.Config.DatabasePrimitives;
using Azure.DataApiBuilder.Config.ObjectModel;
using Azure.DataApiBuilder.Core.Configurations;
+using Azure.DataApiBuilder.Core.Models;
using Azure.DataApiBuilder.Core.Parsers;
using Azure.DataApiBuilder.Core.Resolvers;
using Azure.DataApiBuilder.Service.Exceptions;
using Azure.DataApiBuilder.Service.GraphQLBuilder;
+using Azure.DataApiBuilder.Service.GraphQLBuilder.Directives;
using HotChocolate.Language;
+using Microsoft.OData.Edm;
namespace Azure.DataApiBuilder.Core.Services.MetadataProviders
{
public class CosmosSqlMetadataProvider : ISqlMetadataProvider
{
+ private ODataParser _oDataParser = new();
+
private readonly IFileSystem _fileSystem;
private readonly DatabaseType _databaseType;
private CosmosDbNoSQLDataSourceOptions _cosmosDb;
private readonly RuntimeConfig _runtimeConfig;
private Dictionary _partitionKeyPaths = new();
+ ///
+ /// This contains each entity into EDM model convention which will be used to traverse DB Policy filter using ODataParser
+ ///
+ public EdmModel EdmModel { get; set; } = new();
+
+ ///
+ /// This dictionary contains entity name as key (or its alias) and its path(s) in the graphQL schema as value which will be used in the generated conditions for the entity
+ ///
+ public Dictionary> EntityWithJoins { get; set; } = new();
+
///
public Dictionary GraphQLStoredProcedureExposedNameToEntityNameMap { get; set; } = new();
@@ -65,6 +80,181 @@ public CosmosSqlMetadataProvider(RuntimeConfigProvider runtimeConfigProvider, IF
}
ParseSchemaGraphQLFieldsForGraphQLType();
+ ParseSchemaGraphQLFieldsForJoins();
+
+ InitODataParser();
+ }
+
+ ///
+ /// Initialize OData parser by building OData model.
+ /// The parser will be used for parsing filter clause and order by clause.
+ ///
+ private void InitODataParser()
+ {
+ _oDataParser.BuildModel(GraphQLSchemaRoot);
+ }
+
+ ///
+ /// Parse the schema to get the entity paths for prefixes.
+ /// It will collect all the paths for each entity and its field, starting from the container model.
+ ///
+ /// e.g. If we have the following schema:
+ /// type Planet @model(name:""PlanetAlias"") {
+ /// id : ID!,
+ /// name : String,
+ /// character: Character,
+ /// stars: [Star],
+ /// sun: Star
+ /// }
+ ///
+ /// type Star {
+ /// id : ID,
+ /// name : String
+ /// }
+ ///
+ /// type Character {
+ /// id : ID,
+ /// name : String,
+ /// type: String,
+ /// homePlanet: Int,
+ /// primaryFunction: String,
+ /// star: Star
+ /// }
+ /// It would generate the following EntityWithJoins dictionary:
+ /// KEY: PlanetAlias
+ /// VALUE:
+ /// a) Path = c, EntityName = PlanetAlias
+ ///
+ /// KEY: Star
+ /// VALUE:
+ /// a) Path = c, ColumnName = stars , EntityName = Star, Alias = table0, JoinStatement = table0 IN c.stars
+ /// b) Path = c , ColumnName = sun, EntityName = Star
+ /// c) Path = c.character, ColumnName = star , EntityName = Star
+ ///
+ /// KEY: Character
+ /// VALUE:
+ /// a) Path = c, ColumnName = character , EntityName = Character
+ ///
+ /// EntityWithJoins dictionary indicates the paths for each entity. There "Planet" has one path i.e. "c" on the other hand Star has 3 paths.with one join statement.
+ /// This information is getting used to resolve DB Policy and generate cosmos DB sql query conditions for them.
+ ///
+ private void ParseSchemaGraphQLFieldsForJoins()
+ {
+ IncrementingInteger tableCounter = new();
+
+ Dictionary schemaDefinitions = new();
+
+ // Step1: Collect all the schema definitions in a dictionary for easy lookup of the corresponding fields
+ foreach (ObjectTypeDefinitionNode typeDefinition in GraphQLSchemaRoot.Definitions)
+ {
+ schemaDefinitions.Add(typeDefinition.Name.Value, typeDefinition);
+ }
+
+ // Step2:
+ // a) Traverse the schema to find the container model
+ // b) Once it is found, start collecting all the paths for each entity and its field.
+ foreach (IDefinitionNode typeDefinition in GraphQLSchemaRoot.Definitions)
+ {
+ if (typeDefinition is ObjectTypeDefinitionNode node && node.Directives.Any(a => a.Name.Value == ModelDirectiveType.DirectiveName))
+ {
+ string modelName = GraphQLNaming.ObjectTypeToEntityName(node);
+
+ if (EntityWithJoins.TryGetValue(modelName, out List? entityWithJoins))
+ {
+ entityWithJoins.Add(new(Path: CosmosQueryStructure.COSMOSDB_CONTAINER_DEFAULT_ALIAS, EntityName: modelName));
+ }
+ else
+ {
+ EntityWithJoins.Add(
+ modelName,
+ new List
+ {
+ new (Path: CosmosQueryStructure.COSMOSDB_CONTAINER_DEFAULT_ALIAS, EntityName: modelName)
+ });
+ }
+
+ ProcessSchema(node.Fields, schemaDefinitions, CosmosQueryStructure.COSMOSDB_CONTAINER_DEFAULT_ALIAS, tableCounter);
+ }
+ }
+ }
+
+ ///
+ /// Once container is found, it will traverse the fields and inner fields to get the paths for each entity.
+ /// Following steps are implemented here:
+ /// 1. If the entity is not in the runtime config, skip it.
+ /// 2. If the field is an array type, we need to create a table alias which will be used when creating JOINs to that table.
+ /// 3. Create a new EntityDbPolicyCosmosModel object with all the entity related information and add it to the EntityWithJoins dictionary.
+ /// 4. Check if we get previous entity with join information, if yes append it to the current entity also
+ /// 5. Recursively call this function, to process the schema
+ ///
+ ///
+ ///
+ ///
+ /// indicates the parent entity for which we are processing the schema.
+ private void ProcessSchema(IReadOnlyList fields,
+ Dictionary schemaDocument,
+ string currentPath,
+ IncrementingInteger tableCounter,
+ EntityDbPolicyCosmosModel? previousEntity = null)
+ {
+ // Traverse the fields and add them to the path
+ foreach (FieldDefinitionNode field in fields)
+ {
+ string entityType = field.Type.NamedType().Name.Value;
+ // If the entity is not in the runtime config, skip it
+ if (!_runtimeConfig.Entities.ContainsKey(entityType))
+ {
+ continue;
+ }
+
+ string? alias = null;
+ bool isArrayType = field.Type is ListTypeNode;
+ if (isArrayType)
+ {
+ // Since we don't have query structure here,
+ // we are going to generate alias and use this counter to generate unique alias for each table at later stage.
+ alias = $"table{tableCounter.Next()}";
+ }
+
+ EntityDbPolicyCosmosModel currentEntity = new(
+ Path: currentPath,
+ EntityName: entityType,
+ ColumnName: field.Name.Value,
+ Alias: alias);
+
+ if (EntityWithJoins.ContainsKey(entityType))
+ {
+ EntityWithJoins[entityType].Add(currentEntity);
+ }
+ else
+ {
+ EntityWithJoins.Add(
+ entityType,
+ new List() {
+ currentEntity
+ });
+ }
+
+ if (previousEntity is not null)
+ {
+ if (string.IsNullOrEmpty(currentEntity.JoinStatement))
+ {
+ currentEntity.JoinStatement = previousEntity.JoinStatement;
+ }
+ else
+ {
+ currentEntity.JoinStatement = previousEntity.JoinStatement + " JOIN " + currentEntity.JoinStatement;
+ }
+ }
+
+ // If the field is an array type, we need to create a table alias which will be used when creating JOINs to that table.
+ ProcessSchema(
+ fields: schemaDocument[entityType].Fields,
+ schemaDocument: schemaDocument,
+ currentPath: isArrayType ? $"{alias}" : $"{currentPath}.{field.Name.Value}",
+ tableCounter: tableCounter,
+ previousEntity: isArrayType ? currentEntity : null);
+ }
}
///
@@ -246,7 +436,7 @@ public List GetSchemaGraphQLFieldNamesForEntityName(string entityName)
public ODataParser GetODataParser()
{
- throw new NotImplementedException();
+ return _oDataParser;
}
public IQueryBuilder GetQueryBuilder()
diff --git a/src/Core/Services/TypeHelper.cs b/src/Core/Services/TypeHelper.cs
index b39b4a3d81..dd57cdaa46 100644
--- a/src/Core/Services/TypeHelper.cs
+++ b/src/Core/Services/TypeHelper.cs
@@ -6,6 +6,7 @@
using System.Net;
using Azure.DataApiBuilder.Core.Services.OpenAPI;
using Azure.DataApiBuilder.Service.Exceptions;
+using HotChocolate.Language;
using Microsoft.OData.Edm;
namespace Azure.DataApiBuilder.Core.Services
@@ -158,6 +159,43 @@ public static EdmPrimitiveTypeKind GetEdmPrimitiveTypeFromSystemType(Type column
return type;
}
+ ///
+ /// Given GraphQl type, returns the corresponding primitive type kind.
+ ///
+ /// Type of the column.
+ /// EdmPrimitiveTypeKind
+ /// Throws when the column
+ public static EdmPrimitiveTypeKind GetEdmPrimitiveTypeFromITypeNode(ITypeNode columnSystemType)
+ {
+ string graphQlType;
+ if (columnSystemType.IsListType())
+ {
+ graphQlType = ((ListTypeNode)columnSystemType).NamedType().Name.Value;
+ }
+ else if (columnSystemType.IsNonNullType())
+ {
+ graphQlType = ((NonNullTypeNode)columnSystemType).NamedType().Name.Value;
+ }
+ else
+ {
+ graphQlType = ((NamedTypeNode)columnSystemType).Name.Value;
+ }
+
+ // https://graphql.org/learn/schema/#scalar-types
+ EdmPrimitiveTypeKind type = graphQlType switch
+ {
+ "String" => EdmPrimitiveTypeKind.String,
+ "ID" => EdmPrimitiveTypeKind.Guid,
+ "Int" => EdmPrimitiveTypeKind.Int32,
+ "Float" => EdmPrimitiveTypeKind.Decimal,
+ "Boolean" => EdmPrimitiveTypeKind.Boolean,
+ "Date" => EdmPrimitiveTypeKind.Date,
+ _ => EdmPrimitiveTypeKind.PrimitiveType
+ };
+
+ return type;
+ }
+
///
/// Converts the .NET Framework (System/CLR) type to JsonDataType.
/// Primitive data types in the OpenAPI standard (OAS) are based on the types supported
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/MutationTests.cs b/src/Service.Tests/CosmosTests/MutationTests.cs
index 6e7c72b97c..02513d63ff 100644
--- a/src/Service.Tests/CosmosTests/MutationTests.cs
+++ b/src/Service.Tests/CosmosTests/MutationTests.cs
@@ -261,27 +261,26 @@ public async Task MutationMissingRequiredPartitionKeyValueReturnError()
[TestMethod]
[DataRow("field-mutation-with-read-permission", DataApiBuilderException.GRAPHQL_MUTATION_FIELD_AUTHZ_FAILURE, DisplayName = "AuthZ failure for create mutation because of reference to excluded/disallowed fields.")]
[DataRow("authenticated", MutationTests.NO_ERROR_MESSAGE, DisplayName = "AuthZ success when role has no create/read operation restrictions.")]
- [DataRow("only-create-role", "The mutation operation createEarth was successful " +
+ [DataRow("only-create-role", "The mutation operation createPlanetAgain was successful " +
"but the current user is unauthorized to view the response due to lack of read permissions", DisplayName = "Successful create operation but AuthZ failure for read when role has ONLY create permission and NO read permission.")]
[DataRow("wildcard-exclude-fields-role", DataApiBuilderException.GRAPHQL_MUTATION_FIELD_AUTHZ_FAILURE, DisplayName = "AuthZ failure for create mutation because of reference to excluded/disallowed field using wildcard.")]
[DataRow("only-update-role", MutationTests.USER_NOT_AUTHORIZED, DisplayName = "AuthZ failure when create permission is NOT there.")]
public async Task CreateItemWithAuthPermissions(string roleName, string expectedErrorMessage)
{
- // Run mutation Add Earth;
+ // Run mutation Add AuthTestModel;
string id = Guid.NewGuid().ToString();
const string name = "test_name";
string mutation = $@"
mutation {{
- createEarth (item: {{ id: ""{id}"", name: ""{name}"" }}) {{
+ createPlanetAgain (item: {{ id: ""{id}"", name: ""{name}"" }}) {{
id
name
}}
}}";
- string authtoken = AuthTestHelper.CreateStaticWebAppsEasyAuthToken(specificRole: roleName);
- JsonElement response = await ExecuteGraphQLRequestAsync("createEarth", mutation, variables: new(), authToken: authtoken, clientRoleHeader: roleName);
+ string authToken = AuthTestHelper.CreateStaticWebAppsEasyAuthToken(specificRole: roleName);
+ JsonElement response = await ExecuteGraphQLRequestAsync("createPlanetAgain", mutation, variables: new(), authToken: authToken, clientRoleHeader: roleName);
// Validate the result contains the GraphQL authorization error code.
- Console.WriteLine(response.ToString());
if (string.IsNullOrEmpty(expectedErrorMessage))
{
Assert.AreEqual(id, response.GetProperty("id").GetString());
@@ -301,8 +300,8 @@ public async Task CreateItemWithAuthPermissions(string roleName, string expected
[TestMethod]
[DataRow("field-mutation-with-read-permission", DataApiBuilderException.GRAPHQL_MUTATION_FIELD_AUTHZ_FAILURE, DisplayName = "AuthZ failure for update mutation because of reference to excluded/disallowed fields.")]
[DataRow("authenticated", NO_ERROR_MESSAGE, DisplayName = "AuthZ success when role has no update/read operation restrictions.")]
- [DataRow("only-update-role", "The mutation operation updateEarth was successful " +
- "but the current user is unauthorized to view the response due to lack of read permissions", DisplayName = "AuthZ failure but sucessful operation where role has ONLY update permission and NO read permission.")]
+ [DataRow("only-update-role", "The mutation operation updatePlanetAgain was successful " +
+ "but the current user is unauthorized to view the response due to lack of read permissions", DisplayName = "AuthZ failure but successful operation where role has ONLY update permission and NO read permission.")]
[DataRow("wildcard-exclude-fields-role", DataApiBuilderException.GRAPHQL_MUTATION_FIELD_AUTHZ_FAILURE, DisplayName = "AuthZ failure for update mutation because of reference to excluded/disallowed field using wildcard.")]
[DataRow("only-create-role", MutationTests.USER_NOT_AUTHORIZED, DisplayName = "AuthZ failure when update permission is NOT there.")]
public async Task UpdateItemWithAuthPermissions(string roleName, string expectedErrorMessage)
@@ -312,13 +311,13 @@ public async Task UpdateItemWithAuthPermissions(string roleName, string expected
const string name = "test_name";
string createMutation = $@"
mutation {{
- createEarth (item: {{ id: ""{id}"", name: ""{name}"" }}) {{
+ createPlanetAgain (item: {{ id: ""{id}"", name: ""{name}"" }}) {{
id
name
}}
}}";
- JsonElement createResponse = await ExecuteGraphQLRequestAsync("createEarth", createMutation,
+ JsonElement createResponse = await ExecuteGraphQLRequestAsync("createPlanetAgain", createMutation,
variables: new(),
authToken: AuthTestHelper.CreateStaticWebAppsEasyAuthToken(specificRole: AuthorizationType.Authenticated.ToString()),
clientRoleHeader: AuthorizationType.Authenticated.ToString());
@@ -326,10 +325,10 @@ public async Task UpdateItemWithAuthPermissions(string roleName, string expected
// Making sure item is created successfully
Assert.AreEqual(id, createResponse.GetProperty("id").GetString());
- // Run mutation Update Earth;
+ // Run mutation Update AuthTestModel;
string mutation = @"
-mutation ($id: ID!, $partitionKeyValue: String!, $item: UpdateEarthInput!) {
- updateEarth (id: $id, _partitionKeyValue: $partitionKeyValue, item: $item) {
+mutation ($id: ID!, $partitionKeyValue: String!, $item: UpdatePlanetAgainInput!) {
+ updatePlanetAgain (id: $id, _partitionKeyValue: $partitionKeyValue, item: $item) {
id
name
}
@@ -340,15 +339,14 @@ public async Task UpdateItemWithAuthPermissions(string roleName, string expected
name = "new_name"
};
- string authtoken = AuthTestHelper.CreateStaticWebAppsEasyAuthToken(specificRole: roleName);
+ string authToken = AuthTestHelper.CreateStaticWebAppsEasyAuthToken(specificRole: roleName);
JsonElement response = await ExecuteGraphQLRequestAsync(
- queryName: "updateEarth",
+ queryName: "updatePlanetAgain",
query: mutation,
variables: new() { { "id", id }, { "partitionKeyValue", id }, { "item", update } },
- authToken: authtoken,
+ authToken: authToken,
clientRoleHeader: roleName);
- Console.WriteLine(response.ToString());
if (string.IsNullOrEmpty(expectedErrorMessage))
{
Assert.AreEqual(id, response.GetProperty("id").GetString());
@@ -368,8 +366,8 @@ public async Task UpdateItemWithAuthPermissions(string roleName, string expected
[TestMethod]
[DataRow("field-mutation-with-read-permission", MutationTests.NO_ERROR_MESSAGE, DisplayName = "AuthZ success and blank response for delete mutation because of reference to excluded/disallowed fields.")]
[DataRow("authenticated", MutationTests.NO_ERROR_MESSAGE, DisplayName = "AuthZ success and blank response when role has no delete operation restrictions.")]
- [DataRow("only-delete-role", "The mutation operation deleteEarth was successful " +
- "but the current user is unauthorized to view the response due to lack of read permissions", DisplayName = "AuthZ failure but sucessful operation where role has ONLY delete permission and NO read permission.")]
+ [DataRow("only-delete-role", "The mutation operation deletePlanetAgain was successful " +
+ "but the current user is unauthorized to view the response due to lack of read permissions", DisplayName = "AuthZ failure but successful operation where role has ONLY delete permission and NO read permission.")]
[DataRow("wildcard-exclude-fields-role", MutationTests.NO_ERROR_MESSAGE, DisplayName = "AuthZ success and blank response for delete mutation because of reference to excluded/disallowed fields using wildcard")]
[DataRow("only-create-role", MutationTests.USER_NOT_AUTHORIZED, DisplayName = "AuthZ failure when delete permission is NOT there.")]
public async Task DeleteItemWithAuthPermissions(string roleName, string expectedErrorMessage)
@@ -379,13 +377,13 @@ public async Task DeleteItemWithAuthPermissions(string roleName, string expected
const string name = "test_name";
string createMutation = $@"
mutation {{
- createEarth (item: {{ id: ""{id}"", name: ""{name}"" }}) {{
+ createPlanetAgain (item: {{ id: ""{id}"", name: ""{name}"" }}) {{
id
name
}}
}}";
- JsonElement createResponse = await ExecuteGraphQLRequestAsync("createEarth", createMutation,
+ JsonElement createResponse = await ExecuteGraphQLRequestAsync("createPlanetAgain", createMutation,
variables: new(),
authToken: AuthTestHelper.CreateStaticWebAppsEasyAuthToken(specificRole: AuthorizationType.Authenticated.ToString()),
clientRoleHeader: AuthorizationType.Authenticated.ToString());
@@ -393,24 +391,22 @@ public async Task DeleteItemWithAuthPermissions(string roleName, string expected
// Making sure item is created successfully
Assert.AreEqual(id, createResponse.GetProperty("id").GetString());
- // Run mutation Update Earth;
+ // Run mutation Update AuthTestModel;
string mutation = @"
mutation ($id: ID!, $partitionKeyValue: String!) {
- deleteEarth (id: $id, _partitionKeyValue: $partitionKeyValue) {
+ deletePlanetAgain (id: $id, _partitionKeyValue: $partitionKeyValue) {
id
name
}
}";
- string authtoken = AuthTestHelper.CreateStaticWebAppsEasyAuthToken(specificRole: roleName);
+ string authToken = AuthTestHelper.CreateStaticWebAppsEasyAuthToken(specificRole: roleName);
JsonElement response = await ExecuteGraphQLRequestAsync(
- queryName: "deleteEarth",
+ queryName: "deletePlanetAgain",
query: mutation,
variables: new() { { "id", id }, { "partitionKeyValue", id } },
- authToken: authtoken,
+ authToken: authToken,
clientRoleHeader: roleName);
- Console.WriteLine(response.ToString());
-
if (string.IsNullOrEmpty(expectedErrorMessage))
{
Assert.IsTrue(string.IsNullOrEmpty(response.ToString()));
diff --git a/src/Service.Tests/CosmosTests/QueryFilterTests.cs b/src/Service.Tests/CosmosTests/QueryFilterTests.cs
index 4f75b5b0b5..0dac6a1c1c 100644
--- a/src/Service.Tests/CosmosTests/QueryFilterTests.cs
+++ b/src/Service.Tests/CosmosTests/QueryFilterTests.cs
@@ -99,10 +99,17 @@ public async Task TestStringMultiFiltersWithAndCondition()
}
}";
- string dbQueryWithJoin = "SELECT c.name FROM c " +
- "JOIN a IN c.additionalAttributes " +
- "JOIN b IN c.moons " +
- "WHERE a.name = \"volcano1\" and b.name = \"1 moon\" and b.details LIKE \"%11%\"";
+ string dbQueryWithJoin = "SELECT c.name " +
+ "FROM c " +
+ "WHERE (EXISTS (SELECT 1 " +
+ "FROM table0 IN c.additionalAttributes " +
+ "WHERE table0.name = \"volcano1\" ) AND " +
+ "EXISTS (SELECT 1 " +
+ "FROM table2 IN c.moons " +
+ "WHERE table2.name = \"1 moon\" ) AND " +
+ "EXISTS (SELECT 1 " +
+ "FROM table4 IN c.moons " +
+ "WHERE table4.details LIKE \"%11%\" ) )";
await ExecuteAndValidateResult(_graphQLQueryName, gqlQuery, dbQueryWithJoin);
}
@@ -147,9 +154,11 @@ public async Task TestStringFiltersOnNestedArrayType()
}";
string dbQueryWithJoin = "SELECT c.name FROM c " +
- "JOIN a IN c.moons " +
- "JOIN b IN a.moonAdditionalAttributes " +
- "WHERE b.name = \"moonattr0\"";
+ "WHERE EXISTS (SELECT 1 " +
+ "FROM table0 IN c.moons " +
+ "WHERE EXISTS (SELECT 1 " +
+ "FROM table1 IN table0.moonAdditionalAttributes " +
+ "WHERE table1.name = \"moonattr0\" ))";
await ExecuteAndValidateResult(_graphQLQueryName, gqlQuery, dbQueryWithJoin);
}
@@ -171,10 +180,13 @@ public async Task TestStringFiltersOnTwoLevelNestedArrayType()
}";
string dbQueryWithJoin = "SELECT c.name FROM c " +
- "JOIN a IN c.moons " +
- "JOIN b IN a.moonAdditionalAttributes " +
- "JOIN d IN b.moreAttributes " +
- "WHERE d.name = \"moonattr0\"";
+ "WHERE EXISTS(SELECT 1 " +
+ "FROM table0 IN c.moons " +
+ "WHERE EXISTS(SELECT 1 " +
+ "FROM table1 IN table0.moonAdditionalAttributes " +
+ "WHERE EXISTS(SELECT 1 " +
+ "FROM table2 IN table1.moreAttributes " +
+ "WHERE table2.name = \"moonattr0\")))";
await ExecuteAndValidateResult(_graphQLQueryName, gqlQuery, dbQueryWithJoin);
}
@@ -254,10 +266,14 @@ public async Task TestStringMultiFiltersOnArrayTypeWithOrCondition()
}
}";
- string dbQueryWithJoin = "SELECT c.name FROM c " +
- "JOIN a IN c.additionalAttributes " +
- "JOIN b IN c.moons " +
- "WHERE a.name = \"volcano1\" OR b.name = \"1 moon\"";
+ string dbQueryWithJoin = "SELECT c.name " +
+ "FROM c " +
+ "WHERE (EXISTS (SELECT 1 " +
+ "FROM table0 IN c.additionalAttributes " +
+ "WHERE table0.name = \"volcano1\" ) OR " +
+ "EXISTS (SELECT 1 " +
+ "FROM table2 IN c.moons " +
+ "WHERE table2.name = \"1 moon\" ) )";
await ExecuteAndValidateResult(_graphQLQueryName, gqlQuery, dbQueryWithJoin);
}
@@ -881,6 +897,51 @@ public async Task TestFilterWithEntityNameAlias()
await ExecuteAndValidateResult(_graphQLQueryName, gqlQuery, dbQuery);
}
+ ///
+ /// For "item-level-permission-role" role, DB policies are defined. This test confirms that all the DB policies are considered.
+ /// For the reference, Below conditions are applied for an Entity in Db Config file.
+ /// 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'"
+ ///
+ [TestMethod]
+ public async Task TestQueryFilterFieldAuth_Only_AuthorizedArrayItem()
+ {
+ string gqlQuery = @"{
+ planets(first: 1, " + QueryBuilder.FILTER_FIELD_NAME + @" : { character: {type: {eq: ""Mars""}}})
+ {
+ items {
+ id
+ }
+ }
+ }";
+
+ // Now get the item with item level permission
+ string clientRoleHeader = "item-level-permission-role";
+ // string clientRoleHeader = "authenticated";
+ JsonElement actual = await ExecuteGraphQLRequestAsync(
+ queryName: _graphQLQueryName,
+ query: gqlQuery,
+ authToken: AuthTestHelper.CreateStaticWebAppsEasyAuthToken(specificRole: clientRoleHeader),
+ clientRoleHeader: clientRoleHeader);
+
+ string dbQuery = $"SELECT c.id " +
+ $"FROM c " +
+ $"WHERE c.character.type = 'Mars' " +
+ $"AND c.earth.type = 'earth0' " + // From DB Policy
+ $"AND EXISTS (SELECT VALUE 1 " +
+ $"FROM table1 IN c.additionalAttributes " +
+ $"WHERE (table1.name = 'volcano0')) " + // From DB Policy
+ $"AND EXISTS (SELECT VALUE 1 " +
+ $"FROM table2 IN c.moons " +
+ $"JOIN table3 IN table2.moonAdditionalAttributes " +
+ $"WHERE (table3.name = 'moonattr0'))"; // From DB Policy
+
+ 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
@@ -1018,7 +1079,7 @@ public async Task TestQueryFilterNestedFieldAuth_UnauthorizedNestedField()
///
/// Tests that the nested field level query filter fails authorization when nested object is
- /// unauthorized. Here, Nested array type 'moreAttributes' is avaliable for 'Authenticated' role only and
+ /// unauthorized. Here, Nested array type 'moreAttributes' is available for 'Authenticated' role only and
/// we are trying to access it with 'anonymous' role.
///
[TestMethod]
diff --git a/src/Service.Tests/CosmosTests/QueryTests.cs b/src/Service.Tests/CosmosTests/QueryTests.cs
index f7be33d833..3dc9732c7c 100644
--- a/src/Service.Tests/CosmosTests/QueryTests.cs
+++ b/src/Service.Tests/CosmosTests/QueryTests.cs
@@ -59,10 +59,9 @@ public class QueryTests : TestBase
";
public static readonly string MoonWithInvalidAuthorizationPolicy = @"
query ($id: ID, $partitionKeyValue: String) {
- moon_by_pk (id: $id, _partitionKeyValue: $partitionKeyValue){
+ invalidAuthModel_by_pk (id: $id, _partitionKeyValue: $partitionKeyValue){
id
name
- details
}
}";
private static List _idList;
@@ -103,7 +102,7 @@ public async Task GetWithInvalidAuthorizationPolicyInSchema()
string id = _idList[0];
string clientRoleHeader = AuthorizationType.Authenticated.ToString();
JsonElement response = await ExecuteGraphQLRequestAsync(
- queryName: "moon_by_pk",
+ queryName: "invalidAuthModel_by_pk",
query: MoonWithInvalidAuthorizationPolicy,
variables: new() { { "id", id }, { "partitionKeyValue", id } },
authToken: AuthTestHelper.CreateStaticWebAppsEasyAuthToken(specificRole: clientRoleHeader),
@@ -307,11 +306,11 @@ public async Task GetByPrimaryKeyWhenEntityNameDoesntMatchGraphQLType()
string id = _idList[0];
string query = @$"
query {{
- star_by_pk (id: ""{id}"", _partitionKeyValue: ""{id}"") {{
+ planet_by_pk (id: ""{id}"", _partitionKeyValue: ""{id}"") {{
id
}}
}}";
- JsonElement response = await ExecuteGraphQLRequestAsync("star_by_pk", query);
+ JsonElement response = await ExecuteGraphQLRequestAsync("planet_by_pk", query);
// Validate results
Assert.AreEqual(id, response.GetProperty("id").GetString());
diff --git a/src/Service.Tests/CosmosTests/TestBase.cs b/src/Service.Tests/CosmosTests/TestBase.cs
index c4d4a1031b..12e881f52e 100644
--- a/src/Service.Tests/CosmosTests/TestBase.cs
+++ b/src/Service.Tests/CosmosTests/TestBase.cs
@@ -30,10 +30,10 @@ namespace Azure.DataApiBuilder.Service.Tests.CosmosTests;
public class TestBase
{
internal const string DATABASE_NAME = "graphqldb";
- // Intentionally removed name attibute from Planet model to test scenario where the 'name' attribute
+ // Intentionally removed name attribute 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(name:""PlanetAlias"") {
id : ID!,
name : String,
character: Character,
@@ -56,51 +56,64 @@ 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 {
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""])
+}
+
+type InvalidAuthModel @model @authorize(policy: ""Crater"") {
+ id : ID!,
+ name : String
+}
+
+type PlanetAgain @model {
id : ID,
name : String,
type: String @authorize(roles: [""authenticated""])
-}";
+}
+";
private static string[] _planets = { "Earth", "Mars", "Jupiter", "Tatooine", "Endor", "Dagobah", "Hoth", "Bespin", "Spec%ial" };
diff --git a/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForCosmos.verified.txt b/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForCosmos.verified.txt
index be3f09b998..51d8543ed5 100644
--- a/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForCosmos.verified.txt
+++ b/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForCosmos.verified.txt
@@ -31,7 +31,7 @@
},
Entities: [
{
- Planet: {
+ PlanetAlias: {
Source: {
Object: graphqldb.planet
},
@@ -90,6 +90,14 @@
Action: Read
}
]
+ },
+ {
+ Role: item-level-permission-role,
+ Actions: [
+ {
+ Action: Read
+ }
+ ]
}
]
}
@@ -124,12 +132,20 @@
Action: Delete
}
]
+ },
+ {
+ Role: item-level-permission-role,
+ Actions: [
+ {
+ Action: Read
+ }
+ ]
}
]
}
},
{
- StarAlias: {
+ Star: {
Source: {
Object: graphqldb.planet
},
@@ -180,7 +196,7 @@
}
},
{
- TagAlias: {
+ Tag: {
Source: {
Object: graphqldb.planet
},
@@ -282,42 +298,32 @@
Role: field-mutation-with-read-permission,
Actions: [
{
- Action: Update,
+ Action: Read
+ }
+ ]
+ },
+ {
+ Role: anonymous,
+ Actions: [
+ {
+ Action: Read,
Fields: {
Exclude: [
- name
+ *
],
Include: [
- id,
- type
+ *
]
}
},
{
- Action: Delete,
- Fields: {
- Exclude: [
- name
- ],
- Include: [
- id,
- type
- ]
- }
+ Action: Create
},
{
- Action: Create,
- Fields: {
- Exclude: [
- name
- ],
- Include: [
- id
- ]
- }
+ Action: Update
},
{
- Action: Read
+ Action: Delete
}
]
},
@@ -356,63 +362,13 @@
]
},
{
- Role: wildcard-exclude-fields-role,
+ Role: item-level-permission-role,
Actions: [
{
Action: Read,
- Fields: {
- Exclude: [
- *
- ]
- }
- },
- {
- Action: Delete,
- Fields: {
- Exclude: [
- *
- ]
+ Policy: {
+ Database: @item.type eq 'earth0'
}
- },
- {
- Action: Update,
- Fields: {
- Exclude: [
- *
- ]
- }
- },
- {
- Action: Create,
- Fields: {
- Exclude: [
- *
- ]
- }
- }
- ]
- },
- {
- Role: only-create-role,
- Actions: [
- {
- Action: Create
- }
- ]
- },
- {
- Role: only-update-role,
- Actions: [
- {
- Action: Update
- }
- ]
- },
- {
- Role: only-delete-role,
- Actions: [
- {
- Action: Delete
}
]
}
@@ -482,6 +438,17 @@
Action: *
}
]
+ },
+ {
+ Role: item-level-permission-role,
+ Actions: [
+ {
+ Action: Read,
+ Policy: {
+ Database: @item.name eq 'volcano0'
+ }
+ }
+ ]
}
]
}
@@ -507,12 +474,23 @@
Action: *
}
]
+ },
+ {
+ Role: item-level-permission-role,
+ Actions: [
+ {
+ Action: Read,
+ Policy: {
+ Database: @item.name eq 'moonattr0'
+ }
+ }
+ ]
}
]
}
},
{
- MoreAttrAlias: {
+ MoreAttribute: {
Source: {
Object: graphqldb.planet
},
@@ -582,6 +560,233 @@
}
]
}
+ },
+ {
+ PlanetAgain: {
+ Source: {
+ Object: graphqldb.newcontainer
+ },
+ GraphQL: {
+ Singular: PlanetAgain,
+ Plural: PlanetAgains,
+ Enabled: true
+ },
+ Rest: {
+ Enabled: false
+ },
+ Permissions: [
+ {
+ Role: field-mutation-with-read-permission,
+ Actions: [
+ {
+ Action: Update,
+ Fields: {
+ Exclude: [
+ name
+ ],
+ Include: [
+ id,
+ type
+ ]
+ }
+ },
+ {
+ Action: Delete,
+ Fields: {
+ Exclude: [
+ name
+ ],
+ Include: [
+ id,
+ type
+ ]
+ }
+ },
+ {
+ Action: Create,
+ Fields: {
+ Exclude: [
+ name
+ ],
+ Include: [
+ id
+ ]
+ }
+ },
+ {
+ Action: Read
+ }
+ ]
+ },
+ {
+ Role: authenticated,
+ Actions: [
+ {
+ Action: Create
+ },
+ {
+ Action: Read
+ },
+ {
+ Action: Update
+ },
+ {
+ Action: Delete
+ }
+ ]
+ },
+ {
+ Role: limited-read-role,
+ Actions: [
+ {
+ Action: Read,
+ Fields: {
+ Exclude: [
+ name
+ ],
+ Include: [
+ id,
+ type
+ ]
+ }
+ }
+ ]
+ },
+ {
+ Role: wildcard-exclude-fields-role,
+ Actions: [
+ {
+ Action: Read,
+ Fields: {
+ Exclude: [
+ *
+ ]
+ }
+ },
+ {
+ Action: Delete,
+ Fields: {
+ Exclude: [
+ *
+ ]
+ }
+ },
+ {
+ Action: Update,
+ Fields: {
+ Exclude: [
+ *
+ ]
+ }
+ },
+ {
+ Action: Create,
+ Fields: {
+ Exclude: [
+ *
+ ]
+ }
+ }
+ ]
+ },
+ {
+ Role: only-create-role,
+ Actions: [
+ {
+ Action: Create
+ }
+ ]
+ },
+ {
+ Role: only-update-role,
+ Actions: [
+ {
+ Action: Update
+ }
+ ]
+ },
+ {
+ Role: only-delete-role,
+ Actions: [
+ {
+ Action: Delete
+ }
+ ]
+ }
+ ]
+ }
+ },
+ {
+ InvalidAuthModel: {
+ Source: {
+ Object: graphqldb.invalidAuthModelContainer
+ },
+ GraphQL: {
+ Singular: InvalidAuthModel,
+ Plural: InvalidAuthModels,
+ Enabled: true
+ },
+ Rest: {
+ Enabled: false
+ },
+ Permissions: [
+ {
+ Role: anonymous,
+ Actions: [
+ {
+ Action: Update,
+ Fields: {
+ Exclude: [
+ *
+ ]
+ }
+ },
+ {
+ Action: Read,
+ Fields: {
+ Exclude: [
+ name
+ ],
+ Include: [
+ id
+ ]
+ }
+ },
+ {
+ Action: Create,
+ Fields: {
+ Exclude: [
+ name
+ ],
+ Include: [
+ id
+ ]
+ }
+ },
+ {
+ Action: Delete
+ }
+ ]
+ },
+ {
+ Role: authenticated,
+ Actions: [
+ {
+ Action: Create
+ },
+ {
+ Action: Read
+ },
+ {
+ Action: Update
+ },
+ {
+ Action: Delete
+ }
+ ]
+ }
+ ]
+ }
}
]
}
\ No newline at end of file
diff --git a/src/Service.Tests/dab-config.CosmosDb_NoSql.json b/src/Service.Tests/dab-config.CosmosDb_NoSql.json
index c9f429929e..c55314e592 100644
--- a/src/Service.Tests/dab-config.CosmosDb_NoSql.json
+++ b/src/Service.Tests/dab-config.CosmosDb_NoSql.json
@@ -34,7 +34,7 @@
}
},
"entities": {
- "Planet": {
+ "PlanetAlias": {
"source": {
"object": "graphqldb.planet"
},
@@ -96,6 +96,14 @@
"action": "read"
}
]
+ },
+ {
+ "role": "item-level-permission-role",
+ "actions": [
+ {
+ "action": "read"
+ }
+ ]
}
]
},
@@ -130,10 +138,18 @@
"action": "delete"
}
]
+ },
+ {
+ "role": "item-level-permission-role",
+ "actions": [
+ {
+ "action": "read"
+ }
+ ]
}
]
},
- "StarAlias": {
+ "Star": {
"source": {
"object": "graphqldb.planet"
},
@@ -184,7 +200,7 @@
}
]
},
- "TagAlias": {
+ "Tag": {
"source": {
"object": "graphqldb.planet"
},
@@ -288,42 +304,32 @@
"role": "field-mutation-with-read-permission",
"actions": [
{
- "action": "update",
+ "action": "read"
+ }
+ ]
+ },
+ {
+ "role": "anonymous",
+ "actions": [
+ {
+ "action": "read",
"fields": {
"exclude": [
- "name"
+ "*"
],
"include": [
- "id",
- "type"
+ "*"
]
}
},
{
- "action": "delete",
- "fields": {
- "exclude": [
- "name"
- ],
- "include": [
- "id",
- "type"
- ]
- }
+ "action": "create"
},
{
- "action": "create",
- "fields": {
- "exclude": [
- "name"
- ],
- "include": [
- "id"
- ]
- }
+ "action": "update"
},
{
- "action": "read"
+ "action": "delete"
}
]
},
@@ -362,63 +368,13 @@
]
},
{
- "role": "wildcard-exclude-fields-role",
+ "role": "item-level-permission-role",
"actions": [
{
"action": "read",
- "fields": {
- "exclude": [
- "*"
- ]
- }
- },
- {
- "action": "delete",
- "fields": {
- "exclude": [
- "*"
- ]
+ "policy": {
+ "database": "@item.type eq 'earth0'"
}
- },
- {
- "action": "update",
- "fields": {
- "exclude": [
- "*"
- ]
- }
- },
- {
- "action": "create",
- "fields": {
- "exclude": [
- "*"
- ]
- }
- }
- ]
- },
- {
- "role": "only-create-role",
- "actions": [
- {
- "action": "create"
- }
- ]
- },
- {
- "role": "only-update-role",
- "actions": [
- {
- "action": "update"
- }
- ]
- },
- {
- "role": "only-delete-role",
- "actions": [
- {
- "action": "delete"
}
]
}
@@ -488,6 +444,17 @@
"action": "*"
}
]
+ },
+ {
+ "role": "item-level-permission-role",
+ "actions": [
+ {
+ "action": "read",
+ "policy": {
+ "database": "@item.name eq 'volcano0'"
+ }
+ }
+ ]
}
]
},
@@ -513,10 +480,21 @@
"action": "*"
}
]
+ },
+ {
+ "role": "item-level-permission-role",
+ "actions": [
+ {
+ "action": "read",
+ "policy": {
+ "database": "@item.name eq 'moonattr0'"
+ }
+ }
+ ]
}
]
},
- "MoreAttrAlias": {
+ "MoreAttribute": {
"source": {
"object": "graphqldb.planet"
},
@@ -587,6 +565,233 @@
]
}
]
+ },
+ "PlanetAgain": {
+ "source": {
+ "object": "graphqldb.newcontainer"
+ },
+ "graphql": {
+ "enabled": true,
+ "type": {
+ "singular": "PlanetAgain",
+ "plural": "PlanetAgains"
+ }
+ },
+ "rest": {
+ "enabled": false
+ },
+ "permissions": [
+ {
+ "role": "field-mutation-with-read-permission",
+ "actions": [
+ {
+ "action": "update",
+ "fields": {
+ "exclude": [
+ "name"
+ ],
+ "include": [
+ "id",
+ "type"
+ ]
+ }
+ },
+ {
+ "action": "delete",
+ "fields": {
+ "exclude": [
+ "name"
+ ],
+ "include": [
+ "id",
+ "type"
+ ]
+ }
+ },
+ {
+ "action": "create",
+ "fields": {
+ "exclude": [
+ "name"
+ ],
+ "include": [
+ "id"
+ ]
+ }
+ },
+ {
+ "action": "read"
+ }
+ ]
+ },
+ {
+ "role": "authenticated",
+ "actions": [
+ {
+ "action": "create"
+ },
+ {
+ "action": "read"
+ },
+ {
+ "action": "update"
+ },
+ {
+ "action": "delete"
+ }
+ ]
+ },
+ {
+ "role": "limited-read-role",
+ "actions": [
+ {
+ "action": "read",
+ "fields": {
+ "exclude": [
+ "name"
+ ],
+ "include": [
+ "id",
+ "type"
+ ]
+ }
+ }
+ ]
+ },
+ {
+ "role": "wildcard-exclude-fields-role",
+ "actions": [
+ {
+ "action": "read",
+ "fields": {
+ "exclude": [
+ "*"
+ ]
+ }
+ },
+ {
+ "action": "delete",
+ "fields": {
+ "exclude": [
+ "*"
+ ]
+ }
+ },
+ {
+ "action": "update",
+ "fields": {
+ "exclude": [
+ "*"
+ ]
+ }
+ },
+ {
+ "action": "create",
+ "fields": {
+ "exclude": [
+ "*"
+ ]
+ }
+ }
+ ]
+ },
+ {
+ "role": "only-create-role",
+ "actions": [
+ {
+ "action": "create"
+ }
+ ]
+ },
+ {
+ "role": "only-update-role",
+ "actions": [
+ {
+ "action": "update"
+ }
+ ]
+ },
+ {
+ "role": "only-delete-role",
+ "actions": [
+ {
+ "action": "delete"
+ }
+ ]
+ }
+ ]
+ },
+ "InvalidAuthModel": {
+ "source": {
+ "object": "graphqldb.invalidAuthModelContainer"
+ },
+ "graphql": {
+ "enabled": true,
+ "type": {
+ "singular": "InvalidAuthModel",
+ "plural": "InvalidAuthModels"
+ }
+ },
+ "rest": {
+ "enabled": false
+ },
+ "permissions": [
+ {
+ "role": "anonymous",
+ "actions": [
+ {
+ "action": "update",
+ "fields": {
+ "exclude": [
+ "*"
+ ]
+ }
+ },
+ {
+ "action": "read",
+ "fields": {
+ "exclude": [
+ "name"
+ ],
+ "include": [
+ "id"
+ ]
+ }
+ },
+ {
+ "action": "create",
+ "fields": {
+ "exclude": [
+ "name"
+ ],
+ "include": [
+ "id"
+ ]
+ }
+ },
+ {
+ "action": "delete"
+ }
+ ]
+ },
+ {
+ "role": "authenticated",
+ "actions": [
+ {
+ "action": "create"
+ },
+ {
+ "action": "read"
+ },
+ {
+ "action": "update"
+ },
+ {
+ "action": "delete"
+ }
+ ]
+ }
+ ]
}
}
}
\ No newline at end of file