Skip to content

Commit ee2ec24

Browse files
committed
Changes from code review
1 parent 00ca8ee commit ee2ec24

4 files changed

Lines changed: 257 additions & 290 deletions

File tree

Lines changed: 53 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,85 +1,89 @@
11
using Bit.Api.AdminConsole.Authorization;
2-
using Bit.Core.Entities;
3-
using Bit.Core.Models.Api;
2+
using Bit.Core.Exceptions;
43
using Bit.Core.Repositories;
54
using Microsoft.AspNetCore.Mvc;
6-
using Microsoft.AspNetCore.Mvc.Filters;
5+
using Microsoft.AspNetCore.Mvc.ModelBinding;
6+
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
77

88
namespace Bit.Api.AdminConsole.Attributes;
99

1010
/// <summary>
11-
/// Validates that the specified organization user belongs to the organization identified by the
12-
/// <c>orgId</c> or <c>organizationId</c> route parameter, and optionally injects the loaded
13-
/// <see cref="OrganizationUser"/> into the action method arguments.
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.
1414
/// </summary>
1515
/// <remarks>
16-
/// <para>The organization user is resolved from the route parameter named by
17-
/// <paramref name="organizationUserIdRouteParam"/> (default <c>"id"</c>). Its
18-
/// <see cref="OrganizationUser.OrganizationId"/> must match the organization route value.
19-
/// If validation fails, the request is short-circuited with an appropriate error response.</para>
20-
/// <para>The injected <see cref="OrganizationUser"/> parameter must be marked with
21-
/// <c>[BindNever]</c> to bypass model binding.</para>
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.
2219
/// </remarks>
2320
/// <example>
2421
/// <code><![CDATA[
25-
/// [HttpGet("{id}")]
26-
/// [InjectOrganizationUser]
27-
/// public async Task<IResult> GetAsync(Guid id, [BindNever] OrganizationUser organizationUser)
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)
2827
///
2928
/// [HttpPost("{organizationUserId}/accept")]
30-
/// [InjectOrganizationUser("organizationUserId")]
31-
/// public async Task<IResult> AcceptAsync(Guid organizationUserId, [BindNever] OrganizationUser organizationUser)
29+
/// public async Task<IResult> AcceptAsync(Guid organizationUserId,
30+
/// [InjectOrganizationUser("organizationUserId")] OrganizationUser organizationUser)
3231
/// ]]></code>
3332
/// </example>
34-
/// <param name="organizationUserIdRouteParam">
35-
/// Name of the route parameter containing the organization user ID. Defaults to <c>"id"</c>.
36-
/// </param>
37-
public class InjectOrganizationUserAttribute(string organizationUserIdRouteParam = "id") : ActionFilterAttribute
33+
[AttributeUsage(AttributeTargets.Parameter)]
34+
public sealed class InjectOrganizationUserAttribute(string organizationUserIdRouteParam = "id")
35+
: ModelBinderAttribute(typeof(OrganizationUserModelBinder))
3836
{
39-
public override async Task OnActionExecutionAsync(
40-
ActionExecutingContext context,
41-
ActionExecutionDelegate next)
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)
4253
{
54+
var defaultMetadata = bindingContext.ModelMetadata as DefaultModelMetadata;
55+
var attr = defaultMetadata?.Attributes.ParameterAttributes
56+
?.OfType<InjectOrganizationUserAttribute>()
57+
.FirstOrDefault()
58+
?? new InjectOrganizationUserAttribute();
59+
4360
Guid orgId;
4461
try
4562
{
46-
orgId = context.HttpContext.GetOrganizationId();
63+
orgId = bindingContext.HttpContext.GetOrganizationId();
4764
}
4865
catch (InvalidOperationException)
4966
{
50-
context.Result = new BadRequestObjectResult(
51-
new ErrorResponseModel("Route parameter 'orgId' or 'organizationId' is missing or invalid."));
52-
return;
67+
throw new BadRequestException("Route parameter 'orgId' or 'organizationId' is missing or invalid.");
5368
}
5469

55-
if (!context.RouteData.Values.TryGetValue(organizationUserIdRouteParam, out var orgUserIdRouteValue) ||
56-
!Guid.TryParse(orgUserIdRouteValue?.ToString(), out var orgUserId))
70+
var routeValues = bindingContext.ActionContext.RouteData.Values;
71+
if (!routeValues.TryGetValue(attr.OrganizationUserIdRouteParam, out var idValue)
72+
|| !Guid.TryParse(idValue?.ToString(), out var orgUserId))
5773
{
58-
context.Result = new BadRequestObjectResult(
59-
new ErrorResponseModel($"Route parameter '{organizationUserIdRouteParam}' is missing or invalid."));
60-
return;
74+
throw new BadRequestException(
75+
$"Route parameter '{attr.OrganizationUserIdRouteParam}' is missing or invalid.");
6176
}
6277

63-
var organizationUserRepository = context.HttpContext.RequestServices
78+
var repo = bindingContext.HttpContext.RequestServices
6479
.GetRequiredService<IOrganizationUserRepository>();
6580

66-
var organizationUser = await organizationUserRepository.GetByIdAsync(orgUserId);
67-
68-
if (organizationUser == null || organizationUser.OrganizationId != orgId)
69-
{
70-
context.Result = new NotFoundObjectResult(
71-
new ErrorResponseModel("Organization user not found."));
72-
return;
73-
}
74-
75-
var organizationUserParameter = context.ActionDescriptor.Parameters
76-
.FirstOrDefault(p => p.ParameterType == typeof(OrganizationUser));
77-
78-
if (organizationUserParameter != null)
81+
var organizationUser = await repo.GetByIdAsync(orgUserId);
82+
if (organizationUser is null || organizationUser.OrganizationId != orgId)
7983
{
80-
context.ActionArguments[organizationUserParameter.Name] = organizationUser;
84+
throw new NotFoundException();
8185
}
8286

83-
await next();
87+
bindingContext.Result = ModelBindingResult.Success(organizationUser);
8488
}
8589
}

src/Api/AdminConsole/Controllers/OrganizationUsersController.cs

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,6 @@
4343
using Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests;
4444
using Microsoft.AspNetCore.Authorization;
4545
using Microsoft.AspNetCore.Mvc;
46-
using Microsoft.AspNetCore.Mvc.ModelBinding;
4746
using AccountRecoveryV2 = Bit.Core.AdminConsole.OrganizationFeatures.AccountRecovery.v2;
4847
using V1_RevokeOrganizationUserCommand = Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RevokeUser.v1.IRevokeOrganizationUserCommand;
4948
using V2_RevokeOrganizationUserCommand = Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RevokeUser.v2;
@@ -506,16 +505,14 @@ await _organizationService.UpdateUserResetPasswordEnrollmentAsync(
506505
/// </summary>
507506
[HttpPut("{id}/reset-password")]
508507
[Authorize<ManageAccountRecoveryRequirement>]
509-
[InjectOrganizationUser]
510508
public Task<IResult> PutResetPassword(Guid orgId, Guid id, [FromBody] OrganizationUserResetPasswordRequestModel model,
511-
[BindNever] OrganizationUser targetOrganizationUser)
509+
[InjectOrganizationUser] OrganizationUser targetOrganizationUser)
512510
=> PutRecoverAccount(orgId, id, model, targetOrganizationUser);
513511

514512
[HttpPut("{id}/recover-account")]
515513
[Authorize<ManageAccountRecoveryRequirement>]
516-
[InjectOrganizationUser]
517514
public async Task<IResult> PutRecoverAccount(Guid orgId, Guid id, [FromBody] OrganizationUserResetPasswordRequestModel model,
518-
[BindNever] OrganizationUser targetOrganizationUser)
515+
[InjectOrganizationUser] OrganizationUser targetOrganizationUser)
519516
{
520517

521518
var authorizationResult = await _authorizationService.AuthorizeAsync(User, targetOrganizationUser, new RecoverAccountAuthorizationRequirement());

0 commit comments

Comments
 (0)