integreat-io/integreat

View on GitHub
src/service/utils/authData.ts

Summary

Maintainability
B
6 hrs
Test Coverage
import { validateRoleOrIdent } from './authAction.js'
import getField from '../../utils/getField.js'
import { arrayIncludes } from '../../utils/array.js'
import { isTypedData, isNullOrUndefined, isRootIdent } from '../../utils/is.js'
import type { Action, Response, TypedData, Ident } from '../../types.js'
import type Schema from '../../schema/Schema.js'
import type { Access } from '../../schema/types.js'
 
export type AuthorizeDataFn = (action: Action, allowRaw?: boolean) => Action
 
const isStringOrArray = (value: unknown): value is string | string[] =>
typeof value === 'string' || Array.isArray(value)
 
function getValueAndCompare(
item: TypedData,
fieldPath: string,
compareValue?: string | string[],
) {
const values = getField(item, fieldPath)
return isStringOrArray(values) && arrayIncludes(compareValue, values)
}
 
const isIdentFromFieldOrIdentOk = (
item: TypedData,
field?: string,
present?: string | string[],
) => field && getValueAndCompare(item, field, present)
 
const isItemAuthorized = (item: TypedData, access: Access, ident?: Ident) =>
(!access.identFromField && !access.roleFromField) || // No data based auth required
validateRoleOrIdent(access.role, ident?.roles) || // We are authorized by role
validateRoleOrIdent(access.ident, ident?.id) || // We are authorized by ident
isIdentFromFieldOrIdentOk(item, access.identFromField, ident?.id) || // We are authorized by identFromField, or it's not needed
isIdentFromFieldOrIdentOk(item, access.roleFromField, ident?.roles) // We are authorized by roleFromField, or it's not needed
 
const authorizeItem =
Function `authorizeItem` has a Cognitive Complexity of 13 (exceeds 5 allowed). Consider refactoring.
(
schemas: Map<string, Schema>,
actionType: string,
allowRaw: boolean,
ident?: Ident,
) =>
(item: unknown): string | undefined => {
if (!isTypedData(item)) {
return allowRaw ? undefined : 'RAW_DATA'
}
const schema = schemas.get(item.$type)
if (!schema) {
return 'NO_SCHEMA'
}
 
const access = schema.accessForAction(actionType)
if (isItemAuthorized(item, access, ident)) {
// We either are already authorized trough `role` or `ident`, we have no
// `identFromField` or `roleFromField`, or we have are authorized by one
// of them
return undefined
} else {
// Authorization failed and we'll return the reason. If both ident and
// role is missing, we'll return `WRONG_IDENT` as the reason.
return access.identFromField ? 'WRONG_IDENT' : 'MISSING_ROLE'
}
}
 
const generateWarning = (removedCount: number, isToService: boolean) =>
removedCount > 0
? `${removedCount} item${
removedCount === 1 ? ' was' : 's were'
} removed from ${
isToService ? 'request' : 'response'
} data due to lack of access`
: undefined
 
Function `generateErrorAndReason` has a Cognitive Complexity of 10 (exceeds 5 allowed). Consider refactoring.
const generateErrorAndReason = (
reason: string,
data: unknown,
isToService: boolean,
service?: string,
) =>
reason === 'RAW_DATA'
? `Authentication was refused for raw ${
isToService ? 'request' : 'response'
} data${
service ? ` ${isToService ? 'to' : 'from'} service '${service}'` : ''
}`
: `Authentication was refused for type '${(data as TypedData).$type}'${
service ? ` on service '${service}'` : ''
}`
 
/**
* Authorize an array of data items, and remove the ones that are not
* authorized. Wrap the data in a response object and include a warning if
* any items were removed.
*/
function authorizeDataArrayAndWrapInResponse(
data: unknown[],
authItemFn: (item: unknown) => string | undefined,
isToService: boolean,
) {
const authed = data.filter((data: unknown) => authItemFn(data) === undefined)
const warning = generateWarning(data.length - authed.length, isToService)
return { data: authed, warning }
}
 
/**
* Authorize a single data item and wrap it in a response object if successful.
* If not authorized, we return a `noaccess` response.
*/
function authorizeDataItemAndWrapInResponse(
data: unknown,
authItemFn: (item: unknown) => string | undefined,
isToService: boolean,
service?: string,
) {
if (!isNullOrUndefined(data)) {
const reason = authItemFn(data)
if (typeof reason === 'string') {
const error = generateErrorAndReason(reason, data, isToService, service)
return {
data: undefined,
status: 'noaccess',
error,
reason,
origin: 'auth:data',
}
}
}
return { data }
}
 
const authorizeDataAndWrapInResponse = (
data: unknown,
authItemFn: (item: unknown) => string | undefined,
isToService: boolean,
service?: string,
): Response =>
Array.isArray(data)
? authorizeDataArrayAndWrapInResponse(data, authItemFn, isToService)
: authorizeDataItemAndWrapInResponse(data, authItemFn, isToService, service)
 
const isError = (status?: string | null) =>
typeof status === 'string' && status !== 'ok'
 
Function `authorizeDataBase` has a Cognitive Complexity of 15 (exceeds 5 allowed). Consider refactoring.
const authorizeDataBase = (
schemas: Map<string, Schema>,
isToService: boolean,
) =>
Function `authorizeData` has 46 lines of code (exceeds 25 allowed). Consider refactoring.
function authorizeData(action: Action, allowRaw = false): Action {
if (isRootIdent(action.meta?.ident)) {
return action
}
 
const {
type: actionType,
payload: { targetService },
meta: { ident } = {},
} = action
const {
data,
status: authedStatus,
error,
reason,
warning,
} = authorizeDataAndWrapInResponse(
isToService ? action.payload.data : action.response?.data,
authorizeItem(schemas, actionType, allowRaw, ident),
isToService,
targetService,
)
const status =
isError(action.response?.status) || !authedStatus
? action.response?.status
: authedStatus
 
const response =
status !== undefined || warning
? {
...action.response,
...(!isToService && { data }),
...(!action.response?.error && error && { error }),
...(!action.response?.error && reason && { reason }),
...(warning && { warning }),
...(isError(status)
? { origin: action.response?.origin || 'auth:data' }
: {}),
status,
}
: undefined
 
return {
...action,
payload: {
...action.payload,
...(isToService && { data }),
},
...(response && { response }),
}
}
 
export const fromService = (schemas: Map<string, Schema>): AuthorizeDataFn =>
authorizeDataBase(schemas, false)
 
export const toService = (schemas: Map<string, Schema>): AuthorizeDataFn =>
authorizeDataBase(schemas, true)