💥 Simplified permission node system and data structure

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

View File

@@ -1,17 +1,12 @@
using DysonNetwork.Shared.Auth;
namespace DysonNetwork.Pass.Permission;
using System;
using Microsoft.Extensions.Logging;
using DysonNetwork.Shared.Models;
using Shared.Models;
[AttributeUsage(AttributeTargets.Method)]
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)
public class LocalPermissionMiddleware(RequestDelegate next, ILogger<LocalPermissionMiddleware> logger)
{
private const string ForbiddenMessage = "Insufficient permissions";
private const string UnauthorizedMessage = "Authentication required";
@@ -21,15 +16,15 @@ public class PermissionMiddleware(RequestDelegate next, ILogger<PermissionMiddle
var endpoint = httpContext.GetEndpoint();
var attr = endpoint?.Metadata
.OfType<RequiredPermissionAttribute>()
.OfType<AskPermissionAttribute>()
.FirstOrDefault();
if (attr != null)
{
// 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;
await httpContext.Response.WriteAsync("Server configuration error");
return;
@@ -37,7 +32,7 @@ public class PermissionMiddleware(RequestDelegate next, ILogger<PermissionMiddle
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;
await httpContext.Response.WriteAsync(UnauthorizedMessage);
return;
@@ -46,33 +41,29 @@ public class PermissionMiddleware(RequestDelegate next, ILogger<PermissionMiddle
if (currentUser.IsSuperuser)
{
// Bypass the permission check for performance
logger.LogDebug("Superuser {UserId} bypassing permission check for {Area}/{Key}",
currentUser.Id, attr.Area, attr.Key);
logger.LogDebug("Superuser {UserId} bypassing permission check for {Key}", currentUser.Id, attr.Key);
await next(httpContext);
return;
}
var actor = $"user:{currentUser.Id}";
var actor = currentUser.Id.ToString();
try
{
var permNode = await pm.GetPermissionAsync<bool>(actor, attr.Area, attr.Key);
var permNode = await pm.GetPermissionAsync<bool>(actor, attr.Key);
if (!permNode)
{
logger.LogWarning("Permission denied for user {UserId}: {Area}/{Key}",
currentUser.Id, attr.Area, attr.Key);
logger.LogWarning("Permission denied for user {UserId}: {Key}", currentUser.Id, attr.Key);
httpContext.Response.StatusCode = StatusCodes.Status403Forbidden;
await httpContext.Response.WriteAsync(ForbiddenMessage);
return;
}
logger.LogDebug("Permission granted for user {UserId}: {Area}/{Key}",
currentUser.Id, attr.Area, attr.Key);
logger.LogDebug("Permission granted for user {UserId}: {Key}", currentUser.Id, attr.Key);
}
catch (Exception ex)
{
logger.LogError(ex, "Error checking permission for user {UserId}: {Area}/{Key}",
currentUser.Id, attr.Area, attr.Key);
logger.LogError(ex, "Error checking permission for user {UserId}: {Key}", currentUser.Id, attr.Key);
httpContext.Response.StatusCode = StatusCodes.Status500InternalServerError;
await httpContext.Response.WriteAsync("Permission check failed");
return;

View File

@@ -4,6 +4,7 @@ using Microsoft.Extensions.Options;
using NodaTime;
using System.Text.Json;
using DysonNetwork.Shared.Cache;
using DysonNetwork.Shared.Data;
using DysonNetwork.Shared.Models;
namespace DysonNetwork.Pass.Permission;
@@ -28,8 +29,8 @@ public class PermissionService(
private const string PermissionGroupCacheKeyPrefix = "perm-cg:";
private const string PermissionGroupPrefix = "perm-g:";
private static string GetPermissionCacheKey(string actor, string area, string key) =>
PermissionCacheKeyPrefix + actor + ":" + area + ":" + key;
private static string GetPermissionCacheKey(string actor, string key) =>
PermissionCacheKeyPrefix + actor + ":" + key;
private static string GetGroupsCacheKey(string actor) =>
PermissionGroupCacheKeyPrefix + actor;
@@ -37,50 +38,56 @@ public class PermissionService(
private static string GetPermissionGroupKey(string 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;
}
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
if (string.IsNullOrWhiteSpace(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))
throw new ArgumentException("Key cannot be null or empty", nameof(key));
var cacheKey = GetPermissionCacheKey(actor, area, key);
var cacheKey = GetPermissionCacheKey(actor, key);
try
{
var (hit, cachedValue) = await cache.GetAsyncWithStatus<T>(cacheKey);
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;
}
var now = SystemClock.Instance.GetCurrentInstant();
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;
await cache.SetWithGroupsAsync(cacheKey, result,
[GetPermissionGroupKey(actor)],
_options.CacheExpiration);
logger.LogDebug("Permission resolved for {Actor}:{Area}:{Key} = {Result}",
actor, area, key, result != null);
logger.LogDebug("Permission resolved for {Type}:{Actor}:{Key} = {Result}", type, actor, key,
result != null);
return result;
}
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;
}
}
@@ -109,33 +116,34 @@ public class PermissionService(
return groupsId;
}
private async Task<SnPermissionNode?> FindPermissionNodeAsync(string actor, string area, string key,
List<Guid> groupsId, Instant now)
private async Task<SnPermissionNode?> FindPermissionNodeAsync(
PermissionNodeActorType type,
string actor,
string key,
List<Guid> groupsId
)
{
var now = SystemClock.Instance.GetCurrentInstant();
// First try exact match (highest priority)
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)))
.Where(n => n.Key == key && n.Area == area)
.Where(n => n.Key == key)
.Where(n => n.ExpiredAt == null || n.ExpiredAt > now)
.Where(n => n.AffectedAt == null || n.AffectedAt <= now)
.FirstOrDefaultAsync();
if (exactMatch != null)
{
return exactMatch;
}
// If no exact match and wildcards are enabled, try wildcard matches
if (!_options.EnableWildcardMatching)
{
return null;
}
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)))
.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.AffectedAt == null || n.AffectedAt <= now)
.Take(_options.MaxWildcardMatches)
@@ -147,36 +155,21 @@ public class PermissionService(
foreach (var node in wildcardMatches)
{
var score = CalculateWildcardMatchScore(node.Area, node.Key, area, key);
if (score > bestMatchScore)
{
bestMatch = node;
bestMatchScore = score;
}
var score = CalculateWildcardMatchScore(node.Key, key);
if (score <= bestMatchScore) continue;
bestMatch = node;
bestMatchScore = score;
}
if (bestMatch != null)
{
logger.LogDebug("Found wildcard permission match: {NodeArea}:{NodeKey} for {Area}:{Key}",
bestMatch.Area, bestMatch.Key, area, key);
}
logger.LogDebug("Found wildcard permission match: {NodeKey} for {Key}", bestMatch.Key, key);
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
// 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;
return CalculatePatternMatchScore(nodeKey, targetKey);
}
private static int CalculatePatternMatchScore(string pattern, string target)
@@ -184,31 +177,30 @@ public class PermissionService(
if (pattern == target)
return int.MaxValue; // Exact match
if (!pattern.Contains("*"))
if (!pattern.Contains('*'))
return -1; // No wildcard, not a match
// Simple wildcard matching: * matches any sequence of characters
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))
{
// Score based on specificity (shorter patterns are less specific)
var wildcardCount = pattern.Count(c => c == '*');
var length = pattern.Length;
return Math.Max(1, 1000 - (wildcardCount * 100) - length);
}
if (!regex.IsMatch(target)) return -1; // No match
return -1; // No match
// Score based on specificity (shorter patterns are less specific)
var wildcardCount = pattern.Count(c => c == '*');
var length = pattern.Length;
return Math.Max(1, 1000 - wildcardCount * 100 - length);
}
public async Task<SnPermissionNode> AddPermissionNode<T>(
string actor,
string area,
string key,
T value,
Instant? expiredAt = null,
Instant? affectedAt = null
Instant? affectedAt = null,
PermissionNodeActorType type = PermissionNodeActorType.Account
)
{
if (value is null) throw new ArgumentNullException(nameof(value));
@@ -216,8 +208,8 @@ public class PermissionService(
var node = new SnPermissionNode
{
Actor = actor,
Type = type,
Key = key,
Area = area,
Value = SerializePermissionValue(value),
ExpiredAt = expiredAt,
AffectedAt = affectedAt
@@ -227,7 +219,7 @@ public class PermissionService(
await db.SaveChangesAsync();
// Invalidate related caches
await InvalidatePermissionCacheAsync(actor, area, key);
await InvalidatePermissionCacheAsync(actor, key);
return node;
}
@@ -235,11 +227,11 @@ public class PermissionService(
public async Task<SnPermissionNode> AddPermissionNodeToGroup<T>(
SnPermissionGroup group,
string actor,
string area,
string key,
T value,
Instant? expiredAt = null,
Instant? affectedAt = null
Instant? affectedAt = null,
PermissionNodeActorType type = PermissionNodeActorType.Account
)
{
if (value is null) throw new ArgumentNullException(nameof(value));
@@ -247,8 +239,8 @@ public class PermissionService(
var node = new SnPermissionNode
{
Actor = actor,
Type = type,
Key = key,
Area = area,
Value = SerializePermissionValue(value),
ExpiredAt = expiredAt,
AffectedAt = affectedAt,
@@ -260,44 +252,45 @@ public class PermissionService(
await db.SaveChangesAsync();
// Invalidate related caches
await InvalidatePermissionCacheAsync(actor, area, key);
await InvalidatePermissionCacheAsync(actor, key);
await cache.RemoveAsync(GetGroupsCacheKey(actor));
await cache.RemoveGroupAsync(GetPermissionGroupKey(actor));
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
.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();
if (node is not null) db.PermissionNodes.Remove(node);
await db.SaveChangesAsync();
// 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
.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();
if (node is null) return;
db.PermissionNodes.Remove(node);
await db.SaveChangesAsync();
// Invalidate caches
await InvalidatePermissionCacheAsync(actor, area, key);
await InvalidatePermissionCacheAsync(actor, key);
await cache.RemoveAsync(GetGroupsCacheKey(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);
}
@@ -312,12 +305,11 @@ public class PermissionService(
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
{
Actor = actor,
Area = area,
Key = key,
Value = SerializePermissionValue(value),
};
@@ -341,8 +333,7 @@ public class PermissionService(
(n.GroupId != null && groupsId.Contains(n.GroupId.Value)))
.Where(n => n.ExpiredAt == null || n.ExpiredAt > now)
.Where(n => n.AffectedAt == null || n.AffectedAt <= now)
.OrderBy(n => n.Area)
.ThenBy(n => n.Key)
.OrderBy(n => n.Key)
.ToListAsync();
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.ExpiredAt == null || n.ExpiredAt > now)
.Where(n => n.AffectedAt == null || n.AffectedAt <= now)
.OrderBy(n => n.Area)
.ThenBy(n => n.Key)
.OrderBy(n => n.Key)
.ToListAsync();
logger.LogDebug("Listed {Count} direct permissions for actor {Actor}", permissions.Count, actor);
@@ -424,4 +414,4 @@ public class PermissionService(
throw;
}
}
}
}

View File

@@ -9,31 +9,33 @@ using NodaTime.Serialization.Protobuf;
namespace DysonNetwork.Pass.Permission;
public class PermissionServiceGrpc(
PermissionService permissionService,
PermissionService psv,
AppDatabase db,
ILogger<PermissionServiceGrpc> logger
) : DysonNetwork.Shared.Proto.PermissionService.PermissionServiceBase
{
public override async Task<HasPermissionResponse> HasPermission(HasPermissionRequest request, ServerCallContext context)
{
var type = SnPermissionNode.ConvertProtoActorType(request.Type);
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 };
}
catch (Exception ex)
{
logger.LogError(ex, "Error checking permission for actor {Actor}, area {Area}, key {Key}",
request.Actor, request.Area, request.Key);
logger.LogError(ex, "Error checking permission for {Type}:{Area}:{Key}",
type, request.Actor, request.Key);
throw new RpcException(new Status(StatusCode.Internal, "Permission check failed"));
}
}
public override async Task<GetPermissionResponse> GetPermission(GetPermissionRequest request, ServerCallContext context)
{
var type = SnPermissionNode.ConvertProtoActorType(request.Type);
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
{
Value = permissionValue != null ? Value.Parser.ParseJson(permissionValue.RootElement.GetRawText()) : null
@@ -41,14 +43,15 @@ public class PermissionServiceGrpc(
}
catch (Exception ex)
{
logger.LogError(ex, "Error getting permission for actor {Actor}, area {Area}, key {Key}",
request.Actor, request.Area, request.Key);
logger.LogError(ex, "Error getting permission for {Type}:{Area}:{Key}",
type, request.Actor, request.Key);
throw new RpcException(new Status(StatusCode.Internal, "Failed to retrieve permission"));
}
}
public override async Task<AddPermissionNodeResponse> AddPermissionNode(AddPermissionNodeRequest request, ServerCallContext context)
{
var type = SnPermissionNode.ConvertProtoActorType(request.Type);
try
{
JsonDocument jsonValue;
@@ -58,18 +61,18 @@ public class PermissionServiceGrpc(
}
catch (JsonException ex)
{
logger.LogWarning(ex, "Invalid JSON in permission value for actor {Actor}, area {Area}, key {Key}",
request.Actor, request.Area, request.Key);
logger.LogError(ex, "Invalid JSON in permission value for {Type}:{Area}:{Key}",
type, request.Actor, request.Key);
throw new RpcException(new Status(StatusCode.InvalidArgument, "Invalid permission value format"));
}
var node = await permissionService.AddPermissionNode(
var node = await psv.AddPermissionNode(
request.Actor,
request.Area,
request.Key,
jsonValue,
request.ExpiredAt?.ToInstant(),
request.AffectedAt?.ToInstant()
request.AffectedAt?.ToInstant(),
type
);
return new AddPermissionNodeResponse { Node = node.ToProtoValue() };
}
@@ -79,14 +82,15 @@ public class PermissionServiceGrpc(
}
catch (Exception ex)
{
logger.LogError(ex, "Error adding permission node for actor {Actor}, area {Area}, key {Key}",
request.Actor, request.Area, request.Key);
logger.LogError(ex, "Error adding permission for {Type}:{Area}:{Key}",
type, request.Actor, request.Key);
throw new RpcException(new Status(StatusCode.Internal, "Failed to add permission node"));
}
}
public override async Task<AddPermissionNodeToGroupResponse> AddPermissionNodeToGroup(AddPermissionNodeToGroupRequest request, ServerCallContext context)
{
var type = SnPermissionNode.ConvertProtoActorType(request.Type);
try
{
var group = await FindPermissionGroupAsync(request.Group.Id);
@@ -102,19 +106,19 @@ public class PermissionServiceGrpc(
}
catch (JsonException ex)
{
logger.LogWarning(ex, "Invalid JSON in permission value for group {GroupId}, actor {Actor}, area {Area}, key {Key}",
request.Group.Id, request.Actor, request.Area, request.Key);
logger.LogError(ex, "Invalid JSON in permission value for {Type}:{Area}:{Key}",
type, request.Actor, request.Key);
throw new RpcException(new Status(StatusCode.InvalidArgument, "Invalid permission value format"));
}
var node = await permissionService.AddPermissionNodeToGroup(
var node = await psv.AddPermissionNodeToGroup(
group,
request.Actor,
request.Area,
request.Key,
jsonValue,
request.ExpiredAt?.ToInstant(),
request.AffectedAt?.ToInstant()
request.AffectedAt?.ToInstant(),
type
);
return new AddPermissionNodeToGroupResponse { Node = node.ToProtoValue() };
}
@@ -124,23 +128,24 @@ public class PermissionServiceGrpc(
}
catch (Exception ex)
{
logger.LogError(ex, "Error adding permission node to group {GroupId} for actor {Actor}, area {Area}, key {Key}",
request.Group.Id, request.Actor, request.Area, request.Key);
logger.LogError(ex, "Error adding permission for {Type}:{Area}:{Key}",
type, request.Actor, request.Key);
throw new RpcException(new Status(StatusCode.Internal, "Failed to add permission node to group"));
}
}
public override async Task<RemovePermissionNodeResponse> RemovePermissionNode(RemovePermissionNodeRequest request, ServerCallContext context)
{
var type = SnPermissionNode.ConvertProtoActorType(request.Type);
try
{
await permissionService.RemovePermissionNode(request.Actor, request.Area, request.Key);
await psv.RemovePermissionNode(request.Actor, request.Key, type);
return new RemovePermissionNodeResponse { Success = true };
}
catch (Exception ex)
{
logger.LogError(ex, "Error removing permission node for actor {Actor}, area {Area}, key {Key}",
request.Actor, request.Area, request.Key);
logger.LogError(ex, "Error removing permission for {Type}:{Area}:{Key}",
type, request.Actor, request.Key);
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"));
}
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 };
}
catch (RpcException)
@@ -164,20 +169,18 @@ public class PermissionServiceGrpc(
}
catch (Exception ex)
{
logger.LogError(ex, "Error removing permission node from group {GroupId} for actor {Actor}, area {Area}, key {Key}",
request.Group.Id, request.Actor, request.Area, request.Key);
logger.LogError(ex, "Error removing permission from group for {Area}:{Key}",
request.Actor, request.Key);
throw new RpcException(new Status(StatusCode.Internal, "Failed to remove permission node from group"));
}
}
private async Task<SnPermissionGroup?> FindPermissionGroupAsync(string groupId)
{
if (!Guid.TryParse(groupId, out var guid))
{
logger.LogWarning("Invalid GUID format for group ID: {GroupId}", groupId);
return null;
}
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);
return null;
return await db.PermissionGroups.FirstOrDefaultAsync(g => g.Id == guid);
}
}