feathers-casl
Version:
Add access control with CASL to your feathers application.
155 lines (126 loc) • 3.61 kB
text/typescript
import { replaceItems } from 'feathers-hooks-common'
import { subject } from '@casl/ability'
import _pick from 'lodash/pick.js'
import _isEmpty from 'lodash/isEmpty.js'
import { shouldSkip, mergeArrays, getItemsIsArray } from 'feathers-utils'
import {
getPersistedConfig,
getAbility,
makeOptions,
getConditionalSelect,
refetchItems,
HOOKNAME,
} from './authorize.hook.utils.js'
import {
getAvailableFields,
hasRestrictingFields,
getModelName,
} from '../../utils/index.js'
import { Forbidden } from '@feathersjs/errors'
import type { HookContext } from '@feathersjs/feathers'
import type {
AuthorizeHookOptions,
HasRestrictingFieldsOptions,
} from '../../types.js'
import { getMethodName } from '../../utils/getMethodName.js'
export const authorizeAfter = async <H extends HookContext = HookContext>(
context: H,
options: AuthorizeHookOptions,
) => {
if (shouldSkip(HOOKNAME, context, options) || !context.params) {
return context
}
// eslint-disable-next-line prefer-const
let { isArray, items } = getItemsIsArray(context, { from: 'result' })
if (!items.length) {
return context
}
options = makeOptions(context.app, options)
const modelName = getModelName(options.modelName, context)
if (!modelName) {
return context
}
const skipCheckConditions = getPersistedConfig(
context,
'skipRestrictingRead.conditions',
)
const skipCheckFields = getPersistedConfig(
context,
'skipRestrictingRead.fields',
)
if (skipCheckConditions && skipCheckFields) {
return context
}
const { params } = context
params.ability = await getAbility(context, options)
if (!params.ability) {
// Ignore internal or not authenticated requests
return context
}
const { ability } = params
const availableFields = getAvailableFields(context, options)
const hasRestrictingFieldsOptions: HasRestrictingFieldsOptions = {
availableFields: availableFields,
}
const getOrFind = isArray ? 'find' : 'get'
const $select: string[] | undefined = params.query?.$select
const method = getMethodName(context, options)
if (method !== 'remove') {
const $newSelect = getConditionalSelect(
$select,
ability,
getOrFind,
modelName,
)
if ($newSelect) {
const _items = await refetchItems(context)
if (_items) {
items = _items
}
}
}
const pickFieldsForItem = (item: Record<string, unknown>) => {
if (
!skipCheckConditions &&
!ability.can(getOrFind, subject(modelName, item))
) {
return undefined
}
let fields = hasRestrictingFields(
ability,
getOrFind,
subject(modelName, item),
hasRestrictingFieldsOptions,
)
if (fields === true) {
// full restriction
return {}
} else if (skipCheckFields || (!fields && !$select)) {
// no restrictions
return item
} else if (fields && $select) {
fields = mergeArrays(fields, $select, 'intersect') as string[]
} else {
fields = fields ? fields : $select
}
return _pick(item, fields)
}
let result
if (isArray) {
result = []
for (let i = 0, n = items.length; i < n; i++) {
const item = pickFieldsForItem(items[i])
if (item) {
result.push(item)
}
}
} else {
result = pickFieldsForItem(items[0])
if (method === 'get' && _isEmpty(result)) {
if (options.actionOnForbidden) options.actionOnForbidden()
throw new Forbidden(`You're not allowed to ${method} ${modelName}`)
}
}
replaceItems(context, result)
return context
}