From b59fef222f5bdd3328f2f3af0e3c14294b18b4d1 Mon Sep 17 00:00:00 2001 From: martinshoob Date: Sun, 10 Aug 2025 18:07:34 +0200 Subject: [PATCH] Implement admin-only authorization policy Adds an authorization policy to restrict access to admin-only endpoints. Creates an `AdminOnlyRequirement` and `AdminOnlyHandler` to check if a user has admin privileges. Applies the "AdminOnly" policy to the AdminController to secure admin functionalities. Modifies the endpoint for changing user admin status to include the user ID in the route. --- ...tusRequest.cs => ChangeAdminStatusBody.cs} | 3 +- .../AdminOnlyRequirement.cs | 49 +++++++++++++++++++ DrinkRateAPI/Controllers/AdminController.cs | 22 +++------ DrinkRateAPI/Program.cs | 30 +++++++----- 4 files changed, 74 insertions(+), 30 deletions(-) rename DrinkRateAPI/ApiModels/UserProfile/{ChangeUserAdminStatusRequest.cs => ChangeAdminStatusBody.cs} (51%) create mode 100644 DrinkRateAPI/AuthorizationPolicies/AdminOnlyRequirement.cs diff --git a/DrinkRateAPI/ApiModels/UserProfile/ChangeUserAdminStatusRequest.cs b/DrinkRateAPI/ApiModels/UserProfile/ChangeAdminStatusBody.cs similarity index 51% rename from DrinkRateAPI/ApiModels/UserProfile/ChangeUserAdminStatusRequest.cs rename to DrinkRateAPI/ApiModels/UserProfile/ChangeAdminStatusBody.cs index e28228d..238546f 100644 --- a/DrinkRateAPI/ApiModels/UserProfile/ChangeUserAdminStatusRequest.cs +++ b/DrinkRateAPI/ApiModels/UserProfile/ChangeAdminStatusBody.cs @@ -1,7 +1,6 @@ namespace DrinkRateAPI.ApiModels.UserProfile; -public class ChangeUserAdminStatusRequest +public class ChangeAdminStatusBody { - public required string UserId { get; set; } public bool ChangeStatusTo { get; set; } } \ No newline at end of file diff --git a/DrinkRateAPI/AuthorizationPolicies/AdminOnlyRequirement.cs b/DrinkRateAPI/AuthorizationPolicies/AdminOnlyRequirement.cs new file mode 100644 index 0000000..e972e3b --- /dev/null +++ b/DrinkRateAPI/AuthorizationPolicies/AdminOnlyRequirement.cs @@ -0,0 +1,49 @@ +using DrinkRateAPI.DbEntities; +using DrinkRateAPI.Services; + +namespace DrinkRateAPI.AuthorizationPolicies; +using Microsoft.AspNetCore.Authorization; + +public class AdminOnlyRequirement : IAuthorizationRequirement +{ +} + +public class AdminOnlyHandler : AuthorizationHandler +{ + private readonly ApplicationUserService _applicationUserService; + private readonly UserProfileService _userProfileService; + + public AdminOnlyHandler( + ApplicationUserService applicationUserService, + UserProfileService userProfileService) + { + _applicationUserService = applicationUserService; + _userProfileService = userProfileService; + } + + protected override async Task HandleRequirementAsync( + AuthorizationHandlerContext context, + AdminOnlyRequirement requirement) + { + DbUserProfile userProfile; + + try + { + userProfile = await _applicationUserService.UserProfileByApplicationUserAsync(context.User); + } + catch (Exception _) + { + context.Fail(); + return; + } + + if (_userProfileService.IsUserProfileAdmin(userProfile)) + { + context.Succeed(requirement); + } + else + { + context.Fail(); + } + } +} \ No newline at end of file diff --git a/DrinkRateAPI/Controllers/AdminController.cs b/DrinkRateAPI/Controllers/AdminController.cs index 59223f5..4bbc46d 100644 --- a/DrinkRateAPI/Controllers/AdminController.cs +++ b/DrinkRateAPI/Controllers/AdminController.cs @@ -1,38 +1,30 @@ using DrinkRateAPI.ApiModels.UserProfile; using DrinkRateAPI.Services; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; namespace DrinkRateAPI.Controllers; [ApiController] [Route("admin")] +[Authorize(Policy = "AdminOnly")] public class AdminController : ControllerBase { private readonly ILogger _logger; - private readonly ApplicationUserService _applicationUserService; private readonly UserProfileService _userProfileService; - public AdminController(ILogger logger, ApplicationUserService applicationUserService, - UserProfileService userProfileService) + public AdminController(ILogger logger, UserProfileService userProfileService) { _logger = logger; - _applicationUserService = applicationUserService; _userProfileService = userProfileService; } - [HttpPut] - [Route("adminStatus")] + [HttpPut("users/{userId}/adminStatus")] [Produces("application/json")] - public async Task PutUserAdminStatus([FromBody] ChangeUserAdminStatusRequest request) + public async Task PutUserAdminStatus(string userId, [FromBody] ChangeAdminStatusBody body) { - var userProfile = await _applicationUserService.UserProfileByApplicationUserAsync(User); - if (!_userProfileService.IsUserProfileAdmin(userProfile)) - { - return Unauthorized(); - } + var changedProfile = await _userProfileService.ChangeUserAdminStatusAsync(userId, body.ChangeStatusTo); - var changedProfile = await _userProfileService.ChangeUserAdminStatusAsync(request.UserId, request.ChangeStatusTo); - return Ok(changedProfile); } -} +} \ No newline at end of file diff --git a/DrinkRateAPI/Program.cs b/DrinkRateAPI/Program.cs index acdcb89..051ce01 100644 --- a/DrinkRateAPI/Program.cs +++ b/DrinkRateAPI/Program.cs @@ -1,6 +1,8 @@ +using DrinkRateAPI.AuthorizationPolicies; using DrinkRateAPI.Contexts; using DrinkRateAPI.DbEntities; using DrinkRateAPI.Services; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; using Microsoft.OpenApi.Models; @@ -11,10 +13,13 @@ var builder = WebApplication.CreateBuilder(args); builder.Services.AddControllers(); // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); -builder.Services.AddAuthorization(); +builder.Services.AddAuthorizationBuilder() + .AddPolicy("AdminOnly", policy => + policy.Requirements.Add(new AdminOnlyRequirement())); builder.Services.AddIdentityApiEndpoints() .AddEntityFrameworkStores(); builder.Services.AddScoped, UserWithProfileManager>(); +builder.Services.AddScoped(); builder.Services.AddSwaggerGen(c => { @@ -32,20 +37,19 @@ builder.Services.AddSwaggerGen(c => c.AddSecurityRequirement(new OpenApiSecurityRequirement() { - { - new OpenApiSecurityScheme { - Reference = new OpenApiReference + new OpenApiSecurityScheme { - Type = ReferenceType.SecurityScheme, - Id = "Bearer" + Reference = new OpenApiReference + { + Type = ReferenceType.SecurityScheme, + Id = "Bearer" + }, + Scheme = "oauth2", + Name = "Bearer", + In = ParameterLocation.Header, }, - Scheme = "oauth2", - Name = "Bearer", - In = ParameterLocation.Header, - - }, - new List() + new List() } }); }); @@ -72,4 +76,4 @@ app.UseAuthorization(); app.MapControllers(); -app.Run(); +app.Run(); \ No newline at end of file