Skip to content

Commit c86f5ef

Browse files
committed
Merge branch 'main' into ac/pm-27882/add-sendorganizationconfirmationcommand
2 parents d93d100 + 1b17d99 commit c86f5ef

File tree

20 files changed

+675
-150
lines changed

20 files changed

+675
-150
lines changed
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
using Bit.Core.Context;
2+
3+
namespace Bit.Api.AdminConsole.Authorization.Requirements;
4+
5+
/// <summary>
6+
/// Requires that the user is a member of the organization.
7+
/// </summary>
8+
public class MemberRequirement : IOrganizationRequirement
9+
{
10+
public Task<bool> AuthorizeAsync(
11+
CurrentContextOrganization? organizationClaims,
12+
Func<Task<bool>> isProviderUserForOrg)
13+
=> Task.FromResult(organizationClaims is not null);
14+
}

src/Api/AdminConsole/Controllers/OrganizationUsersController.cs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
2020
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers;
2121
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RestoreUser.v1;
22+
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.SelfRevokeUser;
2223
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
2324
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
2425
using Bit.Core.AdminConsole.Repositories;
@@ -81,6 +82,7 @@ public class OrganizationUsersController : BaseAdminConsoleController
8182
private readonly IInitPendingOrganizationCommand _initPendingOrganizationCommand;
8283
private readonly V1_RevokeOrganizationUserCommand _revokeOrganizationUserCommand;
8384
private readonly IAdminRecoverAccountCommand _adminRecoverAccountCommand;
85+
private readonly ISelfRevokeOrganizationUserCommand _selfRevokeOrganizationUserCommand;
8486

8587
public OrganizationUsersController(IOrganizationRepository organizationRepository,
8688
IOrganizationUserRepository organizationUserRepository,
@@ -112,7 +114,8 @@ public OrganizationUsersController(IOrganizationRepository organizationRepositor
112114
IBulkResendOrganizationInvitesCommand bulkResendOrganizationInvitesCommand,
113115
IAdminRecoverAccountCommand adminRecoverAccountCommand,
114116
IAutomaticallyConfirmOrganizationUserCommand automaticallyConfirmOrganizationUserCommand,
115-
V2_RevokeOrganizationUserCommand.IRevokeOrganizationUserCommand revokeOrganizationUserCommandVNext)
117+
V2_RevokeOrganizationUserCommand.IRevokeOrganizationUserCommand revokeOrganizationUserCommandVNext,
118+
ISelfRevokeOrganizationUserCommand selfRevokeOrganizationUserCommand)
116119
{
117120
_organizationRepository = organizationRepository;
118121
_organizationUserRepository = organizationUserRepository;
@@ -145,6 +148,7 @@ public OrganizationUsersController(IOrganizationRepository organizationRepositor
145148
_initPendingOrganizationCommand = initPendingOrganizationCommand;
146149
_revokeOrganizationUserCommand = revokeOrganizationUserCommand;
147150
_adminRecoverAccountCommand = adminRecoverAccountCommand;
151+
_selfRevokeOrganizationUserCommand = selfRevokeOrganizationUserCommand;
148152
}
149153

150154
[HttpGet("{id}")]
@@ -635,6 +639,20 @@ public async Task RevokeAsync(Guid orgId, Guid id)
635639
await RestoreOrRevokeUserAsync(orgId, id, _revokeOrganizationUserCommand.RevokeUserAsync);
636640
}
637641

642+
[HttpPut("revoke-self")]
643+
[Authorize<MemberRequirement>]
644+
public async Task<IResult> RevokeSelfAsync(Guid orgId)
645+
{
646+
var userId = _userService.GetProperUserId(User);
647+
if (!userId.HasValue)
648+
{
649+
throw new UnauthorizedAccessException();
650+
}
651+
652+
var result = await _selfRevokeOrganizationUserCommand.SelfRevokeUserAsync(orgId, userId.Value);
653+
return Handle(result);
654+
}
655+
638656
[HttpPatch("{id}/revoke")]
639657
[Obsolete("This endpoint is deprecated. Use PUT method instead")]
640658
[Authorize<ManageUsersRequirement>]

src/Api/AdminConsole/Controllers/PoliciesController.cs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
using Bit.Api.AdminConsole.Models.Response.Helpers;
88
using Bit.Api.AdminConsole.Models.Response.Organizations;
99
using Bit.Api.Models.Response;
10-
using Bit.Core;
1110
using Bit.Core.AdminConsole.Entities;
1211
using Bit.Core.AdminConsole.Enums;
1312
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces;
@@ -212,7 +211,6 @@ public async Task<PolicyResponseModel> Put(Guid orgId, PolicyType type, [FromBod
212211
}
213212

214213
[HttpPut("{type}/vnext")]
215-
[RequireFeatureAttribute(FeatureFlagKeys.CreateDefaultLocation)]
216214
[Authorize<ManagePoliciesRequirement>]
217215
public async Task<PolicyResponseModel> PutVNext(Guid orgId, PolicyType type, [FromBody] SavePolicyRequest model)
218216
{

src/Api/Tools/Controllers/OrganizationExportController.cs

Lines changed: 8 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
using Bit.Api.Tools.Authorization;
22
using Bit.Api.Tools.Models.Response;
3-
using Bit.Core;
43
using Bit.Core.AdminConsole.OrganizationFeatures.Shared.Authorization;
54
using Bit.Core.Exceptions;
65
using Bit.Core.Repositories;
@@ -21,7 +20,6 @@ public class OrganizationExportController : Controller
2120
private readonly IAuthorizationService _authorizationService;
2221
private readonly IOrganizationCiphersQuery _organizationCiphersQuery;
2322
private readonly ICollectionRepository _collectionRepository;
24-
private readonly IFeatureService _featureService;
2523

2624
public OrganizationExportController(
2725
IUserService userService,
@@ -36,7 +34,6 @@ public OrganizationExportController(
3634
_authorizationService = authorizationService;
3735
_organizationCiphersQuery = organizationCiphersQuery;
3836
_collectionRepository = collectionRepository;
39-
_featureService = featureService;
4037
}
4138

4239
[HttpGet("export")]
@@ -46,33 +43,20 @@ public async Task<IActionResult> Export(Guid organizationId)
4643
VaultExportOperations.ExportWholeVault);
4744
var canExportManaged = await _authorizationService.AuthorizeAsync(User, new OrganizationScope(organizationId),
4845
VaultExportOperations.ExportManagedCollections);
49-
var createDefaultLocationEnabled = _featureService.IsEnabled(FeatureFlagKeys.CreateDefaultLocation);
5046

5147
if (canExportAll.Succeeded)
5248
{
53-
if (createDefaultLocationEnabled)
54-
{
55-
var allOrganizationCiphers =
56-
await _organizationCiphersQuery.GetAllOrganizationCiphersExcludingDefaultUserCollections(
57-
organizationId);
49+
var allOrganizationCiphers =
50+
await _organizationCiphersQuery.GetAllOrganizationCiphersExcludingDefaultUserCollections(
51+
organizationId);
5852

59-
var allCollections = await _collectionRepository
60-
.GetManySharedCollectionsByOrganizationIdAsync(
61-
organizationId);
53+
var allCollections = await _collectionRepository
54+
.GetManySharedCollectionsByOrganizationIdAsync(
55+
organizationId);
6256

6357

64-
return Ok(new OrganizationExportResponseModel(allOrganizationCiphers, allCollections,
65-
_globalSettings));
66-
}
67-
else
68-
{
69-
var allOrganizationCiphers = await _organizationCiphersQuery.GetAllOrganizationCiphers(organizationId);
70-
71-
var allCollections = await _collectionRepository.GetManyByOrganizationIdAsync(organizationId);
72-
73-
return Ok(new OrganizationExportResponseModel(allOrganizationCiphers, allCollections,
74-
_globalSettings));
75-
}
58+
return Ok(new OrganizationExportResponseModel(allOrganizationCiphers, allCollections,
59+
_globalSettings));
7660
}
7761

7862
if (canExportManaged.Succeeded)

src/Api/Vault/Controllers/CiphersController.cs

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
using Bit.Api.Vault.Models.Request;
1111
using Bit.Api.Vault.Models.Response;
1212
using Bit.Core;
13-
using Bit.Core.AdminConsole.Services;
1413
using Bit.Core.Context;
1514
using Bit.Core.Entities;
1615
using Bit.Core.Enums;
@@ -43,7 +42,6 @@ public class CiphersController : Controller
4342
private readonly ICipherService _cipherService;
4443
private readonly IUserService _userService;
4544
private readonly IAttachmentStorageService _attachmentStorageService;
46-
private readonly IProviderService _providerService;
4745
private readonly ICurrentContext _currentContext;
4846
private readonly ILogger<CiphersController> _logger;
4947
private readonly GlobalSettings _globalSettings;
@@ -52,31 +50,27 @@ public class CiphersController : Controller
5250
private readonly ICollectionRepository _collectionRepository;
5351
private readonly IArchiveCiphersCommand _archiveCiphersCommand;
5452
private readonly IUnarchiveCiphersCommand _unarchiveCiphersCommand;
55-
private readonly IFeatureService _featureService;
5653

5754
public CiphersController(
5855
ICipherRepository cipherRepository,
5956
ICollectionCipherRepository collectionCipherRepository,
6057
ICipherService cipherService,
6158
IUserService userService,
6259
IAttachmentStorageService attachmentStorageService,
63-
IProviderService providerService,
6460
ICurrentContext currentContext,
6561
ILogger<CiphersController> logger,
6662
GlobalSettings globalSettings,
6763
IOrganizationCiphersQuery organizationCiphersQuery,
6864
IApplicationCacheService applicationCacheService,
6965
ICollectionRepository collectionRepository,
7066
IArchiveCiphersCommand archiveCiphersCommand,
71-
IUnarchiveCiphersCommand unarchiveCiphersCommand,
72-
IFeatureService featureService)
67+
IUnarchiveCiphersCommand unarchiveCiphersCommand)
7368
{
7469
_cipherRepository = cipherRepository;
7570
_collectionCipherRepository = collectionCipherRepository;
7671
_cipherService = cipherService;
7772
_userService = userService;
7873
_attachmentStorageService = attachmentStorageService;
79-
_providerService = providerService;
8074
_currentContext = currentContext;
8175
_logger = logger;
8276
_globalSettings = globalSettings;
@@ -85,7 +79,6 @@ public CiphersController(
8579
_collectionRepository = collectionRepository;
8680
_archiveCiphersCommand = archiveCiphersCommand;
8781
_unarchiveCiphersCommand = unarchiveCiphersCommand;
88-
_featureService = featureService;
8982
}
9083

9184
[HttpGet("{id}")]
@@ -344,8 +337,7 @@ public async Task<ListResponseModel<CipherMiniDetailsResponseModel>> GetOrganiza
344337
throw new NotFoundException();
345338
}
346339

347-
bool excludeDefaultUserCollections = _featureService.IsEnabled(FeatureFlagKeys.CreateDefaultLocation) && !includeMemberItems;
348-
var allOrganizationCiphers = excludeDefaultUserCollections
340+
var allOrganizationCiphers = !includeMemberItems
349341
?
350342
await _organizationCiphersQuery.GetAllOrganizationCiphersExcludingDefaultUserCollections(organizationId)
351343
:

src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommand.cs

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -282,11 +282,6 @@ private async Task<IEnumerable<string>> GetUserDeviceIdsAsync(Guid userId)
282282
/// <param name="defaultUserCollectionName">The encrypted default user collection name.</param>
283283
private async Task CreateDefaultCollectionAsync(OrganizationUser organizationUser, string defaultUserCollectionName)
284284
{
285-
if (!_featureService.IsEnabled(FeatureFlagKeys.CreateDefaultLocation))
286-
{
287-
return;
288-
}
289-
290285
// Skip if no collection name provided (backwards compatibility)
291286
if (string.IsNullOrWhiteSpace(defaultUserCollectionName))
292287
{
@@ -325,11 +320,6 @@ private async Task CreateDefaultCollectionAsync(OrganizationUser organizationUse
325320
private async Task CreateManyDefaultCollectionsAsync(Guid organizationId,
326321
IEnumerable<OrganizationUser> confirmedOrganizationUsers, string defaultUserCollectionName)
327322
{
328-
if (!_featureService.IsEnabled(FeatureFlagKeys.CreateDefaultLocation))
329-
{
330-
return;
331-
}
332-
333323
// Skip if no collection name provided (backwards compatibility)
334324
if (string.IsNullOrWhiteSpace(defaultUserCollectionName))
335325
{
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
using Bit.Core.AdminConsole.Utilities.v2;
2+
3+
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.SelfRevokeUser;
4+
5+
public record OrganizationUserNotFound() : NotFoundError("Organization user not found.");
6+
public record NotEligibleForSelfRevoke() : BadRequestError("User is not eligible for self-revocation. The organization data ownership policy must be enabled and the user must be a confirmed member.");
7+
public record LastOwnerCannotSelfRevoke() : BadRequestError("The last owner cannot revoke themselves.");
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
using Bit.Core.AdminConsole.Utilities.v2.Results;
2+
3+
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.SelfRevokeUser;
4+
5+
/// <summary>
6+
/// Allows users to revoke themselves from an organization when declining to migrate personal items
7+
/// under the OrganizationDataOwnership policy.
8+
/// </summary>
9+
public interface ISelfRevokeOrganizationUserCommand
10+
{
11+
/// <summary>
12+
/// Revokes a user from an organization.
13+
/// </summary>
14+
/// <param name="organizationId">The organization ID.</param>
15+
/// <param name="userId">The user ID to revoke.</param>
16+
/// <returns>A <see cref="CommandResult"/> indicating success or containing an error.</returns>
17+
/// <remarks>
18+
/// Validates the OrganizationDataOwnership policy is enabled and applies to the user (currently Owners/Admins are exempt),
19+
/// the user is a confirmed member, and prevents the last owner from revoking themselves.
20+
/// </remarks>
21+
Task<CommandResult> SelfRevokeUserAsync(Guid organizationId, Guid userId);
22+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
2+
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
3+
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
4+
using Bit.Core.AdminConsole.Utilities.v2.Results;
5+
using Bit.Core.Enums;
6+
using Bit.Core.Platform.Push;
7+
using Bit.Core.Repositories;
8+
using Bit.Core.Services;
9+
using OneOf.Types;
10+
11+
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.SelfRevokeUser;
12+
13+
public class SelfRevokeOrganizationUserCommand(
14+
IOrganizationUserRepository organizationUserRepository,
15+
IPolicyRequirementQuery policyRequirementQuery,
16+
IHasConfirmedOwnersExceptQuery hasConfirmedOwnersExceptQuery,
17+
IEventService eventService,
18+
IPushNotificationService pushNotificationService)
19+
: ISelfRevokeOrganizationUserCommand
20+
{
21+
public async Task<CommandResult> SelfRevokeUserAsync(Guid organizationId, Guid userId)
22+
{
23+
var organizationUser = await organizationUserRepository.GetByOrganizationAsync(organizationId, userId);
24+
if (organizationUser == null)
25+
{
26+
return new OrganizationUserNotFound();
27+
}
28+
29+
var policyRequirement = await policyRequirementQuery.GetAsync<OrganizationDataOwnershipPolicyRequirement>(userId);
30+
31+
if (!policyRequirement.EligibleForSelfRevoke(organizationId))
32+
{
33+
return new NotEligibleForSelfRevoke();
34+
}
35+
36+
// Prevent the last owner from revoking themselves, which would brick the organization
37+
if (organizationUser.Type == OrganizationUserType.Owner)
38+
{
39+
var hasOtherOwner = await hasConfirmedOwnersExceptQuery.HasConfirmedOwnersExceptAsync(
40+
organizationId,
41+
[organizationUser.Id],
42+
includeProvider: true);
43+
44+
if (!hasOtherOwner)
45+
{
46+
return new LastOwnerCannotSelfRevoke();
47+
}
48+
}
49+
50+
await organizationUserRepository.RevokeAsync(organizationUser.Id);
51+
await eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_SelfRevoked);
52+
await pushNotificationService.PushSyncOrgKeysAsync(organizationUser.UserId!.Value);
53+
54+
return new None();
55+
}
56+
}

src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/OrganizationDataOwnershipPolicyRequirement.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,24 @@ public bool IgnoreStorageLimitsOnMigration(Guid organizationId)
8383
return _policyDetails.Any(p => p.OrganizationId == organizationId &&
8484
p.OrganizationUserStatus == OrganizationUserStatusType.Confirmed);
8585
}
86+
87+
/// <summary>
88+
/// Determines if a user is eligible for self-revocation under the Organization Data Ownership policy.
89+
/// A user is eligible if they are a confirmed member of the organization and the policy is enabled.
90+
/// This also handles exempt roles (Owner/Admin) and policy disabled state via the factory's Enforce predicate.
91+
/// </summary>
92+
/// <param name="organizationId">The organization ID to check.</param>
93+
/// <returns>True if the user is eligible for self-revocation (policy applies to them), false otherwise.</returns>
94+
/// <remarks>
95+
/// Self-revoke is used to opt out of migrating the user's personal vault to the organization as required by this policy.
96+
/// </remarks>
97+
public bool EligibleForSelfRevoke(Guid organizationId)
98+
{
99+
var policyDetail = _policyDetails
100+
.FirstOrDefault(p => p.OrganizationId == organizationId);
101+
102+
return policyDetail?.HasStatus([OrganizationUserStatusType.Confirmed]) ?? false;
103+
}
86104
}
87105

88106
public record DefaultCollectionRequest(Guid OrganizationUserId, bool ShouldCreateDefaultCollection)

0 commit comments

Comments
 (0)