Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
14 commits
Select commit Hold shift + click to select a range
7ea4d6a
feature(#6): this commit introduces the injection of the permissions …
https-richardy Mar 13, 2026
3100f95
feature(#6): this commit introduces new 409 responses when attempting…
https-richardy Mar 13, 2026
a4dd64e
feature(#6): this commit introduces the new 409 response into the con…
https-richardy Mar 13, 2026
734269d
feature(#6): this commit introduces new tests to the test suite to en…
https-richardy Mar 13, 2026
a00b767
feature(#6): this commit simplifies the creation of reserved permissi…
https-richardy Mar 13, 2026
a4b387e
feature(#6): this commit defines the contract for the permission name…
https-richardy Mar 13, 2026
52f26db
feature(#6): this commit defines the new error representing the error…
https-richardy Mar 13, 2026
2b7749b
feature(#6): this commit introduces security policy restrictions/chec…
https-richardy Mar 13, 2026
94f58a6
feature(#6): this commit renames the static constant class
https-richardy Mar 13, 2026
84c8a76
feature(#6): this commit introduces the implementation of the permiss…
https-richardy Mar 13, 2026
32aa29d
feature(#6): this commit removes unnecessary global usage directive
https-richardy Mar 13, 2026
a087c3f
feature(#6): this commit renames the file and adds hash set for reser…
https-richardy Mar 13, 2026
e60544b
feature(#6): this commit introduces global usage reference to the “co…
https-richardy Mar 13, 2026
191a89d
feature(#6): this commit fixes non-deterministic behaviors when runni…
https-richardy Mar 13, 2026
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,11 +1,18 @@
namespace HttpsRichardy.Federation.Application.Handlers.Permission;

public sealed class PermissionCreationHandler(IPermissionCollection collection, IRealmProvider realmProvider) :
public sealed class PermissionCreationHandler(IPermissionCollection collection, IPermissionNamespacePolicy policy, IRealmProvider realmProvider) :
IDispatchHandler<PermissionCreationScheme, Result<PermissionDetailsScheme>>
{
public async Task<Result<PermissionDetailsScheme>> HandleAsync(PermissionCreationScheme parameters, CancellationToken cancellation = default)
{
var realm = realmProvider.GetCurrentRealm();
var result = await policy.EnsurePermissionIsAllowedAsync(realm, new() { Name = parameters.Name }, cancellation);

if (result.IsFailure)
{
return Result<PermissionDetailsScheme>.Failure(PermissionErrors.PermissionNameIsReserved);
}

var filters = PermissionFilters.WithSpecifications()
.WithName(parameters.Name)
.Build();
Expand All @@ -23,4 +30,4 @@ public async Task<Result<PermissionDetailsScheme>> HandleAsync(PermissionCreatio

return Result<PermissionDetailsScheme>.Success(PermissionMapper.AsResponse(createdPermission));
}
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
namespace HttpsRichardy.Federation.Application.Handlers.Permission;

public sealed class PermissionUpdateHandler(IPermissionCollection collection, IRealmProvider realmProvider) :
public sealed class PermissionUpdateHandler(IPermissionCollection collection, IPermissionNamespacePolicy policy, IRealmProvider realmProvider) :
IDispatchHandler<PermissionUpdateScheme, Result<PermissionDetailsScheme>>
{
public async Task<Result<PermissionDetailsScheme>> HandleAsync(PermissionUpdateScheme parameters, CancellationToken cancellation = default)
Expand All @@ -18,10 +18,16 @@ public async Task<Result<PermissionDetailsScheme>> HandleAsync(PermissionUpdateS
return Result<PermissionDetailsScheme>.Failure(PermissionErrors.PermissionDoesNotExist);
}

var result = await policy.EnsurePermissionIsAllowedAsync(realm, new() { Name = parameters.Name }, cancellation);
if (result.IsFailure)
{
return Result<PermissionDetailsScheme>.Failure(PermissionErrors.PermissionNameIsReserved);
}

permission = PermissionMapper.AsPermission(parameters, permission, realm);

var updatedPermission = await collection.UpdateAsync(permission, cancellation: cancellation);

return Result<PermissionDetailsScheme>.Success(PermissionMapper.AsResponse(updatedPermission));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public async Task<Result<RealmDetailsScheme>> HandleAsync(
var defaultRealm = matchingRealms.FirstOrDefault()!;

realm.Permissions = defaultRealm.Permissions
.Where(permission => DefaultRealmPermissions.InitialPermissions.Contains(permission.Name))
.Where(permission => RealmPermissions.InitialPermissions.Contains(permission.Name))
.ToList();

await collection.InsertAsync(realm, cancellation: cancellation);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
namespace HttpsRichardy.Federation.Application.Policies;

public sealed class PermissionNamespacePolicy : IPermissionNamespacePolicy
{
public async Task<Result> EnsurePermissionIsAllowedAsync(
Realm realm, Permission permission, CancellationToken cancellation = default)
{
var isReserved = RealmPermissions.SystemPermissions
.Contains(permission.Name);

return isReserved
? Result.Failure(PermissionErrors.PermissionNameIsReserved)
: Result.Success();
}
}
1 change: 0 additions & 1 deletion Source/HttpsRichardy.Federation.Application/Usings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@
global using HttpsRichardy.Federation.Application.Providers;
global using HttpsRichardy.Federation.Application.Mappers;
global using HttpsRichardy.Federation.Application.Utilities;
global using HttpsRichardy.Federation.Application.Handlers.Authorization;

global using FluentValidation;
global using HttpsRichardy.Dispatcher.Contracts;

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
namespace HttpsRichardy.Federation.Common.Constants;

public static class RealmPermissions
{
public static readonly HashSet<string> InitialPermissions =
[
Permissions.CreateGroup,
Permissions.DeleteGroup,
Permissions.ViewGroups,
Permissions.EditGroup,

Permissions.DeleteUser,
Permissions.EditUser,
Permissions.ViewUsers,

Permissions.CreatePermission,
Permissions.AssignPermissions,
Permissions.RevokePermissions,
Permissions.ViewPermissions,
Permissions.EditPermission,
Permissions.DeletePermission,

Permissions.CreateScope,
Permissions.EditScope,
Permissions.DeleteGroup,
Permissions.ViewScopes
];

public static readonly HashSet<string> SystemPermissions =
[
Permissions.CreateGroup,
Permissions.DeleteGroup,
Permissions.EditGroup,
Permissions.ViewGroups,

Permissions.DeleteUser,
Permissions.EditUser,
Permissions.ViewUsers,

Permissions.CreatePermission,
Permissions.AssignPermissions,
Permissions.RevokePermissions,
Permissions.ViewPermissions,
Permissions.EditPermission,
Permissions.DeletePermission,

Permissions.CreateRealm,
Permissions.DeleteRealm,
Permissions.EditRealm,
Permissions.ViewRealms,

Permissions.CreateScope,
Permissions.EditScope,
Permissions.DeleteScope,
Permissions.ViewScopes
];
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ public static class PermissionErrors
Description: "The permission with the specified name already exists."
);

public static readonly Error PermissionNameIsReserved = new(
Code: "#ERROR-7B1E2",
Description: "The permission name is reserved by the system."
);

public static readonly Error PermissionDoesNotExist = new(
Code: "#ERROR-93697",
Description: "The specified permission does not exist."
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
namespace HttpsRichardy.Federation.Domain.Policies;

// defines a policy responsible for protecting the system permission namespace
// from unauthorized usage by realms

// certain permissions are reserved by the federation system and represent
// privileged administrative capabilities (e.g. managing realms or federation resources)

// this policy ensures that realms cannot create or manipulate permissions
// whose identifiers belong to the reserved system namespace, preventing
// privilege escalation through permission name collision

public interface IPermissionNamespacePolicy
{
public Task<Result> EnsurePermissionIsAllowedAsync(
Realm realm,
Permission permission,
CancellationToken cancellation = default
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ public static void AddServices(this IServiceCollection services)
services.AddTransient<IAuthenticationService, AuthenticationService>();
services.AddTransient<ISecurityTokenService, JwtSecurityTokenService>();
services.AddTransient<IClientCredentialsGenerator, ClientCredentialsGenerator>();

services.AddTransient<IRedirectUriPolicy, RedirectUriPolicy>();
services.AddTransient<IPermissionNamespacePolicy, PermissionNamespacePolicy>();

services.AddTransient<IAuthorizationFlowHandler, ClientCredentialsGrantHandler>();
services.AddTransient<IAuthorizationFlowHandler, AuthorizationCodeGrantHandler>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ public async Task<IActionResult> CreatePermissionAsync([FromBody] PermissionCrea

{ IsFailure: true } when result.Error == PermissionErrors.PermissionAlreadyExists =>
StatusCode(StatusCodes.Status409Conflict, result.Error),

{ IsFailure: true } when result.Error == PermissionErrors.PermissionNameIsReserved =>
StatusCode(StatusCodes.Status409Conflict, result.Error),
};
}

Expand All @@ -52,6 +55,9 @@ public async Task<IActionResult> UpdatePermissionAsync([FromRoute] string id, [F

{ IsFailure: true } when result.Error == PermissionErrors.PermissionDoesNotExist =>
StatusCode(StatusCodes.Status404NotFound, result.Error),

{ IsFailure: true } when result.Error == PermissionErrors.PermissionNameIsReserved =>
StatusCode(StatusCodes.Status409Conflict, result.Error),
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ public static void CreatePermissionAsync(PermissionCreationScheme request, Cance
[ApiConventionNameMatch(ApiConventionNameMatchBehavior.Prefix)]
[ProducesResponseType(typeof(PermissionDetailsScheme), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(Error), StatusCodes.Status404NotFound)]
[ProducesResponseType(typeof(Error), StatusCodes.Status409Conflict)]
public static void UpdatePermissionAsync(string id, PermissionUpdateScheme request, CancellationToken cancellation) { }

[ApiConventionNameMatch(ApiConventionNameMatchBehavior.Prefix)]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,33 +33,12 @@ public static async Task UseBootstrapperAsync(this IApplicationBuilder builder)
}

defaultRealm.SecretHash = await passwordHasher.HashPasswordAsync(realmCredentials.ClientId + defaultRealm.Name);
defaultRealm.Permissions = [
new() { Id = Identifier.Generate<Permission>(), Name = Permissions.CreateGroup, RealmId = defaultRealm.Id },
new() { Id = Identifier.Generate<Permission>(), Name = Permissions.DeleteGroup, RealmId = defaultRealm.Id },
new() { Id = Identifier.Generate<Permission>(), Name = Permissions.ViewGroups, RealmId = defaultRealm.Id },
new() { Id = Identifier.Generate<Permission>(), Name = Permissions.EditGroup, RealmId = defaultRealm.Id },

new() { Id = Identifier.Generate<Permission>(), Name = Permissions.DeleteUser, RealmId = defaultRealm.Id },
new() { Id = Identifier.Generate<Permission>(), Name = Permissions.EditUser, RealmId = defaultRealm.Id },
new() { Id = Identifier.Generate<Permission>(), Name = Permissions.ViewUsers, RealmId = defaultRealm.Id },

new() { Id = Identifier.Generate<Permission>(), Name = Permissions.CreateRealm, RealmId = defaultRealm.Id },
new() { Id = Identifier.Generate<Permission>(), Name = Permissions.DeleteRealm, RealmId = defaultRealm.Id },
new() { Id = Identifier.Generate<Permission>(), Name = Permissions.EditRealm, RealmId = defaultRealm.Id },
new() { Id = Identifier.Generate<Permission>(), Name = Permissions.ViewRealms, RealmId = defaultRealm.Id },

new() { Id = Identifier.Generate<Permission>(), Name = Permissions.CreatePermission, RealmId = defaultRealm.Id },
new() { Id = Identifier.Generate<Permission>(), Name = Permissions.AssignPermissions, RealmId = defaultRealm.Id },
new() { Id = Identifier.Generate<Permission>(), Name = Permissions.RevokePermissions, RealmId = defaultRealm.Id },
new() { Id = Identifier.Generate<Permission>(), Name = Permissions.ViewPermissions, RealmId = defaultRealm.Id },
new() { Id = Identifier.Generate<Permission>(), Name = Permissions.EditPermission, RealmId = defaultRealm.Id },
new() { Id = Identifier.Generate<Permission>(), Name = Permissions.DeletePermission, RealmId = defaultRealm.Id },

new() { Id = Identifier.Generate<Permission>(), Name = Permissions.CreateScope, RealmId = defaultRealm.Id },
new() { Id = Identifier.Generate<Permission>(), Name = Permissions.EditScope, RealmId = defaultRealm.Id },
new() { Id = Identifier.Generate<Permission>(), Name = Permissions.DeleteScope, RealmId = defaultRealm.Id },
new() { Id = Identifier.Generate<Permission>(), Name = Permissions.ViewScopes, RealmId = defaultRealm.Id },
];
defaultRealm.Permissions = [.. RealmPermissions.SystemPermissions.Select(permissionName => new Permission
{
Id = Identifier.Generate<Permission>(),
Name = permissionName,
RealmId = defaultRealm.Id
})];

var scopes = new List<Scope>
{
Expand Down
Loading
Loading