I just got a PM asking how to do this, so figured I’d write it up for any interested…
First, create a boot file like this:
import { abilitiesPlugin } from '@casl/vue'
export default ({ Vue }) => {
Vue.use(abilitiesPlugin)
}
In your boot folder, have a subfolder or file containing your abilities:
/* ability objects look like this:
* {"name": "Role edit", "subject": "Role", "actions": "edit", "inverted": "false", "conditions": "???", "fields": "???", "reason": "???"}
* -- name, subject, and actions are mandatory.
* -- Inverted defaults to false.
* -- Conditions must use fields belonging to the model used in Subject. *** Note: We aren't tying permissions to models in our application.
* -- Fields is used when field-level permissions are desired
* -- Reason is usually set for inverted rules and can be used to tell a user why they can"t do something.
*
* See also: https://stalniy.github.io/casl/abilities/2017/07/20/define-abilities.html
*/
// TODO move this to only the server side and add an API endpoint to retrieve whatever we need from there instead of here
export const abilities = '{"allAbilities": [' +
' {"group_name": "Administration",' +
' "description": "Administer this application",' +
' "abilities": [' +
' {"name": "Administer manage", "subject": "Administer", "actions": "manage"}' +
' ]' +
' },' +
' { "group_name": "CQA",' +
' "description": "View and/or edit CQA",' +
' "abilities": [' +
' {"name": "CQA edit", "subject": "CQA", "actions": "edit"},' +
' {"name": "CQA view", "subject": "CQA", "actions": "view"}' +
' ]' +
' },' +
' { "group_name": "Kinetics",' +
' "description": "View and/or edit Kinetics",' +
' "abilities": [' +
' {"name": "Kinetics edit", "subject": "Kinetics", "actions": "edit"},' +
' {"name": "Kinetics view", "subject": "Kinetics", "actions": "view"}' +
' ]' +
' },' +
' {"group_name": "Permissions",' +
' "description": "View defined permissions (CASL abilities)",' +
' "abilities": [' +
' {"name": "Permissions view", "subject": "Permission", "actions": "view"}' +
' ]' +
' },' +
' {"group_name": "PharmAssist",' +
' "description": "Permissions for the PharmAssist workflows",' +
' "abilities": [' +
' {"name": "PharmAssist view", "subject": "PharmAssist", "actions": "view"},' +
' {"name": "PharmAssist edit", "subject": "PharmAssist", "actions": "edit"},' +
' {"name": "PharmAssist Reports view", "subject": "PharmAssist Reports", "actions": "view"}' +
' ]' +
' },' +
' { "group_name": "Reports",' +
' "description": "View and/or manage reports",' +
' "abilities": [' +
' {"name": "Reports edit", "subject": "Reports", "actions": "edit"},' +
' {"name": "Reports view", "subject": "Reports", "actions": "view"}' +
' ]' +
' },' +
' { "group_name": "Role",' +
' "description": "View and/or manage roles",' +
' "abilities": [' +
' {"name": "Role edit", "subject": "Role", "actions": "edit"},' +
' {"name": "Role delete", "subject": "Role", "actions": "delete"},' +
' {"name": "Role view", "subject": "Role", "actions": "view"}' +
' ]' +
' },' +
' {"group_name": "RolePerms",' +
' "description": "View and/or manage rolesPerms",' +
' "abilities": [' +
' {"name": "RolesPerms edit", "subject": "RolesPerms", "actions": "edit"},' +
' {"name": "RolesPerms view", "subject": "RolesPerms", "actions": "view"}' +
' ]' +
' },' +
' { "group_name": "Training",' +
' "description": "View and/or edit training",' +
' "abilities": [' +
' {"name": "Training edit", "subject": "Training", "actions": "edit"},' +
' {"name": "Training view", "subject": "Training", "actions": "view"}' +
' ]' +
' },' +
' {"group_name": "Users",' +
' "description": "View and/or manage users\' roles",' +
' "abilities": [' +
' {"name": "User edit", "subject": "User", "actions": "edit"},' +
' {"name": "User delete", "subject": "User", "actions": "delete"},' +
' {"name": "User view", "subject": "User", "actions": "view"}' +
' ]' +
' },' +
' {"group_name": "Groups",' +
' "description": "View and/or manage group\' roles",' +
' "abilities": [' +
' {"name": "Group edit", "subject": "Group", "actions": "edit"},' +
' {"name": "Group delete", "subject": "Group", "actions": "delete"},' +
' {"name": "Group view", "subject": "Group", "actions": "view"}' +
' ]' +
' },' +
' {"group_name": "Security and Logs",' +
' "description": "View and/or manage logs",' +
' "abilities": [' +
' {"name": "Watchdog view", "subject": "Watchdog", "actions": "view"},' +
' {"name": "ReportUsage view", "subject": "ReportUsage", "actions": "view"},' +
' {"name": "Session view", "subject": "Session", "actions": "view"}' +
' ]' +
' }' +
']' +
'}'
export function displayAbilities () {
let newAblArray = []
let abl = JSON.parse(abilities)
abl.allAbilities.forEach((element) => {
element.abilities.forEach((el) => {
el.id = newAblArray.length + 1
if (typeof el.inverted === 'undefined') {
el.inverted = false
}
newAblArray.push(el)
})
})
// console.log('newAbl', newAblArray)
return newAblArray
}
export function displayAbilitiesGroups () {
var abl = JSON.parse(abilities, (key, value) => {
return key === 'abilities' ? '' : value
})
// console.log('ability groups', abl)
return abl.allAbilities
}
export function getAbilityTree () {
var abl = JSON.parse(abilities)
// console.log('ability groups', abl)
return abl.allAbilities
}
You can intercept users before they go to a page using something like the below. I put it in my boot folder:
/**
* This file is to be used to control Vue's routing behavior. We can and should intercept
* users who attempt to go to a URL without authorization. They could do this by bookmarking
* a path they no longer have access for. This is the more fool-proof protection of our content
* than the permissions checking we have on menu items and buttons, although that is important
* too.
*
* 1) Use the routes.js file to identify the routes that need protection.
* 2) Use the permissions defined in abilities.js as the things to check.
*
* Q: Why do we use 'can' and repeat a call to next()? Can't we just use cannot and fall
* through to a common next()?
*
* A: Using cannot is a bad idea because all of our rules are positive. We must ensure someone
* CAN do something before sending them along. Additionally, what if we forget to update this
* file with new rules or paths? We need to default to blocking access, not granting it.
*/
// import Ability
import { Ability } from '@casl/ability'
const routeAbility = new Ability()
export default ({ app, router, store, Vue }) => {
router.beforeEach((to, from, next) => {
const redirectPath = {
path: '/',
query: { redirect: to.path }
}
routeAbility.update(store.state.account.abilityRules)
// Now you need to add your authentication logic here, like calling an API endpoint
// console.log('to', to)
// console.log('from', from)
if (to.matched.some(record => record.meta.requiresAuth)) {
if (!store.state.account.authenticated) {
// console.log('not authenticated')
next(redirectPath)
} else {
// console.log('authenticated')
if (store.state.account.abilityRules.length !== 0) {
// console.log('not empty')
// console.log('abilityRules', store.state.account.abilityRules)
//* ****************** Administration *************************************** userRoles
if (to.matched.some(record => record.path === '/users')) {
// console.log('users entered')
// console.log('routeAbility', routeAbility)
if (routeAbility.can('manage', 'Administer')) {
// console.log('can adminster users')
if (to.matched.some(record => record.path === '/users/userRoles')) {
if (routeAbility.can('view', 'User')) {
// console.log('can user')
next()
}
} else if (to.matched.some(record => record.path === '/users/groupRoles')) {
if (routeAbility.can('view', 'Group')) {
// console.log('can group')
next()
}
} else if (to.matched.some(record => record.path === '/users/rolePerms')) {
if (routeAbility.can('view', 'RolesPerms')) {
// console.log('can rolesperms')
next()
}
} else if (to.matched.some(record => record.path === '/users/roles')) {
if (routeAbility.can('view', 'Role')) {
// console.log('can role')
next()
}
} else if (to.matched.some(record => record.path === '/users/permissions')) {
if (routeAbility.can('view', 'Permission')) {
// console.log('can permission')
next()
}
} else {
next(redirectPath)
}
} else {
// console.log('cannot')
next(redirectPath)
}
}
if (to.matched.some(record => record.path === '/usage')) {
// console.log('users entered')
// console.log('routeAbility', routeAbility)
if (routeAbility.can('manage', 'Administer')) {
// console.log('can usage')
if (to.matched.some(record => record.path === '/usage/securityLog')) {
if (routeAbility.can('view', 'Watchdog')) {
// console.log('can securityLog')
next()
}
} else if (to.matched.some(record => record.path === '/usage/reportUsage')) {
if (routeAbility.can('view', 'ReportUsage')) {
// console.log('can reportUsage')
next()
}
} else if (to.matched.some(record => record.path === '/usage/sessionLog')) {
if (routeAbility.can('view', 'Session')) {
// console.log('can sessionLog')
next()
}
}
next()
} else {
// console.log('cannot')
next(redirectPath)
}
}
if (to.matched.some(record => record.path === '/reports')) {
if (routeAbility.can('manage', 'Administer')) {
next()
}
}
if (to.matched.some(record => record.path === '/server')) {
if (routeAbility.can('manage', 'Administer')) {
next()
}
}
//* ******************* End User Menu Items Begin ****************************
if (to.matched.some(record => record.path === '/userReports')) {
if (routeAbility.can('edit', 'Reports') || routeAbility.can('view', 'Reports')) {
// console.log('can Reports')
next()
} else {
// console.log('cannot Reports')
next(redirectPath)
}
}
if (to.matched.some(record => record.path === '/kinetics')) {
if (routeAbility.can('edit', 'Kinetics') || routeAbility.can('view', 'Kinetics')) {
// console.log('can Kinetics')
next()
} else {
// console.log('cannot Kinetics')
next(redirectPath)
}
}
if (to.matched.some(record => record.path === '/pharmAssist')) {
if (routeAbility.can('edit', 'PharmAssist') ||
routeAbility.can('view', 'PharmAssist') ||
routeAbility.can('view', 'PharmAssist Reports')) {
// console.log('can PharmAssist')
next()
} else {
// console.log('cannot PharmAssist')
next(redirectPath)
}
}
if (to.matched.some(record => record.path === '/cqa')) {
if (routeAbility.can('edit', 'CQA') || routeAbility.can('view', 'CQA')) {
// console.log('can CQA')
next()
} else {
// console.log('cannot CQA')
next(redirectPath)
}
}
if (to.matched.some(record => record.path === '/training')) {
if (routeAbility.can('edit', 'Training') || routeAbility.can('view', 'Training')) {
// console.log('can Training')
next()
} else {
// console.log('cannot Training')
next(redirectPath)
}
}
} else {
// the user has no assigned permissions, send them to the main page
// TODO throw an error about not having permissions
// console.log('ability rules empty')
next(redirectPath)
}
// next(redirectPath)
}
} else {
// doesn't require authentication, so this is safe to forward without further checks
next()
}
})
}
In my router folder, I have this in my index.js file:
import Vue from 'vue'
import VueRouter from 'vue-router'
import routes from './routes'
Vue.use(VueRouter)
/*
* If not building with SSR mode, you can
* directly export the Router instantiation
*/
export default function (/* { store, ssrContext } */) {
const Router = new VueRouter({
scrollBehavior: () => ({ x: 0, y: 0 }),
routes,
// Leave these as is and change from quasar.conf.js instead!
// quasar.conf.js -> build -> vueRouterMode
// quasar.conf.js -> build -> publicPath
mode: process.env.VUE_ROUTER_MODE,
base: process.env.VUE_ROUTER_BASE
})
return Router
}
and in the routes.js file in the same folder:
import Default from '../layouts/default'
import Index from '../pages/index'
import UserReports from '../pages/userReports'
import AllReports from '../pages/usersReports/allReports'
import RecentReports from '../pages/usersReports/recentReports'
import DepartmentReports from '../pages/usersReports/departmentReports'
import FacilityReports from '../pages/usersReports/facilityReports'
import ThrowAway from '../pages/throwAway'
import Kinetics from '../pages/kinetics'
import PharmAssist from '../pages/pharmAssist/allWorkflows'
import CQA from '../pages/cqa'
import Training from '../pages/training'
import UsersAdmin from '../pages/usersAdmin'
import Roles from '../pages/users/roles'
import Permissions from '../pages/users/permissions'
import RolesPerms from '../pages/users/rolePerms'
import UserRoles from '../pages/users/userRoles'
import GroupRoles from '../pages/users/groupRoles'
import Usage from '../pages/usage'
import SecurityLogs from '../pages/usage_security_logs/securityLog'
import ReportUsage from '../pages/usage_security_logs/reportUsage'
import SessionLogs from '../pages/usage_security_logs/sessionLog'
import Reports from '../pages/reports'
import Schedule from '../pages/manageSchedule/schedule'
import Manage from '../pages/manageSchedule/manage'
import Server from '../pages/server'
const routes = [
{
path: '/',
component: Default,
children: [
{ path: '', component: Index },
{
path: 'userReports',
component: UserReports,
meta: { requiresAuth: true },
children: [
{
path: '',
redirect: 'reports/my',
meta: { requiresAuth: true }
},
{
path: 'recentReports',
component: RecentReports,
meta: { requiresAuth: true }
},
{
path: 'departmentReports',
component: DepartmentReports,
meta: { requiresAuth: true }
},
{
path: 'facilityReports',
component: FacilityReports,
meta: { requiresAuth: true }
},
{
path: 'reports/:mode',
component: AllReports,
meta: { requiresAuth: true }
}
]
},
{
path: 'throwAway',
component: ThrowAway
},
{
path: 'kinetics',
component: Kinetics,
meta: { requiresAuth: true }
},
{
path: 'pharmAssist',
component: PharmAssist,
meta: { requiresAuth: true }
},
{
path: 'cqa',
component: CQA,
meta: { requiresAuth: true }
},
{
path: 'training',
component: Training,
meta: { requiresAuth: true }
},
{
path: 'users',
component: UsersAdmin,
meta: { requiresAuth: true },
children: [
{
path: '',
redirect: 'userRoles',
meta: { requiresAuth: true }
},
{
path: 'userRoles',
component: UserRoles,
meta: { requiresAuth: true }
},
{
path: 'groupRoles',
component: GroupRoles,
meta: { requiresAuth: true }
},
{
path: 'roles',
component: Roles,
meta: { requiresAuth: true }
},
{
path: 'permissions',
component: Permissions,
meta: { requiresAuth: true }
},
{
path: 'rolePerms',
component: RolesPerms,
meta: { requiresAuth: true }
}
]
},
{
path: 'usage',
component: Usage,
meta: { requiresAuth: true },
children: [
{
path: '',
redirect: 'reportUsage',
meta: { requiresAuth: true }
},
{
path: 'reportUsage',
component: ReportUsage,
meta: { requiresAuth: true }
},
{
path: 'securityLog',
component: SecurityLogs,
meta: { requiresAuth: true }
},
{
path: 'sessionLog',
component: SessionLogs,
meta: { requiresAuth: true }
}
]
},
{
path: 'reports',
component: Reports,
meta: { requiresAuth: true },
children: [
{
path: '',
redirect: 'schedule',
meta: { requiresAuth: true }
},
{
path: 'schedule',
component: Schedule,
meta: { requiresAuth: true }
},
{
path: 'manage',
component: Manage,
meta: { requiresAuth: true }
}
]
},
{ path: 'server', component: Server, meta: { requiresAuth: true } }
]
}
]
// Always leave this as last one
if (process.env.MODE !== 'ssr') {
routes.push({
path: '*',
component: () => import('pages/Error404.vue')
})
}
export default routes
And that is it. Navigation is intercepted when a user clicks a link and evaluated before the page changes.