Using feathers-casl
with feathers-pinia
and feathers Dove
- How to define CASL rules
- Feathers-CASL Dove release v1.0.1
- Feathers-CASL Feathers-vuex documentation (most of this comes from here)
casl @casl/ability@5 feathers-casl@pre
import casl from 'feathers-casl'
app.configure(casl())
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 });
}
// /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;
}
]
}
}
// /authentication.js
import { hooks } from './services/authentication/authentication.hooks.js'
app.use('authentication', authentication)
app.service('authentication').hooks(hooks)
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)
}
})
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),
]
}
})
// /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)
casl @casl/ability@5 @casl/vue feathers-casl@pre
// /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)
}
},
})
import { abilitiesPlugin } from '@casl/vue';
export default defineNuxtPlugin(nuxtApp => {
const caslStore = useCaslStore()
nuxtApp.vueApp.use(abilitiesPlugin, caslStore.ability)
})
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;
}
})
// /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()
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
}
})