diff --git a/common-features/src/extensions/Modules/Authorization/BasePermissionService.cs b/common-features/src/extensions/Modules/Authorization/BasePermissionService.cs new file mode 100644 index 0000000000..666289aeb7 --- /dev/null +++ b/common-features/src/extensions/Modules/Authorization/BasePermissionService.cs @@ -0,0 +1,249 @@ + +namespace Serenity.Extensions; + +using Microsoft.Extensions.Caching.Memory; +using System.Reflection; +using System.Security.Claims; + +/// +/// Base permission service that provides common functionality for permission services. +/// +/// User accessor +/// Role permission service +/// HTTP context items accessor +public abstract class BasePermissionService( + IUserAccessor userAccessor, + IRolePermissionService rolePermissions, + IHttpContextItemsAccessor httpContextItemsAccessor = null) : IPermissionService, ITransientGrantor +{ + private readonly IRolePermissionService rolePermissions = rolePermissions ?? throw new ArgumentNullException(nameof(rolePermissions)); + private readonly IUserAccessor userAccessor = userAccessor ?? throw new ArgumentNullException(nameof(userAccessor)); + private readonly TransientGrantingPermissionService transientGrantor = new(permissionService: null, httpContextItemsAccessor); + + /// + /// Gets whether the specified permission is a valid permission key. + /// By default, a permission key is valid if it is not null or empty. + /// + /// Permission + protected virtual bool IsValidKey(string permission) + { + return !string.IsNullOrEmpty(permission); + } + + /// + /// Gets whether the specified permission is an asterisk, e.g. "*" that + /// grants permission to all including anonymous users. + /// + /// Permission + protected virtual bool IsAsterisk(string permission) + { + return permission == "*"; + } + + /// + /// Gets whether the specified permission is a question mark, e.g. "?" that + /// grants permission to logged in users. + /// + /// Permission + protected virtual bool IsQuestionMark(string permission) + { + return permission == "?"; + } + + /// + /// Checks if the transient grantor has the specified permission. + /// + /// Permission + protected virtual bool IsTransientlyGranted(string permission) + { + return transientGrantor.HasPermission(permission); + } + + /// + /// Checks if anonymous users have the specified permission. + /// By default, they don't have any permission. + /// + /// + /// + protected virtual bool AnonymousUsersHavePermission(string permission) + { + return false; + } + + /// + /// Returns true if the provided user has the permission to impersonate as another user. + /// Unless overridden, no user have this permission. + /// + /// User + /// Permission + protected virtual bool HasImpersonationPermission(ClaimsPrincipal user, string permission) + { + return IsSuperAdmin(user); + } + + /// + /// Gets whether the specified permission is an impersonation permission. + /// By default, impersonation permissions are permissions that start with "ImpersonateAs". + /// + /// + /// + protected virtual bool IsImpersonationPermission(string permission) + { + return permission.StartsWith("ImpersonateAs", StringComparison.OrdinalIgnoreCase); + } + + /// + /// Gets whether the specified user is a super admin. + /// + /// Username + protected virtual bool IsSuperAdmin(ClaimsPrincipal user) + { + return false; + } + + /// + /// Checks if the super admin has the specified permission. + /// By default, super admin has all permissions. + /// + /// Username + /// Permission + /// + protected virtual bool SuperAdminHasPermission(ClaimsPrincipal user, string permission) + { + return true; + } + + /// + public virtual bool HasPermission(string permission) + { + if (!IsValidKey(permission)) + return false; + + if (IsAsterisk(permission) || IsTransientlyGranted(permission)) + return true; + + var isLoggedIn = userAccessor.IsLoggedIn(); + + if (IsQuestionMark(permission)) + return isLoggedIn; + + if (!isLoggedIn) + return AnonymousUsersHavePermission(permission); + + var user = userAccessor.User; + if (string.IsNullOrEmpty(user?.Identity?.Name)) + return AnonymousUsersHavePermission(permission); + + if (IsImpersonationPermission(permission)) + return HasImpersonationPermission(user, permission); + + if (IsSuperAdmin(user) && + SuperAdminHasPermission(user, permission)) + return true; + + var userPermission = UserHasPermission(user, permission); + if (userPermission != null) + return userPermission.Value; + + foreach (var role in GetUserRoles(user)) + { + if (rolePermissions.HasPermission(role, permission)) + return true; + } + + return false; + } + + + /// + /// Gets the roles of the specified user. + /// + /// User + protected abstract IEnumerable GetUserRoles(ClaimsPrincipal user); + + /// + /// Gets if user has the specified permission directly, not via roles. + /// Returns null if permission is not granted or denied directly. + /// + /// User + /// Permission + protected abstract bool? UserHasPermission(ClaimsPrincipal user, string permission); + + /// + /// Gets implicit permissions defined in the application. + /// + /// Memory cache + /// Type source + /// + public static IDictionary> GetImplicitPermissions(IMemoryCache memoryCache, + ITypeSource typeSource) + { + ArgumentNullException.ThrowIfNull(memoryCache); + + ArgumentNullException.ThrowIfNull(typeSource); + + return memoryCache.Get>>("ImplicitPermissions", TimeSpan.Zero, () => + { + var result = new Dictionary>(StringComparer.OrdinalIgnoreCase); + + void addFrom(Type type) + { + foreach (var member in type.GetFields(BindingFlags.Static | BindingFlags.DeclaredOnly | + BindingFlags.Public | BindingFlags.NonPublic)) + { + if (member.FieldType != typeof(string)) + continue; + + if (member.GetValue(null) is not string key) + continue; + + foreach (var attr in member.GetCustomAttributes()) + { + if (!result.TryGetValue(key, out HashSet list)) + { + list = new HashSet(StringComparer.OrdinalIgnoreCase); + result[key] = list; + } + + list.Add(attr.Value); + } + } + + foreach (var nested in type.GetNestedTypes(BindingFlags.Public | BindingFlags.DeclaredOnly)) + addFrom(nested); + } + + foreach (var type in typeSource.GetTypesWithAttribute( + typeof(NestedPermissionKeysAttribute))) + { + addFrom(type); + } + + return result; + }); + } + + /// + public virtual void Grant(params string[] permissions) + { + transientGrantor.Grant(permissions); + } + + /// + public virtual void GrantAll() + { + transientGrantor.GrantAll(); + } + + /// + public virtual void UndoGrant() + { + transientGrantor.UndoGrant(); + } + + /// + public virtual bool IsAllGranted() => transientGrantor.IsAllGranted(); + + /// + public virtual IEnumerable GetGranted() => transientGrantor.GetGranted(); +} \ No newline at end of file diff --git a/common-features/src/extensions/Modules/Authorization/BasePermissionServiceT.cs b/common-features/src/extensions/Modules/Authorization/BasePermissionServiceT.cs new file mode 100644 index 0000000000..fbb73e4109 --- /dev/null +++ b/common-features/src/extensions/Modules/Authorization/BasePermissionServiceT.cs @@ -0,0 +1,194 @@ + +using System.Security.Claims; + +namespace Serenity.Extensions; + +/// +/// Base permission service that provides common functionality for permission services. +/// +/// User permission row type +/// User role row type +/// Cache +/// Sql connections +/// Type source +/// User accessor +/// Role permissions +/// HTTP context items accessor +public abstract class BasePermissionService( + ITwoLevelCache cache, + ISqlConnections sqlConnections, + ITypeSource typeSource, + IUserAccessor userAccessor, + IRolePermissionService rolePermissions, + IHttpContextItemsAccessor httpContextItemsAccessor = null) : + BasePermissionService(userAccessor, rolePermissions, httpContextItemsAccessor) + where TUserPermissionRow : class, IUserPermissionRow, new() + where TUserRoleRow : class, IUserRoleRow, new() +{ + private readonly ITwoLevelCache cache = cache ?? throw new ArgumentNullException(nameof(cache)); + private readonly ISqlConnections sqlConnections = sqlConnections ?? throw new ArgumentNullException(nameof(sqlConnections)); + + /// + protected override bool? UserHasPermission(ClaimsPrincipal user, string permission) + { + return GetUserPermissions(user)? + .TryGetValue(permission, out bool grant) == true ? grant : null; + } + + /// + /// Gets directly assigned permissions for the specified user. + /// + /// User + protected virtual IDictionary GetUserPermissions(ClaimsPrincipal user) + { + if (user == null) + return null; + + return GetCachedUserPermissions(user); + } + + /// + /// Gets the cache duration for user permissions. + /// Default is zero, meaning it will be cached indefinitely, unless + /// expired by using the cache group key. + /// + protected virtual TimeSpan GetUserPermissionsCacheDuration() + { + return TimeSpan.Zero; + } + + private TUserPermissionRow userPermissionRow; + + /// + /// Gets the cache group key for user permissions. + /// + protected virtual string GetUserPermissionsCacheGroupKey() + { + return (userPermissionRow ??= new()).Fields.GenerationKey; + } + + /// + /// Gets the cache key for user permissions. + /// + /// User + protected virtual string GetUserPermissionsCacheKey(ClaimsPrincipal user) + { + return "UserPermissions:" + user.GetIdentifier(); + } + + /// + /// Gets the cached user permissions. + /// + /// User + protected virtual IDictionary GetCachedUserPermissions(ClaimsPrincipal user) + { + return cache.GetLocalStoreOnly(GetUserPermissionsCacheKey(user), + GetUserPermissionsCacheDuration(), GetUserPermissionsCacheGroupKey(), + () => LoadUserPermissions(user)); + } + + /// + /// Loads user permissions from database. + /// + /// User + protected virtual IDictionary LoadUserPermissions(ClaimsPrincipal user) + { + using var connection = sqlConnections.NewFor(); + var result = new Dictionary(StringComparer.OrdinalIgnoreCase); + userPermissionRow ??= new(); + + connection.List(query => + { + var userId = userPermissionRow.UserIdField.ConvertValue( + user.GetIdentifier(), CultureInfo.InvariantCulture); + + query.Select(userPermissionRow.PermissionKeyField) + .Where(userPermissionRow.UserIdField == new ValueCriteria(userId)); + + if (userPermissionRow.GrantedField is not null) + query.Select(userPermissionRow.GrantedField); + }).ForEach(x => result[x.PermissionKeyField[x]] = x.GrantedField?[x] ?? true); + + var implicitPermissions = GetImplicitPermissions(cache.Memory, typeSource); + foreach (var pair in result.ToArray()) + { + if (pair.Value && implicitPermissions.TryGetValue(pair.Key, out var list)) + foreach (var x in list) + result[x] = true; + } + + return result; + } + + /// + /// Gets the cache duration for user roles. Default is zero, + /// meaning it will be cached indefinitely, unless expired by using + /// the cache group key. + /// + protected virtual TimeSpan GetUserRolesCacheDuration() + { + return TimeSpan.Zero; + } + + private TUserRoleRow userRoleRow; + + /// + /// Gets the cache group key for user roles. + /// + protected virtual string GetUserRolesCacheGroupKey() + { + return (userRoleRow ??= new()).Fields.GenerationKey; + } + + /// + /// Gets the cache key for user roles. + /// + /// User + protected virtual string GetUserRolesCacheKey(ClaimsPrincipal user) + { + return "UserRoles:" + user; + } + + /// + /// Gets the cached user roles. + /// + /// User + protected virtual IEnumerable GetCachedUserRoles(ClaimsPrincipal user) + { + return cache.GetLocalStoreOnly(GetUserRolesCacheKey(user), + GetUserRolesCacheDuration(), GetUserRolesCacheGroupKey(), + () => LoadUserRoles(user)); + } + + /// + /// Gets the roles of the specified user. + /// + /// User + protected override IEnumerable GetUserRoles(ClaimsPrincipal user) + { + if (user == null) + return null; + + return GetCachedUserRoles(user); + } + + /// + /// Loads user roles from database. + /// + /// User + protected virtual IEnumerable LoadUserRoles(ClaimsPrincipal user) + { + using var connection = sqlConnections.NewFor(); + var result = new HashSet(); + userRoleRow ??= new(); + var userId = userRoleRow.UserIdField.ConvertValue(user.GetIdentifier(), + CultureInfo.InvariantCulture); + + connection.List(q => q + .Select(userRoleRow.RoleKeyOrNameField) + .Where(userRoleRow.UserIdField == new ValueCriteria(userId))) + .ForEach(x => result.Add(x.RoleKeyOrNameField[x])); + + return result; + } +} \ No newline at end of file diff --git a/common-features/src/extensions/Modules/Authorization/BaseUserRetrieveService.cs b/common-features/src/extensions/Modules/Authorization/BaseUserRetrieveService.cs index 813106629c..2cbe3352d0 100644 --- a/common-features/src/extensions/Modules/Authorization/BaseUserRetrieveService.cs +++ b/common-features/src/extensions/Modules/Authorization/BaseUserRetrieveService.cs @@ -1,9 +1,14 @@ namespace Serenity.Extensions; +/// +/// Base user retrieve service that provides common functionality for user retrieve services. +/// +/// Cache public abstract class BaseUserRetrieveService(ITwoLevelCache cache) : IUserRetrieveService, IRemoveCachedUser, IRemoveAll { private readonly ITwoLevelCache cache = cache ?? throw new ArgumentNullException(nameof(cache)); + /// public virtual IUserDefinition ById(string id) { if (!IsValidUserId(id)) @@ -12,6 +17,7 @@ public virtual IUserDefinition ById(string id) return GetCachedById(id); } + /// public virtual IUserDefinition ByUsername(string username) { if (!IsValidUsername(username)) @@ -20,36 +26,80 @@ public virtual IUserDefinition ByUsername(string username) return GetCachedByUsername(username); } + /// + /// Gets the cache duration for user retrieval. Default is zero, meaning it will be + /// cached indefinitely unless expired by using the cache group key. + /// protected virtual TimeSpan GetCacheDuration() => TimeSpan.Zero; + + /// + /// Gets the cache group key for user retrieval. + /// protected abstract string GetCacheGroupKey(); + /// + /// Gets the cache key for the specified user ID. + /// + /// User ID protected virtual string GetIdCacheKey(string id) => "UserByID_" + id; + + /// + /// Gets the cache key for the specified username. + /// + /// Username protected virtual string GetUsernameCacheKey(string username) => "UserByName_" + username.ToLowerInvariant(); + /// + /// Gets the cached user by the specified ID. + /// + /// User ID protected virtual IUserDefinition GetCachedById(string id) { return cache.GetLocalStoreOnly(GetIdCacheKey(id), GetCacheDuration(), GetCacheGroupKey(), () => LoadById(id)); } + /// + /// Gets the cached user by the specified username. + /// + /// Username protected virtual IUserDefinition GetCachedByUsername(string username) { return cache.GetLocalStoreOnly(GetUsernameCacheKey(username), GetCacheDuration(), GetCacheGroupKey(), () => LoadByUsername(username)); } + /// + /// Checks if the specified user ID is valid. By default, it checks if it is not null or empty. + /// + /// User ID protected virtual bool IsValidUserId(string userId) => !string.IsNullOrEmpty(userId); + /// + /// Checks if the specified username is valid. By default, it checks if it is not null or empty. + /// + /// Username protected virtual bool IsValidUsername(string username) => !string.IsNullOrEmpty(username); + /// + /// Loads the user by the specified ID from database. + /// + /// User ID protected abstract IUserDefinition LoadById(string id); + + /// + /// Loads the user by the specified username from database + /// + /// Username protected abstract IUserDefinition LoadByUsername(string username); + /// public virtual void RemoveAll() { cache.ExpireGroupItems(GetCacheGroupKey()); } + /// public virtual void RemoveCachedUser(string userId, string username) { if (IsValidUserId(userId)) diff --git a/common-features/src/extensions/Modules/Authorization/BaseUserRetrieveServiceT.cs b/common-features/src/extensions/Modules/Authorization/BaseUserRetrieveServiceT.cs index 7655df797c..cd6e121ceb 100644 --- a/common-features/src/extensions/Modules/Authorization/BaseUserRetrieveServiceT.cs +++ b/common-features/src/extensions/Modules/Authorization/BaseUserRetrieveServiceT.cs @@ -1,13 +1,28 @@ namespace Serenity.Extensions; +/// +/// Base user retrieve service that provides common functionality for user retrieve services. +/// +/// User row type +/// Cache +/// SQL connections public abstract class BaseUserRetrieveService(ITwoLevelCache cache, ISqlConnections sqlConnections) : BaseUserRetrieveService(cache) where TRow : class, IRow, IIdRow, INameRow, new() { private readonly ISqlConnections sqlConnections = sqlConnections ?? throw new ArgumentNullException(nameof(sqlConnections)); + /// + /// Converts the specified row to a user definition. + /// + /// User row protected abstract IUserDefinition ToUserDefinition(TRow row); + /// + /// Loads a user from the database by the specified criteria. + /// + /// Connection + /// Criteria protected virtual IUserDefinition LoadByCriteria(System.Data.IDbConnection connection, BaseCriteria criteria) { var user = connection.TrySingle(criteria); @@ -21,11 +36,18 @@ protected virtual IUserDefinition LoadByCriteria(System.Data.IDbConnection conne private Field idField; private Field nameField; + /// + /// Gets the cache group key for user retrieval. + /// protected override string GetCacheGroupKey() { return (cacheGroupKey ??= new TRow().Fields.GenerationKey); } + /// + /// Checks if the specified user ID is valid. + /// + /// User ID protected override bool IsValidUserId(string userId) { if (!base.IsValidUserId(userId)) @@ -44,6 +66,10 @@ protected override bool IsValidUserId(string userId) return true; } + /// + /// Loads the user by the specified ID from database. + /// + /// User ID protected override IUserDefinition LoadById(string id) { idField ??= new TRow().IdField; @@ -53,6 +79,10 @@ protected override IUserDefinition LoadById(string id) new ValueCriteria(idField.ConvertValue(id, CultureInfo.InvariantCulture))); } + /// + /// Loads the user by the specified username from database. + /// + /// Username protected override IUserDefinition LoadByUsername(string username) { nameField ??= new TRow().NameField; diff --git a/common-features/src/extensions/Modules/Authorization/IUserPermissionRow.cs b/common-features/src/extensions/Modules/Authorization/IUserPermissionRow.cs new file mode 100644 index 0000000000..6a3f3d79b3 --- /dev/null +++ b/common-features/src/extensions/Modules/Authorization/IUserPermissionRow.cs @@ -0,0 +1,23 @@ +namespace Serenity.Data; + +/// +/// User permission row interface +/// +public interface IUserPermissionRow : IRow +{ + /// + /// User ID field + /// + Field UserIdField { get; } + + /// + /// Permission key field + /// + StringField PermissionKeyField { get; } + + /// + /// Granted field, might be null if not available. Used to optionally + /// revoke permissions granted via roles. + /// + BooleanField GrantedField { get; } +} \ No newline at end of file diff --git a/common-features/src/extensions/Modules/Authorization/IUserRoleRow.cs b/common-features/src/extensions/Modules/Authorization/IUserRoleRow.cs new file mode 100644 index 0000000000..82ff49ca76 --- /dev/null +++ b/common-features/src/extensions/Modules/Authorization/IUserRoleRow.cs @@ -0,0 +1,17 @@ +namespace Serenity.Data; + +/// +/// User role row interface +/// +public interface IUserRoleRow : IRow +{ + /// + /// User ID field + /// + Field UserIdField { get; } + + /// + /// User role key or the role name field + /// + StringField RoleKeyOrNameField { get; } +} diff --git a/serene/src/Serene.Web/Modules/Administration/UserPermission/ImplicitPermissionsDataScript.cs b/serene/src/Serene.Web/Modules/Administration/UserPermission/ImplicitPermissionsDataScript.cs index bfc4d545d6..e30d24f3cf 100644 --- a/serene/src/Serene.Web/Modules/Administration/UserPermission/ImplicitPermissionsDataScript.cs +++ b/serene/src/Serene.Web/Modules/Administration/UserPermission/ImplicitPermissionsDataScript.cs @@ -1,27 +1,15 @@ +using Microsoft.Extensions.Caching.Memory; namespace Serene.Administration; -using Microsoft.Extensions.Caching.Memory; -using Serenity.Abstractions; -using Serenity.ComponentModel; -using Serenity.Web; -using System; -using System.Collections.Generic; - [DataScript("Administration.ImplicitPermissions", Permission = PermissionKeys.Security)] -public class ImplicitPermissionsDataScript : DataScript>> +public class ImplicitPermissionsDataScript(IMemoryCache cache, ITypeSource typeSource) : DataScript>> { - private readonly IMemoryCache cache; - private readonly ITypeSource typeSource; - - public ImplicitPermissionsDataScript(IMemoryCache cache, ITypeSource typeSource) - { - this.cache = cache ?? throw new ArgumentNullException(nameof(cache)); - this.typeSource = typeSource ?? throw new ArgumentNullException(nameof(typeSource)); - } + private readonly IMemoryCache cache = cache ?? throw new ArgumentNullException(nameof(cache)); + private readonly ITypeSource typeSource = typeSource ?? throw new ArgumentNullException(nameof(typeSource)); protected override IDictionary> GetData() { - return AppServices.PermissionService.GetImplicitPermissions(cache, typeSource); + return BasePermissionService.GetImplicitPermissions(cache, typeSource); } } \ No newline at end of file diff --git a/serene/src/Serene.Web/Modules/Administration/UserPermission/UserPermissionRow.cs b/serene/src/Serene.Web/Modules/Administration/UserPermission/UserPermissionRow.cs index cf7b3561fc..5427455117 100644 --- a/serene/src/Serene.Web/Modules/Administration/UserPermission/UserPermissionRow.cs +++ b/serene/src/Serene.Web/Modules/Administration/UserPermission/UserPermissionRow.cs @@ -1,10 +1,10 @@ -namespace Serene.Administration; +namespace Serene.Administration; [ConnectionKey("Default"), Module("Administration"), TableName("UserPermissions")] [DisplayName("UserPermissions"), InstanceName("UserPermissions")] [ReadPermission(PermissionKeys.Security)] [ModifyPermission(PermissionKeys.Security)] -public sealed class UserPermissionRow : Row, IIdRow, INameRow +public sealed class UserPermissionRow : Row, IIdRow, INameRow, IUserPermissionRow { [DisplayName("User Permission Id"), Identity, IdProperty] public long? UserPermissionId { get => fields.UserPermissionId[this]; set => fields.UserPermissionId[this] = value; } @@ -24,6 +24,10 @@ public sealed class UserPermissionRow : Row, IIdRow [DisplayName("User Display Name"), Expression("jUser.[DisplayName]")] public string User { get => fields.User[this]; set => fields.User[this] = value; } + Field IUserPermissionRow.UserIdField => fields.UserId; + StringField IUserPermissionRow.PermissionKeyField => fields.PermissionKey; + BooleanField IUserPermissionRow.GrantedField => fields.Granted; + public class RowFields : RowFieldsBase { public Int64Field UserPermissionId; diff --git a/serene/src/Serene.Web/Modules/Administration/UserRole/UserRoleRow.cs b/serene/src/Serene.Web/Modules/Administration/UserRole/UserRoleRow.cs index 20ed363823..426e7959eb 100644 --- a/serene/src/Serene.Web/Modules/Administration/UserRole/UserRoleRow.cs +++ b/serene/src/Serene.Web/Modules/Administration/UserRole/UserRoleRow.cs @@ -4,7 +4,7 @@ namespace Serene.Administration; [DisplayName("UserRoles"), InstanceName("UserRoles")] [ReadPermission(PermissionKeys.Security)] [ModifyPermission(PermissionKeys.Security)] -public sealed class UserRoleRow : Row, IIdRow +public sealed class UserRoleRow : Row, IIdRow, IUserRoleRow { const string jRole = nameof(jRole); const string jUser = nameof(jUser); @@ -27,6 +27,10 @@ public sealed class UserRoleRow : Row, IIdRow [DisplayName("Role"), Expression($"{jRole}.[RoleName]")] public string RoleName { get => fields.RoleName[this]; set => fields.RoleName[this] = value; } + public Field UserIdField => fields.UserId; + + public StringField RoleKeyOrNameField => fields.RoleName; + public class RowFields : RowFieldsBase { public Int64Field UserRoleId; diff --git a/serene/src/Serene.Web/Modules/Common/AppServices/PermissionService.cs b/serene/src/Serene.Web/Modules/Common/AppServices/PermissionService.cs index 0c0e26294e..5f2f888e1c 100644 --- a/serene/src/Serene.Web/Modules/Common/AppServices/PermissionService.cs +++ b/serene/src/Serene.Web/Modules/Common/AppServices/PermissionService.cs @@ -1,7 +1,5 @@ -using Microsoft.Extensions.Caching.Memory; using Serene.Administration; -using System.Globalization; -using System.Reflection; +using System.Security.Claims; namespace Serene.AppServices; @@ -10,168 +8,13 @@ public class PermissionService(ITwoLevelCache cache, ITypeSource typeSource, IUserAccessor userAccessor, IRolePermissionService rolePermissions, - IHttpContextItemsAccessor httpContextItemsAccessor = null) : IPermissionService, ITransientGrantor + IHttpContextItemsAccessor httpContextItemsAccessor = null) : + BasePermissionService(cache, sqlConnections, typeSource, + userAccessor, rolePermissions, httpContextItemsAccessor) { - private readonly ITwoLevelCache cache = cache ?? throw new ArgumentNullException(nameof(cache)); - private readonly IRolePermissionService rolePermissions = rolePermissions ?? throw new ArgumentNullException(nameof(rolePermissions)); - private readonly ISqlConnections sqlConnections = sqlConnections ?? throw new ArgumentNullException(nameof(sqlConnections)); - private readonly ITypeSource typeSource = typeSource ?? throw new ArgumentNullException(nameof(typeSource)); - private readonly IUserAccessor userAccessor = userAccessor ?? throw new ArgumentNullException(nameof(userAccessor)); - private readonly TransientGrantingPermissionService transientGrantor = new(permissionService: null, httpContextItemsAccessor); - - public bool HasPermission(string permission) - { - if (string.IsNullOrEmpty(permission)) - return false; - - if (permission == "*" || transientGrantor.HasPermission(permission)) - return true; - - var isLoggedIn = userAccessor.IsLoggedIn(); - - if (permission == "?") - return isLoggedIn; - - if (!isLoggedIn) - return false; - - var username = userAccessor.User?.Identity?.Name; - if (username == "admin") - return true; - - // only admin has impersonation permission - if (string.Compare(permission, "ImpersonateAs", StringComparison.OrdinalIgnoreCase) == 0) - return false; - - var userId = Convert.ToInt32(userAccessor.User.GetIdentifier(), CultureInfo.InvariantCulture); - - if (GetUserPermissions(userId).TryGetValue(permission, out bool grant)) - return grant; - - foreach (var role in GetUserRoles(userId)) - { - if (rolePermissions.HasPermission(role, permission)) - return true; - } - - return false; - } - - private Dictionary GetUserPermissions(int userId) - { - var fld = UserPermissionRow.Fields; - - return cache.GetLocalStoreOnly("UserPermissions:" + userId, TimeSpan.Zero, fld.GenerationKey, () => - { - using var connection = sqlConnections.NewByKey("Default"); - var result = new Dictionary(StringComparer.OrdinalIgnoreCase); - - connection.List(q => q - .Select(fld.PermissionKey) - .Select(fld.Granted) - .Where(new Criteria(fld.UserId) == userId)) - .ForEach(x => result[x.PermissionKey] = x.Granted ?? true); - - var implicitPermissions = PermissionService.GetImplicitPermissions(cache.Memory, typeSource); - foreach (var pair in result.ToArray()) - { - if (pair.Value && implicitPermissions.TryGetValue(pair.Key, out HashSet list)) - foreach (var x in list) - result[x] = true; - } - - return result; - }); - } - - private HashSet GetUserRoles(int userId) - { - var fld = UserRoleRow.Fields; - - return cache.GetLocalStoreOnly("UserRoles:" + userId, TimeSpan.Zero, fld.GenerationKey, () => - { - using var connection = sqlConnections.NewByKey("Default"); - var result = new HashSet(); - - connection.List(q => q - .Select(fld.RoleName) - .Where(new Criteria(fld.UserId) == userId)) - .ForEach(x => result.Add(x.RoleName)); - - return result; - }); - } - - public static IDictionary> GetImplicitPermissions(IMemoryCache memoryCache, - ITypeSource typeSource) - { - ArgumentNullException.ThrowIfNull(memoryCache); - - ArgumentNullException.ThrowIfNull(typeSource); - - return memoryCache.Get>>("ImplicitPermissions", TimeSpan.Zero, () => - { - var result = new Dictionary>(StringComparer.OrdinalIgnoreCase); - - void addFrom(Type type) - { - foreach (var member in type.GetFields(BindingFlags.Static | BindingFlags.DeclaredOnly | - BindingFlags.Public | BindingFlags.NonPublic)) - { - if (member.FieldType != typeof(string)) - continue; - - if (member.GetValue(null) is not string key) - continue; - - foreach (var attr in member.GetCustomAttributes()) - { - if (!result.TryGetValue(key, out HashSet list)) - { - list = new HashSet(StringComparer.OrdinalIgnoreCase); - result[key] = list; - } - - list.Add(attr.Value); - } - } - - foreach (var nested in type.GetNestedTypes(BindingFlags.Public | BindingFlags.DeclaredOnly)) - addFrom(nested); - } - - foreach (var type in typeSource.GetTypesWithAttribute( - typeof(NestedPermissionKeysAttribute))) - { - addFrom(type); - } - - return result; - }); - } - /// - public void Grant(params string[] permissions) + protected override bool IsSuperAdmin(ClaimsPrincipal user) { - transientGrantor.Grant(permissions); + return user.Identity?.Name == "admin"; } - - /// - public void GrantAll() - { - transientGrantor.GrantAll(); - } - - /// - public void UndoGrant() - { - transientGrantor.UndoGrant(); - } - - /// - public bool IsAllGranted() => transientGrantor.IsAllGranted(); - - /// - public IEnumerable GetGranted() => transientGrantor.GetGranted(); - } \ No newline at end of file