forked from coracle/flotilla
refactor: address PR review feedback for RBAC
This commit is contained in:
@@ -17,11 +17,7 @@
|
|||||||
import Report from "@app/components/Report.svelte"
|
import Report from "@app/components/Report.svelte"
|
||||||
import EventShare from "@app/components/EventShare.svelte"
|
import EventShare from "@app/components/EventShare.svelte"
|
||||||
import EventDeleteConfirm from "@app/components/EventDeleteConfirm.svelte"
|
import EventDeleteConfirm from "@app/components/EventDeleteConfirm.svelte"
|
||||||
import {
|
import {deriveHasPermission, ROOM_PERMISSION_DELETE_EVENT} from "@app/core/roles"
|
||||||
deriveUserIsSpaceAdmin,
|
|
||||||
deriveHasPermission,
|
|
||||||
ROOM_PERMISSION_DELETE_EVENT,
|
|
||||||
} from "@app/core/roles"
|
|
||||||
import {hasNip29} from "@app/core/state"
|
import {hasNip29} from "@app/core/state"
|
||||||
import {pushModal} from "@app/util/modal"
|
import {pushModal} from "@app/util/modal"
|
||||||
import {pushToast} from "@app/util/toast"
|
import {pushToast} from "@app/util/toast"
|
||||||
@@ -39,9 +35,7 @@
|
|||||||
|
|
||||||
const isRoot = event.kind !== COMMENT
|
const isRoot = event.kind !== COMMENT
|
||||||
const h = getTagValue("h", event.tags)
|
const h = getTagValue("h", event.tags)
|
||||||
const canDelete = h
|
const canDelete = deriveHasPermission(url, h, ROOM_PERMISSION_DELETE_EVENT)
|
||||||
? deriveHasPermission(url, h, ROOM_PERMISSION_DELETE_EVENT)
|
|
||||||
: deriveUserIsSpaceAdmin(url)
|
|
||||||
|
|
||||||
const report = () => pushModal(Report, {url, event})
|
const report = () => pushModal(Report, {url, event})
|
||||||
|
|
||||||
|
|||||||
+54
-103
@@ -1,7 +1,7 @@
|
|||||||
import {derived, readable, type Readable} from "svelte/store"
|
import {derived, readable, type Readable} from "svelte/store"
|
||||||
import {first, memoize, removeUndefined, simpleCache, sortBy, uniq} from "@welshman/lib"
|
import {first, memoize, removeUndefined, simpleCache, sortBy, uniq} from "@welshman/lib"
|
||||||
import {deriveArray, deriveEventsByIdForUrl} from "@welshman/store"
|
import {pubkey, manageRelay} from "@welshman/app"
|
||||||
import {pubkey, repository, tracker, manageRelay} from "@welshman/app"
|
import {deriveEventsForUrl} from "@app/core/state"
|
||||||
import {
|
import {
|
||||||
ManagementMethod,
|
ManagementMethod,
|
||||||
ROOM_ADD_MEMBER,
|
ROOM_ADD_MEMBER,
|
||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
getTagValue,
|
getTagValue,
|
||||||
isRelayUrl,
|
isRelayUrl,
|
||||||
} from "@welshman/util"
|
} from "@welshman/util"
|
||||||
import type {Filter, TrustedEvent} from "@welshman/util"
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
|
|
||||||
export const ROOM_ROLES = 39003
|
export const ROOM_ROLES = 39003
|
||||||
|
|
||||||
@@ -79,22 +79,12 @@ type ParsedRoleState = {
|
|||||||
hasPermissionTags: boolean
|
hasPermissionTags: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
type RoomSnapshot = {
|
|
||||||
h: string
|
|
||||||
rolesState: ParsedRoleState
|
|
||||||
members: RoomMember[]
|
|
||||||
admins: RoomMember[]
|
|
||||||
}
|
|
||||||
|
|
||||||
type SpaceRoleState = {
|
type SpaceRoleState = {
|
||||||
hasPermissionTags: boolean
|
hasPermissionTags: boolean
|
||||||
userPermissions: Set<number>
|
userPermissions: Set<number>
|
||||||
memberRoles: Map<string, RoleDefinition[]>
|
memberRoles: Map<string, RoleDefinition[]>
|
||||||
}
|
}
|
||||||
|
|
||||||
const deriveEventsForUrl = (url: string, filters: Filter[] = [{}]) =>
|
|
||||||
deriveArray(deriveEventsByIdForUrl({url, tracker, repository, filters}))
|
|
||||||
|
|
||||||
const makeRoleDefinition = (name: string): RoleDefinition => ({
|
const makeRoleDefinition = (name: string): RoleDefinition => ({
|
||||||
name,
|
name,
|
||||||
permissions: [],
|
permissions: [],
|
||||||
@@ -121,14 +111,6 @@ const asAccess = (value: string | undefined): RoleAccess | undefined => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getLatestEventByKind = (events: TrustedEvent[], kind: number, h: string) =>
|
|
||||||
first(
|
|
||||||
sortBy(
|
|
||||||
event => -event.created_at,
|
|
||||||
events.filter(event => event.kind === kind && getTagValue("d", event.tags) === h),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
const parseRoleState = (event?: TrustedEvent): ParsedRoleState => {
|
const parseRoleState = (event?: TrustedEvent): ParsedRoleState => {
|
||||||
const roles = new Map<string, RoleDefinition>()
|
const roles = new Map<string, RoleDefinition>()
|
||||||
let hasPermissionTags = false
|
let hasPermissionTags = false
|
||||||
@@ -250,27 +232,19 @@ export const parseRoomMembers = (tags: string[][]) => {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
const deriveRoomListStore = simpleCache(([url, h]: [string, string]) =>
|
|
||||||
deriveEventsForUrl(url, [{kinds: [ROOM_MEMBERS, ROOM_ADMINS], "#d": [h]}]),
|
|
||||||
)
|
|
||||||
|
|
||||||
export const deriveRoomMembers = (url: string, h: string) =>
|
export const deriveRoomMembers = (url: string, h: string) =>
|
||||||
derived(deriveRoomListStore(url, h), $events => {
|
derived(deriveEventsForUrl(url, [{kinds: [ROOM_MEMBERS], "#d": [h]}]), ([event]) =>
|
||||||
const event = getLatestEventByKind($events, ROOM_MEMBERS, h)
|
parseRoomMembers(event?.tags || []),
|
||||||
|
)
|
||||||
return parseRoomMembers(event?.tags || [])
|
|
||||||
})
|
|
||||||
|
|
||||||
export const deriveRoomAdmins = (url: string, h: string) =>
|
export const deriveRoomAdmins = (url: string, h: string) =>
|
||||||
derived(deriveRoomListStore(url, h), $events => {
|
derived(deriveEventsForUrl(url, [{kinds: [ROOM_ADMINS], "#d": [h]}]), ([event]) =>
|
||||||
const event = getLatestEventByKind($events, ROOM_ADMINS, h)
|
parseRoomMembers(event?.tags || []),
|
||||||
|
)
|
||||||
return parseRoomMembers(event?.tags || [])
|
|
||||||
})
|
|
||||||
|
|
||||||
const deriveRoomRoleState = simpleCache(([url, h]: [string, string]) =>
|
const deriveRoomRoleState = simpleCache(([url, h]: [string, string]) =>
|
||||||
derived(deriveEventsForUrl(url, [{kinds: [ROOM_ROLES], "#d": [h]}]), $events =>
|
derived(deriveEventsForUrl(url, [{kinds: [ROOM_ROLES], "#d": [h]}]), ([event]) =>
|
||||||
parseRoleState(getLatestEventByKind($events, ROOM_ROLES, h)),
|
parseRoleState(event),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -395,52 +369,6 @@ export const deriveUserPermissions = (url: string, h: string) =>
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
const buildRoomSnapshots = (events: TrustedEvent[]) => {
|
|
||||||
const latestByH = new Map<
|
|
||||||
string,
|
|
||||||
{roles?: TrustedEvent; members?: TrustedEvent; admins?: TrustedEvent}
|
|
||||||
>()
|
|
||||||
|
|
||||||
for (const event of sortBy(x => -x.created_at, events)) {
|
|
||||||
const h = getTagValue("d", event.tags)
|
|
||||||
|
|
||||||
if (!h) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!latestByH.has(h)) {
|
|
||||||
latestByH.set(h, {})
|
|
||||||
}
|
|
||||||
|
|
||||||
const entry = latestByH.get(h)!
|
|
||||||
|
|
||||||
if (event.kind === ROOM_ROLES && !entry.roles) {
|
|
||||||
entry.roles = event
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.kind === ROOM_MEMBERS && !entry.members) {
|
|
||||||
entry.members = event
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.kind === ROOM_ADMINS && !entry.admins) {
|
|
||||||
entry.admins = event
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const snapshots: RoomSnapshot[] = []
|
|
||||||
|
|
||||||
for (const [h, {roles, members, admins}] of latestByH.entries()) {
|
|
||||||
snapshots.push({
|
|
||||||
h,
|
|
||||||
rolesState: parseRoleState(roles),
|
|
||||||
members: parseRoomMembers(members?.tags || []),
|
|
||||||
admins: parseRoomMembers(admins?.tags || []),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return snapshots
|
|
||||||
}
|
|
||||||
|
|
||||||
const mergeRoleDefinitions = (left: RoleDefinition[], right: RoleDefinition[]) => {
|
const mergeRoleDefinitions = (left: RoleDefinition[], right: RoleDefinition[]) => {
|
||||||
const merged = new Map<string, RoleDefinition>()
|
const merged = new Map<string, RoleDefinition>()
|
||||||
|
|
||||||
@@ -488,28 +416,48 @@ const deriveSpaceRoleState = simpleCache(([url]: [string]) =>
|
|||||||
([$pubkey, $events]): SpaceRoleState => {
|
([$pubkey, $events]): SpaceRoleState => {
|
||||||
const userPermissions = new Set<number>()
|
const userPermissions = new Set<number>()
|
||||||
const memberRoles = new Map<string, RoleDefinition[]>()
|
const memberRoles = new Map<string, RoleDefinition[]>()
|
||||||
const snapshots = buildRoomSnapshots($events)
|
const rolesByH = new Map<string, ReturnType<typeof parseRoleState>>()
|
||||||
const hasPermissionTags = snapshots.some(snapshot => snapshot.rolesState.hasPermissionTags)
|
let hasPermissionTags = false
|
||||||
|
|
||||||
for (const snapshot of snapshots) {
|
for (const event of $events) {
|
||||||
const allMembers = [...snapshot.members, ...snapshot.admins]
|
if (event.kind === ROOM_ROLES) {
|
||||||
|
const h = getTagValue("d", event.tags)
|
||||||
for (const member of allMembers) {
|
if (h) {
|
||||||
const resolvedRoles = getResolvedRoles(snapshot.rolesState.roles, member.roles)
|
const parsed = parseRoleState(event)
|
||||||
|
rolesByH.set(h, parsed)
|
||||||
if (resolvedRoles.length === 0) {
|
if (parsed.hasPermissionTags) {
|
||||||
continue
|
hasPermissionTags = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
memberRoles.set(
|
for (const event of $events) {
|
||||||
member.pubkey,
|
if (event.kind === ROOM_MEMBERS || event.kind === ROOM_ADMINS) {
|
||||||
mergeRoleDefinitions(memberRoles.get(member.pubkey) || [], resolvedRoles),
|
const h = getTagValue("d", event.tags)
|
||||||
)
|
if (!h) continue
|
||||||
|
|
||||||
if ($pubkey === member.pubkey && hasPermissionTags) {
|
const rolesState = rolesByH.get(h)
|
||||||
for (const role of resolvedRoles) {
|
if (!rolesState) continue
|
||||||
for (const permission of role.permissions) {
|
|
||||||
userPermissions.add(permission)
|
const members = parseRoomMembers(event.tags)
|
||||||
|
for (const member of members) {
|
||||||
|
const resolvedRoles = getResolvedRoles(rolesState.roles, member.roles)
|
||||||
|
|
||||||
|
if (resolvedRoles.length === 0) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
memberRoles.set(
|
||||||
|
member.pubkey,
|
||||||
|
mergeRoleDefinitions(memberRoles.get(member.pubkey) || [], resolvedRoles),
|
||||||
|
)
|
||||||
|
|
||||||
|
if ($pubkey === member.pubkey && rolesState.hasPermissionTags) {
|
||||||
|
for (const role of resolvedRoles) {
|
||||||
|
for (const permission of role.permissions) {
|
||||||
|
userPermissions.add(permission)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -575,11 +523,14 @@ export const deriveUserIsRoomAdmin = (url: string, h: string) =>
|
|||||||
([$permissions, $isSpaceAdmin]) => $isSpaceAdmin || $permissions.size > 0,
|
([$permissions, $isSpaceAdmin]) => $isSpaceAdmin || $permissions.size > 0,
|
||||||
)
|
)
|
||||||
|
|
||||||
export const deriveHasPermission = (url: string, h: string, kind: number) =>
|
export const deriveHasPermission = (url: string, h: string | undefined, kind: number) => {
|
||||||
derived(
|
if (!h) return deriveUserIsSpaceAdmin(url)
|
||||||
|
|
||||||
|
return derived(
|
||||||
[deriveUserPermissions(url, h), deriveUserIsSpaceAdmin(url)],
|
[deriveUserPermissions(url, h), deriveUserIsSpaceAdmin(url)],
|
||||||
([$permissions, $isSpaceAdmin]) => $isSpaceAdmin || $permissions.has(kind),
|
([$permissions, $isSpaceAdmin]) => $isSpaceAdmin || $permissions.has(kind),
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export const deriveRoomRoleDefinitions = (url: string, h: string) =>
|
export const deriveRoomRoleDefinitions = (url: string, h: string) =>
|
||||||
derived(deriveRoomRoles(url, h), $roomRoles =>
|
derived(deriveRoomRoles(url, h), $roomRoles =>
|
||||||
|
|||||||
Reference in New Issue
Block a user