import { ChannelPermissionOverride, PermissionState, Room, RoomPermissionKey, RoomRole } from '../../../../shared-kernel'; import { SYSTEM_ROLE_IDS } from '../constants/access-control.constants'; import type { MemberIdentity } from '../models/access-control.model'; import { buildRoleLookup, getRolePermissionState, matchesIdentity, normalizePermissionState, roleSortAscending, compareText } from '../util/access-control.util'; import { getAssignedRoleIds, getHighestAssignedRole } from './role-assignment.rules'; import { getRoomRoleById, normalizeRoomRoles } from './role.rules'; function resolveRolePermissionState(roles: readonly RoomRole[], assignedRoleIds: readonly string[], permission: RoomPermissionKey): PermissionState { const roleLookup = buildRoleLookup(roles); const assignedRoles = assignedRoleIds.map((roleId) => roleLookup.get(roleId)).filter((role): role is RoomRole => !!role); const effectiveRoles = [roleLookup.get(SYSTEM_ROLE_IDS.everyone), ...assignedRoles] .filter((role): role is RoomRole => !!role) .sort(roleSortAscending); let state: PermissionState = 'inherit'; for (const role of effectiveRoles) { const nextState = getRolePermissionState(role, permission); if (nextState !== 'inherit') { state = nextState; } } return state; } function resolveChannelOverrideState( overrides: readonly ChannelPermissionOverride[], roles: readonly RoomRole[], assignedRoleIds: readonly string[], identity: MemberIdentity, channelId: string, permission: RoomPermissionKey, baseState: PermissionState ): PermissionState { const roleLookup = buildRoleLookup(roles); let state = baseState; const everyoneOverride = overrides.find( (override) => override.channelId === channelId && override.targetType === 'role' && override.targetId === SYSTEM_ROLE_IDS.everyone && override.permission === permission ); if (everyoneOverride?.value && everyoneOverride.value !== 'inherit') { state = everyoneOverride.value; } const orderedAssignedRoles = assignedRoleIds .map((roleId) => roleLookup.get(roleId)) .filter((role): role is RoomRole => !!role) .sort(roleSortAscending); for (const role of orderedAssignedRoles) { const override = overrides.find( (candidateOverride) => candidateOverride.channelId === channelId && candidateOverride.targetType === 'role' && candidateOverride.targetId === role.id && candidateOverride.permission === permission ); if (override?.value && override.value !== 'inherit') { state = override.value; } } const userOverride = overrides.find( (override) => override.channelId === channelId && override.targetType === 'user' && override.permission === permission && (override.targetId === identity.id || override.targetId === identity.oderId) ); if (userOverride?.value && userOverride.value !== 'inherit') { state = userOverride.value; } return state; } export function normalizeChannelPermissionOverrides( overrides: readonly ChannelPermissionOverride[] | undefined, roles: readonly RoomRole[] ): ChannelPermissionOverride[] { const validRoleIds = new Set(roles.map((role) => role.id)); const normalizedByKey = new Map(); for (const override of overrides ?? []) { if (!override || typeof override !== 'object') { continue; } const channelId = typeof override.channelId === 'string' ? override.channelId.trim() : ''; const targetId = typeof override.targetId === 'string' ? override.targetId.trim() : ''; const targetType = override.targetType === 'role' || override.targetType === 'user' ? override.targetType : null; const permission = override.permission; const value = normalizePermissionState(override.value); if (!channelId || !targetId || !targetType || !permission || value === 'inherit') { continue; } if (targetType === 'role' && !validRoleIds.has(targetId)) { continue; } normalizedByKey.set(`${channelId}:${targetType}:${targetId}:${permission}`, { channelId, targetType, targetId, permission, value }); } return Array.from(normalizedByKey.values()).sort((firstOverride, secondOverride) => { const channelCompare = compareText(firstOverride.channelId, secondOverride.channelId); if (channelCompare !== 0) { return channelCompare; } if (firstOverride.targetType !== secondOverride.targetType) { return compareText(firstOverride.targetType, secondOverride.targetType); } const targetCompare = compareText(firstOverride.targetId, secondOverride.targetId); if (targetCompare !== 0) { return targetCompare; } return compareText(firstOverride.permission, secondOverride.permission); }); } export function resolveRoomPermission( room: Room, identity: MemberIdentity | null | undefined, permission: RoomPermissionKey, channelId?: string ): boolean { if (!identity) { return false; } if (room.hostId === identity.id || room.hostId === identity.oderId) { return true; } const roles = normalizeRoomRoles(room.roles, room.permissions); const assignedRoleIds = getAssignedRoleIds(room.roleAssignments, identity); const roleState = resolveRolePermissionState(roles, assignedRoleIds, permission); const channelState = channelId ? resolveChannelOverrideState( normalizeChannelPermissionOverrides(room.channelPermissions, roles), roles, assignedRoleIds, identity, channelId, permission, roleState ) : roleState; return channelState === 'allow'; } export function canManageMember( room: Room, actor: MemberIdentity | null | undefined, target: MemberIdentity | null | undefined, permission: 'kickMembers' | 'banMembers' | 'manageRoles' ): boolean { if (!actor || !target) { return false; } const isActorOwner = room.hostId === actor.id || room.hostId === actor.oderId; const isTargetOwner = room.hostId === target.id || room.hostId === target.oderId; const isSameIdentity = matchesIdentity(actor, { userId: target.id || target.oderId || '', oderId: target.oderId }); if (isSameIdentity) { return false; } if (isTargetOwner && !isActorOwner) { return false; } if (isActorOwner) { return true; } if (!resolveRoomPermission(room, actor, permission)) { return false; } const actorRole = getHighestAssignedRole(room, actor); const targetRole = getHighestAssignedRole(room, target); return (actorRole?.position ?? 0) > (targetRole?.position ?? 0); } export function canManageRole(room: Room, actor: MemberIdentity | null | undefined, roleId: string): boolean { if (!actor || !roleId) { return false; } if (room.hostId === actor.id || room.hostId === actor.oderId) { return true; } if (!resolveRoomPermission(room, actor, 'manageRoles')) { return false; } const targetRole = getRoomRoleById(normalizeRoomRoles(room.roles, room.permissions), roleId); const actorRole = getHighestAssignedRole(room, actor); if (!targetRole) { return false; } return (actorRole?.position ?? 0) > targetRole.position; }