Project

permit_yo

0.01
No commit activity in last 3 years
No release in over 3 years
An engine that provides authorization for Rails 3 apps.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
 Dependencies

Development

~> 3.1
>= 0
~> 2.13.0

Runtime

 Project Readme

This Rails 3 engine is a highly modified fork of the excellent authorization plugin by Bill Katz, which I’ve used fondly for years. It keeps the stellar controller-level DSL but reimplements the model-level scheme for determining authorization.

Permit, Yo¶ ↑

This engine provides a flexible way to add authorization to Rails 3 applications. It plays nicely with whatever authentication system you’d like to use.

The gem is composed of two parts: a controller and view level DSL and a default implementation of the model tier methods the DSL relies on.

Installation¶ ↑

In your Gemfile:

gem "permit_yo"

Example Usage¶ ↑

class MeetingController < ApplicationController

  permit "rubyists and wanna_be_rubyists", :except => :public_page

  def public_page
    render :text => "We're all in Chicago"
  end

  def secret_info
    permit "interested in Answers and (matz or dhh)" do
      render :text => "The Answer = 42"
    end
  end

  def rails_conf
    @meeting = Meeting.find_by_name 'RailsConf'
    permit "attendees of :meeting or swedish_mensa_supermodels" do
      venue = Hotel.find_by_name("Wyndham O'Hare")
      if permit? "traveller to :venue and not speaker"
        Partay.all_night_long
      end
    end
  end

end

Controller and View DSL¶ ↑

There are three flavors of permit:

  1. permit used in a controller declaratively, which acts as a before_filter

  2. permit used as a method with a block, which only executes the block if authorized

  3. permit? used as a method to return true if authorized

The DSL has two types of usage:

  1. Model-independent user roles, as in permit("admin")

  2. Model-dependent roles, as in permit("sysadmin of :website")

Phrases in the DSL can be joined with and and or, and many prepositions can be used.

Model Methods¶ ↑

There are two model methods used by this system which correspond to the two types of usage in the DSL:

  1. Model-independent user roles, such as permit("admin"), determine if a user has the role by calling the has_role?(role) method on the user object.

  2. Model-dependent roles, such as permit("sysadmin of :website"), determine if the the user has the role by calling the accepts_role?(role, user) method on the model object in question.

These methods can be implemented in any fashion you choose. For convenience, a default implementation is provided.

Default Implementation¶ ↑

The default implementation for has_role?(role) delegates to the method role?. For instance, permit("admin") will be authorized if user.admin? is not nil and not false. If the user object does not respond to :admin?, it will not be authorized.

The default implementation for accepts_role?(role, user) is similar, but checks for both singular and plural methods. For instance, permit("sysadmin of :website") will return true if either website.sysadmin == user or website.sysadmins.include?(user).

Configuration¶ ↑

To use the default implementation, simply call acts_as_authorized_user from your User model, and acts_as_authorizable from any model you wish to be authorizable. To use a custom implementation, simply define has_role?(role) on your user and accepts_role?(role, user) on the relevant models. It is possible for another gem to provide alternate implementations for you, but none currently exist.

All of the messages presented to the user are configurable and translatable. To change the messages or provide a new translation, simply add the permit_yo.permission_denied and permit_yo.require_user keys to your locale file. See config/locals/en.yml as an example.

Many other settings are configurable. To change them, simply set the following settings in your application’s configuration block to what you desire.

Redirection¶ ↑

This engine handles “redirection” for two types of cases: 1) when the user is not logged in (current_user == nil) and 2) when the user is unauthorized.

When a user is requesting HTML, the engine sets the flash and redirects the user. The flash key used, the message given, and where the user is redirected to are all easily configurable using settings and locale files. Further, where the user is redirected to can be overridden on a controller-by-controller basis by providing the methods require_user_redirection or permission_denied_redirection. This allows you to redirect to different locations in different parts of your application, for instance:

class Admin::ProtectedController < ApplicationController
  permit "admin"

protected
  def require_user_redirection
    "/admin/signin"
  end

  def permission_denied_redirection
    "/admin/"
  end
end

When a user is requesting something else – like XML, JSON, or JS – this engine returns a blank body with a 401 (unauthorized) status code for the login required case, and a 403 (forbidden) status code for the permission denied case. In this way if you provide an API or have a lot of AJAX on your site the calling clients can get more relevant information than a 302 redirect.

You can override how the “redirection” is handled for each format by implementing the controller methods handle_require_user_redirection_for_#{format} and handle_permission_denied_redirection_for_#{format} for each given format, like so:

def handle_require_user_redirection_for_html
  render :text => nil, :status => :not_acceptable
end

def handle_permission_denied_redirection_for_html
  render :text => nil, :status => :not_acceptable
end

def handle_require_user_redirection_for_xml
  render :text => nil, :status => :not_acceptable
end

def handle_permission_denied_redirection_for_xml
  render :text => nil, :status => :not_acceptable
end

Settings¶ ↑

config.permit_yo.implementation = :default

This is the implementation of the model tier methods to use. If it is :default the methods acts_as_authorized_user and acts_as_authorizable will provide the default implementation described above. It is possible for other gems to provide other implementations; if this happens you would change this key to select the one you desire.

config.permit_yo.require_user_redirection = { :controller => 'user_sessions', :action => 'new' }

This should be set to the path or hash of where the user should be redirected if they are not currently logged in.

config.permit_yo.permission_denied_redirection = ''

This should be set to the path or hash of where the user should be redirected if they are not authorized to perform the action they attempted.

config.permit_yo.store_location_method = :store_location

This is the name of the method that should be called to store the URI the unauthenticated user requested so that they can be redirected back to it after login (your authentication system will provide this).

config.permit_yo.current_user_method = :current_user

This is the name of the method that provides the currently logged in user for authorization purposes.

config.permit_yo.require_user_flash = :alert
config.permit_yo.permission_denied_flash = :alert

These are the names of the flash keys to be used to store the authorization messages. Many people like :notice but some prefer :error or :alert.

Jumpstarting with the default¶ ↑

For a typical installation you would add both mixins to your User model.

class User < ActiveRecord::Base

  # Authorization plugin
  acts_as_authorized_user
  acts_as_authorizable

...

Then in each additional model that you want to be able to restrict based on role you would add just the acts_as_authorizable mixin like this:

class Event < ActiveRecord::Base

  acts_as_authorizable

...

More Details¶ ↑

permit and permit?¶ ↑

permit and permit? take an authorization expression and a hash of options that typically includes any objects that need to be queried:

permit <authorization expression> [, options hash ]
permit? <authorization expression> [, options hash ]

The difference between permit and permit? is redirection. permit is a declarative statement and redirects by default. It can also be used as a class or an instance method, gating the access to an entire controller in a before_filter fashion.

permit? is only an instance method, can be used within expressions, does not redirect by default.

The authorization expression is a boolean expression made up of permitted roles, prepositions, and authorizable models. Examples include “admin” (User model assumed), “moderator of :workshop” (looks at options hash and then @workshop), “‘top salesman’ at :company” (multiword roles delimited by single quotes), or “scheduled for Exam” (queries class method of Exam).

Note that we can use several permitted prepositions (‘of’, ‘for’, ‘in’, ‘on’, ‘to’, ‘at’, ‘by’). In the discussion below, we assume you use the “of” preposition. You can modify the permitted prepositions by changing the constant in Authorization::Base::Parser.

  • If a specified role has no “of <model>” designation, we assume it is a user role (i.e., the model is the user-like object).

  • If an “of model” designation is given but no “model” key/value is supplied in the hash, we check if an instance variable @model if it’s available.

  • If the model is capitalized, we assume it’s a class and query Model#self.accepts_role? (the class method) for the permission. (Currently only available in ObjectRolesTable mixin.)

For each role, a query is sent to the appropriate model object.

The grammar for the authorization expression is:

       <expr> ::= (<expr>) | not <expr> | <term> or <expr> | <term> and <expr> | <term>
       <term> ::= <role> | <role> <preposition> <model>
<preposition> ::= of | for | in | on | to | at | by
      <model> ::= /:*\w+/
       <role> ::= /\w+/ | /'.*'/

Parentheses should be used to clarify permissions. Note that you may prefix the model with an optional “:” – the first versions of Authorization plugin made this mandatory but it’s now optional since the mandatory preposition makes models unambiguous.

Options¶ ↑

:allow_guests => false.

We can allow permission processing without a current user object. The default is false.

:user => YourUserObject.

The name of your user object.

:get_user_method => method_name

The method name provided should return a user object. Default is #current_user, which is the how acts_as_authenticated works.

:only => [ :method1, :method2 ]

Array of methods to apply permit (not valid when used in instance methods)

:except => [ :method1, :method2 ]

Array of methods that won’t have permission checking (not valid when used in instance methods)

:redirect => boolean

Default is true. If false, permit will not redirect to denied page.

:require_user_redirection => path or hash

default is "{ :controller => 'session', :action => 'new' }"

Path or Hash where user will be redirected if not logged in ()

:require_user_message => 'my message'

A string to present to your users when login is required. Default is ‘Login is required to access the requested page.’

:permission_denied_redirection => path or hash

Path or Hash where user will be redirected if logged in but not authorized (default is ”)

:permission_denied_message => 'my message'

Message that will be presented to the user when permission is denied. Default is ‘Permission denied. You cannot access the requested page.’

Pattern of use¶ ↑

We expect the application to provide the following methods:

#current_user (method name configurable)¶ ↑

Returns some user object, like an instance of my favorite class, UserFromMars. A user object, from the Authorization viewpoint, is simply an object that provides a has_role? method.

Note that duck typing means we don’t care what else the UserFromMars might be doing. We only care that we can get an id from whatever it is, and we can check if a given role string is associated with it. By using acts_as_authorized_user, we inject what we need into the user object.

If you use an authorization expression “admin of :foo”, we check permission by asking foo if it accepts_role?('admin', user). So for each model that is used in an expression, we assume that it provides the accepts_role?(role, user) method.

Note that user can be nil if :allow_guests => true.

#store_location (optional, method name configurable)¶ ↑

This method will be called if authorization fails and the user is about to be redirected to the login action. This allows the application to return to the desired page after login. If the application doesn’t provide this method, the method will not be called.

The name of the method for storing a location can be modified by changing the constant STORE_LOCATION_METHOD in environment.rb. Also, the default login and permission denied pages are defined by the constants LOGIN_REQUIRED_REDIRECTION and PERMISSION_DENIED_REDIRECTION in authorization.rb and can be overriden in your environment.rb.

Conventions¶ ↑

Roles specified without the “of model” designation:

  1. We see if there is a current_user method available that will return a user object. This method can be overridden with the :user hash.

  2. Once a user object is determined, we pass the role to user.has_role? and expect a true return value if the user has the given role.

Roles specified with “of model” designation:

  1. We attempt to query an object in the options hash that has a matching key. Example: permit "knight for justice", :justice => @abstract_idea

  2. If there is no object with a matching key, we see if there’s a matching instance variable. Example: @meeting defined before we use permit "moderator of meeting"

  3. Once the model object is determined, we pass the role and user (determined in the manner above) to model.accepts_role?

Copyright © 2010 Ian Terrell for code version 2.0 and above. Code originating in prior versions is copyright their respective authors.