feathers-casl
Version:
Add access control with CASL to your feathers application.
164 lines (135 loc) • 4.05 kB
text/typescript
import { subject } from '@casl/ability'
import _pick from 'lodash/pick.js'
import _isEmpty from 'lodash/isEmpty.js'
import { getResultIsArray, mutateResult } from 'feathers-utils'
import { shouldSkip, mergeArrays } from '@fratzinger/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, result: items } = getResultIsArray(context)
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 as typeof items
}
}
}
const pickFieldsForItem = (item: Record<string, unknown>) => {
if (
!skipCheckConditions &&
!ability.can(getOrFind, subject(modelName, item))
) {
return undefined
}
const restrictingFields = hasRestrictingFields(
ability,
getOrFind,
subject(modelName, item),
hasRestrictingFieldsOptions,
)
if (restrictingFields === true) {
// full restriction
return {}
} else if (skipCheckFields || (!restrictingFields && !$select)) {
// no restrictions
return item
}
const pickFields: string[] =
restrictingFields && $select
? (mergeArrays(restrictingFields, $select, 'intersect') as string[])
: ((restrictingFields || $select) as string[])
return _pick(item, pickFields)
}
let newResult: (Record<string, unknown> | undefined)[]
if (isArray) {
newResult = items
.map(pickFieldsForItem)
.filter((x): x is Record<string, unknown> => !!x)
} else {
const single = pickFieldsForItem(items[0])
if (method === 'get' && _isEmpty(single)) {
if (options.actionOnForbidden) options.actionOnForbidden()
/* v8 ignore start */
if (options.debug) {
console.error(
'Feathers-CASL: authorizeAfter hook - all fields are restricted for this action',
method,
modelName,
items[0],
)
}
/* v8 ignore stop */
throw new Forbidden(`You're not allowed to ${method} ${modelName}`)
}
newResult = [single]
}
await mutateResult(context, (item) => item, {
transform: () => newResult as any[],
})
return context
}