💥 Simplified permission node system and data structure
This commit is contained in:
@@ -69,7 +69,7 @@ public class DeveloperController(
|
|||||||
|
|
||||||
[HttpPost("{name}/enroll")]
|
[HttpPost("{name}/enroll")]
|
||||||
[Authorize]
|
[Authorize]
|
||||||
[RequiredPermission("global", "developers.create")]
|
[AskPermission("developers.create")]
|
||||||
public async Task<ActionResult<SnDeveloper>> EnrollDeveloperProgram(string name)
|
public async Task<ActionResult<SnDeveloper>> EnrollDeveloperProgram(string name)
|
||||||
{
|
{
|
||||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ public static class ApplicationConfiguration
|
|||||||
|
|
||||||
app.UseAuthentication();
|
app.UseAuthentication();
|
||||||
app.UseAuthorization();
|
app.UseAuthorization();
|
||||||
app.UseMiddleware<PermissionMiddleware>();
|
app.UseMiddleware<RemotePermissionMiddleware>();
|
||||||
|
|
||||||
app.MapControllers();
|
app.MapControllers();
|
||||||
|
|
||||||
|
|||||||
@@ -381,7 +381,7 @@ public class FileController(
|
|||||||
|
|
||||||
[Authorize]
|
[Authorize]
|
||||||
[HttpDelete("recycle")]
|
[HttpDelete("recycle")]
|
||||||
[RequiredPermission("maintenance", "files.delete.recycle")]
|
[AskPermission("files.delete.recycle")]
|
||||||
public async Task<ActionResult> DeleteAllRecycledFiles()
|
public async Task<ActionResult> DeleteAllRecycledFiles()
|
||||||
{
|
{
|
||||||
var count = await fs.DeleteAllRecycledFilesAsync();
|
var count = await fs.DeleteAllRecycledFilesAsync();
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ public class FileUploadController(
|
|||||||
if (currentUser.IsSuperuser) return null;
|
if (currentUser.IsSuperuser) return null;
|
||||||
|
|
||||||
var allowed = await permission.HasPermissionAsync(new HasPermissionRequest
|
var allowed = await permission.HasPermissionAsync(new HasPermissionRequest
|
||||||
{ Actor = $"user:{currentUser.Id}", Area = "global", Key = "files.create" });
|
{ Actor = currentUser.Id, Key = "files.create" });
|
||||||
|
|
||||||
return allowed.HasPermission
|
return allowed.HasPermission
|
||||||
? null
|
? null
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ using DysonNetwork.Pass.Auth;
|
|||||||
using DysonNetwork.Pass.Credit;
|
using DysonNetwork.Pass.Credit;
|
||||||
using DysonNetwork.Pass.Permission;
|
using DysonNetwork.Pass.Permission;
|
||||||
using DysonNetwork.Pass.Wallet;
|
using DysonNetwork.Pass.Wallet;
|
||||||
|
using DysonNetwork.Shared.Auth;
|
||||||
using DysonNetwork.Shared.GeoIp;
|
using DysonNetwork.Shared.GeoIp;
|
||||||
using DysonNetwork.Shared.Http;
|
using DysonNetwork.Shared.Http;
|
||||||
using DysonNetwork.Shared.Models;
|
using DysonNetwork.Shared.Models;
|
||||||
@@ -319,7 +320,7 @@ public class AccountController(
|
|||||||
|
|
||||||
[HttpPost("credits/validate")]
|
[HttpPost("credits/validate")]
|
||||||
[Authorize]
|
[Authorize]
|
||||||
[RequiredPermission("maintenance", "credits.validate.perform")]
|
[AskPermission("credits.validate.perform")]
|
||||||
public async Task<IActionResult> PerformSocialCreditValidation()
|
public async Task<IActionResult> PerformSocialCreditValidation()
|
||||||
{
|
{
|
||||||
await socialCreditService.ValidateSocialCredits();
|
await socialCreditService.ValidateSocialCredits();
|
||||||
@@ -328,7 +329,7 @@ public class AccountController(
|
|||||||
|
|
||||||
[HttpDelete("{name}")]
|
[HttpDelete("{name}")]
|
||||||
[Authorize]
|
[Authorize]
|
||||||
[RequiredPermission("maintenance", "accounts.deletion")]
|
[AskPermission("accounts.deletion")]
|
||||||
public async Task<IActionResult> AdminDeleteAccount(string name)
|
public async Task<IActionResult> AdminDeleteAccount(string name)
|
||||||
{
|
{
|
||||||
var account = await accounts.LookupAccount(name);
|
var account = await accounts.LookupAccount(name);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using DysonNetwork.Pass.Permission;
|
using DysonNetwork.Pass.Permission;
|
||||||
using DysonNetwork.Pass.Wallet;
|
using DysonNetwork.Pass.Wallet;
|
||||||
|
using DysonNetwork.Shared.Auth;
|
||||||
using DysonNetwork.Shared.Http;
|
using DysonNetwork.Shared.Http;
|
||||||
using DysonNetwork.Shared.Models;
|
using DysonNetwork.Shared.Models;
|
||||||
using DysonNetwork.Shared.Proto;
|
using DysonNetwork.Shared.Proto;
|
||||||
@@ -194,7 +195,7 @@ public class AccountCurrentController(
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpPatch("statuses")]
|
[HttpPatch("statuses")]
|
||||||
[RequiredPermission("global", "accounts.statuses.update")]
|
[AskPermission("accounts.statuses.update")]
|
||||||
public async Task<ActionResult<SnAccountStatus>> UpdateStatus([FromBody] AccountController.StatusRequest request)
|
public async Task<ActionResult<SnAccountStatus>> UpdateStatus([FromBody] AccountController.StatusRequest request)
|
||||||
{
|
{
|
||||||
if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized();
|
if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized();
|
||||||
@@ -228,7 +229,7 @@ public class AccountCurrentController(
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("statuses")]
|
[HttpPost("statuses")]
|
||||||
[RequiredPermission("global", "accounts.statuses.create")]
|
[AskPermission("accounts.statuses.create")]
|
||||||
public async Task<ActionResult<SnAccountStatus>> CreateStatus([FromBody] AccountController.StatusRequest request)
|
public async Task<ActionResult<SnAccountStatus>> CreateStatus([FromBody] AccountController.StatusRequest request)
|
||||||
{
|
{
|
||||||
if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized();
|
if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized();
|
||||||
|
|||||||
@@ -158,7 +158,7 @@ public class AccountService(
|
|||||||
{
|
{
|
||||||
db.PermissionGroupMembers.Add(new SnPermissionGroupMember
|
db.PermissionGroupMembers.Add(new SnPermissionGroupMember
|
||||||
{
|
{
|
||||||
Actor = $"user:{account.Id}",
|
Actor = account.Id.ToString(),
|
||||||
Group = defaultGroup
|
Group = defaultGroup
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -194,7 +194,7 @@ public class MagicSpellService(
|
|||||||
{
|
{
|
||||||
db.PermissionGroupMembers.Add(new SnPermissionGroupMember
|
db.PermissionGroupMembers.Add(new SnPermissionGroupMember
|
||||||
{
|
{
|
||||||
Actor = $"user:{account.Id}",
|
Actor = account.Id.ToString(),
|
||||||
Group = defaultGroup
|
Group = defaultGroup
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ public class AppDatabase(
|
|||||||
"stickers.packs.create",
|
"stickers.packs.create",
|
||||||
"stickers.create"
|
"stickers.create"
|
||||||
}.Select(permission =>
|
}.Select(permission =>
|
||||||
PermissionService.NewPermissionNode("group:default", "global", permission, true))
|
PermissionService.NewPermissionNode("group:default", permission, true))
|
||||||
.ToList()
|
.ToList()
|
||||||
});
|
});
|
||||||
await context.SaveChangesAsync(cancellationToken);
|
await context.SaveChangesAsync(cancellationToken);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ using System.ComponentModel.DataAnnotations;
|
|||||||
using DysonNetwork.Shared.Models;
|
using DysonNetwork.Shared.Models;
|
||||||
using DysonNetwork.Pass.Wallet;
|
using DysonNetwork.Pass.Wallet;
|
||||||
using DysonNetwork.Pass.Permission;
|
using DysonNetwork.Pass.Permission;
|
||||||
|
using DysonNetwork.Shared.Auth;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
@@ -81,7 +82,7 @@ public class LotteryController(AppDatabase db, LotteryService lotteryService) :
|
|||||||
|
|
||||||
[HttpPost("draw")]
|
[HttpPost("draw")]
|
||||||
[Authorize]
|
[Authorize]
|
||||||
[RequiredPermission("maintenance", "lotteries.draw.perform")]
|
[AskPermission("lotteries.draw.perform")]
|
||||||
public async Task<IActionResult> PerformLotteryDraw()
|
public async Task<IActionResult> PerformLotteryDraw()
|
||||||
{
|
{
|
||||||
await lotteryService.DrawLotteries();
|
await lotteryService.DrawLotteries();
|
||||||
|
|||||||
2872
DysonNetwork.Pass/Migrations/20251202134035_SimplifiedPermissionNode.Designer.cs
generated
Normal file
2872
DysonNetwork.Pass/Migrations/20251202134035_SimplifiedPermissionNode.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,59 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace DysonNetwork.Pass.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class SimplifiedPermissionNode : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "ix_permission_nodes_key_area_actor",
|
||||||
|
table: "permission_nodes");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "area",
|
||||||
|
table: "permission_nodes");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<int>(
|
||||||
|
name: "type",
|
||||||
|
table: "permission_nodes",
|
||||||
|
type: "integer",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: 0);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "ix_permission_nodes_key_actor",
|
||||||
|
table: "permission_nodes",
|
||||||
|
columns: new[] { "key", "actor" });
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "ix_permission_nodes_key_actor",
|
||||||
|
table: "permission_nodes");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "type",
|
||||||
|
table: "permission_nodes");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "area",
|
||||||
|
table: "permission_nodes",
|
||||||
|
type: "character varying(1024)",
|
||||||
|
maxLength: 1024,
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: "");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "ix_permission_nodes_key_area_actor",
|
||||||
|
table: "permission_nodes",
|
||||||
|
columns: new[] { "key", "area", "actor" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,17 +1,12 @@
|
|||||||
|
using DysonNetwork.Shared.Auth;
|
||||||
|
|
||||||
namespace DysonNetwork.Pass.Permission;
|
namespace DysonNetwork.Pass.Permission;
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using DysonNetwork.Shared.Models;
|
using Shared.Models;
|
||||||
|
|
||||||
[AttributeUsage(AttributeTargets.Method)]
|
public class LocalPermissionMiddleware(RequestDelegate next, ILogger<LocalPermissionMiddleware> logger)
|
||||||
public class RequiredPermissionAttribute(string area, string key) : Attribute
|
|
||||||
{
|
|
||||||
public string Area { get; set; } = area;
|
|
||||||
public string Key { get; } = key;
|
|
||||||
}
|
|
||||||
|
|
||||||
public class PermissionMiddleware(RequestDelegate next, ILogger<PermissionMiddleware> logger)
|
|
||||||
{
|
{
|
||||||
private const string ForbiddenMessage = "Insufficient permissions";
|
private const string ForbiddenMessage = "Insufficient permissions";
|
||||||
private const string UnauthorizedMessage = "Authentication required";
|
private const string UnauthorizedMessage = "Authentication required";
|
||||||
@@ -21,15 +16,15 @@ public class PermissionMiddleware(RequestDelegate next, ILogger<PermissionMiddle
|
|||||||
var endpoint = httpContext.GetEndpoint();
|
var endpoint = httpContext.GetEndpoint();
|
||||||
|
|
||||||
var attr = endpoint?.Metadata
|
var attr = endpoint?.Metadata
|
||||||
.OfType<RequiredPermissionAttribute>()
|
.OfType<AskPermissionAttribute>()
|
||||||
.FirstOrDefault();
|
.FirstOrDefault();
|
||||||
|
|
||||||
if (attr != null)
|
if (attr != null)
|
||||||
{
|
{
|
||||||
// Validate permission attributes
|
// Validate permission attributes
|
||||||
if (string.IsNullOrWhiteSpace(attr.Area) || string.IsNullOrWhiteSpace(attr.Key))
|
if (string.IsNullOrWhiteSpace(attr.Key))
|
||||||
{
|
{
|
||||||
logger.LogWarning("Invalid permission attribute: Area='{Area}', Key='{Key}'", attr.Area, attr.Key);
|
logger.LogWarning("Invalid permission attribute: Key='{Key}'", attr.Key);
|
||||||
httpContext.Response.StatusCode = StatusCodes.Status500InternalServerError;
|
httpContext.Response.StatusCode = StatusCodes.Status500InternalServerError;
|
||||||
await httpContext.Response.WriteAsync("Server configuration error");
|
await httpContext.Response.WriteAsync("Server configuration error");
|
||||||
return;
|
return;
|
||||||
@@ -37,7 +32,7 @@ public class PermissionMiddleware(RequestDelegate next, ILogger<PermissionMiddle
|
|||||||
|
|
||||||
if (httpContext.Items["CurrentUser"] is not SnAccount currentUser)
|
if (httpContext.Items["CurrentUser"] is not SnAccount currentUser)
|
||||||
{
|
{
|
||||||
logger.LogWarning("Permission check failed: No authenticated user for {Area}/{Key}", attr.Area, attr.Key);
|
logger.LogWarning("Permission check failed: No authenticated user for {Key}", attr.Key);
|
||||||
httpContext.Response.StatusCode = StatusCodes.Status401Unauthorized;
|
httpContext.Response.StatusCode = StatusCodes.Status401Unauthorized;
|
||||||
await httpContext.Response.WriteAsync(UnauthorizedMessage);
|
await httpContext.Response.WriteAsync(UnauthorizedMessage);
|
||||||
return;
|
return;
|
||||||
@@ -46,33 +41,29 @@ public class PermissionMiddleware(RequestDelegate next, ILogger<PermissionMiddle
|
|||||||
if (currentUser.IsSuperuser)
|
if (currentUser.IsSuperuser)
|
||||||
{
|
{
|
||||||
// Bypass the permission check for performance
|
// Bypass the permission check for performance
|
||||||
logger.LogDebug("Superuser {UserId} bypassing permission check for {Area}/{Key}",
|
logger.LogDebug("Superuser {UserId} bypassing permission check for {Key}", currentUser.Id, attr.Key);
|
||||||
currentUser.Id, attr.Area, attr.Key);
|
|
||||||
await next(httpContext);
|
await next(httpContext);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var actor = $"user:{currentUser.Id}";
|
var actor = currentUser.Id.ToString();
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var permNode = await pm.GetPermissionAsync<bool>(actor, attr.Area, attr.Key);
|
var permNode = await pm.GetPermissionAsync<bool>(actor, attr.Key);
|
||||||
|
|
||||||
if (!permNode)
|
if (!permNode)
|
||||||
{
|
{
|
||||||
logger.LogWarning("Permission denied for user {UserId}: {Area}/{Key}",
|
logger.LogWarning("Permission denied for user {UserId}: {Key}", currentUser.Id, attr.Key);
|
||||||
currentUser.Id, attr.Area, attr.Key);
|
|
||||||
httpContext.Response.StatusCode = StatusCodes.Status403Forbidden;
|
httpContext.Response.StatusCode = StatusCodes.Status403Forbidden;
|
||||||
await httpContext.Response.WriteAsync(ForbiddenMessage);
|
await httpContext.Response.WriteAsync(ForbiddenMessage);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.LogDebug("Permission granted for user {UserId}: {Area}/{Key}",
|
logger.LogDebug("Permission granted for user {UserId}: {Key}", currentUser.Id, attr.Key);
|
||||||
currentUser.Id, attr.Area, attr.Key);
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
logger.LogError(ex, "Error checking permission for user {UserId}: {Area}/{Key}",
|
logger.LogError(ex, "Error checking permission for user {UserId}: {Key}", currentUser.Id, attr.Key);
|
||||||
currentUser.Id, attr.Area, attr.Key);
|
|
||||||
httpContext.Response.StatusCode = StatusCodes.Status500InternalServerError;
|
httpContext.Response.StatusCode = StatusCodes.Status500InternalServerError;
|
||||||
await httpContext.Response.WriteAsync("Permission check failed");
|
await httpContext.Response.WriteAsync("Permission check failed");
|
||||||
return;
|
return;
|
||||||
@@ -4,6 +4,7 @@ using Microsoft.Extensions.Options;
|
|||||||
using NodaTime;
|
using NodaTime;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using DysonNetwork.Shared.Cache;
|
using DysonNetwork.Shared.Cache;
|
||||||
|
using DysonNetwork.Shared.Data;
|
||||||
using DysonNetwork.Shared.Models;
|
using DysonNetwork.Shared.Models;
|
||||||
|
|
||||||
namespace DysonNetwork.Pass.Permission;
|
namespace DysonNetwork.Pass.Permission;
|
||||||
@@ -28,8 +29,8 @@ public class PermissionService(
|
|||||||
private const string PermissionGroupCacheKeyPrefix = "perm-cg:";
|
private const string PermissionGroupCacheKeyPrefix = "perm-cg:";
|
||||||
private const string PermissionGroupPrefix = "perm-g:";
|
private const string PermissionGroupPrefix = "perm-g:";
|
||||||
|
|
||||||
private static string GetPermissionCacheKey(string actor, string area, string key) =>
|
private static string GetPermissionCacheKey(string actor, string key) =>
|
||||||
PermissionCacheKeyPrefix + actor + ":" + area + ":" + key;
|
PermissionCacheKeyPrefix + actor + ":" + key;
|
||||||
|
|
||||||
private static string GetGroupsCacheKey(string actor) =>
|
private static string GetGroupsCacheKey(string actor) =>
|
||||||
PermissionGroupCacheKeyPrefix + actor;
|
PermissionGroupCacheKeyPrefix + actor;
|
||||||
@@ -37,50 +38,56 @@ public class PermissionService(
|
|||||||
private static string GetPermissionGroupKey(string actor) =>
|
private static string GetPermissionGroupKey(string actor) =>
|
||||||
PermissionGroupPrefix + actor;
|
PermissionGroupPrefix + actor;
|
||||||
|
|
||||||
public async Task<bool> HasPermissionAsync(string actor, string area, string key)
|
public async Task<bool> HasPermissionAsync(
|
||||||
|
string actor,
|
||||||
|
string key,
|
||||||
|
PermissionNodeActorType type = PermissionNodeActorType.Account
|
||||||
|
)
|
||||||
{
|
{
|
||||||
var value = await GetPermissionAsync<bool>(actor, area, key);
|
var value = await GetPermissionAsync<bool>(actor, key, type);
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<T?> GetPermissionAsync<T>(string actor, string area, string key)
|
public async Task<T?> GetPermissionAsync<T>(
|
||||||
|
string actor,
|
||||||
|
string key,
|
||||||
|
PermissionNodeActorType type = PermissionNodeActorType.Account
|
||||||
|
)
|
||||||
{
|
{
|
||||||
// Input validation
|
// Input validation
|
||||||
if (string.IsNullOrWhiteSpace(actor))
|
if (string.IsNullOrWhiteSpace(actor))
|
||||||
throw new ArgumentException("Actor cannot be null or empty", nameof(actor));
|
throw new ArgumentException("Actor cannot be null or empty", nameof(actor));
|
||||||
if (string.IsNullOrWhiteSpace(area))
|
|
||||||
throw new ArgumentException("Area cannot be null or empty", nameof(area));
|
|
||||||
if (string.IsNullOrWhiteSpace(key))
|
if (string.IsNullOrWhiteSpace(key))
|
||||||
throw new ArgumentException("Key cannot be null or empty", nameof(key));
|
throw new ArgumentException("Key cannot be null or empty", nameof(key));
|
||||||
|
|
||||||
var cacheKey = GetPermissionCacheKey(actor, area, key);
|
var cacheKey = GetPermissionCacheKey(actor, key);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var (hit, cachedValue) = await cache.GetAsyncWithStatus<T>(cacheKey);
|
var (hit, cachedValue) = await cache.GetAsyncWithStatus<T>(cacheKey);
|
||||||
if (hit)
|
if (hit)
|
||||||
{
|
{
|
||||||
logger.LogDebug("Permission cache hit for {Actor}:{Area}:{Key}", actor, area, key);
|
logger.LogDebug("Permission cache hit for {Type}:{Actor}:{Key}", type, actor, key);
|
||||||
return cachedValue;
|
return cachedValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
var now = SystemClock.Instance.GetCurrentInstant();
|
var now = SystemClock.Instance.GetCurrentInstant();
|
||||||
var groupsId = await GetOrCacheUserGroupsAsync(actor, now);
|
var groupsId = await GetOrCacheUserGroupsAsync(actor, now);
|
||||||
|
|
||||||
var permission = await FindPermissionNodeAsync(actor, area, key, groupsId, now);
|
var permission = await FindPermissionNodeAsync(type, actor, key, groupsId);
|
||||||
var result = permission != null ? DeserializePermissionValue<T>(permission.Value) : default;
|
var result = permission != null ? DeserializePermissionValue<T>(permission.Value) : default;
|
||||||
|
|
||||||
await cache.SetWithGroupsAsync(cacheKey, result,
|
await cache.SetWithGroupsAsync(cacheKey, result,
|
||||||
[GetPermissionGroupKey(actor)],
|
[GetPermissionGroupKey(actor)],
|
||||||
_options.CacheExpiration);
|
_options.CacheExpiration);
|
||||||
|
|
||||||
logger.LogDebug("Permission resolved for {Actor}:{Area}:{Key} = {Result}",
|
logger.LogDebug("Permission resolved for {Type}:{Actor}:{Key} = {Result}", type, actor, key,
|
||||||
actor, area, key, result != null);
|
result != null);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
logger.LogError(ex, "Error retrieving permission for {Actor}:{Area}:{Key}", actor, area, key);
|
logger.LogError(ex, "Error retrieving permission for {Type}:{Actor}:{Key}", type, actor, key);
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -109,33 +116,34 @@ public class PermissionService(
|
|||||||
return groupsId;
|
return groupsId;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<SnPermissionNode?> FindPermissionNodeAsync(string actor, string area, string key,
|
private async Task<SnPermissionNode?> FindPermissionNodeAsync(
|
||||||
List<Guid> groupsId, Instant now)
|
PermissionNodeActorType type,
|
||||||
|
string actor,
|
||||||
|
string key,
|
||||||
|
List<Guid> groupsId
|
||||||
|
)
|
||||||
{
|
{
|
||||||
|
var now = SystemClock.Instance.GetCurrentInstant();
|
||||||
// First try exact match (highest priority)
|
// First try exact match (highest priority)
|
||||||
var exactMatch = await db.PermissionNodes
|
var exactMatch = await db.PermissionNodes
|
||||||
.Where(n => (n.GroupId == null && n.Actor == actor) ||
|
.Where(n => (n.GroupId == null && n.Actor == actor && n.Type == type) ||
|
||||||
(n.GroupId != null && groupsId.Contains(n.GroupId.Value)))
|
(n.GroupId != null && groupsId.Contains(n.GroupId.Value)))
|
||||||
.Where(n => n.Key == key && n.Area == area)
|
.Where(n => n.Key == key)
|
||||||
.Where(n => n.ExpiredAt == null || n.ExpiredAt > now)
|
.Where(n => n.ExpiredAt == null || n.ExpiredAt > now)
|
||||||
.Where(n => n.AffectedAt == null || n.AffectedAt <= now)
|
.Where(n => n.AffectedAt == null || n.AffectedAt <= now)
|
||||||
.FirstOrDefaultAsync();
|
.FirstOrDefaultAsync();
|
||||||
|
|
||||||
if (exactMatch != null)
|
if (exactMatch != null)
|
||||||
{
|
|
||||||
return exactMatch;
|
return exactMatch;
|
||||||
}
|
|
||||||
|
|
||||||
// If no exact match and wildcards are enabled, try wildcard matches
|
// If no exact match and wildcards are enabled, try wildcard matches
|
||||||
if (!_options.EnableWildcardMatching)
|
if (!_options.EnableWildcardMatching)
|
||||||
{
|
|
||||||
return null;
|
return null;
|
||||||
}
|
|
||||||
|
|
||||||
var wildcardMatches = await db.PermissionNodes
|
var wildcardMatches = await db.PermissionNodes
|
||||||
.Where(n => (n.GroupId == null && n.Actor == actor) ||
|
.Where(n => (n.GroupId == null && n.Actor == actor && n.Type == type) ||
|
||||||
(n.GroupId != null && groupsId.Contains(n.GroupId.Value)))
|
(n.GroupId != null && groupsId.Contains(n.GroupId.Value)))
|
||||||
.Where(n => (n.Key.Contains("*") || n.Area.Contains("*")))
|
.Where(n => EF.Functions.Like(n.Key, "%*%"))
|
||||||
.Where(n => n.ExpiredAt == null || n.ExpiredAt > now)
|
.Where(n => n.ExpiredAt == null || n.ExpiredAt > now)
|
||||||
.Where(n => n.AffectedAt == null || n.AffectedAt <= now)
|
.Where(n => n.AffectedAt == null || n.AffectedAt <= now)
|
||||||
.Take(_options.MaxWildcardMatches)
|
.Take(_options.MaxWildcardMatches)
|
||||||
@@ -147,36 +155,21 @@ public class PermissionService(
|
|||||||
|
|
||||||
foreach (var node in wildcardMatches)
|
foreach (var node in wildcardMatches)
|
||||||
{
|
{
|
||||||
var score = CalculateWildcardMatchScore(node.Area, node.Key, area, key);
|
var score = CalculateWildcardMatchScore(node.Key, key);
|
||||||
if (score > bestMatchScore)
|
if (score <= bestMatchScore) continue;
|
||||||
{
|
|
||||||
bestMatch = node;
|
bestMatch = node;
|
||||||
bestMatchScore = score;
|
bestMatchScore = score;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (bestMatch != null)
|
if (bestMatch != null)
|
||||||
{
|
logger.LogDebug("Found wildcard permission match: {NodeKey} for {Key}", bestMatch.Key, key);
|
||||||
logger.LogDebug("Found wildcard permission match: {NodeArea}:{NodeKey} for {Area}:{Key}",
|
|
||||||
bestMatch.Area, bestMatch.Key, area, key);
|
|
||||||
}
|
|
||||||
|
|
||||||
return bestMatch;
|
return bestMatch;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int CalculateWildcardMatchScore(string nodeArea, string nodeKey, string targetArea, string targetKey)
|
private static int CalculateWildcardMatchScore(string nodeKey, string targetKey)
|
||||||
{
|
{
|
||||||
// Calculate how well the wildcard pattern matches
|
return CalculatePatternMatchScore(nodeKey, targetKey);
|
||||||
// Higher score = better match
|
|
||||||
var areaScore = CalculatePatternMatchScore(nodeArea, targetArea);
|
|
||||||
var keyScore = CalculatePatternMatchScore(nodeKey, targetKey);
|
|
||||||
|
|
||||||
// Perfect match gets highest score
|
|
||||||
if (areaScore == int.MaxValue && keyScore == int.MaxValue)
|
|
||||||
return int.MaxValue;
|
|
||||||
|
|
||||||
// Prefer area matches over key matches, more specific patterns over general ones
|
|
||||||
return (areaScore * 1000) + keyScore;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int CalculatePatternMatchScore(string pattern, string target)
|
private static int CalculatePatternMatchScore(string pattern, string target)
|
||||||
@@ -184,31 +177,30 @@ public class PermissionService(
|
|||||||
if (pattern == target)
|
if (pattern == target)
|
||||||
return int.MaxValue; // Exact match
|
return int.MaxValue; // Exact match
|
||||||
|
|
||||||
if (!pattern.Contains("*"))
|
if (!pattern.Contains('*'))
|
||||||
return -1; // No wildcard, not a match
|
return -1; // No wildcard, not a match
|
||||||
|
|
||||||
// Simple wildcard matching: * matches any sequence of characters
|
// Simple wildcard matching: * matches any sequence of characters
|
||||||
var regexPattern = "^" + System.Text.RegularExpressions.Regex.Escape(pattern).Replace("\\*", ".*") + "$";
|
var regexPattern = "^" + System.Text.RegularExpressions.Regex.Escape(pattern).Replace("\\*", ".*") + "$";
|
||||||
var regex = new System.Text.RegularExpressions.Regex(regexPattern, System.Text.RegularExpressions.RegexOptions.IgnoreCase);
|
var regex = new System.Text.RegularExpressions.Regex(regexPattern,
|
||||||
|
System.Text.RegularExpressions.RegexOptions.IgnoreCase);
|
||||||
|
|
||||||
|
if (!regex.IsMatch(target)) return -1; // No match
|
||||||
|
|
||||||
if (regex.IsMatch(target))
|
|
||||||
{
|
|
||||||
// Score based on specificity (shorter patterns are less specific)
|
// Score based on specificity (shorter patterns are less specific)
|
||||||
var wildcardCount = pattern.Count(c => c == '*');
|
var wildcardCount = pattern.Count(c => c == '*');
|
||||||
var length = pattern.Length;
|
var length = pattern.Length;
|
||||||
return Math.Max(1, 1000 - (wildcardCount * 100) - length);
|
|
||||||
}
|
|
||||||
|
|
||||||
return -1; // No match
|
return Math.Max(1, 1000 - wildcardCount * 100 - length);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<SnPermissionNode> AddPermissionNode<T>(
|
public async Task<SnPermissionNode> AddPermissionNode<T>(
|
||||||
string actor,
|
string actor,
|
||||||
string area,
|
|
||||||
string key,
|
string key,
|
||||||
T value,
|
T value,
|
||||||
Instant? expiredAt = null,
|
Instant? expiredAt = null,
|
||||||
Instant? affectedAt = null
|
Instant? affectedAt = null,
|
||||||
|
PermissionNodeActorType type = PermissionNodeActorType.Account
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
if (value is null) throw new ArgumentNullException(nameof(value));
|
if (value is null) throw new ArgumentNullException(nameof(value));
|
||||||
@@ -216,8 +208,8 @@ public class PermissionService(
|
|||||||
var node = new SnPermissionNode
|
var node = new SnPermissionNode
|
||||||
{
|
{
|
||||||
Actor = actor,
|
Actor = actor,
|
||||||
|
Type = type,
|
||||||
Key = key,
|
Key = key,
|
||||||
Area = area,
|
|
||||||
Value = SerializePermissionValue(value),
|
Value = SerializePermissionValue(value),
|
||||||
ExpiredAt = expiredAt,
|
ExpiredAt = expiredAt,
|
||||||
AffectedAt = affectedAt
|
AffectedAt = affectedAt
|
||||||
@@ -227,7 +219,7 @@ public class PermissionService(
|
|||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
// Invalidate related caches
|
// Invalidate related caches
|
||||||
await InvalidatePermissionCacheAsync(actor, area, key);
|
await InvalidatePermissionCacheAsync(actor, key);
|
||||||
|
|
||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
@@ -235,11 +227,11 @@ public class PermissionService(
|
|||||||
public async Task<SnPermissionNode> AddPermissionNodeToGroup<T>(
|
public async Task<SnPermissionNode> AddPermissionNodeToGroup<T>(
|
||||||
SnPermissionGroup group,
|
SnPermissionGroup group,
|
||||||
string actor,
|
string actor,
|
||||||
string area,
|
|
||||||
string key,
|
string key,
|
||||||
T value,
|
T value,
|
||||||
Instant? expiredAt = null,
|
Instant? expiredAt = null,
|
||||||
Instant? affectedAt = null
|
Instant? affectedAt = null,
|
||||||
|
PermissionNodeActorType type = PermissionNodeActorType.Account
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
if (value is null) throw new ArgumentNullException(nameof(value));
|
if (value is null) throw new ArgumentNullException(nameof(value));
|
||||||
@@ -247,8 +239,8 @@ public class PermissionService(
|
|||||||
var node = new SnPermissionNode
|
var node = new SnPermissionNode
|
||||||
{
|
{
|
||||||
Actor = actor,
|
Actor = actor,
|
||||||
|
Type = type,
|
||||||
Key = key,
|
Key = key,
|
||||||
Area = area,
|
|
||||||
Value = SerializePermissionValue(value),
|
Value = SerializePermissionValue(value),
|
||||||
ExpiredAt = expiredAt,
|
ExpiredAt = expiredAt,
|
||||||
AffectedAt = affectedAt,
|
AffectedAt = affectedAt,
|
||||||
@@ -260,44 +252,45 @@ public class PermissionService(
|
|||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
// Invalidate related caches
|
// Invalidate related caches
|
||||||
await InvalidatePermissionCacheAsync(actor, area, key);
|
await InvalidatePermissionCacheAsync(actor, key);
|
||||||
await cache.RemoveAsync(GetGroupsCacheKey(actor));
|
await cache.RemoveAsync(GetGroupsCacheKey(actor));
|
||||||
await cache.RemoveGroupAsync(GetPermissionGroupKey(actor));
|
await cache.RemoveGroupAsync(GetPermissionGroupKey(actor));
|
||||||
|
|
||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task RemovePermissionNode(string actor, string area, string key)
|
public async Task RemovePermissionNode(string actor, string key, PermissionNodeActorType? type)
|
||||||
{
|
{
|
||||||
var node = await db.PermissionNodes
|
var node = await db.PermissionNodes
|
||||||
.Where(n => n.Actor == actor && n.Area == area && n.Key == key)
|
.Where(n => n.Actor == actor && n.Key == key)
|
||||||
|
.If(type is not null, q => q.Where(n => n.Type == type))
|
||||||
.FirstOrDefaultAsync();
|
.FirstOrDefaultAsync();
|
||||||
if (node is not null) db.PermissionNodes.Remove(node);
|
if (node is not null) db.PermissionNodes.Remove(node);
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
// Invalidate cache
|
// Invalidate cache
|
||||||
await InvalidatePermissionCacheAsync(actor, area, key);
|
await InvalidatePermissionCacheAsync(actor, key);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task RemovePermissionNodeFromGroup<T>(SnPermissionGroup group, string actor, string area, string key)
|
public async Task RemovePermissionNodeFromGroup<T>(SnPermissionGroup group, string actor, string key)
|
||||||
{
|
{
|
||||||
var node = await db.PermissionNodes
|
var node = await db.PermissionNodes
|
||||||
.Where(n => n.GroupId == group.Id)
|
.Where(n => n.GroupId == group.Id)
|
||||||
.Where(n => n.Actor == actor && n.Area == area && n.Key == key)
|
.Where(n => n.Actor == actor && n.Key == key && n.Type == PermissionNodeActorType.Group)
|
||||||
.FirstOrDefaultAsync();
|
.FirstOrDefaultAsync();
|
||||||
if (node is null) return;
|
if (node is null) return;
|
||||||
db.PermissionNodes.Remove(node);
|
db.PermissionNodes.Remove(node);
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
// Invalidate caches
|
// Invalidate caches
|
||||||
await InvalidatePermissionCacheAsync(actor, area, key);
|
await InvalidatePermissionCacheAsync(actor, key);
|
||||||
await cache.RemoveAsync(GetGroupsCacheKey(actor));
|
await cache.RemoveAsync(GetGroupsCacheKey(actor));
|
||||||
await cache.RemoveGroupAsync(GetPermissionGroupKey(actor));
|
await cache.RemoveGroupAsync(GetPermissionGroupKey(actor));
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task InvalidatePermissionCacheAsync(string actor, string area, string key)
|
private async Task InvalidatePermissionCacheAsync(string actor, string key)
|
||||||
{
|
{
|
||||||
var cacheKey = GetPermissionCacheKey(actor, area, key);
|
var cacheKey = GetPermissionCacheKey(actor, key);
|
||||||
await cache.RemoveAsync(cacheKey);
|
await cache.RemoveAsync(cacheKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -312,12 +305,11 @@ public class PermissionService(
|
|||||||
return JsonDocument.Parse(str);
|
return JsonDocument.Parse(str);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static SnPermissionNode NewPermissionNode<T>(string actor, string area, string key, T value)
|
public static SnPermissionNode NewPermissionNode<T>(string actor, string key, T value)
|
||||||
{
|
{
|
||||||
return new SnPermissionNode
|
return new SnPermissionNode
|
||||||
{
|
{
|
||||||
Actor = actor,
|
Actor = actor,
|
||||||
Area = area,
|
|
||||||
Key = key,
|
Key = key,
|
||||||
Value = SerializePermissionValue(value),
|
Value = SerializePermissionValue(value),
|
||||||
};
|
};
|
||||||
@@ -341,8 +333,7 @@ public class PermissionService(
|
|||||||
(n.GroupId != null && groupsId.Contains(n.GroupId.Value)))
|
(n.GroupId != null && groupsId.Contains(n.GroupId.Value)))
|
||||||
.Where(n => n.ExpiredAt == null || n.ExpiredAt > now)
|
.Where(n => n.ExpiredAt == null || n.ExpiredAt > now)
|
||||||
.Where(n => n.AffectedAt == null || n.AffectedAt <= now)
|
.Where(n => n.AffectedAt == null || n.AffectedAt <= now)
|
||||||
.OrderBy(n => n.Area)
|
.OrderBy(n => n.Key)
|
||||||
.ThenBy(n => n.Key)
|
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
|
||||||
logger.LogDebug("Listed {Count} effective permissions for actor {Actor}", permissions.Count, actor);
|
logger.LogDebug("Listed {Count} effective permissions for actor {Actor}", permissions.Count, actor);
|
||||||
@@ -370,8 +361,7 @@ public class PermissionService(
|
|||||||
.Where(n => n.GroupId == null && n.Actor == actor)
|
.Where(n => n.GroupId == null && n.Actor == actor)
|
||||||
.Where(n => n.ExpiredAt == null || n.ExpiredAt > now)
|
.Where(n => n.ExpiredAt == null || n.ExpiredAt > now)
|
||||||
.Where(n => n.AffectedAt == null || n.AffectedAt <= now)
|
.Where(n => n.AffectedAt == null || n.AffectedAt <= now)
|
||||||
.OrderBy(n => n.Area)
|
.OrderBy(n => n.Key)
|
||||||
.ThenBy(n => n.Key)
|
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
|
||||||
logger.LogDebug("Listed {Count} direct permissions for actor {Actor}", permissions.Count, actor);
|
logger.LogDebug("Listed {Count} direct permissions for actor {Actor}", permissions.Count, actor);
|
||||||
|
|||||||
@@ -9,31 +9,33 @@ using NodaTime.Serialization.Protobuf;
|
|||||||
namespace DysonNetwork.Pass.Permission;
|
namespace DysonNetwork.Pass.Permission;
|
||||||
|
|
||||||
public class PermissionServiceGrpc(
|
public class PermissionServiceGrpc(
|
||||||
PermissionService permissionService,
|
PermissionService psv,
|
||||||
AppDatabase db,
|
AppDatabase db,
|
||||||
ILogger<PermissionServiceGrpc> logger
|
ILogger<PermissionServiceGrpc> logger
|
||||||
) : DysonNetwork.Shared.Proto.PermissionService.PermissionServiceBase
|
) : DysonNetwork.Shared.Proto.PermissionService.PermissionServiceBase
|
||||||
{
|
{
|
||||||
public override async Task<HasPermissionResponse> HasPermission(HasPermissionRequest request, ServerCallContext context)
|
public override async Task<HasPermissionResponse> HasPermission(HasPermissionRequest request, ServerCallContext context)
|
||||||
{
|
{
|
||||||
|
var type = SnPermissionNode.ConvertProtoActorType(request.Type);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var hasPermission = await permissionService.HasPermissionAsync(request.Actor, request.Area, request.Key);
|
var hasPermission = await psv.HasPermissionAsync(request.Actor, request.Key, type);
|
||||||
return new HasPermissionResponse { HasPermission = hasPermission };
|
return new HasPermissionResponse { HasPermission = hasPermission };
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
logger.LogError(ex, "Error checking permission for actor {Actor}, area {Area}, key {Key}",
|
logger.LogError(ex, "Error checking permission for {Type}:{Area}:{Key}",
|
||||||
request.Actor, request.Area, request.Key);
|
type, request.Actor, request.Key);
|
||||||
throw new RpcException(new Status(StatusCode.Internal, "Permission check failed"));
|
throw new RpcException(new Status(StatusCode.Internal, "Permission check failed"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task<GetPermissionResponse> GetPermission(GetPermissionRequest request, ServerCallContext context)
|
public override async Task<GetPermissionResponse> GetPermission(GetPermissionRequest request, ServerCallContext context)
|
||||||
{
|
{
|
||||||
|
var type = SnPermissionNode.ConvertProtoActorType(request.Type);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var permissionValue = await permissionService.GetPermissionAsync<JsonDocument>(request.Actor, request.Area, request.Key);
|
var permissionValue = await psv.GetPermissionAsync<JsonDocument>(request.Actor, request.Key, type);
|
||||||
return new GetPermissionResponse
|
return new GetPermissionResponse
|
||||||
{
|
{
|
||||||
Value = permissionValue != null ? Value.Parser.ParseJson(permissionValue.RootElement.GetRawText()) : null
|
Value = permissionValue != null ? Value.Parser.ParseJson(permissionValue.RootElement.GetRawText()) : null
|
||||||
@@ -41,14 +43,15 @@ public class PermissionServiceGrpc(
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
logger.LogError(ex, "Error getting permission for actor {Actor}, area {Area}, key {Key}",
|
logger.LogError(ex, "Error getting permission for {Type}:{Area}:{Key}",
|
||||||
request.Actor, request.Area, request.Key);
|
type, request.Actor, request.Key);
|
||||||
throw new RpcException(new Status(StatusCode.Internal, "Failed to retrieve permission"));
|
throw new RpcException(new Status(StatusCode.Internal, "Failed to retrieve permission"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task<AddPermissionNodeResponse> AddPermissionNode(AddPermissionNodeRequest request, ServerCallContext context)
|
public override async Task<AddPermissionNodeResponse> AddPermissionNode(AddPermissionNodeRequest request, ServerCallContext context)
|
||||||
{
|
{
|
||||||
|
var type = SnPermissionNode.ConvertProtoActorType(request.Type);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
JsonDocument jsonValue;
|
JsonDocument jsonValue;
|
||||||
@@ -58,18 +61,18 @@ public class PermissionServiceGrpc(
|
|||||||
}
|
}
|
||||||
catch (JsonException ex)
|
catch (JsonException ex)
|
||||||
{
|
{
|
||||||
logger.LogWarning(ex, "Invalid JSON in permission value for actor {Actor}, area {Area}, key {Key}",
|
logger.LogError(ex, "Invalid JSON in permission value for {Type}:{Area}:{Key}",
|
||||||
request.Actor, request.Area, request.Key);
|
type, request.Actor, request.Key);
|
||||||
throw new RpcException(new Status(StatusCode.InvalidArgument, "Invalid permission value format"));
|
throw new RpcException(new Status(StatusCode.InvalidArgument, "Invalid permission value format"));
|
||||||
}
|
}
|
||||||
|
|
||||||
var node = await permissionService.AddPermissionNode(
|
var node = await psv.AddPermissionNode(
|
||||||
request.Actor,
|
request.Actor,
|
||||||
request.Area,
|
|
||||||
request.Key,
|
request.Key,
|
||||||
jsonValue,
|
jsonValue,
|
||||||
request.ExpiredAt?.ToInstant(),
|
request.ExpiredAt?.ToInstant(),
|
||||||
request.AffectedAt?.ToInstant()
|
request.AffectedAt?.ToInstant(),
|
||||||
|
type
|
||||||
);
|
);
|
||||||
return new AddPermissionNodeResponse { Node = node.ToProtoValue() };
|
return new AddPermissionNodeResponse { Node = node.ToProtoValue() };
|
||||||
}
|
}
|
||||||
@@ -79,14 +82,15 @@ public class PermissionServiceGrpc(
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
logger.LogError(ex, "Error adding permission node for actor {Actor}, area {Area}, key {Key}",
|
logger.LogError(ex, "Error adding permission for {Type}:{Area}:{Key}",
|
||||||
request.Actor, request.Area, request.Key);
|
type, request.Actor, request.Key);
|
||||||
throw new RpcException(new Status(StatusCode.Internal, "Failed to add permission node"));
|
throw new RpcException(new Status(StatusCode.Internal, "Failed to add permission node"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task<AddPermissionNodeToGroupResponse> AddPermissionNodeToGroup(AddPermissionNodeToGroupRequest request, ServerCallContext context)
|
public override async Task<AddPermissionNodeToGroupResponse> AddPermissionNodeToGroup(AddPermissionNodeToGroupRequest request, ServerCallContext context)
|
||||||
{
|
{
|
||||||
|
var type = SnPermissionNode.ConvertProtoActorType(request.Type);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var group = await FindPermissionGroupAsync(request.Group.Id);
|
var group = await FindPermissionGroupAsync(request.Group.Id);
|
||||||
@@ -102,19 +106,19 @@ public class PermissionServiceGrpc(
|
|||||||
}
|
}
|
||||||
catch (JsonException ex)
|
catch (JsonException ex)
|
||||||
{
|
{
|
||||||
logger.LogWarning(ex, "Invalid JSON in permission value for group {GroupId}, actor {Actor}, area {Area}, key {Key}",
|
logger.LogError(ex, "Invalid JSON in permission value for {Type}:{Area}:{Key}",
|
||||||
request.Group.Id, request.Actor, request.Area, request.Key);
|
type, request.Actor, request.Key);
|
||||||
throw new RpcException(new Status(StatusCode.InvalidArgument, "Invalid permission value format"));
|
throw new RpcException(new Status(StatusCode.InvalidArgument, "Invalid permission value format"));
|
||||||
}
|
}
|
||||||
|
|
||||||
var node = await permissionService.AddPermissionNodeToGroup(
|
var node = await psv.AddPermissionNodeToGroup(
|
||||||
group,
|
group,
|
||||||
request.Actor,
|
request.Actor,
|
||||||
request.Area,
|
|
||||||
request.Key,
|
request.Key,
|
||||||
jsonValue,
|
jsonValue,
|
||||||
request.ExpiredAt?.ToInstant(),
|
request.ExpiredAt?.ToInstant(),
|
||||||
request.AffectedAt?.ToInstant()
|
request.AffectedAt?.ToInstant(),
|
||||||
|
type
|
||||||
);
|
);
|
||||||
return new AddPermissionNodeToGroupResponse { Node = node.ToProtoValue() };
|
return new AddPermissionNodeToGroupResponse { Node = node.ToProtoValue() };
|
||||||
}
|
}
|
||||||
@@ -124,23 +128,24 @@ public class PermissionServiceGrpc(
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
logger.LogError(ex, "Error adding permission node to group {GroupId} for actor {Actor}, area {Area}, key {Key}",
|
logger.LogError(ex, "Error adding permission for {Type}:{Area}:{Key}",
|
||||||
request.Group.Id, request.Actor, request.Area, request.Key);
|
type, request.Actor, request.Key);
|
||||||
throw new RpcException(new Status(StatusCode.Internal, "Failed to add permission node to group"));
|
throw new RpcException(new Status(StatusCode.Internal, "Failed to add permission node to group"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task<RemovePermissionNodeResponse> RemovePermissionNode(RemovePermissionNodeRequest request, ServerCallContext context)
|
public override async Task<RemovePermissionNodeResponse> RemovePermissionNode(RemovePermissionNodeRequest request, ServerCallContext context)
|
||||||
{
|
{
|
||||||
|
var type = SnPermissionNode.ConvertProtoActorType(request.Type);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await permissionService.RemovePermissionNode(request.Actor, request.Area, request.Key);
|
await psv.RemovePermissionNode(request.Actor, request.Key, type);
|
||||||
return new RemovePermissionNodeResponse { Success = true };
|
return new RemovePermissionNodeResponse { Success = true };
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
logger.LogError(ex, "Error removing permission node for actor {Actor}, area {Area}, key {Key}",
|
logger.LogError(ex, "Error removing permission for {Type}:{Area}:{Key}",
|
||||||
request.Actor, request.Area, request.Key);
|
type, request.Actor, request.Key);
|
||||||
throw new RpcException(new Status(StatusCode.Internal, "Failed to remove permission node"));
|
throw new RpcException(new Status(StatusCode.Internal, "Failed to remove permission node"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -155,7 +160,7 @@ public class PermissionServiceGrpc(
|
|||||||
throw new RpcException(new Status(StatusCode.NotFound, "Permission group not found"));
|
throw new RpcException(new Status(StatusCode.NotFound, "Permission group not found"));
|
||||||
}
|
}
|
||||||
|
|
||||||
await permissionService.RemovePermissionNodeFromGroup<JsonDocument>(group, request.Actor, request.Area, request.Key);
|
await psv.RemovePermissionNodeFromGroup<JsonDocument>(group, request.Actor, request.Key);
|
||||||
return new RemovePermissionNodeFromGroupResponse { Success = true };
|
return new RemovePermissionNodeFromGroupResponse { Success = true };
|
||||||
}
|
}
|
||||||
catch (RpcException)
|
catch (RpcException)
|
||||||
@@ -164,20 +169,18 @@ public class PermissionServiceGrpc(
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
logger.LogError(ex, "Error removing permission node from group {GroupId} for actor {Actor}, area {Area}, key {Key}",
|
logger.LogError(ex, "Error removing permission from group for {Area}:{Key}",
|
||||||
request.Group.Id, request.Actor, request.Area, request.Key);
|
request.Actor, request.Key);
|
||||||
throw new RpcException(new Status(StatusCode.Internal, "Failed to remove permission node from group"));
|
throw new RpcException(new Status(StatusCode.Internal, "Failed to remove permission node from group"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<SnPermissionGroup?> FindPermissionGroupAsync(string groupId)
|
private async Task<SnPermissionGroup?> FindPermissionGroupAsync(string groupId)
|
||||||
{
|
{
|
||||||
if (!Guid.TryParse(groupId, out var guid))
|
if (Guid.TryParse(groupId, out var guid))
|
||||||
{
|
return await db.PermissionGroups.FirstOrDefaultAsync(g => g.Id == guid);
|
||||||
logger.LogWarning("Invalid GUID format for group ID: {GroupId}", groupId);
|
logger.LogWarning("Invalid GUID format for group ID: {GroupId}", groupId);
|
||||||
return null;
|
return null;
|
||||||
}
|
|
||||||
|
|
||||||
return await db.PermissionGroups.FirstOrDefaultAsync(g => g.Id == guid);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ using DysonNetwork.Pass.Permission;
|
|||||||
using DysonNetwork.Shared.Models;
|
using DysonNetwork.Shared.Models;
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
using DysonNetwork.Shared.Auth;
|
||||||
|
|
||||||
namespace DysonNetwork.Pass;
|
namespace DysonNetwork.Pass;
|
||||||
|
|
||||||
@@ -19,16 +20,20 @@ public class PermissionController(
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Check if an actor has a specific permission
|
/// Check if an actor has a specific permission
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[HttpGet("check/{actor}/{area}/{key}")]
|
[HttpGet("check/{actor}/{key}")]
|
||||||
[RequiredPermission("maintenance", "permissions.check")]
|
[AskPermission("permissions.check")]
|
||||||
[ProducesResponseType<bool>(StatusCodes.Status200OK)]
|
[ProducesResponseType<bool>(StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||||
public async Task<IActionResult> CheckPermission(string actor, string area, string key)
|
public async Task<IActionResult> CheckPermission(
|
||||||
|
[FromRoute] string actor,
|
||||||
|
[FromRoute] string key,
|
||||||
|
[FromQuery] PermissionNodeActorType type = PermissionNodeActorType.Account
|
||||||
|
)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var hasPermission = await permissionService.HasPermissionAsync(actor, area, key);
|
var hasPermission = await permissionService.HasPermissionAsync(actor, key, type);
|
||||||
return Ok(hasPermission);
|
return Ok(hasPermission);
|
||||||
}
|
}
|
||||||
catch (ArgumentException ex)
|
catch (ArgumentException ex)
|
||||||
@@ -45,7 +50,7 @@ public class PermissionController(
|
|||||||
/// Get all effective permissions for an actor (including group permissions)
|
/// Get all effective permissions for an actor (including group permissions)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[HttpGet("actors/{actor}/permissions/effective")]
|
[HttpGet("actors/{actor}/permissions/effective")]
|
||||||
[RequiredPermission("maintenance", "permissions.check")]
|
[AskPermission("permissions.check")]
|
||||||
[ProducesResponseType<List<SnPermissionNode>>(StatusCodes.Status200OK)]
|
[ProducesResponseType<List<SnPermissionNode>>(StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||||
@@ -70,7 +75,7 @@ public class PermissionController(
|
|||||||
/// Get all direct permissions for an actor (excluding group permissions)
|
/// Get all direct permissions for an actor (excluding group permissions)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[HttpGet("actors/{actor}/permissions/direct")]
|
[HttpGet("actors/{actor}/permissions/direct")]
|
||||||
[RequiredPermission("maintenance", "permissions.check")]
|
[AskPermission("permissions.check")]
|
||||||
[ProducesResponseType<List<SnPermissionNode>>(StatusCodes.Status200OK)]
|
[ProducesResponseType<List<SnPermissionNode>>(StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||||
@@ -94,28 +99,27 @@ public class PermissionController(
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Give a permission to an actor
|
/// Give a permission to an actor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[HttpPost("actors/{actor}/permissions/{area}/{key}")]
|
[HttpPost("actors/{actor}/permissions/{key}")]
|
||||||
[RequiredPermission("maintenance", "permissions.manage")]
|
[AskPermission("permissions.manage")]
|
||||||
[ProducesResponseType<SnPermissionNode>(StatusCodes.Status201Created)]
|
[ProducesResponseType<SnPermissionNode>(StatusCodes.Status201Created)]
|
||||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||||
public async Task<IActionResult> GivePermission(
|
public async Task<IActionResult> GivePermission(
|
||||||
string actor,
|
string actor,
|
||||||
string area,
|
|
||||||
string key,
|
string key,
|
||||||
[FromBody] PermissionRequest request)
|
[FromBody] PermissionRequest request
|
||||||
|
)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var permission = await permissionService.AddPermissionNode(
|
var permission = await permissionService.AddPermissionNode(
|
||||||
actor,
|
actor,
|
||||||
area,
|
|
||||||
key,
|
key,
|
||||||
JsonDocument.Parse(JsonSerializer.Serialize(request.Value)),
|
JsonDocument.Parse(JsonSerializer.Serialize(request.Value)),
|
||||||
request.ExpiredAt,
|
request.ExpiredAt,
|
||||||
request.AffectedAt
|
request.AffectedAt
|
||||||
);
|
);
|
||||||
return Created($"/api/permissions/actors/{actor}/permissions/{area}/{key}", permission);
|
return Created($"/api/permissions/actors/{actor}/permissions/{key}", permission);
|
||||||
}
|
}
|
||||||
catch (ArgumentException ex)
|
catch (ArgumentException ex)
|
||||||
{
|
{
|
||||||
@@ -130,16 +134,20 @@ public class PermissionController(
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Remove a permission from an actor
|
/// Remove a permission from an actor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[HttpDelete("actors/{actor}/permissions/{area}/{key}")]
|
[HttpDelete("actors/{actor}/permissions/{key}")]
|
||||||
[RequiredPermission("maintenance", "permissions.manage")]
|
[AskPermission("permissions.manage")]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||||
public async Task<IActionResult> RemovePermission(string actor, string area, string key)
|
public async Task<IActionResult> RemovePermission(
|
||||||
|
string actor,
|
||||||
|
string key,
|
||||||
|
[FromQuery] PermissionNodeActorType type = PermissionNodeActorType.Account
|
||||||
|
)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await permissionService.RemovePermissionNode(actor, area, key);
|
await permissionService.RemovePermissionNode(actor, key, type);
|
||||||
return NoContent();
|
return NoContent();
|
||||||
}
|
}
|
||||||
catch (ArgumentException ex)
|
catch (ArgumentException ex)
|
||||||
@@ -156,7 +164,7 @@ public class PermissionController(
|
|||||||
/// Get all groups for an actor
|
/// Get all groups for an actor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[HttpGet("actors/{actor}/groups")]
|
[HttpGet("actors/{actor}/groups")]
|
||||||
[RequiredPermission("maintenance", "permissions.groups.check")]
|
[AskPermission("permissions.groups.check")]
|
||||||
[ProducesResponseType<List<SnPermissionGroupMember>>(StatusCodes.Status200OK)]
|
[ProducesResponseType<List<SnPermissionGroupMember>>(StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||||
@@ -183,8 +191,8 @@ public class PermissionController(
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Add an actor to a permission group
|
/// Add an actor to a permission group
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[HttpPost("actors/{actor}/groups/{groupId}")]
|
[HttpPost("actors/{actor}/groups/{groupId:guid}")]
|
||||||
[RequiredPermission("maintenance", "permissions.groups.manage")]
|
[AskPermission("permissions.groups.manage")]
|
||||||
[ProducesResponseType<SnPermissionGroupMember>(StatusCodes.Status201Created)]
|
[ProducesResponseType<SnPermissionGroupMember>(StatusCodes.Status201Created)]
|
||||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
@@ -192,7 +200,8 @@ public class PermissionController(
|
|||||||
public async Task<IActionResult> AddActorToGroup(
|
public async Task<IActionResult> AddActorToGroup(
|
||||||
string actor,
|
string actor,
|
||||||
Guid groupId,
|
Guid groupId,
|
||||||
[FromBody] GroupMembershipRequest? request = null)
|
[FromBody] GroupMembershipRequest? request = null
|
||||||
|
)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -238,7 +247,7 @@ public class PermissionController(
|
|||||||
/// Remove an actor from a permission group
|
/// Remove an actor from a permission group
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[HttpDelete("actors/{actor}/groups/{groupId}")]
|
[HttpDelete("actors/{actor}/groups/{groupId}")]
|
||||||
[RequiredPermission("maintenance", "permissions.groups.manage")]
|
[AskPermission("permissions.groups.manage")]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||||
@@ -272,7 +281,7 @@ public class PermissionController(
|
|||||||
/// Clear permission cache for an actor
|
/// Clear permission cache for an actor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[HttpPost("actors/{actor}/cache/clear")]
|
[HttpPost("actors/{actor}/cache/clear")]
|
||||||
[RequiredPermission("maintenance", "permissions.cache.manage")]
|
[AskPermission("permissions.cache.manage")]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||||
@@ -297,7 +306,7 @@ public class PermissionController(
|
|||||||
/// Validate a permission pattern
|
/// Validate a permission pattern
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[HttpPost("validate-pattern")]
|
[HttpPost("validate-pattern")]
|
||||||
[RequiredPermission("maintenance", "permissions.check")]
|
[AskPermission("permissions.check")]
|
||||||
[ProducesResponseType<PatternValidationResponse>(StatusCodes.Status200OK)]
|
[ProducesResponseType<PatternValidationResponse>(StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||||
public IActionResult ValidatePattern([FromBody] PatternValidationRequest request)
|
public IActionResult ValidatePattern([FromBody] PatternValidationRequest request)
|
||||||
@@ -322,14 +331,14 @@ public class PermissionController(
|
|||||||
public class PermissionRequest
|
public class PermissionRequest
|
||||||
{
|
{
|
||||||
public object? Value { get; set; }
|
public object? Value { get; set; }
|
||||||
public NodaTime.Instant? ExpiredAt { get; set; }
|
public Instant? ExpiredAt { get; set; }
|
||||||
public NodaTime.Instant? AffectedAt { get; set; }
|
public Instant? AffectedAt { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class GroupMembershipRequest
|
public class GroupMembershipRequest
|
||||||
{
|
{
|
||||||
public NodaTime.Instant? ExpiredAt { get; set; }
|
public Instant? ExpiredAt { get; set; }
|
||||||
public NodaTime.Instant? AffectedAt { get; set; }
|
public Instant? AffectedAt { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class PatternValidationRequest
|
public class PatternValidationRequest
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using DysonNetwork.Pass.Permission;
|
using DysonNetwork.Pass.Permission;
|
||||||
|
using DysonNetwork.Shared.Auth;
|
||||||
using DysonNetwork.Shared.Models;
|
using DysonNetwork.Shared.Models;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
@@ -51,7 +52,7 @@ public class SnAbuseReportController(
|
|||||||
|
|
||||||
[HttpGet("")]
|
[HttpGet("")]
|
||||||
[Authorize]
|
[Authorize]
|
||||||
[RequiredPermission("safety", "reports.view")]
|
[AskPermission("reports.view")]
|
||||||
[ProducesResponseType<List<SnAbuseReport>>(StatusCodes.Status200OK)]
|
[ProducesResponseType<List<SnAbuseReport>>(StatusCodes.Status200OK)]
|
||||||
public async Task<ActionResult<List<SnAbuseReport>>> GetReports(
|
public async Task<ActionResult<List<SnAbuseReport>>> GetReports(
|
||||||
[FromQuery] int offset = 0,
|
[FromQuery] int offset = 0,
|
||||||
@@ -85,7 +86,7 @@ public class SnAbuseReportController(
|
|||||||
|
|
||||||
[HttpGet("{id}")]
|
[HttpGet("{id}")]
|
||||||
[Authorize]
|
[Authorize]
|
||||||
[RequiredPermission("safety", "reports.view")]
|
[AskPermission("reports.view")]
|
||||||
[ProducesResponseType<SnAbuseReport>(StatusCodes.Status200OK)]
|
[ProducesResponseType<SnAbuseReport>(StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
public async Task<ActionResult<SnAbuseReport>> GetReportById(Guid id)
|
public async Task<ActionResult<SnAbuseReport>> GetReportById(Guid id)
|
||||||
@@ -122,7 +123,7 @@ public class SnAbuseReportController(
|
|||||||
|
|
||||||
[HttpPost("{id}/resolve")]
|
[HttpPost("{id}/resolve")]
|
||||||
[Authorize]
|
[Authorize]
|
||||||
[RequiredPermission("safety", "reports.resolve")]
|
[AskPermission("reports.resolve")]
|
||||||
[ProducesResponseType<SnAbuseReport>(StatusCodes.Status200OK)]
|
[ProducesResponseType<SnAbuseReport>(StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
public async Task<ActionResult<SnAbuseReport>> ResolveReport(Guid id, [FromBody] ResolveReportRequest request)
|
public async Task<ActionResult<SnAbuseReport>> ResolveReport(Guid id, [FromBody] ResolveReportRequest request)
|
||||||
@@ -144,7 +145,7 @@ public class SnAbuseReportController(
|
|||||||
|
|
||||||
[HttpGet("count")]
|
[HttpGet("count")]
|
||||||
[Authorize]
|
[Authorize]
|
||||||
[RequiredPermission("safety", "reports.view")]
|
[AskPermission("reports.view")]
|
||||||
[ProducesResponseType<object>(StatusCodes.Status200OK)]
|
[ProducesResponseType<object>(StatusCodes.Status200OK)]
|
||||||
public async Task<ActionResult<object>> GetReportsCount()
|
public async Task<ActionResult<object>> GetReportsCount()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ public static class ApplicationConfiguration
|
|||||||
app.UseWebSockets();
|
app.UseWebSockets();
|
||||||
app.UseAuthentication();
|
app.UseAuthentication();
|
||||||
app.UseAuthorization();
|
app.UseAuthorization();
|
||||||
app.UseMiddleware<PermissionMiddleware>();
|
app.UseMiddleware<LocalPermissionMiddleware>();
|
||||||
|
|
||||||
app.MapControllers().RequireRateLimiting("fixed");
|
app.MapControllers().RequireRateLimiting("fixed");
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using DysonNetwork.Pass.Auth;
|
using DysonNetwork.Pass.Auth;
|
||||||
using DysonNetwork.Pass.Permission;
|
using DysonNetwork.Pass.Permission;
|
||||||
|
using DysonNetwork.Shared.Auth;
|
||||||
using DysonNetwork.Shared.Cache;
|
using DysonNetwork.Shared.Cache;
|
||||||
using DysonNetwork.Shared.Models;
|
using DysonNetwork.Shared.Models;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
@@ -196,7 +197,7 @@ public class WalletController(
|
|||||||
|
|
||||||
[HttpPost("balance")]
|
[HttpPost("balance")]
|
||||||
[Authorize]
|
[Authorize]
|
||||||
[RequiredPermission("maintenance", "wallets.balance.modify")]
|
[AskPermission("wallets.balance.modify")]
|
||||||
public async Task<ActionResult<SnWalletTransaction>> ModifyWalletBalance([FromBody] WalletBalanceRequest request)
|
public async Task<ActionResult<SnWalletTransaction>> ModifyWalletBalance([FromBody] WalletBalanceRequest request)
|
||||||
{
|
{
|
||||||
var wallet = await ws.GetWalletAsync(request.AccountId);
|
var wallet = await ws.GetWalletAsync(request.AccountId);
|
||||||
|
|||||||
@@ -139,7 +139,7 @@ public class NotificationController(
|
|||||||
|
|
||||||
[HttpPost("send")]
|
[HttpPost("send")]
|
||||||
[Authorize]
|
[Authorize]
|
||||||
[RequiredPermission("global", "notifications.send")]
|
[AskPermission("notifications.send")]
|
||||||
public async Task<ActionResult> SendNotification(
|
public async Task<ActionResult> SendNotification(
|
||||||
[FromBody] NotificationWithAimRequest request,
|
[FromBody] NotificationWithAimRequest request,
|
||||||
[FromQuery] bool save = false
|
[FromQuery] bool save = false
|
||||||
|
|||||||
@@ -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;
|
namespace DysonNetwork.Shared.Models;
|
||||||
|
|
||||||
|
public enum PermissionNodeActorType
|
||||||
|
{
|
||||||
|
Account,
|
||||||
|
Group
|
||||||
|
}
|
||||||
|
|
||||||
/// The permission node model provides the infrastructure of permission control in Dyson Network.
|
/// The permission node model provides the infrastructure of permission control in Dyson Network.
|
||||||
/// It based on the ABAC permission model.
|
/// 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 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
|
/// 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.
|
/// 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 class SnPermissionNode : ModelBase, IDisposable
|
||||||
{
|
{
|
||||||
public Guid Id { get; set; } = Guid.NewGuid();
|
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 Actor { get; set; } = null!;
|
||||||
[MaxLength(1024)] public string Area { get; set; } = null!;
|
|
||||||
[MaxLength(1024)] public string Key { get; set; } = null!;
|
[MaxLength(1024)] public string Key { get; set; } = null!;
|
||||||
[Column(TypeName = "jsonb")] public JsonDocument Value { get; set; } = null!;
|
[Column(TypeName = "jsonb")] public JsonDocument Value { get; set; } = null!;
|
||||||
public Instant? ExpiredAt { get; set; } = null;
|
public Instant? ExpiredAt { get; set; } = null;
|
||||||
@@ -39,7 +45,12 @@ public class SnPermissionNode : ModelBase, IDisposable
|
|||||||
{
|
{
|
||||||
Id = Id.ToString(),
|
Id = Id.ToString(),
|
||||||
Actor = Actor,
|
Actor = Actor,
|
||||||
Area = Area,
|
Type = Type switch
|
||||||
|
{
|
||||||
|
PermissionNodeActorType.Account => Proto.PermissionNodeActorType.Account,
|
||||||
|
PermissionNodeActorType.Group => Proto.PermissionNodeActorType.Group,
|
||||||
|
_ => throw new ArgumentOutOfRangeException()
|
||||||
|
},
|
||||||
Key = Key,
|
Key = Key,
|
||||||
Value = Google.Protobuf.WellKnownTypes.Value.Parser.ParseJson(Value.RootElement.GetRawText()),
|
Value = Google.Protobuf.WellKnownTypes.Value.Parser.ParseJson(Value.RootElement.GetRawText()),
|
||||||
ExpiredAt = ExpiredAt?.ToTimestamp(),
|
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()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
Value.Dispose();
|
Value.Dispose();
|
||||||
|
|||||||
@@ -106,11 +106,16 @@ message ValidateResponse {
|
|||||||
bool valid = 1;
|
bool valid = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum PermissionNodeActorType {
|
||||||
|
ACCOUNT = 0;
|
||||||
|
GROUP = 1;
|
||||||
|
}
|
||||||
|
|
||||||
// Permission related messages and services
|
// Permission related messages and services
|
||||||
message PermissionNode {
|
message PermissionNode {
|
||||||
string id = 1;
|
string id = 1;
|
||||||
string actor = 2;
|
string actor = 2;
|
||||||
string area = 3;
|
PermissionNodeActorType type = 3;
|
||||||
string key = 4;
|
string key = 4;
|
||||||
google.protobuf.Value value = 5; // Using Value to represent generic type
|
google.protobuf.Value value = 5; // Using Value to represent generic type
|
||||||
google.protobuf.Timestamp expired_at = 6;
|
google.protobuf.Timestamp expired_at = 6;
|
||||||
@@ -126,8 +131,8 @@ message PermissionGroup {
|
|||||||
|
|
||||||
message HasPermissionRequest {
|
message HasPermissionRequest {
|
||||||
string actor = 1;
|
string actor = 1;
|
||||||
string area = 2;
|
string key = 2;
|
||||||
string key = 3;
|
optional PermissionNodeActorType type = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
message HasPermissionResponse {
|
message HasPermissionResponse {
|
||||||
@@ -136,7 +141,7 @@ message HasPermissionResponse {
|
|||||||
|
|
||||||
message GetPermissionRequest {
|
message GetPermissionRequest {
|
||||||
string actor = 1;
|
string actor = 1;
|
||||||
string area = 2;
|
optional PermissionNodeActorType type = 2;
|
||||||
string key = 3;
|
string key = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,7 +151,7 @@ message GetPermissionResponse {
|
|||||||
|
|
||||||
message AddPermissionNodeRequest {
|
message AddPermissionNodeRequest {
|
||||||
string actor = 1;
|
string actor = 1;
|
||||||
string area = 2;
|
optional PermissionNodeActorType type = 2;
|
||||||
string key = 3;
|
string key = 3;
|
||||||
google.protobuf.Value value = 4;
|
google.protobuf.Value value = 4;
|
||||||
google.protobuf.Timestamp expired_at = 5;
|
google.protobuf.Timestamp expired_at = 5;
|
||||||
@@ -160,7 +165,7 @@ message AddPermissionNodeResponse {
|
|||||||
message AddPermissionNodeToGroupRequest {
|
message AddPermissionNodeToGroupRequest {
|
||||||
PermissionGroup group = 1;
|
PermissionGroup group = 1;
|
||||||
string actor = 2;
|
string actor = 2;
|
||||||
string area = 3;
|
optional PermissionNodeActorType type = 3;
|
||||||
string key = 4;
|
string key = 4;
|
||||||
google.protobuf.Value value = 5;
|
google.protobuf.Value value = 5;
|
||||||
google.protobuf.Timestamp expired_at = 6;
|
google.protobuf.Timestamp expired_at = 6;
|
||||||
@@ -173,7 +178,7 @@ message AddPermissionNodeToGroupResponse {
|
|||||||
|
|
||||||
message RemovePermissionNodeRequest {
|
message RemovePermissionNodeRequest {
|
||||||
string actor = 1;
|
string actor = 1;
|
||||||
string area = 2;
|
optional PermissionNodeActorType type = 2;
|
||||||
string key = 3;
|
string key = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -184,7 +189,6 @@ message RemovePermissionNodeResponse {
|
|||||||
message RemovePermissionNodeFromGroupRequest {
|
message RemovePermissionNodeFromGroupRequest {
|
||||||
PermissionGroup group = 1;
|
PermissionGroup group = 1;
|
||||||
string actor = 2;
|
string actor = 2;
|
||||||
string area = 3;
|
|
||||||
string key = 4;
|
string key = 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -243,7 +243,7 @@ public partial class ChatController(
|
|||||||
|
|
||||||
[HttpPost("{roomId:guid}/messages")]
|
[HttpPost("{roomId:guid}/messages")]
|
||||||
[Authorize]
|
[Authorize]
|
||||||
[RequiredPermission("global", "chat.messages.create")]
|
[AskPermission("chat.messages.create")]
|
||||||
public async Task<ActionResult> SendMessage([FromBody] SendMessageRequest request, Guid roomId)
|
public async Task<ActionResult> SendMessage([FromBody] SendMessageRequest request, Guid roomId)
|
||||||
{
|
{
|
||||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||||
|
|||||||
@@ -179,7 +179,7 @@ public class ChatRoomController(
|
|||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
[Authorize]
|
[Authorize]
|
||||||
[RequiredPermission("global", "chat.create")]
|
[AskPermission("chat.create")]
|
||||||
public async Task<ActionResult<SnChatRoom>> CreateChatRoom(ChatRoomRequest request)
|
public async Task<ActionResult<SnChatRoom>> CreateChatRoom(ChatRoomRequest request)
|
||||||
{
|
{
|
||||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||||
|
|||||||
@@ -525,7 +525,7 @@ public class PostController(
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
[RequiredPermission("global", "posts.create")]
|
[AskPermission("posts.create")]
|
||||||
public async Task<ActionResult<SnPost>> CreatePost(
|
public async Task<ActionResult<SnPost>> CreatePost(
|
||||||
[FromBody] PostRequest request,
|
[FromBody] PostRequest request,
|
||||||
[FromQuery(Name = "pub")] string? pubName
|
[FromQuery(Name = "pub")] string? pubName
|
||||||
@@ -725,7 +725,7 @@ public class PostController(
|
|||||||
|
|
||||||
[HttpPost("{id:guid}/reactions")]
|
[HttpPost("{id:guid}/reactions")]
|
||||||
[Authorize]
|
[Authorize]
|
||||||
[RequiredPermission("global", "posts.react")]
|
[AskPermission("posts.react")]
|
||||||
public async Task<ActionResult<SnPostReaction>> ReactPost(
|
public async Task<ActionResult<SnPostReaction>> ReactPost(
|
||||||
Guid id,
|
Guid id,
|
||||||
[FromBody] PostReactionRequest request
|
[FromBody] PostReactionRequest request
|
||||||
|
|||||||
@@ -341,7 +341,7 @@ public class PublisherController(
|
|||||||
|
|
||||||
[HttpPost("individual")]
|
[HttpPost("individual")]
|
||||||
[Authorize]
|
[Authorize]
|
||||||
[RequiredPermission("global", "publishers.create")]
|
[AskPermission("publishers.create")]
|
||||||
public async Task<ActionResult<SnPublisher>> CreatePublisherIndividual(
|
public async Task<ActionResult<SnPublisher>> CreatePublisherIndividual(
|
||||||
[FromBody] PublisherRequest request
|
[FromBody] PublisherRequest request
|
||||||
)
|
)
|
||||||
@@ -426,7 +426,7 @@ public class PublisherController(
|
|||||||
|
|
||||||
[HttpPost("organization/{realmSlug}")]
|
[HttpPost("organization/{realmSlug}")]
|
||||||
[Authorize]
|
[Authorize]
|
||||||
[RequiredPermission("global", "publishers.create")]
|
[AskPermission("publishers.create")]
|
||||||
public async Task<ActionResult<SnPublisher>> CreatePublisherOrganization(
|
public async Task<ActionResult<SnPublisher>> CreatePublisherOrganization(
|
||||||
string realmSlug,
|
string realmSlug,
|
||||||
[FromBody] PublisherRequest request
|
[FromBody] PublisherRequest request
|
||||||
@@ -833,7 +833,7 @@ public class PublisherController(
|
|||||||
|
|
||||||
[HttpPost("{name}/features")]
|
[HttpPost("{name}/features")]
|
||||||
[Authorize]
|
[Authorize]
|
||||||
[RequiredPermission("maintenance", "publishers.features")]
|
[AskPermission("publishers.features")]
|
||||||
public async Task<ActionResult<PublisherFeature>> AddPublisherFeature(
|
public async Task<ActionResult<PublisherFeature>> AddPublisherFeature(
|
||||||
string name,
|
string name,
|
||||||
[FromBody] PublisherFeatureRequest request
|
[FromBody] PublisherFeatureRequest request
|
||||||
@@ -858,7 +858,7 @@ public class PublisherController(
|
|||||||
|
|
||||||
[HttpDelete("{name}/features/{flag}")]
|
[HttpDelete("{name}/features/{flag}")]
|
||||||
[Authorize]
|
[Authorize]
|
||||||
[RequiredPermission("maintenance", "publishers.features")]
|
[AskPermission("publishers.features")]
|
||||||
public async Task<ActionResult> RemovePublisherFeature(string name, string flag)
|
public async Task<ActionResult> RemovePublisherFeature(string name, string flag)
|
||||||
{
|
{
|
||||||
var publisher = await db.Publishers.Where(p => p.Name == name).FirstOrDefaultAsync();
|
var publisher = await db.Publishers.Where(p => p.Name == name).FirstOrDefaultAsync();
|
||||||
@@ -880,7 +880,7 @@ public class PublisherController(
|
|||||||
|
|
||||||
[HttpPost("rewards/settle")]
|
[HttpPost("rewards/settle")]
|
||||||
[Authorize]
|
[Authorize]
|
||||||
[RequiredPermission("maintenance", "publishers.reward.settle")]
|
[AskPermission("publishers.reward.settle")]
|
||||||
public async Task<IActionResult> PerformLotteryDraw()
|
public async Task<IActionResult> PerformLotteryDraw()
|
||||||
{
|
{
|
||||||
await ps.SettlePublisherRewards();
|
await ps.SettlePublisherRewards();
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ public static class ApplicationConfiguration
|
|||||||
app.UseWebSockets();
|
app.UseWebSockets();
|
||||||
app.UseAuthentication();
|
app.UseAuthentication();
|
||||||
app.UseAuthorization();
|
app.UseAuthorization();
|
||||||
app.UseMiddleware<PermissionMiddleware>();
|
app.UseMiddleware<RemotePermissionMiddleware>();
|
||||||
|
|
||||||
app.MapControllers();
|
app.MapControllers();
|
||||||
|
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ public class StickerController(
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
[RequiredPermission("global", "stickers.packs.create")]
|
[AskPermission("stickers.packs.create")]
|
||||||
public async Task<ActionResult<StickerPack>> CreateStickerPack(
|
public async Task<ActionResult<StickerPack>> CreateStickerPack(
|
||||||
[FromBody] StickerPackRequest request,
|
[FromBody] StickerPackRequest request,
|
||||||
[FromQuery(Name = "pub")] string publisherName
|
[FromQuery(Name = "pub")] string publisherName
|
||||||
@@ -334,7 +334,7 @@ public class StickerController(
|
|||||||
public const int MaxStickersPerPack = 24;
|
public const int MaxStickersPerPack = 24;
|
||||||
|
|
||||||
[HttpPost("{packId:guid}/content")]
|
[HttpPost("{packId:guid}/content")]
|
||||||
[RequiredPermission("global", "stickers.create")]
|
[AskPermission("stickers.create")]
|
||||||
public async Task<IActionResult> CreateSticker(Guid packId, [FromBody] StickerRequest request)
|
public async Task<IActionResult> CreateSticker(Guid packId, [FromBody] StickerRequest request)
|
||||||
{
|
{
|
||||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser)
|
if (HttpContext.Items["CurrentUser"] is not Account currentUser)
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ public class WebReaderController(WebReaderService reader, ILogger<WebReaderContr
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
[HttpDelete("link/cache")]
|
[HttpDelete("link/cache")]
|
||||||
[Authorize]
|
[Authorize]
|
||||||
[RequiredPermission("maintenance", "cache.scrap")]
|
[AskPermission("cache.scrap")]
|
||||||
public async Task<IActionResult> InvalidateCache([FromQuery] string url)
|
public async Task<IActionResult> InvalidateCache([FromQuery] string url)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(url))
|
if (string.IsNullOrEmpty(url))
|
||||||
@@ -76,7 +76,7 @@ public class WebReaderController(WebReaderService reader, ILogger<WebReaderContr
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
[HttpDelete("cache/all")]
|
[HttpDelete("cache/all")]
|
||||||
[Authorize]
|
[Authorize]
|
||||||
[RequiredPermission("maintenance", "cache.scrap")]
|
[AskPermission("cache.scrap")]
|
||||||
public async Task<IActionResult> InvalidateAllCache()
|
public async Task<IActionResult> InvalidateAllCache()
|
||||||
{
|
{
|
||||||
await reader.InvalidateAllCachedPreviewsAsync();
|
await reader.InvalidateAllCachedPreviewsAsync();
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ public static class ApplicationConfiguration
|
|||||||
app.UseWebSockets();
|
app.UseWebSockets();
|
||||||
app.UseAuthentication();
|
app.UseAuthentication();
|
||||||
app.UseAuthorization();
|
app.UseAuthorization();
|
||||||
app.UseMiddleware<PermissionMiddleware>();
|
app.UseMiddleware<RemotePermissionMiddleware>();
|
||||||
|
|
||||||
app.MapControllers();
|
app.MapControllers();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user