Skip to content

Authorize with Branches

Joe Acklin edited this page Nov 16, 2017 · 6 revisions

Background

Typically you'll only need to authorize a particular action by the currently authenticated user and their role. That's not always the case though. For my project I needed to also authorize things by a particular store branch.

For example each branch has report that includes detailed sales information. Each branch is also independently owned and operated. The manager of the Pittsburgh branch is only permitted to view the sales information at his branch. That same manager is allowed to view staff schedules and inventory in the Cranberry branch though, just not the detailed sales report.

With a traditional authorization library you only really get to answer the question:

Can the current user access the sales report?

This isn't enough. The current user has a manager role, but not for all the branches. You could create a new role. pittsburgh_manager maybe? But now any time a new branch is created you have to create a new set of rules for the new role.

We needed an authorization solution that answers this question:

Can the current user access the sales report for the Pittsburgh branch?

Another example where I've used this canner feature is was for a centralized user role management app. The app tracks all of your company's users and what roles they have for each app.

For this case you want to know:

Does Joe have admin for application A?

Instead of the standard:

Does Joe have admin access?

Canner supports this requirement. You don't have to use it, but its there if you do.

Feeding Canner your Branch

If you do need to use the authorization by branch functionality you'll need to supply canner with the branch that is currently in use.

For my project I have a current_branch method in the application_controller similar to current_user. We then give users who have the proper permissions, the ability to change the branch.

All you need to do is tell canner how to get your current branch. If you're like us and have a current_branch method in your application_controller then you can turn on the TV and plop down on the couch because you're done!

If your method is different, maybe it's called current_location, then you'll need to override the canner_branch method.

def canner_branch
  Store.find_by_location(@current_user.location)
end

This can be done in the application controller or in a specific controller if you need to.

Answering the Authorization Question

Now that canner knows how to find which branch its dealing with, and which user ( through current_user ) its time to start creating some authorization rules.

There are 2 main ways you'll want to authorize by user and branch.

The first is limiting objects based on the user and branch.
The second is preventing access to a controller action based on the user and branch.

Canner Scope

An example of when you might use a canner_scope is if you wanted the Orders index action to only list the orders for the currently selected store branch.

Lets work this one through. You have an Order model, and an Orders Controller. Typically you would write the active record query that suits your needs and move on.

The problem with that is when you need the same thing in another part of the app you have to duplicated it.

We all try to avoid that when we can. That's why scopes exist.

In the case of authorization though you want to keep all the logic used for authorization in one place.
Then you can use canner any time you want deal with authorization and know its consistent.

In canner that one consistent place is the Policy. Lets start by creating on for our Order model:

rails g canner:policy order

In the generated OrderPolicy class you'll notice this method is stubbed for you by default:

  def canner_scope
    case @method
    when :index
      Order.all
    else
      Order.none
    end
  end

The following will ensure only the orders for the current branch are displayed.

  def canner_scope
    case @method
    when :index
      Order.where(branch: @current_branch)
    else
      Order.none
    end
  end

Or maybe you want to display only the Orders the current user has created in the current branch:

  def canner_scope
    case @method
    when :index
      Order.where(branch: @current_branch, created_by: @current_user)
    else
      Order.none
    end
  end

To use this authorization scope you'll include this in your OrdersController's index action.

@orders = canner_scope(:index, :order)

Can?

The second way you might want to authorize by both the user and branch by preventing access to a particular controller action all together.

Lets take the same model and policy as above. This time we want to prevent certain users from placing orders. Maybe we have a banned list of problem customers... the ones who aren't always right.

In this case we'll complete the can? method. The generator has built the following for you:

  def can?
    case @method
    when :index
      # has_role?(:admin)
    else
      false
    end
  end

Lets make this a little more useful.

  def can?
    case @method
    when :new, :create
      @current_user.banned?(@current_branch) ? false : true
    else
      false
    end
  end
```

Now if a user attempts to create a new order at a branch they have been banned from, we can display
a polite message letting them know they no longer have access to do so.

More commonly you'll use this method to determine what actions a user can hit by role like this:

```ruby
  def can?
    case @method
    when :edit, :update, :destroy
      @current_user.has_role?(:admin)
    when :index, :new, :create
      @current_user.has_role?(:guest)
    else
      false
    end
  end
```

If you have complicated rules and several actions you might get more creative with your `can?` method.
If its simple though keep it simple.  A simple case statement works fine for most cases.

Canner intentionally doesn't provide any magic for the authorization policies.  Its better to be able to clearly
see what is going on and not taken by surprise later.