Skip to content

Commit

Permalink
Merge pull request #674 from cabinetoffice/feature/DP-627-create-supp…
Browse files Browse the repository at this point in the history
…ort-admin-role

Feature/dp 627 create support admin role
  • Loading branch information
rmohammed-goaco authored Oct 1, 2024
2 parents af78e41 + ee9193c commit 3ed146f
Show file tree
Hide file tree
Showing 16 changed files with 127 additions and 33 deletions.
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

0 comments on commit 3ed146f

Please sign in to comment.