Skip to content

Commit

Permalink
Rework authz policy.
Browse files Browse the repository at this point in the history
  • Loading branch information
bitbound committed Oct 1, 2024
1 parent 407c946 commit 98eb08b
Show file tree
Hide file tree
Showing 15 changed files with 133 additions and 43 deletions.
1 change: 1 addition & 0 deletions ControlR.Web.Client/Authz/UserClaimTypes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@
public static class UserClaimTypes
{
public const string TenantUid = "controlr:tenant:uid";
public const string TenantId = "controlr:tenant:id";
}
22 changes: 21 additions & 1 deletion ControlR.Web.Client/Extensions/ClaimsPrincipalExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,28 @@ public static bool IsAuthenticated(this ClaimsPrincipal user)
return user.Identity?.IsAuthenticated ?? false;
}

public static bool TryGetTenantUid(
public static bool TryGetTenantId(
this ClaimsPrincipal user,
out int tenantId)
{
tenantId = 0;
if (!user.IsAuthenticated())
{
return false;
}

var tenantClaim = user.FindFirst(UserClaimTypes.TenantId);
if (!int.TryParse(tenantClaim?.Value, out var id))
{
return false;
}

tenantId = id;
return true;
}

public static bool TryGetTenantUid(
this ClaimsPrincipal user,
out Guid tenantUid)
{
tenantUid = Guid.Empty;
Expand Down
2 changes: 1 addition & 1 deletion ControlR.Web.Server/Api/DevicesController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public async Task<ActionResult<List<DeviceDto>>> Get(

await foreach (var device in deviceQuery.AsAsyncEnumerable())
{
var authResult = await authorizationService.AuthorizeAsync(User, device, RemoteControlByDevicePolicy.PolicyName);
var authResult = await authorizationService.AuthorizeAsync(User, device, DeviceAccessByDeviceResourcePolicy.PolicyName);
if (authResult.Succeeded)
{
authorizedDevices.Add(device.ToDto());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@ public static class AuthorizationPolicyBuilderExtensions
public static AuthorizationPolicyBuilder RequireServiceProviderAssertion(
this AuthorizationPolicyBuilder builder,
Func<IServiceProvider, AuthorizationHandlerContext, Task<bool>> assertion)
{
builder.Requirements.Add(new ServiceProviderAsyncRequirement(assertion));
return builder;
}

public static AuthorizationPolicyBuilder RequireServiceProviderAssertion(
this AuthorizationPolicyBuilder builder,
Func<IServiceProvider, AuthorizationHandlerContext, bool> assertion)
{
builder.Requirements.Add(new ServiceProviderRequirement(assertion));
return builder;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
using ControlR.Web.Client.Extensions;
using Microsoft.AspNetCore.Authorization;

namespace ControlR.Web.Server.Authz.Policies;

public static class DeviceAccessByDeviceResourcePolicy
{
public const string PolicyName = "DeviceAccessByDeviceResourcePolicy";

public static AuthorizationPolicy Create()
{
return new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.RequireServiceProviderAssertion((sp, handlerCtx) =>
{
if (handlerCtx.Resource is not Device device)
{
return false;
}

if (handlerCtx.User.TryGetTenantId(out var tenantId))
{
return false;
}

if (device.TenantId != tenantId)
{
return false;
}

if (handlerCtx.User.IsInRole(RoleNames.ServerAdministrator) ||
handlerCtx.User.IsInRole(RoleNames.DeviceAdministrator))
{
return true;
}

handlerCtx.Fail();
return false;
})
.Build();
}
}
25 changes: 0 additions & 25 deletions ControlR.Web.Server/Authz/Policies/RemoteControlByDevicePolicy.cs

This file was deleted.

9 changes: 9 additions & 0 deletions ControlR.Web.Server/Authz/ServiceProviderAsyncRequirement.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using Microsoft.AspNetCore.Authorization;

namespace ControlR.Web.Server.Authz;

public class ServiceProviderAsyncRequirement(Func<IServiceProvider, AuthorizationHandlerContext, Task<bool>> assertion)
: IAuthorizationRequirement
{
public Func<IServiceProvider, AuthorizationHandlerContext, Task<bool>> Assertion { get; } = assertion;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using Microsoft.AspNetCore.Authorization;

namespace ControlR.Web.Server.Authz;

public class ServiceProviderAsyncRequirementHandler(IServiceProvider serviceProvider) : IAuthorizationHandler
{
private readonly IServiceProvider _serviceProvider = serviceProvider;

public async Task HandleAsync(AuthorizationHandlerContext context)
{
foreach (var requirement in context.Requirements.OfType<ServiceProviderAsyncRequirement>())
{
if (await requirement.Assertion.Invoke(_serviceProvider, context))
{
context.Succeed(requirement);
}
}
}
}
4 changes: 2 additions & 2 deletions ControlR.Web.Server/Authz/ServiceProviderRequirement.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

namespace ControlR.Web.Server.Authz;

public class ServiceProviderRequirement(Func<IServiceProvider, AuthorizationHandlerContext, Task<bool>> assertion)
public class ServiceProviderRequirement(Func<IServiceProvider, AuthorizationHandlerContext, bool> assertion)
: IAuthorizationRequirement
{
public Func<IServiceProvider, AuthorizationHandlerContext, Task<bool>> Assertion { get; } = assertion;
public Func<IServiceProvider, AuthorizationHandlerContext, bool> Assertion { get; } = assertion;
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,13 @@ public class ServiceProviderRequirementHandler(IServiceProvider serviceProvider)

public async Task HandleAsync(AuthorizationHandlerContext context)
{
foreach (var requirement in context.Requirements.OfType<ServiceProviderRequirement>())
var requirements = context.Requirements
.OfType<ServiceProviderRequirement>()
.ToAsyncEnumerable();

await foreach (var requirement in requirements)
{
if (await requirement.Assertion.Invoke(_serviceProvider, context))
if (requirement.Assertion.Invoke(_serviceProvider, context))
{
context.Succeed(requirement);
}
Expand Down
5 changes: 5 additions & 0 deletions ControlR.Web.Server/Hubs/HubGroupNames.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,9 @@ public static string GetDeviceGroupName(Guid deviceId)
{
return $"device-{deviceId}";
}

public static string GetUserRoleGroupName(string roleName, Guid tenantUid)
{
return $"tenant-{tenantUid}-user-role-{roleName}";
}
}
23 changes: 15 additions & 8 deletions ControlR.Web.Server/Hubs/ViewerHub.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Runtime.CompilerServices;
using ControlR.Web.Client.Extensions;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.Options;
Expand Down Expand Up @@ -263,17 +264,23 @@ public override async Task OnConnectedAsync()
return;
}

// TODO: Add user to groups.
//await Groups.AddToGroupAsync(Context.ConnectionId, publicKey);

if (IsServerAdmin())
if (Context.User.TryGetTenantUid(out var tenantUid))
{
await Groups.AddToGroupAsync(Context.ConnectionId, HubGroupNames.ServerAdministrators);
if (Context.User.IsInRole(RoleNames.ServerAdministrator))
{
await Groups.AddToGroupAsync(Context.ConnectionId, HubGroupNames.ServerAdministrators);

var getResult = await serverStatsProvider.GetServerStats();
if (getResult.IsSuccess)
{
await Clients.Caller.ReceiveServerStats(getResult.Value);
}
}

var getResult = await serverStatsProvider.GetServerStats();
if (getResult.IsSuccess)
if (Context.User.IsInRole(RoleNames.DeviceAdministrator))
{
await Clients.Caller.ReceiveServerStats(getResult.Value);
var hubGroupName = HubGroupNames.GetUserRoleGroupName(RoleNames.DeviceAdministrator, tenantUid);
await Groups.AddToGroupAsync(Context.ConnectionId, hubGroupName);
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion ControlR.Web.Server/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@
builder.Services
.AddAuthorizationBuilder()
.AddPolicy(PolicyNames.RequireServerAdministrator, AuthorizationPolicies.RequireServerAdministrator)
.AddPolicy(RemoteControlByDevicePolicy.PolicyName, RemoteControlByDevicePolicy.Create());
.AddPolicy(DeviceAccessByDeviceResourcePolicy.PolicyName, DeviceAccessByDeviceResourcePolicy.Create());

// Add Identity services.
builder.Services
Expand Down
3 changes: 2 additions & 1 deletion ControlR.Web.Server/Usings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,5 @@
global using ControlR.Web.Server.Data.Entities;
global using ControlR.Libraries.Shared.Dtos.ServerApi;
global using ControlR.Web.Server.Authz.Policies;
global using ControlR.Web.Server.Data;
global using ControlR.Web.Server.Data;
global using Microsoft.AspNetCore.Identity;
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,10 @@ public static string StringJoin(this IEnumerable<string> self, string separator)

public static async IAsyncEnumerable<T> ToAsyncEnumerable<T>(this IEnumerable<T> enumerable)
{
await Task.Yield();

foreach (var item in enumerable)
{
yield return item;
await Task.Yield();
}
}

Expand Down

0 comments on commit 98eb08b

Please sign in to comment.