Skip to content

Commit

Permalink
Refactoring to add test for subject collapsing
Browse files Browse the repository at this point in the history
  • Loading branch information
elsand committed Dec 12, 2024
1 parent fe0040e commit d5e418c
Show file tree
Hide file tree
Showing 3 changed files with 138 additions and 58 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -171,71 +171,21 @@ private async Task<DialogSearchAuthorizationResult> PerformDialogSearchAuthoriza
.ToDictionary(kv => kv.Key, kv => kv.Value),
};

await CollapseSubjectResources(dialogSearchAuthorizationResult, authorizedParties, request.ConstraintServiceResources, cancellationToken);
await AuthorizationHelper.CollapseSubjectResources(
dialogSearchAuthorizationResult,
authorizedParties,
request.ConstraintServiceResources,
GetAllSubjectResources,
cancellationToken);

return dialogSearchAuthorizationResult;
}

private async Task CollapseSubjectResources(
DialogSearchAuthorizationResult dialogSearchAuthorizationResult,
AuthorizedPartiesResult authorizedParties,
List<string> constraintResources,
CancellationToken cancellationToken)
{
var authorizedPartiesWithRoles = authorizedParties.AuthorizedParties
.Where(p => p.AuthorizedRoles.Count != 0)
.ToList();

// Extract all unique subjects (authorized roles) from all parties.
var uniqueSubjects = authorizedPartiesWithRoles
.SelectMany(p => p.AuthorizedRoles)
.ToHashSet();

// Retrieve all subject resource mappings, considering any provided constraints
var subjectResources = await GetSubjectResources(uniqueSubjects, constraintResources, cancellationToken);

// Group resources by subject for O(1) lookups when looping
var subjectToResources = subjectResources
.GroupBy(sr => sr.Subject)
.ToDictionary(g => g.Key, g => g.Select(sr => sr.Resource).ToHashSet());

foreach (var partyEntry in authorizedPartiesWithRoles)
{
// Get or create the resource list for the current party, so we can
// union the resource level accesses to those granted via roles
if (!dialogSearchAuthorizationResult.ResourcesByParties.TryGetValue(partyEntry.Party, out var resourceList))
{
resourceList = new HashSet<string>();
dialogSearchAuthorizationResult.ResourcesByParties[partyEntry.Party] = resourceList;
}

foreach (var subject in partyEntry.AuthorizedRoles)
{
if (subjectToResources.TryGetValue(subject, out var subjectResourceSet))
{
resourceList.UnionWith(subjectResourceSet);
}
}

// Remove the party if it has no authorized resources
if (resourceList.Count == 0)
{
dialogSearchAuthorizationResult.ResourcesByParties.Remove(partyEntry.Party);
}
}
}

private async Task<List<SubjectResource>> GetSubjectResources(IEnumerable<string> subjects, List<string> resourceConstraints, CancellationToken cancellationToken)
{
// Fetch all subject resources from the database
var subjectResources = await _subjectResourcesCache.GetOrSetAsync(nameof(SubjectResource), async ct
private async Task<List<SubjectResource>> GetAllSubjectResources(CancellationToken cancellationToken) =>
await _subjectResourcesCache.GetOrSetAsync(nameof(SubjectResource), async ct
=> await _dialogDbContext.SubjectResources.ToListAsync(cancellationToken: ct),
token: cancellationToken);

// Return the subject resources matched with the subjects
return subjectResources.Where(x => subjects.Contains(x.Subject) && (resourceConstraints.Count == 0 || resourceConstraints.Contains(x.Resource))).ToList();
}

private async Task<DialogDetailsAuthorizationResult> PerformDialogDetailsAuthorization(
DialogDetailsAuthorizationRequest request, CancellationToken cancellationToken)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
using Digdir.Domain.Dialogporten.Application.Externals.AltinnAuthorization;
using Digdir.Domain.Dialogporten.Domain.SubjectResources;

namespace Digdir.Domain.Dialogporten.Infrastructure.Altinn.Authorization;

internal static class AuthorizationHelper
{
public static async Task CollapseSubjectResources(
DialogSearchAuthorizationResult dialogSearchAuthorizationResult,
AuthorizedPartiesResult authorizedParties,
List<string> constraintResources,
Func<CancellationToken, Task<List<SubjectResource>>> getAllSubjectResources,
CancellationToken cancellationToken)
{
var authorizedPartiesWithRoles = authorizedParties.AuthorizedParties
.Where(p => p.AuthorizedRoles.Count != 0)
.ToList();

var uniqueSubjects = authorizedPartiesWithRoles
.SelectMany(p => p.AuthorizedRoles)
.ToHashSet();

var subjectResources = (await getAllSubjectResources(cancellationToken))
.Where(x => uniqueSubjects.Contains(x.Subject) && (constraintResources.Count == 0 || constraintResources.Contains(x.Resource))).ToList();

var subjectToResources = subjectResources
.GroupBy(sr => sr.Subject)
.ToDictionary(g => g.Key, g => g.Select(sr => sr.Resource).ToHashSet());

foreach (var partyEntry in authorizedPartiesWithRoles)
{
if (!dialogSearchAuthorizationResult.ResourcesByParties.TryGetValue(partyEntry.Party, out var resourceList))
{
resourceList = new HashSet<string>();
dialogSearchAuthorizationResult.ResourcesByParties[partyEntry.Party] = resourceList;
}

foreach (var subject in partyEntry.AuthorizedRoles)
{
if (subjectToResources.TryGetValue(subject, out var subjectResourceSet))
{
resourceList.UnionWith(subjectResourceSet);
}
}

if (resourceList.Count == 0)
{
dialogSearchAuthorizationResult.ResourcesByParties.Remove(partyEntry.Party);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
using Digdir.Domain.Dialogporten.Application.Externals.AltinnAuthorization;
using Digdir.Domain.Dialogporten.Domain.SubjectResources;
using Digdir.Domain.Dialogporten.Infrastructure.Altinn.Authorization;
using Xunit;

namespace Digdir.Domain.Dialogporten.Infrastructure.Unit.Tests;

public class AuthorizationHelperTests
{
[Fact]
public async Task CollapseSubjectResources_ShouldCollapseCorrectly()
{
// Arrange
var dialogSearchAuthorizationResult = new DialogSearchAuthorizationResult
{
ResourcesByParties = new Dictionary<string, HashSet<string>>()
};
var authorizedParties = new AuthorizedPartiesResult
{
AuthorizedParties = new List<AuthorizedParty>
{
new()
{
Party = "party1",
AuthorizedRoles = new List<string> { "role1", "role2" }
},
new()
{
Party = "party2",
AuthorizedRoles = new List<string> { "role2" }
},
new()
{
Party = "party3",
AuthorizedRoles = new List<string> { "role3" }
}
}
};
var constraintResources = new List<string> { "resource1", "resource2", "resource4" };

// Simulate subject resources
var subjectResources = new List<SubjectResource>
{
new() { Subject = "role1", Resource = "resource1" },
new() { Subject = "role1", Resource = "resource2" },
new() { Subject = "role2", Resource = "resource2" },
new() { Subject = "role2", Resource = "resource3" },
new() { Subject = "role2", Resource = "resource4" },
new() { Subject = "role3", Resource = "resource5" }, // Note: not in constraintResources
};

Task<List<SubjectResource>> GetSubjectResources(CancellationToken token)
{
return Task.FromResult(subjectResources);
}

// Act
await AuthorizationHelper.CollapseSubjectResources(
dialogSearchAuthorizationResult,
authorizedParties,
constraintResources,
GetSubjectResources,
CancellationToken.None);

// Assert
Assert.Equal(2, dialogSearchAuthorizationResult.ResourcesByParties.Count);
Assert.Contains("party1", dialogSearchAuthorizationResult.ResourcesByParties.Keys);
Assert.Contains("resource1", dialogSearchAuthorizationResult.ResourcesByParties["party1"]);
Assert.Contains("resource2", dialogSearchAuthorizationResult.ResourcesByParties["party1"]);
Assert.Contains("resource4", dialogSearchAuthorizationResult.ResourcesByParties["party1"]);
Assert.Equal(3, dialogSearchAuthorizationResult.ResourcesByParties["party1"].Count);

Assert.Contains("party2", dialogSearchAuthorizationResult.ResourcesByParties.Keys);
Assert.Contains("resource2", dialogSearchAuthorizationResult.ResourcesByParties["party2"]);
Assert.Contains("resource4", dialogSearchAuthorizationResult.ResourcesByParties["party2"]);
Assert.Equal(2, dialogSearchAuthorizationResult.ResourcesByParties["party2"].Count);
}
}

0 comments on commit d5e418c

Please sign in to comment.