Summary Update.
This commit is contained in:
@@ -12,6 +12,10 @@ public sealed class PermissionService
|
||||
_db = db;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Owners/admins always allowed. Non-admins blocked from read-only channels (#welcome,
|
||||
/// #files). Everyone else passes through the normal channel-level Deny → Allow → role ladder.
|
||||
/// </summary>
|
||||
public async Task<bool> CanSendMessagesAsync(string username, string channelId)
|
||||
{
|
||||
if (await IsOwnerOrAdminAsync(username))
|
||||
@@ -23,39 +27,57 @@ public sealed class PermissionService
|
||||
return await HasPermissionAsync(username, channelId, PermissionFlags.SendMessages);
|
||||
}
|
||||
|
||||
/// <summary>Server-wide ability to create channels. Gates the "+" button on the sidebar.</summary>
|
||||
public async Task<bool> CanManageChannelsAsync(string username) =>
|
||||
await IsOwnerOrAdminAsync(username) ||
|
||||
await HasGlobalPermissionAsync(username, PermissionFlags.ManageChannels);
|
||||
|
||||
/// <summary>Per-channel ability to delete/edit OTHER people's messages. Authors can always delete their own.</summary>
|
||||
public async Task<bool> CanManageMessagesAsync(string username, string channelId) =>
|
||||
await IsOwnerOrAdminAsync(username) ||
|
||||
await HasPermissionAsync(username, channelId, PermissionFlags.ManageMessages);
|
||||
|
||||
/// <summary>Convenience query — exposes the owner-or-admin shortcut as a public method.</summary>
|
||||
public async Task<bool> IsAdministratorAsync(string username) =>
|
||||
await IsOwnerOrAdminAsync(username);
|
||||
|
||||
/// <summary>
|
||||
/// "Visibility" — default-allow. Only blocks if a channel-level Deny mask explicitly
|
||||
/// removes ViewChannel for the user's role. Owners/admins bypass.
|
||||
/// </summary>
|
||||
public async Task<bool> CanViewChannelAsync(string username, string channelId)
|
||||
{
|
||||
if (await IsOwnerOrAdminAsync(username)) return true;
|
||||
return !await IsDeniedByChannelAsync(username, channelId, PermissionFlags.ViewChannel);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Voice-channel Speak. Default-allow. Blocked by channel-level Deny. Used at RtcJoin
|
||||
/// time so denied users can't even register voice presence.
|
||||
/// </summary>
|
||||
public async Task<bool> CanSpeakAsync(string username, string channelId)
|
||||
{
|
||||
if (await IsOwnerOrAdminAsync(username)) return true;
|
||||
return !await IsDeniedByChannelAsync(username, channelId, PermissionFlags.Speak);
|
||||
}
|
||||
|
||||
/// <summary>Server-wide ability to delete channels. ManageChannels OR explicit DeleteChannel.</summary>
|
||||
public async Task<bool> CanDeleteChannelAsync(string username) =>
|
||||
await IsOwnerOrAdminAsync(username) ||
|
||||
await HasGlobalPermissionAsync(username, PermissionFlags.ManageChannels) ||
|
||||
await HasGlobalPermissionAsync(username, PermissionFlags.DeleteChannel);
|
||||
|
||||
/// <summary>Server-wide ability to edit channels. ManageChannels OR explicit EditChannel.</summary>
|
||||
public async Task<bool> CanEditChannelAsync(string username) =>
|
||||
await IsOwnerOrAdminAsync(username) ||
|
||||
await HasGlobalPermissionAsync(username, PermissionFlags.ManageChannels) ||
|
||||
await HasGlobalPermissionAsync(username, PermissionFlags.EditChannel);
|
||||
|
||||
/// <summary>
|
||||
/// Step 1 of the ladder: owner flag OR Administrator permission on any assigned role.
|
||||
/// Owner check goes first because it doesn't require roles to be seeded — server owner
|
||||
/// is authoritative regardless of role-table state.
|
||||
/// </summary>
|
||||
private async Task<bool> IsOwnerOrAdminAsync(string username)
|
||||
{
|
||||
if (await IsServerOwnerAsync(username))
|
||||
@@ -65,6 +87,13 @@ public sealed class PermissionService
|
||||
return roles.Any(r => r.Permissions.HasFlag(PermissionFlags.Administrator));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The canonical permission ladder for per-channel checks:
|
||||
/// 1. Owner/admin → true.
|
||||
/// 2. Channel-level Deny mask for any of the user's roles → false (Deny wins).
|
||||
/// 3. Channel-level Allow mask for any of the user's roles → true.
|
||||
/// 4. Base role permissions → fallback.
|
||||
/// </summary>
|
||||
private async Task<bool> HasPermissionAsync(
|
||||
string username, string channelId, PermissionFlags flag)
|
||||
{
|
||||
@@ -86,6 +115,10 @@ public sealed class PermissionService
|
||||
return userRoles.Any(r => r.Permissions.HasFlag(flag));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Server-wide (not channel-scoped) permission check. Used for things like ManageChannels
|
||||
/// where there's no specific channel context. Admin flag short-circuits.
|
||||
/// </summary>
|
||||
private async Task<bool> HasGlobalPermissionAsync(string username, PermissionFlags flag)
|
||||
{
|
||||
var roles = await GetUserRolesAsync(username);
|
||||
@@ -94,6 +127,10 @@ public sealed class PermissionService
|
||||
r.Permissions.HasFlag(flag));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// "Was this permission explicitly denied here?" — used by default-allow permissions
|
||||
/// (ViewChannel, Speak) which only become restrictive when there's a Deny override.
|
||||
/// </summary>
|
||||
private async Task<bool> IsDeniedByChannelAsync(string username, string channelId, PermissionFlags flag)
|
||||
{
|
||||
var userRoles = await GetUserRolesAsync(username);
|
||||
@@ -107,6 +144,10 @@ public sealed class PermissionService
|
||||
.Any(co => co.Deny.HasFlag(flag));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks ServerMembers.IsOwner directly. This is the authoritative ownership test —
|
||||
/// independent of the role table, so ownership keeps working even if roles aren't seeded.
|
||||
/// </summary>
|
||||
private async Task<bool> IsServerOwnerAsync(string username)
|
||||
{
|
||||
var userId = $"users:{username.ToLower()}";
|
||||
@@ -116,6 +157,11 @@ public sealed class PermissionService
|
||||
m.IsOwner);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads every Role row currently assigned to the user via UserRoles. Empty list if the
|
||||
/// user has no role assignments (which means they implicitly fail every permission check
|
||||
/// unless they happen to be the server owner).
|
||||
/// </summary>
|
||||
private async Task<List<Roles>> GetUserRolesAsync(string username)
|
||||
{
|
||||
var userId = $"users:{username.ToLower()}";
|
||||
@@ -134,12 +180,14 @@ public sealed class PermissionService
|
||||
.ToList();
|
||||
}
|
||||
|
||||
/// <summary>Loads every channel_permissions override row for a channel (all roles, all flags).</summary>
|
||||
private async Task<List<ChannelPermissions>> GetChannelPermissionsAsync(string channelId)
|
||||
{
|
||||
var all = await _db.Select<ChannelPermissions>("channel_permissions");
|
||||
return all.Where(cp => cp.ChannelId == channelId).ToList();
|
||||
}
|
||||
|
||||
/// <summary>True if the channel's IsReadOnly flag is set on its row in the channels table.</summary>
|
||||
private async Task<bool> IsChannelReadOnlyAsync(string channelId)
|
||||
{
|
||||
var channels = await _db.Select<Channels>("channels");
|
||||
@@ -147,6 +195,7 @@ public sealed class PermissionService
|
||||
return channel?.IsReadOnly ?? false;
|
||||
}
|
||||
|
||||
/// <summary>SurrealDB's Id object → "table:id" string. Local copy because PermissionService isn't a friend of ChatSocketBehavior.</summary>
|
||||
private static string GetRecordIdString(object? id)
|
||||
{
|
||||
if (id is null) return string.Empty;
|
||||
|
||||
Reference in New Issue
Block a user