249 lines
7.1 KiB
TypeScript
249 lines
7.1 KiB
TypeScript
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;
|
|
}
|