Updating a CASL ability in Quasar Vue3 with Composition API

327 Views Asked by At

I'm using Quasar ^2.0.0 and Vue 3.2.22. I'm using "@casl/vue" and "@casl/ability" for permissions. I'm having trouble updating an existing ability. The idea is to be able to update the app's ability based on a user role received after authentication. When I try to use the update method to update my rules, I get this error:

Uncaught (in promise) TypeError: Cannot read properties of undefined (reading 'm')

Here's where I'm attempting to make the update (post-authentication).

import { AbilityBuilder, Ability } from "@casl/ability";
import { useAbility } from "@casl/vue";

const { update } = useAbility();

function updateAbility() {
  const { rules, can, cannot } = new AbilityBuilder(Ability);

  can(["read", "update", "create", "delete"], "Notes");
  cannot("read", ["Photos", "Videos", "Users", "Files"]);

  update(rules);
}

// Eventually updateAbility will receive a `user` that contains one of several possible roles
onMounted(async () => {
  await onAuthStateChanged(auth, async (user) => {
    ...
    updateAbility();
    ...
  });
});

If I comment out the above updateAbility, I'm able to successfully use can elsewhere in the application based on the configuration in my boot file.

import { useAbility } from "@casl/vue";

const { can } = useAbility();
console.log(can("create", "Videos")); // returns false as expected
casl.js (boot file)
import { boot } from "quasar/wrappers";
import ability from "src/services/ability";
import { abilitiesPlugin } from "@casl/vue";

export default boot(({ app }) => {
  app.use(abilitiesPlugin, ability);

  app.config.globalProperties.$ability = ability;
});

ability.js
import { defineAbility } from "@casl/ability";

export default defineAbility((can, cannot) => {
  cannot(["read", "update", "create", "delete"], "Notes");
  can(
    ["read", "update", "create", "delete"],
    ["Photos", "Videos", "Users", "Files"]
  );
});

I don't know what's causing this error. Any help would be appreciated!

1

There are 1 best solutions below

0
andrewhl On

The solution was actually to use createMongoAbility alongside AbilityBuilder. Unfortunately, the documentation for CASL/Vue is a bit out of date. The key concepts I needed to learn were:

  1. The useAbility composition hook actually returns the full global PureAbility instance, which includes the can and update methods.
  2. The AbilityBuilder instance provides a rules array that contains all the rule information needed to update the ability.
[
    {
        "action": [
            "read",
            "create",
            "update",
            "delete"
        ],
        "subject": "Notes"
    },
    {
        "action": [
            "read",
            "create",
            "update",
            "delete"
        ],
        "subject": "Photos"
    },
    {
        "action": [
            "read",
            "create",
            "update",
            "delete"
        ],
        "subject": "Videos"
    },
    {
        "action": [
            "read",
            "create",
            "update",
            "delete"
        ],
        "subject": "Files"
    }
]
  1. Calling the can or cannot methods provided by AbilityBuilder automatically updates its rules array. You can define any rules you want and simply pass the rules array to your global PureAbility instance using its update method.

Note: The can and cannot methods on the AbilityBuilder are not the same as the can method on the PureAbility instance. This confused me. The former define the rules, and the latter checks them.

import { AbilityBuilder, createMongoAbility } from "@casl/ability";
import { useAbility } from "@casl/vue";

const ability = useAbility();

function defineRulesFor(role: string) {
  const { can, cannot, rules } = new AbilityBuilder(createMongoAbility);

  switch (role) {
    case "parent":
      can(["read", "create", "update", "delete"], "Notes");
      cannot(["read", "create", "update", "delete"], "Photos");
      cannot(["read", "create", "update", "delete"], "Videos");
      cannot(["read", "create", "update", "delete"], "Files");
      break;
    case "teacher":
      can(["read", "create", "update", "delete"], "Notes");
      can(["read", "create", "update", "delete"], "Photos");
      can(["read", "create", "update", "delete"], "Videos");
      can(["read", "create", "update", "delete"], "Files");
      break;
    default:
      break;
  }

  return rules;
}

onMounted(async () => {
  await onAuthStateChanged(auth, async (user) => {

    // This action returns the authUser object that contains the role
    const authUser = await store.dispatch("users/setAuthUser", user);

    const rules = defineRulesFor(authUser.role);
    ability.update(rules);
  });
});