refactor: stricter domain: access-control
This commit is contained in:
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user