diff --git a/Azure.DataGateway.Service.sln b/Azure.DataGateway.Service.sln index 2aa6d8eab6..c87729b8f8 100644 --- a/Azure.DataGateway.Service.sln +++ b/Azure.DataGateway.Service.sln @@ -25,6 +25,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Azure.DataGateway.Config", EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Azure.DataGateway.Service.GraphQLBuilder", "DataGateway.Service.GraphQLBuilder\Azure.DataGateway.Service.GraphQLBuilder.csproj", "{E0B51C8F-493D-4C69-8B27-C114D3874176}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Azure.DataGateway.Auth", "DataGateway.Auth\Azure.DataGateway.Auth.csproj", "{249FF898-AD6E-46F2-B441-F6926BCD5179}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -47,6 +49,10 @@ Global {E0B51C8F-493D-4C69-8B27-C114D3874176}.Debug|Any CPU.Build.0 = Debug|Any CPU {E0B51C8F-493D-4C69-8B27-C114D3874176}.Release|Any CPU.ActiveCfg = Release|Any CPU {E0B51C8F-493D-4C69-8B27-C114D3874176}.Release|Any CPU.Build.0 = Release|Any CPU + {249FF898-AD6E-46F2-B441-F6926BCD5179}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {249FF898-AD6E-46F2-B441-F6926BCD5179}.Debug|Any CPU.Build.0 = Debug|Any CPU + {249FF898-AD6E-46F2-B441-F6926BCD5179}.Release|Any CPU.ActiveCfg = Release|Any CPU + {249FF898-AD6E-46F2-B441-F6926BCD5179}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/DataGateway.Auth/AuthorizationMetadataHelpers.cs b/DataGateway.Auth/AuthorizationMetadataHelpers.cs new file mode 100644 index 0000000000..d12ccc5a80 --- /dev/null +++ b/DataGateway.Auth/AuthorizationMetadataHelpers.cs @@ -0,0 +1,57 @@ +namespace Azure.DataGateway.Auth +{ + /// + /// Represents the permission metadata of an entity. + /// An entity's top-level permission structure is a collection + /// of roles. + /// + public class EntityMetadata + { + /// + /// Given the key (roleName) returns the associated RoleMetadata object. + /// To retrieve all roles associated with an entity -> RoleToActionMap.Keys() + /// + public Dictionary RoleToActionMap { get; set; } = new(); + + /// + /// Given the key (actionName) returns a key/value collection of fieldName to Roles + /// i.e. READ action + /// Key(field): id -> Value(collection): permitted in {Role1, Role2, ..., RoleN} + /// Key(field): title -> Value(collection): permitted in {Role1} + /// + public Dictionary>> FieldToRolesMap { get; set; } = new(); + + /// + /// Given the key (actionName) returns a collection of roles + /// defining config permissions for the action. + /// i.e. READ action is permitted in {Role1, Role2, ..., RoleN} + /// + public Dictionary> ActionToRolesMap { get; set; } = new(); + } + + /// + /// Represents the permission metadata of a role + /// A role's top-level permission structure is a collection of + /// actions allowed for that role: Create, Read, Update, Delete, * (wildcard) + /// + public class RoleMetadata + { + /// + /// Given the key (actionName) returns the associated ActionMetadata object. + /// + public Dictionary ActionToColumnMap { get; set; } = new(); + } + + /// + /// Represents the permission metadata of an action + /// An action lists both columns that are included and/or exluded + /// for that action. + /// + public class ActionMetadata + { + public string? DatabasePolicy { get; set; } + public HashSet Included { get; set; } = new(); + public HashSet Excluded { get; set; } = new(); + public HashSet Allowed { get; set; } = new(); + } +} diff --git a/DataGateway.Auth/Azure.DataGateway.Auth.csproj b/DataGateway.Auth/Azure.DataGateway.Auth.csproj new file mode 100644 index 0000000000..2cc75b2654 --- /dev/null +++ b/DataGateway.Auth/Azure.DataGateway.Auth.csproj @@ -0,0 +1,13 @@ + + + + net6.0 + enable + enable + + + + + + + diff --git a/DataGateway.Service/Authorization/IAuthorizationResolver.cs b/DataGateway.Auth/IAuthorizationResolver.cs similarity index 73% rename from DataGateway.Service/Authorization/IAuthorizationResolver.cs rename to DataGateway.Auth/IAuthorizationResolver.cs index ed69f2d182..5eb2ecb644 100644 --- a/DataGateway.Service/Authorization/IAuthorizationResolver.cs +++ b/DataGateway.Auth/IAuthorizationResolver.cs @@ -1,7 +1,6 @@ -using System.Collections.Generic; using Microsoft.AspNetCore.Http; -namespace Azure.DataGateway.Service.Authorization +namespace Azure.DataGateway.Auth { /// /// Interface for authorization decision-making. Each method performs lookups within a @@ -9,6 +8,11 @@ namespace Azure.DataGateway.Service.Authorization /// public interface IAuthorizationResolver { + /// + /// Representation of authorization permissions for each entity in the runtime config. + /// + public Dictionary EntityPermissionsMap { get; } + /// /// Checks for the existence of the client role header in httpContext.Request.Headers /// and evaluates that header against the authenticated (httpContext.User)'s roles @@ -61,5 +65,27 @@ public interface IAuthorizationResolver /// Contains token claims of the authenticated user used in policy evaluation. /// Returns the parsed policy, if successfully processed, or an exception otherwise. public string TryProcessDBPolicy(string entityName, string roleName, string action, HttpContext httpContext); + + /// + /// Returns a list of roles which define permissions for the provided action. + /// i.e. list of roles which allow the action "read" on entityName. + /// + /// Entity to lookup permissions + /// Action to lookup applicable roles + /// Collection of roles. Empty list if entityPermissionsMap is null. + public static IEnumerable GetRolesForAction(string entityName, string actionName, Dictionary? entityPermissionsMap) + { + if (entityName is null) + { + throw new ArgumentNullException(paramName: "entityName"); + } + + if (entityPermissionsMap is not null && entityPermissionsMap[entityName].ActionToRolesMap.TryGetValue(actionName, out List? roleList) && roleList is not null) + { + return roleList; + } + + return new List(); + } } } diff --git a/DataGateway.Service.Tests/Authorization/REST/RestAuthorizationHandlerUnitTests.cs b/DataGateway.Service.Tests/Authorization/REST/RestAuthorizationHandlerUnitTests.cs index 461a51e201..501e2855e2 100644 --- a/DataGateway.Service.Tests/Authorization/REST/RestAuthorizationHandlerUnitTests.cs +++ b/DataGateway.Service.Tests/Authorization/REST/RestAuthorizationHandlerUnitTests.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Security.Claims; using System.Threading.Tasks; +using Azure.DataGateway.Auth; using Azure.DataGateway.Config; using Azure.DataGateway.Service.Authorization; using Azure.DataGateway.Service.Exceptions; diff --git a/DataGateway.Service.Tests/SqlTests/SqlTestBase.cs b/DataGateway.Service.Tests/SqlTests/SqlTestBase.cs index 06a0641588..8326c48784 100644 --- a/DataGateway.Service.Tests/SqlTests/SqlTestBase.cs +++ b/DataGateway.Service.Tests/SqlTests/SqlTestBase.cs @@ -9,6 +9,7 @@ using System.Text.Json; using System.Threading.Tasks; using System.Web; +using Azure.DataGateway.Auth; using Azure.DataGateway.Config; using Azure.DataGateway.Service.Authorization; using Azure.DataGateway.Service.Configurations; diff --git a/DataGateway.Service/Authorization/AuthorizationResolver.cs b/DataGateway.Service/Authorization/AuthorizationResolver.cs index 460e3ab075..3da5820ddb 100644 --- a/DataGateway.Service/Authorization/AuthorizationResolver.cs +++ b/DataGateway.Service/Authorization/AuthorizationResolver.cs @@ -5,11 +5,11 @@ using System.Security.Claims; using System.Text.Json; using System.Text.RegularExpressions; +using Azure.DataGateway.Auth; using Azure.DataGateway.Config; using Azure.DataGateway.Service.Configurations; using Azure.DataGateway.Service.Exceptions; using Azure.DataGateway.Service.Models; -using Azure.DataGateway.Service.Models.Authorization; using Azure.DataGateway.Service.Services; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Primitives; @@ -25,10 +25,10 @@ namespace Azure.DataGateway.Service.Authorization public class AuthorizationResolver : IAuthorizationResolver { private ISqlMetadataProvider _metadataProvider; - private Dictionary _entityPermissionMap = new(); private const string WILDCARD = "*"; public const string CLIENT_ROLE_HEADER = "X-MS-API-ROLE"; private static readonly HashSet _validActions = new() { ActionType.CREATE, ActionType.READ, ActionType.UPDATE, ActionType.DELETE }; + public Dictionary EntityPermissionsMap { get; private set; } = new(); public AuthorizationResolver( RuntimeConfigProvider runtimeConfigProvider, @@ -97,7 +97,7 @@ public bool IsValidRoleContext(HttpContext httpContext) /// public bool AreRoleAndActionDefinedForEntity(string entityName, string roleName, string action) { - if (_entityPermissionMap.TryGetValue(entityName, out EntityMetadata? valueOfEntityToRole)) + if (EntityPermissionsMap.TryGetValue(entityName, out EntityMetadata? valueOfEntityToRole)) { if (valueOfEntityToRole.RoleToActionMap.TryGetValue(roleName, out RoleMetadata? valueOfRoleToAction)) { @@ -119,7 +119,7 @@ public bool AreColumnsAllowedForAction(string entityName, string roleName, strin Assert.IsFalse(columns.Count() == 0, message: "columns.Count() should be greater than 0."); ActionMetadata actionToColumnMap; - RoleMetadata roleInEntity = _entityPermissionMap[entityName].RoleToActionMap[roleName]; + RoleMetadata roleInEntity = EntityPermissionsMap[entityName].RoleToActionMap[roleName]; try { @@ -139,8 +139,8 @@ public bool AreColumnsAllowedForAction(string entityName, string roleName, strin if (_metadataProvider.TryGetBackingColumn(entityName, field: exposedColumn, out string? backingColumn)) { // backingColumn will not be null when TryGetBackingColumn() is true. - if (actionToColumnMap.excluded.Contains(backingColumn!) || actionToColumnMap.excluded.Contains(WILDCARD) || - !(actionToColumnMap.included.Contains(WILDCARD) || actionToColumnMap.included.Contains(backingColumn!))) + if (actionToColumnMap.Excluded.Contains(backingColumn!) || actionToColumnMap.Excluded.Contains(WILDCARD) || + !(actionToColumnMap.Included.Contains(WILDCARD) || actionToColumnMap.Included.Contains(backingColumn!))) { // If column is present in excluded OR excluded='*' // If column is absent from included and included!=* @@ -185,7 +185,7 @@ private string GetDBPolicyForRequest(string entityName, string roleName, string // entityMetaData.RoleToActionMap[roleName] finds the roleMetaData for the current roleName // roleMetaData.ActionToColumnMap[action] finds the actionMetaData for the current action // actionMetaData.databasePolicy finds the required database policy - RoleMetadata roleMetadata = _entityPermissionMap[entityName].RoleToActionMap[roleName]; + RoleMetadata roleMetadata = EntityPermissionsMap[entityName].RoleToActionMap[roleName]; roleMetadata.ActionToColumnMap.TryGetValue(action, out ActionMetadata? actionMetadata); // If action exists in map (explicitly specified in config), use its policy @@ -194,13 +194,13 @@ private string GetDBPolicyForRequest(string entityName, string roleName, string string? dbPolicy; if (actionMetadata is not null) { - dbPolicy = actionMetadata.databasePolicy; + dbPolicy = actionMetadata.DatabasePolicy; } // else check if wildcard exists in action map, if so use its policy, else null else { roleMetadata.ActionToColumnMap.TryGetValue(WILDCARD, out ActionMetadata? wildcardMetadata); - dbPolicy = wildcardMetadata is not null ? wildcardMetadata.databasePolicy : null; + dbPolicy = wildcardMetadata is not null ? wildcardMetadata.DatabasePolicy : null; } return dbPolicy is not null ? dbPolicy : string.Empty; @@ -231,7 +231,7 @@ public void SetEntityPermissionMap(RuntimeConfig? runtimeConfig) if (actionElement.ValueKind is JsonValueKind.String) { actionName = actionElement.ToString(); - actionToColumn.included.UnionWith(ResolveTableDefinitionColumns(entityName)); + actionToColumn.Included.UnionWith(ResolveTableDefinitionColumns(entityName)); } else { @@ -253,11 +253,11 @@ public void SetEntityPermissionMap(RuntimeConfig? runtimeConfig) // resolved when no columns were included in a request. if (actionObj.Fields.Include.Length == 1 && actionObj.Fields.Include[0] == WILDCARD) { - actionToColumn.included.UnionWith(ResolveTableDefinitionColumns(entityName)); + actionToColumn.Included.UnionWith(ResolveTableDefinitionColumns(entityName)); } else { - actionToColumn.included = new(actionObj.Fields.Include); + actionToColumn.Included = new(actionObj.Fields.Include); } } @@ -267,17 +267,17 @@ public void SetEntityPermissionMap(RuntimeConfig? runtimeConfig) // columns must be resolved and placed in the actionToColumn Key/Value store. if (actionObj.Fields.Exclude.Length == 1 && actionObj.Fields.Exclude[0] == WILDCARD) { - actionToColumn.excluded.UnionWith(ResolveTableDefinitionColumns(entityName)); + actionToColumn.Excluded.UnionWith(ResolveTableDefinitionColumns(entityName)); } else { - actionToColumn.excluded = new(actionObj.Fields.Exclude); + actionToColumn.Excluded = new(actionObj.Fields.Exclude); } } if (actionObj.Policy is not null && actionObj.Policy.Database is not null) { - actionToColumn.databasePolicy = actionObj.Policy.Database; + actionToColumn.DatabasePolicy = actionObj.Policy.Database; } } } @@ -288,15 +288,15 @@ public void SetEntityPermissionMap(RuntimeConfig? runtimeConfig) entityToRoleMap.RoleToActionMap[role] = roleToAction; } - _entityPermissionMap[entityName] = entityToRoleMap; + EntityPermissionsMap[entityName] = entityToRoleMap; } } /// public IEnumerable GetAllowedColumns(string entityName, string roleName, string action) { - ActionMetadata actionMetadata = _entityPermissionMap[entityName].RoleToActionMap[roleName].ActionToColumnMap[action]; - IEnumerable allowedDBColumns = actionMetadata.included.Except(actionMetadata.excluded); + ActionMetadata actionMetadata = EntityPermissionsMap[entityName].RoleToActionMap[roleName].ActionToColumnMap[action]; + IEnumerable allowedDBColumns = actionMetadata.Included.Except(actionMetadata.Excluded); List allowedExposedColumns = new(); foreach (string dbColumn in allowedDBColumns) diff --git a/DataGateway.Service/Authorization/RestAuthorizationHandler.cs b/DataGateway.Service/Authorization/RestAuthorizationHandler.cs index 834fb8d932..a92232d95c 100644 --- a/DataGateway.Service/Authorization/RestAuthorizationHandler.cs +++ b/DataGateway.Service/Authorization/RestAuthorizationHandler.cs @@ -2,6 +2,7 @@ using System.Linq; using System.Net; using System.Threading.Tasks; +using Azure.DataGateway.Auth; using Azure.DataGateway.Config; using Azure.DataGateway.Service.Exceptions; using Azure.DataGateway.Service.Models; diff --git a/DataGateway.Service/Azure.DataGateway.Service.csproj b/DataGateway.Service/Azure.DataGateway.Service.csproj index 3ed4670f16..162bf480cb 100644 --- a/DataGateway.Service/Azure.DataGateway.Service.csproj +++ b/DataGateway.Service/Azure.DataGateway.Service.csproj @@ -85,6 +85,7 @@ + diff --git a/DataGateway.Service/Models/Authorization/AuthorizationMetadataHelpers.cs b/DataGateway.Service/Models/Authorization/AuthorizationMetadataHelpers.cs deleted file mode 100644 index 910b19f467..0000000000 --- a/DataGateway.Service/Models/Authorization/AuthorizationMetadataHelpers.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System.Collections.Generic; - -namespace Azure.DataGateway.Service.Models.Authorization -{ - /// - /// Represents the permission metadata of an entity. - /// An entity's top-level permission structure is a collection - /// of roles. - /// - class EntityMetadata - { - /// - /// Given the key (roleName) returns the associated RoleDS object. - /// - public Dictionary RoleToActionMap = new(); - } - - /// - /// Represents the permission metadata of a role - /// A role's top-level permission structure is a collection of - /// actions allowed for that role: Create, Read, Update, Delete, * (wildcard) - /// - class RoleMetadata - { - /// - /// Given the key (actionName) returns the associated ActionDS object. - /// - public Dictionary ActionToColumnMap = new(); - } - - /// - /// Represents the permission metadata of an action - /// An action lists both columns that are included and/or exluded - /// for that action. - /// - class ActionMetadata - { - public string? databasePolicy; - public HashSet included = new(); - public HashSet excluded = new(); - } -} diff --git a/DataGateway.Service/Services/RestService.cs b/DataGateway.Service/Services/RestService.cs index b689de794a..f31d700955 100644 --- a/DataGateway.Service/Services/RestService.cs +++ b/DataGateway.Service/Services/RestService.cs @@ -7,6 +7,7 @@ using System.Text.Json; using System.Threading.Tasks; using System.Web; +using Azure.DataGateway.Auth; using Azure.DataGateway.Config; using Azure.DataGateway.Service.Authorization; using Azure.DataGateway.Service.Exceptions; diff --git a/DataGateway.Service/Startup.cs b/DataGateway.Service/Startup.cs index 6eecef0a74..f9551f7c9d 100644 --- a/DataGateway.Service/Startup.cs +++ b/DataGateway.Service/Startup.cs @@ -2,6 +2,7 @@ using System.IO.Abstractions; using System.Net.Http; using System.Threading.Tasks; +using Azure.DataGateway.Auth; using Azure.DataGateway.Config; using Azure.DataGateway.Service.AuthenticationHelpers; using Azure.DataGateway.Service.Authorization;