From e9222e7fce2c1128daffc5965c1f6c9f9e82abe6 Mon Sep 17 00:00:00 2001 From: Brandon Date: Mon, 31 Mar 2025 13:56:58 -0400 Subject: [PATCH] wip --- .../OrganizationUsersController.cs | 10 ++- .../OrganizationAuthorizationHandler.cs | 47 ++++++++++++ .../Authorization/OrganizationOperation.cs | 10 +++ .../InitPendingOrganizationCommand.cs | 76 +++++++++++++++++++ .../IInitPendingOrganizationCommand.cs | 9 +++ .../Services/IOrganizationService.cs | 1 + .../Implementations/OrganizationService.cs | 2 +- ...OrganizationServiceCollectionExtensions.cs | 2 + 8 files changed, 155 insertions(+), 2 deletions(-) create mode 100644 src/Core/AdminConsole/OrganizationFeatures/Organizations/Authorization/OrganizationAuthorizationHandler.cs create mode 100644 src/Core/AdminConsole/OrganizationFeatures/Organizations/Authorization/OrganizationOperation.cs create mode 100644 src/Core/AdminConsole/OrganizationFeatures/Organizations/InitPendingOrganizationCommand.cs create mode 100644 src/Core/AdminConsole/OrganizationFeatures/Organizations/Interfaces/IInitPendingOrganizationCommand.cs diff --git a/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs b/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs index 5fd9109077..d646f9fcce 100644 --- a/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs +++ b/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs @@ -6,6 +6,7 @@ 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.Organizations.Authorization; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Authorization; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RestoreUser.v1; @@ -63,6 +64,7 @@ public class OrganizationUsersController : Controller private readonly IPricingClient _pricingClient; private readonly IConfirmOrganizationUserCommand _confirmOrganizationUserCommand; private readonly IRestoreOrganizationUserCommand _restoreOrganizationUserCommand; + private readonly IInitPendingOrganizationCommand _initPendingOrganizationCommand; public OrganizationUsersController( IOrganizationRepository organizationRepository, @@ -313,7 +315,13 @@ public class OrganizationUsersController : Controller throw new UnauthorizedAccessException(); } - await _organizationService.InitPendingOrganization(user.Id, orgId, organizationUserId, model.Keys.PublicKey, model.Keys.EncryptedPrivateKey, model.CollectionName); + var authorizationResult = await _authorizationService.AuthorizeAsync(User, new OrganizationScope(orgId), OrganizationOperations.Update); + if (!authorizationResult.Succeeded) + { + throw new NotFoundException(); + } + + await _initPendingOrganizationCommand.InitPendingOrganizationAsync(user.Id, orgId, organizationUserId, model.Keys.PublicKey, model.Keys.EncryptedPrivateKey, model.CollectionName); await _acceptOrgUserCommand.AcceptOrgUserByEmailTokenAsync(organizationUserId, user, model.Token, _userService); await _confirmOrganizationUserCommand.ConfirmUserAsync(orgId, organizationUserId, model.Key, user.Id); } diff --git a/src/Core/AdminConsole/OrganizationFeatures/Organizations/Authorization/OrganizationAuthorizationHandler.cs b/src/Core/AdminConsole/OrganizationFeatures/Organizations/Authorization/OrganizationAuthorizationHandler.cs new file mode 100644 index 0000000000..6d41e5ed98 --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/Organizations/Authorization/OrganizationAuthorizationHandler.cs @@ -0,0 +1,47 @@ +#nullable enable +using Bit.Core.AdminConsole.OrganizationFeatures.Shared.Authorization; +using Bit.Core.Context; +using Microsoft.AspNetCore.Authorization; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Authorization; + +public class OrganizationAuthorizationHandler + : AuthorizationHandler +{ + private readonly ICurrentContext _currentContext; + + public OrganizationAuthorizationHandler(ICurrentContext currentContext) + { + _currentContext = currentContext; + } + + protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, + OrganizationOperationRequirement requirement, OrganizationScope organizationScope) + { + var authorized = false; + + switch (requirement) + { + case not null when requirement.Name == nameof(OrganizationOperations.Update): + authorized = await CanUpdateAsync(organizationScope); + break; + } + + if (authorized) + { + context.Succeed(requirement!); + } + } + + private async Task CanUpdateAsync(Guid organizationId) + { + var organization = _currentContext.GetOrganization(organizationId); + if (organization != null) + { + return true; + } + + // Allow provider users to update organization data if they are a provider for the target organization + return await _currentContext.ProviderUserForOrgAsync(organizationId); + } +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/Organizations/Authorization/OrganizationOperation.cs b/src/Core/AdminConsole/OrganizationFeatures/Organizations/Authorization/OrganizationOperation.cs new file mode 100644 index 0000000000..1a58e15b57 --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/Organizations/Authorization/OrganizationOperation.cs @@ -0,0 +1,10 @@ +using Microsoft.AspNetCore.Authorization.Infrastructure; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Authorization; + +public class OrganizationOperationRequirement : OperationAuthorizationRequirement; + +public static class OrganizationOperations +{ + public static OrganizationOperationRequirement Update = new() { Name = nameof(Update) }; +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/Organizations/InitPendingOrganizationCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/Organizations/InitPendingOrganizationCommand.cs new file mode 100644 index 0000000000..7a9e5f7c72 --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/Organizations/InitPendingOrganizationCommand.cs @@ -0,0 +1,76 @@ +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Exceptions; +using Bit.Core.Models.Data; +using Bit.Core.OrganizationFeatures.OrganizationUsers.Interfaces; +using Bit.Core.Repositories; +using Bit.Core.Services; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers; + +public class InitPendingOrganizationCommand : IInitPendingOrganizationCommand +{ + + private readonly IOrganizationService _organizationService; + private readonly ICollectionRepository _collectionRepository; + private readonly IOrganizationRepository _organizationRepository; + + public InitPendingOrganizationCommand( + IOrganizationService organizationService, + ICollectionRepository collectionRepository, + IOrganizationRepository organizationRepository + ) + { + _organizationService = organizationService; + _collectionRepository = collectionRepository; + _organizationRepository = organizationRepository; + } + + public async Task InitPendingOrganizationAsync(Guid userId, Guid organizationId, Guid organizationUserId, string publicKey, string privateKey, string collectionName) + { + await _organizationService.ValidateSignUpPoliciesAsync(userId); + + var org = await _organizationRepository.GetByIdAsync(organizationId); + + if (org.Enabled) + { + throw new BadRequestException("Organization is already enabled."); + } + + if (org.Status != OrganizationStatusType.Pending) + { + throw new BadRequestException("Organization is not on a Pending status."); + } + + if (!string.IsNullOrEmpty(org.PublicKey)) + { + throw new BadRequestException("Organization already has a Public Key."); + } + + if (!string.IsNullOrEmpty(org.PrivateKey)) + { + throw new BadRequestException("Organization already has a Private Key."); + } + + org.Enabled = true; + org.Status = OrganizationStatusType.Created; + org.PublicKey = publicKey; + org.PrivateKey = privateKey; + + await _organizationService.UpdateAsync(org); + + if (!string.IsNullOrWhiteSpace(collectionName)) + { + // give the owner Can Manage access over the default collection + List defaultOwnerAccess = + [new CollectionAccessSelection { Id = organizationUserId, HidePasswords = false, ReadOnly = false, Manage = true }]; + + var defaultCollection = new Collection + { + Name = collectionName, + OrganizationId = org.Id + }; + await _collectionRepository.CreateAsync(defaultCollection, null, defaultOwnerAccess); + } + } +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/Organizations/Interfaces/IInitPendingOrganizationCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/Organizations/Interfaces/IInitPendingOrganizationCommand.cs new file mode 100644 index 0000000000..4806ab981b --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/Organizations/Interfaces/IInitPendingOrganizationCommand.cs @@ -0,0 +1,9 @@ +namespace Bit.Core.OrganizationFeatures.OrganizationUsers.Interfaces; + +public interface IInitPendingOrganizationCommand +{ + /// + /// Accept an invitation to initialize and join an organization created via the Admin Portal + /// + Task InitPendingOrganizationAsync(Guid userId, Guid organizationId, Guid organizationUserId, string publicKey, string privateKey, string collectionName); +} diff --git a/src/Core/AdminConsole/Services/IOrganizationService.cs b/src/Core/AdminConsole/Services/IOrganizationService.cs index e0088f1f74..d2a6c16e62 100644 --- a/src/Core/AdminConsole/Services/IOrganizationService.cs +++ b/src/Core/AdminConsole/Services/IOrganizationService.cs @@ -63,4 +63,5 @@ public interface IOrganizationService Task ValidateOrganizationUserUpdatePermissions(Guid organizationId, OrganizationUserType newType, OrganizationUserType? oldType, Permissions permissions); Task ValidateOrganizationCustomPermissionsEnabledAsync(Guid organizationId, OrganizationUserType newType); + Task ValidateSignUpPoliciesAsync(Guid ownerId); } diff --git a/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs b/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs index 64bd434327..ac5f56ede5 100644 --- a/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs +++ b/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs @@ -496,7 +496,7 @@ public class OrganizationService : IOrganizationService return returnValue; } - private async Task ValidateSignUpPoliciesAsync(Guid ownerId) + public async Task ValidateSignUpPoliciesAsync(Guid ownerId) { var anySingleOrgPolicies = await _policyService.AnyPoliciesApplicableToUserAsync(ownerId, PolicyType.SingleOrg); if (anySingleOrgPolicies) diff --git a/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs b/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs index 59cfdace65..cf37b9a3d8 100644 --- a/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs +++ b/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs @@ -9,6 +9,7 @@ using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationConnections.Interfa using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.Organizations; +using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Authorization; using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Authorization; @@ -173,6 +174,7 @@ public static class OrganizationServiceCollectionExtensions services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); }