When CanCan Can’t

When we set out to rewrite the authentication system for our API, we knew it was going to be a large task. We already have a system with pretty simple controls, and a large code base using it. CanCan was in place for model-based authorization, but that didn’t really suffice for our needs because we trust different users to do different actions based on the calling context. Here’s a quick wish list of what we wanted:

  • Expirations on access
  • Revocable access
  • Client contexts for access (the user can perform certain actions from an in-store iPad, but other actions from the homepage)
  • User contexts for access (the owner of a store can see different information on their iPad than a customer)
  • Userless access (what can a client do)
  • Minimal interaction with a database (keep as much information as possible in CPU land to minimize the impact of authorization)

Client and user contexts kept us from using CanCan effectively. That’s a shame. It is a VERY good gem for simple access controls. We started hacking into it initially to provide access controls, but it ended up being lookup heavy and was not ideal for our uses.

The end result is a two sided system. One side controls what actions an access token is allowed to call, the other controls what data is returned. This allows us to write simple controllers and not have to worry (much) about authorization in them.

Every client in our system belongs to a permissions set grouping. This grouping is associated with a key in a hash of allowed actions and allowed attributes.

Clients associated with the “mobile” group

Note how we have multiple ways in which a user can be allowed. When our user is verified, we provide more information about the user than if they are unverified (such as someone who hasn’t yet confirmed their email). This list is abbreviated to help explain the concept.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
:mobile => {
  :with_verified_user => {
    :actions => [
                 "users#update",
                 ] + public_actions,
    :models => {
      :user => [:access_token,:created_at, :email, :name, :password_set, :preferred_card_id],
   }
  },
  :client_only => {
    :actions => ['oauth#me',
                 'oauth#access_token',
                 'oauth#login',
                 'oauth#register',
                 ] + public_actions,
    :models => {
      :user => [:access_token, :admin, :email, :name, :password_set],
    }
  },
  :with_unverified_user => {
    :actions => [ 'oauth#login',
                  'oauth#refresh_access_token',
                ] + public_actions,
    :models => {
      :user => [:email]
    }
  }
}

Using this, a user controller can be as simple as…

1
2
user = User.find(id)
user.to_json

…and the output would be only email for an unverified user, while a verified user can see the access_token, email, name, :password_set, etc.

Our access tokens are bound to a user and a client (with groups), which allows us to determine the access levels above and prune responses appropriately.

If you like this idea, but hate the implementation, check out another way of solving the same problem (exposing specific attributes of an API response depending on permission level).

https://github.com/intridea/grape-entity

Ask a question or share this article, we’d love to hear from you!

Tweet