-
-
Notifications
You must be signed in to change notification settings - Fork 32
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
041949a
commit 419a7c5
Showing
8 changed files
with
234 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,16 +1,19 @@ | ||
namespace GenHTTP.Api.Content.Authentication; | ||
|
||
/// <summary> | ||
/// Information about a user that is associated with | ||
/// the currently handled request. | ||
/// Information about a user that is associated with the currently handled request. | ||
/// </summary> | ||
public interface IUser | ||
{ | ||
|
||
/// <summary> | ||
/// The name of the user as it should be shown on | ||
/// the UI or in log files. | ||
/// The name of the user as it should be shown on the UI or in log files. | ||
/// </summary> | ||
string DisplayName { get; } | ||
|
||
/// <summary> | ||
/// The roles of this user. | ||
/// </summary> | ||
string[]? Roles => null; | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
using GenHTTP.Modules.Authentication.Roles; | ||
using GenHTTP.Modules.Reflection; | ||
|
||
namespace GenHTTP.Modules.Authentication; | ||
|
||
/// <summary> | ||
/// When annotated on a service method, requests will only be allowed | ||
/// if the authenticated user has the specified roles. | ||
/// </summary> | ||
/// <param name="roles"></param> | ||
[AttributeUsage(AttributeTargets.Method)] | ||
public class RequireRoleAttribute(params string[] roles) : InterceptWithAttribute<RoleInterceptor> | ||
{ | ||
|
||
/// <summary> | ||
/// The roles which need to be present in order to let | ||
/// the request pass. | ||
/// </summary> | ||
public string[] Roles { get; } = roles; | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
using GenHTTP.Api.Content; | ||
using GenHTTP.Api.Content.Authentication; | ||
using GenHTTP.Api.Protocol; | ||
|
||
using GenHTTP.Modules.Reflection; | ||
using GenHTTP.Modules.Reflection.Operations; | ||
|
||
namespace GenHTTP.Modules.Authentication.Roles; | ||
|
||
public class RoleInterceptor : IOperationInterceptor | ||
{ | ||
private string[]? _Roles; | ||
|
||
public void Configure(object attribute) | ||
{ | ||
if (attribute is RequireRoleAttribute roleAttribute) | ||
{ | ||
_Roles = roleAttribute.Roles; | ||
} | ||
} | ||
|
||
public ValueTask<InterceptionResult?> InterceptAsync(IRequest request, Operation operation, IReadOnlyDictionary<string, object?> arguments) | ||
{ | ||
if (_Roles?.Length > 0) | ||
{ | ||
var user = request.GetUser<IUser>(); | ||
|
||
if (user == null) | ||
{ | ||
throw new ProviderException(ResponseStatus.Unauthorized, "Credentials are required for this endpoint"); | ||
} | ||
|
||
var userRoles = user.Roles; | ||
|
||
var missing = new List<string>(_Roles.Length); | ||
|
||
if (userRoles != null) | ||
{ | ||
foreach (var role in _Roles) | ||
{ | ||
if (!userRoles.Contains(role)) | ||
{ | ||
missing.Add(role); | ||
} | ||
} | ||
} | ||
else | ||
{ | ||
missing.AddRange(_Roles); | ||
} | ||
|
||
if (missing.Count > 0) | ||
{ | ||
throw new ProviderException(ResponseStatus.Forbidden, $"User lacks the following roles to access this endpoint: {string.Join(", ", missing)}"); | ||
} | ||
} | ||
|
||
return default; | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
using System.Net; | ||
|
||
using GenHTTP.Api.Content; | ||
using GenHTTP.Api.Content.Authentication; | ||
using GenHTTP.Api.Protocol; | ||
|
||
using GenHTTP.Modules.Authentication; | ||
using GenHTTP.Modules.Functional; | ||
|
||
using Microsoft.VisualStudio.TestTools.UnitTesting; | ||
|
||
namespace GenHTTP.Testing.Acceptance.Modules.Authentication; | ||
|
||
[TestClass] | ||
public class RoleTests | ||
{ | ||
|
||
#region Supporting data structures | ||
|
||
public class UserSettingConcernBuilder(IUser? user) : IConcernBuilder | ||
{ | ||
|
||
public IConcern Build(IHandler content) => new UserSettingConcern(content, user); | ||
|
||
} | ||
|
||
public class UserSettingConcern : IConcern | ||
{ | ||
|
||
public IHandler Content { get; } | ||
|
||
public IUser? User { get; } | ||
|
||
public UserSettingConcern(IHandler content, IUser? user) | ||
{ | ||
Content = content; | ||
User = user; | ||
} | ||
|
||
public ValueTask PrepareAsync() => Content.PrepareAsync(); | ||
|
||
public ValueTask<IResponse?> HandleAsync(IRequest request) | ||
{ | ||
if (User != null) | ||
{ | ||
request.SetUser(User); | ||
} | ||
|
||
return Content.HandleAsync(request); | ||
} | ||
|
||
} | ||
|
||
public class RoleUser(string[]? roles) : IUser | ||
{ | ||
|
||
public string DisplayName => "Role User"; | ||
|
||
public string[]? Roles => roles; | ||
|
||
} | ||
|
||
#endregion | ||
|
||
#region Tests | ||
|
||
[TestMethod] | ||
[MultiEngineTest] | ||
public async Task TestNoUser(TestEngine engine) | ||
{ | ||
using var response = await RunAsync(null, engine); | ||
|
||
await response.AssertStatusAsync(HttpStatusCode.Unauthorized); | ||
} | ||
|
||
[TestMethod] | ||
[MultiEngineTest] | ||
public async Task TestNoRoles(TestEngine engine) | ||
{ | ||
using var response = await RunAsync(new RoleUser(null), engine); | ||
|
||
await response.AssertStatusAsync(HttpStatusCode.Forbidden); | ||
} | ||
|
||
[TestMethod] | ||
[MultiEngineTest] | ||
public async Task TestInsufficientRoles(TestEngine engine) | ||
{ | ||
using var response = await RunAsync(new RoleUser(["ADMIN"]), engine); | ||
|
||
await response.AssertStatusAsync(HttpStatusCode.Forbidden); | ||
|
||
AssertX.Contains("SUPER_ADMIN", await response.GetContentAsync()); | ||
} | ||
|
||
[TestMethod] | ||
[MultiEngineTest] | ||
public async Task TestSufficientRoles(TestEngine engine) | ||
{ | ||
using var response = await RunAsync(new RoleUser(["ADMIN", "SUPER_ADMIN"]), engine); | ||
|
||
await response.AssertStatusAsync(HttpStatusCode.OK); | ||
} | ||
|
||
#endregion | ||
|
||
#region Helpers | ||
|
||
private static async Task<HttpResponseMessage> RunAsync(IUser? user, TestEngine engine) | ||
{ | ||
var app = Inline.Create() | ||
.Get([RequireRole("ADMIN", "SUPER_ADMIN")]() => 42) | ||
.Add(new UserSettingConcernBuilder(user)); | ||
|
||
await using var host = await TestHost.RunAsync(app, engine: engine); | ||
|
||
return await host.GetResponseAsync(); | ||
} | ||
|
||
#endregion | ||
|
||
} |