Skip to content

jacklin10/canner

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

67 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Canner

Code Climate Build Status Gem Version

Canner is an authorization gem heavily modeled after Pundit.

Canner's intention is to provide you a framework for authorization that has little to no magic.
Your canner policies can be as simple or as complicated as your app requires.

Who needs another auth gem? There's a bunch of very good ones out there.
Pundit, cancan, cancancan and Declarative Authorization to name a few alternatives.

Unfortunately for me, none of those solutions had built in support for a requirement I had.

I needed to authorize a user by more than just a role, I needed to authorize a user by role and location.
The other auth libraries out there don't support this requirement out of the box.

With canner you can ask: Can joe having the role manager access the reports for the pittsburgh store.

You just have to let canner know the branch you are currently operating on.

For details see the wiki page Authorize with Branches ( Store Locations )

Also note that canner works fine if you don't need this particular feature, its just there if you do.

Compatability

Canner works with rails 4.x and 5.x.

Installation

I've only tested Canner on rails 4 but it should work fine in rails 3.2 apps.

To install Canner just put the following in your gem file.

gem "canner"

Then run

bundle install

Now include canner in your application_controller.rb

include Canner

You'll then need to create a Policy class for the models you'd like to authorize.

rails g canner:policy user

If your app gets roles from a user in a way other than @current_user.roles then you'll need to override the fetch_roles policy method.

rails g canner:fetch_roles

More details are available in the wiki: Overriding the Fetching of Roles

Policies

As mentioned Canner is strongly influenced by Pundit and is also based on Policy objects. Your policy objects should be named using the following pattern:

  <model_name>Policy.rb
  i.e:  User.rb / UserPolicy.rb, Customer.rb / CustomerPolicy.rb

Generator

You can also use our generator to create the policy for you:

rails g canner:policy <model name>

Your policy models need to implement 2 methods:

def canner_scope
end

def can?
end

canner_scope

You'll want to implement this method for each of your model policies that extend from base_policy.rb.

The canner_scope method is used to scope the authorized models consistently in your app.

For example in my app the Customers controller uses the canner_scope to ensure only Users from the current_company are displayed.

class CustomersController < ApplicationController
  respond_to :html, :json
  before_action :authenticate_user!

  def index
    @customers = canner_scope(:index, :customer)

    can?(:index, :customer)
  end
end

and the policy is:

class CustomerPolicy < BasePolicy

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

  def can?
    case @method
    when :new, :index, :create, :update, :edit
      has_role?(:admin)
    else
      false
    end
  end

end

Now you don't really need to think about the auth logic when fetching a list of customers. Just make sure you use the policy and you'll only show the users what is intended.

Also if your policy changes at some point its a one place fix.

can?

You use the can? method to determine if the current_user is able to access an action or resource.

The example above uses a straightforward case statement to determine if the current_user can access the current action or resource.

The symbols in the when portion of the case match your typical actions in the example but they can be whatever you want really.

case @method
when :something_random
  has_role?(:admin)
else
  false
end

Then in controller do: can?(:something_random, :customer)

In english the can method is saying:

Can the currently signed in user access the something_random action? Oh, and by the way please use the CustomerPolicy's can? method to do the checking.

can?(:something_random, :user) would use the ... you guessed it UserPolicy's can? method.

If you want to deny access by default across all model policies you could do something as simple as:

def can?
  false
end

in your base_policy's can? method

instance_can?

You use the instance_can? method to determine if the current_user is able to modify a particular instance of an object.

For example, if a user wants to edit a particular item they may end up here:

/items/3/edit

The user changes the item price and moves on with their day.

Now we have another user who decides they want to see what happens if they tinker with the url to potential edit other items. They enter the following:

/items/13/edit

Maybe item 13 belongs to a different company, or is in a cateogory that this user isn't supposed to see. If you don't defend against this situation a clever user can gain access to any item in the system.

The instance_can? method helps in these situations.

In your items controller for the edit, update and destroy methods add something like:

@item = Item.find params[:id]
instance_can? :manage, :item, @item

Your item_policy.rb will have something like:

def instance_can?(item)
  case @method
  when :manage
    return @current_user.item_categories.include?(item.category)
  else
    false
  end
end

Now an access denied message will be shown to any users attempting to access an item in a category they don't belong to.

Your policy can be more complex if needed. Canner is just a framework so you can get as creative as you want just so long as you eventually return true or false.

For example, maybe your admin user is allowed to edit any items? You could do something like this:

def instance_can?(item)
  case @method
  when :manage
    return has_role?(:admin) ? true : @current_user.company == item.company
  else
    false
  end
end

You can enforce that your methods check for this just like you can for canner_scope or can?.
The next section shows you how.

Forcing Controller Authorization

You are able to force the use of controller authorization with canner.
I recommend you do this so you don't forget to wrap authorization about some of your resources.

To make sure your controller actions are using the can? method add this near the top of your application_controller.rb. Use the unless: option for ensuring we ignore controllers related to authentication.

after_action :ensure_auth

# using devise?
after_action :ensure_auth, unless: :devise_controller?

# using CASino?
after_action :ensure_auth, unless: -> { self.is_a? CASino::SessionsController }

And to make sure you are using the canner_scope do the following:

after_action :ensure_scope, only: :index

Note the use of only here. You usually won't need the canner_scope on anything except for the index to be strictly enforced.

And finally, if you want to enforce that you are using instance_can? use something like:

after_action :ensure_instance_checking, only: [:edit, :destroy, :update]

If you would like to skip one of the enforcements for a specific controller add one or all of these:

skip_filter :ensure_scope
skip_filter :ensure_auth
skip_filter :ensure_instance_checking

Handle Canner Authorization Failures

When a user does stumble onto something they don't have access to you'll want to politely tell them about it and direct the app flow as you see fit.

To accomplish this in your application_controller.rb add

rescue_from Canner::NotAuthorizedError, with: :user_not_authorized

You can name your method whatever you want. Mine is user_not_authorized and looks like this:

private

def user_not_authorized(exception)
  flash[:error] = exception.message
  redirect_to(request.referrer || root_path)
end

Using can? in views

You'll likely want to show or hide on screen items based on a users' role. This is done in canner like this:

  = link_to 'Create Customer', new_customer_path if canner_policy(:new, :customer).can?

This will look in the CustomerPolicy's can? method implemention and follow whatever rules you have for the :new symbol.

So assuming the CustomerPolicy can? method provided below the currently signed in user would only be able to see the create customer link if they had an admin role.

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

Testing

See the wiki for some testing tips Testing

About

Full featured rails authorization without all the magic.

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages