Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions Azure.DataGateway.Service.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
57 changes: 57 additions & 0 deletions DataGateway.Auth/AuthorizationMetadataHelpers.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
namespace Azure.DataGateway.Auth
{
/// <summary>
/// Represents the permission metadata of an entity.
/// An entity's top-level permission structure is a collection
/// of roles.
/// </summary>
public class EntityMetadata
{
/// <summary>
/// Given the key (roleName) returns the associated RoleMetadata object.
/// To retrieve all roles associated with an entity -> RoleToActionMap.Keys()
/// </summary>
public Dictionary<string, RoleMetadata> RoleToActionMap { get; set; } = new();

/// <summary>
/// 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}
/// </summary>
public Dictionary<string, Dictionary<string, IEnumerable<string>>> FieldToRolesMap { get; set; } = new();

/// <summary>
/// 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}
/// </summary>
public Dictionary<string, List<string>> ActionToRolesMap { get; set; } = new();
}

/// <summary>
/// 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)
/// </summary>
public class RoleMetadata
{
/// <summary>
/// Given the key (actionName) returns the associated ActionMetadata object.
/// </summary>
public Dictionary<string, ActionMetadata> ActionToColumnMap { get; set; } = new();
}

/// <summary>
/// Represents the permission metadata of an action
/// An action lists both columns that are included and/or exluded
/// for that action.
/// </summary>
public class ActionMetadata
{
public string? DatabasePolicy { get; set; }
public HashSet<string> Included { get; set; } = new();
public HashSet<string> Excluded { get; set; } = new();
public HashSet<string> Allowed { get; set; } = new();
}
}
13 changes: 13 additions & 0 deletions DataGateway.Auth/Azure.DataGateway.Auth.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.2.0" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
using System.Collections.Generic;
using Microsoft.AspNetCore.Http;

namespace Azure.DataGateway.Service.Authorization
namespace Azure.DataGateway.Auth
{
/// <summary>
/// Interface for authorization decision-making. Each method performs lookups within a
/// structure representing permissions defined in the runtime config.
/// </summary>
public interface IAuthorizationResolver
{
/// <summary>
/// Representation of authorization permissions for each entity in the runtime config.
/// </summary>
public Dictionary<string, EntityMetadata> EntityPermissionsMap { get; }

/// <summary>
/// Checks for the existence of the client role header in httpContext.Request.Headers
/// and evaluates that header against the authenticated (httpContext.User)'s roles
Expand Down Expand Up @@ -61,5 +65,27 @@ public interface IAuthorizationResolver
/// <param name="httpContext">Contains token claims of the authenticated user used in policy evaluation.</param>
/// <returns>Returns the parsed policy, if successfully processed, or an exception otherwise.</returns>
public string TryProcessDBPolicy(string entityName, string roleName, string action, HttpContext httpContext);

/// <summary>
/// Returns a list of roles which define permissions for the provided action.
/// i.e. list of roles which allow the action "read" on entityName.
/// </summary>
/// <param name="entityName">Entity to lookup permissions</param>
/// <param name="actionName">Action to lookup applicable roles</param>
/// <returns>Collection of roles. Empty list if entityPermissionsMap is null.</returns>
public static IEnumerable<string> GetRolesForAction(string entityName, string actionName, Dictionary<string, EntityMetadata>? entityPermissionsMap)
{
if (entityName is null)
{
throw new ArgumentNullException(paramName: "entityName");
}

if (entityPermissionsMap is not null && entityPermissionsMap[entityName].ActionToRolesMap.TryGetValue(actionName, out List<string>? roleList) && roleList is not null)
Comment thread
seantleonard marked this conversation as resolved.
{
return roleList;
}

return new List<string>();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions DataGateway.Service.Tests/SqlTests/SqlTestBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
36 changes: 18 additions & 18 deletions DataGateway.Service/Authorization/AuthorizationResolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -25,10 +25,10 @@ namespace Azure.DataGateway.Service.Authorization
public class AuthorizationResolver : IAuthorizationResolver
{
private ISqlMetadataProvider _metadataProvider;
private Dictionary<string, EntityMetadata> _entityPermissionMap = new();
private const string WILDCARD = "*";
public const string CLIENT_ROLE_HEADER = "X-MS-API-ROLE";
private static readonly HashSet<string> _validActions = new() { ActionType.CREATE, ActionType.READ, ActionType.UPDATE, ActionType.DELETE };
public Dictionary<string, EntityMetadata> EntityPermissionsMap { get; private set; } = new();

public AuthorizationResolver(
RuntimeConfigProvider runtimeConfigProvider,
Expand Down Expand Up @@ -97,7 +97,7 @@ public bool IsValidRoleContext(HttpContext httpContext)
/// <inheritdoc />
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))
{
Expand All @@ -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
{
Expand All @@ -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!=*
Expand Down Expand Up @@ -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
Expand All @@ -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;
Expand Down Expand Up @@ -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
{
Expand All @@ -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);
}
}

Expand All @@ -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;
}
}
}
Expand All @@ -288,15 +288,15 @@ public void SetEntityPermissionMap(RuntimeConfig? runtimeConfig)
entityToRoleMap.RoleToActionMap[role] = roleToAction;
}

_entityPermissionMap[entityName] = entityToRoleMap;
EntityPermissionsMap[entityName] = entityToRoleMap;
}
}

/// <inheritdoc />
public IEnumerable<string> GetAllowedColumns(string entityName, string roleName, string action)
{
ActionMetadata actionMetadata = _entityPermissionMap[entityName].RoleToActionMap[roleName].ActionToColumnMap[action];
IEnumerable<string> allowedDBColumns = actionMetadata.included.Except(actionMetadata.excluded);
ActionMetadata actionMetadata = EntityPermissionsMap[entityName].RoleToActionMap[roleName].ActionToColumnMap[action];
IEnumerable<string> allowedDBColumns = actionMetadata.Included.Except(actionMetadata.Excluded);
List<string> allowedExposedColumns = new();

foreach (string dbColumn in allowedDBColumns)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions DataGateway.Service/Azure.DataGateway.Service.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@
<ProjectCapability Include="CSharp;Managed;ClassDesigner" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\DataGateway.Auth\Azure.DataGateway.Auth.csproj" />
<ProjectReference Include="..\DataGateway.Config\Azure.DataGateway.Config.csproj" />
<ProjectReference Include="..\DataGateway.Service.GraphQLBuilder\Azure.DataGateway.Service.GraphQLBuilder.csproj" />
</ItemGroup>
Expand Down

This file was deleted.

1 change: 1 addition & 0 deletions DataGateway.Service/Services/RestService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions DataGateway.Service/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down