✨ Casbin permission check
This commit is contained in:
		| @@ -9,6 +9,8 @@ public class Account : BaseModel | ||||
|     public long Id { get; set; } | ||||
|     [MaxLength(256)] public string Name { get; set; } = string.Empty; | ||||
|     [MaxLength(256)] public string Nick { get; set; } = string.Empty; | ||||
|     [MaxLength(32)] public string Language { get; set; } = string.Empty; | ||||
|     public bool IsSuperuser { get; set; } = false; | ||||
|  | ||||
|     public ICollection<AccountContact> Contacts { get; set; } = new List<AccountContact>(); | ||||
|  | ||||
|   | ||||
| @@ -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; } | ||||
|   | ||||
							
								
								
									
										14
									
								
								DysonNetwork.Sphere/Casbin.conf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								DysonNetwork.Sphere/Casbin.conf
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | ||||
| [request_definition] | ||||
| r = sub, dom, obj, act | ||||
|  | ||||
| [policy_definition] | ||||
| p = sub, dom, obj, act | ||||
|  | ||||
| [role_definition] | ||||
| g = _, _, _ | ||||
|  | ||||
| [policy_effect] | ||||
| e = some(where (p.eft == allow)) | ||||
|  | ||||
| [matchers] | ||||
| m = r.sub.StartsWith("super:") || (g(r.sub, p.sub, r.dom) && r.dom == p.dom && r.obj == p.obj && r.act == p.act) | ||||
| @@ -10,6 +10,8 @@ | ||||
|  | ||||
|     <ItemGroup> | ||||
|         <PackageReference Include="BCrypt.Net-Next" Version="4.0.3" /> | ||||
|         <PackageReference Include="Casbin.NET" Version="2.12.0" /> | ||||
|         <PackageReference Include="Casbin.NET.Adapter.EFCore" Version="2.5.0" /> | ||||
|         <PackageReference Include="EFCore.NamingConventions" Version="9.0.0" /> | ||||
|         <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.4" /> | ||||
|         <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.2" /> | ||||
|   | ||||
| @@ -14,7 +14,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; | ||||
| namespace DysonNetwork.Sphere.Migrations | ||||
| { | ||||
|     [DbContext(typeof(AppDatabase))] | ||||
|     [Migration("20250409150800_AddAuthSession")] | ||||
|     [Migration("20250410150812_AddAuthSession")] | ||||
|     partial class AddAuthSession | ||||
|     { | ||||
|         /// <inheritdoc /> | ||||
| @@ -177,11 +177,6 @@ namespace DysonNetwork.Sphere.Migrations | ||||
|                         .HasColumnType("jsonb") | ||||
|                         .HasColumnName("blacklist_factors"); | ||||
| 
 | ||||
|                     b.Property<List<string>>("Claims") | ||||
|                         .IsRequired() | ||||
|                         .HasColumnType("jsonb") | ||||
|                         .HasColumnName("claims"); | ||||
| 
 | ||||
|                     b.Property<Instant>("CreatedAt") | ||||
|                         .HasColumnType("timestamp with time zone") | ||||
|                         .HasColumnName("created_at"); | ||||
| @@ -209,6 +204,11 @@ namespace DysonNetwork.Sphere.Migrations | ||||
|                         .HasColumnType("character varying(1024)") | ||||
|                         .HasColumnName("nonce"); | ||||
| 
 | ||||
|                     b.Property<List<string>>("Scopes") | ||||
|                         .IsRequired() | ||||
|                         .HasColumnType("jsonb") | ||||
|                         .HasColumnName("scopes"); | ||||
| 
 | ||||
|                     b.Property<int>("StepRemain") | ||||
|                         .HasColumnType("integer") | ||||
|                         .HasColumnName("step_remain"); | ||||
| @@ -22,8 +22,8 @@ namespace DysonNetwork.Sphere.Migrations | ||||
|                     step_remain = table.Column<int>(type: "integer", nullable: false), | ||||
|                     step_total = table.Column<int>(type: "integer", nullable: false), | ||||
|                     blacklist_factors = table.Column<List<long>>(type: "jsonb", nullable: false), | ||||
|                     claims = table.Column<List<string>>(type: "jsonb", nullable: false), | ||||
|                     audiences = table.Column<List<string>>(type: "jsonb", nullable: false), | ||||
|                     scopes = table.Column<List<string>>(type: "jsonb", nullable: false), | ||||
|                     ip_address = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: true), | ||||
|                     user_agent = table.Column<string>(type: "character varying(512)", maxLength: 512, nullable: true), | ||||
|                     device_id = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true), | ||||
							
								
								
									
										365
									
								
								DysonNetwork.Sphere/Migrations/20250410161354_AddSuperUser.Designer.cs
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										365
									
								
								DysonNetwork.Sphere/Migrations/20250410161354_AddSuperUser.Designer.cs
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,365 @@ | ||||
| // <auto-generated /> | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using DysonNetwork.Sphere; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Microsoft.EntityFrameworkCore.Infrastructure; | ||||
| using Microsoft.EntityFrameworkCore.Migrations; | ||||
| using Microsoft.EntityFrameworkCore.Storage.ValueConversion; | ||||
| using NodaTime; | ||||
| using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; | ||||
|  | ||||
| #nullable disable | ||||
|  | ||||
| namespace DysonNetwork.Sphere.Migrations | ||||
| { | ||||
|     [DbContext(typeof(AppDatabase))] | ||||
|     [Migration("20250410161354_AddSuperUser")] | ||||
|     partial class AddSuperUser | ||||
|     { | ||||
|         /// <inheritdoc /> | ||||
|         protected override void BuildTargetModel(ModelBuilder modelBuilder) | ||||
|         { | ||||
| #pragma warning disable 612, 618 | ||||
|             modelBuilder | ||||
|                 .HasAnnotation("ProductVersion", "9.0.3") | ||||
|                 .HasAnnotation("Relational:MaxIdentifierLength", 63); | ||||
|  | ||||
|             NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); | ||||
|  | ||||
|             modelBuilder.Entity("DysonNetwork.Sphere.Account.Account", b => | ||||
|                 { | ||||
|                     b.Property<long>("Id") | ||||
|                         .ValueGeneratedOnAdd() | ||||
|                         .HasColumnType("bigint") | ||||
|                         .HasColumnName("id"); | ||||
|  | ||||
|                     NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id")); | ||||
|  | ||||
|                     b.Property<Instant>("CreatedAt") | ||||
|                         .HasColumnType("timestamp with time zone") | ||||
|                         .HasColumnName("created_at"); | ||||
|  | ||||
|                     b.Property<Instant?>("DeletedAt") | ||||
|                         .HasColumnType("timestamp with time zone") | ||||
|                         .HasColumnName("deleted_at"); | ||||
|  | ||||
|                     b.Property<bool>("IsSuperuser") | ||||
|                         .HasColumnType("boolean") | ||||
|                         .HasColumnName("is_superuser"); | ||||
|  | ||||
|                     b.Property<string>("Language") | ||||
|                         .IsRequired() | ||||
|                         .HasMaxLength(32) | ||||
|                         .HasColumnType("character varying(32)") | ||||
|                         .HasColumnName("language"); | ||||
|  | ||||
|                     b.Property<string>("Name") | ||||
|                         .IsRequired() | ||||
|                         .HasMaxLength(256) | ||||
|                         .HasColumnType("character varying(256)") | ||||
|                         .HasColumnName("name"); | ||||
|  | ||||
|                     b.Property<string>("Nick") | ||||
|                         .IsRequired() | ||||
|                         .HasMaxLength(256) | ||||
|                         .HasColumnType("character varying(256)") | ||||
|                         .HasColumnName("nick"); | ||||
|  | ||||
|                     b.Property<Instant>("UpdatedAt") | ||||
|                         .HasColumnType("timestamp with time zone") | ||||
|                         .HasColumnName("updated_at"); | ||||
|  | ||||
|                     b.HasKey("Id") | ||||
|                         .HasName("pk_accounts"); | ||||
|  | ||||
|                     b.ToTable("accounts", (string)null); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("DysonNetwork.Sphere.Account.AccountAuthFactor", b => | ||||
|                 { | ||||
|                     b.Property<long>("Id") | ||||
|                         .ValueGeneratedOnAdd() | ||||
|                         .HasColumnType("bigint") | ||||
|                         .HasColumnName("id"); | ||||
|  | ||||
|                     NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id")); | ||||
|  | ||||
|                     b.Property<long>("AccountId") | ||||
|                         .HasColumnType("bigint") | ||||
|                         .HasColumnName("account_id"); | ||||
|  | ||||
|                     b.Property<Instant>("CreatedAt") | ||||
|                         .HasColumnType("timestamp with time zone") | ||||
|                         .HasColumnName("created_at"); | ||||
|  | ||||
|                     b.Property<Instant?>("DeletedAt") | ||||
|                         .HasColumnType("timestamp with time zone") | ||||
|                         .HasColumnName("deleted_at"); | ||||
|  | ||||
|                     b.Property<string>("Secret") | ||||
|                         .HasColumnType("text") | ||||
|                         .HasColumnName("secret"); | ||||
|  | ||||
|                     b.Property<int>("Type") | ||||
|                         .HasColumnType("integer") | ||||
|                         .HasColumnName("type"); | ||||
|  | ||||
|                     b.Property<Instant>("UpdatedAt") | ||||
|                         .HasColumnType("timestamp with time zone") | ||||
|                         .HasColumnName("updated_at"); | ||||
|  | ||||
|                     b.HasKey("Id") | ||||
|                         .HasName("pk_account_auth_factors"); | ||||
|  | ||||
|                     b.HasIndex("AccountId") | ||||
|                         .HasDatabaseName("ix_account_auth_factors_account_id"); | ||||
|  | ||||
|                     b.ToTable("account_auth_factors", (string)null); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("DysonNetwork.Sphere.Account.AccountContact", b => | ||||
|                 { | ||||
|                     b.Property<long>("Id") | ||||
|                         .ValueGeneratedOnAdd() | ||||
|                         .HasColumnType("bigint") | ||||
|                         .HasColumnName("id"); | ||||
|  | ||||
|                     NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id")); | ||||
|  | ||||
|                     b.Property<long>("AccountId") | ||||
|                         .HasColumnType("bigint") | ||||
|                         .HasColumnName("account_id"); | ||||
|  | ||||
|                     b.Property<string>("Content") | ||||
|                         .IsRequired() | ||||
|                         .HasMaxLength(1024) | ||||
|                         .HasColumnType("character varying(1024)") | ||||
|                         .HasColumnName("content"); | ||||
|  | ||||
|                     b.Property<Instant>("CreatedAt") | ||||
|                         .HasColumnType("timestamp with time zone") | ||||
|                         .HasColumnName("created_at"); | ||||
|  | ||||
|                     b.Property<Instant?>("DeletedAt") | ||||
|                         .HasColumnType("timestamp with time zone") | ||||
|                         .HasColumnName("deleted_at"); | ||||
|  | ||||
|                     b.Property<int>("Type") | ||||
|                         .HasColumnType("integer") | ||||
|                         .HasColumnName("type"); | ||||
|  | ||||
|                     b.Property<Instant>("UpdatedAt") | ||||
|                         .HasColumnType("timestamp with time zone") | ||||
|                         .HasColumnName("updated_at"); | ||||
|  | ||||
|                     b.Property<Instant?>("VerifiedAt") | ||||
|                         .HasColumnType("timestamp with time zone") | ||||
|                         .HasColumnName("verified_at"); | ||||
|  | ||||
|                     b.HasKey("Id") | ||||
|                         .HasName("pk_account_contacts"); | ||||
|  | ||||
|                     b.HasIndex("AccountId") | ||||
|                         .HasDatabaseName("ix_account_contacts_account_id"); | ||||
|  | ||||
|                     b.ToTable("account_contacts", (string)null); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("DysonNetwork.Sphere.Auth.Challenge", b => | ||||
|                 { | ||||
|                     b.Property<Guid>("Id") | ||||
|                         .ValueGeneratedOnAdd() | ||||
|                         .HasColumnType("uuid") | ||||
|                         .HasColumnName("id"); | ||||
|  | ||||
|                     b.Property<long>("AccountId") | ||||
|                         .HasColumnType("bigint") | ||||
|                         .HasColumnName("account_id"); | ||||
|  | ||||
|                     b.Property<List<string>>("Audiences") | ||||
|                         .IsRequired() | ||||
|                         .HasColumnType("jsonb") | ||||
|                         .HasColumnName("audiences"); | ||||
|  | ||||
|                     b.Property<List<long>>("BlacklistFactors") | ||||
|                         .IsRequired() | ||||
|                         .HasColumnType("jsonb") | ||||
|                         .HasColumnName("blacklist_factors"); | ||||
|  | ||||
|                     b.Property<Instant>("CreatedAt") | ||||
|                         .HasColumnType("timestamp with time zone") | ||||
|                         .HasColumnName("created_at"); | ||||
|  | ||||
|                     b.Property<Instant?>("DeletedAt") | ||||
|                         .HasColumnType("timestamp with time zone") | ||||
|                         .HasColumnName("deleted_at"); | ||||
|  | ||||
|                     b.Property<string>("DeviceId") | ||||
|                         .HasMaxLength(256) | ||||
|                         .HasColumnType("character varying(256)") | ||||
|                         .HasColumnName("device_id"); | ||||
|  | ||||
|                     b.Property<Instant?>("ExpiredAt") | ||||
|                         .HasColumnType("timestamp with time zone") | ||||
|                         .HasColumnName("expired_at"); | ||||
|  | ||||
|                     b.Property<string>("IpAddress") | ||||
|                         .HasMaxLength(128) | ||||
|                         .HasColumnType("character varying(128)") | ||||
|                         .HasColumnName("ip_address"); | ||||
|  | ||||
|                     b.Property<string>("Nonce") | ||||
|                         .HasMaxLength(1024) | ||||
|                         .HasColumnType("character varying(1024)") | ||||
|                         .HasColumnName("nonce"); | ||||
|  | ||||
|                     b.Property<List<string>>("Scopes") | ||||
|                         .IsRequired() | ||||
|                         .HasColumnType("jsonb") | ||||
|                         .HasColumnName("scopes"); | ||||
|  | ||||
|                     b.Property<int>("StepRemain") | ||||
|                         .HasColumnType("integer") | ||||
|                         .HasColumnName("step_remain"); | ||||
|  | ||||
|                     b.Property<int>("StepTotal") | ||||
|                         .HasColumnType("integer") | ||||
|                         .HasColumnName("step_total"); | ||||
|  | ||||
|                     b.Property<Instant>("UpdatedAt") | ||||
|                         .HasColumnType("timestamp with time zone") | ||||
|                         .HasColumnName("updated_at"); | ||||
|  | ||||
|                     b.Property<string>("UserAgent") | ||||
|                         .HasMaxLength(512) | ||||
|                         .HasColumnType("character varying(512)") | ||||
|                         .HasColumnName("user_agent"); | ||||
|  | ||||
|                     b.HasKey("Id") | ||||
|                         .HasName("pk_auth_challenges"); | ||||
|  | ||||
|                     b.HasIndex("AccountId") | ||||
|                         .HasDatabaseName("ix_auth_challenges_account_id"); | ||||
|  | ||||
|                     b.ToTable("auth_challenges", (string)null); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("DysonNetwork.Sphere.Auth.Session", b => | ||||
|                 { | ||||
|                     b.Property<Guid>("Id") | ||||
|                         .ValueGeneratedOnAdd() | ||||
|                         .HasColumnType("uuid") | ||||
|                         .HasColumnName("id"); | ||||
|  | ||||
|                     b.Property<long>("AccountId") | ||||
|                         .HasColumnType("bigint") | ||||
|                         .HasColumnName("account_id"); | ||||
|  | ||||
|                     b.Property<Guid>("ChallengeId") | ||||
|                         .HasColumnType("uuid") | ||||
|                         .HasColumnName("challenge_id"); | ||||
|  | ||||
|                     b.Property<Instant>("CreatedAt") | ||||
|                         .HasColumnType("timestamp with time zone") | ||||
|                         .HasColumnName("created_at"); | ||||
|  | ||||
|                     b.Property<Instant?>("DeletedAt") | ||||
|                         .HasColumnType("timestamp with time zone") | ||||
|                         .HasColumnName("deleted_at"); | ||||
|  | ||||
|                     b.Property<Instant?>("ExpiredAt") | ||||
|                         .HasColumnType("timestamp with time zone") | ||||
|                         .HasColumnName("expired_at"); | ||||
|  | ||||
|                     b.Property<Instant?>("LastGrantedAt") | ||||
|                         .HasColumnType("timestamp with time zone") | ||||
|                         .HasColumnName("last_granted_at"); | ||||
|  | ||||
|                     b.Property<Instant>("UpdatedAt") | ||||
|                         .HasColumnType("timestamp with time zone") | ||||
|                         .HasColumnName("updated_at"); | ||||
|  | ||||
|                     b.HasKey("Id") | ||||
|                         .HasName("pk_auth_sessions"); | ||||
|  | ||||
|                     b.HasIndex("AccountId") | ||||
|                         .HasDatabaseName("ix_auth_sessions_account_id"); | ||||
|  | ||||
|                     b.HasIndex("ChallengeId") | ||||
|                         .HasDatabaseName("ix_auth_sessions_challenge_id"); | ||||
|  | ||||
|                     b.ToTable("auth_sessions", (string)null); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("DysonNetwork.Sphere.Account.AccountAuthFactor", b => | ||||
|                 { | ||||
|                     b.HasOne("DysonNetwork.Sphere.Account.Account", "Account") | ||||
|                         .WithMany("AuthFactors") | ||||
|                         .HasForeignKey("AccountId") | ||||
|                         .OnDelete(DeleteBehavior.Cascade) | ||||
|                         .IsRequired() | ||||
|                         .HasConstraintName("fk_account_auth_factors_accounts_account_id"); | ||||
|  | ||||
|                     b.Navigation("Account"); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("DysonNetwork.Sphere.Account.AccountContact", b => | ||||
|                 { | ||||
|                     b.HasOne("DysonNetwork.Sphere.Account.Account", "Account") | ||||
|                         .WithMany("Contacts") | ||||
|                         .HasForeignKey("AccountId") | ||||
|                         .OnDelete(DeleteBehavior.Cascade) | ||||
|                         .IsRequired() | ||||
|                         .HasConstraintName("fk_account_contacts_accounts_account_id"); | ||||
|  | ||||
|                     b.Navigation("Account"); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("DysonNetwork.Sphere.Auth.Challenge", b => | ||||
|                 { | ||||
|                     b.HasOne("DysonNetwork.Sphere.Account.Account", "Account") | ||||
|                         .WithMany("Challenges") | ||||
|                         .HasForeignKey("AccountId") | ||||
|                         .OnDelete(DeleteBehavior.Cascade) | ||||
|                         .IsRequired() | ||||
|                         .HasConstraintName("fk_auth_challenges_accounts_account_id"); | ||||
|  | ||||
|                     b.Navigation("Account"); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("DysonNetwork.Sphere.Auth.Session", b => | ||||
|                 { | ||||
|                     b.HasOne("DysonNetwork.Sphere.Account.Account", "Account") | ||||
|                         .WithMany("Sessions") | ||||
|                         .HasForeignKey("AccountId") | ||||
|                         .OnDelete(DeleteBehavior.Cascade) | ||||
|                         .IsRequired() | ||||
|                         .HasConstraintName("fk_auth_sessions_accounts_account_id"); | ||||
|  | ||||
|                     b.HasOne("DysonNetwork.Sphere.Auth.Challenge", "Challenge") | ||||
|                         .WithMany() | ||||
|                         .HasForeignKey("ChallengeId") | ||||
|                         .OnDelete(DeleteBehavior.Cascade) | ||||
|                         .IsRequired() | ||||
|                         .HasConstraintName("fk_auth_sessions_auth_challenges_challenge_id"); | ||||
|  | ||||
|                     b.Navigation("Account"); | ||||
|  | ||||
|                     b.Navigation("Challenge"); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("DysonNetwork.Sphere.Account.Account", b => | ||||
|                 { | ||||
|                     b.Navigation("AuthFactors"); | ||||
|  | ||||
|                     b.Navigation("Challenges"); | ||||
|  | ||||
|                     b.Navigation("Contacts"); | ||||
|  | ||||
|                     b.Navigation("Sessions"); | ||||
|                 }); | ||||
| #pragma warning restore 612, 618 | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,41 @@ | ||||
| using Microsoft.EntityFrameworkCore.Migrations; | ||||
|  | ||||
| #nullable disable | ||||
|  | ||||
| namespace DysonNetwork.Sphere.Migrations | ||||
| { | ||||
|     /// <inheritdoc /> | ||||
|     public partial class AddSuperUser : Migration | ||||
|     { | ||||
|         /// <inheritdoc /> | ||||
|         protected override void Up(MigrationBuilder migrationBuilder) | ||||
|         { | ||||
|             migrationBuilder.AddColumn<bool>( | ||||
|                 name: "is_superuser", | ||||
|                 table: "accounts", | ||||
|                 type: "boolean", | ||||
|                 nullable: false, | ||||
|                 defaultValue: false); | ||||
|  | ||||
|             migrationBuilder.AddColumn<string>( | ||||
|                 name: "language", | ||||
|                 table: "accounts", | ||||
|                 type: "character varying(32)", | ||||
|                 maxLength: 32, | ||||
|                 nullable: false, | ||||
|                 defaultValue: ""); | ||||
|         } | ||||
|  | ||||
|         /// <inheritdoc /> | ||||
|         protected override void Down(MigrationBuilder migrationBuilder) | ||||
|         { | ||||
|             migrationBuilder.DropColumn( | ||||
|                 name: "is_superuser", | ||||
|                 table: "accounts"); | ||||
|  | ||||
|             migrationBuilder.DropColumn( | ||||
|                 name: "language", | ||||
|                 table: "accounts"); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -41,6 +41,16 @@ namespace DysonNetwork.Sphere.Migrations | ||||
|                         .HasColumnType("timestamp with time zone") | ||||
|                         .HasColumnName("deleted_at"); | ||||
|  | ||||
|                     b.Property<bool>("IsSuperuser") | ||||
|                         .HasColumnType("boolean") | ||||
|                         .HasColumnName("is_superuser"); | ||||
|  | ||||
|                     b.Property<string>("Language") | ||||
|                         .IsRequired() | ||||
|                         .HasMaxLength(32) | ||||
|                         .HasColumnType("character varying(32)") | ||||
|                         .HasColumnName("language"); | ||||
|  | ||||
|                     b.Property<string>("Name") | ||||
|                         .IsRequired() | ||||
|                         .HasMaxLength(256) | ||||
| @@ -174,11 +184,6 @@ namespace DysonNetwork.Sphere.Migrations | ||||
|                         .HasColumnType("jsonb") | ||||
|                         .HasColumnName("blacklist_factors"); | ||||
|  | ||||
|                     b.Property<List<string>>("Claims") | ||||
|                         .IsRequired() | ||||
|                         .HasColumnType("jsonb") | ||||
|                         .HasColumnName("claims"); | ||||
|  | ||||
|                     b.Property<Instant>("CreatedAt") | ||||
|                         .HasColumnType("timestamp with time zone") | ||||
|                         .HasColumnName("created_at"); | ||||
| @@ -206,6 +211,11 @@ namespace DysonNetwork.Sphere.Migrations | ||||
|                         .HasColumnType("character varying(1024)") | ||||
|                         .HasColumnName("nonce"); | ||||
|  | ||||
|                     b.Property<List<string>>("Scopes") | ||||
|                         .IsRequired() | ||||
|                         .HasColumnType("jsonb") | ||||
|                         .HasColumnName("scopes"); | ||||
|  | ||||
|                     b.Property<int>("StepRemain") | ||||
|                         .HasColumnType("integer") | ||||
|                         .HasColumnName("step_remain"); | ||||
|   | ||||
| @@ -1,9 +1,12 @@ | ||||
| using System.Security.Cryptography; | ||||
| using System.Text; | ||||
| using System.Text.Json; | ||||
| using Casbin; | ||||
| using Casbin.Persist.Adapter.EFCore; | ||||
| using DysonNetwork.Sphere; | ||||
| using DysonNetwork.Sphere.Account; | ||||
| using DysonNetwork.Sphere.Auth; | ||||
| using Microsoft.AspNetCore.Authorization; | ||||
| using Microsoft.AspNetCore.HttpOverrides; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Microsoft.IdentityModel.Tokens; | ||||
| @@ -25,6 +28,23 @@ builder.Services.AddControllers().AddJsonOptions(options => | ||||
| }); | ||||
| builder.Services.AddHttpContextAccessor(); | ||||
|  | ||||
| // Casbin permissions | ||||
|  | ||||
| var casbinDbContext = new CasbinDbContext<int>( | ||||
|     new DbContextOptionsBuilder<CasbinDbContext<int>>() | ||||
|         .UseNpgsql(builder.Configuration.GetConnectionString("Guard")) | ||||
|         .Options | ||||
| ); | ||||
| var casbinEfcore = new EFCoreAdapter<int>(casbinDbContext); | ||||
| casbinDbContext.Database.EnsureCreated(); | ||||
| var casbinEncofcer = new Enforcer("Casbin.conf", casbinEfcore); | ||||
| casbinEncofcer.LoadPolicy(); | ||||
|  | ||||
| builder.Services.AddSingleton<IEnforcer>(casbinEncofcer); | ||||
| builder.Services.AddSingleton<IAuthorizationHandler, CasbinAuthorizationHandler>(); | ||||
|  | ||||
| // Other pipelines | ||||
|  | ||||
| builder.Services.AddAuthorization(); | ||||
| builder.Services.AddAuthentication("Bearer").AddJwtBearer(options => | ||||
| { | ||||
|   | ||||
| @@ -7,7 +7,8 @@ | ||||
|   }, | ||||
|   "AllowedHosts": "*", | ||||
|   "ConnectionStrings": { | ||||
|     "App": "Host=localhost;Port=5432;Database=dyson_network;Username=postgres;Password=postgres" | ||||
|     "App": "Host=localhost;Port=5432;Database=dyson_network;Username=postgres;Password=postgres", | ||||
|     "Guard": "Host=localhost;Port=5432;Database=dyson_network_casbin;Username=postgres;Password=postgres" | ||||
|   }, | ||||
|   "Authentication": { | ||||
|     "Schemes": { | ||||
|   | ||||
		Reference in New Issue
	
	Block a user