All files / lib/hooks/authorize authorize.hook.utils.ts

89.33% Statements 67/75
79.71% Branches 55/69
92.86% Functions 13/14
90.16% Lines 55/61

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 1511x 1x 1x   1x         1x                   1x       680x 680x     1x       14x 14x               469x       1x     4x     1x 680x 680x         1x         464x 149x       149x       315x 300x     15x 15x 8x 8x   7x             1x 230x   230x 2x 2x   230x     1x             164x 28x 28x       1x           8x 8x 8x 8x             8x 8x     1x 25x     1x 205x 205x     1x       1x 56x     1x 410x    
import _get from "lodash/get";
import _set from "lodash/set";
import _unset from "lodash/unset";
 
import { Forbidden } from "@feathersjs/errors";
 
import { AnyAbility } from "@casl/ability";
import { Application, HookContext } from "@feathersjs/feathers";
 
import {
  isMulti
} from "feathers-utils";
 
import { 
  AuthorizeHookOptions,
  InitOptions,
  Path
} from "../../types";
 
export const makeOptions = (
  app: Application, 
  options?: Partial<AuthorizeHookOptions>
): AuthorizeHookOptions => {
  options = options || {};
  return Object.assign({}, defaultOptions, getAppOptions(app), options);
};
 
const defaultOptions: AuthorizeHookOptions = {
  ability: undefined,
  actionOnForbidden: undefined,
  availableFields: (context: HookContext): string[] => {
    const availableFields: string[] | ((context: HookContext) => string[]) = context.service.options?.casl?.availableFields;
    Eif (!availableFields) return undefined;
    return (typeof availableFields === "function")
      ? availableFields(context)
      : availableFields;
  },
  checkMultiActions: false,
  checkAbilityForInternal: false,
  modelName: (context: Pick<HookContext, "path">): string => {
    return context.path;
  }
};
 
export const makeDefaultOptions = (
  options?: Partial<AuthorizeHookOptions>
): AuthorizeHookOptions => {
  return Object.assign({}, defaultOptions, options);
};
 
const getAppOptions = (app: Application): AuthorizeHookOptions | Record<string, never> => {
  const caslOptions: InitOptions = app?.get("casl");
  return (caslOptions && caslOptions.authorizeHook)
    ? caslOptions.authorizeHook
    : {};
};
 
export const getAbility = (
  context: HookContext, 
  options?: Pick<AuthorizeHookOptions, "ability" | "checkAbilityForInternal">
): Promise<AnyAbility|undefined> => {
  // if params.ability is set, return it over options.ability
  if (context?.params?.ability) { 
    Iif (typeof context.params.ability === "function") {
      const ability = context.params.ability(context);
      return Promise.resolve(ability);
    } else {
      return Promise.resolve(context.params.ability);
    }
  }
 
  if (!options.checkAbilityForInternal && !context.params?.provider) {
    return Promise.resolve(undefined);
  }
 
  Eif (options?.ability) {
    if (typeof options.ability === "function") {
      const ability = options.ability(context);
      return Promise.resolve(ability);
    } else {
      return Promise.resolve(options.ability);
    }
  }
  
  return Promise.resolve(undefined);
};
 
const move = (context: HookContext, fromPath: Path, toPath: Path) => {
  const val = _get(context, fromPath);
 
  if (val !== undefined) {
    _unset(context, fromPath);
    _set(context, toPath, val);
  }
  return val;
};
 
export const throwUnlessCan = (
  ability: AnyAbility, 
  method: string, 
  resource: string|Record<string, unknown>, 
  modelName: string,
  actionOnForbidden: () => void
): void => {
  if (ability.cannot(method, resource)) {
    Iif (actionOnForbidden) actionOnForbidden();
    throw new Forbidden(`You are not allowed to ${method} ${modelName}`);
  }
};
 
export const checkMulti = (
  context: HookContext, 
  ability: AnyAbility, 
  modelName: string,
  options?: Pick<AuthorizeHookOptions, "actionOnForbidden">
): boolean => {
  const { method } = context;
  const currentIsMulti = isMulti(context);
  Iif (!currentIsMulti) { return true; }
  Iif (
    (method === "find" && ability.can(method, modelName)) ||
    (ability.can(`${method}-multi`, modelName))
  ) {
    return true;
  }
 
  if (options?.actionOnForbidden) options.actionOnForbidden();
  throw new Forbidden(`You're not allowed to multi-${method} ${modelName}`);
};
 
export const hide$select = (context: HookContext): unknown => {
  return move(context, "params.query.$select", "params.casl.$select");
};
 
export const restore$select = (context: HookContext): string[]|undefined => {
  move(context, "params.casl.$select", "params.query.$select");
  return _get(context, "params.query.$select");
};
 
export const get$select = (context: HookContext): unknown => {
  return getPersistedConfig(context, "$select");
};
 
export const setPersistedConfig = (context: HookContext, key: Path, val: unknown): HookContext => {
  return _set(context, `params.casl.${key}`, val);
};
 
export const getPersistedConfig = (context: HookContext, key: Path): unknown => {
  return _get(context, `params.casl.${key}`);
};