forked from gongxuegit/tiku-backend.net
feat: harden SaaS authentication and authorization
This commit is contained in:
149
Tiku.Application/Security/ICurrentAccessContext.cs
Normal file
149
Tiku.Application/Security/ICurrentAccessContext.cs
Normal file
@@ -0,0 +1,149 @@
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Tiku.Application.Security;
|
||||
|
||||
public enum DataScopeMode
|
||||
{
|
||||
Self,
|
||||
Restricted,
|
||||
All
|
||||
}
|
||||
|
||||
public sealed record CurrentDataScope(
|
||||
DataScopeMode Mode,
|
||||
IReadOnlySet<Guid> RegionIds,
|
||||
IReadOnlySet<Guid> ClassIds,
|
||||
bool IncludesSelf)
|
||||
{
|
||||
public static CurrentDataScope Self { get; } = new(
|
||||
DataScopeMode.Self,
|
||||
new HashSet<Guid>(),
|
||||
new HashSet<Guid>(),
|
||||
true);
|
||||
|
||||
public bool AllowsResource(Guid currentUserId, Guid? ownerUserId = null, Guid? regionId = null, Guid? classId = null)
|
||||
{
|
||||
if (Mode == DataScopeMode.All)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (IncludesSelf && ownerUserId == currentUserId)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return Mode == DataScopeMode.Restricted &&
|
||||
((regionId.HasValue && RegionIds.Contains(regionId.Value)) ||
|
||||
(classId.HasValue && ClassIds.Contains(classId.Value)));
|
||||
}
|
||||
|
||||
public static CurrentDataScope Merge(IEnumerable<JsonElement> roleScopes)
|
||||
{
|
||||
var regionIds = new HashSet<Guid>();
|
||||
var classIds = new HashSet<Guid>();
|
||||
var includesSelf = false;
|
||||
var hasRestrictedScope = false;
|
||||
|
||||
foreach (var roleScope in roleScopes)
|
||||
{
|
||||
var parsed = Parse(roleScope);
|
||||
if (parsed.Mode == DataScopeMode.All)
|
||||
{
|
||||
return new CurrentDataScope(DataScopeMode.All, new HashSet<Guid>(), new HashSet<Guid>(), true);
|
||||
}
|
||||
|
||||
includesSelf |= parsed.IncludesSelf;
|
||||
hasRestrictedScope |= parsed.Mode == DataScopeMode.Restricted;
|
||||
regionIds.UnionWith(parsed.RegionIds);
|
||||
classIds.UnionWith(parsed.ClassIds);
|
||||
}
|
||||
|
||||
return hasRestrictedScope || regionIds.Count > 0 || classIds.Count > 0
|
||||
? new CurrentDataScope(DataScopeMode.Restricted, regionIds, classIds, includesSelf)
|
||||
: Self;
|
||||
}
|
||||
|
||||
private static CurrentDataScope Parse(JsonElement value)
|
||||
{
|
||||
if (value.ValueKind != JsonValueKind.Object)
|
||||
{
|
||||
return Self;
|
||||
}
|
||||
|
||||
var mode = ReadString(value, "mode") ?? ReadString(value, "type");
|
||||
if (string.Equals(mode, nameof(DataScopeMode.All), StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return new CurrentDataScope(DataScopeMode.All, new HashSet<Guid>(), new HashSet<Guid>(), true);
|
||||
}
|
||||
|
||||
if (string.Equals(mode, nameof(DataScopeMode.Self), StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return Self;
|
||||
}
|
||||
|
||||
var regions = ReadGuids(value, "regionIds");
|
||||
var classes = ReadGuids(value, "classIds");
|
||||
var restricted = string.Equals(mode, nameof(DataScopeMode.Restricted), StringComparison.OrdinalIgnoreCase) ||
|
||||
regions.Count > 0 ||
|
||||
classes.Count > 0;
|
||||
|
||||
return restricted
|
||||
? new CurrentDataScope(DataScopeMode.Restricted, regions, classes, ReadBoolean(value, "includesSelf") || ReadBoolean(value, "ownLeadsOnly"))
|
||||
: Self;
|
||||
}
|
||||
|
||||
private static string? ReadString(JsonElement value, string propertyName)
|
||||
{
|
||||
return value.TryGetProperty(propertyName, out var property) && property.ValueKind == JsonValueKind.String
|
||||
? property.GetString()
|
||||
: null;
|
||||
}
|
||||
|
||||
private static bool ReadBoolean(JsonElement value, string propertyName)
|
||||
{
|
||||
return value.TryGetProperty(propertyName, out var property) &&
|
||||
property.ValueKind is JsonValueKind.True or JsonValueKind.False &&
|
||||
property.GetBoolean();
|
||||
}
|
||||
|
||||
private static HashSet<Guid> ReadGuids(JsonElement value, string propertyName)
|
||||
{
|
||||
var result = new HashSet<Guid>();
|
||||
if (!value.TryGetProperty(propertyName, out var property) || property.ValueKind != JsonValueKind.Array)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
foreach (var item in property.EnumerateArray())
|
||||
{
|
||||
if (item.ValueKind == JsonValueKind.String && Guid.TryParse(item.GetString(), out var id))
|
||||
{
|
||||
result.Add(id);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed record CurrentAccessSnapshot(
|
||||
Guid? UserId,
|
||||
Guid? TenantId,
|
||||
bool IsUserActive,
|
||||
bool IsCurrentTenantMember,
|
||||
IReadOnlySet<string> TenantPermissions,
|
||||
IReadOnlySet<string> PlatformPermissions,
|
||||
CurrentDataScope DataScope)
|
||||
{
|
||||
public bool HasTenantPermission(string permissionCode) =>
|
||||
IsCurrentTenantMember && TenantPermissions.Contains(permissionCode);
|
||||
|
||||
public bool HasPlatformPermission(string permissionCode) =>
|
||||
IsUserActive && PlatformPermissions.Contains(permissionCode);
|
||||
}
|
||||
|
||||
public interface ICurrentAccessContext
|
||||
{
|
||||
Task<CurrentAccessSnapshot> GetAsync(CancellationToken cancellationToken = default);
|
||||
}
|
||||
Reference in New Issue
Block a user