Skip to content

Commit 018417b

Browse files
Merge branch 'main' into billing/pm-29061/remove-ff-24996
2 parents 90f6588 + 2026ca1 commit 018417b

247 files changed

Lines changed: 25630 additions & 1231 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/review-code.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ name: Code Review
22

33
on:
44
pull_request:
5-
types: [opened, synchronize, reopened, ready_for_review]
5+
types: [opened, synchronize, reopened]
66

77
permissions: {}
88

bitwarden_license/src/Sso/Controllers/AccountController.cs

Lines changed: 4 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -680,22 +680,10 @@ await _organizationService.AdjustSeatsAsync(organization.Id,
680680
ApiKey = CoreHelpers.SecureRandomString(30)
681681
};
682682

683-
/*
684-
The feature flag is checked here so that we can send the new MJML welcome email templates.
685-
The other organization invites flows have an OrganizationUser allowing the RegisterUserCommand the ability
686-
to fetch the Organization. The old method RegisterUser(User) here does not have that context, so we need
687-
to use a new method RegisterSSOAutoProvisionedUserAsync(User, Organization) to send the correct email.
688-
[PM-28057]: Prefer RegisterSSOAutoProvisionedUserAsync for SSO auto-provisioned users.
689-
TODO: Remove Feature flag: PM-28221
690-
*/
691-
if (_featureService.IsEnabled(FeatureFlagKeys.MjmlWelcomeEmailTemplates))
692-
{
693-
await _registerUserCommand.RegisterSSOAutoProvisionedUserAsync(newUser, organization);
694-
}
695-
else
696-
{
697-
await _registerUserCommand.RegisterUser(newUser);
698-
}
683+
// Always use RegisterSSOAutoProvisionedUserAsync to ensure organization context is available
684+
// for domain validation (BlockClaimedDomainAccountCreation policy) and welcome emails.
685+
// The feature flag logic for welcome email templates is handled internally by RegisterUserCommand.
686+
await _registerUserCommand.RegisterSSOAutoProvisionedUserAsync(newUser, organization);
699687

700688
// If the organization has 2fa policy enabled, make sure to default jit user 2fa to email
701689
var twoFactorPolicy =

bitwarden_license/test/SSO.Test/Controllers/AccountControllerTest.cs

Lines changed: 0 additions & 129 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
using Bit.Core.Auth.Models.Business.Tokenables;
77
using Bit.Core.Auth.Models.Data;
88
using Bit.Core.Auth.Repositories;
9-
using Bit.Core.Auth.UserFeatures.Registration;
109
using Bit.Core.Entities;
1110
using Bit.Core.Enums;
1211
using Bit.Core.Repositories;
@@ -21,7 +20,6 @@
2120
using Duende.IdentityServer.Services;
2221
using Microsoft.AspNetCore.Authentication;
2322
using Microsoft.AspNetCore.Http;
24-
using Microsoft.AspNetCore.Identity;
2523
using Microsoft.AspNetCore.Mvc;
2624
using Microsoft.Extensions.DependencyInjection;
2725
using NSubstitute;
@@ -1013,133 +1011,6 @@ public async Task ExternalCallback_Measurements_FlagOnVsOff_Comparisons(
10131011
}
10141012
}
10151013

1016-
[Theory, BitAutoData]
1017-
public async Task AutoProvisionUserAsync_WithFeatureFlagEnabled_CallsRegisterSSOAutoProvisionedUser(
1018-
SutProvider<AccountController> sutProvider)
1019-
{
1020-
// Arrange
1021-
var orgId = Guid.NewGuid();
1022-
var providerUserId = "ext-new-user";
1023-
var email = "newuser@example.com";
1024-
var organization = new Organization { Id = orgId, Name = "Test Org", Seats = null };
1025-
1026-
// No existing user (JIT provisioning scenario)
1027-
sutProvider.GetDependency<IUserRepository>().GetByEmailAsync(email).Returns((User?)null);
1028-
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(orgId).Returns(organization);
1029-
sutProvider.GetDependency<IOrganizationUserRepository>().GetByOrganizationEmailAsync(orgId, email)
1030-
.Returns((OrganizationUser?)null);
1031-
1032-
// Feature flag enabled
1033-
sutProvider.GetDependency<IFeatureService>()
1034-
.IsEnabled(FeatureFlagKeys.MjmlWelcomeEmailTemplates)
1035-
.Returns(true);
1036-
1037-
// Mock the RegisterSSOAutoProvisionedUserAsync to return success
1038-
sutProvider.GetDependency<IRegisterUserCommand>()
1039-
.RegisterSSOAutoProvisionedUserAsync(Arg.Any<User>(), Arg.Any<Organization>())
1040-
.Returns(IdentityResult.Success);
1041-
1042-
var claims = new[]
1043-
{
1044-
new Claim(JwtClaimTypes.Email, email),
1045-
new Claim(JwtClaimTypes.Name, "New User")
1046-
} as IEnumerable<Claim>;
1047-
var config = new SsoConfigurationData();
1048-
1049-
var method = typeof(AccountController).GetMethod(
1050-
"CreateUserAndOrgUserConditionallyAsync",
1051-
BindingFlags.Instance | BindingFlags.NonPublic);
1052-
Assert.NotNull(method);
1053-
1054-
// Act
1055-
var task = (Task<(User user, Organization organization, OrganizationUser orgUser)>)method!.Invoke(
1056-
sutProvider.Sut,
1057-
new object[]
1058-
{
1059-
orgId.ToString(),
1060-
providerUserId,
1061-
claims,
1062-
null!,
1063-
config
1064-
})!;
1065-
1066-
var result = await task;
1067-
1068-
// Assert
1069-
await sutProvider.GetDependency<IRegisterUserCommand>().Received(1)
1070-
.RegisterSSOAutoProvisionedUserAsync(
1071-
Arg.Is<User>(u => u.Email == email && u.Name == "New User"),
1072-
Arg.Is<Organization>(o => o.Id == orgId && o.Name == "Test Org"));
1073-
1074-
Assert.NotNull(result.user);
1075-
Assert.Equal(email, result.user.Email);
1076-
Assert.Equal(organization.Id, result.organization.Id);
1077-
}
1078-
1079-
[Theory, BitAutoData]
1080-
public async Task AutoProvisionUserAsync_WithFeatureFlagDisabled_CallsRegisterUserInstead(
1081-
SutProvider<AccountController> sutProvider)
1082-
{
1083-
// Arrange
1084-
var orgId = Guid.NewGuid();
1085-
var providerUserId = "ext-legacy-user";
1086-
var email = "legacyuser@example.com";
1087-
var organization = new Organization { Id = orgId, Name = "Test Org", Seats = null };
1088-
1089-
// No existing user (JIT provisioning scenario)
1090-
sutProvider.GetDependency<IUserRepository>().GetByEmailAsync(email).Returns((User?)null);
1091-
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(orgId).Returns(organization);
1092-
sutProvider.GetDependency<IOrganizationUserRepository>().GetByOrganizationEmailAsync(orgId, email)
1093-
.Returns((OrganizationUser?)null);
1094-
1095-
// Feature flag disabled
1096-
sutProvider.GetDependency<IFeatureService>()
1097-
.IsEnabled(FeatureFlagKeys.MjmlWelcomeEmailTemplates)
1098-
.Returns(false);
1099-
1100-
// Mock the RegisterUser to return success
1101-
sutProvider.GetDependency<IRegisterUserCommand>()
1102-
.RegisterUser(Arg.Any<User>())
1103-
.Returns(IdentityResult.Success);
1104-
1105-
var claims = new[]
1106-
{
1107-
new Claim(JwtClaimTypes.Email, email),
1108-
new Claim(JwtClaimTypes.Name, "Legacy User")
1109-
} as IEnumerable<Claim>;
1110-
var config = new SsoConfigurationData();
1111-
1112-
var method = typeof(AccountController).GetMethod(
1113-
"CreateUserAndOrgUserConditionallyAsync",
1114-
BindingFlags.Instance | BindingFlags.NonPublic);
1115-
Assert.NotNull(method);
1116-
1117-
// Act
1118-
var task = (Task<(User user, Organization organization, OrganizationUser orgUser)>)method!.Invoke(
1119-
sutProvider.Sut,
1120-
new object[]
1121-
{
1122-
orgId.ToString(),
1123-
providerUserId,
1124-
claims,
1125-
null!,
1126-
config
1127-
})!;
1128-
1129-
var result = await task;
1130-
1131-
// Assert
1132-
await sutProvider.GetDependency<IRegisterUserCommand>().Received(1)
1133-
.RegisterUser(Arg.Is<User>(u => u.Email == email && u.Name == "Legacy User"));
1134-
1135-
// Verify the new method was NOT called
1136-
await sutProvider.GetDependency<IRegisterUserCommand>().DidNotReceive()
1137-
.RegisterSSOAutoProvisionedUserAsync(Arg.Any<User>(), Arg.Any<Organization>());
1138-
1139-
Assert.NotNull(result.user);
1140-
Assert.Equal(email, result.user.Email);
1141-
}
1142-
11431014
[Theory, BitAutoData]
11441015
public void ExternalChallenge_WithMatchingOrgId_Succeeds(
11451016
SutProvider<AccountController> sutProvider,
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/Auth/Controllers/WebAuthnController.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121
namespace Bit.Api.Auth.Controllers;
2222

2323
[Route("webauthn")]
24-
[Authorize(Policies.Web)]
2524
public class WebAuthnController : Controller
2625
{
2726
private readonly IUserService _userService;
@@ -62,6 +61,7 @@ public WebAuthnController(
6261
_featureService = featureService;
6362
}
6463

64+
[Authorize(Policies.Web)]
6565
[HttpGet("")]
6666
public async Task<ListResponseModel<WebAuthnCredentialResponseModel>> Get()
6767
{
@@ -71,6 +71,7 @@ public async Task<ListResponseModel<WebAuthnCredentialResponseModel>> Get()
7171
return new ListResponseModel<WebAuthnCredentialResponseModel>(credentials.Select(c => new WebAuthnCredentialResponseModel(c)));
7272
}
7373

74+
[Authorize(Policies.Application)]
7475
[HttpPost("attestation-options")]
7576
public async Task<WebAuthnCredentialCreateOptionsResponseModel> AttestationOptions([FromBody] SecretVerificationRequestModel model)
7677
{
@@ -88,6 +89,7 @@ public async Task<WebAuthnCredentialCreateOptionsResponseModel> AttestationOptio
8889
};
8990
}
9091

92+
[Authorize(Policies.Web)]
9193
[HttpPost("assertion-options")]
9294
public async Task<WebAuthnLoginAssertionOptionsResponseModel> AssertionOptions([FromBody] SecretVerificationRequestModel model)
9395
{
@@ -104,6 +106,7 @@ public async Task<WebAuthnLoginAssertionOptionsResponseModel> AssertionOptions([
104106
};
105107
}
106108

109+
[Authorize(Policies.Application)]
107110
[HttpPost("")]
108111
public async Task Post([FromBody] WebAuthnLoginCredentialCreateRequestModel model)
109112
{
@@ -149,6 +152,7 @@ private async Task ValidateIfUserCanUsePasskeyLogin(Guid userId)
149152
}
150153
}
151154

155+
[Authorize(Policies.Application)]
152156
[HttpPut()]
153157
public async Task UpdateCredential([FromBody] WebAuthnLoginCredentialUpdateRequestModel model)
154158
{
@@ -172,6 +176,7 @@ public async Task UpdateCredential([FromBody] WebAuthnLoginCredentialUpdateReque
172176
await _credentialRepository.UpdateAsync(credential);
173177
}
174178

179+
[Authorize(Policies.Web)]
175180
[HttpPost("{id}/delete")]
176181
public async Task Delete(Guid id, [FromBody] SecretVerificationRequestModel model)
177182
{

src/Api/Auth/Models/Request/TwoFactorRequestModels.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -273,7 +273,7 @@ public override IEnumerable<ValidationResult> Validate(ValidationContext validat
273273
yield return validationResult;
274274
}
275275

276-
if (!Id.HasValue || Id < 0 || Id > 5)
276+
if (!Id.HasValue)
277277
{
278278
yield return new ValidationResult("Invalid Key Id", new string[] { nameof(Id) });
279279
}

src/Api/Billing/Controllers/VNext/AccountBillingVNextController.cs

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
using Bit.Api.Billing.Attributes;
22
using Bit.Api.Billing.Models.Requests.Payment;
33
using Bit.Api.Billing.Models.Requests.Premium;
4+
using Bit.Api.Billing.Models.Requests.Storage;
5+
using Bit.Core;
6+
using Bit.Core.Billing.Licenses.Queries;
47
using Bit.Core.Billing.Payment.Commands;
58
using Bit.Core.Billing.Payment.Queries;
69
using Bit.Core.Billing.Premium.Commands;
@@ -20,7 +23,9 @@ public class AccountBillingVNextController(
2023
ICreatePremiumCloudHostedSubscriptionCommand createPremiumCloudHostedSubscriptionCommand,
2124
IGetCreditQuery getCreditQuery,
2225
IGetPaymentMethodQuery getPaymentMethodQuery,
23-
IUpdatePaymentMethodCommand updatePaymentMethodCommand) : BaseBillingController
26+
IGetUserLicenseQuery getUserLicenseQuery,
27+
IUpdatePaymentMethodCommand updatePaymentMethodCommand,
28+
IUpdatePremiumStorageCommand updatePremiumStorageCommand) : BaseBillingController
2429
{
2530
[HttpGet("credit")]
2631
[InjectUser]
@@ -75,4 +80,24 @@ public async Task<IResult> CreateSubscriptionAsync(
7580
user, paymentMethod, billingAddress, additionalStorageGb);
7681
return Handle(result);
7782
}
83+
84+
[HttpGet("license")]
85+
[InjectUser]
86+
public async Task<IResult> GetLicenseAsync(
87+
[BindNever] User user)
88+
{
89+
var response = await getUserLicenseQuery.Run(user);
90+
return TypedResults.Ok(response);
91+
}
92+
93+
[HttpPut("storage")]
94+
[RequireFeature(FeatureFlagKeys.PM29594_UpdateIndividualSubscriptionPage)]
95+
[InjectUser]
96+
public async Task<IResult> UpdateStorageAsync(
97+
[BindNever] User user,
98+
[FromBody] StorageUpdateRequest request)
99+
{
100+
var result = await updatePremiumStorageCommand.Run(user, request.AdditionalStorageGb);
101+
return Handle(result);
102+
}
78103
}

0 commit comments

Comments
 (0)