diff --git a/DataGateway.Auth/AuthorizationMetadataHelpers.cs b/DataGateway.Auth/AuthorizationMetadataHelpers.cs index d12ccc5a80..b7873bd70d 100644 --- a/DataGateway.Auth/AuthorizationMetadataHelpers.cs +++ b/DataGateway.Auth/AuthorizationMetadataHelpers.cs @@ -19,7 +19,7 @@ public class EntityMetadata /// Key(field): id -> Value(collection): permitted in {Role1, Role2, ..., RoleN} /// Key(field): title -> Value(collection): permitted in {Role1} /// - public Dictionary>> FieldToRolesMap { get; set; } = new(); + public Dictionary>> FieldToRolesMap { get; set; } = new(); /// /// Given the key (actionName) returns a collection of roles diff --git a/DataGateway.Auth/IAuthorizationResolver.cs b/DataGateway.Auth/IAuthorizationResolver.cs index 0e9786399e..934a84de7e 100644 --- a/DataGateway.Auth/IAuthorizationResolver.cs +++ b/DataGateway.Auth/IAuthorizationResolver.cs @@ -79,10 +79,10 @@ public interface IAuthorizationResolver /// Applicable to GraphQL field directive @authorize on ObjectType fields. /// /// EntityName whose actionMetadata will be searched. - /// ActionName to lookup field permissions - /// Specific field to get collection of roles - /// Collection of role names allowed to perform actionType on Entity's field. - public IEnumerable GetRolesForField(string entityName, string actionName, string field); + /// Field to lookup action permissions + /// Specific action to get collection of roles + /// Collection of role names allowed to perform actionName on Entity's field. + public IEnumerable GetRolesForField(string entityName, string field, string actionName); /// /// Returns a list of roles which define permissions for the provided action. diff --git a/DataGateway.Service.GraphQLBuilder/GraphQLUtils.cs b/DataGateway.Service.GraphQLBuilder/GraphQLUtils.cs index 1c17876cec..4136c7dff8 100644 --- a/DataGateway.Service.GraphQLBuilder/GraphQLUtils.cs +++ b/DataGateway.Service.GraphQLBuilder/GraphQLUtils.cs @@ -15,6 +15,7 @@ public static class GraphQLUtils public const string AUTHORIZE_DIRECTIVE_ARGUMENT_ROLES = "roles"; public const string OBJECT_TYPE_MUTATION = "mutation"; public const string OBJECT_TYPE_QUERY = "query"; + public const string SYSTEM_ROLE_ANONYMOUS = "anonymous"; public static bool IsModelType(ObjectTypeDefinitionNode objectTypeDefinitionNode) { diff --git a/DataGateway.Service.GraphQLBuilder/Sql/SchemaConverter.cs b/DataGateway.Service.GraphQLBuilder/Sql/SchemaConverter.cs index a9ea8b56dc..0897799417 100644 --- a/DataGateway.Service.GraphQLBuilder/Sql/SchemaConverter.cs +++ b/DataGateway.Service.GraphQLBuilder/Sql/SchemaConverter.cs @@ -20,13 +20,18 @@ public static class SchemaConverter /// Name of the entity in the runtime config to generate the GraphQL object type for. /// SQL table definition information. /// Runtime config information for the table. + /// Key/Value Collection mapping entity name to the entity object, + /// currently used to lookup relationship metadata. + /// Roles to add to authorize directive at the object level (applies to query/read ops). + /// Roles to add to authorize directive at the field level (applies to mutations). /// A GraphQL object type to be provided to a Hot Chocolate GraphQL document. public static ObjectTypeDefinitionNode FromTableDefinition( string entityName, TableDefinition tableDefinition, [NotNull] Entity configEntity, Dictionary entities, - IEnumerable? rolesAllowedForEntity = null) + IEnumerable rolesAllowedForEntity, + IDictionary> rolesAllowedForFields) { Dictionary fields = new(); List objectTypeDirectives = new(); @@ -70,16 +75,32 @@ public static ObjectTypeDefinitionNode FromTableDefinition( directives.Add(new DirectiveNode(DefaultValueDirectiveType.DirectiveName, new ArgumentNode("value", arg))); } - NamedTypeNode fieldType = new(GetGraphQLTypeForColumnType(column.SystemType)); - FieldDefinitionNode field = new( - location: null, - new(FormatNameForField(columnName)), - description: null, - new List(), - column.IsNullable ? fieldType : new NonNullTypeNode(fieldType), - directives); - - fields.Add(columnName, field); + // If no roles are allowed for the field, we should not include it in the schema. + // Consequently, the field is only added to schema if this conditional evaluates to TRUE. + if (rolesAllowedForFields.TryGetValue(key: columnName, out IEnumerable? roles)) + { + // Roles will not be null here if TryGetValue evaluates to true, so here we check if there are any roles to process. + if (roles.Count() > 0) + { + // Add field to object definition but do not add @authorize directive + // if anonymous is defined for field since authentication is not required. + if (!roles.Contains(GraphQLUtils.SYSTEM_ROLE_ANONYMOUS)) + { + directives.Add(GraphQLUtils.CreateAuthorizationDirective(roles)); + } + + NamedTypeNode fieldType = new(GetGraphQLTypeForColumnType(column.SystemType)); + FieldDefinitionNode field = new( + location: null, + new(FormatNameForField(columnName)), + description: null, + new List(), + column.IsNullable ? fieldType : new NonNullTypeNode(fieldType), + directives); + + fields.Add(columnName, field); + } + } } if (configEntity.Relationships is not null) @@ -125,7 +146,7 @@ public static ObjectTypeDefinitionNode FromTableDefinition( // Any roles passed in will be added to the authorize directive for this ObjectType // taking the form: @authorize(roles: [“role1”, ..., “roleN”]) - if (rolesAllowedForEntity is not null) + if (rolesAllowedForEntity.Count() >= 1) { objectTypeDirectives.Add(GraphQLUtils.CreateAuthorizationDirective(rolesAllowedForEntity)); } diff --git a/DataGateway.Service.Tests/GraphQLBuilder/Sql/SchemaConverterTests.cs b/DataGateway.Service.Tests/GraphQLBuilder/Sql/SchemaConverterTests.cs index 638aba991a..550bd596eb 100644 --- a/DataGateway.Service.Tests/GraphQLBuilder/Sql/SchemaConverterTests.cs +++ b/DataGateway.Service.Tests/GraphQLBuilder/Sql/SchemaConverterTests.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using Azure.DataGateway.Config; +using Azure.DataGateway.Service.GraphQLBuilder; using Azure.DataGateway.Service.GraphQLBuilder.Directives; using Azure.DataGateway.Service.GraphQLBuilder.Queries; using Azure.DataGateway.Service.GraphQLBuilder.Sql; @@ -40,7 +41,14 @@ public void EntityNameBecomesObjectName(string entityName, string expected) { TableDefinition table = new(); - ObjectTypeDefinitionNode od = SchemaConverter.FromTableDefinition(entityName, table, GenerateEmptyEntity(), new()); + ObjectTypeDefinitionNode od = SchemaConverter.FromTableDefinition( + entityName, + table, + GenerateEmptyEntity(), + new(), + rolesAllowedForEntity: GetRolesAllowedForEntity(), + rolesAllowedForFields: GetFieldToRolesMap() + ); Assert.AreEqual(expected, od.Name.Value); } @@ -65,7 +73,14 @@ public void ColumnNameBecomesFieldName(string columnName, string expected) SystemType = typeof(string) }); - ObjectTypeDefinitionNode od = SchemaConverter.FromTableDefinition("table", table, GenerateEmptyEntity(), new()); + ObjectTypeDefinitionNode od = SchemaConverter.FromTableDefinition( + "table", + table, + GenerateEmptyEntity(), + new(), + rolesAllowedForEntity: GetRolesAllowedForEntity(), + rolesAllowedForFields: GetFieldToRolesMap(columnName: table.Columns.First().Key) + ); Assert.AreEqual(expected, od.Fields[0].Name.Value); } @@ -82,10 +97,18 @@ public void PrimaryKeyColumnHasAppropriateDirective() }); table.PrimaryKey.Add(columnName); - ObjectTypeDefinitionNode od = SchemaConverter.FromTableDefinition("table", table, GenerateEmptyEntity(), new()); + ObjectTypeDefinitionNode od = SchemaConverter.FromTableDefinition( + "table", + table, + GenerateEmptyEntity(), + new(), + rolesAllowedForEntity: GetRolesAllowedForEntity(), + rolesAllowedForFields: GetFieldToRolesMap() + ); FieldDefinitionNode field = od.Fields.First(f => f.Name.Value == columnName); - Assert.AreEqual(1, field.Directives.Count); + // Authorization directive implicitly created so actual count should be 1 + {expected number of directives}. + Assert.AreEqual(2, field.Directives.Count); Assert.AreEqual(PrimaryKeyDirectiveType.DirectiveName, field.Directives[0].Name.Value); } @@ -101,7 +124,14 @@ public void MultiplePrimaryKeysAllMappedWithDirectives() table.PrimaryKey.Add(columnName); } - ObjectTypeDefinitionNode od = SchemaConverter.FromTableDefinition("table", table, GenerateEmptyEntity(), new()); + ObjectTypeDefinitionNode od = SchemaConverter.FromTableDefinition( + "table", + table, + GenerateEmptyEntity(), + new(), + rolesAllowedForEntity: GetRolesAllowedForEntity(), + rolesAllowedForFields: GetFieldToRolesMap() + ); foreach (FieldDefinitionNode field in od.Fields) { @@ -113,14 +143,23 @@ public void MultiplePrimaryKeysAllMappedWithDirectives() [TestMethod] public void MultipleColumnsAllMapped() { + int customColumnCount = 5; + TableDefinition table = new(); - for (int i = 0; i < 5; i++) + for (int i = 0; i < customColumnCount; i++) { table.Columns.Add($"col{i}", new ColumnDefinition { SystemType = typeof(string) }); } - ObjectTypeDefinitionNode od = SchemaConverter.FromTableDefinition("table", table, GenerateEmptyEntity(), new()); + ObjectTypeDefinitionNode od = SchemaConverter.FromTableDefinition( + "table", + table, + GenerateEmptyEntity(), + new(), + rolesAllowedForEntity: GetRolesAllowedForEntity(), + rolesAllowedForFields: GetFieldToRolesMap(additionalColumns: customColumnCount) + ); Assert.AreEqual(table.Columns.Count, od.Fields.Count); } @@ -147,7 +186,14 @@ public void SystemTypeMapsToCorrectGraphQLType(Type systemType, string graphQLTy SystemType = systemType }); - ObjectTypeDefinitionNode od = SchemaConverter.FromTableDefinition("table", table, GenerateEmptyEntity(), new()); + ObjectTypeDefinitionNode od = SchemaConverter.FromTableDefinition( + "table", + table, + GenerateEmptyEntity(), + new(), + rolesAllowedForEntity: GetRolesAllowedForEntity(), + rolesAllowedForFields: GetFieldToRolesMap() + ); FieldDefinitionNode field = od.Fields.First(f => f.Name.Value == columnName); Assert.AreEqual(graphQLType, field.Type.NamedType().Name.Value); @@ -165,7 +211,14 @@ public void NullColumnBecomesNullField() IsNullable = true, }); - ObjectTypeDefinitionNode od = SchemaConverter.FromTableDefinition("table", table, GenerateEmptyEntity(), new()); + ObjectTypeDefinitionNode od = SchemaConverter.FromTableDefinition( + "table", + table, + GenerateEmptyEntity(), + new(), + rolesAllowedForEntity: GetRolesAllowedForEntity(), + rolesAllowedForFields: GetFieldToRolesMap() + ); FieldDefinitionNode field = od.Fields.First(f => f.Name.Value == columnName); Assert.IsFalse(field.Type.IsNonNullType()); @@ -183,7 +236,14 @@ public void NonNullColumnBecomesNonNullField() IsNullable = false, }); - ObjectTypeDefinitionNode od = SchemaConverter.FromTableDefinition("table", table, GenerateEmptyEntity(), new()); + ObjectTypeDefinitionNode od = SchemaConverter.FromTableDefinition( + "table", + table, + GenerateEmptyEntity(), + new(), + rolesAllowedForEntity: GetRolesAllowedForEntity(), + rolesAllowedForFields: GetFieldToRolesMap() + ); FieldDefinitionNode field = od.Fields.First(f => f.Name.Value == columnName); Assert.IsTrue(field.Type.IsNonNullType()); @@ -236,7 +296,13 @@ public void WhenForeignKeyDefinedButNoRelationship_GraphQLWontModelIt() ObjectTypeDefinitionNode od = SchemaConverter.FromTableDefinition( - SOURCE_ENTITY, table, configEntity, new() { { TARGET_ENTITY, relationshipEntity } }); + SOURCE_ENTITY, + table, + configEntity, + new() { { TARGET_ENTITY, relationshipEntity } }, + rolesAllowedForEntity: GetRolesAllowedForEntity(), + rolesAllowedForFields: GetFieldToRolesMap() + ); Assert.AreEqual(2, od.Fields.Count); } @@ -250,11 +316,23 @@ public void SingularNamingRulesDeterminedByRuntimeConfig(string entityName, stri TableDefinition table = new(); Entity configEntity = GenerateEmptyEntity() with { GraphQL = new SingularPlural(singular, null) }; - ObjectTypeDefinitionNode od = SchemaConverter.FromTableDefinition(entityName, table, configEntity, new()); + ObjectTypeDefinitionNode od = SchemaConverter.FromTableDefinition( + entityName, + table, + configEntity, + new(), + rolesAllowedForEntity: GetRolesAllowedForEntity(), + rolesAllowedForFields: GetFieldToRolesMap() + ); Assert.AreEqual(expected, od.Name.Value); } + /// + /// When schema ObjectTypeDefinition is created, + /// its fields contain the @authorize directive + /// when rolesAllowedForFields() returns a role list + /// [TestMethod] public void AutoGeneratedFieldHasDirectiveIndicatingSuch() { @@ -268,7 +346,14 @@ public void AutoGeneratedFieldHasDirectiveIndicatingSuch() }); Entity configEntity = GenerateEmptyEntity(); - ObjectTypeDefinitionNode od = SchemaConverter.FromTableDefinition("entity", table, configEntity, new()); + ObjectTypeDefinitionNode od = SchemaConverter.FromTableDefinition( + "entity", + table, + configEntity, + new(), + rolesAllowedForEntity: GetRolesAllowedForEntity(), + rolesAllowedForFields: GetFieldToRolesMap() + ); Assert.IsTrue(od.Fields[0].Directives.Any(d => d.Name.Value == AutoGeneratedDirectiveType.DirectiveName)); } @@ -310,15 +395,152 @@ public void DefaultValueGetsSetOnDirective(object defaultValue, string fieldName }); Entity configEntity = GenerateEmptyEntity(); - ObjectTypeDefinitionNode od = SchemaConverter.FromTableDefinition("entity", table, configEntity, new()); - - Assert.AreEqual(1, od.Fields[0].Directives.Count); + ObjectTypeDefinitionNode od = SchemaConverter.FromTableDefinition( + "entity", + table, + configEntity, + new(), + rolesAllowedForEntity: GetRolesAllowedForEntity(), + rolesAllowedForFields: GetFieldToRolesMap() + ); + + // @authorize directive is implicitly created so the count to compare to is 2 + Assert.AreEqual(2, od.Fields[0].Directives.Count); DirectiveNode directive = od.Fields[0].Directives[0]; ObjectValueNode value = (ObjectValueNode)directive.Arguments[0].Value; Assert.AreEqual(fieldName, value.Fields[0].Name.Value); Assert.AreEqual(kind, value.Fields[0].Value.Kind); } + /// + /// Tests that each field on an ObjectTypeDefinition includes + /// the expected @authorize directive. + /// Given a the anonymous role provided by GetFieldToRolesMap() + /// id - {anonymous, authenticated, role3, roleN} + /// title - {authenticated} + /// field3 - {role3, roleN} + /// Adds directive @authorize(roles=[role1, role2, role3]). + /// + [DataTestMethod] + [DataRow(new string[] { "authenticated" }, DisplayName = "One non-anonymous system role (authenticated) defined for field, @authorize directive added.")] + [DataRow(new string[] { "authenticated", "role1" }, DisplayName = "Mixed role types (non-anonymous) roles defined for field, @authorize directive added.")] + [DataRow(new string[] { "role1", "role2", "role3" }, DisplayName = "Multiple non-system roles defined for field, @authorize directive added.")] + public void AutoGeneratedFieldHasAuthorizeDirective(string[] rolesForField) + { + TableDefinition table = new(); + string columnName = "columnName"; + table.Columns.Add(columnName, new ColumnDefinition + { + SystemType = typeof(string), + IsNullable = false, + IsAutoGenerated = true, + }); + + Entity configEntity = GenerateEmptyEntity(); + ObjectTypeDefinitionNode od = SchemaConverter.FromTableDefinition( + "entity", + table, + configEntity, + new(), + rolesAllowedForEntity: GetRolesAllowedForEntity(), + rolesAllowedForFields: GetFieldToRolesMap(rolesForField: rolesForField) + ); + + // Ensures all fields added have the appropriate @authorize directive. + Assert.IsTrue(od.Fields.All(field => field.Directives.Any(d => d.Name.Value == GraphQLUtils.AUTHORIZE_DIRECTIVE))); + } + + /// + /// Tests that each field on an ObjectTypeDefinition does not include + /// an @authorize directive. + /// Given a set of roles provided by GetFieldToRolesMap() + /// id - {anonymous, authenticated, role3, roleN} + /// title - {authenticated} + /// field3 - {role3, roleN} + /// Adds directive @authorize(roles=[role1, role2, role3]). + /// + [DataTestMethod] + [DataRow(new string[] { "anonymous" }, DisplayName = "Anonymous is only role for field")] + [DataRow(new string[] { "anonymous", "Role1" }, DisplayName = "Anonymous is 1 of many roles for field")] + [DataRow(new string[] { "authenticated", "anonymous" }, DisplayName = "Anonymous and authenticated are present and randomly ordered, anonymous wins.")] + public void FieldWithAnonymousAccessHasNoAuthorizeDirective(string[] rolesForField) + { + TableDefinition table = new(); + string columnName = "columnName"; + table.Columns.Add(columnName, new ColumnDefinition + { + SystemType = typeof(string), + IsNullable = false, + IsAutoGenerated = true, + }); + + Entity configEntity = GenerateEmptyEntity(); + ObjectTypeDefinitionNode od = SchemaConverter.FromTableDefinition( + "entity", + table, + configEntity, + new(), + rolesAllowedForEntity: GetRolesAllowedForEntity(), + rolesAllowedForFields: GetFieldToRolesMap(rolesForField: rolesForField) + ); + + // Ensures no field has the @authorize directive. + Assert.IsFalse(od.Fields.All(field => field.Directives.Any(d => d.Name.Value == GraphQLUtils.AUTHORIZE_DIRECTIVE)), + message: "@authorize directive must not be present for field with anonymous access permissions."); + } + + /// + /// Mocks a list of roles for the Schema Converter Tests + /// + /// Collection of roles + private static IEnumerable GetRolesAllowedForEntity() + { + return new List() + { + "authenticated" + }; + } + + /// + /// Mocks FieldToRoleMap for Schema Converter Tests + /// For tests that require arbitrary number of columns, + /// the additionalColumns argument should be used to define + /// the desired number of columns. + /// Default is 0 and results in the two constant fields created that are + /// relevant to most tests in SchemaConverterTests. + /// + /// number of columns/fields to generate + /// custom column name + /// Key Value Map of Field to Roles + private static IDictionary> GetFieldToRolesMap(int additionalColumns = 0, string columnName = "", IEnumerable rolesForField = null) + { + Dictionary> fieldToRolesMap = new(); + + if (rolesForField is null) + { + rolesForField = GetRolesAllowedForEntity(); + } + + if (additionalColumns != 0) + { + for (int columnNumber = 0; columnNumber < additionalColumns; columnNumber++) + { + fieldToRolesMap.Add("col" + columnNumber.ToString(), rolesForField); + } + } + else if (!string.IsNullOrEmpty(columnName)) + { + fieldToRolesMap.Add(columnName, rolesForField); + } + else + { + fieldToRolesMap.Add(COLUMN_NAME, rolesForField); + fieldToRolesMap.Add(REF_COLNAME, rolesForField); + } + + return fieldToRolesMap; + } + private static Entity GenerateEmptyEntity() { return new Entity("dbo.entity", Rest: null, GraphQL: null, Array.Empty(), Relationships: new(), Mappings: new()); @@ -346,10 +568,13 @@ private static ObjectTypeDefinitionNode GenerateObjectWithRelationship(Cardinali Entity configEntity = GenerateEmptyEntity() with { Relationships = relationships }; Entity relationshipEntity = GenerateEmptyEntity(); - return SchemaConverter.FromTableDefinition - (SOURCE_ENTITY, - table, - configEntity, new() { { TARGET_ENTITY, relationshipEntity } }); + return SchemaConverter.FromTableDefinition( + SOURCE_ENTITY, + table, + configEntity, new() { { TARGET_ENTITY, relationshipEntity } }, + rolesAllowedForEntity: GetRolesAllowedForEntity(), + rolesAllowedForFields: GetFieldToRolesMap() + ); } private static TableDefinition GenerateTableWithForeignKeyDefinition() diff --git a/DataGateway.Service/Authorization/AuthorizationResolver.cs b/DataGateway.Service/Authorization/AuthorizationResolver.cs index f02a63d3da..da10434c92 100644 --- a/DataGateway.Service/Authorization/AuthorizationResolver.cs +++ b/DataGateway.Service/Authorization/AuthorizationResolver.cs @@ -9,6 +9,7 @@ using Azure.DataGateway.Config; using Azure.DataGateway.Service.Configurations; using Azure.DataGateway.Service.Exceptions; +using Azure.DataGateway.Service.Models; using Azure.DataGateway.Service.Services; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Primitives; @@ -230,6 +231,9 @@ public void SetEntityPermissionMap(RuntimeConfig? runtimeConfig) string actionName = string.Empty; ActionMetadata actionToColumn = new(); IEnumerable allTableColumns = ResolveTableDefinitionColumns(entityName); + + // Implicitly, all table columns are 'allowed' when an actiontype is a string. + // Since no granular field permissions exist for this action within the current role. if (actionElement.ValueKind is JsonValueKind.String) { actionName = actionElement.ToString(); @@ -291,6 +295,12 @@ public void SetEntityPermissionMap(RuntimeConfig? runtimeConfig) entityToRoleMap.ActionToRolesMap[actionName].Add(role); } + foreach (string allowedColumn in actionToColumn.Allowed) + { + entityToRoleMap.FieldToRolesMap.TryAdd(key: allowedColumn, CreateActionToRoleMap()); + entityToRoleMap.FieldToRolesMap[allowedColumn][actionName].Add(role); + } + roleToAction.ActionToColumnMap[actionName] = actionToColumn; } @@ -492,12 +502,12 @@ public IEnumerable GetRolesForAction(string entityName, string actionNam /// Applicable to GraphQL field directive @authorize on ObjectType fields. /// /// EntityName whose actionMetadata will be searched. - /// ActionName to lookup field permissions - /// Specific field to get collection of roles + /// Field to lookup action permissions + /// Specific action to get collection of roles /// Collection of role names allowed to perform actionName on Entity's field. - public IEnumerable GetRolesForField(string entityName, string actionName, string field) + public IEnumerable GetRolesForField(string entityName, string field, string actionName) { - return EntityPermissionsMap[entityName].FieldToRolesMap[actionName][field]; + return EntityPermissionsMap[entityName].FieldToRolesMap[field][actionName]; } /// @@ -515,6 +525,25 @@ private IEnumerable ResolveTableDefinitionColumns(string entityName) return _metadataProvider.GetTableDefinition(entityName).Columns.Keys; } + + /// + /// Creates new key value map of + /// Key: ActionType + /// Value: Collection of role names. + /// There are only four possible actions + /// + /// + private static Dictionary> CreateActionToRoleMap() + { + return new Dictionary>() + { + { ActionType.CREATE, new List()}, + { ActionType.READ, new List()}, + { ActionType.UPDATE, new List()}, + { ActionType.DELETE, new List()} + }; + } + #endregion } } diff --git a/DataGateway.Service/Services/GraphQLService.cs b/DataGateway.Service/Services/GraphQLService.cs index bd9bb01d34..dd01f819a7 100644 --- a/DataGateway.Service/Services/GraphQLService.cs +++ b/DataGateway.Service/Services/GraphQLService.cs @@ -13,6 +13,7 @@ using Azure.DataGateway.Service.GraphQLBuilder.Mutations; using Azure.DataGateway.Service.GraphQLBuilder.Queries; using Azure.DataGateway.Service.GraphQLBuilder.Sql; +using Azure.DataGateway.Service.Models; using Azure.DataGateway.Service.Resolvers; using Azure.DataGateway.Service.Services.MetadataProviders; using HotChocolate; @@ -173,6 +174,13 @@ DatabaseType.postgresql or Parse(root, inputTypes); } + /// + /// Generates the ObjectTypeDefinitionNodes and InputObjectTypeDefinitionNodes as part of GraphQL Schema generation + /// with the provided entities listed in the runtime configuration. + /// + /// Key/Value Collection {entityName -> Entity object} + /// Root GraphQLSchema DocumentNode and inputNodes to be processed by downstream schema generation helpers. + /// private (DocumentNode, Dictionary) GenerateSqlGraphQLObjects(Dictionary entities) { Dictionary objectTypes = new(); @@ -191,8 +199,30 @@ DatabaseType.postgresql or // Collection of role names allowed to access entity, to be added to the authorize directive // of the objectTypeDefinitionNode. The authorize Directive is one of many directives created. IEnumerable rolesAllowedForEntity = _authorizationResolver.GetRolesForEntity(entityName); + Dictionary> rolesAllowedForFields = new(); + foreach (string column in tableDefinition.Columns.Keys) + { + IEnumerable roles = _authorizationResolver.GetRolesForField(entityName, field: column, actionName: ActionType.READ); + if (!rolesAllowedForFields.TryAdd(key: column, value: roles)) + { + throw new DataGatewayException( + message: "Column already processed for building ObjectTypeDefinition authorization definition.", + statusCode: System.Net.HttpStatusCode.InternalServerError, + subStatusCode: DataGatewayException.SubStatusCodes.ErrorInInitialization + ); + } + } + + // The roles allowed for Fields are the roles allowed to READ the fields, so any role that has a read definition for the field. + ObjectTypeDefinitionNode node = SchemaConverter.FromTableDefinition( + entityName, + tableDefinition, + entity, + entities, + rolesAllowedForEntity, + rolesAllowedForFields + ); - ObjectTypeDefinitionNode node = SchemaConverter.FromTableDefinition(entityName, tableDefinition, entity, entities, rolesAllowedForEntity); InputTypeBuilder.GenerateInputTypesForObjectType(node, inputObjects); objectTypes.Add(entityName, node); }