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

Part4: Person, Tenant, Forms, Entity Verification API endpoints authorization #644

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions Libraries/CO.CDP.Authentication/Extensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Hosting;
using Microsoft.IdentityModel.Tokens;
using static CO.CDP.Authentication.Constants;

namespace CO.CDP.Authentication;

Expand Down Expand Up @@ -86,6 +87,11 @@ public static AuthorizationBuilder AddEntityVerificationAuthorization(this IServ
{
return services
.AddAuthorizationBuilder()
.AddPolicy("OneLoginPolicy", policy =>
{
policy.RequireAuthenticatedUser();
policy.RequireClaim(ClaimType.Channel, Channel.OneLogin);
})
.SetFallbackPolicy(
new AuthorizationPolicyBuilder()
.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using CO.CDP.EntityVerification.Model;
using CO.CDP.EntityVerification.UseCase;
using FluentAssertions;
using Microsoft.Extensions.DependencyInjection;
using Moq;
using System.Net;
using static CO.CDP.Authentication.Constants;
using static System.Net.HttpStatusCode;

namespace CO.CDP.EntityVerification.Tests.Api;

public class GetIdentifiersTests
{
private readonly Mock<IUseCase<LookupIdentifierQuery, IEnumerable<Identifier>>> _lookupIdentifierUseCase = new();

[Theory]
[InlineData(OK, Channel.OneLogin)]
[InlineData(Forbidden, Channel.ServiceKey)]
[InlineData(Forbidden, Channel.OrganisationKey)]
[InlineData(Forbidden, "unknown_channel")]
public async Task GetIdentifiers_Authorization_ReturnsExpectedStatusCode(
HttpStatusCode expectedStatusCode, string channel)
{
var identifier = "test_identifier";
_lookupIdentifierUseCase.Setup(useCase => useCase.Execute(It.IsAny<LookupIdentifierQuery>()))
.ReturnsAsync([new Identifier { Scheme = "SIC", Id = "01230", LegalName = "Acme Ltd" }]);

var factory = new TestAuthorizationWebApplicationFactory<Program>(
channel, serviceCollection: s => s.AddScoped(_ => _lookupIdentifierUseCase.Object));

var response = await factory.CreateClient().GetAsync($"/identifiers/{identifier}");

response.StatusCode.Should().Be(expectedStatusCode);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authorization.Policy;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System.Security.Claims;

namespace CO.CDP.EntityVerification.Tests.Api;

public class TestAuthorizationWebApplicationFactory<TProgram>(
string channel,
Action<IServiceCollection>? serviceCollection = null)
: WebApplicationFactory<TProgram> where TProgram : class
{
protected override IHost CreateHost(IHostBuilder builder)
{
if (serviceCollection != null) builder.ConfigureServices(serviceCollection);

builder.ConfigureServices(services =>
{
services.AddTransient<IPolicyEvaluator>(sp => new AuthorizationPolicyEvaluator(
ActivatorUtilities.CreateInstance<PolicyEvaluator>(sp), channel));
});

return base.CreateHost(builder);
}
}

public class AuthorizationPolicyEvaluator(PolicyEvaluator innerEvaluator, string? channel) : IPolicyEvaluator
{
const string JwtBearerScheme = "Bearer";

public async Task<AuthenticateResult> AuthenticateAsync(AuthorizationPolicy policy, HttpContext context)
{
var claimsIdentity = new ClaimsIdentity(JwtBearerScheme);
if (!string.IsNullOrWhiteSpace(channel)) claimsIdentity.AddClaims([new Claim("channel", channel)]);

return await Task.FromResult(AuthenticateResult.Success(new AuthenticationTicket(new ClaimsPrincipal(claimsIdentity),
new AuthenticationProperties(), JwtBearerScheme)));
}

public Task<PolicyAuthorizationResult> AuthorizeAsync(
AuthorizationPolicy policy, AuthenticateResult authenticationResult, HttpContext context, object? resource)
{
return innerEvaluator.AuthorizeAsync(policy, authenticationResult, context, resource);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using CO.CDP.Swashbuckle.Filter;
using CO.CDP.Swashbuckle.Security;
using DotSwashbuckle.AspNetCore.SwaggerGen;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.OpenApi.Models;

Expand All @@ -14,7 +15,8 @@ public static class PponEndpointExtensions
public static void UsePponEndpoints(this WebApplication app)
{
app.MapGet("/identifiers/{identifier}",
async (string identifier, IUseCase<LookupIdentifierQuery, IEnumerable<Identifier>> useCase) =>
[Authorize(Policy = "OneLoginPolicy")]
async (string identifier, IUseCase<LookupIdentifierQuery, IEnumerable<Identifier>> useCase) =>
await useCase.Execute(new LookupIdentifierQuery(identifier))
.AndThen(identifier => identifier.Any() ? Results.Ok(identifier) : Results.NotFound()))
.Produces<IEnumerable<Identifier>>(StatusCodes.Status200OK, "application/json")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using Microsoft.Extensions.Hosting;
using Moq;
using System.Net;
using static CO.CDP.Authentication.Constants;
using static System.Net.HttpStatusCode;

namespace CO.CDP.Forms.WebApi.Tests.Api;
Expand Down Expand Up @@ -36,4 +37,28 @@ public async Task DeleteSupplierInformation_TestCases(bool useCaseResult, HttpSt

response.StatusCode.Should().Be(expectedStatusCode);
}

[Theory]
[InlineData(NoContent, Channel.OneLogin, OrganisationPersonScope.Admin)]
[InlineData(NoContent, Channel.OneLogin, OrganisationPersonScope.Editor)]
[InlineData(Forbidden, Channel.OneLogin, OrganisationPersonScope.Viewer)]
[InlineData(Forbidden, Channel.ServiceKey)]
[InlineData(Forbidden, Channel.OrganisationKey)]
[InlineData(Forbidden, "unknown_channel")]
public async Task GetFormSectionQuestions_Authorization_ReturnsExpectedStatusCode(
HttpStatusCode expectedStatusCode, string channel, string? scope = null)
{
var answerSetId = Guid.NewGuid();
var organisationId = Guid.NewGuid();
var command = (organisationId, answerSetId);

_deleteAnswerSetUseCase.Setup(uc => uc.Execute(command)).ReturnsAsync(true);

var factory = new TestAuthorizationWebApplicationFactory<Program>(
channel, organisationId, scope, serviceCollection: s => s.AddScoped(_ => _deleteAnswerSetUseCase.Object));

var response = await factory.CreateClient().DeleteAsync($"/forms/answer-sets/{answerSetId}?organisation-id={organisationId}");

response.StatusCode.Should().Be(expectedStatusCode);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Moq;
using System.Net;
using static CO.CDP.Authentication.Constants;
using static System.Net.HttpStatusCode;

namespace CO.CDP.Forms.WebApi.Tests.Api;
Expand All @@ -19,7 +21,7 @@ public GetFormSectionQuestionsTest()
TestWebApplicationFactory<Program> factory = new(builder =>
{
builder.ConfigureServices(services =>
services.AddScoped<IUseCase<(Guid formId, Guid sectionId, Guid organisationId), SectionQuestionsResponse?>>(_ => _getFormSectionQuestionsUseCase.Object)
services.AddScoped(_ => _getFormSectionQuestionsUseCase.Object)
);
});
_httpClient = factory.CreateClient();
Expand Down Expand Up @@ -59,6 +61,32 @@ public async Task ItFindsTheFormSectionWithQuestions()
await response.Should().HaveContent(sectionQuestionsResponse);
}

[Theory]
[InlineData(OK, Channel.OneLogin, OrganisationPersonScope.Admin)]
[InlineData(OK, Channel.OneLogin, OrganisationPersonScope.Editor)]
[InlineData(OK, Channel.OneLogin, OrganisationPersonScope.Viewer)]
[InlineData(Forbidden, Channel.ServiceKey)]
[InlineData(Forbidden, Channel.OrganisationKey)]
[InlineData(Forbidden, "unknown_channel")]
public async Task GetFormSectionQuestions_Authorization_ReturnsExpectedStatusCode(
HttpStatusCode expectedStatusCode, string channel, string? scope = null)
{
var formId = Guid.NewGuid();
var sectionId = Guid.NewGuid();
var organisationId = Guid.NewGuid();
var command = (formId, sectionId, organisationId);

_getFormSectionQuestionsUseCase.Setup(uc => uc.Execute(command))
.ReturnsAsync(new SectionQuestionsResponse());

var factory = new TestAuthorizationWebApplicationFactory<Program>(
channel, organisationId, scope, serviceCollection: s => s.AddScoped(_ => _getFormSectionQuestionsUseCase.Object));

var response = await factory.CreateClient().GetAsync($"/forms/{formId}/sections/{sectionId}/questions?organisation-id={organisationId}");

response.StatusCode.Should().Be(expectedStatusCode);
}

private static SectionQuestionsResponse GivenSectionQuestionsResponse()
{
var question1 = Guid.NewGuid();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Moq;
using System.Net;
using System.Net.Http.Json;
using static CO.CDP.Authentication.Constants;
using static System.Net.HttpStatusCode;

namespace CO.CDP.Forms.WebApi.Tests.Api;
Expand Down Expand Up @@ -58,4 +60,27 @@ public async Task GetFormSections_WhenFormIdExists_ShouldReturnOk()

response.Should().BeEquivalentTo(formSections);
}

[Theory]
[InlineData(OK, Channel.OneLogin)]
[InlineData(Forbidden, Channel.ServiceKey)]
[InlineData(Forbidden, Channel.OrganisationKey)]
[InlineData(Forbidden, "unknown_channel")]
public async Task GetFormSections_Authorization_ReturnsExpectedStatusCode(
HttpStatusCode expectedStatusCode, string channel)
{
var formId = Guid.NewGuid();
var organisationId = Guid.NewGuid();
var command = (formId, organisationId);

_useCase.Setup(uc => uc.Execute(command))
.ReturnsAsync(new FormSectionResponse { FormSections = [] });

var factory = new TestAuthorizationWebApplicationFactory<Program>(
channel, serviceCollection: s => s.AddScoped(_ => _useCase.Object));

var response = await factory.CreateClient().GetAsync($"/forms/{formId}/sections?organisation-id={organisationId}");

response.StatusCode.Should().Be(expectedStatusCode);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
using CO.CDP.Forms.WebApi.Model;
using CO.CDP.Forms.WebApi.UseCase;
using CO.CDP.TestKit.Mvc;
using FluentAssertions;
using Microsoft.Extensions.DependencyInjection;
using Moq;
using System.Net;
using System.Net.Http.Json;
using static CO.CDP.Authentication.Constants;
using static System.Net.HttpStatusCode;

namespace CO.CDP.Forms.WebApi.Tests.Api;

public class PutFormSectionAnswersTest
{
private readonly Mock<IUseCase<(Guid formId, Guid sectionId, Guid answerSetId, Guid organisationId, UpdateFormSectionAnswers updateFormSectionAnswers), bool>> _updateFormSectionAnswersUseCase = new();

[Theory]
[InlineData(NoContent, Channel.OneLogin, OrganisationPersonScope.Admin)]
[InlineData(NoContent, Channel.OneLogin, OrganisationPersonScope.Editor)]
[InlineData(Forbidden, Channel.OneLogin, OrganisationPersonScope.Viewer)]
[InlineData(Forbidden, Channel.ServiceKey)]
[InlineData(Forbidden, Channel.OrganisationKey)]
[InlineData(Forbidden, "unknown_channel")]
public async Task GetFormSectionQuestions_Authorization_ReturnsExpectedStatusCode(
HttpStatusCode expectedStatusCode, string channel, string? scope = null)
{
var formId = Guid.NewGuid();
var sectionId = Guid.NewGuid();
var answerSetId = Guid.NewGuid();
var organisationId = Guid.NewGuid();
var updateFormSectionAnswers = new UpdateFormSectionAnswers();
var command = (formId, sectionId, answerSetId, organisationId, updateFormSectionAnswers);

_updateFormSectionAnswersUseCase.Setup(uc => uc.Execute(command)).ReturnsAsync(true);

var factory = new TestAuthorizationWebApplicationFactory<Program>(
channel, organisationId, scope, serviceCollection: s => s.AddScoped(_ => _updateFormSectionAnswersUseCase.Object));

var response = await factory.CreateClient().PutAsJsonAsync(
$"/forms/{formId}/sections/{sectionId}/answers/{answerSetId}?organisation-id={organisationId}", updateFormSectionAnswers);

response.StatusCode.Should().Be(expectedStatusCode);
}
}
25 changes: 20 additions & 5 deletions Services/CO.CDP.Forms.WebApi/Api/Forms.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using CO.CDP.Authentication.Authorization;
using CO.CDP.Forms.WebApi.Model;
using CO.CDP.Forms.WebApi.UseCase;
using CO.CDP.Functional;
Expand All @@ -9,6 +10,7 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.OpenApi.Models;
using System.Reflection;
using static CO.CDP.Authentication.Constants;

namespace CO.CDP.Forms.WebApi.Api;

Expand All @@ -17,7 +19,8 @@ public static class EndpointExtensions
public static void UseFormsEndpoints(this WebApplication app)
{
app.MapGet("/forms/{formId}/sections",
async (Guid formId, [FromQuery(Name = "organisation-id")] Guid organisationId, IUseCase<(Guid, Guid), FormSectionResponse?> useCase) =>
[OrganisationAuthorize([AuthenticationChannel.OneLogin])]
async (Guid formId, [FromQuery(Name = "organisation-id")] Guid organisationId, IUseCase<(Guid, Guid), FormSectionResponse?> useCase) =>
await useCase.Execute((formId, organisationId))
.AndThen(res => res != null ? Results.Ok(res) : Results.NotFound()))
.Produces<FormSectionResponse>(StatusCodes.Status200OK, "application/json")
Expand All @@ -37,7 +40,11 @@ await useCase.Execute((formId, organisationId))
});

app.MapGet("/forms/{formId}/sections/{sectionId}/questions",
async (Guid formId, Guid sectionId, [FromQuery(Name = "organisation-id")] Guid organisationId, IUseCase<(Guid, Guid, Guid), SectionQuestionsResponse?> useCase) =>
[OrganisationAuthorize(
[AuthenticationChannel.OneLogin],
[OrganisationPersonScope.Admin, OrganisationPersonScope.Editor, OrganisationPersonScope.Viewer],
OrganisationIdLocation.QueryString)]
async (Guid formId, Guid sectionId, [FromQuery(Name = "organisation-id")] Guid organisationId, IUseCase<(Guid, Guid, Guid), SectionQuestionsResponse?> useCase) =>
await useCase.Execute((formId, sectionId, organisationId))
.AndThen(sectionQuestions => sectionQuestions != null ? Results.Ok(sectionQuestions) : Results.NotFound()))
.Produces<SectionQuestionsResponse>(StatusCodes.Status200OK, "application/json")
Expand All @@ -56,8 +63,12 @@ await useCase.Execute((formId, sectionId, organisationId))
return operation;
});

app.MapPut("/forms/{formId}/sections/{sectionId}/answers/{answerSetId}", async (
Guid formId, Guid sectionId, Guid answerSetId,
app.MapPut("/forms/{formId}/sections/{sectionId}/answers/{answerSetId}",
[OrganisationAuthorize(
[AuthenticationChannel.OneLogin],
[OrganisationPersonScope.Admin, OrganisationPersonScope.Editor],
OrganisationIdLocation.QueryString)]
async (Guid formId, Guid sectionId, Guid answerSetId,
[FromQuery(Name = "organisation-id")] Guid organisationId,
[FromBody] UpdateFormSectionAnswers updateFormSectionAnswers,
IUseCase<(Guid formId, Guid sectionId, Guid answerSetId, Guid organisationId, UpdateFormSectionAnswers updateFormSectionAnswers), bool> updateFormSectionAnswersUseCase) =>
Expand All @@ -82,7 +93,11 @@ await useCase.Execute((formId, sectionId, organisationId))
});

app.MapDelete("/forms/answer-sets/{answerSetId}",
async (Guid answerSetId, [FromQuery(Name = "organisation-id")] Guid organisationId, IUseCase<(Guid, Guid), bool> useCase) =>
[OrganisationAuthorize(
[AuthenticationChannel.OneLogin],
[OrganisationPersonScope.Admin, OrganisationPersonScope.Editor],
OrganisationIdLocation.QueryString)]
async (Guid answerSetId, [FromQuery(Name = "organisation-id")] Guid organisationId, IUseCase<(Guid, Guid), bool> useCase) =>
await useCase.Execute((organisationId, answerSetId))
.AndThen(success => success ? Results.NoContent() : Results.NotFound()))
.Produces(StatusCodes.Status204NoContent)
Expand Down
Loading