Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix Anon Endpoints #215

Merged
merged 11 commits into from
Jul 17, 2024
8 changes: 2 additions & 6 deletions LeaderboardBackend.Test/Leaderboards.cs
Original file line number Diff line number Diff line change
Expand Up @@ -143,12 +143,8 @@ await _apiClient.Post<LeaderboardViewModel>(
}

CreateLeaderboardRequest reqForInexistentBoard = _createBoardReqFaker.Generate();

RequestFailureException e = Assert.ThrowsAsync<RequestFailureException>(
() => _apiClient.Get<string>($"/api/leaderboards/{reqForInexistentBoard.Slug}", new())
)!;

e.Response.StatusCode.Should().Be(HttpStatusCode.NotFound);
Func<Task<string>> act = async () => await _apiClient.Get<string>($"/api/leaderboards/{reqForInexistentBoard.Slug}", new());
await act.Should().ThrowAsync<RequestFailureException>().Where(e => e.Response.StatusCode == HttpStatusCode.NotFound);
}

private static string ListToQueryString<T>(IEnumerable<T> list, string key)
Expand Down
4 changes: 2 additions & 2 deletions LeaderboardBackend/Authorization/MiddlewareResultHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ public async Task HandleAsync(
PolicyAuthorizationResult policyAuthorizationResult
)
{
if (policyAuthorizationResult.Forbidden)
if (policyAuthorizationResult.AuthorizationFailure?.FailureReasons.Any(fr => fr.Handler is UserTypeAuthorizationHandler) ?? false)
{
httpContext.Response.StatusCode = (int)HttpStatusCode.NotFound;
httpContext.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
zysim marked this conversation as resolved.
Show resolved Hide resolved
return;
}

Expand Down
60 changes: 19 additions & 41 deletions LeaderboardBackend/Authorization/UserTypeAuthorizationHandler.cs
Original file line number Diff line number Diff line change
@@ -1,28 +1,20 @@
using System.Diagnostics.CodeAnalysis;
using LeaderboardBackend.Models.Entities;
using LeaderboardBackend.Result;
using LeaderboardBackend.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;

namespace LeaderboardBackend.Authorization;

public class UserTypeAuthorizationHandler : AuthorizationHandler<UserTypeRequirement>
public class UserTypeAuthorizationHandler(
IOptions<JwtConfig> config,
IUserService userService
) : AuthorizationHandler<UserTypeRequirement>
{
private readonly IAuthService _authService;
private readonly TokenValidationParameters _jwtValidationParams;
private readonly IUserService _userService;

public UserTypeAuthorizationHandler(
IAuthService authService,
IOptions<JwtConfig> config,
IUserService userService
)
{
_authService = authService;
_jwtValidationParams = Jwt.ValidationParameters.GetInstance(config.Value);
_userService = userService;
}
private readonly TokenValidationParameters _jwtValidationParams = Jwt.ValidationParameters.GetInstance(config.Value);
private readonly IUserService _userService = userService;

protected override async Task HandleRequirementAsync(
AuthorizationHandlerContext context,
Expand All @@ -34,37 +26,23 @@ UserTypeRequirement requirement
return;
}

Guid? userId = _authService.GetUserIdFromClaims(context.User);
GetUserResult res = await _userService.GetUserFromClaims(context.User);

if (userId is null)
res.Switch(user =>
{
context.Fail();
return;
}

User? user = _userService.GetUserById(userId.Value).Result;

if (user is null || !Handle(user, requirement))
{
// FIXME: Work out how to fail as a ForbiddenResult.
context.Fail();
return;
}

context.Succeed(requirement);

return;
if (Handle(user, requirement))
{
context.Succeed(requirement);
}
}, badCredentials => context.Fail(new(this, "Bad Credentials")), notFound => context.Fail(new(this, "User Not Found")));
zysim marked this conversation as resolved.
Show resolved Hide resolved
}

private bool Handle(User user, UserTypeRequirement requirement)
private static bool Handle(User user, UserTypeRequirement requirement) => requirement.Type switch
{
return requirement.Type switch
{
UserTypes.ADMINISTRATOR => user.IsAdmin,
UserTypes.USER => true,
_ => false,
};
}
UserTypes.ADMINISTRATOR => user.IsAdmin,
UserTypes.USER => true,
_ => false,
};
private static bool TryGetJwtFromHttpContext(
AuthorizationHandlerContext context,
[NotNullWhen(true)] out string? token
Expand Down
1 change: 1 addition & 0 deletions LeaderboardBackend/Controllers/AccountController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ public async Task<ActionResult<LoginResponse>> Login(
);
}

[Authorize]
[HttpPost("confirm")]
[SwaggerOperation("Resends the account confirmation link.")]
[SwaggerResponse(200, "A new confirmation link was generated.")]
Expand Down
9 changes: 3 additions & 6 deletions LeaderboardBackend/Controllers/CategoriesController.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using LeaderboardBackend.Authorization;
using LeaderboardBackend.Models.Entities;
using LeaderboardBackend.Models.Requests;
using LeaderboardBackend.Models.ViewModels;
Expand All @@ -24,8 +25,6 @@ public CategoriesController(ICategoryService categoryService)
[SwaggerResponse(404)]
public async Task<ActionResult<CategoryViewModel>> GetCategory(long id)
{
// NOTE: Should this use [AllowAnonymous]? - Ero

Category? category = await _categoryService.GetCategory(id);

if (category == null)
Expand All @@ -36,18 +35,16 @@ public async Task<ActionResult<CategoryViewModel>> GetCategory(long id)
return Ok(CategoryViewModel.MapFrom(category));
}

[Authorize(Policy = UserTypes.ADMINISTRATOR)]
[HttpPost]
zysim marked this conversation as resolved.
Show resolved Hide resolved
[SwaggerOperation("Creates a new Category. This request is restricted to Moderators.")]
[SwaggerResponse(201)]
[SwaggerResponse(404)]
[SwaggerResponse(403)]
[SwaggerResponse(422, Type = typeof(ValidationProblemDetails))]
public async Task<ActionResult<CategoryViewModel>> CreateCategory(
[FromBody] CreateCategoryRequest request
)
{
// FIXME: Allow only moderators to call this! - Ero
// NOTE: Allow administrators to call this as well? - Ero

// NOTE: Check that body.PlayersMax > body.PlayersMin? - Ero
Category category =
new()
Expand Down
2 changes: 1 addition & 1 deletion LeaderboardBackend/Controllers/LeaderboardsController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ [FromQuery] long[] ids
[SwaggerOperation("Creates a new leaderboard. This request is restricted to Administrators.")]
[SwaggerResponse(201)]
[SwaggerResponse(401)]
[SwaggerResponse(404, "The requesting `User` is unauthorized to create `Leaderboard`s.")]
[SwaggerResponse(403, "The requesting `User` is unauthorized to create `Leaderboard`s.")]
[SwaggerResponse(422, Type = typeof(ValidationProblemDetails))]
public async Task<ActionResult<LeaderboardViewModel>> CreateLeaderboard(
[FromBody] CreateLeaderboardRequest request
Expand Down
1 change: 1 addition & 0 deletions LeaderboardBackend/Controllers/RunsController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ public async Task<ActionResult<RunViewModel>> GetRun(Guid id)
return Ok(RunViewModel.MapFrom(run));
}

[Authorize]
[HttpPost]
[SwaggerOperation("Creates a new Run.")]
[SwaggerResponse(201)]
Expand Down
1 change: 1 addition & 0 deletions LeaderboardBackend/Controllers/UsersController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ public async Task<ActionResult<UserViewModel>> GetUserById(
return Ok(UserViewModel.MapFrom(user));
}

[Authorize]
[HttpGet("me")]
[SwaggerOperation(
"Gets the currently logged-in User.",
Expand Down
28 changes: 7 additions & 21 deletions LeaderboardBackend/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -221,40 +221,26 @@
JsonWebTokenHandler.DefaultInboundClaimTypeMap.Clear();
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer();

// Configure authorisation.
builder.Services.AddAuthorization(options =>
{
options.AddPolicy(
UserTypes.ADMINISTRATOR,
policy =>
builder.Services.AddAuthorizationBuilder()
.AddPolicy(UserTypes.ADMINISTRATOR, policy =>
{
policy.AuthenticationSchemes.Add(JwtBearerDefaults.AuthenticationScheme);
policy.RequireAuthenticatedUser();
policy.Requirements.Add(new UserTypeRequirement(UserTypes.ADMINISTRATOR));
}
);
options.AddPolicy(
UserTypes.MODERATOR,
policy =>
)
.AddPolicy(UserTypes.MODERATOR, policy =>
zysim marked this conversation as resolved.
Show resolved Hide resolved
{
policy.AuthenticationSchemes.Add(JwtBearerDefaults.AuthenticationScheme);
policy.RequireAuthenticatedUser();
policy.Requirements.Add(new UserTypeRequirement(UserTypes.MODERATOR));
}
);

// Handles empty [Authorize] attributes
options.DefaultPolicy = new AuthorizationPolicyBuilder()
)
.SetDefaultPolicy(new AuthorizationPolicyBuilder()
.AddAuthenticationSchemes(new[] { JwtBearerDefaults.AuthenticationScheme })
.RequireAuthenticatedUser()
.AddRequirements(new[] { new UserTypeRequirement(UserTypes.USER) })
.Build();

options.FallbackPolicy = new AuthorizationPolicyBuilder()
.AddAuthenticationSchemes(new[] { JwtBearerDefaults.AuthenticationScheme })
.RequireAuthenticatedUser()
.Build();
});
.Build());

builder.Services.AddSingleton<IValidatorInterceptor, LeaderboardBackend.Models.Validation.UseErrorCodeInterceptor>();
builder.Services.AddFluentValidationAutoValidation(c =>
Expand Down
6 changes: 3 additions & 3 deletions LeaderboardBackend/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -437,8 +437,8 @@
}
}
},
"404": {
"description": "Not Found"
"403": {
"description": "Forbidden"
},
"422": {
"description": "Unprocessable Content",
Expand Down Expand Up @@ -635,7 +635,7 @@
"401": {
"description": "Unauthorized"
},
"404": {
"403": {
"description": "The requesting `User` is unauthorized to create `Leaderboard`s."
},
"422": {
Expand Down