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

0% Statements 0/63
0% Branches 0/51
0% Functions 0/2
0% Lines 0/60

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 151 152 153 154 155 156 157 158 159                                                                                                                                                                                                                                                                                                                             
import { Forbidden } from "@feathersjs/errors";
import _isEmpty from "lodash/isEmpty";
import _pick from "lodash/pick";
import {
  mergeQuery,
  shouldSkip,
  pushSet,
  isMulti
} from "@artesa/feathers-utils";
 
import getQueryFor from "../../utils/getQueryFor";
import hasRestrictingFields from "../../utils/hasRestrictingFields";
import hasRestrictingConditions from "../../utils/hasRestrictingConditions";
 
import {
  makeDefaultOptions,
  hide$select,
  setPersistedConfig,
  checkMulti,
  getAbility,
  throwUnlessCan
} from "./authorize.hook.utils";
 
import {
  HookContext
} from "@feathersjs/feathers";
 
import {
  AuthorizeHookOptions,
  GetQueryOptions
} from "../../types";
 
const HOOKNAME = "authorize";
 
export default (options: AuthorizeHookOptions): ((context: HookContext) => Promise<HookContext>) => {
  options = makeDefaultOptions(options);
  return async (context: HookContext): Promise<HookContext> => {
    //TODO: make as isomorphic hook -> for vue-client
    if (
      shouldSkip(HOOKNAME, context) ||
      context.type !== "before" ||
      !context.params
    ) { return context; }
    const { service, method, id, params } = context;
    const { getModelName } = options;
 
    const modelName = getModelName(context);
    if (!modelName) { return context; }
 
    params.ability = await getAbility(context, options);
    if (!params.ability) {
      // Interne Anfrage oder nicht authentifiziert -> Ignorieren
      return context;
    }
 
    const { ability } = params;
 
    const { subjectHelper, checkMultiActions } = options;
 
    if (checkMultiActions) {
      checkMulti(context, ability, modelName);
    }
 
    throwUnlessCan(ability, method, modelName, modelName);
 
    // if context is with multiple items, there's a change that we need to handle each iteam seperately
    if (isMulti(context)) {
      // if has conditions -> hide $select for after-hook, because
      if (hasRestrictingConditions(ability, "read", modelName)) {
        hide$select(context);
      } else {
        setPersistedConfig(context, "skipRestrictingRead.conditions", true);
      }
 
      // if has no restricting fields at all -> can skip _pick() in after-hook
      if (!hasRestrictingFields(ability, "read", modelName)) {
        setPersistedConfig(context, "skipRestrictingRead.fields", true);
      }
    }
 
    if (["get", "patch", "update", "remove"].includes(method) && id != null) {
      // single: get | patch | update | remove
 
      // get complete item for `throwUnlessCan`-check to be trustworthy
      // -> initial 'get' and 'remove' have no data at all
      // -> initial 'patch' maybe has just partial data
      // -> initial 'update' maybe has completely changed data, for what the check could pass but not for inital data
      const queryGet = Object.assign({}, params.query || {});
      delete queryGet.$select;
      const paramsGet = Object.assign({}, params, { query: queryGet });
      paramsGet.skipHooks = (params.skipHooks && params.skipHooks.slice()) || [];
      pushSet(paramsGet, "skipHooks", `${HOOKNAME}`, { unique: true });
 
      const item = await service.get(id, paramsGet);
 
      throwUnlessCan(ability, method, subjectHelper(modelName, item), modelName);
      if (method === "get") {
        context.result = item;
        //pushSet(context, "params.skipHooks", "after");
        return context;
      }
 
      // ensure that only allowed data gets changed
      if (["update", "patch"].includes(method)) {
        const fields = hasRestrictingFields(ability, method, subjectHelper(modelName, item));
        if (!fields) { return context; }
 
        const data = _pick(context.data, fields);
 
        // if fields are not overlapping -> throw
        if (_isEmpty(data)) {
          throw new Forbidden("You're not allowed to make this request");
        }
 
        //TODO: if some fields not match -> `actionOnForbiddenUpdate`
 
        if (method === "patch") {
          context.data = data;
        } else {
          // merge with inital data
          const itemPlain = await service._get(id);
          context.data = Object.assign({}, itemPlain, data);
        }
      }
 
      return context;
    } else if (method === "find" || (["patch", "remove"].includes(method) && id == null)) {
      // multi: find | patch | remove
      if (hasRestrictingConditions(ability, method, modelName)) {
        // TODO: if query and context.params.query differ -> seperate calls
        const options: GetQueryOptions = {
          skipFields: method === "find"
        };
        const query = getQueryFor(ability, method, modelName, options);
 
        if (!_isEmpty(query)) {
          if (!context.params.query) {
            context.params.query = query;
          } else {
            context.params.query = mergeQuery(context.params.query, query, { defaultHandle: "intersect" });
          }
        }
      }
 
      return context;
    } else if (method === "create") {
      // create: single | multi
      // we have all information we need (maybe we need populated data?)
      const data = (Array.isArray(context.data)) ? context.data : [context.data];
      for (let i = 0; i < data.length; i++) {
        throwUnlessCan(ability, method, subjectHelper(modelName, data[i]), modelName);
      }
      return context;
    }
 
    return context;
  };
};