Skip to content

Commit e48b8c6

Browse files
authored
Merge branch 'main' into SM-1592-API
2 parents 56bc9e5 + 43d1497 commit e48b8c6

File tree

67 files changed

+2057
-305
lines changed

Some content is hidden

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

67 files changed

+2057
-305
lines changed

.github/renovate.json5

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,10 @@
4141
matchUpdateTypes: ["patch"],
4242
dependencyDashboardApproval: false,
4343
},
44+
{
45+
matchSourceUrls: ["https://github.com/bitwarden/sdk-internal"],
46+
groupName: "sdk-internal",
47+
},
4448
{
4549
matchManagers: ["dockerfile", "docker-compose"],
4650
commitMessagePrefix: "[deps] BRE:",

bitwarden_license/src/Sso/Controllers/AccountController.cs

Lines changed: 146 additions & 88 deletions
Large diffs are not rendered by default.

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

Lines changed: 79 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -74,17 +74,6 @@ private static IAuthenticationService SetupHttpContextWithAuth(
7474
return resolvedAuthService;
7575
}
7676

77-
private static void InvokeEnsureOrgUserStatusAllowed(
78-
AccountController controller,
79-
OrganizationUserStatusType status)
80-
{
81-
var method = typeof(AccountController).GetMethod(
82-
"EnsureAcceptedOrConfirmedOrgUserStatus",
83-
BindingFlags.Instance | BindingFlags.NonPublic);
84-
Assert.NotNull(method);
85-
method.Invoke(controller, [status, "Org"]);
86-
}
87-
8877
private static AuthenticateResult BuildSuccessfulExternalAuth(Guid orgId, string providerUserId, string email)
8978
{
9079
var claims = new[]
@@ -241,82 +230,6 @@ private async Task<LookupCounts> MeasureCountsForScenarioAsync(
241230
return counts;
242231
}
243232

244-
[Theory, BitAutoData]
245-
public void EnsureOrgUserStatusAllowed_AllowsAcceptedAndConfirmed(
246-
SutProvider<AccountController> sutProvider)
247-
{
248-
// Arrange
249-
sutProvider.GetDependency<II18nService>()
250-
.T(Arg.Any<string>(), Arg.Any<object?[]>())
251-
.Returns(ci => (string)ci[0]!);
252-
253-
// Act
254-
var ex1 = Record.Exception(() =>
255-
InvokeEnsureOrgUserStatusAllowed(sutProvider.Sut, OrganizationUserStatusType.Accepted));
256-
var ex2 = Record.Exception(() =>
257-
InvokeEnsureOrgUserStatusAllowed(sutProvider.Sut, OrganizationUserStatusType.Confirmed));
258-
259-
// Assert
260-
Assert.Null(ex1);
261-
Assert.Null(ex2);
262-
}
263-
264-
[Theory, BitAutoData]
265-
public void EnsureOrgUserStatusAllowed_Invited_ThrowsAcceptInvite(
266-
SutProvider<AccountController> sutProvider)
267-
{
268-
// Arrange
269-
sutProvider.GetDependency<II18nService>()
270-
.T(Arg.Any<string>(), Arg.Any<object?[]>())
271-
.Returns(ci => (string)ci[0]!);
272-
273-
// Act
274-
var ex = Assert.Throws<TargetInvocationException>(() =>
275-
InvokeEnsureOrgUserStatusAllowed(sutProvider.Sut, OrganizationUserStatusType.Invited));
276-
277-
// Assert
278-
Assert.IsType<Exception>(ex.InnerException);
279-
Assert.Equal("AcceptInviteBeforeUsingSSO", ex.InnerException!.Message);
280-
}
281-
282-
[Theory, BitAutoData]
283-
public void EnsureOrgUserStatusAllowed_Revoked_ThrowsAccessRevoked(
284-
SutProvider<AccountController> sutProvider)
285-
{
286-
// Arrange
287-
sutProvider.GetDependency<II18nService>()
288-
.T(Arg.Any<string>(), Arg.Any<object?[]>())
289-
.Returns(ci => (string)ci[0]!);
290-
291-
// Act
292-
var ex = Assert.Throws<TargetInvocationException>(() =>
293-
InvokeEnsureOrgUserStatusAllowed(sutProvider.Sut, OrganizationUserStatusType.Revoked));
294-
295-
// Assert
296-
Assert.IsType<Exception>(ex.InnerException);
297-
Assert.Equal("OrganizationUserAccessRevoked", ex.InnerException!.Message);
298-
}
299-
300-
[Theory, BitAutoData]
301-
public void EnsureOrgUserStatusAllowed_UnknownStatus_ThrowsUnknown(
302-
SutProvider<AccountController> sutProvider)
303-
{
304-
// Arrange
305-
sutProvider.GetDependency<II18nService>()
306-
.T(Arg.Any<string>(), Arg.Any<object?[]>())
307-
.Returns(ci => (string)ci[0]!);
308-
309-
var unknown = (OrganizationUserStatusType)999;
310-
311-
// Act
312-
var ex = Assert.Throws<TargetInvocationException>(() =>
313-
InvokeEnsureOrgUserStatusAllowed(sutProvider.Sut, unknown));
314-
315-
// Assert
316-
Assert.IsType<Exception>(ex.InnerException);
317-
Assert.Equal("OrganizationUserUnknownStatus", ex.InnerException!.Message);
318-
}
319-
320233
[Theory, BitAutoData]
321234
public async Task ExternalCallback_PreventNonCompliantTrue_ExistingUser_NoOrgUser_ThrowsCouldNotFindOrganizationUser(
322235
SutProvider<AccountController> sutProvider)
@@ -357,7 +270,7 @@ public async Task ExternalCallback_PreventNonCompliantTrue_ExistingUser_NoOrgUse
357270
}
358271

359272
[Theory, BitAutoData]
360-
public async Task ExternalCallback_PreventNonCompliantTrue_ExistingUser_OrgUserInvited_ThrowsAcceptInvite(
273+
public async Task ExternalCallback_PreventNonCompliantTrue_ExistingUser_OrgUserInvited_AllowsLogin(
361274
SutProvider<AccountController> sutProvider)
362275
{
363276
// Arrange
@@ -374,7 +287,7 @@ public async Task ExternalCallback_PreventNonCompliantTrue_ExistingUser_OrgUserI
374287
};
375288

376289
var authResult = BuildSuccessfulExternalAuth(orgId, providerUserId, user.Email!);
377-
SetupHttpContextWithAuth(sutProvider, authResult);
290+
var authService = SetupHttpContextWithAuth(sutProvider, authResult);
378291

379292
sutProvider.GetDependency<II18nService>()
380293
.T(Arg.Any<string>(), Arg.Any<object?[]>())
@@ -392,9 +305,23 @@ public async Task ExternalCallback_PreventNonCompliantTrue_ExistingUser_OrgUserI
392305
sutProvider.GetDependency<IIdentityServerInteractionService>()
393306
.GetAuthorizationContextAsync("~/").Returns((AuthorizationRequest?)null);
394307

395-
// Act + Assert
396-
var ex = await Assert.ThrowsAsync<Exception>(() => sutProvider.Sut.ExternalCallback());
397-
Assert.Equal("AcceptInviteBeforeUsingSSO", ex.Message);
308+
// Act
309+
var result = await sutProvider.Sut.ExternalCallback();
310+
311+
// Assert
312+
var redirect = Assert.IsType<RedirectResult>(result);
313+
Assert.Equal("~/", redirect.Url);
314+
315+
await authService.Received().SignInAsync(
316+
Arg.Any<HttpContext>(),
317+
Arg.Any<string?>(),
318+
Arg.Any<ClaimsPrincipal>(),
319+
Arg.Any<AuthenticationProperties>());
320+
321+
await authService.Received().SignOutAsync(
322+
Arg.Any<HttpContext>(),
323+
AuthenticationSchemes.BitwardenExternalCookieAuthenticationScheme,
324+
Arg.Any<AuthenticationProperties>());
398325
}
399326

400327
[Theory, BitAutoData]
@@ -930,13 +857,13 @@ public async Task ExternalCallback_PreventNonCompliantFalse_JitProvision_Measure
930857
}
931858

932859
[Theory, BitAutoData]
933-
public async Task AutoProvisionUserAsync_WithExistingAcceptedUser_CreatesSsoLinkAndReturnsUser(
860+
public async Task CreateUserAndOrgUserConditionallyAsync_WithExistingAcceptedUser_CreatesSsoLinkAndReturnsUser(
934861
SutProvider<AccountController> sutProvider)
935862
{
936863
// Arrange
937864
var orgId = Guid.NewGuid();
938-
var providerUserId = "ext-456";
939-
var email = "jit@example.com";
865+
var providerUserId = "provider-user-id";
866+
var email = "user@example.com";
940867
var existingUser = new User { Id = Guid.NewGuid(), Email = email };
941868
var organization = new Organization { Id = orgId, Name = "Org" };
942869
var orgUser = new OrganizationUser
@@ -965,12 +892,12 @@ public async Task AutoProvisionUserAsync_WithExistingAcceptedUser_CreatesSsoLink
965892
var config = new SsoConfigurationData();
966893

967894
var method = typeof(AccountController).GetMethod(
968-
"AutoProvisionUserAsync",
895+
"CreateUserAndOrgUserConditionallyAsync",
969896
BindingFlags.Instance | BindingFlags.NonPublic);
970897
Assert.NotNull(method);
971898

972899
// Act
973-
var task = (Task<(User user, Organization organization, OrganizationUser orgUser)>)method!.Invoke(sutProvider.Sut, new object[]
900+
var task = (Task<(User user, Organization organization, OrganizationUser orgUser)>)method.Invoke(sutProvider.Sut, new object[]
974901
{
975902
orgId.ToString(),
976903
providerUserId,
@@ -992,6 +919,61 @@ await sutProvider.GetDependency<ISsoUserRepository>().Received().CreateAsync(Arg
992919
EventType.OrganizationUser_FirstSsoLogin);
993920
}
994921

922+
[Theory, BitAutoData]
923+
public async Task CreateUserAndOrgUserConditionallyAsync_WithExistingInvitedUser_ThrowsAcceptInviteBeforeUsingSSO(
924+
SutProvider<AccountController> sutProvider)
925+
{
926+
// Arrange
927+
var orgId = Guid.NewGuid();
928+
var providerUserId = "provider-user-id";
929+
var email = "[email protected]";
930+
var existingUser = new User { Id = Guid.NewGuid(), Email = email, UsesKeyConnector = false };
931+
var organization = new Organization { Id = orgId, Name = "Org" };
932+
var orgUser = new OrganizationUser
933+
{
934+
OrganizationId = orgId,
935+
UserId = existingUser.Id,
936+
Status = OrganizationUserStatusType.Invited,
937+
Type = OrganizationUserType.User
938+
};
939+
940+
// i18n returns the key so we can assert on message contents
941+
sutProvider.GetDependency<II18nService>()
942+
.T(Arg.Any<string>(), Arg.Any<object?[]>())
943+
.Returns(ci => (string)ci[0]!);
944+
945+
// Arrange repository expectations for the flow
946+
sutProvider.GetDependency<IUserRepository>().GetByEmailAsync(email).Returns(existingUser);
947+
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(orgId).Returns(organization);
948+
sutProvider.GetDependency<IOrganizationUserRepository>().GetManyByUserAsync(existingUser.Id)
949+
.Returns(new List<OrganizationUser> { orgUser });
950+
951+
var claims = new[]
952+
{
953+
new Claim(JwtClaimTypes.Email, email),
954+
new Claim(JwtClaimTypes.Name, "Invited User")
955+
} as IEnumerable<Claim>;
956+
var config = new SsoConfigurationData();
957+
958+
var method = typeof(AccountController).GetMethod(
959+
"CreateUserAndOrgUserConditionallyAsync",
960+
BindingFlags.Instance | BindingFlags.NonPublic);
961+
Assert.NotNull(method);
962+
963+
// Act + Assert
964+
var task = (Task<(User user, Organization organization, OrganizationUser orgUser)>)method.Invoke(sutProvider.Sut, new object[]
965+
{
966+
orgId.ToString(),
967+
providerUserId,
968+
claims,
969+
null!,
970+
config
971+
})!;
972+
973+
var ex = await Assert.ThrowsAsync<Exception>(async () => await task);
974+
Assert.Equal("AcceptInviteBeforeUsingSSO", ex.Message);
975+
}
976+
995977
/// <summary>
996978
/// PM-24579: Temporary comparison test to ensure the feature flag ON does not
997979
/// regress lookup counts compared to OFF. When removing the flag, delete this

src/Api/AdminConsole/Authorization/AuthorizationHandlerCollectionExtensions.cs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,11 @@ public static void AddAdminConsoleAuthorizationHandlers(this IServiceCollection
1212
services.TryAddScoped<IOrganizationContext, OrganizationContext>();
1313

1414
services.TryAddEnumerable([
15-
ServiceDescriptor.Scoped<IAuthorizationHandler, BulkCollectionAuthorizationHandler>(),
16-
ServiceDescriptor.Scoped<IAuthorizationHandler, CollectionAuthorizationHandler>(),
17-
ServiceDescriptor.Scoped<IAuthorizationHandler, GroupAuthorizationHandler>(),
18-
ServiceDescriptor.Scoped<IAuthorizationHandler, OrganizationRequirementHandler>(),
19-
ServiceDescriptor.Scoped<IAuthorizationHandler, RecoverAccountAuthorizationHandler>(),
20-
]);
15+
ServiceDescriptor.Scoped<IAuthorizationHandler, BulkCollectionAuthorizationHandler>(),
16+
ServiceDescriptor.Scoped<IAuthorizationHandler, CollectionAuthorizationHandler>(),
17+
ServiceDescriptor.Scoped<IAuthorizationHandler, GroupAuthorizationHandler>(),
18+
ServiceDescriptor.Scoped<IAuthorizationHandler, OrganizationRequirementHandler>(),
19+
ServiceDescriptor.Scoped<IAuthorizationHandler, RecoverAccountAuthorizationHandler>(),
20+
]);
2121
}
2222
}

src/Api/AdminConsole/Controllers/PoliciesController.cs

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
using Bit.Core.AdminConsole.Enums;
1313
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces;
1414
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
15+
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents.Interfaces;
1516
using Bit.Core.AdminConsole.Repositories;
1617
using Bit.Core.Auth.Models.Business.Tokenables;
1718
using Bit.Core.Context;
@@ -41,8 +42,9 @@ public class PoliciesController : Controller
4142
private readonly IDataProtectorTokenFactory<OrgUserInviteTokenable> _orgUserInviteTokenDataFactory;
4243
private readonly IPolicyRepository _policyRepository;
4344
private readonly IUserService _userService;
44-
45+
private readonly IFeatureService _featureService;
4546
private readonly ISavePolicyCommand _savePolicyCommand;
47+
private readonly IVNextSavePolicyCommand _vNextSavePolicyCommand;
4648

4749
public PoliciesController(IPolicyRepository policyRepository,
4850
IOrganizationUserRepository organizationUserRepository,
@@ -53,7 +55,9 @@ public PoliciesController(IPolicyRepository policyRepository,
5355
IDataProtectorTokenFactory<OrgUserInviteTokenable> orgUserInviteTokenDataFactory,
5456
IOrganizationHasVerifiedDomainsQuery organizationHasVerifiedDomainsQuery,
5557
IOrganizationRepository organizationRepository,
56-
ISavePolicyCommand savePolicyCommand)
58+
IFeatureService featureService,
59+
ISavePolicyCommand savePolicyCommand,
60+
IVNextSavePolicyCommand vNextSavePolicyCommand)
5761
{
5862
_policyRepository = policyRepository;
5963
_organizationUserRepository = organizationUserRepository;
@@ -65,7 +69,9 @@ public PoliciesController(IPolicyRepository policyRepository,
6569
_organizationRepository = organizationRepository;
6670
_orgUserInviteTokenDataFactory = orgUserInviteTokenDataFactory;
6771
_organizationHasVerifiedDomainsQuery = organizationHasVerifiedDomainsQuery;
72+
_featureService = featureService;
6873
_savePolicyCommand = savePolicyCommand;
74+
_vNextSavePolicyCommand = vNextSavePolicyCommand;
6975
}
7076

7177
[HttpGet("{type}")]
@@ -221,7 +227,9 @@ public async Task<PolicyResponseModel> PutVNext(Guid orgId, [FromBody] SavePolic
221227
{
222228
var savePolicyRequest = await model.ToSavePolicyModelAsync(orgId, _currentContext);
223229

224-
var policy = await _savePolicyCommand.VNextSaveAsync(savePolicyRequest);
230+
var policy = _featureService.IsEnabled(FeatureFlagKeys.PolicyValidatorsRefactor) ?
231+
await _vNextSavePolicyCommand.SaveAsync(savePolicyRequest) :
232+
await _savePolicyCommand.VNextSaveAsync(savePolicyRequest);
225233

226234
return new PolicyResponseModel(policy);
227235
}

src/Api/AdminConsole/Public/Controllers/PoliciesController.cs

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,15 @@
55
using Bit.Api.AdminConsole.Public.Models.Request;
66
using Bit.Api.AdminConsole.Public.Models.Response;
77
using Bit.Api.Models.Public.Response;
8+
using Bit.Core;
9+
using Bit.Core.AdminConsole.Entities;
810
using Bit.Core.AdminConsole.Enums;
911
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
12+
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents.Interfaces;
1013
using Bit.Core.AdminConsole.Repositories;
1114
using Bit.Core.AdminConsole.Services;
1215
using Bit.Core.Context;
16+
using Bit.Core.Services;
1317
using Microsoft.AspNetCore.Authorization;
1418
using Microsoft.AspNetCore.Mvc;
1519

@@ -22,18 +26,24 @@ public class PoliciesController : Controller
2226
private readonly IPolicyRepository _policyRepository;
2327
private readonly IPolicyService _policyService;
2428
private readonly ICurrentContext _currentContext;
29+
private readonly IFeatureService _featureService;
2530
private readonly ISavePolicyCommand _savePolicyCommand;
31+
private readonly IVNextSavePolicyCommand _vNextSavePolicyCommand;
2632

2733
public PoliciesController(
2834
IPolicyRepository policyRepository,
2935
IPolicyService policyService,
3036
ICurrentContext currentContext,
31-
ISavePolicyCommand savePolicyCommand)
37+
IFeatureService featureService,
38+
ISavePolicyCommand savePolicyCommand,
39+
IVNextSavePolicyCommand vNextSavePolicyCommand)
3240
{
3341
_policyRepository = policyRepository;
3442
_policyService = policyService;
3543
_currentContext = currentContext;
44+
_featureService = featureService;
3645
_savePolicyCommand = savePolicyCommand;
46+
_vNextSavePolicyCommand = vNextSavePolicyCommand;
3747
}
3848

3949
/// <summary>
@@ -87,8 +97,17 @@ public async Task<IActionResult> List()
8797
[ProducesResponseType((int)HttpStatusCode.NotFound)]
8898
public async Task<IActionResult> Put(PolicyType type, [FromBody] PolicyUpdateRequestModel model)
8999
{
90-
var policyUpdate = model.ToPolicyUpdate(_currentContext.OrganizationId!.Value, type);
91-
var policy = await _savePolicyCommand.SaveAsync(policyUpdate);
100+
Policy policy;
101+
if (_featureService.IsEnabled(FeatureFlagKeys.PolicyValidatorsRefactor))
102+
{
103+
var savePolicyModel = model.ToSavePolicyModel(_currentContext.OrganizationId!.Value, type);
104+
policy = await _vNextSavePolicyCommand.SaveAsync(savePolicyModel);
105+
}
106+
else
107+
{
108+
var policyUpdate = model.ToPolicyUpdate(_currentContext.OrganizationId!.Value, type);
109+
policy = await _savePolicyCommand.SaveAsync(policyUpdate);
110+
}
92111

93112
var response = new PolicyResponseModel(policy);
94113
return new JsonResult(response);

0 commit comments

Comments
 (0)