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