refactor: stricter domain: access-control

This commit is contained in:
2026-04-11 13:25:26 +02:00
parent 6800c73292
commit 0b9a9f311e
16 changed files with 46 additions and 44 deletions

View File

@@ -0,0 +1,248 @@
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<string, ChannelPermissionOverride>();
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;
}