💥 Simplified permission node system and data structure
This commit is contained in:
@@ -1,72 +0,0 @@
|
||||
using DysonNetwork.Shared.Proto;
|
||||
using Grpc.Core;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace DysonNetwork.Shared.Auth
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
|
||||
public class RequiredPermissionAttribute(string area, string key) : Attribute
|
||||
{
|
||||
public string Area { get; set; } = area;
|
||||
public string Key { get; } = key;
|
||||
}
|
||||
|
||||
public class PermissionMiddleware(RequestDelegate next)
|
||||
{
|
||||
public async Task InvokeAsync(HttpContext httpContext, PermissionService.PermissionServiceClient permissionService, ILogger<PermissionMiddleware> logger)
|
||||
{
|
||||
var endpoint = httpContext.GetEndpoint();
|
||||
|
||||
var attr = endpoint?.Metadata
|
||||
.OfType<RequiredPermissionAttribute>()
|
||||
.FirstOrDefault();
|
||||
|
||||
if (attr != null)
|
||||
{
|
||||
if (httpContext.Items["CurrentUser"] is not Account currentUser)
|
||||
{
|
||||
httpContext.Response.StatusCode = StatusCodes.Status403Forbidden;
|
||||
await httpContext.Response.WriteAsync("Unauthorized");
|
||||
return;
|
||||
}
|
||||
|
||||
// Assuming Account proto has a bool field 'is_superuser' which is generated as 'IsSuperuser'
|
||||
if (currentUser.IsSuperuser)
|
||||
{
|
||||
// Bypass the permission check for performance
|
||||
await next(httpContext);
|
||||
return;
|
||||
}
|
||||
|
||||
var actor = $"user:{currentUser.Id}";
|
||||
|
||||
try
|
||||
{
|
||||
var permResp = await permissionService.HasPermissionAsync(new HasPermissionRequest
|
||||
{
|
||||
Actor = actor,
|
||||
Area = attr.Area,
|
||||
Key = attr.Key
|
||||
});
|
||||
|
||||
if (!permResp.HasPermission)
|
||||
{
|
||||
httpContext.Response.StatusCode = StatusCodes.Status403Forbidden;
|
||||
await httpContext.Response.WriteAsync($"Permission {attr.Area}/{attr.Key} was required.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
catch (RpcException ex)
|
||||
{
|
||||
logger.LogError(ex, "gRPC call to PermissionService failed while checking permission {Area}/{Key} for actor {Actor}", attr.Area, attr.Key, actor);
|
||||
httpContext.Response.StatusCode = StatusCodes.Status500InternalServerError;
|
||||
await httpContext.Response.WriteAsync("Error checking permissions.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await next(httpContext);
|
||||
}
|
||||
}
|
||||
}
|
||||
72
DysonNetwork.Shared/Auth/RemotePermissionMiddleware.cs
Normal file
72
DysonNetwork.Shared/Auth/RemotePermissionMiddleware.cs
Normal file
@@ -0,0 +1,72 @@
|
||||
using DysonNetwork.Shared.Proto;
|
||||
using Grpc.Core;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace DysonNetwork.Shared.Auth;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
|
||||
public class AskPermissionAttribute(string key, PermissionNodeActorType type = PermissionNodeActorType.Account)
|
||||
: Attribute
|
||||
{
|
||||
public string Key { get; } = key;
|
||||
public PermissionNodeActorType Type { get; } = type;
|
||||
}
|
||||
|
||||
public class RemotePermissionMiddleware(RequestDelegate next)
|
||||
{
|
||||
public async Task InvokeAsync(HttpContext httpContext, PermissionService.PermissionServiceClient permissionService,
|
||||
ILogger<RemotePermissionMiddleware> logger)
|
||||
{
|
||||
var endpoint = httpContext.GetEndpoint();
|
||||
|
||||
var attr = endpoint?.Metadata
|
||||
.OfType<AskPermissionAttribute>()
|
||||
.FirstOrDefault();
|
||||
|
||||
if (attr != null)
|
||||
{
|
||||
if (httpContext.Items["CurrentUser"] is not Account currentUser)
|
||||
{
|
||||
httpContext.Response.StatusCode = StatusCodes.Status403Forbidden;
|
||||
await httpContext.Response.WriteAsync("Unauthorized");
|
||||
return;
|
||||
}
|
||||
|
||||
// Superuser will bypass all the permission check
|
||||
if (currentUser.IsSuperuser)
|
||||
{
|
||||
await next(httpContext);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var permResp = await permissionService.HasPermissionAsync(new HasPermissionRequest
|
||||
{
|
||||
Actor = currentUser.Id,
|
||||
Key = attr.Key
|
||||
});
|
||||
|
||||
if (!permResp.HasPermission)
|
||||
{
|
||||
httpContext.Response.StatusCode = StatusCodes.Status403Forbidden;
|
||||
await httpContext.Response.WriteAsync($"Permission {attr.Key} was required.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
catch (RpcException ex)
|
||||
{
|
||||
logger.LogError(ex,
|
||||
"gRPC call to PermissionService failed while checking permission {Key} for actor {Actor}", attr.Key,
|
||||
currentUser.Id
|
||||
);
|
||||
httpContext.Response.StatusCode = StatusCodes.Status500InternalServerError;
|
||||
await httpContext.Response.WriteAsync("Error checking permissions.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await next(httpContext);
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,12 @@ using NodaTime.Serialization.Protobuf;
|
||||
|
||||
namespace DysonNetwork.Shared.Models;
|
||||
|
||||
public enum PermissionNodeActorType
|
||||
{
|
||||
Account,
|
||||
Group
|
||||
}
|
||||
|
||||
/// The permission node model provides the infrastructure of permission control in Dyson Network.
|
||||
/// It based on the ABAC permission model.
|
||||
///
|
||||
@@ -19,12 +25,12 @@ namespace DysonNetwork.Shared.Models;
|
||||
/// And the actor shows who owns the permission, in most cases, the user:<userId>
|
||||
/// and when the permission node has a GroupId, the actor will be set to the group, but it won't work on checking
|
||||
/// expect the member of that permission group inherent the permission from the group.
|
||||
[Index(nameof(Key), nameof(Area), nameof(Actor))]
|
||||
[Index(nameof(Key), nameof(Actor))]
|
||||
public class SnPermissionNode : ModelBase, IDisposable
|
||||
{
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
public PermissionNodeActorType Type { get; set; } = PermissionNodeActorType.Account;
|
||||
[MaxLength(1024)] public string Actor { get; set; } = null!;
|
||||
[MaxLength(1024)] public string Area { get; set; } = null!;
|
||||
[MaxLength(1024)] public string Key { get; set; } = null!;
|
||||
[Column(TypeName = "jsonb")] public JsonDocument Value { get; set; } = null!;
|
||||
public Instant? ExpiredAt { get; set; } = null;
|
||||
@@ -39,7 +45,12 @@ public class SnPermissionNode : ModelBase, IDisposable
|
||||
{
|
||||
Id = Id.ToString(),
|
||||
Actor = Actor,
|
||||
Area = Area,
|
||||
Type = Type switch
|
||||
{
|
||||
PermissionNodeActorType.Account => Proto.PermissionNodeActorType.Account,
|
||||
PermissionNodeActorType.Group => Proto.PermissionNodeActorType.Group,
|
||||
_ => throw new ArgumentOutOfRangeException()
|
||||
},
|
||||
Key = Key,
|
||||
Value = Google.Protobuf.WellKnownTypes.Value.Parser.ParseJson(Value.RootElement.GetRawText()),
|
||||
ExpiredAt = ExpiredAt?.ToTimestamp(),
|
||||
@@ -48,6 +59,16 @@ public class SnPermissionNode : ModelBase, IDisposable
|
||||
};
|
||||
}
|
||||
|
||||
public static PermissionNodeActorType ConvertProtoActorType(Proto.PermissionNodeActorType? val)
|
||||
{
|
||||
return val switch
|
||||
{
|
||||
Proto.PermissionNodeActorType.Account => PermissionNodeActorType.Account,
|
||||
Proto.PermissionNodeActorType.Group => PermissionNodeActorType.Group,
|
||||
_ => PermissionNodeActorType.Account
|
||||
};
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Value.Dispose();
|
||||
|
||||
@@ -12,193 +12,197 @@ import "account.proto";
|
||||
|
||||
// Represents a user session
|
||||
message AuthSession {
|
||||
string id = 1;
|
||||
optional google.protobuf.Timestamp last_granted_at = 3;
|
||||
optional google.protobuf.Timestamp expired_at = 4;
|
||||
string account_id = 5;
|
||||
Account account = 6;
|
||||
string challenge_id = 7;
|
||||
AuthChallenge challenge = 8;
|
||||
google.protobuf.StringValue app_id = 9;
|
||||
optional string client_id = 10;
|
||||
optional string parent_session_id = 11;
|
||||
AuthClient client = 12;
|
||||
string id = 1;
|
||||
optional google.protobuf.Timestamp last_granted_at = 3;
|
||||
optional google.protobuf.Timestamp expired_at = 4;
|
||||
string account_id = 5;
|
||||
Account account = 6;
|
||||
string challenge_id = 7;
|
||||
AuthChallenge challenge = 8;
|
||||
google.protobuf.StringValue app_id = 9;
|
||||
optional string client_id = 10;
|
||||
optional string parent_session_id = 11;
|
||||
AuthClient client = 12;
|
||||
}
|
||||
|
||||
// Represents an authentication challenge
|
||||
message AuthChallenge {
|
||||
string id = 1;
|
||||
google.protobuf.Timestamp expired_at = 2;
|
||||
int32 step_remain = 3;
|
||||
int32 step_total = 4;
|
||||
int32 failed_attempts = 5;
|
||||
ChallengeType type = 7;
|
||||
repeated string blacklist_factors = 8;
|
||||
repeated string audiences = 9;
|
||||
repeated string scopes = 10;
|
||||
google.protobuf.StringValue ip_address = 11;
|
||||
google.protobuf.StringValue user_agent = 12;
|
||||
google.protobuf.StringValue device_id = 13;
|
||||
google.protobuf.StringValue nonce = 14;
|
||||
// Point location is omitted as there is no direct proto equivalent.
|
||||
string account_id = 15;
|
||||
google.protobuf.StringValue device_name = 16;
|
||||
ClientPlatform platform = 17;
|
||||
string id = 1;
|
||||
google.protobuf.Timestamp expired_at = 2;
|
||||
int32 step_remain = 3;
|
||||
int32 step_total = 4;
|
||||
int32 failed_attempts = 5;
|
||||
ChallengeType type = 7;
|
||||
repeated string blacklist_factors = 8;
|
||||
repeated string audiences = 9;
|
||||
repeated string scopes = 10;
|
||||
google.protobuf.StringValue ip_address = 11;
|
||||
google.protobuf.StringValue user_agent = 12;
|
||||
google.protobuf.StringValue device_id = 13;
|
||||
google.protobuf.StringValue nonce = 14;
|
||||
// Point location is omitted as there is no direct proto equivalent.
|
||||
string account_id = 15;
|
||||
google.protobuf.StringValue device_name = 16;
|
||||
ClientPlatform platform = 17;
|
||||
}
|
||||
|
||||
message AuthClient {
|
||||
string id = 1;
|
||||
ClientPlatform platform = 2;
|
||||
google.protobuf.StringValue device_name = 3;
|
||||
google.protobuf.StringValue device_label = 4;
|
||||
string device_id = 5;
|
||||
string account_id = 6;
|
||||
string id = 1;
|
||||
ClientPlatform platform = 2;
|
||||
google.protobuf.StringValue device_name = 3;
|
||||
google.protobuf.StringValue device_label = 4;
|
||||
string device_id = 5;
|
||||
string account_id = 6;
|
||||
}
|
||||
|
||||
// Enum for challenge types
|
||||
enum ChallengeType {
|
||||
CHALLENGE_TYPE_UNSPECIFIED = 0;
|
||||
LOGIN = 1;
|
||||
OAUTH = 2;
|
||||
OIDC = 3;
|
||||
CHALLENGE_TYPE_UNSPECIFIED = 0;
|
||||
LOGIN = 1;
|
||||
OAUTH = 2;
|
||||
OIDC = 3;
|
||||
}
|
||||
|
||||
// Enum for client platforms
|
||||
enum ClientPlatform {
|
||||
CLIENT_PLATFORM_UNSPECIFIED = 0;
|
||||
UNIDENTIFIED = 1;
|
||||
WEB = 2;
|
||||
IOS = 3;
|
||||
ANDROID = 4;
|
||||
MACOS = 5;
|
||||
WINDOWS = 6;
|
||||
LINUX = 7;
|
||||
CLIENT_PLATFORM_UNSPECIFIED = 0;
|
||||
UNIDENTIFIED = 1;
|
||||
WEB = 2;
|
||||
IOS = 3;
|
||||
ANDROID = 4;
|
||||
MACOS = 5;
|
||||
WINDOWS = 6;
|
||||
LINUX = 7;
|
||||
}
|
||||
|
||||
service AuthService {
|
||||
rpc Authenticate(AuthenticateRequest) returns (AuthenticateResponse) {}
|
||||
rpc Authenticate(AuthenticateRequest) returns (AuthenticateResponse) {}
|
||||
|
||||
rpc ValidatePin(ValidatePinRequest) returns (ValidateResponse) {}
|
||||
rpc ValidateCaptcha(ValidateCaptchaRequest) returns (ValidateResponse) {}
|
||||
rpc ValidatePin(ValidatePinRequest) returns (ValidateResponse) {}
|
||||
rpc ValidateCaptcha(ValidateCaptchaRequest) returns (ValidateResponse) {}
|
||||
}
|
||||
|
||||
message AuthenticateRequest {
|
||||
string token = 1;
|
||||
optional google.protobuf.StringValue ip_address = 2;
|
||||
string token = 1;
|
||||
optional google.protobuf.StringValue ip_address = 2;
|
||||
}
|
||||
|
||||
message AuthenticateResponse {
|
||||
bool valid = 1;
|
||||
optional string message = 2;
|
||||
optional AuthSession session = 3;
|
||||
bool valid = 1;
|
||||
optional string message = 2;
|
||||
optional AuthSession session = 3;
|
||||
}
|
||||
|
||||
message ValidatePinRequest {
|
||||
string account_id = 1;
|
||||
string pin = 2;
|
||||
string account_id = 1;
|
||||
string pin = 2;
|
||||
}
|
||||
|
||||
message ValidateCaptchaRequest {
|
||||
string token = 1;
|
||||
string token = 1;
|
||||
}
|
||||
|
||||
message ValidateResponse {
|
||||
bool valid = 1;
|
||||
bool valid = 1;
|
||||
}
|
||||
|
||||
enum PermissionNodeActorType {
|
||||
ACCOUNT = 0;
|
||||
GROUP = 1;
|
||||
}
|
||||
|
||||
// Permission related messages and services
|
||||
message PermissionNode {
|
||||
string id = 1;
|
||||
string actor = 2;
|
||||
string area = 3;
|
||||
string key = 4;
|
||||
google.protobuf.Value value = 5; // Using Value to represent generic type
|
||||
google.protobuf.Timestamp expired_at = 6;
|
||||
google.protobuf.Timestamp affected_at = 7;
|
||||
string group_id = 8; // Optional group ID
|
||||
string id = 1;
|
||||
string actor = 2;
|
||||
PermissionNodeActorType type = 3;
|
||||
string key = 4;
|
||||
google.protobuf.Value value = 5; // Using Value to represent generic type
|
||||
google.protobuf.Timestamp expired_at = 6;
|
||||
google.protobuf.Timestamp affected_at = 7;
|
||||
string group_id = 8; // Optional group ID
|
||||
}
|
||||
|
||||
message PermissionGroup {
|
||||
string id = 1;
|
||||
string name = 2;
|
||||
google.protobuf.Timestamp created_at = 3;
|
||||
string id = 1;
|
||||
string name = 2;
|
||||
google.protobuf.Timestamp created_at = 3;
|
||||
}
|
||||
|
||||
message HasPermissionRequest {
|
||||
string actor = 1;
|
||||
string area = 2;
|
||||
string key = 3;
|
||||
string actor = 1;
|
||||
string key = 2;
|
||||
optional PermissionNodeActorType type = 3;
|
||||
}
|
||||
|
||||
message HasPermissionResponse {
|
||||
bool has_permission = 1;
|
||||
bool has_permission = 1;
|
||||
}
|
||||
|
||||
message GetPermissionRequest {
|
||||
string actor = 1;
|
||||
string area = 2;
|
||||
string key = 3;
|
||||
string actor = 1;
|
||||
optional PermissionNodeActorType type = 2;
|
||||
string key = 3;
|
||||
}
|
||||
|
||||
message GetPermissionResponse {
|
||||
google.protobuf.Value value = 1; // Using Value to represent generic type
|
||||
google.protobuf.Value value = 1; // Using Value to represent generic type
|
||||
}
|
||||
|
||||
message AddPermissionNodeRequest {
|
||||
string actor = 1;
|
||||
string area = 2;
|
||||
string key = 3;
|
||||
google.protobuf.Value value = 4;
|
||||
google.protobuf.Timestamp expired_at = 5;
|
||||
google.protobuf.Timestamp affected_at = 6;
|
||||
string actor = 1;
|
||||
optional PermissionNodeActorType type = 2;
|
||||
string key = 3;
|
||||
google.protobuf.Value value = 4;
|
||||
google.protobuf.Timestamp expired_at = 5;
|
||||
google.protobuf.Timestamp affected_at = 6;
|
||||
}
|
||||
|
||||
message AddPermissionNodeResponse {
|
||||
PermissionNode node = 1;
|
||||
PermissionNode node = 1;
|
||||
}
|
||||
|
||||
message AddPermissionNodeToGroupRequest {
|
||||
PermissionGroup group = 1;
|
||||
string actor = 2;
|
||||
string area = 3;
|
||||
string key = 4;
|
||||
google.protobuf.Value value = 5;
|
||||
google.protobuf.Timestamp expired_at = 6;
|
||||
google.protobuf.Timestamp affected_at = 7;
|
||||
PermissionGroup group = 1;
|
||||
string actor = 2;
|
||||
optional PermissionNodeActorType type = 3;
|
||||
string key = 4;
|
||||
google.protobuf.Value value = 5;
|
||||
google.protobuf.Timestamp expired_at = 6;
|
||||
google.protobuf.Timestamp affected_at = 7;
|
||||
}
|
||||
|
||||
message AddPermissionNodeToGroupResponse {
|
||||
PermissionNode node = 1;
|
||||
PermissionNode node = 1;
|
||||
}
|
||||
|
||||
message RemovePermissionNodeRequest {
|
||||
string actor = 1;
|
||||
string area = 2;
|
||||
string key = 3;
|
||||
string actor = 1;
|
||||
optional PermissionNodeActorType type = 2;
|
||||
string key = 3;
|
||||
}
|
||||
|
||||
message RemovePermissionNodeResponse {
|
||||
bool success = 1;
|
||||
bool success = 1;
|
||||
}
|
||||
|
||||
message RemovePermissionNodeFromGroupRequest {
|
||||
PermissionGroup group = 1;
|
||||
string actor = 2;
|
||||
string area = 3;
|
||||
string key = 4;
|
||||
PermissionGroup group = 1;
|
||||
string actor = 2;
|
||||
string key = 4;
|
||||
}
|
||||
|
||||
message RemovePermissionNodeFromGroupResponse {
|
||||
bool success = 1;
|
||||
bool success = 1;
|
||||
}
|
||||
|
||||
service PermissionService {
|
||||
rpc HasPermission(HasPermissionRequest) returns (HasPermissionResponse) {}
|
||||
rpc GetPermission(GetPermissionRequest) returns (GetPermissionResponse) {}
|
||||
rpc AddPermissionNode(AddPermissionNodeRequest) returns (AddPermissionNodeResponse) {}
|
||||
rpc AddPermissionNodeToGroup(AddPermissionNodeToGroupRequest) returns (AddPermissionNodeToGroupResponse) {}
|
||||
rpc RemovePermissionNode(RemovePermissionNodeRequest) returns (RemovePermissionNodeResponse) {}
|
||||
rpc RemovePermissionNodeFromGroup(RemovePermissionNodeFromGroupRequest)
|
||||
returns (RemovePermissionNodeFromGroupResponse) {}
|
||||
rpc HasPermission(HasPermissionRequest) returns (HasPermissionResponse) {}
|
||||
rpc GetPermission(GetPermissionRequest) returns (GetPermissionResponse) {}
|
||||
rpc AddPermissionNode(AddPermissionNodeRequest) returns (AddPermissionNodeResponse) {}
|
||||
rpc AddPermissionNodeToGroup(AddPermissionNodeToGroupRequest) returns (AddPermissionNodeToGroupResponse) {}
|
||||
rpc RemovePermissionNode(RemovePermissionNodeRequest) returns (RemovePermissionNodeResponse) {}
|
||||
rpc RemovePermissionNodeFromGroup(RemovePermissionNodeFromGroupRequest)
|
||||
returns (RemovePermissionNodeFromGroupResponse) {}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user