The most simple access control for your Meteor React app

For my last Meteor React app I’ve designed the most simple role based access control. The basic idea is that users can have multiple roles and every action possible is only allowed by a specified set of roles. For my Meteor React app the following scenarios were considered:

Only users with specific roles are allowed to…

  • call a Meteor method.
  • subscribe to a publication.
  • display React components in the UI.

To solve this problem a role based access control (RBAC) has been implemented:

user -> roles -> action -> permission for publication / method / component

Now you might miss the url access control as a scenario, why did I ignore it? Well, the answer is simple, a user won’t access what he can’t see. As long as we hide buttons and links which might lead to a restricted view and control access on the data layer properly, there’s no need to have access control for urls.

/imports/helpers/config.js

The config file merges the access control list (acl) and Meteor settings. For every possible action a set of roles is defined.

import { Meteor } from 'meteor/meteor'
import { objectAssign } from './index'

let acl = {
  // routers permissions
  'routers.read': [ 'admin', 'spec', 'tech', 'user' ],
  'routers.insert': [ 'admin', 'spec', 'tech' ],
  'routers.update': [ 'admin', 'spec', 'tech' ],
  'routers.remove': [ 'admin', 'spec' ],
  'routers.restore': [ 'admin' , 'spec' ],
  'routers.export': [ 'admin', 'spec' ],

  // notification permissions
  'notifications.read': [ 'admin', 'spec', 'tech' ],
  'notifications.receive': [ 'admin', 'spec', 'tech' ],
  'notifications.insert': [ 'admin', 'spec', 'tech' ],
  'notifications.remove': [ 'admin', 'spec', 'tech' ],
  'notifications.export': [ 'admin', 'spec' ],
}

export default objectAssign(Meteor.settings.private, Meteor.settings.public, { acl: acl })

Of course you could create a collection to store the acl and make the permission model dynamic.

/imports/helpers/isAllowed.js

This is the only function required to check the users permission.

import { config } from './index'

export default (action, roles) => {
  let allowed = false
  let allowedRoles = config.acl[action]
  roles = roles != null ? roles : []
  roles.map((role) => {
     allowed = allowedRoles.indexOf(role) != -1
  })
  return allowed
}

Simple isn’t it?

/server/methods/routers.js

This example shows how the permission is checked in a Meteor method.

...
import { isAllowed } from '/imports/helpers'

export default () => {
  Meteor.methods({
    'routers.insert'(object) {
      check(object, Object)

      // check permissions
      let roles = Meteor.userId() ? Meteor.user().roles : null
      if (!isAllowed('routers.insert', roles)) {
        throw new Meteor.Error(i18n.error.insufficent_rights, i18n.message.insufficent_rights_for_method)
      }

      // insert object
      ...

Make sure the permission check is the first thing done when the method is executed.

/server/publication/router.js

Now we restrict access on the data access layer.

...
import { isAllowed } from '/imports/helpers'

export default () => {

  Meteor.publish('routers.list', function(selector = {}) {

    // check permissions
    let user = Meteor.users.findOne(this.userId)
    let roles = user ? user.roles : null
    if (isAllowed('routers.read', roles)) {
      return Routers.find(selector)
    } else {
      this.stop()
      return
    }
  })
  ...

The user roles are accessible using this.user in a publication.

client/routers/Router.js

Finally you can restrict the visibility of React components quite easily.

...
import { isAllowed } from '/imports/helpers'

class Router extends React.Component {
  ...
  render() {
    let { loading, user, i18n } = this.props
    return loading ? <CircularProgress /> : <Card>
          ...

          { isAllowed('routers.update', user ? user.roles : null) ?
          <RaisedButton
          type="submit"
          label={ i18n.button.update }
          primary={ true } />
          : null }

          { isAllowed('routers.remove', user ? user.roles : null) ?
          <RaisedButton
          onTouchTap={ this.toggleDialog.bind(this, 'openRemoveDialog') }
          label={ i18n.button.remove }
          secondary={ true } />
          : null }

          ...
    </Card>
  }
}

export default Router

As you can see the isAllowed function is used for all scenarios.

Do you like this solution? Leave a comment and tell my more.

Leave a Reply