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;