diff --git a/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs b/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs index 7f81ddc042..d49f4386e2 100644 --- a/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs +++ b/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs @@ -146,7 +146,7 @@ public class OrganizationUsersController : Controller } [HttpGet("")] - [ManageUsersRequirement] + [OrganizationAuthorize(typeof(ManageUsersRequirement))] public async Task> Get(Guid orgId, bool includeGroups = false, bool includeCollections = false) { var organizationUsers = await _organizationUserUserDetailsQuery.GetOrganizationUserUserDetails( diff --git a/src/Core/AdminConsole/OrganizationFeatures/AdminConsoleRequirementsHandler.cs b/src/Core/AdminConsole/OrganizationFeatures/AdminConsoleRequirementsHandler.cs new file mode 100644 index 0000000000..0191dee0fc --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/AdminConsoleRequirementsHandler.cs @@ -0,0 +1,32 @@ +#nullable enable + +using Bit.Core.AdminConsole.OrganizationFeatures.Shared.Authorization; +using Bit.Core.Context; +using Bit.Core.Enums; +using Microsoft.AspNetCore.Http; + +namespace Bit.Core.AdminConsole.OrganizationFeatures; + +public class ManageUsersRequirement : IOrganizationRequirement; + +public class AdminConsoleRequirementsHandler(ICurrentContext currentContext, IHttpContextAccessor httpContextAccessor) + : OrganizationRequirementHandler(currentContext, httpContextAccessor) +{ + protected override async Task HandleOrganizationRequirementAsync(IOrganizationRequirement requirement, + Guid organizationId, CurrentContextOrganization? organization) + { + var authorized = requirement switch + { + ManageUsersRequirement => await ManageUsersAsync(organizationId, organization), + _ => false + }; + + return authorized; + } + + private async Task ManageUsersAsync(Guid organizationId, CurrentContextOrganization? organization) + => organization is + { Type: OrganizationUserType.Owner or OrganizationUserType.Admin } or + { Permissions.ManageUsers: true } + || await IsProviderForOrganizationAsync(organizationId); +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/RoleAuthorizationHandler.cs b/src/Core/AdminConsole/OrganizationFeatures/RoleAuthorizationHandler.cs deleted file mode 100644 index 2cc6d3e207..0000000000 --- a/src/Core/AdminConsole/OrganizationFeatures/RoleAuthorizationHandler.cs +++ /dev/null @@ -1,67 +0,0 @@ -#nullable enable - -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 abstract class OrganizationRequirementAttribute - : AuthorizeAttribute, IAuthorizationRequirement, IAuthorizationRequirementData -{ - public IEnumerable GetRequirements() => [this]; -} - -public abstract class OrganizationRequirementHandler : AuthorizationHandler -{ - protected Guid? OrganizationId { get; set; } - protected CurrentContextOrganization? Organization { get; set; } - - protected OrganizationRequirementHandler(ICurrentContext currentContext, IHttpContextAccessor httpContextAccessor) - { - if (httpContextAccessor.HttpContext is null) - { - return; - } - - httpContextAccessor.HttpContext.GetRouteData().Values.TryGetValue("orgId", out var orgIdParam); - if (!Guid.TryParse(orgIdParam?.ToString(), out var orgId)) - { - // No orgId supplied, unable to authorize - return; - } - - OrganizationId = orgId; - if (OrganizationId.HasValue) - { - Organization = currentContext.GetOrganization(OrganizationId.Value); - } - } -} - -public class ManageUsersRequirementAttribute : OrganizationRequirementAttribute; - -public class AdminConsoleRequirementsHandler(ICurrentContext currentContext, IHttpContextAccessor httpContextAccessor) - : OrganizationRequirementHandler(currentContext, httpContextAccessor) -{ - protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, - OrganizationRequirementAttribute requirement) - { - var authorized = requirement switch - { - ManageUsersRequirementAttribute => Organization is - { Type: OrganizationUserType.Owner or OrganizationUserType.Admin } or - { Permissions.ManageUsers: true }, - _ => false - }; - - if (authorized) - { - context.Succeed(requirement); - } - - return Task.CompletedTask; - } -} diff --git a/src/Core/AdminConsole/OrganizationFeatures/Shared/Authorization/OrganizationAuthorizeAttribute.cs b/src/Core/AdminConsole/OrganizationFeatures/Shared/Authorization/OrganizationAuthorizeAttribute.cs new file mode 100644 index 0000000000..f018313891 --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/Shared/Authorization/OrganizationAuthorizeAttribute.cs @@ -0,0 +1,13 @@ +using Microsoft.AspNetCore.Authorization; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.Shared.Authorization; + +public class OrganizationAuthorizeAttribute(Type requirementType) + : AuthorizeAttribute, IAuthorizationRequirementData +{ + public IEnumerable GetRequirements() + { + var requirement = (IOrganizationRequirement)Activator.CreateInstance(requirementType)!; + yield return requirement; + } +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/Shared/Authorization/OrganizationRequirementHandler.cs b/src/Core/AdminConsole/OrganizationFeatures/Shared/Authorization/OrganizationRequirementHandler.cs new file mode 100644 index 0000000000..3f1a78ac4e --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/Shared/Authorization/OrganizationRequirementHandler.cs @@ -0,0 +1,41 @@ +#nullable enable + +using Bit.Core.Context; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.Shared.Authorization; + +public interface IOrganizationRequirement : IAuthorizationRequirement; + +public abstract class OrganizationRequirementHandler(ICurrentContext currentContext, IHttpContextAccessor httpContextAccessor) : AuthorizationHandler +{ + protected abstract Task HandleOrganizationRequirementAsync(IOrganizationRequirement requirement, Guid organizationId, CurrentContextOrganization? organization); + + protected async Task IsProviderForOrganizationAsync(Guid organizationId) => + await currentContext.ProviderUserForOrgAsync(organizationId); + + protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, IOrganizationRequirement requirement) + { + if (httpContextAccessor.HttpContext is null) + { + return; + } + + httpContextAccessor.HttpContext.GetRouteData().Values.TryGetValue("orgId", out var orgIdParam); + if (!Guid.TryParse(orgIdParam?.ToString(), out var orgId)) + { + // No orgId supplied, unable to authorize + return; + } + + var organization = currentContext.GetOrganization(orgId); + + var authorized = await HandleOrganizationRequirementAsync(requirement, orgId, organization); + if (authorized) + { + context.Succeed(requirement); + } + } +}