103 lines
2.3 KiB
TypeScript
103 lines
2.3 KiB
TypeScript
export type Scope = 'none' | 'own' | 'all'
|
|
|
|
export const SCOPE_RANK: Record<Scope, number> = { none: 0, own: 1, all: 2 }
|
|
|
|
export function isScope(value: unknown): value is Scope {
|
|
return typeof value === 'string' && Object.keys(SCOPE_RANK).includes(value)
|
|
}
|
|
|
|
export interface Grant {
|
|
key: string
|
|
scope: Scope
|
|
}
|
|
|
|
export interface RoleRef {
|
|
id: number
|
|
name: string
|
|
built_in: boolean
|
|
locked: boolean
|
|
}
|
|
|
|
export interface AssignedBy {
|
|
id: number
|
|
name: string
|
|
}
|
|
|
|
export interface Access {
|
|
role: RoleRef
|
|
permissions: Grant[]
|
|
roleAssignedBy?: AssignedBy | null
|
|
}
|
|
|
|
export function parseRoleAssignedBy(value: unknown): AssignedBy | null {
|
|
if (typeof value !== 'object' || value === null) {
|
|
return null
|
|
}
|
|
const { id, name } = value as { id?: unknown; name?: unknown }
|
|
return typeof id === 'number' && typeof name === 'string' && name !== '' ? { id, name } : null
|
|
}
|
|
|
|
export interface CanOptions {
|
|
ownerId?: number
|
|
typeKey?: string
|
|
scope?: 'own' | 'all'
|
|
}
|
|
|
|
export interface ScopeOptions {
|
|
typeKey?: string
|
|
}
|
|
|
|
const TYPE_SEPARATOR = ':'
|
|
|
|
export function formatGrantKey(key: string, typeKey?: string) {
|
|
return typeKey === undefined ? key : `${key}${TYPE_SEPARATOR}${typeKey}`
|
|
}
|
|
|
|
export function parseGrantKey(key: string): { base: string; typeKey?: string } {
|
|
const at = key.indexOf(TYPE_SEPARATOR)
|
|
if (at === -1) {
|
|
return { base: key }
|
|
}
|
|
return { base: key.slice(0, at), typeKey: key.slice(at + TYPE_SEPARATOR.length) }
|
|
}
|
|
|
|
function scopeOfGrant(grants: readonly Grant[], key: string): Scope | undefined {
|
|
return grants.find((grant) => grant.key === key)?.scope
|
|
}
|
|
|
|
export function effectiveScope(
|
|
grants: readonly Grant[],
|
|
key: string,
|
|
options: ScopeOptions = {},
|
|
): Scope {
|
|
if (options.typeKey) {
|
|
const typed = scopeOfGrant(grants, formatGrantKey(key, options.typeKey))
|
|
if (typed !== undefined) {
|
|
return typed
|
|
}
|
|
}
|
|
return scopeOfGrant(grants, key) ?? 'none'
|
|
}
|
|
|
|
export function allows(
|
|
grants: readonly Grant[],
|
|
userId: number | undefined,
|
|
key: string,
|
|
options: CanOptions = {},
|
|
): boolean {
|
|
const scope = effectiveScope(grants, key, options)
|
|
if (scope === 'none') {
|
|
return false
|
|
}
|
|
if (scope === 'all') {
|
|
return true
|
|
}
|
|
if (options.scope === 'all') {
|
|
return false
|
|
}
|
|
if (options.ownerId === undefined) {
|
|
return true
|
|
}
|
|
return userId !== undefined && options.ownerId !== 0 && options.ownerId === userId
|
|
}
|