[Solved] How to use CASL with Quasar



  • 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.



  • Note that the abilities file must also be used on the server side if you’re going to manage the permissions in your own application. Instead of @casl/vue, you will use

    import { Ability } from '@casl/ability'
    

    That would be a much bigger post.



  • You can also use $can checks in your Vue files.

    One of my Computed fields is defined like this:

    hideChecks () {
      return !(this.$can('edit', 'RolesPerms'))
    },
    

    The computed field is useful if you have several elements that need to be hidden and they aren’t in a shared DIV.

    Similarly, you can use them in your templates too like this:

    <q-route-tab
      v-if="$can('view','User')"
      to="userRoles"
      :count="numUsers"
      name="tab-user-roles"
      exact
    >...
    

    The q-route-tab won’t be shown without the right permissions.



  • To give you a quick thumbnail idea of what else I’m doing with permissions/abilities, I’ll just say that I have permissions/abilities defined in a file (mentioned above). That file is copied to both the server app and front-end app. I have DB tables for role-perms (roles linked to permissions), roles, users and user-roles (users linked to roles). I have an administration section of my front-end, with corresponding server-side functionality, to setup all the linkages between users, roles, and abilities.

    When a user logs in, I lookup all of their permissions via the linkages just mentioned. I then save their ability tree to my Vuex store for easy reference. I also save it to the user’s session. I can’t recall which one is used by the $can operation.



  • @rconstantine Thanks for your help, appreciated your time/support



  • Very nice! Thanks for sharing this.



  • @rconstantine

    I have got a question:
    Why have you define your abilities like this:

    '  {"group_name": "Administration",' +
    '    "description": "Administer this application",' +
    '    "abilities": [' +
    '      {"name": "Administer manage", "subject": "Administer", "actions": "manage"}' +
    '    ]' +
    '  },
    

    Instead of like this

    '  {"group_name": "Administration",' +
    '    "description": "Administer this application",' +
    '    "abilities": [' +
    '      {"name": "Administer manage users", "subject": "users", "actions": "manage"}' +
    '      {"name": "Administer manage users", "subject": "usage", "actions": "manage"}' +
    '      {"name": "Administer manage users", "subject": "reports", "actions": "manage"}' +
    '      {"name": "Administer manage users", "subject": "server", "actions": "manage"}' +
    '    ]' +
    '  },
    

    And then in your index.js:

    if (to.matched.some(record => record.path === '/reports')) {
                if (routeAbility.can('manage', 'users')) {
                  next()
                }
              }
              if (to.matched.some(record => record.path === '/reports')) {
                if (routeAbility.can('manage', 'usage')) {
                  next()
                }
              }
    
              if (to.matched.some(record => record.path === '/reports')) {
                if (routeAbility.can('manage', 'reports')) {
                  next()
                }
              }
    
              if (to.matched.some(record => record.path === '/server')) {
                if (routeAbility.can('manage', 'server')) {
                  next()
                }
              }
    

    I need to understand because i want to give the ability to the user to create new group of user and then depending of the new ability the user can or cannot go to a route.



  • It doesn’t matter how you do it, but if you look more closely at mine, I have several categories that are administrative. The one you quoted is an umbrella I use which shows or hides an entire submenu called ‘administration’ or something. Then, under that, the menu items of ‘users’, ‘usage’, and other things are there, and it’s those things that have further categories defined and abilities like ‘view’ and ‘edit’ for each.

    Like for users, I have:

    '  {"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"}' +
      '    ]' +
      '  },' +
    

    A user has to have one or more of the above permissions to see the menu item, AND the Administer manage permission to see the administrative menu at all. Put another way, if I accidentally gave someone Administer manage permission, but none of the other administrative permissions, they would see an empty Administration menu with no children. On the other hand, if I accidentally gave someone the Edit User permission, they wouldn’t actually be able to get to it or see that the path to edit users exists unless I also gave them the Administer manage permission.

    Of course, you could do things differently. Permissions (abilities) are completely arbitrary in how you designate them because it’s up to you to then use them to prevent access wherever you want to.



  • Great thanks a lot, i didn’t get the fact that you will give 2 abilities. Thanks for your post it’s help a lot ! you should write a medium article 😉


Log in to reply