Skip to content

Commit

Permalink
Add roles authentication to v2 endpoints
Browse files Browse the repository at this point in the history
  • Loading branch information
MrKevJoy committed Nov 16, 2023
1 parent 050f0fd commit 3c6204c
Show file tree
Hide file tree
Showing 12 changed files with 111 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ public class ApiClient
{
public required string ClientId { get; set; }
public required List<string> ApiKey { get; set; }
public required List<string> Roles { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,21 +46,25 @@ protected override Task<AuthenticateResult> HandleAuthenticateAsync()
return Task.FromResult(AuthenticateResult.Fail($"No client found with specified API key."));
}

var principal = CreatePrincipal(client.ClientId);
var principal = CreatePrincipal(client.ClientId, client.Roles);
var ticket = new AuthenticationTicket(principal, Scheme.Name);

LogContext.PushProperty("ClientId", client.ClientId);

return Task.FromResult(AuthenticateResult.Success(ticket));
}

public static ClaimsPrincipal CreatePrincipal(string clientId)
public static ClaimsPrincipal CreatePrincipal(string clientId, IEnumerable<string> roles)
{
var identity = new ClaimsIdentity(new[]
{
new Claim(ClaimTypes.Name, clientId)
});

foreach(var role in roles)
{
identity.AddClaim(new Claim(ClaimTypes.Role, role));
}
return new ClaimsPrincipal(identity);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,8 @@ public static class AuthorizationPolicies
public const string ApiKey = nameof(ApiKey);
public const string IdentityUserWithTrn = nameof(IdentityUserWithTrn);
public const string Hangfire = nameof(Hangfire);
public const string GetTeacher = nameof(GetTeacher);
public const string UpdateTeacher = nameof(UpdateTeacher);
public const string UpdateNpq = nameof(UpdateNpq);
public const string UnlockTeacher = nameof(UnlockTeacher);
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ private static ApiClient[] GetClientsFromConfiguration(IConfiguration configurat
var client = new ApiClient()
{
ClientId = clientId,
ApiKey = new List<string>()

ApiKey = new List<string>(),
Roles = new List<string>()
};
kvp.Bind(client);
if (!client.ApiKey.Any() && !string.IsNullOrEmpty(apiKey))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace TeachingRecordSystem.Api.Infrastructure.Security;

public class RoleNames
{
public const string GetPerson = "GetPerson";
public const string UpdatePerson = "UpdatePerson";
public const string UpdateNpq = "UpdateNpq";
public const string UnlockTeacher = "UnlockTeacher";
}
24 changes: 24 additions & 0 deletions TeachingRecordSystem/src/TeachingRecordSystem.Api/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,30 @@ public static void Main(string[] args)
.AddAuthenticationSchemes(BasicAuthenticationDefaults.AuthenticationScheme)
.RequireAuthenticatedUser()
);

options.AddPolicy(
AuthorizationPolicies.GetTeacher,
policy => policy
.AddAuthenticationSchemes(ApiKeyAuthenticationHandler.AuthenticationScheme)
.RequireRole(new[] { RoleNames.GetPerson, RoleNames.UpdatePerson }));

options.AddPolicy(
AuthorizationPolicies.UpdateTeacher,
policy => policy
.AddAuthenticationSchemes(ApiKeyAuthenticationHandler.AuthenticationScheme)
.RequireRole(new[] { RoleNames.UpdatePerson }));

options.AddPolicy(
AuthorizationPolicies.UpdateNpq,
policy => policy
.AddAuthenticationSchemes(ApiKeyAuthenticationHandler.AuthenticationScheme)
.RequireRole(new[] { RoleNames.UpdateNpq }));

options.AddPolicy(
AuthorizationPolicies.UnlockTeacher,
policy => policy
.AddAuthenticationSchemes(ApiKeyAuthenticationHandler.AuthenticationScheme)
.RequireRole(new[] { RoleNames.UnlockTeacher }));
});

services
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
using MediatR;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using NSwag.Annotations;
using TeachingRecordSystem.Api.Infrastructure.Filters;
using TeachingRecordSystem.Api.Infrastructure.Security;
using TeachingRecordSystem.Api.V2.Requests;

namespace TeachingRecordSystem.Api.V2.Controllers;

[ApiController]
[Route("npq-qualifications")]
[Authorize(Policy = AuthorizationPolicies.UpdateNpq)]
public class NpqQualificationsController : ControllerBase
{
private readonly IMediator _mediator;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
using MediatR;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using NSwag.Annotations;
using TeachingRecordSystem.Api.Infrastructure.Filters;
using TeachingRecordSystem.Api.Infrastructure.Logging;
using TeachingRecordSystem.Api.Infrastructure.Security;
using TeachingRecordSystem.Api.V2.Requests;
using TeachingRecordSystem.Api.V2.Responses;

Expand All @@ -25,6 +27,7 @@ public TeachersController(IMediator mediator)
summary: "Find teachers",
description: "Returns teachers matching the specified criteria")]
[ProducesResponseType(typeof(FindTeachersResponse), StatusCodes.Status200OK)]
[Authorize(Policy= AuthorizationPolicies.GetTeacher)]
public async Task<IActionResult> FindTeachers(FindTeachersRequest request)
{
var response = await _mediator.Send(request);
Expand All @@ -37,12 +40,14 @@ public async Task<IActionResult> FindTeachers(FindTeachersRequest request)
summary: "Get teacher",
description: "Gets an individual teacher by their TRN")]
[ProducesResponseType(typeof(GetTeacherResponse), StatusCodes.Status200OK)]
[Authorize(Policy = AuthorizationPolicies.GetTeacher)]
public async Task<IActionResult> GetTeacher([FromRoute] GetTeacherRequest request)
{
var response = await _mediator.Send(request);
return response != null ? Ok(response) : NotFound();
}


[HttpPatch("update/{trn}")]
[OpenApiOperation(
operationId: "UpdateTeacher",
Expand All @@ -52,6 +57,7 @@ public async Task<IActionResult> GetTeacher([FromRoute] GetTeacherRequest reques
[MapError(10001, statusCode: StatusCodes.Status404NotFound)]
[MapError(10002, statusCode: StatusCodes.Status409Conflict)]
[RedactQueryParam("birthdate")]
[Authorize(Policy = AuthorizationPolicies.UpdateTeacher)]
public async Task<IActionResult> Update([FromBody] UpdateTeacherRequest request)
{
await _mediator.Send(request);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
using MediatR;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using NSwag.Annotations;
using TeachingRecordSystem.Api.Infrastructure.Security;
using TeachingRecordSystem.Api.V2.Requests;
using TeachingRecordSystem.Api.V2.Responses;

namespace TeachingRecordSystem.Api.V2.Controllers;

[ApiController]
[Route("unlock-teacher")]
[Authorize(Policy = AuthorizationPolicies.UnlockTeacher)]
public class UnlockTeacherController : ControllerBase
{
private readonly IMediator _mediator;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ protected ApiTestBase(ApiFixture apiFixture)
{
ApiFixture = apiFixture;
_testServices = TestScopedServices.Reset();
SetCurrentApiClient("tests");
SetCurrentApiClient(Array.Empty<string>());

{
HttpClientWithApiKey = apiFixture.CreateClient();
Expand Down Expand Up @@ -80,10 +80,11 @@ public HttpClient GetHttpClientWithIdentityAccessToken(string trn, string scope
return httpClient;
}

protected void SetCurrentApiClient(string clientId)
protected void SetCurrentApiClient(IEnumerable<string> roles, string clientId="tests")
{
var currentUserProvider = ApiFixture.Services.GetRequiredService<CurrentApiClientProvider>();
currentUserProvider.CurrentApiClientId = clientId;
currentUserProvider.Roles = roles.ToArray();
}

public virtual async Task<T> WithDbContext<T>(Func<TrsDbContext, Task<T>> action)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,11 @@ protected override Task<AuthenticateResult> HandleAuthenticateAsync()
}

var currentApiClientId = _currentApiClientProvider.CurrentApiClientId;
var currentRoles = _currentApiClientProvider.Roles;

if (currentApiClientId is not null)
{
var principal = ApiKeyAuthenticationHandler.CreatePrincipal(currentApiClientId);
var principal = ApiKeyAuthenticationHandler.CreatePrincipal(currentApiClientId, currentRoles);

var ticket = new AuthenticationTicket(principal, Scheme.Name);

Expand All @@ -51,11 +52,19 @@ public class TestAuthenticationOptions : AuthenticationSchemeOptions { }
public class CurrentApiClientProvider
{
private readonly AsyncLocal<string> _currentApiClientId = new();
private readonly AsyncLocal<string[]> _roles = new();

[DisallowNull]
public string? CurrentApiClientId
{
get => _currentApiClientId.Value;
set => _currentApiClientId.Value = value;
}

[DisallowNull]
public string[]? Roles
{
get => _roles.Value;
set => _roles.Value = value;
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
#nullable disable
using System.Reflection;
using Microsoft.Xrm.Sdk;
using TeachingRecordSystem.Api.Infrastructure.Security;
using TeachingRecordSystem.Api.Properties;
using TeachingRecordSystem.Api.V2.ApiModels;
using Xunit.Sdk;

namespace TeachingRecordSystem.Api.Tests.V2.Operations;

Expand All @@ -10,6 +13,27 @@ public class GetTeacherTests : ApiTestBase
public GetTeacherTests(ApiFixture apiFixture)
: base(apiFixture)
{
SetCurrentApiClient(new[] { RoleNames.GetPerson });
}

[Theory, RoleNamesData(new[] { RoleNames.GetPerson, RoleNames.UpdatePerson })]
public async Task GetTeacher_ClientDoesNotHavePermission_ReturnsForbidden(string[] roles)
{
// Arrange
SetCurrentApiClient(roles);
var trn = "1234567";

DataverseAdapterMock
.Setup(mock => mock.GetTeacherByTrn(trn, It.IsAny<string[]>(), true))
.ReturnsAsync((Contact)null);

var request = new HttpRequestMessage(HttpMethod.Get, $"/v2/teachers/{trn}");

// Act
var response = await HttpClientWithApiKey.SendAsync(request);

// Assert
Assert.Equal(StatusCodes.Status403Forbidden, (int)response.StatusCode);
}

[Theory]
Expand Down Expand Up @@ -338,3 +362,19 @@ await AssertEx.JsonResponseEquals(
});
}
}

public class RoleNamesData : DataAttribute
{
private string[] RolesToExclude { get; }

public RoleNamesData(string[] except = null)
{
RolesToExclude = except;
}
public override IEnumerable<object[]> GetData(MethodInfo testMethod)
{
var allRoles = new object[] { RoleNames.UpdateNpq, RoleNames.UpdatePerson, RoleNames.GetPerson, RoleNames.UnlockTeacher};
var excluded = allRoles.Except(RolesToExclude);
return new[] { new object[] { excluded } };
}
}

0 comments on commit 3c6204c

Please sign in to comment.