Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weโ€™ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[PM-19332] Create InitPendingOrganizationCommand #5584

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
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;
Expand Down Expand Up @@ -63,6 +64,7 @@
private readonly IPricingClient _pricingClient;
private readonly IConfirmOrganizationUserCommand _confirmOrganizationUserCommand;
private readonly IRestoreOrganizationUserCommand _restoreOrganizationUserCommand;
private readonly IInitPendingOrganizationCommand _initPendingOrganizationCommand;

Check failure on line 67 in src/Api/AdminConsole/Controllers/OrganizationUsersController.cs

View workflow job for this annotation

GitHub Actions / Run tests

Field 'OrganizationUsersController._initPendingOrganizationCommand' is never assigned to, and will always have its default value null

Check failure on line 67 in src/Api/AdminConsole/Controllers/OrganizationUsersController.cs

View workflow job for this annotation

GitHub Actions / Build artifacts (Api, ./src)

Field 'OrganizationUsersController._initPendingOrganizationCommand' is never assigned to, and will always have its default value null

public OrganizationUsersController(
IOrganizationRepository organizationRepository,
Expand Down Expand Up @@ -313,7 +315,13 @@
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);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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<OrganizationOperationRequirement, OrganizationScope>
{
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<bool> CanUpdateAsync(Guid organizationId)
{
var organization = _currentContext.GetOrganization(organizationId);
if (organization != null)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not fully sure which checks here would really be best

{
return true;
}

// Allow provider users to update organization data if they are a provider for the target organization
return await _currentContext.ProviderUserForOrgAsync(organizationId);
}
}
Original file line number Diff line number Diff line change
@@ -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) };
}
Original file line number Diff line number Diff line change
@@ -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<CollectionAccessSelection> 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);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
๏ปฟnamespace Bit.Core.OrganizationFeatures.OrganizationUsers.Interfaces;

public interface IInitPendingOrganizationCommand
{
/// <summary>
/// Accept an invitation to initialize and join an organization created via the Admin Portal
/// </summary>
Task InitPendingOrganizationAsync(Guid userId, Guid organizationId, Guid organizationUserId, string publicKey, string privateKey, string collectionName);
}
1 change: 1 addition & 0 deletions src/Core/AdminConsole/Services/IOrganizationService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -63,4 +63,5 @@ Task<List<Tuple<OrganizationUser, string>>> RevokeUsersAsync(Guid organizationId
Task ValidateOrganizationUserUpdatePermissions(Guid organizationId, OrganizationUserType newType,
OrganizationUserType? oldType, Permissions permissions);
Task ValidateOrganizationCustomPermissionsEnabledAsync(Guid organizationId, OrganizationUserType newType);
Task ValidateSignUpPoliciesAsync(Guid ownerId);
Copy link
Member

@eliykat eliykat Apr 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method is just a single policy check - I don't think it needs to be reused. I suggest copying it into the new command rather than exposing it as public here. One less tie back to OrganizationService. (It's also what we did in the cloud org signup command)

}
Original file line number Diff line number Diff line change
Expand Up @@ -496,7 +496,7 @@ await _referenceEventService.RaiseEventAsync(
return returnValue;
}

private async Task ValidateSignUpPoliciesAsync(Guid ownerId)
public async Task ValidateSignUpPoliciesAsync(Guid ownerId)
{
var anySingleOrgPolicies = await _policyService.AnyPoliciesApplicableToUserAsync(ownerId, PolicyType.SingleOrg);
if (anySingleOrgPolicies)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
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;
Expand Down Expand Up @@ -173,6 +174,7 @@ private static void AddOrganizationUserCommandsQueries(this IServiceCollection s

services.AddScoped<IAuthorizationHandler, OrganizationUserUserMiniDetailsAuthorizationHandler>();
services.AddScoped<IAuthorizationHandler, OrganizationUserUserDetailsAuthorizationHandler>();
services.AddScoped<IAuthorizationHandler, OrganizationAuthorizationHandler>();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this goes here... Which method should contain this, AddOrganizationAuthCommands?

services.AddScoped<IHasConfirmedOwnersExceptQuery, HasConfirmedOwnersExceptQuery>();
}

Expand Down
Loading