|
1 | 1 | using Bit.Api.AdminConsole.Authorization; |
2 | | -using Bit.Core.Entities; |
3 | | -using Bit.Core.Models.Api; |
| 2 | +using Bit.Core.Exceptions; |
4 | 3 | using Bit.Core.Repositories; |
5 | 4 | using Microsoft.AspNetCore.Mvc; |
6 | | -using Microsoft.AspNetCore.Mvc.Filters; |
| 5 | +using Microsoft.AspNetCore.Mvc.ModelBinding; |
| 6 | +using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata; |
7 | 7 |
|
8 | 8 | namespace Bit.Api.AdminConsole.Attributes; |
9 | 9 |
|
10 | 10 | /// <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. |
14 | 14 | /// </summary> |
15 | 15 | /// <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. |
22 | 19 | /// </remarks> |
23 | 20 | /// <example> |
24 | 21 | /// <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) |
28 | 27 | /// |
29 | 28 | /// [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) |
32 | 31 | /// ]]></code> |
33 | 32 | /// </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)) |
38 | 36 | { |
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) |
42 | 53 | { |
| 54 | + var defaultMetadata = bindingContext.ModelMetadata as DefaultModelMetadata; |
| 55 | + var attr = defaultMetadata?.Attributes.ParameterAttributes |
| 56 | + ?.OfType<InjectOrganizationUserAttribute>() |
| 57 | + .FirstOrDefault() |
| 58 | + ?? new InjectOrganizationUserAttribute(); |
| 59 | + |
43 | 60 | Guid orgId; |
44 | 61 | try |
45 | 62 | { |
46 | | - orgId = context.HttpContext.GetOrganizationId(); |
| 63 | + orgId = bindingContext.HttpContext.GetOrganizationId(); |
47 | 64 | } |
48 | 65 | catch (InvalidOperationException) |
49 | 66 | { |
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."); |
53 | 68 | } |
54 | 69 |
|
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)) |
57 | 73 | { |
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."); |
61 | 76 | } |
62 | 77 |
|
63 | | - var organizationUserRepository = context.HttpContext.RequestServices |
| 78 | + var repo = bindingContext.HttpContext.RequestServices |
64 | 79 | .GetRequiredService<IOrganizationUserRepository>(); |
65 | 80 |
|
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) |
79 | 83 | { |
80 | | - context.ActionArguments[organizationUserParameter.Name] = organizationUser; |
| 84 | + throw new NotFoundException(); |
81 | 85 | } |
82 | 86 |
|
83 | | - await next(); |
| 87 | + bindingContext.Result = ModelBindingResult.Success(organizationUser); |
84 | 88 | } |
85 | 89 | } |
0 commit comments