feathers-casl

website·Indexed 30 days ago

https://feathers-casl.netlify.app/

A permission management library for FeathersJS based on CASL, providing built-in support for channels, authorization hooks, and utility functions to define who can create, read, update, or delete resources.

Tokens
6.8K
Snippets
25
Records
38
Agent score
75%

What's inside feathers-casl

  1. Implement the authorize hook in services

    The authorize hook enforces the defined CASL rules. It must be configured with the specific database adapter being used. For full functionality, it should be applied as both a before hook (to filter queries) and an after hook (to filter results).
    import { authenticate } from "@feathersjs/authentication";
    import { authorize } from "feathers-casl";
    
    // Specify the adapter (e.g., @feathersjs/mongodb, @feathersjs/knex, etc.)
    const authorizeHook = authorize({ adapter: "@feathersjs/mongodb" });
    
    export default {
      before: {
        all: [authenticate("jwt")],
        find: [authorizeHook],
        get: [authorizeHook],
        create: [authorizeHook],
        update: [authorizeHook],
        patch: [authorizeHook],
        remove: [authorizeHook]
      },
      after: {
        all: [authorizeHook]
      }
    };
  2. Use checkBasicPermission for early request rejection

    checkBasicPermission performs a basic permission check for the current request and rejects it if not allowed. It is designed to be placed early in the hooks chain, typically immediately after authenticate('jwt').

    It can also be used as a before hook on the client side (if the CASL ability is available) to skip API requests entirely and reduce network traffic.

    Warning: Do not use checkBasicPermission exclusively. It must be used in conjunction with the authorize hook to ensure full security and data filtering.

  3. Install feathers-casl and CASL

    To add CASL-based access control to a FeathersJS project, install both feathers-casl and @casl/ability.
    # npm
    npm i feathers-casl @casl/ability
    
    # yarn
    yarn add feathers-casl @casl/ability
    
    # pnpm
    pnpm i feathers-casl @casl/ability
  4. Install CASL for Angular, React, or Aurelia

    To use CASL on the client side with Angular, React, or Aurelia, install the corresponding framework package along with the core @casl/ability package.
    # For Angular
    npm install @casl/angular @casl/ability
    
    # For React
    npm install @casl/react @casl/ability
    
    # For Aurelia
    npm install @casl/aurelia @casl/ability
  5. Use feathers-casl with the Express REST transport

    When using the Express transport (without sockets), abilities must be defined manually in the service hooks. Place the ability definition logic after the authenticate() hook and before the authorize() hook to ensure the user is identified before permissions are assigned.
    // src/services/tasks/tasks.hooks.ts
    
    export default {
      before: {
        all: [
          authenticate("jwt"),
    
          // Set abilities if a user exists
          (context) => {
            const { user } = context.params;
            if (user) context.params.ability = defineAbilitiesFor(user);
            return context;
          }
        ]
        // ...
      }
      // ...
    };
  6. Implement CASL with FeathersVuex

    Integrating CASL with FeathersVuex requires a custom Vuex plugin to synchronize ability rules with the authentication state. The plugin ensures rules are fetched upon authentication and cleared upon logout. The implementation involves three steps: creating the Vuex plugin, registering the plugin in the store, and registering the @casl/vue plugin in the main Vue instance.
    // 1. Create the Vuex plugin (src/store/vuex.plugin.casl.ts)
    import { Ability, createAliasResolver, detectSubjectType as defaultDetector } from "@casl/ability";
    import { BaseModel } from "@/@src/store/feathers/client.js";
    
    const detectSubjectType = (subject) => {
      if (typeof subject === "string") return subject;
      if (!(subject instanceof BaseModel)) return defaultDetector(subject);
      return subject.constructor.servicePath;
    };
    
    const resolveAction = createAliasResolver({
      update: "patch",
      read: ["get", "find"],
      delete: "remove"
    });
    
    const ability = new Ability([], { detectSubjectType, resolveAction });
    
    const caslPlugin = (store) => {
      store.registerModule("casl", {
        namespaced: true,
        state: { ability, rules: [] },
        mutations: {
          setRules(state, rules) {
            state.rules = rules;
            state.ability.update(rules);
          }
        }
      });
      store.subscribeAction({
        after: (action, state) => {
          if (action.type === "auth/responseHandler") {
            const { rules } = action.payload;
            if (!rules || !state.auth.user) {
              store.commit("casl/setRules", []);
              return;
            }
            store.commit("casl/setRules", rules);
          } else if (action.type === "auth/logout") {
            store.commit("casl/setRules", []);
          }
        }
      });
    };
    
    export { ability, caslPlugin };
    
    // 2. Insert into store (src/store/index.ts)
    import { caslPlugin } from '@/store/vuex.plugin.casl';
    export const store = new Vuex.Store({
      plugins: [caslPlugin],
    });
    
    // 3. Register @casl/vue plugin (main.ts)
    import Vue from "vue";
    import { abilitiesPlugin } from "@casl/vue";
    import { store } from "@/store";
    
    Vue.use(abilitiesPlugin, store.state.casl.ability);
  7. Implement permission-based real-time publishing with feathers-casl

    feathers-casl provides built-in support for FeathersJS channels, allowing you to publish events only to users who have the required CASL permissions to read the data. This is specifically for real-time connections (e.g., socket.io) and is not applicable to REST-only apps. To implement this, you must map the user's ability to the connection during login and use getChannelsWithReadAbility within the app.publish method.
    // src/channels.ts
    import { getChannelsWithReadAbility, makeChannelOptions } from "feathers-casl";
    
    export default function (app) {
      if (typeof app.channel !== "function") {
        return;
      }
    
      app.on("login", (authResult: any, { connection }) => {
        if (connection) {
          // Map ability from auth hook to connection so it is available in HookContext as `params.ability`
          if (authResult.ability) {
            connection.ability = authResult.ability;
            connection.rules = authResult.rules;
          }
        }
      });
    
      const caslOptions = makeChannelOptions(app);
    
      app.publish((data, context) => {
        return getChannelsWithReadAbility(app, data, context, caslOptions);
      });
    }
  8. Integrate feathers-casl with FeathersJS channels for real-time updates

    To ensure users only receive real-time updates for items they have permission to read, integrate feathers-casl into your channels.ts configuration. This involves mapping the ability from the authentication result to the connection and using getChannelsWithReadAbility within the app.publish method.
    // src/channels.ts
    import { getChannelsWithReadAbility, makeChannelOptions } from "feathers-casl";
    
    export default function (app) {
      if (typeof app.channel !== "function") {
        return;
      }
    
      app.on("login", (authResult: any, { connection }) => {
        if (connection) {
          // Map ability from auth hook to connection so it is available in HookContext as `params.ability`
          if (authResult.ability) {
            connection.ability = authResult.ability;
            connection.rules = authResult.rules;
          }
        }
      });
    
      const caslOptions = makeChannelOptions(app);
    
      app.publish((data, context) => {
        return getChannelsWithReadAbility(app, data, context, caslOptions);
      });
    }
  9. Inject CASL abilities into the hooks context

    To make abilities available to the authorize hook and channels, attach the generated ability to the user object or context params during the authentication process. feathers-casl looks for context.params.ability in hooks and connection.ability in channels.
    // src/services/authentication/authentication.hooks.ts
    import { defineAbilitiesFor } from "./abilities";
    
    export default {
      after: {
        create: [
          (context) => {
            const { user } = context.result;
            if (!user) return context;
            const ability = defineAbilitiesFor(user);
            context.result.ability = ability;
            context.result.rules = ability.rules;
            return context;
          }
        ]
      }
    };
  10. Manage rule priority in CASL definitions

    In CASL, rules are processed in the order they are defined. Subsequent rules overwrite or further specify the rules defined before them. If a cannot rule follows a can rule for the same action and subject, the cannot rule will take precedence.
    const ability = defineAbility((can, cannot) => {
      can("read", "users");
      cannot("read", "users"); // This overrides the previous 'can' rule
    });
  11. Define conditional field-level permissions correctly

    When defining field-level permissions (subsets) based on conditions, avoid overlapping rules that might lead to unexpected $select behavior. Instead of defining a general field list and then adding a specific field for a specific user, define the general field list for users who do not meet the specific condition, and a separate rule for the user who does.
    const user = { id: 1 };
    
    const ability = defineAbility((can, cannot) => {
      // Give basic fields to everyone EXCEPT the current user
      can("read", "users", ["id", "name", "email"], { id: { $ne: 1 } });
      // Give full access to the current user
      can("read", "users", { id: user.id });
    });
  12. Use the checkBasicPermission hook for early request rejection

    The checkBasicPermission hook performs a lightweight permission check designed to reject unauthorized requests at an early stage. It does not handle complex conditions or field-level permissions.

    For all methods except create, it performs a simple can(method, modelName) check. For create operations, it can perform a can('create', item) check if the checkCreateForData option is enabled.

    If checkMultiActions is set to true, the hook also validates permissions for multi-record operations using the following CASL rules:

    • find: can('find', modelName)
    • multi:create: can('create-multi', modelName)
    • multi:patch: can('patch-multi', modelName)
    • multi:remove: can('remove-multi', modelName)