diff --git a/src/Core/AdminConsole/OrganizationFeatures/Shared/Authorization/AuthorizeAttribute.cs b/src/Api/AdminConsole/Authorization/AuthorizeAttribute.cs similarity index 89% rename from src/Core/AdminConsole/OrganizationFeatures/Shared/Authorization/AuthorizeAttribute.cs rename to src/Api/AdminConsole/Authorization/AuthorizeAttribute.cs index 93705e7fc1..9be09d30cd 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Shared/Authorization/AuthorizeAttribute.cs +++ b/src/Api/AdminConsole/Authorization/AuthorizeAttribute.cs @@ -2,7 +2,7 @@ using Microsoft.AspNetCore.Authorization; -namespace Bit.Core.AdminConsole.OrganizationFeatures.Shared.Authorization; +namespace Bit.Api.AdminConsole.Authorization; /// /// An attribute which requires authorization using the specified requirement. diff --git a/src/Core/AdminConsole/OrganizationFeatures/Shared/Authorization/IOrganizationRequirement.cs b/src/Api/AdminConsole/Authorization/IOrganizationRequirement.cs similarity index 64% rename from src/Core/AdminConsole/OrganizationFeatures/Shared/Authorization/IOrganizationRequirement.cs rename to src/Api/AdminConsole/Authorization/IOrganizationRequirement.cs index a2f2ff74e7..5c6a6a73b9 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Shared/Authorization/IOrganizationRequirement.cs +++ b/src/Api/AdminConsole/Authorization/IOrganizationRequirement.cs @@ -1,9 +1,10 @@ #nullable enable +using Bit.Api.AdminConsole.Context; using Bit.Core.Context; using Microsoft.AspNetCore.Authorization; -namespace Bit.Core.AdminConsole.OrganizationFeatures.Shared.Authorization; +namespace Bit.Api.AdminConsole.Authorization; /// /// A requirement that implements this interface will be handled by , @@ -13,6 +14,8 @@ namespace Bit.Core.AdminConsole.OrganizationFeatures.Shared.Authorization; /// public interface IOrganizationRequirement : IAuthorizationRequirement { - // TODO: avoid injecting all of ICurrentContext? - public Task AuthorizeAsync(Guid organizationId, CurrentContextOrganization? organizationClaims, ICurrentContext currentContext); + public Task AuthorizeAsync( + Guid organizationId, + CurrentContextOrganization? organizationClaims, + IProviderOrganizationContext providerOrganizationContext); } diff --git a/src/Core/AdminConsole/OrganizationFeatures/Shared/Authorization/OrganizationRequirementHandler.cs b/src/Api/AdminConsole/Authorization/OrganizationRequirementHandler.cs similarity index 71% rename from src/Core/AdminConsole/OrganizationFeatures/Shared/Authorization/OrganizationRequirementHandler.cs rename to src/Api/AdminConsole/Authorization/OrganizationRequirementHandler.cs index 5a2402d9e7..987c0f236a 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Shared/Authorization/OrganizationRequirementHandler.cs +++ b/src/Api/AdminConsole/Authorization/OrganizationRequirementHandler.cs @@ -1,10 +1,10 @@ #nullable enable +using Bit.Api.AdminConsole.Context; using Bit.Core.Context; using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; -namespace Bit.Core.AdminConsole.OrganizationFeatures.Shared.Authorization; +namespace Bit.Api.AdminConsole.Authorization; /// /// Handles any requirement that implements . @@ -13,7 +13,10 @@ namespace Bit.Core.AdminConsole.OrganizationFeatures.Shared.Authorization; /// /// /// -public class OrganizationRequirementHandler(ICurrentContext currentContext, IHttpContextAccessor httpContextAccessor) +public class OrganizationRequirementHandler( + ICurrentContext currentContext, + IProviderOrganizationContext providerOrganizationContext, + IHttpContextAccessor httpContextAccessor) : AuthorizationHandler { protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, IOrganizationRequirement requirement) @@ -24,9 +27,9 @@ public class OrganizationRequirementHandler(ICurrentContext currentContext, IHtt throw new Exception("No organizationId found in route. IOrganizationRequirement cannot be used on this endpoint."); } - var organization = currentContext.GetOrganization(organizationId.Value); + var organizationClaims = currentContext.GetOrganization(organizationId.Value); - var authorized = await requirement.AuthorizeAsync(organizationId.Value, organization, currentContext); + var authorized = await requirement.AuthorizeAsync(organizationId.Value, organizationClaims, providerOrganizationContext); if (authorized) { diff --git a/src/Core/AdminConsole/OrganizationFeatures/Shared/Authorization/OrganizationRequirementHelpers.cs b/src/Api/AdminConsole/Authorization/OrganizationRequirementHelpers.cs similarity index 78% rename from src/Core/AdminConsole/OrganizationFeatures/Shared/Authorization/OrganizationRequirementHelpers.cs rename to src/Api/AdminConsole/Authorization/OrganizationRequirementHelpers.cs index 4f17dfe135..5449810953 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Shared/Authorization/OrganizationRequirementHelpers.cs +++ b/src/Api/AdminConsole/Authorization/OrganizationRequirementHelpers.cs @@ -1,9 +1,6 @@ #nullable enable -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Routing; - -namespace Bit.Core.AdminConsole.OrganizationFeatures.Shared.Authorization; +namespace Bit.Api.AdminConsole.Authorization; public static class OrganizationRequirementHelpers { diff --git a/src/Api/AdminConsole/Authorization/Requirements/ManageUsersRequirement.cs b/src/Api/AdminConsole/Authorization/Requirements/ManageUsersRequirement.cs new file mode 100644 index 0000000000..e107202f33 --- /dev/null +++ b/src/Api/AdminConsole/Authorization/Requirements/ManageUsersRequirement.cs @@ -0,0 +1,19 @@ +#nullable enable + +using Bit.Api.AdminConsole.Context; +using Bit.Core.Context; +using Bit.Core.Enums; + +namespace Bit.Api.AdminConsole.Authorization.Requirements; + +public class ManageUsersRequirement : IOrganizationRequirement +{ + public async Task AuthorizeAsync( + Guid organizationId, + CurrentContextOrganization? organizationClaims, + IProviderOrganizationContext providerOrganizationContext) + => organizationClaims is + { Type: OrganizationUserType.Owner or OrganizationUserType.Admin } or + { Permissions.ManageUsers: true } + || await providerOrganizationContext.ProviderUserForOrgAsync(organizationId); +} diff --git a/src/Api/AdminConsole/Authorization/Requirements/MemberOrProviderRequirement.cs b/src/Api/AdminConsole/Authorization/Requirements/MemberOrProviderRequirement.cs new file mode 100644 index 0000000000..b18928dbb8 --- /dev/null +++ b/src/Api/AdminConsole/Authorization/Requirements/MemberOrProviderRequirement.cs @@ -0,0 +1,18 @@ +#nullable enable + +using Bit.Api.AdminConsole.Context; +using Bit.Core.Context; + +namespace Bit.Api.AdminConsole.Authorization.Requirements; + +/// +/// Requires that the user is a member of the organization or a provider for the organization. +/// +public class MemberOrProviderRequirement : IOrganizationRequirement +{ + public async Task AuthorizeAsync( + Guid organizationId, + CurrentContextOrganization? organizationClaims, + IProviderOrganizationContext providerOrganizationContext) + => organizationClaims is not null || await providerOrganizationContext.ProviderUserForOrgAsync(organizationId); +} diff --git a/src/Api/AdminConsole/Context/IProviderOrganizationContext.cs b/src/Api/AdminConsole/Context/IProviderOrganizationContext.cs new file mode 100644 index 0000000000..c157c2df3e --- /dev/null +++ b/src/Api/AdminConsole/Context/IProviderOrganizationContext.cs @@ -0,0 +1,12 @@ +namespace Bit.Api.AdminConsole.Context; + +/// +/// Current context for the relationship between ProviderUsers and Organizations that they manage. +/// +public interface IProviderOrganizationContext +{ + /// + /// Returns true if the current user is a ProviderUser for the specified organization, false otherwise. + /// + Task ProviderUserForOrgAsync(Guid orgId); +} diff --git a/src/Api/AdminConsole/Context/ProviderOrganizationContext.cs b/src/Api/AdminConsole/Context/ProviderOrganizationContext.cs new file mode 100644 index 0000000000..98dec42d26 --- /dev/null +++ b/src/Api/AdminConsole/Context/ProviderOrganizationContext.cs @@ -0,0 +1,19 @@ +using Bit.Core.Context; + +namespace Bit.Api.AdminConsole.Context; + +public class ProviderOrganizationContext(ICurrentContext currentContext) : IProviderOrganizationContext +{ + /// + public async Task ProviderUserForOrgAsync(Guid orgId) + { + // If the user doesn't have any ProviderUser claims (in relation to the provider), they can't have a provider + // relationship to any organization. + if (currentContext.Providers.Count == 0) + { + return false; + } + + return await currentContext.ProviderUserForOrgAsync(orgId); + } +} diff --git a/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs b/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs index 426a41a17e..3a20cb39ea 100644 --- a/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs +++ b/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs @@ -1,4 +1,6 @@ -using Bit.Api.AdminConsole.Models.Request.Organizations; +using Bit.Api.AdminConsole.Authorization; +using Bit.Api.AdminConsole.Authorization.Requirements; +using Bit.Api.AdminConsole.Models.Request.Organizations; using Bit.Api.AdminConsole.Models.Response.Organizations; using Bit.Api.Models.Request.Organizations; using Bit.Api.Models.Response; @@ -6,11 +8,9 @@ using Bit.Api.Vault.AuthorizationHandlers.Collections; using Bit.Core; using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; -using Bit.Core.AdminConsole.OrganizationFeatures; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.Policies; using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; -using Bit.Core.AdminConsole.OrganizationFeatures.Shared.Authorization; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Auth.Enums; using Bit.Core.Auth.Repositories; diff --git a/src/Api/Startup.cs b/src/Api/Startup.cs index 138caed34b..c1375b2031 100644 --- a/src/Api/Startup.cs +++ b/src/Api/Startup.cs @@ -7,6 +7,7 @@ using Stripe; using Bit.Core.Utilities; using Duende.IdentityModel; using System.Globalization; +using Bit.Api.AdminConsole.Context; using Bit.Api.AdminConsole.Models.Request.Organizations; using Bit.Api.Auth.Models.Request; using Bit.Api.KeyManagement.Validators; @@ -84,6 +85,7 @@ public class Startup // Context services.AddScoped(); + services.AddScoped(); services.TryAddSingleton(); // Caching diff --git a/src/Api/Utilities/ServiceCollectionExtensions.cs b/src/Api/Utilities/ServiceCollectionExtensions.cs index c53c9f7692..4c8589657e 100644 --- a/src/Api/Utilities/ServiceCollectionExtensions.cs +++ b/src/Api/Utilities/ServiceCollectionExtensions.cs @@ -1,7 +1,7 @@ -using Bit.Api.Tools.Authorization; +using Bit.Api.AdminConsole.Authorization; +using Bit.Api.Tools.Authorization; using Bit.Api.Vault.AuthorizationHandlers.Collections; using Bit.Core.AdminConsole.OrganizationFeatures.Groups.Authorization; -using Bit.Core.AdminConsole.OrganizationFeatures.Shared.Authorization; using Bit.Core.IdentityServer; using Bit.Core.Settings; using Bit.Core.Utilities; diff --git a/src/Core/AdminConsole/OrganizationFeatures/ManageUsersRequirement.cs b/src/Core/AdminConsole/OrganizationFeatures/ManageUsersRequirement.cs deleted file mode 100644 index 6e528d9f5d..0000000000 --- a/src/Core/AdminConsole/OrganizationFeatures/ManageUsersRequirement.cs +++ /dev/null @@ -1,16 +0,0 @@ -#nullable enable - -using Bit.Core.AdminConsole.OrganizationFeatures.Shared.Authorization; -using Bit.Core.Context; -using Bit.Core.Enums; - -namespace Bit.Core.AdminConsole.OrganizationFeatures; - -public class ManageUsersRequirement : IOrganizationRequirement -{ - public async Task AuthorizeAsync(Guid organizationId, CurrentContextOrganization? organizationClaims, ICurrentContext currentContext) - => organizationClaims is - { Type: OrganizationUserType.Owner or OrganizationUserType.Admin } or - { Permissions.ManageUsers: true } - || await currentContext.ProviderUserForOrgAsync(organizationId); -} diff --git a/src/Core/AdminConsole/OrganizationFeatures/MemberOrProviderRequirement.cs b/src/Core/AdminConsole/OrganizationFeatures/MemberOrProviderRequirement.cs deleted file mode 100644 index e1c0c8f372..0000000000 --- a/src/Core/AdminConsole/OrganizationFeatures/MemberOrProviderRequirement.cs +++ /dev/null @@ -1,15 +0,0 @@ -#nullable enable - -using Bit.Core.AdminConsole.OrganizationFeatures.Shared.Authorization; -using Bit.Core.Context; - -namespace Bit.Core.AdminConsole.OrganizationFeatures; - -/// -/// Requires that the user is a member of the organization or a provider for the organization. -/// -public class MemberOrProviderRequirement : IOrganizationRequirement -{ - public async Task AuthorizeAsync(Guid organizationId, CurrentContextOrganization? organizationClaims, ICurrentContext currentContext) - => organizationClaims is not null || await currentContext.ProviderUserForOrgAsync(organizationId); -} diff --git a/src/Core/Context/ICurrentContext.cs b/src/Core/Context/ICurrentContext.cs index 42843ce6d7..e91e20009a 100644 --- a/src/Core/Context/ICurrentContext.cs +++ b/src/Core/Context/ICurrentContext.cs @@ -22,6 +22,7 @@ public interface ICurrentContext string IpAddress { get; set; } string CountryName { get; set; } List Organizations { get; set; } + List Providers { get; set; } Guid? InstallationId { get; set; } Guid? OrganizationId { get; set; } IdentityClientType IdentityClientType { get; set; } @@ -59,6 +60,7 @@ public interface ICurrentContext Task EditSubscription(Guid orgId); Task EditPaymentMethods(Guid orgId); Task ViewBillingHistory(Guid orgId); + [Obsolete("Use IProviderOrganizationContext.ProviderUserForOrgAsync instead.")] Task ProviderUserForOrgAsync(Guid orgId); bool ProviderProviderAdmin(Guid providerId); bool ProviderUser(Guid providerId);