From 1c697544b82fd654bf4cd0b51b56a0447fe55a1c Mon Sep 17 00:00:00 2001 From: Thomas Rittson Date: Fri, 21 Mar 2025 11:52:48 +1000 Subject: [PATCH] First pass using policy based auth --- .../OrganizationUsersController.cs | 13 +++---- src/Api/Startup.cs | 19 ++++++++-- .../Utilities/ServiceCollectionExtensions.cs | 3 ++ .../RoleAuthorizationHandler.cs | 36 +++++++++++++++++++ 4 files changed, 63 insertions(+), 8 deletions(-) create mode 100644 src/Core/AdminConsole/OrganizationFeatures/RoleAuthorizationHandler.cs diff --git a/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs b/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs index 5a73e57204..682f9776e1 100644 --- a/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs +++ b/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs @@ -145,14 +145,15 @@ public class OrganizationUsersController : Controller } [HttpGet("")] + [Authorize(Policy = "owner")] public async Task> Get(Guid orgId, bool includeGroups = false, bool includeCollections = false) { - var authorized = (await _authorizationService.AuthorizeAsync( - User, new OrganizationScope(orgId), OrganizationUserUserDetailsOperations.ReadAll)).Succeeded; - if (!authorized) - { - throw new NotFoundException(); - } + // var authorized = (await _authorizationService.AuthorizeAsync( + // User, new OrganizationScope(orgId), OrganizationUserUserDetailsOperations.ReadAll)).Succeeded; + // if (!authorized) + // { + // throw new NotFoundException(); + // } var organizationUsers = await _organizationUserUserDetailsQuery.GetOrganizationUserUserDetails( new OrganizationUserUserDetailsQueryRequest diff --git a/src/Api/Startup.cs b/src/Api/Startup.cs index 5849bfb634..8ca6baf044 100644 --- a/src/Api/Startup.cs +++ b/src/Api/Startup.cs @@ -27,8 +27,10 @@ using Bit.Core.OrganizationFeatures.OrganizationSubscriptions; using Bit.Core.Tools.Entities; using Bit.Core.Vault.Entities; using Bit.Api.Auth.Models.Request.WebAuthn; +using Bit.Core.AdminConsole.OrganizationFeatures; using Bit.Core.Auth.Models.Data; using Bit.Core.Auth.Identity.TokenProviders; +using Bit.Core.Enums; using Bit.Core.Tools.ImportFeatures; using Bit.Core.Tools.ReportFeatures; @@ -143,6 +145,18 @@ public class Startup (c.Value.Contains(ApiScopes.Api) || c.Value.Contains(ApiScopes.ApiSecrets)) )); }); + + // Simplest implementation: check for role + // Issues: + // - unable to specify custom permissions + // - multiple policies are treated as AND rather than OR + // - does not allow for more complex conditional logic - e.g. providers can affect whether owners can view billing + // Alternative: describe broad action/capability, e.g. ManageUsers, ManageGroups, ViewBilling, similar to CurrentContext today + // the handler is then implemented per domain to define who can do those things + config.AddPolicy("owner", policy + => policy.AddRequirements(new RoleRequirement(OrganizationUserType.Owner))); + config.AddPolicy("admin", policy + => policy.AddRequirements(new RoleRequirement(OrganizationUserType.Admin))); }); services.AddScoped(); @@ -255,11 +269,12 @@ public class Startup // Add authentication and authorization to the request pipeline. app.UseAuthentication(); - app.UseAuthorization(); - // Add current context + // Add current context - before authz app.UseMiddleware(); + app.UseAuthorization(); + // Add endpoints to the request pipeline. app.UseEndpoints(endpoints => { diff --git a/src/Api/Utilities/ServiceCollectionExtensions.cs b/src/Api/Utilities/ServiceCollectionExtensions.cs index feeac03e54..e74cc22e26 100644 --- a/src/Api/Utilities/ServiceCollectionExtensions.cs +++ b/src/Api/Utilities/ServiceCollectionExtensions.cs @@ -1,5 +1,6 @@ using Bit.Api.Tools.Authorization; using Bit.Api.Vault.AuthorizationHandlers.Collections; +using Bit.Core.AdminConsole.OrganizationFeatures; using Bit.Core.AdminConsole.OrganizationFeatures.Groups.Authorization; using Bit.Core.IdentityServer; using Bit.Core.Settings; @@ -105,5 +106,7 @@ public static class ServiceCollectionExtensions services.AddScoped(); services.AddScoped(); services.AddScoped(); + + services.AddScoped(); } } diff --git a/src/Core/AdminConsole/OrganizationFeatures/RoleAuthorizationHandler.cs b/src/Core/AdminConsole/OrganizationFeatures/RoleAuthorizationHandler.cs new file mode 100644 index 0000000000..b75565edba --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/RoleAuthorizationHandler.cs @@ -0,0 +1,36 @@ +using Bit.Core.Context; +using Bit.Core.Enums; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; + +namespace Bit.Core.AdminConsole.OrganizationFeatures; + +public record RoleRequirement(OrganizationUserType Role) : IAuthorizationRequirement; + +public class RoleAuthorizationHandler(ICurrentContext currentContext, IHttpContextAccessor httpContextAccessor) : AuthorizationHandler +{ + protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, RoleRequirement requirement) + { + if (httpContextAccessor.HttpContext is null) + { + return Task.CompletedTask; + } + + httpContextAccessor.HttpContext.GetRouteData().Values.TryGetValue("orgId", out var orgIdParam); + if (!Guid.TryParse(orgIdParam?.ToString(), out var orgId)) + { + // No orgId supplied, unable to authorize + return Task.CompletedTask; + } + + // This could be an extension method on ClaimsPrincipal + var orgClaims = currentContext.GetOrganization(orgId); + if (orgClaims?.Type == requirement.Role) + { + context.Succeed(requirement); + } + + return Task.CompletedTask; + } +}