Main Menu
  • Home
  • Services
    • Strategy
      • Technology Partner
      • UI / UX
      • Artificial Intelligence

      Core Technologies
      • PHP
      • JavaScript
      • Laravel
      • VueJs
      • AWS
    • Development
      • Software Development
      • Mobile App Development
      • Software Integration
      • Software Support (SLA)
      • Managed Hosting
      • Microsoft Access Databases
    • Industries
      • Manufacturing
      • Transport / Logistics
      • Finance
      • Retail & E-commerce
      • Government
      • Construction
      • Health
      • Insurance
      • Mining
  • Our Work
  • About
  • Blog

© 2020 Codium Pty Ltd.

Codium Logo Codium Logo
  • Services
    • Strategy
      • Technology Partner
      • UI / UX
      • Artificial Intelligence

      Core Technologies
      • PHP
      • JavaScript
      • Laravel
      • VueJs
      • AWS
    • Development
      • Software Development
      • Mobile App Development
      • Software Integration
      • Software Support (SLA)
      • Managed Hosting
      • Microsoft Access Databases
    • Industries
      • Manufacturing
      • Transport / Logistics
      • Finance
      • Retail & E-commerce
      • Government
      • Construction
      • Health
      • Insurance
      • Mining
  • Our Work
  • About
  • Blog

Using Bouncer with Laravel and Vue

20 Aug 2020, in Developer Perspectives

  • Blog
  • Developer Perspectives
using-bouncer-with-laravel-and-vuejs-feature

Using Bouncer with Laravel and Vue

Our goal was to find a way to control what actions a user is allowed to perform, and to have these permissions stored in one central location (Laravel API) while being consumed by a Vue.js frontend. We can do this by utilising Bouncer.

About Bouncer

Bouncer is a package made by JosephSilber that integrates with laravel to provide a way manage a user's roles & permissions in your app. Check out the github page for instructions on how to setup Bouncer.

Using Bouncer with Laravel

After running Bouncer's migrations you can easily provide abilities to your users in a variety of ways:

Giving a user a simple named ability

Bouncer::allow($user)->to('archive-customers');

Relating an ability to a model

Bouncer::allow($user)->to('archive', Customer::class);

You can also specify the model instance you want them to have this ability on:

Bouncer::allow($user)->to('archive', $customer);

Giving ownership of a model

Bouncer::allow($user)->toOwn(Customer::class);

// Pass an array to only allow certain actions to performed on our Customer
Bouncer::allow($user)->toOwn(Customer::class)->to(['archive', 'edit']);

By using toOwn, this allows our user or role to only be able to perform actions on the customer's that they own, which is determined by the customer's user_id which is checked against the authenticated user's id. (this can be modified)

Allowing users to manage a model

Bouncer::allow($user)->toManage(Customer::class)

This is similar to toOwn except in that it doesn't care about who owns the model. So essentially a user who can manage a customer can do anything to that model that isn't explicitly forbidden.

Using Bouncer on an app with a Vue frontend

So using Bouncer's docs we can figure out how to use Bouncer with Laravel easily enough. However we create a lot of application's where the frontend is using Vue and there are often cases where we may want to change a frontend components based on a user's permissions, e.g., hiding a navigation bar item based on whether a user has access to that page or not.

Sending the abilities to the frontend

So how can we check on our frontend if a user is allowed to perform certain actions?

First we need to return our abilities to the frontend when we retrieve our logged in user. Bouncer provides a couple of ways to do this:

Abilities can be retrieved through the abilities relationship

return $user->abilities;

This will return a collection of Silber\Bouncer\Database\Ability objects and the json will look somewhat similar to the following:

[
    {
        "id": 3,
        "name": "edit",
        "title": "Edit Customers".
        "entity_id": 24,
        "entity_type": "customer"
        "only_owned": false,
        "pivot": {
            "ability_id": 3,
            "entity_id": 36,
            "entity_type": "App\User\User",
            "forbidden": 0
        }
    }
]

In this example we are using Laravel's morphMap() so we can use a simpler name for the customer model rather than the full namespace, so the 'entity_type' is 'customer' rather than 'App\Customer\Customer'

The problem with returning abilities via the relationship is that it doesn't return any abilities granted to the user through any roles they have assigned. So if you are using roles in your app, this won't be sufficient.

Bouncer also provides two methods for getting the abilities of a user and their roles.

// Get all abilities the user has.
$user->getAbilities();

// Get the abilities explicitly forbidden
$user->getForbiddenAbilities();

However it would be nice to get all of these abilities in one collection. A user may have been given ability and then later had that ability forbidden. The results of getAbilities() however doesn't have anything to indicate that this ability is forbidden as all it has is the old ability they were originally given.

So on my User model I made a function to return all the user's abilities with a flag to indicate whether this ability was forbidden or not.

/**
 * Return all the user's abilities and if they have explicitly been
 * forbidden from having an ability, then we return with a forbidden flag
 * set to true. Unfortunately bouncer doesn't have a method for returning
 * all abilities, forbidden or not
 */
public function getUserAbilities()
{
    $abilities = $this->getAbilities()->merge($this->getForbiddenAbilities());

    $abilities->each(function ($ability) {
        $ability->forbidden = $this->getForbiddenAbilities()->contains($ability);
    });

    return $abilities;
}

So at this point we should be able to return these abilities with our user and the frontend will have access to all of these when it's checking a user's permissions.

Checking abilities on the frontend

The next thing we need to do is create a way to check on the frontend if the logged in user has the required abilities to perform an action. So in our solution we have a global mixin that adds some getters we can use to check abilities.

import Vue from 'vue'
import { mapState, mapGetters } from 'vuex'

Vue.mixin({
    computed: {
        ...mapState('auth', {
            $user: 'user'
        }),

        ...mapGetters('auth', {
            $can: 'can',
            $cannot: 'cannot',
            $isA: 'isA',
            $isNotA: 'isNotA'
        })
    }
})

These getters are defined in our auth.js module which is where our logged in user is defined.

import Bouncer from 'utils/bouncer'

export default {
    namespaced: true,

    state: {
        user: {}
    },

    getters: {
        bouncer: (state) => new Bouncer(state.user),
        can: (state, getters) => getters.bouncer.can.bind(getters.bouncer),
        cannot: (state, getters) => getters.bouncer.cannot.bind(getters.bouncer),
        isA: (state, getters) => getters.bouncer.isA.bind(getters.bouncer),
        isNotA: (state, getters) => getters.bouncer.isNotA.bind(getters.bouncer)
    }
}

And as you can see we have a third file we've called bouncer.js which is where the methods for checking roles and abilities are stored.

The constructor:

import { map, pick, find } from 'lodash'

export default class Bouncer {
    constructor (user) {
        if (!user) {
            this.id = null
            this.abilities = []
            this.roles = []

            return
        }

        const abilityMapper = (ability) => {
            return pick(ability, [
                'id',
                'entity_id',
                'entity_type',
                'name',
                'forbidden',
                'only_owned',
                'title'
            ])
        }

        this.id = user.id
        this.roles = map(user.roles, role => pick(role, ['name', 'title']))
        this.abilities = map(user.abilities || [], abilityMapper)
    }
}

You may not need to map the roles and abilities depending on how you return them from the api.

Here is our can() method that does all our permission checking. It's basically a bunch of if statements to filter down to the abilities related to the one we are checking and then return a true boolean if it's not forbidden.

// Find the abilities that give the user permission to do the ability we are
// checking and if we have one and the ability isn't one that forbids them
// then we return true.
can (abilityName, entityType = null, entity = null) {
    // Filter abilities to only ones that might be relevant to the given ability name.
    let abilities = this.abilities.filter((ability) => {
        if (abilityName === ability.name || ability.name === '*') {
            if (ability.entity_type === '*') {
                return true
            }

            // if the ability has only_owned set to true entities to be allowed to be accessed
            // then we need to check that the entity's user_id matches the id of our
            // user
            if (ability.only_owned) {
                if (entity === null || entityType === null) {
                    return false
                }

                if (entityType === ability.entity_type && entity.user_id !== this.id) {
                    return false
                }
            }

            if (ability.entity_type && entityType !== ability.entity_type) {
                return false
            }

            if (ability.entity_id) {
                if (!entity) {
                    return false
                }

                if (entity.id !== ability.entity_id) {
                    return false
                }
            }

            return true
        }

        return false
    })

    // if there are no relevant abilities or some of the relevant abilities are
    // forbidden then return false
    if (abilities.length === 0 || abilities.some(ability => ability.forbidden)) {
        return false
    }

    return true
}

We also have an isA() method that checks that the user is a particular role.

// Determine if the user's roles contain any of the roles we are looking for
isA (roles) {
    roles = typeof roles === 'string' ? Array.from(arguments) : roles

    return roles.some((name) => {
        return find(this.roles, { name })
    })
}

And to check the opposites we have a cannot() and isNotA

cannot (ability, entityType = null, entity = null) {
    return !this.can(ability, entityType, entity)
}


isNotA (roles) {
    return !this.isA(roles)
}

Then when you need to check on our Vue component simply call the $can() or $isA() and pass in the ability you want to check, and any relevant entity name (this is why I used morphMap) and the relevant entity id if your are checking a specific entity

<router-link v-show="$can('archive', 'customer')" :to="{ name: 'customer-archive' }">
    <a class="nav-link">
        <i class="fa fal-id-badge"></i> Customer Archive
    </a>
</router-link>

<!-- or -->

<button v-if="$can('archive', 'customer', customerObject)" type="button">
    Archive
</button>
computed: {
    showLabel () {
        return $isA('admin')
    },


    disableButton () {
        return $can('archive-customers')
    }
}

Checking permissions on the Router

We may also want to check a user's permissions when they change routes, to prevent access to routes that the user should be able to access. A way to do this is using Vue Router's navigation guards.

router.beforeEach((to, from, next) => {
    const bouncer = store.getters['auth/bouncer']

    if (has(to.meta, 'permissions') || has(to.meta, 'roles')) {
        if (typeof to.meta.permissions === 'function') {
            if (!to.meta.permissions(bouncer, to, from)) {
                // Push notification to inform user they do not have permission

                // redirect to a universal page they will have access to
                next({ name: 'home', replace: true })

                return
            }
        } else if ((has(to.meta, 'permissions') && bouncer.cannot(to.meta.permissions)) || (has(to.meta, 'roles') && bouncer.isNotA(to.meta.roles))) {
            // Push notification to inform user they do not have permission

            // redirect to a universal page they will have access to
            next({ name: 'home', replace: true })

            return
        }
    }

    // they either have permissions or no permissions are required so continue on
    // to the intended route
    next()
})

So here we are using Lodash's has to see if the route meta object has permissions or roles defined on it and if a permissions function was defined we run that function to determine if the user has access to the route. If not then we just check using our bouncer utility's cannot or isNotA and in the case they don't have access to this route then we can redirect to some other page, otherwise we'll continue on to the next route.

On our actual routes we will define our permissions like so:

{
    path: '/users/create',
    name: 'users.create',
    component: UsersCreate,
    meta: {
        permissions: 'create-users',
        roles: ['admin']
    }
},

or in this case we are checking they can edit a specific user and they came from a certain route:

{
    path: '/users/:id',
    name: 'users.edit',
    component: UsersEdit,
    meta: {
        permissions (bouncer, to, from) {
            return bouncer.can('edit', 'user', { id: to.params.id } ) 
                && from.name === 'users.create'
        }
    }
}

Summary

  • So at this point we've got our user's abilities in one nice collection from the api and are returning it with our logged in user's resource.
  • We have created our can and isNotA methods to check the user's abilities and roles
  • We are using the methods on our Vue components
  • We are checking before each route change that user's has permission to the route
Codium Logo
Codium Pty Ltd.
Ground Floor, 207 Greenhill Road
Eastwood, 5063
Adelaide, South Australia

Company

  • About us
  • Blog
  • Terms of Business
  • Customer Experience
  • Quality Policy
  • Security Policy
  • Privacy Policy

Services

  • Software Development
  • Mobile App Development
  • Support Services
  • Managed Hosting
  • Software Integration
  • Technology Partner
  • Microsoft Access

Support

  • Contact Us
  • Customer Feedback
  • Careers
  • Codium Remote Support

Socialise

© Codium Pty Ltd. All Rights Reserved.

Back Top