diff --git a/config-generators/mssql-commands.txt b/config-generators/mssql-commands.txt
index c4af5e2f77..6b97f3ebfb 100644
--- a/config-generators/mssql-commands.txt
+++ b/config-generators/mssql-commands.txt
@@ -14,6 +14,7 @@ add stocks_price --config "dab-config.MsSql.json" --source stocks_price --permis
update stocks_price --config "dab-config.MsSql.json" --permissions "anonymous:read"
update stocks_price --config "dab-config.MsSql.json" --permissions "TestNestedFilterFieldIsNull_ColumnForbidden:read" --fields.exclude "price"
update stocks_price --config "dab-config.MsSql.json" --permissions "TestNestedFilterFieldIsNull_EntityReadForbidden:create"
+update stocks_price --config "dab-config.MsSql.json" --permissions "test_role_with_excluded_fields_on_create:create,read,update,delete"
add Tree --config "dab-config.MsSql.json" --source trees --permissions "anonymous:create,read,update,delete"
add Shrub --config "dab-config.MsSql.json" --source trees --permissions "anonymous:create,read,update,delete" --rest plants
add Fungus --config "dab-config.MsSql.json" --source fungi --permissions "anonymous:create,read,update,delete" --graphql "fungus:fungi"
@@ -65,6 +66,8 @@ update Publisher --config "dab-config.MsSql.json" --permissions "database_policy
update Publisher --config "dab-config.MsSql.json" --permissions "database_policy_tester:update" --policy-database "@item.id ne 1234"
update Publisher --config "dab-config.MsSql.json" --permissions "database_policy_tester:create" --policy-database "@item.name ne 'New publisher'"
update Stock --config "dab-config.MsSql.json" --permissions "authenticated:create,read,update,delete"
+update Stock --config "dab-config.MsSql.json" --permissions "test_role_with_excluded_fields_on_create:read,update,delete"
+update Stock --config "dab-config.MsSql.json" --permissions "test_role_with_excluded_fields_on_create:create" --fields.exclude "piecesAvailable"
update Stock --config "dab-config.MsSql.json" --rest commodities --graphql true --relationship stocks_price --target.entity stocks_price --cardinality one
update Book --config "dab-config.MsSql.json" --permissions "authenticated:create,read,update,delete"
update Book --config "dab-config.MsSql.json" --relationship publishers --target.entity Publisher --cardinality one
@@ -112,6 +115,7 @@ update WebsiteUser --config "dab-config.MsSql.json" --permissions "authenticated
update Revenue --config "dab-config.MsSql.json" --permissions "database_policy_tester:create" --policy-database "@item.revenue gt 1000"
update Comic --config "dab-config.MsSql.json" --permissions "authenticated:create,read,update,delete" --rest true --graphql true --relationship myseries --target.entity series --cardinality one
update series --config "dab-config.MsSql.json" --relationship comics --target.entity Comic --cardinality many
+update stocks_price --config "dab-config.MsSql.json" --relationship Stock --target.entity Stock --cardinality one
update Broker --config "dab-config.MsSql.json" --permissions "authenticated:create,update,read,delete" --graphql false
update Tree --config "dab-config.MsSql.json" --rest true --graphql false --permissions "authenticated:create,read,update,delete" --map "species:Scientific Name,region:United State's Region"
update Shrub --config "dab-config.MsSql.json" --permissions "authenticated:create,read,update,delete" --map "species:fancyName"
diff --git a/src/Core/Authorization/AuthorizationResolver.cs b/src/Core/Authorization/AuthorizationResolver.cs
index 93e4622f29..64785de703 100644
--- a/src/Core/Authorization/AuthorizationResolver.cs
+++ b/src/Core/Authorization/AuthorizationResolver.cs
@@ -13,6 +13,7 @@
using Azure.DataApiBuilder.Core.Services;
using Azure.DataApiBuilder.Core.Services.MetadataProviders;
using Azure.DataApiBuilder.Service.Exceptions;
+using HotChocolate.Resolvers;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Primitives;
@@ -213,6 +214,31 @@ public string GetDBPolicyForRequest(string entityName, string roleName, EntityAc
return dbPolicy is not null ? dbPolicy : string.Empty;
}
+ ///
+ /// Helper method to get the role with which the GraphQL API request was executed.
+ ///
+ /// HotChocolate context for the GraphQL request.
+ /// Role of the current GraphQL API request.
+ /// Throws exception when no client role could be inferred from the context.
+ public static string GetRoleOfGraphQLRequest(IMiddlewareContext context)
+ {
+ string role = string.Empty;
+ if (context.ContextData.TryGetValue(key: CLIENT_ROLE_HEADER, out object? value) && value is StringValues stringVals)
+ {
+ role = stringVals.ToString();
+ }
+
+ if (string.IsNullOrEmpty(role))
+ {
+ throw new DataApiBuilderException(
+ message: "No ClientRoleHeader available to perform authorization.",
+ statusCode: HttpStatusCode.Forbidden,
+ subStatusCode: DataApiBuilderException.SubStatusCodes.AuthorizationCheckFailed);
+ }
+
+ return role;
+ }
+
#region Helpers
///
/// Method to read in data from the config class into a Dictionary for quick lookup
diff --git a/src/Core/Resolvers/CosmosMutationEngine.cs b/src/Core/Resolvers/CosmosMutationEngine.cs
index 4f95e4f267..7138db6223 100644
--- a/src/Core/Resolvers/CosmosMutationEngine.cs
+++ b/src/Core/Resolvers/CosmosMutationEngine.cs
@@ -16,7 +16,6 @@
using HotChocolate.Resolvers;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.Cosmos;
-using Microsoft.Extensions.Primitives;
using Newtonsoft.Json.Linq;
namespace Azure.DataApiBuilder.Core.Resolvers
@@ -64,7 +63,7 @@ private async Task ExecuteAsync(IMiddlewareContext context, IDictionary
// If authorization fails, an exception will be thrown and request execution halts.
string graphQLType = context.Selection.Field.Type.NamedType().Name.Value;
string entityName = metadataProvider.GetEntityName(graphQLType);
- AuthorizeMutationFields(context, queryArgs, entityName, resolver.OperationType);
+ AuthorizeMutation(context, queryArgs, entityName, resolver.OperationType);
ItemResponse? response = resolver.OperationType switch
{
@@ -74,7 +73,7 @@ private async Task ExecuteAsync(IMiddlewareContext context, IDictionary
_ => throw new NotSupportedException($"unsupported operation type: {resolver.OperationType}")
};
- string roleName = GetRoleOfGraphQLRequest(context);
+ string roleName = AuthorizationResolver.GetRoleOfGraphQLRequest(context);
// The presence of READ permission is checked in the current role (with which the request is executed) as well as Anonymous role. This is because, for GraphQL requests,
// READ permission is inherited by other roles from Anonymous role when present.
@@ -93,14 +92,13 @@ private async Task ExecuteAsync(IMiddlewareContext context, IDictionary
}
///
- public void AuthorizeMutationFields(
+ public void AuthorizeMutation(
IMiddlewareContext context,
IDictionary parameters,
string entityName,
EntityActionOperation mutationOperation)
{
- string role = GetRoleOfGraphQLRequest(context);
-
+ string clientRole = AuthorizationResolver.GetRoleOfGraphQLRequest(context);
List inputArgumentKeys;
if (mutationOperation != EntityActionOperation.Delete)
{
@@ -114,9 +112,9 @@ public void AuthorizeMutationFields(
bool isAuthorized = mutationOperation switch
{
EntityActionOperation.UpdateGraphQL =>
- _authorizationResolver.AreColumnsAllowedForOperation(entityName, roleName: role, operation: EntityActionOperation.Update, inputArgumentKeys),
+ _authorizationResolver.AreColumnsAllowedForOperation(entityName, roleName: clientRole, operation: EntityActionOperation.Update, inputArgumentKeys),
EntityActionOperation.Create =>
- _authorizationResolver.AreColumnsAllowedForOperation(entityName, roleName: role, operation: mutationOperation, inputArgumentKeys),
+ _authorizationResolver.AreColumnsAllowedForOperation(entityName, roleName: clientRole, operation: mutationOperation, inputArgumentKeys),
EntityActionOperation.Delete => true,// Field level authorization is not supported for delete mutations. A requestor must be authorized
// to perform the delete operation on the entity to reach this point.
_ => throw new DataApiBuilderException(
@@ -261,29 +259,6 @@ private static async Task> HandleUpdateAsync(IDictionary
- /// Helper method to get the role with which the GraphQL API request was executed.
- ///
- /// HotChocolate context for the GraphQL request
- private static string GetRoleOfGraphQLRequest(IMiddlewareContext context)
- {
- string role = string.Empty;
- if (context.ContextData.TryGetValue(key: AuthorizationResolver.CLIENT_ROLE_HEADER, out object? value) && value is StringValues stringVals)
- {
- role = stringVals.ToString();
- }
-
- if (string.IsNullOrEmpty(role))
- {
- throw new DataApiBuilderException(
- message: "No ClientRoleHeader available to perform authorization.",
- statusCode: HttpStatusCode.Unauthorized,
- subStatusCode: DataApiBuilderException.SubStatusCodes.AuthorizationCheckFailed);
- }
-
- return role;
- }
-
///
/// The method is for parsing the mutation input object with nested inner objects when input is passing inline.
///
diff --git a/src/Core/Resolvers/IMutationEngine.cs b/src/Core/Resolvers/IMutationEngine.cs
index da089ed199..30cb188efc 100644
--- a/src/Core/Resolvers/IMutationEngine.cs
+++ b/src/Core/Resolvers/IMutationEngine.cs
@@ -4,6 +4,7 @@
using System.Text.Json;
using Azure.DataApiBuilder.Config.ObjectModel;
using Azure.DataApiBuilder.Core.Models;
+using Azure.DataApiBuilder.Service.Exceptions;
using HotChocolate.Resolvers;
using Microsoft.AspNetCore.Mvc;
@@ -41,12 +42,13 @@ public interface IMutationEngine
///
/// Authorization check on mutation fields provided in a GraphQL Mutation request.
///
- /// Middleware context of the mutation
+ /// GraphQL request context.
+ /// Client role header value extracted from the middleware context of the mutation
/// parameters in the mutation query.
/// entity name
/// mutation operation
///
- public void AuthorizeMutationFields(
+ public void AuthorizeMutation(
IMiddlewareContext context,
IDictionary parameters,
string entityName,
diff --git a/src/Core/Resolvers/SqlMutationEngine.cs b/src/Core/Resolvers/SqlMutationEngine.cs
index 298a7d5d72..63de3f0f87 100644
--- a/src/Core/Resolvers/SqlMutationEngine.cs
+++ b/src/Core/Resolvers/SqlMutationEngine.cs
@@ -19,11 +19,12 @@
using Azure.DataApiBuilder.Service.Exceptions;
using Azure.DataApiBuilder.Service.GraphQLBuilder;
using Azure.DataApiBuilder.Service.GraphQLBuilder.Mutations;
+using Azure.DataApiBuilder.Service.Services;
+using HotChocolate.Language;
using HotChocolate.Resolvers;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.AspNetCore.Mvc;
-using Microsoft.Extensions.Primitives;
namespace Azure.DataApiBuilder.Core.Resolvers
{
@@ -90,11 +91,10 @@ public SqlMutationEngine(
Tuple? result = null;
EntityActionOperation mutationOperation = MutationBuilder.DetermineMutationOperationTypeBasedOnInputType(graphqlMutationName);
+ string roleName = AuthorizationResolver.GetRoleOfGraphQLRequest(context);
// If authorization fails, an exception will be thrown and request execution halts.
- AuthorizeMutationFields(context, parameters, entityName, mutationOperation);
-
- string roleName = GetRoleOfGraphQLRequest(context);
+ AuthorizeMutation(context, parameters, entityName, mutationOperation);
// The presence of READ permission is checked in the current role (with which the request is executed) as well as Anonymous role. This is because, for GraphQL requests,
// READ permission is inherited by other roles from Anonymous role when present.
@@ -218,6 +218,36 @@ await PerformMutationOperation(
return result;
}
+ ///
+ /// Helper method to determine whether a mutation is a mutate one or mutate many operation (eg. createBook/createBooks).
+ ///
+ /// GraphQL request context.
+ private static bool IsPointMutation(IMiddlewareContext context)
+ {
+ IOutputType outputType = context.Selection.Field.Type;
+ if (outputType.TypeName().Value.Equals(GraphQLUtils.DB_OPERATION_RESULT_TYPE))
+ {
+ // Hit when the database type is DwSql. We don't support multiple mutation for DwSql yet.
+ return true;
+ }
+
+ ObjectType underlyingFieldType = GraphQLUtils.UnderlyingGraphQLEntityType(outputType);
+ bool isPointMutation;
+ if (GraphQLUtils.TryExtractGraphQLFieldModelName(underlyingFieldType.Directives, out string? _))
+ {
+ isPointMutation = true;
+ }
+ else
+ {
+ // Model directive is not added to the output type of 'mutate many' mutations.
+ // Thus, absence of model directive here indicates that we are dealing with a 'mutate many'
+ // mutation like createBooks.
+ isPointMutation = false;
+ }
+
+ return isPointMutation;
+ }
+
///
/// Converts exposed column names from the parameters provided to backing column names.
/// parameters.Value is not modified.
@@ -1049,41 +1079,58 @@ private void PopulateParamsFromRestRequest(Dictionary parameter
}
}
- ///
- /// Authorization check on mutation fields provided in a GraphQL Mutation request.
- ///
- ///
- ///
- ///
- ///
- ///
- public void AuthorizeMutationFields(
+ ///
+ public void AuthorizeMutation(
IMiddlewareContext context,
IDictionary parameters,
string entityName,
EntityActionOperation mutationOperation)
{
- string role = GetRoleOfGraphQLRequest(context);
-
- List inputArgumentKeys;
- if (mutationOperation != EntityActionOperation.Delete)
+ string inputArgumentName = MutationBuilder.ITEM_INPUT_ARGUMENT_NAME;
+ string clientRole = AuthorizationResolver.GetRoleOfGraphQLRequest(context);
+ if (mutationOperation is EntityActionOperation.Create)
{
- inputArgumentKeys = BaseSqlQueryStructure.GetSubArgumentNamesFromGQLMutArguments(MutationBuilder.ITEM_INPUT_ARGUMENT_NAME, parameters);
+ if (!IsPointMutation(context))
+ {
+ inputArgumentName = MutationBuilder.ARRAY_INPUT_ARGUMENT_NAME;
+ }
+
+ AuthorizeEntityAndFieldsForMutation(context, clientRole, entityName, mutationOperation, inputArgumentName, parameters);
}
else
{
- inputArgumentKeys = parameters.Keys.ToList();
+ List inputArgumentKeys;
+ if (mutationOperation != EntityActionOperation.Delete)
+ {
+ inputArgumentKeys = BaseSqlQueryStructure.GetSubArgumentNamesFromGQLMutArguments(inputArgumentName, parameters);
+ }
+ else
+ {
+ inputArgumentKeys = parameters.Keys.ToList();
+ }
+
+ if (!AreFieldsAuthorizedForEntity(clientRole, entityName, mutationOperation, inputArgumentKeys))
+ {
+ throw new DataApiBuilderException(
+ message: "Unauthorized due to one or more fields in this mutation.",
+ statusCode: HttpStatusCode.Forbidden,
+ subStatusCode: DataApiBuilderException.SubStatusCodes.AuthorizationCheckFailed
+ );
+ }
}
+ }
+ private bool AreFieldsAuthorizedForEntity(string clientRole, string entityName, EntityActionOperation mutationOperation, IEnumerable inputArgumentKeys)
+ {
bool isAuthorized; // False by default.
switch (mutationOperation)
{
case EntityActionOperation.UpdateGraphQL:
- isAuthorized = _authorizationResolver.AreColumnsAllowedForOperation(entityName, roleName: role, operation: EntityActionOperation.Update, inputArgumentKeys);
+ isAuthorized = _authorizationResolver.AreColumnsAllowedForOperation(entityName, roleName: clientRole, operation: EntityActionOperation.Update, inputArgumentKeys);
break;
case EntityActionOperation.Create:
- isAuthorized = _authorizationResolver.AreColumnsAllowedForOperation(entityName, roleName: role, operation: mutationOperation, inputArgumentKeys);
+ isAuthorized = _authorizationResolver.AreColumnsAllowedForOperation(entityName, roleName: clientRole, operation: mutationOperation, inputArgumentKeys);
break;
case EntityActionOperation.Execute:
case EntityActionOperation.Delete:
@@ -1101,37 +1148,219 @@ public void AuthorizeMutationFields(
);
}
- if (!isAuthorized)
+ return isAuthorized;
+ }
+
+ ///
+ /// Performs authorization checks on entity level permissions and field level permissions for every entity and field
+ /// referenced in a GraphQL mutation for the given client role.
+ ///
+ /// Middleware context.
+ /// Client role header value extracted from the middleware context of the mutation
+ /// Top level entity name.
+ /// Mutation operation
+ /// Name of the input argument (differs based on point/multiple mutation).
+ /// Dictionary of key/value pairs for the argument name/value.
+ /// Throws exception when an authorization check fails.
+ private void AuthorizeEntityAndFieldsForMutation(
+ IMiddlewareContext context,
+ string clientRole,
+ string topLevelEntityName,
+ EntityActionOperation operation,
+ string inputArgumentName,
+ IDictionary parametersDictionary
+ )
+ {
+ if (context.Selection.Field.Arguments.TryGetField(inputArgumentName, out IInputField? schemaForArgument))
+ {
+ // Dictionary to store all the entities and their corresponding exposed column names referenced in the mutation.
+ Dictionary> entityToExposedColumns = new();
+ if (parametersDictionary.TryGetValue(inputArgumentName, out object? parameters))
+ {
+ // Get all the entity names and field names referenced in the mutation.
+ PopulateMutationEntityAndFieldsToAuthorize(entityToExposedColumns, schemaForArgument, topLevelEntityName, context, parameters!);
+ }
+ else
+ {
+ throw new DataApiBuilderException(
+ message: $"{inputArgumentName} cannot be null for mutation:{context.Selection.Field.Name.Value}.",
+ statusCode: HttpStatusCode.BadRequest,
+ subStatusCode: DataApiBuilderException.SubStatusCodes.BadRequest
+ );
+ }
+
+ // Perform authorization checks at field level.
+ foreach ((string entityNameInMutation, HashSet exposedColumnsInEntity) in entityToExposedColumns)
+ {
+ if (!AreFieldsAuthorizedForEntity(clientRole, entityNameInMutation, operation, exposedColumnsInEntity))
+ {
+ throw new DataApiBuilderException(
+ message: $"Unauthorized due to one or more fields in this mutation.",
+ statusCode: HttpStatusCode.Forbidden,
+ subStatusCode: DataApiBuilderException.SubStatusCodes.AuthorizationCheckFailed
+ );
+ }
+ }
+ }
+ else
{
throw new DataApiBuilderException(
- message: "Unauthorized due to one or more fields in this mutation.",
- statusCode: HttpStatusCode.Forbidden,
- subStatusCode: DataApiBuilderException.SubStatusCodes.AuthorizationCheckFailed
- );
+ message: $"Could not interpret the schema for the input argument: {inputArgumentName}",
+ statusCode: HttpStatusCode.BadRequest,
+ subStatusCode: DataApiBuilderException.SubStatusCodes.BadRequest);
}
}
///
- /// Helper method to get the role with which the GraphQL API request was executed.
+ /// Helper method to collect names of all the fields referenced from every entity in a GraphQL mutation.
///
- /// HotChocolate context for the GraphQL request
- private static string GetRoleOfGraphQLRequest(IMiddlewareContext context)
+ /// Dictionary to store all the entities and their corresponding exposed column names referenced in the mutation.
+ /// Schema for the input field.
+ /// Name of the entity.
+ /// Middleware Context.
+ /// Value for the input field.
+ /// 1. mutation {
+ /// createbook(
+ /// item: {
+ /// title: "book #1",
+ /// reviews: [{ content: "Good book." }, { content: "Great book." }],
+ /// publishers: { name: "Macmillan publishers" },
+ /// authors: [{ birthdate: "1997-09-03", name: "Red house authors", author_name: "Dan Brown" }]
+ /// })
+ /// {
+ /// id
+ /// }
+ /// 2. mutation {
+ /// createbooks(
+ /// items: [{
+ /// title: "book #1",
+ /// reviews: [{ content: "Good book." }, { content: "Great book." }],
+ /// publishers: { name: "Macmillan publishers" },
+ /// authors: [{ birthdate: "1997-09-03", name: "Red house authors", author_name: "Dan Brown" }]
+ /// },
+ /// {
+ /// title: "book #2",
+ /// reviews: [{ content: "Awesome book." }, { content: "Average book." }],
+ /// publishers: { name: "Pearson Education" },
+ /// authors: [{ birthdate: "1990-11-04", name: "Penguin Random House", author_name: "William Shakespeare" }]
+ /// }])
+ /// {
+ /// items{
+ /// id
+ /// title
+ /// }
+ /// }
+ private void PopulateMutationEntityAndFieldsToAuthorize(
+ Dictionary> entityToExposedColumns,
+ IInputField schema,
+ string entityName,
+ IMiddlewareContext context,
+ object parameters)
{
- string role = string.Empty;
- if (context.ContextData.TryGetValue(key: AuthorizationResolver.CLIENT_ROLE_HEADER, out object? value) && value is StringValues stringVals)
+ if (parameters is List listOfObjectFieldNode)
{
- role = stringVals.ToString();
+ // For the example createbook mutation written above, the object value for `item` is interpreted as a List i.e.
+ // all the fields present for item namely- title, reviews, publishers, authors are interpreted as ObjectFieldNode.
+ ProcessObjectFieldNodesForAuthZ(
+ entityToExposedColumns: entityToExposedColumns,
+ schemaObject: ExecutionHelper.InputObjectTypeFromIInputField(schema),
+ entityName: entityName,
+ context: context,
+ fieldNodes: listOfObjectFieldNode);
}
-
- if (string.IsNullOrEmpty(role))
+ else if (parameters is List listOfIValueNode)
{
- throw new DataApiBuilderException(
- message: "No ClientRoleHeader available to perform authorization.",
- statusCode: HttpStatusCode.Unauthorized,
- subStatusCode: DataApiBuilderException.SubStatusCodes.AuthorizationCheckFailed);
+ // For the example createbooks mutation written above, the list value for `items` is interpreted as a List.
+ listOfIValueNode.ForEach(iValueNode => PopulateMutationEntityAndFieldsToAuthorize(
+ entityToExposedColumns: entityToExposedColumns,
+ schema: schema,
+ entityName: entityName,
+ context: context,
+ parameters: iValueNode));
+ }
+ else if (parameters is ObjectValueNode objectValueNode)
+ {
+ // For the example createbook mutation written above, the node for publishers field is interpreted as an ObjectValueNode.
+ // Similarly the individual node (elements in the list) for the reviews, authors ListValueNode(s) are also interpreted as ObjectValueNode(s).
+ ProcessObjectFieldNodesForAuthZ(
+ entityToExposedColumns: entityToExposedColumns,
+ schemaObject: ExecutionHelper.InputObjectTypeFromIInputField(schema),
+ entityName: entityName,
+ context: context,
+ fieldNodes: objectValueNode.Fields);
+ }
+ else
+ {
+ ListValueNode listValueNode = (ListValueNode)parameters;
+ // For the example createbook mutation written above, the list values for reviews and authors fields are interpreted as ListValueNode.
+ // All the nodes in the ListValueNode are parsed one by one.
+ listValueNode.GetNodes().ToList().ForEach(objectValueNodeInListValueNode => PopulateMutationEntityAndFieldsToAuthorize(
+ entityToExposedColumns: entityToExposedColumns,
+ schema: schema,
+ entityName: entityName,
+ context: context,
+ parameters: objectValueNodeInListValueNode));
}
+ }
- return role;
+ ///
+ /// Helper method to iterate over all the fields present in the input for the current field and add it to the dictionary
+ /// containing all entities and their corresponding fields.
+ ///
+ /// Dictionary to store all the entities and their corresponding exposed column names referenced in the mutation.
+ /// Input object type for the field.
+ /// Name of the entity.
+ /// Middleware context.
+ /// List of ObjectFieldNodes for the the input field.
+ private void ProcessObjectFieldNodesForAuthZ(
+ Dictionary> entityToExposedColumns,
+ InputObjectType schemaObject,
+ string entityName,
+ IMiddlewareContext context,
+ IReadOnlyList fieldNodes)
+ {
+ RuntimeConfig runtimeConfig = _runtimeConfigProvider.GetConfig();
+ entityToExposedColumns.TryAdd(entityName, new HashSet());
+ string dataSourceName = GraphQLUtils.GetDataSourceNameFromGraphQLContext(context, runtimeConfig);
+ ISqlMetadataProvider metadataProvider = _sqlMetadataProviderFactory.GetMetadataProvider(dataSourceName);
+ foreach (ObjectFieldNode field in fieldNodes)
+ {
+ Tuple fieldDetails = GraphQLUtils.GetFieldDetails(field.Value, context.Variables);
+ SyntaxKind underlyingFieldKind = fieldDetails.Item2;
+
+ // For a column field, we do not have to recurse to process fields in the value - which is required for relationship fields.
+ if (GraphQLUtils.IsScalarField(underlyingFieldKind) || underlyingFieldKind is SyntaxKind.NullValue)
+ {
+ // This code block can be hit in 3 cases:
+ // Case 1. We are processing a column which belongs to this entity,
+ //
+ // Case 2. We are processing the fields for a linking input object. Linking input objects enable users to provide
+ // input for fields belonging to the target entity and the linking entity. Hence the backing column for fields
+ // belonging to the linking entity will not be present in the source definition of this target entity.
+ // We need to skip such fields belonging to linking table as we do not perform authorization checks on them.
+ //
+ // Case 3. When a relationship field is assigned a null value. Such a field also needs to be ignored.
+ if (metadataProvider.TryGetBackingColumn(entityName, field.Name.Value, out string? _))
+ {
+ // Only add those fields to this entity's set of fields which belong to this entity and not the linking entity,
+ // i.e. for Case 1.
+ entityToExposedColumns[entityName].Add(field.Name.Value);
+ }
+ }
+ else
+ {
+ string relationshipName = field.Name.Value;
+ string targetEntityName = runtimeConfig.Entities![entityName].Relationships![relationshipName].TargetEntity;
+
+ // Recurse to process fields in the value of this relationship field.
+ PopulateMutationEntityAndFieldsToAuthorize(
+ entityToExposedColumns,
+ schemaObject.Fields[relationshipName],
+ targetEntityName,
+ context,
+ fieldDetails.Item1!);
+ }
+ }
}
///
diff --git a/src/Service.GraphQLBuilder/GraphQLUtils.cs b/src/Service.GraphQLBuilder/GraphQLUtils.cs
index 78ad23d925..bb786d0be7 100644
--- a/src/Service.GraphQLBuilder/GraphQLUtils.cs
+++ b/src/Service.GraphQLBuilder/GraphQLUtils.cs
@@ -7,6 +7,7 @@
using Azure.DataApiBuilder.Service.Exceptions;
using Azure.DataApiBuilder.Service.GraphQLBuilder.Directives;
using Azure.DataApiBuilder.Service.GraphQLBuilder.Queries;
+using HotChocolate.Execution;
using HotChocolate.Language;
using HotChocolate.Resolvers;
using HotChocolate.Types;
@@ -298,7 +299,7 @@ public static string GetEntityNameFromContext(IPureResolverContext context)
if (graphQLTypeName is DB_OPERATION_RESULT_TYPE)
{
// CUD for a mutation whose result set we do not have. Get Entity name mutation field directive.
- if (GraphQLUtils.TryExtractGraphQLFieldModelName(context.Selection.Field.Directives, out string? modelName))
+ if (TryExtractGraphQLFieldModelName(context.Selection.Field.Directives, out string? modelName))
{
entityName = modelName;
}
@@ -319,7 +320,7 @@ public static string GetEntityNameFromContext(IPureResolverContext context)
// if name on schema is different from name in config.
// Due to possibility of rename functionality, entityName on runtimeConfig could be different from exposed schema name.
- if (GraphQLUtils.TryExtractGraphQLFieldModelName(underlyingFieldType.Directives, out string? modelName))
+ if (TryExtractGraphQLFieldModelName(underlyingFieldType.Directives, out string? modelName))
{
entityName = modelName;
}
@@ -333,6 +334,45 @@ private static string GenerateDataSourceNameKeyFromPath(IPureResolverContext con
return $"{context.Path.ToList()[0]}";
}
+ ///
+ /// Helper method to determine whether a field is a column or complex (relationship) field based on its syntax kind.
+ /// If the SyntaxKind for the field is not ObjectValue and ListValue, it implies we are dealing with a column/scalar field which
+ /// has an IntValue, FloatValue, StringValue, BooleanValue or an EnumValue.
+ ///
+ /// SyntaxKind of the field.
+ /// true if the field is a scalar field, else false.
+ public static bool IsScalarField(SyntaxKind fieldSyntaxKind)
+ {
+ return fieldSyntaxKind is SyntaxKind.IntValue || fieldSyntaxKind is SyntaxKind.FloatValue ||
+ fieldSyntaxKind is SyntaxKind.StringValue || fieldSyntaxKind is SyntaxKind.BooleanValue ||
+ fieldSyntaxKind is SyntaxKind.EnumValue;
+ }
+
+ ///
+ /// Helper method to get the field details i.e. (field value, field kind) from the GraphQL request body.
+ /// If the field value is being provided as a variable in the mutation, a recursive call is made to the method
+ /// to get the actual value of the variable.
+ ///
+ /// Value of the field.
+ /// Collection of variables declared in the GraphQL mutation request.
+ /// A tuple containing a constant field value and the field kind.
+ public static Tuple GetFieldDetails(IValueNode? value, IVariableValueCollection variables)
+ {
+ if (value is null)
+ {
+ return new(null, SyntaxKind.NullValue);
+ }
+
+ if (value.Kind == SyntaxKind.Variable)
+ {
+ string variableName = ((VariableNode)value).Name.Value;
+ IValueNode? variableValue = variables.GetVariable(variableName);
+ return GetFieldDetails(variableValue, variables);
+ }
+
+ return new(value, value.Kind);
+ }
+
///
/// Helper method to generate the linking entity name using the source and target entity names.
///
diff --git a/src/Service.Tests/Authorization/GraphQL/CreateMutationAuthorizationTests.cs b/src/Service.Tests/Authorization/GraphQL/CreateMutationAuthorizationTests.cs
new file mode 100644
index 0000000000..fef0473c10
--- /dev/null
+++ b/src/Service.Tests/Authorization/GraphQL/CreateMutationAuthorizationTests.cs
@@ -0,0 +1,456 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.Text.Json;
+using System.Threading.Tasks;
+using Azure.DataApiBuilder.Service.Exceptions;
+using Azure.DataApiBuilder.Service.Tests.SqlTests;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace Azure.DataApiBuilder.Service.Tests.Authorization.GraphQL
+{
+ [TestClass, TestCategory(TestCategory.MSSQL)]
+ public class CreateMutationAuthorizationTests : SqlTestBase
+ {
+ ///
+ /// Set the database engine for the tests
+ ///
+ [ClassInitialize]
+ public static async Task SetupAsync(TestContext context)
+ {
+ DatabaseEngine = TestCategory.MSSQL;
+ await InitializeTestFixture();
+ }
+
+ #region Point create mutation tests
+
+ ///
+ /// Test to validate that a 'create one' point mutation request will fail if the user does not have create permission on the
+ /// top-level (the only) entity involved in the mutation.
+ ///
+ [TestMethod]
+ public async Task ValidateAuthZCheckOnEntitiesForCreateOnePointMutations()
+ {
+ string createPublisherMutationName = "createPublisher";
+ string createOnePublisherMutation = @"mutation{
+ createPublisher(item: {name: ""Publisher #1""})
+ {
+ id
+ name
+ }
+ }";
+
+ // The anonymous role does not have create permissions on the Publisher entity.
+ // Hence the request will fail during authorization check.
+ await ValidateRequestIsUnauthorized(
+ graphQLMutationName: createPublisherMutationName,
+ graphQLMutation: createOnePublisherMutation,
+ expectedExceptionMessage: "The current user is not authorized to access this resource.",
+ isAuthenticated: false,
+ clientRoleHeader: "anonymous"
+ );
+ }
+
+ ///
+ /// Test to validate that a 'create one' point mutation will fail the AuthZ checks if the user does not have create permission
+ /// on one more columns belonging to the entity in the mutation.
+ ///
+ [TestMethod]
+ public async Task ValidateAuthZCheckOnColumnsForCreateOnePointMutations()
+ {
+ string createOneStockMutationName = "createStock";
+ string createOneStockWithPiecesAvailable = @"mutation {
+ createStock(
+ item:
+ {
+ categoryid: 1,
+ pieceid: 2,
+ categoryName: ""xyz""
+ piecesAvailable: 0
+ }
+ )
+ {
+ categoryid
+ pieceid
+ }
+ }";
+
+ // The 'test_role_with_excluded_fields_on_create' role does not have create permissions on
+ // stocks.piecesAvailable field and hence the authorization check should fail.
+ await ValidateRequestIsUnauthorized(
+ graphQLMutationName: createOneStockMutationName,
+ graphQLMutation: createOneStockWithPiecesAvailable,
+ expectedExceptionMessage: "Unauthorized due to one or more fields in this mutation.",
+ expectedExceptionStatusCode: DataApiBuilderException.SubStatusCodes.AuthorizationCheckFailed.ToString(),
+ isAuthenticated: true,
+ clientRoleHeader: "test_role_with_excluded_fields_on_create");
+ }
+
+ #endregion
+
+ #region Multiple create mutation tests
+ ///
+ /// Test to validate that a 'create one' mutation request can only execute successfully when the user, has create permission
+ /// for all the entities involved in the mutation.
+ ///
+ [TestMethod]
+ public async Task ValidateAuthZCheckOnEntitiesForCreateOneMultipleMutations()
+ {
+ string createBookMutationName = "createbook";
+ string createOneBookMutation = @"mutation {
+ createbook(item: { title: ""Book #1"", publishers: { name: ""Publisher #1""}}) {
+ id
+ title
+ }
+ }";
+
+ // The anonymous role has create permissions on the Book entity but not on the Publisher entity.
+ // Hence the request will fail during authorization check.
+ await ValidateRequestIsUnauthorized(
+ graphQLMutationName: createBookMutationName,
+ graphQLMutation: createOneBookMutation,
+ expectedExceptionMessage: "Unauthorized due to one or more fields in this mutation.",
+ expectedExceptionStatusCode: DataApiBuilderException.SubStatusCodes.AuthorizationCheckFailed.ToString(),
+ isAuthenticated: false,
+ clientRoleHeader: "anonymous"
+ );
+
+ // The authenticated role has create permissions on both the Book and Publisher entities.
+ // Hence the authorization checks will pass.
+ await ValidateRequestIsAuthorized(
+ graphQLMutationName: createBookMutationName,
+ graphQLMutation: createOneBookMutation,
+ isAuthenticated: true,
+ clientRoleHeader: "authenticated"
+ );
+ }
+
+ ///
+ /// Test to validate that a 'create multiple' mutation request can only execute successfully when the user, has create permission
+ /// for all the entities involved in the mutation.
+ ///
+ [TestMethod]
+ public async Task ValidateAuthZCheckOnEntitiesForCreateMultipleMutations()
+ {
+ string createMultipleBooksMutationName = "createbooks";
+ string createMultipleBookMutation = @"mutation {
+ createbooks(items: [{ title: ""Book #1"", publisher_id: 1234 },
+ { title: ""Book #2"", publishers: { name: ""Publisher #2""}}]) {
+ items{
+ id
+ title
+ }
+ }
+ }";
+
+ // The anonymous role has create permissions on the Book entity but not on the Publisher entity.
+ // Hence the request will fail during authorization check.
+ await ValidateRequestIsUnauthorized(
+ graphQLMutationName: createMultipleBooksMutationName,
+ graphQLMutation: createMultipleBookMutation,
+ expectedExceptionMessage: "Unauthorized due to one or more fields in this mutation.",
+ expectedExceptionStatusCode: DataApiBuilderException.SubStatusCodes.AuthorizationCheckFailed.ToString(),
+ isAuthenticated: false,
+ clientRoleHeader: "anonymous");
+
+ // The authenticated role has create permissions on both the Book and Publisher entities.
+ // Hence the authorization checks will pass.
+ await ValidateRequestIsAuthorized(
+ graphQLMutationName: createMultipleBooksMutationName,
+ graphQLMutation: createMultipleBookMutation,
+ isAuthenticated: true,
+ clientRoleHeader: "authenticated",
+ expectedResult: "Expected item argument in mutation arguments."
+ );
+ }
+
+ ///
+ /// Test to validate that a 'create one' mutation request can only execute successfully when the user, in addition to having
+ /// create permission for all the entities involved in the create mutation, has the create permission for all the columns
+ /// present for each entity in the mutation.
+ /// If the user does not have any create permission on one or more column belonging to any of the entity in the
+ /// multiple-create mutation, the request will fail during authorization check.
+ ///
+ [TestMethod]
+ public async Task ValidateAuthZCheckOnColumnsForCreateOneMultipleMutations()
+ {
+ string createOneStockMutationName = "createStock";
+ string createOneStockWithPiecesAvailable = @"mutation {
+ createStock(
+ item:
+ {
+ categoryid: 1,
+ pieceid: 2,
+ categoryName: ""xyz""
+ piecesAvailable: 0,
+ stocks_price:
+ {
+ is_wholesale_price: true
+ instant: ""1996-01-24""
+ }
+ }
+ )
+ {
+ categoryid
+ pieceid
+ }
+ }";
+
+ // The 'test_role_with_excluded_fields_on_create' role does not have create permissions on
+ // stocks.piecesAvailable field and hence the authorization check should fail.
+ await ValidateRequestIsUnauthorized(
+ graphQLMutationName: createOneStockMutationName,
+ graphQLMutation: createOneStockWithPiecesAvailable,
+ expectedExceptionMessage: "Unauthorized due to one or more fields in this mutation.",
+ expectedExceptionStatusCode: DataApiBuilderException.SubStatusCodes.AuthorizationCheckFailed.ToString(),
+ isAuthenticated: true,
+ clientRoleHeader: "test_role_with_excluded_fields_on_create");
+
+ // As soon as we remove the 'piecesAvailable' column from the request body,
+ // the authorization check will pass.
+ string createOneStockWithoutPiecesAvailable = @"mutation {
+ createStock(
+ item:
+ {
+ categoryid: 1,
+ pieceid: 2,
+ categoryName: ""xyz"",
+ stocks_price:
+ {
+ is_wholesale_price: true
+ instant: ""1996-01-24""
+ }
+ }
+ )
+ {
+ categoryid
+ pieceid
+ }
+ }";
+
+ // Since the field stocks.piecesAvailable is not included in the mutation,
+ // the authorization check should pass.
+ await ValidateRequestIsAuthorized(
+ graphQLMutationName: createOneStockMutationName,
+ graphQLMutation: createOneStockWithoutPiecesAvailable,
+ isAuthenticated: true,
+ clientRoleHeader: "test_role_with_excluded_fields_on_create",
+ expectedResult: "");
+
+ // Executing a similar mutation request but with stocks_price as top-level entity.
+ // This validates that the recursive logic to do authorization on fields belonging to related entities
+ // work as expected.
+
+ string createOneStockPriceMutationName = "createstocks_price";
+ string createOneStocksPriceWithPiecesAvailable = @"mutation {
+ createstocks_price(
+ item:
+ {
+ is_wholesale_price: true,
+ instant: ""1996-01-24"",
+ price: 49.6,
+ Stock:
+ {
+ categoryid: 1,
+ pieceid: 2,
+ categoryName: ""xyz""
+ piecesAvailable: 0,
+ }
+ }
+ )
+ {
+ categoryid
+ pieceid
+ }
+ }";
+
+ // The 'test_role_with_excluded_fields_on_create' role does not have create permissions on
+ // stocks.piecesAvailable field and hence the authorization check should fail.
+ await ValidateRequestIsUnauthorized(
+ graphQLMutationName: createOneStockPriceMutationName,
+ graphQLMutation: createOneStocksPriceWithPiecesAvailable,
+ expectedExceptionMessage: "Unauthorized due to one or more fields in this mutation.",
+ expectedExceptionStatusCode: DataApiBuilderException.SubStatusCodes.AuthorizationCheckFailed.ToString(),
+ isAuthenticated: true,
+ clientRoleHeader: "test_role_with_excluded_fields_on_create");
+
+ string createOneStocksPriceWithoutPiecesAvailable = @"mutation {
+ createstocks_price(
+ item:
+ {
+ is_wholesale_price: true,
+ instant: ""1996-01-24"",
+ price: 49.6,
+ Stock:
+ {
+ categoryid: 1,
+ pieceid: 2,
+ categoryName: ""xyz""
+ }
+ }
+ )
+ {
+ categoryid
+ pieceid
+ }
+ }";
+
+ // Since the field stocks.piecesAvailable is not included in the mutation,
+ // the authorization check should pass.
+ await ValidateRequestIsAuthorized(
+ graphQLMutationName: createOneStockMutationName,
+ graphQLMutation: createOneStocksPriceWithoutPiecesAvailable,
+ isAuthenticated: true,
+ clientRoleHeader: "test_role_with_excluded_fields_on_create",
+ expectedResult: "");
+ }
+
+ ///
+ /// Test to validate that a 'create multiple' mutation request can only execute successfully when the user, in addition to having
+ /// create permission for all the entities involved in the create mutation, has the create permission for all the columns
+ /// present for each entity in the mutation.
+ /// If the user does not have any create permission on one or more column belonging to any of the entity in the
+ /// multiple-create mutation, the request will fail during authorization check.
+ ///
+ [TestMethod]
+ public async Task ValidateAuthZCheckOnColumnsForCreateMultipleMutations()
+ {
+ string createMultipleStockMutationName = "createStocks";
+ string createMultipleStocksWithPiecesAvailable = @"mutation {
+ createStocks(
+ items: [
+ {
+ categoryid: 1,
+ pieceid: 2,
+ categoryName: ""xyz""
+ piecesAvailable: 0,
+ stocks_price:
+ {
+ is_wholesale_price: true
+ instant: ""1996-01-24""
+ }
+ }
+ ])
+ {
+ items
+ {
+ categoryid
+ pieceid
+ }
+ }
+ }";
+
+ // The 'test_role_with_excluded_fields_on_create' role does not have create permissions on
+ // stocks.piecesAvailable field and hence the authorization check should fail.
+ await ValidateRequestIsUnauthorized(
+ graphQLMutationName: createMultipleStockMutationName,
+ graphQLMutation: createMultipleStocksWithPiecesAvailable,
+ expectedExceptionMessage: "Unauthorized due to one or more fields in this mutation.",
+ expectedExceptionStatusCode: DataApiBuilderException.SubStatusCodes.AuthorizationCheckFailed.ToString(),
+ isAuthenticated: true,
+ clientRoleHeader: "test_role_with_excluded_fields_on_create");
+
+ // As soon as we remove the 'piecesAvailable' column from the request body,
+ // the authorization check will pass.
+ string createMultipleStocksWithoutPiecesAvailable = @"mutation {
+ createStocks(
+ items: [
+ {
+ categoryid: 1,
+ pieceid: 2,
+ categoryName: ""xyz""
+ piecesAvailable: 0,
+ stocks_price:
+ {
+ is_wholesale_price: true
+ instant: ""1996-01-24""
+ }
+ }
+ ])
+ {
+ items
+ {
+ categoryid
+ pieceid
+ }
+ }
+ }";
+
+ // Since the field stocks.piecesAvailable is not included in the mutation,
+ // the authorization check should pass.
+ await ValidateRequestIsAuthorized(
+ graphQLMutationName: createMultipleStockMutationName,
+ graphQLMutation: createMultipleStocksWithoutPiecesAvailable,
+ isAuthenticated: true,
+ clientRoleHeader: "test_role_with_excluded_fields_on_create",
+ expectedResult: "");
+ }
+
+ #endregion
+
+ #region Test helpers
+ ///
+ /// Helper method to execute and validate response for negative GraphQL requests which expect an authorization failure
+ /// as a result of their execution.
+ ///
+ /// Name of the mutation.
+ /// Request body of the mutation.
+ /// Expected exception message.
+ /// Boolean indicating whether the request should be treated as authenticated or not.
+ /// Value of X-MS-API-ROLE client role header.
+ private async Task ValidateRequestIsUnauthorized(
+ string graphQLMutationName,
+ string graphQLMutation,
+ string expectedExceptionMessage,
+ string expectedExceptionStatusCode = null,
+ bool isAuthenticated = false,
+ string clientRoleHeader = "anonymous")
+ {
+
+ JsonElement actual = await ExecuteGraphQLRequestAsync(
+ query: graphQLMutation,
+ queryName: graphQLMutationName,
+ isAuthenticated: isAuthenticated,
+ variables: null,
+ clientRoleHeader: clientRoleHeader);
+
+ SqlTestHelper.TestForErrorInGraphQLResponse(
+ actual.ToString(),
+ message: expectedExceptionMessage,
+ statusCode: expectedExceptionStatusCode
+ );
+ }
+
+ ///
+ /// Helper method to execute and validate response for positive GraphQL requests which expect a successful execution
+ /// against the database, passing all the Authorization checks en route.
+ ///
+ /// Name of the mutation.
+ /// Request body of the mutation.
+ /// Expected result.
+ /// Boolean indicating whether the request should be treated as authenticated or not.
+ /// Value of X-MS-API-ROLE client role header.
+ private async Task ValidateRequestIsAuthorized(
+ string graphQLMutationName,
+ string graphQLMutation,
+ string expectedResult = "Value cannot be null",
+ bool isAuthenticated = false,
+ string clientRoleHeader = "anonymous")
+ {
+
+ JsonElement actual = await ExecuteGraphQLRequestAsync(
+ query: graphQLMutation,
+ queryName: graphQLMutationName,
+ isAuthenticated: isAuthenticated,
+ variables: null,
+ clientRoleHeader: clientRoleHeader);
+
+ SqlTestHelper.TestForErrorInGraphQLResponse(
+ actual.ToString(),
+ message: expectedResult
+ );
+ }
+
+ #endregion
+ }
+}
diff --git a/src/Service.Tests/Authorization/GraphQL/GraphQLMutationAuthorizationTests.cs b/src/Service.Tests/Authorization/GraphQL/GraphQLMutationAuthorizationTests.cs
index a9355796ac..740684a579 100644
--- a/src/Service.Tests/Authorization/GraphQL/GraphQLMutationAuthorizationTests.cs
+++ b/src/Service.Tests/Authorization/GraphQL/GraphQLMutationAuthorizationTests.cs
@@ -44,13 +44,7 @@ public class GraphQLMutationAuthorizationTests
/// If authorization fails, an exception is thrown and this test validates that scenario.
/// If authorization succeeds, no exceptions are thrown for authorization, and function resolves silently.
///
- ///
- ///
- ///
- ///
[DataTestMethod]
- [DataRow(true, new string[] { "col1", "col2", "col3" }, new string[] { "col1" }, EntityActionOperation.Create, DisplayName = "Create Mutation Field Authorization - Success, Columns Allowed")]
- [DataRow(false, new string[] { "col1", "col2", "col3" }, new string[] { "col1" }, EntityActionOperation.Create, DisplayName = "Create Mutation Field Authorization - Failure, Columns Forbidden")]
[DataRow(true, new string[] { "col1", "col2", "col3" }, new string[] { "col1" }, EntityActionOperation.UpdateGraphQL, DisplayName = "Update Mutation Field Authorization - Success, Columns Allowed")]
[DataRow(false, new string[] { "col1", "col2", "col3" }, new string[] { "col4" }, EntityActionOperation.UpdateGraphQL, DisplayName = "Update Mutation Field Authorization - Failure, Columns Forbidden")]
[DataRow(true, new string[] { "col1", "col2", "col3" }, new string[] { "col1" }, EntityActionOperation.Delete, DisplayName = "Delete Mutation Field Authorization - Success, since authorization to perform the" +
@@ -83,7 +77,7 @@ public void MutationFields_AuthorizationEvaluation(bool isAuthorized, string[] c
bool authorizationResult = false;
try
{
- engine.AuthorizeMutationFields(
+ engine.AuthorizeMutation(
graphQLMiddlewareContext.Object,
parameters,
entityName: TEST_ENTITY,
@@ -105,7 +99,6 @@ public void MutationFields_AuthorizationEvaluation(bool isAuthorized, string[] c
/// Sets up test fixture for class, only to be run once per test run, as defined by
/// MSTest decorator.
///
- ///
private static SqlMutationEngine SetupTestFixture(bool isAuthorized)
{
Mock _queryEngine = new();
diff --git a/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForMsSql.verified.txt b/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForMsSql.verified.txt
index 349c8f4343..387450789c 100644
--- a/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForMsSql.verified.txt
+++ b/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForMsSql.verified.txt
@@ -301,6 +301,28 @@
}
]
},
+ {
+ Role: test_role_with_excluded_fields_on_create,
+ Actions: [
+ {
+ Action: Create,
+ Fields: {
+ Exclude: [
+ piecesAvailable
+ ]
+ }
+ },
+ {
+ Action: Read
+ },
+ {
+ Action: Update
+ },
+ {
+ Action: Delete
+ }
+ ]
+ },
{
Role: TestNestedFilterFieldIsNull_ColumnForbidden,
Actions: [
@@ -1303,8 +1325,30 @@
Action: Create
}
]
+ },
+ {
+ Role: test_role_with_excluded_fields_on_create,
+ Actions: [
+ {
+ Action: Create
+ },
+ {
+ Action: Read
+ },
+ {
+ Action: Update
+ },
+ {
+ Action: Delete
+ }
+ ]
}
- ]
+ ],
+ Relationships: {
+ Stock: {
+ TargetEntity: Stock
+ }
+ }
}
},
{
diff --git a/src/Service.Tests/dab-config.MsSql.json b/src/Service.Tests/dab-config.MsSql.json
index be8a96d2c3..1743052c28 100644
--- a/src/Service.Tests/dab-config.MsSql.json
+++ b/src/Service.Tests/dab-config.MsSql.json
@@ -324,6 +324,28 @@
}
]
},
+ {
+ "role": "test_role_with_excluded_fields_on_create",
+ "actions": [
+ {
+ "action": "create",
+ "fields": {
+ "exclude": [
+ "piecesAvailable"
+ ]
+ }
+ },
+ {
+ "action": "read"
+ },
+ {
+ "action": "update"
+ },
+ {
+ "action": "delete"
+ }
+ ]
+ },
{
"role": "TestNestedFilterFieldIsNull_ColumnForbidden",
"actions": [
@@ -396,6 +418,28 @@
}
]
},
+ {
+ "role": "test_role_with_excluded_fields_on_create",
+ "actions": [
+ {
+ "action": "create",
+ "fields": {
+ "exclude": [
+ "piecesAvailable"
+ ]
+ }
+ },
+ {
+ "action": "read"
+ },
+ {
+ "action": "update"
+ },
+ {
+ "action": "delete"
+ }
+ ]
+ },
{
"role": "test_role_with_policy_excluded_fields",
"actions": [
@@ -1385,8 +1429,52 @@
"action": "create"
}
]
+ },
+ {
+ "role": "test_role_with_excluded_fields_on_create",
+ "actions": [
+ {
+ "action": "create"
+ },
+ {
+ "action": "read"
+ },
+ {
+ "action": "update"
+ },
+ {
+ "action": "delete"
+ }
+ ]
+ },
+ {
+ "role": "test_role_with_excluded_fields_on_create",
+ "actions": [
+ {
+ "action": "create"
+ },
+ {
+ "action": "read"
+ },
+ {
+ "action": "update"
+ },
+ {
+ "action": "delete"
+ }
+ ]
}
- ]
+ ],
+ "relationships": {
+ "Stock": {
+ "cardinality": "one",
+ "target.entity": "Stock",
+ "source.fields": [],
+ "target.fields": [],
+ "linking.source.fields": [],
+ "linking.target.fields": []
+ }
+ }
},
"Tree": {
"source": {