💥 Simplified permission node system and data structure

This commit is contained in:
2025-12-02 21:42:26 +08:00
parent fa2f53ff7a
commit 158cc75c5b
32 changed files with 3333 additions and 379 deletions

View File

@@ -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);
}
}
}

View 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);
}
}

View File

@@ -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:&lt;userId&gt;
/// 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();

View File

@@ -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) {}
}