Skip to content

Instantly share code, notes, and snippets.

@philipimperato
Last active February 12, 2024 18:15
Show Gist options
  • Save philipimperato/38172ceac9ca67f78bcfe0c5c074884e to your computer and use it in GitHub Desktop.
Save philipimperato/38172ceac9ca67f78bcfe0c5c074884e to your computer and use it in GitHub Desktop.
Using feather-casl with feathers-pinia, nuxt 3, and feathersjs (Dove)

Using feathers-casl with feathers-pinia and feathers Dove

Resources

Feathers Dove + feathers-casl API

Required packages

casl @casl/ability@5 feathers-casl@pre 

Configure in app.js

import casl from 'feathers-casl'
app.configure(casl())

Create authentication/authentication.abilities.js

import { AbilityBuilder, Ability, createAliasResolver } from '@casl/ability';

// don't forget this, as `read` is used internally
const resolveAction = createAliasResolver({
  update: 'patch',       // define the same rules for update & patch
  read: ['get', 'find'], // use 'read' as a equivalent for 'get' & 'find'
  delete: 'remove'       // use 'delete' or 'remove'
});

export default function defineAbilitiesFor(user) {
    const { can, cannot, build } = new AbilityBuilder(Ability);

    // How to write rules can be found: 
    if (user.isSuperAdmin) {
        can('manage', 'all')
    } else if(user.isAdmin) {
        can('read', 'all')
    } else {
        // insure the second param matches the service name
        can('read', 'posts')
    }

    return build({ resolveAction });
}

Add abilities to authentication create hook

// /authentication/authenication.hooks.js
import defineAbilitiesFor from './authentication.abilities.js'

export const hooks = {
  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;
      }
    ]
  }
}

Add hook to existing authentication service

// /authentication.js
import { hooks } from './services/authentication/authentication.hooks.js'
app.use('authentication', authentication)
app.service('authentication').hooks(hooks)

Manually add to channels so ability loads into context.params.ability

app.on('login', (authResult, { connection }) => {
    if (connection) {
      if (authResult.ability) {
        connection.ability = authResult.ability;
        connection.rules = authResult.rules;
      }
    
      app.channel('anonymous').leave(connection)
      app.channel('authenticated').join(connection)
    }
})

Authorize each service

Insure that the value you're attempting to limit in the casl ability is in also in the valid query properties!

// /services/service/service.js
import { authorize } from 'feathers-casl'

app.service('service').hooks({
  around: {
    all: [ 
      authenticate('jwt'),
      authorize(),
      schemaHooks.resolveExternal(listingsExternalResolver),
      schemaHooks.resolveResult(listingsResolver),
    ]
  }
})

If you're using mongoDB you need to manually add the $and property to each service

// /services/my-custom-service.class.js
import { MongoDBService } from '@feathersjs/mongodb'

// By default calls the standard MongoDB adapter service methods but can be customized with your own functionality.
export class MyCustomService extends MongoDBService {}

export const getOptions = (app) => {
  return {
    paginate: app.get('paginate'),
    Model: app.get('mongodbClient').then((db) => db.collection('my-custom-service')),
    filters: { $and: true } <- ADD THIS
  }
}

Feathers Pinia (Nuxt 3 & nuxt-feathers-pinia)

Required packages

casl @casl/ability@5 @casl/vue feathers-casl@pre

Create pinia casl store

// /stores/store.casl.ts
import { defineStore } from 'pinia'
import { Ability, createAliasResolver } from '@casl/ability';

const resolveAction = createAliasResolver({
  update: 'patch',       // define the same rules for update & patch
  read: ['get', 'find'], // use 'read' as a equivalent for 'get' & 'find'
  delete: 'remove'       // use 'delete' or 'remove'
});

const detectSubjectType = (subject: any) => {
  if (typeof subject === 'string') return subject;
  return subject.constructor.servicePath;
}

const ability = new Ability([], { detectSubjectType, resolveAction });

export type ICaslState = {
  ability: any,
  rules: any[]
}

export const useCaslStore = defineStore('casl', {
  state: (): ICaslState => ({
    ability,
    rules: []
  }),
  actions: {
    setRules(rules: any) {
      this.rules = rules
      this.ability.update(rules)
    }
  },
})

Add Casl Plugin 2.feathers-casl.ts

import { abilitiesPlugin } from '@casl/vue';

export default defineNuxtPlugin(nuxtApp => {
  const caslStore = useCaslStore()
  nuxtApp.vueApp.use(abilitiesPlugin, caslStore.ability)
})

Add .$onAction in 3.feathers-auth.ts before await reAuthenticate()

const caslStore = useCaslStore()
authStore.$onAction(async ({ name, after }) => {
  switch (name) {
    case 'authenticate':
    case 'reAuthenticate': {
      after(result => {
        const rules = result ? result.rules : []
        caslStore.setRules(rules)
      })
    } break;
    case 'logout': 
    case 'isTokenExpired': {
      after(() => caslStore.setRules([]))
    } break;
    default:
      break;
  }
})

Use $can and $cannot in your components

// /composables/userPermissions.ts
import { useAbility } from '@casl/vue';

export const usePermissions = () => {
  const { can, cannot } = useAbility()
  return { $can: can, $cannot: cannot }
} 

// in component
const { $can } = usePermissions()

Gotchas

Error: You're not allowed to get on 'users'

Because the users table is required to check validation, you need to manually assign the ability outside the around hook and in the before get hook

// /services/users.js
import { authorize } from 'feathers-casl'
import defineAbilitiesFor from './../authentication/authentication.abilities.js'
before: {
  get: [
    authenticate('jwt'),
    context => {
      if (context.params.ability) { return context; }
      const { user } = context.params
      if (user) context.params.ability = defineAbilitiesFor(user)
      return context
    },
    authorize({ adapter: '@feathersjs/mongodb' }),
  ],
}

Error: Invalid filter value $and

Remove the _id assignment that comes default generating a new dove app

export const userQueryResolver = resolve({
  // remove this `_id` resolver below
  _id: async (value, user, context) => {
    if (context.params.user) {
      return context.params.user._id 
    }
    return value
  }
})
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment