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

93.22% Statements 55/59
88% Branches 44/50
100% Functions 3/3
96.3% Lines 52/54

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 1131x 1x 1x 1x   1x   1x   1x             1x         1x   1x   1x 205x 205x   205x           205x   205x 205x   205x 205x   205x 11x     194x   194x 194x   144x     50x 50x   50x         50x       50x 63x 63x 16x     47x   47x     47x   32x 15x 7x   8x     15x       50x 19x 19x 32x   32x       31x 31x 1x 1x       49x   49x      
import { getItems, replaceItems } from "feathers-hooks-common";
import { subject } from "@casl/ability";
import _pick from "lodash/pick";
import _isEmpty from "lodash/isEmpty";
import { HookContext } from "@feathersjs/feathers";
import hasRestrictingFields from "../../utils/hasRestrictingFields";
 
import { shouldSkip, mergeArrays } from "feathers-utils";
 
import {
  getPersistedConfig,
  restore$select,
  getAbility,
  makeOptions
} from "./authorize.hook.utils";
 
import getModelName from "../../utils/getModelName";
 
import {
  AuthorizeHookOptions, HasRestrictingFieldsOptions
} from "../../types";
import { Forbidden } from "@feathersjs/errors";
 
const HOOKNAME = "authorize";
 
export default (options: AuthorizeHookOptions): ((context: HookContext) => Promise<HookContext>) => {
  return async (context: HookContext): Promise<HookContext> => {
    const $select = restore$select(context);
 
    Iif (
      shouldSkip(HOOKNAME, context) ||
      context.type !== "after" ||
      !context.params
    ) { return context; }
 
    options = makeOptions(context.app, options);
 
    const modelName = getModelName(options.modelName, context);
    Iif (!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 items = getItems(context);
 
    const availableFields = (!options?.availableFields)
      ? undefined
      : (typeof options.availableFields === "function")
        ? options.availableFields(context)
        : options.availableFields;
    const hasRestrictingFieldsOptions: HasRestrictingFieldsOptions = {
      availableFields: availableFields
    };
 
    const pickFieldsForItem = (item: Record<string, unknown>) => {
      const method = (Array.isArray(items)) ? "find" : "get";
      if (!skipCheckConditions && !ability.can(method, subject(modelName, item))) { 
        return undefined; 
      }
      
      let fields = hasRestrictingFields(ability, method, subject(modelName, item), hasRestrictingFieldsOptions);
 
      Iif (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 (Array.isArray(items)) {
      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);
      if (context.method === "get" && _isEmpty(result)) {
        Iif (options.actionOnForbidden) options.actionOnForbidden();
        throw new Forbidden(`You're not allowed to ${context.method} ${modelName}`);
      }
    }
 
    replaceItems(context, result);
 
    return context;
  };
};