Skip to content

Commit ee9b014

Browse files
Merge branch 'main' into auth/pm-31631/update-password-prelogin-salt-response
2 parents a2e7e2a + 27ae3d5 commit ee9b014

75 files changed

Lines changed: 4428 additions & 919 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.

bitwarden_license/src/Scim/Groups/PatchGroupCommand.cs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,19 +22,22 @@ public class PatchGroupCommand : IPatchGroupCommand
2222
private readonly IUpdateGroupCommand _updateGroupCommand;
2323
private readonly ILogger<PatchGroupCommand> _logger;
2424
private readonly IOrganizationRepository _organizationRepository;
25+
private readonly TimeProvider _timeProvider;
2526

2627
public PatchGroupCommand(
2728
IGroupRepository groupRepository,
2829
IGroupService groupService,
2930
IUpdateGroupCommand updateGroupCommand,
3031
ILogger<PatchGroupCommand> logger,
31-
IOrganizationRepository organizationRepository)
32+
IOrganizationRepository organizationRepository,
33+
TimeProvider timeProvider)
3234
{
3335
_groupRepository = groupRepository;
3436
_groupService = groupService;
3537
_updateGroupCommand = updateGroupCommand;
3638
_logger = logger;
3739
_organizationRepository = organizationRepository;
40+
_timeProvider = timeProvider;
3841
}
3942

4043
public async Task PatchGroupAsync(Group group, ScimPatchModel model)
@@ -53,7 +56,7 @@ private async Task HandleOperationAsync(Group group, ScimPatchModel.OperationMod
5356
case PatchOps.Replace when operation.Path?.ToLowerInvariant() == PatchPaths.Members:
5457
{
5558
var ids = GetOperationValueIds(operation.Value);
56-
await _groupRepository.UpdateUsersAsync(group.Id, ids);
59+
await _groupRepository.UpdateUsersAsync(group.Id, ids, _timeProvider.GetUtcNow().UtcDateTime);
5760
break;
5861
}
5962

@@ -122,7 +125,7 @@ private async Task HandleOperationAsync(Group group, ScimPatchModel.OperationMod
122125
{
123126
orgUserIds.Remove(v);
124127
}
125-
await _groupRepository.UpdateUsersAsync(group.Id, orgUserIds);
128+
await _groupRepository.UpdateUsersAsync(group.Id, orgUserIds, _timeProvider.GetUtcNow().UtcDateTime);
126129
break;
127130
}
128131

@@ -146,7 +149,7 @@ private async Task AddMembersAsync(Group group, HashSet<Guid> usersToAdd)
146149
return;
147150
}
148151

149-
await _groupRepository.AddGroupUsersByIdAsync(group.Id, usersToAdd);
152+
await _groupRepository.AddGroupUsersByIdAsync(group.Id, usersToAdd, _timeProvider.GetUtcNow().UtcDateTime);
150153
}
151154

152155
private static HashSet<Guid> GetOperationValueIds(JsonElement objArray)

bitwarden_license/src/Scim/Groups/PostGroupCommand.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,6 @@ private async Task UpdateGroupMembersAsync(Group group, ScimGroupRequestModel mo
6262
return;
6363
}
6464

65-
await _groupRepository.UpdateUsersAsync(group.Id, memberIds);
65+
await _groupRepository.UpdateUsersAsync(group.Id, memberIds, group.RevisionDate);
6666
}
6767
}

bitwarden_license/src/Scim/Groups/PutGroupCommand.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,6 @@ private async Task UpdateGroupMembersAsync(Group group, ScimGroupRequestModel mo
5252
}
5353
}
5454

55-
await _groupRepository.UpdateUsersAsync(group.Id, memberIds);
55+
await _groupRepository.UpdateUsersAsync(group.Id, memberIds, group.RevisionDate);
5656
}
5757
}

bitwarden_license/test/Scim.Test/Groups/PatchGroupCommandTests.cs

Lines changed: 40 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
using Bit.Test.Common.AutoFixture;
1414
using Bit.Test.Common.AutoFixture.Attributes;
1515
using Microsoft.Extensions.Logging;
16+
using Microsoft.Extensions.Time.Testing;
1617
using NSubstitute;
1718
using Xunit;
1819

@@ -21,11 +22,14 @@ namespace Bit.Scim.Test.Groups;
2122
[SutProviderCustomize]
2223
public class PatchGroupCommandTests
2324
{
25+
private static readonly DateTime _expectedRevisionDate = DateTime.UtcNow.AddYears(1);
26+
2427
[Theory]
2528
[BitAutoData]
26-
public async Task PatchGroup_ReplaceListMembers_Success(SutProvider<PatchGroupCommand> sutProvider,
29+
public async Task PatchGroup_ReplaceListMembers_Success(
2730
Organization organization, Group group, IEnumerable<Guid> userIds)
2831
{
32+
var sutProvider = SetupSutProvider();
2933
group.OrganizationId = organization.Id;
3034

3135
var scimPatchModel = new ScimPatchModel
@@ -48,7 +52,8 @@ await sutProvider.GetDependency<IGroupRepository>().Received(1).UpdateUsersAsync
4852
group.Id,
4953
Arg.Is<IEnumerable<Guid>>(arg =>
5054
arg.Count() == userIds.Count() &&
51-
arg.ToHashSet().SetEquals(userIds)));
55+
arg.ToHashSet().SetEquals(userIds)),
56+
Arg.Is<DateTime>(d => d == _expectedRevisionDate));
5257
}
5358

5459
[Theory]
@@ -168,8 +173,9 @@ public async Task PatchGroup_ReplaceDisplayNameFromValueObject_MissingOrganizati
168173

169174
[Theory]
170175
[BitAutoData]
171-
public async Task PatchGroup_AddSingleMember_Success(SutProvider<PatchGroupCommand> sutProvider, Organization organization, Group group, ICollection<Guid> existingMembers, Guid userId)
176+
public async Task PatchGroup_AddSingleMember_Success(Organization organization, Group group, ICollection<Guid> existingMembers, Guid userId)
172177
{
178+
var sutProvider = SetupSutProvider();
173179
group.OrganizationId = organization.Id;
174180

175181
sutProvider.GetDependency<IGroupRepository>()
@@ -193,7 +199,8 @@ public async Task PatchGroup_AddSingleMember_Success(SutProvider<PatchGroupComma
193199

194200
await sutProvider.GetDependency<IGroupRepository>().Received(1).AddGroupUsersByIdAsync(
195201
group.Id,
196-
Arg.Is<IEnumerable<Guid>>(arg => arg.Single() == userId));
202+
Arg.Is<IEnumerable<Guid>>(arg => arg.Single() == userId),
203+
Arg.Is<DateTime>(d => d == _expectedRevisionDate));
197204
}
198205

199206
[Theory]
@@ -229,13 +236,14 @@ public async Task PatchGroup_AddSingleMember_ReturnsEarlyIfAlreadyInGroup(
229236

230237
await sutProvider.GetDependency<IGroupRepository>()
231238
.DidNotReceiveWithAnyArgs()
232-
.AddGroupUsersByIdAsync(default, default);
239+
.AddGroupUsersByIdAsync(default, default, default);
233240
}
234241

235242
[Theory]
236243
[BitAutoData]
237-
public async Task PatchGroup_AddListMembers_Success(SutProvider<PatchGroupCommand> sutProvider, Organization organization, Group group, ICollection<Guid> existingMembers, ICollection<Guid> userIds)
244+
public async Task PatchGroup_AddListMembers_Success(Organization organization, Group group, ICollection<Guid> existingMembers, ICollection<Guid> userIds)
238245
{
246+
var sutProvider = SetupSutProvider();
239247
group.OrganizationId = organization.Id;
240248

241249
sutProvider.GetDependency<IGroupRepository>()
@@ -262,15 +270,18 @@ await sutProvider.GetDependency<IGroupRepository>().Received(1).AddGroupUsersByI
262270
group.Id,
263271
Arg.Is<IEnumerable<Guid>>(arg =>
264272
arg.Count() == userIds.Count &&
265-
arg.ToHashSet().SetEquals(userIds)));
273+
arg.ToHashSet().SetEquals(userIds)),
274+
Arg.Is<DateTime>(d => d == _expectedRevisionDate));
266275
}
267276

268277
[Theory]
269278
[BitAutoData]
270279
public async Task PatchGroup_AddListMembers_IgnoresDuplicatesInRequest(
271-
SutProvider<PatchGroupCommand> sutProvider, Organization organization, Group group,
280+
Organization organization, Group group,
272281
ICollection<Guid> existingMembers)
273282
{
283+
var sutProvider = SetupSutProvider();
284+
274285
// Create 3 userIds
275286
var fixture = new Fixture { RepeatCount = 3 };
276287
var userIds = fixture.CreateMany<Guid>().ToList();
@@ -308,17 +319,19 @@ await sutProvider.GetDependency<IGroupRepository>().Received(1).AddGroupUsersByI
308319
group.Id,
309320
Arg.Is<IEnumerable<Guid>>(arg =>
310321
arg.Count() == 3 &&
311-
arg.ToHashSet().SetEquals(userIds)));
322+
arg.ToHashSet().SetEquals(userIds)),
323+
Arg.Is<DateTime>(d => d == _expectedRevisionDate));
312324
}
313325

314326
[Theory]
315327
[BitAutoData]
316328
public async Task PatchGroup_AddListMembers_SuccessIfOnlySomeUsersAreInGroup(
317-
SutProvider<PatchGroupCommand> sutProvider,
318329
Organization organization, Group group,
319330
ICollection<Guid> existingMembers,
320331
ICollection<Guid> userIds)
321332
{
333+
var sutProvider = SetupSutProvider();
334+
322335
// A user is already in the group, but some still need to be added
323336
userIds.Add(existingMembers.First());
324337

@@ -350,7 +363,8 @@ await sutProvider.GetDependency<IGroupRepository>()
350363
group.Id,
351364
Arg.Is<IEnumerable<Guid>>(arg =>
352365
arg.Count() == userIds.Count &&
353-
arg.ToHashSet().SetEquals(userIds)));
366+
arg.ToHashSet().SetEquals(userIds)),
367+
Arg.Is<DateTime>(d => d == _expectedRevisionDate));
354368
}
355369

356370
[Theory]
@@ -379,9 +393,10 @@ public async Task PatchGroup_RemoveSingleMember_Success(SutProvider<PatchGroupCo
379393

380394
[Theory]
381395
[BitAutoData]
382-
public async Task PatchGroup_RemoveListMembers_Success(SutProvider<PatchGroupCommand> sutProvider,
396+
public async Task PatchGroup_RemoveListMembers_Success(
383397
Organization organization, Group group, ICollection<Guid> existingMembers)
384398
{
399+
var sutProvider = SetupSutProvider();
385400
List<Guid> usersToRemove = [existingMembers.First(), existingMembers.Skip(1).First()];
386401
group.OrganizationId = organization.Id;
387402

@@ -412,7 +427,8 @@ await sutProvider.GetDependency<IGroupRepository>()
412427
group.Id,
413428
Arg.Is<IEnumerable<Guid>>(arg =>
414429
arg.Count() == expectedRemainingUsers.Count &&
415-
arg.ToHashSet().SetEquals(expectedRemainingUsers)));
430+
arg.ToHashSet().SetEquals(expectedRemainingUsers)),
431+
Arg.Is<DateTime>(d => d == _expectedRevisionDate));
416432
}
417433

418434
[Theory]
@@ -430,7 +446,7 @@ public async Task PatchGroup_InvalidOperation_Success(SutProvider<PatchGroupComm
430446
await sutProvider.Sut.PatchGroupAsync(group, scimPatchModel);
431447

432448
// Assert: no operation performed
433-
await sutProvider.GetDependency<IGroupRepository>().DidNotReceiveWithAnyArgs().UpdateUsersAsync(default, default);
449+
await sutProvider.GetDependency<IGroupRepository>().DidNotReceiveWithAnyArgs().UpdateUsersAsync(default, default, default);
434450
await sutProvider.GetDependency<IGroupRepository>().DidNotReceiveWithAnyArgs().GetManyUserIdsByIdAsync(default);
435451
await sutProvider.GetDependency<IUpdateGroupCommand>().DidNotReceiveWithAnyArgs().UpdateGroupAsync(default, default);
436452
await sutProvider.GetDependency<IGroupService>().DidNotReceiveWithAnyArgs().DeleteUserAsync(default, default);
@@ -454,9 +470,18 @@ public async Task PatchGroup_NoOperation_Success(
454470

455471
await sutProvider.Sut.PatchGroupAsync(group, scimPatchModel);
456472

457-
await sutProvider.GetDependency<IGroupRepository>().DidNotReceiveWithAnyArgs().UpdateUsersAsync(default, default);
473+
await sutProvider.GetDependency<IGroupRepository>().DidNotReceiveWithAnyArgs().UpdateUsersAsync(default, default, default);
458474
await sutProvider.GetDependency<IGroupRepository>().DidNotReceiveWithAnyArgs().GetManyUserIdsByIdAsync(default);
459475
await sutProvider.GetDependency<IUpdateGroupCommand>().DidNotReceiveWithAnyArgs().UpdateGroupAsync(default, default);
460476
await sutProvider.GetDependency<IGroupService>().DidNotReceiveWithAnyArgs().DeleteUserAsync(default, default);
461477
}
478+
479+
private static SutProvider<PatchGroupCommand> SetupSutProvider()
480+
{
481+
var sutProvider = new SutProvider<PatchGroupCommand>()
482+
.WithFakeTimeProvider()
483+
.Create();
484+
sutProvider.GetDependency<FakeTimeProvider>().SetUtcNow(_expectedRevisionDate);
485+
return sutProvider;
486+
}
462487
}

bitwarden_license/test/Scim.Test/Groups/PostGroupCommandTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ public async Task PostGroup_Success(SutProvider<PostGroupCommand> sutProvider, s
4343
var group = await sutProvider.Sut.PostGroupAsync(organization, scimGroupRequestModel);
4444

4545
await sutProvider.GetDependency<ICreateGroupCommand>().Received(1).CreateGroupAsync(group, organization, EventSystemUser.SCIM, null);
46-
await sutProvider.GetDependency<IGroupRepository>().DidNotReceiveWithAnyArgs().UpdateUsersAsync(default, default);
46+
await sutProvider.GetDependency<IGroupRepository>().DidNotReceiveWithAnyArgs().UpdateUsersAsync(default, default, default);
4747

4848
AssertHelper.AssertPropertyEqual(expectedResult, group, "Id", "CreationDate", "RevisionDate");
4949
}
@@ -74,7 +74,7 @@ public async Task PostGroup_WithMembers_Success(SutProvider<PostGroupCommand> su
7474
var group = await sutProvider.Sut.PostGroupAsync(organization, scimGroupRequestModel);
7575

7676
await sutProvider.GetDependency<ICreateGroupCommand>().Received(1).CreateGroupAsync(group, organization, EventSystemUser.SCIM, null);
77-
await sutProvider.GetDependency<IGroupRepository>().Received(1).UpdateUsersAsync(Arg.Any<Guid>(), Arg.Is<IEnumerable<Guid>>(arg => arg.All(id => membersUserIds.Contains(id))));
77+
await sutProvider.GetDependency<IGroupRepository>().Received(1).UpdateUsersAsync(group.Id, Arg.Is<IEnumerable<Guid>>(arg => arg.All(id => membersUserIds.Contains(id))), group.RevisionDate);
7878

7979
AssertHelper.AssertPropertyEqual(expectedResult, group, "Id", "CreationDate", "RevisionDate");
8080
}

bitwarden_license/test/Scim.Test/Groups/PutGroupCommandTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ public async Task PutGroup_Success(SutProvider<PutGroupCommand> sutProvider, Org
4747
Assert.Equal(displayName, group.Name);
4848

4949
await sutProvider.GetDependency<IUpdateGroupCommand>().Received(1).UpdateGroupAsync(group, organization, EventSystemUser.SCIM);
50-
await sutProvider.GetDependency<IGroupRepository>().DidNotReceiveWithAnyArgs().UpdateUsersAsync(default, default);
50+
await sutProvider.GetDependency<IGroupRepository>().DidNotReceiveWithAnyArgs().UpdateUsersAsync(default, default, default);
5151
}
5252

5353
[Theory]
@@ -81,7 +81,7 @@ public async Task PutGroup_ChangeMembers_Success(SutProvider<PutGroupCommand> su
8181
Assert.Equal(displayName, group.Name);
8282

8383
await sutProvider.GetDependency<IUpdateGroupCommand>().Received(1).UpdateGroupAsync(group, organization, EventSystemUser.SCIM);
84-
await sutProvider.GetDependency<IGroupRepository>().Received(1).UpdateUsersAsync(group.Id, Arg.Is<IEnumerable<Guid>>(arg => arg.All(id => membersUserIds.Contains(id))));
84+
await sutProvider.GetDependency<IGroupRepository>().Received(1).UpdateUsersAsync(group.Id, Arg.Is<IEnumerable<Guid>>(arg => arg.All(id => membersUserIds.Contains(id))), group.RevisionDate);
8585
}
8686

8787
[Theory]
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
using Bit.Api.AdminConsole.Authorization;
2+
using Bit.Core.Exceptions;
3+
using Bit.Core.Repositories;
4+
using Microsoft.AspNetCore.Mvc;
5+
using Microsoft.AspNetCore.Mvc.ModelBinding;
6+
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
7+
8+
namespace Bit.Api.AdminConsole.Attributes;
9+
10+
/// <summary>
11+
/// Binds a <see cref="Bit.Core.Entities.OrganizationUser"/> parameter by loading it from the database
12+
/// and validating that it belongs to the organization identified by the <c>orgId</c> or
13+
/// <c>organizationId</c> route parameter.
14+
/// </summary>
15+
/// <remarks>
16+
/// The organization user is resolved from the route parameter named by
17+
/// <see cref="OrganizationUserIdRouteParam"/> (default <c>"id"</c>). If the user is not found or
18+
/// does not belong to the organization, a <see cref="Bit.Core.Exceptions.NotFoundException"/> is thrown.
19+
/// </remarks>
20+
/// <example>
21+
/// <code><![CDATA[
22+
/// [HttpPut("{id}/recover-account")]
23+
/// [Authorize<ManageAccountRecoveryRequirement>]
24+
/// public async Task<IResult> PutRecoverAccount(Guid orgId, Guid id,
25+
/// [FromBody] OrganizationUserResetPasswordRequestModel model,
26+
/// [InjectOrganizationUser] OrganizationUser targetOrganizationUser)
27+
///
28+
/// [HttpPost("{organizationUserId}/accept")]
29+
/// public async Task<IResult> AcceptAsync(Guid organizationUserId,
30+
/// [InjectOrganizationUser("organizationUserId")] OrganizationUser organizationUser)
31+
/// ]]></code>
32+
/// </example>
33+
[AttributeUsage(AttributeTargets.Parameter)]
34+
public sealed class InjectOrganizationUserAttribute(string organizationUserIdRouteParam = "id")
35+
: ModelBinderAttribute(typeof(OrganizationUserModelBinder))
36+
{
37+
/// <summary>
38+
/// Name of the route parameter containing the organization user ID. Defaults to <c>"id"</c>.
39+
/// </summary>
40+
public string OrganizationUserIdRouteParam { get; } = organizationUserIdRouteParam;
41+
}
42+
43+
/// <summary>
44+
/// Custom model binder that loads an <see cref="Bit.Core.Entities.OrganizationUser"/> from the database,
45+
/// validates that it belongs to the organization identified by the route, and binds it to the parameter.
46+
/// </summary>
47+
/// <remarks>
48+
/// This binder is used via the <see cref="InjectOrganizationUserAttribute"/>.
49+
/// </remarks>
50+
public class OrganizationUserModelBinder : IModelBinder
51+
{
52+
public async Task BindModelAsync(ModelBindingContext bindingContext)
53+
{
54+
var defaultMetadata = bindingContext.ModelMetadata as DefaultModelMetadata;
55+
var attr = defaultMetadata?.Attributes.ParameterAttributes
56+
?.OfType<InjectOrganizationUserAttribute>()
57+
.FirstOrDefault()
58+
?? new InjectOrganizationUserAttribute();
59+
60+
Guid orgId;
61+
try
62+
{
63+
orgId = bindingContext.HttpContext.GetOrganizationId();
64+
}
65+
catch (InvalidOperationException)
66+
{
67+
throw new BadRequestException("Route parameter 'orgId' or 'organizationId' is missing or invalid.");
68+
}
69+
70+
var routeValues = bindingContext.ActionContext.RouteData.Values;
71+
if (!routeValues.TryGetValue(attr.OrganizationUserIdRouteParam, out var idValue)
72+
|| !Guid.TryParse(idValue?.ToString(), out var orgUserId))
73+
{
74+
throw new BadRequestException(
75+
$"Route parameter '{attr.OrganizationUserIdRouteParam}' is missing or invalid.");
76+
}
77+
78+
var repo = bindingContext.HttpContext.RequestServices
79+
.GetRequiredService<IOrganizationUserRepository>();
80+
81+
var organizationUser = await repo.GetByIdAsync(orgUserId);
82+
if (organizationUser is null || organizationUser.OrganizationId != orgId)
83+
{
84+
throw new NotFoundException();
85+
}
86+
87+
bindingContext.Result = ModelBindingResult.Success(organizationUser);
88+
}
89+
}

0 commit comments

Comments
 (0)