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

Feature/dp 627 create support admin role #674

Merged
merged 14 commits into from
Oct 1, 2024
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
40 changes: 32 additions & 8 deletions Frontend/CO.CDP.OrganisationApp.Tests/AuthorizationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public class AuthorizationTests
private static Guid personId = new Guid("5b0d3aa8-94cd-4ede-ba03-546937035690");
private static Guid personInviteGuid = new Guid("330fb1d4-26e2-4c69-898f-6197f9321361");

public HttpClient BuildHttpClient(List<string> userScopes)
public HttpClient BuildHttpClient(List<string> userOrganisationScopes, List<string> userScopes)
{
var services = new ServiceCollection();

Expand All @@ -32,11 +32,11 @@ public HttpClient BuildHttpClient(List<string> userScopes)
Tenant.WebApiClient.PartyRole.Supplier,
Tenant.WebApiClient.PartyRole.Tenderer
],
userScopes,
userOrganisationScopes,
new Uri("http://foo")
);

var person = new Person.WebApiClient.Person("[email protected]", "First name", personId, "Last name");
var person = new Person.WebApiClient.Person("[email protected]", "First name", personId, "Last name", userScopes);

tenantClient.Setup(client => client.LookupTenantAsync())
.ReturnsAsync(
Expand Down Expand Up @@ -83,8 +83,12 @@ [ organisation ]
)
);

personClient.Setup(client => client.LookupPersonAsync(It.IsAny<string>())).ReturnsAsync(person);

services.AddTransient<IOrganisationClient, OrganisationClient>(sc => organisationClient.Object);

services.AddTransient<IPersonClient, PersonClient>(sc => personClient.Object);

_mockSession.Setup(s => s.Get<Models.UserDetails>(Session.UserDetailsKey))
.Returns(new Models.UserDetails() { Email = "[email protected]", UserUrn = "urn", PersonId = person.Id });

Expand Down Expand Up @@ -113,11 +117,31 @@ public static IEnumerable<object[]> TestCases()
yield return new object[] { $"/organisation/{testOrganisationId}/users/{personInviteGuid}/change-role?handler=personInvite", new string[] { "Person invite Last name", "Can add, remove and edit users" } };
}

[Theory]
[MemberData(nameof(TestCases))]
public async Task TestAuthorizationIsSuccessful_WhenUserIsAllowedToAccessResourceAsSupportAdminUser(string url, string[] expectedTexts)
{
var _httpClient = BuildHttpClient([], [PersonScopes.SupportAdmin]);

var request = new HttpRequestMessage(HttpMethod.Get, url);

var response = await _httpClient.SendAsync(request);

var responseBody = await response.Content.ReadAsStringAsync();

responseBody.Should().NotBeNull();
response.StatusCode.Should().Be(HttpStatusCode.OK);
foreach (string expectedText in expectedTexts)
{
responseBody.Should().Contain(expectedText);
}
}

[Theory]
[MemberData(nameof(TestCases))]
public async Task TestAuthorizationIsSuccessful_WhenUserIsAllowedToAccessResourceAsAdminUser(string url, string[] expectedTexts)
{
var _httpClient = BuildHttpClient([OrganisationPersonScopes.Admin]);
var _httpClient = BuildHttpClient([OrganisationPersonScopes.Admin], []);

var request = new HttpRequestMessage(HttpMethod.Get, url);

Expand All @@ -137,7 +161,7 @@ public async Task TestAuthorizationIsSuccessful_WhenUserIsAllowedToAccessResourc
[MemberData(nameof(TestCases))]
public async Task TestAuthorizationIsUnsuccessful_WhenUserIsNotAllowedToAccessResourceAsEditorUser(string url, string[] _)
{
var _httpClient = BuildHttpClient([OrganisationPersonScopes.Editor]);
var _httpClient = BuildHttpClient([OrganisationPersonScopes.Editor], []);

var request = new HttpRequestMessage(HttpMethod.Get, url);

Expand All @@ -154,7 +178,7 @@ public async Task TestAuthorizationIsUnsuccessful_WhenUserIsNotAllowedToAccessRe
[MemberData(nameof(TestCases))]
public async Task TestAuthorizationIsUnsuccessful_WhenUserIsNotAllowedToAccessResourceAsUserWithoutPermissions(string url, string[] _)
{
var _httpClient = BuildHttpClient([]);
var _httpClient = BuildHttpClient([], []);

var request = new HttpRequestMessage(HttpMethod.Get, url);

Expand All @@ -170,7 +194,7 @@ public async Task TestAuthorizationIsUnsuccessful_WhenUserIsNotAllowedToAccessRe
[Fact]
public async Task TestCanSeeUsersLinkOnOrganisationPage_WhenUserIsAllowedToAccessResourceAsAdminUser()
{
var _httpClient = BuildHttpClient([ OrganisationPersonScopes.Admin, OrganisationPersonScopes.Viewer ]);
var _httpClient = BuildHttpClient([ OrganisationPersonScopes.Admin, OrganisationPersonScopes.Viewer ], []);

var request = new HttpRequestMessage(HttpMethod.Get, $"/organisation/{testOrganisationId}");

Expand All @@ -187,7 +211,7 @@ public async Task TestCanSeeUsersLinkOnOrganisationPage_WhenUserIsAllowedToAcces
[Fact]
public async Task TestCannotSeeUsersLinkOnOrganisationPage_WhenUserIsNotAllowedToAccessResourceAsEditorUser()
{
var _httpClient = BuildHttpClient([ OrganisationPersonScopes.Editor ]);
var _httpClient = BuildHttpClient([ OrganisationPersonScopes.Editor ], []);

var request = new HttpRequestMessage(HttpMethod.Get, $"/organisation/{testOrganisationId}");

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ public async Task OnGet_UnknownActionParameter_ShouldReturnToIndex()
private readonly AuthenticateResult authResultFail = AuthenticateResult.Fail(new Exception("Auth failed"));

private readonly Person.WebApiClient.Person dummyPerson
= new("[email protected]", "firstdummy", new Guid("0bacf3d1-3b69-4efa-80e9-3623f4b7786e"), "lastdummy");
= new("[email protected]", "firstdummy", new Guid("0bacf3d1-3b69-4efa-80e9-3623f4b7786e"), "lastdummy", new List<string>());

private OneLogin GivenOneLoginCallbackModel()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public class ClaimOrganisationInviteModelTests

public ClaimOrganisationInviteModelTests()
{
var person = new Person.WebApiClient.Person("test@test", "F1", PersonId, "L1");
var person = new Person.WebApiClient.Person("test@test", "F1", PersonId, "L1", new List<string>());
personClientMock = new Mock<IPersonClient>();
personClientMock.Setup(pc => pc.LookupPersonAsync(UsreUrn)).ReturnsAsync(person);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -289,7 +289,7 @@ public async Task OnPost_UnprocessableEntity_AddsModelError()
}

private readonly Person.WebApiClient.Person dummyPerson
= new("[email protected]", "firstdummy", Guid.NewGuid(), "lastdummy");
= new("[email protected]", "firstdummy", Guid.NewGuid(), "lastdummy", new List<string>());

private YourDetailsModel GivenYourDetailsModel()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ namespace CO.CDP.OrganisationApp.Authorization;

public class CustomAuthorizationPolicyProvider : IAuthorizationPolicyProvider
{
const string POLICY_PREFIX = "OrgScope_";
const string ORG_POLICY_PREFIX = "OrgScope_";
const string POLICY_PREFIX = "PersonScope_";
private readonly DefaultAuthorizationPolicyProvider _fallbackPolicyProvider;

public CustomAuthorizationPolicyProvider(IOptions<AuthorizationOptions> options)
Expand All @@ -15,13 +16,13 @@ public CustomAuthorizationPolicyProvider(IOptions<AuthorizationOptions> options)

public Task<AuthorizationPolicy?> GetPolicyAsync(string policyName)
{
if (policyName.StartsWith(POLICY_PREFIX))
{
var role = policyName.Substring(POLICY_PREFIX.Length);
var role = extractRoleFromPolicyName(policyName);

if (role != null)
{
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.AddRequirements(new OrganizationScopeRequirement(role))
.AddRequirements(new ScopeRequirement(role))
.Build();

return Task.FromResult<AuthorizationPolicy?>(policy);
Expand All @@ -30,6 +31,21 @@ public CustomAuthorizationPolicyProvider(IOptions<AuthorizationOptions> options)
return _fallbackPolicyProvider.GetPolicyAsync(policyName);
}

private static string? extractRoleFromPolicyName(string policyName)
{
if (policyName.StartsWith(ORG_POLICY_PREFIX))
{
return policyName.Substring(ORG_POLICY_PREFIX.Length);
}

if (policyName.StartsWith(POLICY_PREFIX))
{
return policyName.Substring(POLICY_PREFIX.Length);
}

return null;
}

public Task<AuthorizationPolicy> GetDefaultPolicyAsync() => _fallbackPolicyProvider.GetDefaultPolicyAsync();

public Task<AuthorizationPolicy?> GetFallbackPolicyAsync() => _fallbackPolicyProvider.GetFallbackPolicyAsync();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@

namespace CO.CDP.OrganisationApp.Authorization;

public class OrganizationScopeHandler(
public class CustomScopeHandler(
ISession session,
IServiceScopeFactory serviceScopeFactory) : AuthorizationHandler<OrganizationScopeRequirement>
IServiceScopeFactory serviceScopeFactory) : AuthorizationHandler<ScopeRequirement>
{
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, OrganizationScopeRequirement requirement)
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, ScopeRequirement requirement)
{
Models.UserDetails? userDetails = session.Get<Models.UserDetails>(Session.UserDetailsKey);

Expand All @@ -20,23 +20,32 @@ protected override async Task HandleRequirementAsync(AuthorizationHandlerContext
{
IUserInfoService _userInfo = serviceScope.ServiceProvider.GetRequiredService<IUserInfoService>();

var scopes = await _userInfo.GetOrganisationUserScopes();
var userScopes = await _userInfo.GetUserScopes();

// SupportAdmin role can do anything within any organisation
if (userScopes.Contains(PersonScopes.SupportAdmin))
{
context.Succeed(requirement);
return;
}

var organisationUserScopes = await _userInfo.GetOrganisationUserScopes();

// Admin role can do anything within this organisation
if (scopes.Contains(OrganisationPersonScopes.Admin))
if (organisationUserScopes.Contains(OrganisationPersonScopes.Admin))
{
context.Succeed(requirement);
return;
}

if (scopes.Contains(requirement.Scope))
if (organisationUserScopes.Contains(requirement.Scope))
{
context.Succeed(requirement);
return;
}

// Editor role implies viewer permissions
if (requirement.Scope == OrganisationPersonScopes.Viewer && scopes.Contains(OrganisationPersonScopes.Editor))
if (requirement.Scope == OrganisationPersonScopes.Viewer && organisationUserScopes.Contains(OrganisationPersonScopes.Editor))
{
context.Succeed(requirement);
return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@

namespace CO.CDP.OrganisationApp.Authorization;

public class OrganizationScopeRequirement : IAuthorizationRequirement
public class ScopeRequirement : IAuthorizationRequirement
{
public string Scope { get; }

public OrganizationScopeRequirement(string scope)
public ScopeRequirement(string scope)
{
Scope = scope;
}
Expand Down
11 changes: 11 additions & 0 deletions Frontend/CO.CDP.OrganisationApp/Constants/PersonScopes.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
namespace CO.CDP.OrganisationApp.Constants;

public class PersonScopes
{
public const string SupportAdmin = "SUPPORTADMIN";
}

public class PersonScopeRequirement
{
public const string SupportAdmin = "PersonScope_" + PersonScopes.SupportAdmin;
}
2 changes: 1 addition & 1 deletion Frontend/CO.CDP.OrganisationApp/IUserInfoService.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
public interface IUserInfoService
{
public Task<ICollection<String>> GetUserScopes();
public Guid? GetOrganisationId();
public Task<ICollection<String>> GetOrganisationUserScopes();

public Task<bool> UserHasScope(string scope);
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
using CO.CDP.Organisation.WebApiClient;
using CO.CDP.OrganisationApp.Constants;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

namespace CO.CDP.OrganisationApp.Pages.Support;

[Authorize(Policy = PersonScopeRequirement.SupportAdmin)]
public class OrganisationsModel(
IOrganisationClient organisationClient,
ISession session) : LoggedInUserAwareModel(session)
Expand Down
2 changes: 1 addition & 1 deletion Frontend/CO.CDP.OrganisationApp/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@
});

builder.Services.AddSingleton<IAuthorizationPolicyProvider, CustomAuthorizationPolicyProvider>();
builder.Services.AddSingleton<IAuthorizationHandler, OrganizationScopeHandler>();
builder.Services.AddSingleton<IAuthorizationHandler, CustomScopeHandler>();
builder.Services.AddSingleton<IAuthorizationMiddlewareResultHandler, CustomAuthorizationMiddlewareResultHandler>();
builder.Services.AddAuthorization();

Expand Down
28 changes: 26 additions & 2 deletions Frontend/CO.CDP.OrganisationApp/UserInfoService.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,33 @@
using CO.CDP.Person.WebApiClient;
using CO.CDP.Tenant.WebApiClient;

namespace CO.CDP.OrganisationApp;

public class UserInfoService(IHttpContextAccessor httpContextAccessor, ITenantClient tenantClient) : IUserInfoService
public class UserInfoService(IHttpContextAccessor httpContextAccessor, ITenantClient tenantClient, IPersonClient personClient) : IUserInfoService
{
public async Task<ICollection<string>> GetUserScopes()
{
var userUrn = GetUserUrn();
if (userUrn == null)
{
return new List<string>(); // Return an empty list if no userUrn is found
}

var person = await personClient.LookupPersonAsync(userUrn);

if (person == null || person.Scopes == null)
{
return new List<string>(); // Return an empty list if no person or roles are found
}

return person.Scopes;
}

private string? GetUserUrn()
{
return httpContextAccessor.HttpContext?.User?.FindFirst("sub")?.Value;
}

public async Task<ICollection<string>> GetOrganisationUserScopes()
{
Guid? organisationId = GetOrganisationId();
Expand All @@ -14,7 +38,7 @@ public async Task<ICollection<string>> GetOrganisationUserScopes()
if(organisation != null)
{
return organisation.Scopes;
}
}
}

return [];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,25 +25,28 @@ public async Task ItReturnsNullIfNoPersonIsFound()
public async Task ItReturnsTheFoundPerson()
{
var persontId = Guid.NewGuid();
var scopes = new List<string>();
var tenant = new OrganisationInformation.Persistence.Person
{
Id = 42,
Guid = persontId,
Email = "[email protected]",
FirstName = "fn",
LastName = "ln",
Scopes = scopes
};

_repository.Setup(r => r.Find(persontId)).ReturnsAsync(tenant);

var found = await UseCase.Execute(persontId);

found.Should().Be(new Model.Person
found.Should().BeEquivalentTo(new Model.Person
{
Id = persontId,
FirstName = "fn",
LastName = "ln",
Email = "[email protected]",
Scopes = scopes
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,16 @@ public async Task Execute_IfNoPersonIsFound_ReturnsNull()
public async Task Execute_IfPersonIsFound_ReturnsPerson()
{
var personId = Guid.NewGuid();
var scopes = new List<string>();
var persistencePerson = new OrganisationInformation.Persistence.Person
{
Id = 1,
Guid = personId,
FirstName = "fn",
LastName = "ln",
Email = "[email protected]",
UserUrn = "urn:fdc:gov.uk:2022:7wTqYGMFQxgukTSpSI2GodMwe9"
UserUrn = "urn:fdc:gov.uk:2022:7wTqYGMFQxgukTSpSI2GodMwe9",
Scopes = scopes
};

_repository.Setup(r => r.FindByUrn(persistencePerson.UserUrn)).ReturnsAsync(persistencePerson);
Expand All @@ -44,6 +46,7 @@ public async Task Execute_IfPersonIsFound_ReturnsPerson()
Email = "[email protected]",
FirstName = "fn",
LastName = "ln",
Scopes = scopes
}, options => options.ComparingByMembers<Model.Person>());
}
}
Loading