Skip to content
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
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
namespace HttpsRichardy.Federation.Application.Handlers.User;

public sealed class ListUserAssignedPermissionsHandler(IUserCollection collection) :
public sealed class ListUserAssignedPermissionsHandler(IUserCollection collection, IGroupCollection groupCollection) :
IDispatchHandler<ListUserAssignedPermissionsParameters, Result<IReadOnlyCollection<PermissionDetailsScheme>>>
{
public async Task<Result<IReadOnlyCollection<PermissionDetailsScheme>>> HandleAsync(
Expand All @@ -13,8 +13,26 @@ public async Task<Result<IReadOnlyCollection<PermissionDetailsScheme>>> HandleAs
var users = await collection.GetUsersAsync(filters, cancellation);
var user = users.FirstOrDefault();

return user is not null
? Result<IReadOnlyCollection<PermissionDetailsScheme>>.Success(PermissionMapper.AsResponse(user.Permissions))
: Result<IReadOnlyCollection<PermissionDetailsScheme>>.Failure(UserErrors.UserDoesNotExist);
if (user is null)
{
return Result<IReadOnlyCollection<PermissionDetailsScheme>>.Failure(UserErrors.UserDoesNotExist);
}

var identifiers = user.Groups
.Select(group => group.Id)
.ToList();

var groupFilters = GroupFilters.WithSpecifications()
.WithIdentifiers([.. identifiers])
.Build();

var groups = await groupCollection.GetGroupsAsync(groupFilters, cancellation);
var permissions = groups
.SelectMany(group => group.Permissions)
.Concat(user.Permissions)
.DistinctBy(permission => permission.Name)
.ToList();

return Result<IReadOnlyCollection<PermissionDetailsScheme>>.Success(PermissionMapper.AsResponse(permissions));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,18 @@ namespace HttpsRichardy.Federation.Domain.Filtering.Builders;
public sealed class GroupFiltersBuilder :
FiltersBuilderBase<GroupFilters, GroupFiltersBuilder>
{
public GroupFiltersBuilder WithIdentifiers(string[] identifiers)
{
var validIdentifiers = identifiers?
.Where(identifier => !string.IsNullOrWhiteSpace(identifier))
.ToArray();

if (validIdentifiers?.Length > 0)
_filters.Identifiers = validIdentifiers;

return this;
}

public GroupFiltersBuilder WithRealmId(string? realmId)
{
_filters.RealmId = realmId;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ public sealed class GroupFilters : Filters
{
public string? RealmId { get; set; }
public string? Name { get; set; }
public string[]? Identifiers { get; set; }

public static GroupFiltersBuilder WithSpecifications() => new();
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ public static PipelineDefinition<Group, BsonDocument> FilterGroups(this Pipeline
var definitions = new List<FilterDefinition<BsonDocument>>
{
FilterDefinitions.MatchIfNotEmpty(Documents.Group.Id, filters.Id),
FilterDefinitions.MustBeInIfNotEmpty(Documents.Group.Id, filters.Identifiers),
FilterDefinitions.MatchIfNotEmpty(Documents.Group.Name, filters.Name),
FilterDefinitions.MatchIfNotEmpty(Documents.Group.RealmId, realm?.Id),
FilterDefinitions.MatchBool(Documents.Group.IsDeleted, filters.IsDeleted),
Expand Down
163 changes: 163 additions & 0 deletions Tests/Integration/Endpoints/UserEndpointTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,169 @@ public async Task WhenGetUserPermissions_ShouldReturnAssignedPermissions()
}
}

[Fact(DisplayName = "[e2e] - when GET /users/{id}/permissions should return both direct and inherited group permissions")]
public async Task WhenGetUserPermissions_ShouldReturnDirectAndInheritedPermissions()
{
/* arrange: authenticate user and get access token */
var httpClient = factory.HttpClient.WithRealmHeader("master");
var credentials = new AuthenticationCredentials
{
Username = "federation.testing.user",
Password = "federation.testing.password"
};

var authenticationResponse = await httpClient.PostAsJsonAsync("api/v1/identity/authenticate", credentials);
var authenticationResult = await authenticationResponse.Content.ReadFromJsonAsync<AuthenticationResult>();

Assert.NotNull(authenticationResult);
Assert.NotEmpty(authenticationResult.AccessToken);

httpClient.WithAuthorization(authenticationResult.AccessToken);

/* arrange: create a new user */
var enrollmentCredentials = new IdentityEnrollmentCredentials
{
Username = $"user.inherited.permissions.{Guid.NewGuid()}@email.com",
Password = "TestPassword123!"
};

var enrollmentResponse = await httpClient.PostAsJsonAsync("api/v1/identity", enrollmentCredentials);
var user = await enrollmentResponse.Content.ReadFromJsonAsync<UserDetailsScheme>();

Assert.NotNull(user);
Assert.Equal(HttpStatusCode.Created, enrollmentResponse.StatusCode);

/* arrange: create a direct permission */
var directPermissionPayload = _fixture.Build<PermissionCreationScheme>()
.With(permission => permission.Name, $"test.permission.direct.{Guid.NewGuid()}")
.Create();

var directPermissionResponse = await httpClient.PostAsJsonAsync("api/v1/permissions", directPermissionPayload);
var directPermission = await directPermissionResponse.Content.ReadFromJsonAsync<PermissionDetailsScheme>();

Assert.NotNull(directPermission);
Assert.Equal(HttpStatusCode.Created, directPermissionResponse.StatusCode);

/* arrange: assign direct permission to user */
var assignDirectPermissionPayload = new AssignUserPermissionScheme
{
PermissionName = directPermission.Name
};

var assignDirectPermissionResponse = await httpClient.PostAsJsonAsync($"api/v1/users/{user.Id}/permissions", assignDirectPermissionPayload);
Assert.Equal(HttpStatusCode.NoContent, assignDirectPermissionResponse.StatusCode);

/* arrange: create group and inherited permission */
var groupPayload = _fixture.Build<GroupCreationScheme>()
.With(group => group.Name, $"test-group-{Guid.NewGuid()}")
.Create();

var groupResponse = await httpClient.PostAsJsonAsync("api/v1/groups", groupPayload);
var group = await groupResponse.Content.ReadFromJsonAsync<GroupDetailsScheme>();

Assert.NotNull(group);
Assert.Equal(HttpStatusCode.Created, groupResponse.StatusCode);

var inheritedPermissionPayload = _fixture.Build<PermissionCreationScheme>()
.With(permission => permission.Name, $"test.permission.inherited.{Guid.NewGuid()}")
.Create();

var inheritedPermissionResponse = await httpClient.PostAsJsonAsync("api/v1/permissions", inheritedPermissionPayload);
var inheritedPermission = await inheritedPermissionResponse.Content.ReadFromJsonAsync<PermissionDetailsScheme>();

Assert.NotNull(inheritedPermission);
Assert.Equal(HttpStatusCode.Created, inheritedPermissionResponse.StatusCode);

var assignGroupPermissionPayload = new AssignGroupPermissionScheme
{
PermissionName = inheritedPermission.Name
};

var assignGroupPermissionResponse = await httpClient.PostAsJsonAsync($"api/v1/groups/{group.Id}/permissions", assignGroupPermissionPayload);
Assert.Equal(HttpStatusCode.OK, assignGroupPermissionResponse.StatusCode);

/* arrange: assign user to group */
var assignUserToGroupPayload = new AssignUserToGroupScheme
{
GroupId = group.Id
};

var assignUserToGroupResponse = await httpClient.PostAsJsonAsync($"api/v1/users/{user.Id}/groups", assignUserToGroupPayload);
Assert.Equal(HttpStatusCode.NoContent, assignUserToGroupResponse.StatusCode);

/* act: request user permissions */
var response = await httpClient.GetAsync($"api/v1/users/{user.Id}/permissions");
var permissions = await response.Content.ReadFromJsonAsync<IReadOnlyCollection<PermissionDetailsScheme>>();

/* assert: should return both direct and inherited permissions */
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.NotNull(permissions);

Assert.Contains(permissions, permission => permission.Name == directPermission.Name);
Assert.Contains(permissions, permission => permission.Name == inheritedPermission.Name);
}

[Fact(DisplayName = "[e2e] - when GET /users/{id}/permissions and user has no groups should still return direct permissions")]
public async Task WhenGetUserPermissionsWithUserWithoutGroups_ShouldReturnDirectPermissions()
{
/* arrange: authenticate user and get access token */
var httpClient = factory.HttpClient.WithRealmHeader("master");
var credentials = new AuthenticationCredentials
{
Username = "federation.testing.user",
Password = "federation.testing.password"
};

var authenticationResponse = await httpClient.PostAsJsonAsync("api/v1/identity/authenticate", credentials);
var authenticationResult = await authenticationResponse.Content.ReadFromJsonAsync<AuthenticationResult>();

Assert.NotNull(authenticationResult);
Assert.NotEmpty(authenticationResult.AccessToken);

httpClient.WithAuthorization(authenticationResult.AccessToken);

/* arrange: create a new user without assigning any groups */
var enrollmentCredentials = new IdentityEnrollmentCredentials
{
Username = $"user.no.groups.permissions.{Guid.NewGuid()}@email.com",
Password = "TestPassword123!"
};

var enrollmentResponse = await httpClient.PostAsJsonAsync("api/v1/identity", enrollmentCredentials);
var user = await enrollmentResponse.Content.ReadFromJsonAsync<UserDetailsScheme>();

Assert.NotNull(user);
Assert.Equal(HttpStatusCode.Created, enrollmentResponse.StatusCode);

/* arrange: create and assign a direct permission */
var permissionPayload = _fixture.Build<PermissionCreationScheme>()
.With(permission => permission.Name, $"test.permission.direct.only.{Guid.NewGuid()}")
.Create();

var permissionResponse = await httpClient.PostAsJsonAsync("api/v1/permissions", permissionPayload);
var permission = await permissionResponse.Content.ReadFromJsonAsync<PermissionDetailsScheme>();

Assert.NotNull(permission);
Assert.Equal(HttpStatusCode.Created, permissionResponse.StatusCode);

var assignPermissionPayload = new AssignUserPermissionScheme
{
PermissionName = permission.Name
};

var assignPermissionResponse = await httpClient.PostAsJsonAsync($"api/v1/users/{user.Id}/permissions", assignPermissionPayload);
Assert.Equal(HttpStatusCode.NoContent, assignPermissionResponse.StatusCode);

/* act: request user permissions */
var response = await httpClient.GetAsync($"api/v1/users/{user.Id}/permissions");
var permissions = await response.Content.ReadFromJsonAsync<IReadOnlyCollection<PermissionDetailsScheme>>();

/* assert: endpoint should work and return direct permissions even without group membership */
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.NotNull(permissions);
Assert.Contains(permissions, assigned => assigned.Name == permission.Name);
}

[Fact(DisplayName = "[e2e] - when GET /users/{id}/permissions with non-existent user should return 404 #ERROR-E6B32")]
public async Task WhenGetUserPermissionsWithNonExistentUser_ShouldReturnNotFound()
{
Expand Down