✨ Casbin permission check
This commit is contained in:
@@ -16,8 +16,8 @@ public class AuthController(AppDatabase db, AccountService accounts, AuthService
|
||||
{
|
||||
[Required] [MaxLength(256)] public string Account { get; set; } = string.Empty;
|
||||
[MaxLength(512)] public string? DeviceId { get; set; }
|
||||
public List<string> Claims { get; set; } = new();
|
||||
public List<string> Audiences { get; set; } = new();
|
||||
public List<string> Scopes { get; set; } = new();
|
||||
}
|
||||
|
||||
[HttpPost("challenge")]
|
||||
@@ -34,8 +34,8 @@ public class AuthController(AppDatabase db, AccountService accounts, AuthService
|
||||
Account = account,
|
||||
ExpiredAt = Instant.FromDateTimeUtc(DateTime.UtcNow.AddHours(1)),
|
||||
StepTotal = 1,
|
||||
Claims = request.Claims,
|
||||
Audiences = request.Audiences,
|
||||
Scopes = request.Scopes,
|
||||
IpAddress = ipAddress,
|
||||
UserAgent = userAgent,
|
||||
DeviceId = request.DeviceId,
|
||||
@@ -46,6 +46,18 @@ public class AuthController(AppDatabase db, AccountService accounts, AuthService
|
||||
return challenge;
|
||||
}
|
||||
|
||||
[HttpGet("challenge/{id}/factors")]
|
||||
public async Task<ActionResult<List<AccountAuthFactor>>> GetChallengeFactors([FromRoute] Guid id)
|
||||
{
|
||||
var challenge = await db.AuthChallenges
|
||||
.Include(e => e.Account)
|
||||
.Include(e => e.Account.AuthFactors)
|
||||
.Where(e => e.Id == id).FirstOrDefaultAsync();
|
||||
return challenge is null
|
||||
? new NotFoundObjectResult("Auth challenge was not found.")
|
||||
: challenge.Account.AuthFactors.ToList();
|
||||
}
|
||||
|
||||
public class PerformChallengeRequest
|
||||
{
|
||||
[Required] public long FactorId { get; set; }
|
||||
@@ -59,10 +71,10 @@ public class AuthController(AppDatabase db, AccountService accounts, AuthService
|
||||
)
|
||||
{
|
||||
var challenge = await db.AuthChallenges.FindAsync(id);
|
||||
if (challenge is null) return new NotFoundResult();
|
||||
if (challenge is null) return new NotFoundObjectResult("Auth challenge was not found.");
|
||||
|
||||
var factor = await db.AccountAuthFactors.FindAsync(request.FactorId);
|
||||
if (factor is null) return new NotFoundResult();
|
||||
if (factor is null) return new NotFoundObjectResult("Auth factor was not found.");
|
||||
|
||||
if (challenge.StepRemain == 0) return challenge;
|
||||
if (challenge.ExpiredAt.HasValue && challenge.ExpiredAt.Value < Instant.FromDateTimeUtc(DateTime.UtcNow))
|
||||
@@ -84,67 +96,72 @@ public class AuthController(AppDatabase db, AccountService accounts, AuthService
|
||||
await db.SaveChangesAsync();
|
||||
return challenge;
|
||||
}
|
||||
|
||||
[HttpPost("challenge/{id}/grant")]
|
||||
public async Task<ActionResult<SignedTokenPair>> GrantChallengeToken([FromRoute] Guid id)
|
||||
{
|
||||
var challenge = await db.AuthChallenges
|
||||
.Include(e => e.Account)
|
||||
.Where(e => e.Id == id)
|
||||
.FirstOrDefaultAsync();
|
||||
if (challenge is null) return new NotFoundResult();
|
||||
if (challenge.StepRemain != 0) return new BadRequestResult();
|
||||
|
||||
var session = await db.AuthSessions
|
||||
.Where(e => e.Challenge == challenge)
|
||||
.FirstOrDefaultAsync();
|
||||
if (session is not null) return new BadRequestResult();
|
||||
|
||||
session = new Session
|
||||
{
|
||||
LastGrantedAt = Instant.FromDateTimeUtc(DateTime.UtcNow),
|
||||
ExpiredAt = Instant.FromDateTimeUtc(DateTime.UtcNow.AddDays(30)),
|
||||
Account = challenge.Account,
|
||||
Challenge = challenge,
|
||||
};
|
||||
|
||||
db.AuthSessions.Add(session);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
return auth.CreateToken(session);
|
||||
}
|
||||
|
||||
public class TokenExchangeRequest
|
||||
{
|
||||
public string GrantType { get; set; } = string.Empty;
|
||||
public string RefreshToken { get; set; } = string.Empty;
|
||||
public string? RefreshToken { get; set; }
|
||||
public string? Code { get; set; }
|
||||
}
|
||||
|
||||
|
||||
[HttpPost("token")]
|
||||
public async Task<ActionResult<SignedTokenPair>> ExchangeToken([FromBody] TokenExchangeRequest request)
|
||||
{
|
||||
Session? session;
|
||||
switch (request.GrantType)
|
||||
{
|
||||
case "authorization_code":
|
||||
var code = Guid.TryParse(request.Code, out var codeId) ? codeId : Guid.Empty;
|
||||
if (code == Guid.Empty)
|
||||
return new BadRequestObjectResult("Invalid or missing authorization code.");
|
||||
var challenge = await db.AuthChallenges
|
||||
.Include(e => e.Account)
|
||||
.Where(e => e.Id == code)
|
||||
.FirstOrDefaultAsync();
|
||||
if (challenge is null)
|
||||
return new NotFoundObjectResult("Authorization code not found or expired.");
|
||||
if (challenge.StepRemain != 0)
|
||||
return new BadRequestObjectResult("Challenge not yet completed.");
|
||||
|
||||
session = await db.AuthSessions
|
||||
.Where(e => e.Challenge == challenge)
|
||||
.FirstOrDefaultAsync();
|
||||
if (session is not null)
|
||||
return new BadRequestObjectResult("Session already exists for this challenge.");
|
||||
|
||||
session = new Session
|
||||
{
|
||||
LastGrantedAt = Instant.FromDateTimeUtc(DateTime.UtcNow),
|
||||
ExpiredAt = Instant.FromDateTimeUtc(DateTime.UtcNow.AddDays(30)),
|
||||
Account = challenge.Account,
|
||||
Challenge = challenge,
|
||||
};
|
||||
|
||||
db.AuthSessions.Add(session);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
return auth.CreateToken(session);
|
||||
case "refresh_token":
|
||||
var handler = new JwtSecurityTokenHandler();
|
||||
var token = handler.ReadJwtToken(request.RefreshToken);
|
||||
var sessionIdClaim = token.Claims.FirstOrDefault(c => c.Type == "session_id")?.Value;
|
||||
|
||||
if (!Guid.TryParse(sessionIdClaim, out var sessionId))
|
||||
return new UnauthorizedResult();
|
||||
return new UnauthorizedObjectResult("Invalid or missing session_id claim in refresh token.");
|
||||
|
||||
session = await db.AuthSessions.FirstOrDefaultAsync(s => s.Id == sessionId);
|
||||
if (session is null)
|
||||
return new NotFoundObjectResult("Session not found or expired.");
|
||||
|
||||
var session = await db.AuthSessions.FirstOrDefaultAsync(s => s.Id == sessionId);
|
||||
if (session is null) return new NotFoundResult();
|
||||
|
||||
session.LastGrantedAt = Instant.FromDateTimeUtc(DateTime.UtcNow);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
return auth.CreateToken(session);
|
||||
default:
|
||||
return new BadRequestResult();
|
||||
return new BadRequestObjectResult("Unsupported grant type.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
[Authorize]
|
||||
[HttpGet("test")]
|
||||
public async Task<ActionResult> Test()
|
||||
|
@@ -1,6 +1,8 @@
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Security.Claims;
|
||||
using System.Security.Cryptography;
|
||||
using Casbin;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using NodaTime;
|
||||
|
||||
@@ -13,8 +15,68 @@ public class SignedTokenPair
|
||||
public Instant ExpiredAt { get; set; }
|
||||
}
|
||||
|
||||
public class AuthService(AppDatabase db, IConfiguration config)
|
||||
public class AuthService(AppDatabase db, IConfiguration config, IEnforcer enforcer)
|
||||
{
|
||||
public async Task<bool> AssignRoleToUserAsync(string user, string role, string domain = "global")
|
||||
{
|
||||
var added = await enforcer.AddGroupingPolicyAsync(user, role, domain);
|
||||
if (added) await enforcer.SavePolicyAsync();
|
||||
return added;
|
||||
}
|
||||
|
||||
public async Task<bool> AddPermissionToUserAsync(string user, string domain, string obj, string act)
|
||||
{
|
||||
var added = await enforcer.AddPolicyAsync(user, domain, obj, act);
|
||||
if (added) await enforcer.SavePolicyAsync();
|
||||
return added;
|
||||
}
|
||||
|
||||
public async Task<bool> RemovePermissionFromUserAsync(string user, string domain, string obj, string act)
|
||||
{
|
||||
var removed = await enforcer.RemovePolicyAsync(user, domain, obj, act);
|
||||
if (removed) await enforcer.SavePolicyAsync();
|
||||
return removed;
|
||||
}
|
||||
|
||||
public async Task<bool> CreateRoleAsync(string role, string domain, IEnumerable<(string obj, string act)> permissions)
|
||||
{
|
||||
bool anyAdded = false;
|
||||
foreach (var (obj, act) in permissions)
|
||||
{
|
||||
var added = await enforcer.AddPolicyAsync(role, domain, obj, act);
|
||||
if (added) anyAdded = true;
|
||||
}
|
||||
|
||||
if (anyAdded) await enforcer.SavePolicyAsync();
|
||||
return anyAdded;
|
||||
}
|
||||
|
||||
public async Task<bool> AddPermissionsToRoleAsync(string role, string domain, IEnumerable<(string obj, string act)> permissions)
|
||||
{
|
||||
bool anyAdded = false;
|
||||
foreach (var (obj, act) in permissions)
|
||||
{
|
||||
var added = await enforcer.AddPolicyAsync(role, domain, obj, act);
|
||||
if (added) anyAdded = true;
|
||||
}
|
||||
|
||||
if (anyAdded) await enforcer.SavePolicyAsync();
|
||||
return anyAdded;
|
||||
}
|
||||
|
||||
public async Task<bool> RemovePermissionsFromRoleAsync(string role, string domain, IEnumerable<(string obj, string act)> permissions)
|
||||
{
|
||||
bool anyRemoved = false;
|
||||
foreach (var (obj, act) in permissions)
|
||||
{
|
||||
var removed = await enforcer.RemovePolicyAsync(role, domain, obj, act);
|
||||
if (removed) anyRemoved = true;
|
||||
}
|
||||
|
||||
if (anyRemoved) await enforcer.SavePolicyAsync();
|
||||
return anyRemoved;
|
||||
}
|
||||
|
||||
public SignedTokenPair CreateToken(Session session)
|
||||
{
|
||||
var privateKeyPem = File.ReadAllText(config["Jwt:PrivateKeyPath"]!);
|
||||
@@ -23,29 +85,29 @@ public class AuthService(AppDatabase db, IConfiguration config)
|
||||
var key = new RsaSecurityKey(rsa);
|
||||
|
||||
var creds = new SigningCredentials(key, SecurityAlgorithms.RsaSha256);
|
||||
var claims = new List<Claim>
|
||||
{
|
||||
new("user_id", session.Account.Id.ToString()),
|
||||
new("session_id", session.Id.ToString())
|
||||
};
|
||||
|
||||
var accessTokenClaims = new JwtSecurityToken(
|
||||
issuer: "solar-network",
|
||||
audience: string.Join(',', session.Challenge.Audiences),
|
||||
claims: new List<Claim>
|
||||
{
|
||||
new("user_id", session.Account.Id.ToString()),
|
||||
new("session_id", session.Id.ToString())
|
||||
},
|
||||
expires: DateTime.Now.AddMinutes(30),
|
||||
signingCredentials: creds
|
||||
);
|
||||
var refreshTokenClaims = new JwtSecurityToken(
|
||||
issuer: "solar-network",
|
||||
audience: string.Join(',', session.Challenge.Audiences),
|
||||
claims: new List<Claim>
|
||||
{
|
||||
new("user_id", session.Account.Id.ToString()),
|
||||
new("session_id", session.Id.ToString())
|
||||
},
|
||||
claims: claims,
|
||||
expires: DateTime.Now.AddDays(30),
|
||||
signingCredentials: creds
|
||||
);
|
||||
|
||||
session.Challenge.Scopes.ForEach(c => claims.Add(new Claim("scope", c)));
|
||||
if(session.Account.IsSuperuser) claims.Add(new Claim("is_superuser", "1"));
|
||||
var accessTokenClaims = new JwtSecurityToken(
|
||||
issuer: "solar-network",
|
||||
audience: string.Join(',', session.Challenge.Audiences),
|
||||
claims: claims,
|
||||
expires: DateTime.Now.AddMinutes(30),
|
||||
signingCredentials: creds
|
||||
);
|
||||
|
||||
var handler = new JwtSecurityTokenHandler();
|
||||
var accessToken = handler.WriteToken(accessTokenClaims);
|
||||
|
34
DysonNetwork.Sphere/Auth/CasbinRequirement.cs
Normal file
34
DysonNetwork.Sphere/Auth/CasbinRequirement.cs
Normal file
@@ -0,0 +1,34 @@
|
||||
using Casbin;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
|
||||
namespace DysonNetwork.Sphere.Auth;
|
||||
|
||||
public class CasbinRequirement(string domain, string obj, string act) : IAuthorizationRequirement
|
||||
{
|
||||
public string Domain { get; } = domain;
|
||||
public string Object { get; } = obj;
|
||||
public string Action { get; } = act;
|
||||
}
|
||||
|
||||
public class CasbinAuthorizationHandler(IEnforcer enforcer)
|
||||
: AuthorizationHandler<CasbinRequirement>
|
||||
{
|
||||
protected override async Task HandleRequirementAsync(
|
||||
AuthorizationHandlerContext context,
|
||||
CasbinRequirement requirement)
|
||||
{
|
||||
var userId = context.User.FindFirst("user_id")?.Value;
|
||||
if (userId == null) return;
|
||||
var isSuperuser = context.User.FindFirst("is_superuser")?.Value == "1";
|
||||
if (isSuperuser) userId = "super:" + userId;
|
||||
|
||||
var allowed = await enforcer.EnforceAsync(
|
||||
userId,
|
||||
requirement.Domain,
|
||||
requirement.Object,
|
||||
requirement.Action
|
||||
);
|
||||
|
||||
if (allowed) context.Succeed(requirement);
|
||||
}
|
||||
}
|
@@ -22,8 +22,8 @@ public class Challenge : BaseModel
|
||||
public int StepRemain { get; set; }
|
||||
public int StepTotal { get; set; }
|
||||
[Column(TypeName = "jsonb")] public List<long> BlacklistFactors { get; set; } = new();
|
||||
[Column(TypeName = "jsonb")] public List<string> Claims { get; set; } = new();
|
||||
[Column(TypeName = "jsonb")] public List<string> Audiences { get; set; } = new();
|
||||
[Column(TypeName = "jsonb")] public List<string> Scopes { get; set; } = new();
|
||||
[MaxLength(128)] public string? IpAddress { get; set; }
|
||||
[MaxLength(512)] public string? UserAgent { get; set; }
|
||||
[MaxLength(256)] public string? DeviceId { get; set; }
|
||||
|
Reference in New Issue
Block a user