Cyril Duchon-Doris
Cyril Duchon-Doris

Reputation: 13949

Rails simple access control

I am aware that several gems are made to handle authorization in Rails. But is it really worth it to use these gems for simple access controls ?

I only have a few "roles" in my application, and I feel that a powerful gem would be useless and even slow down the response time.

I have already implemented a solution, but then I took some security classes (:p) and I realized my model was wrong ("Allow by default, then restrict" instead of "Deny by default, then allow").

Now how can I simply implement a "deny by default, allow on specific cases" ?

Basically I'd like to put at the very top of my ApplicationController

class ApplicationController < ApplicationController::Base
  before_filter :deny_access

And at the very top of my other controllers :

class some_controller < ApplicationController
  before_filter :allow_access_to_[entity/user]

These allow_access_to_ before_filters should do something like skip_before_filter

def allow_access_to_[...]
  skip_before_filter(:deny_access) if condition
end

But this doesn't work, because these allow_access before filters are not evaluated before the deny_access before_filter

Any workaround, better solution for this custom implementation of access control ?

EDIT

before_action :find_project, except: [:index, :new, :create]
before_action(except: [:show, :index, :new, :create]) do |c|
   c.restrict_access_to_manager(@project.manager)
end

Upvotes: 1

Views: 3670

Answers (4)

Cyril Duchon-Doris
Cyril Duchon-Doris

Reputation: 13949

EDIT : Outdated answer, I have a friendlier implementation that involves using an access_control block

Going with evanbikes suggestion, for now I'll be using prepend_before action. I find it quite simple & flexible, but if I ever realize it's not good enough I will try other things.

Also if you find security issues/other problems with the solution below, please comment and/or downvote. I don't like leaving bad examples in SO.

class ApplicationController < ApplicationController::Base
  include AccessControl
  before_filter :access_denied
  ...

My Access Control module

module AccessControl
  extend ActiveSupport::Concern
  included do
    def access_denied(message: nil)
        unless @authorized
            flash.alert = 'Unauthorized access'
            flash.info = "Authorized entities : #{@authorized_entities.join(', '}" if @authorized_entities
            render 'static_pages/home', :status => :unauthorized
            end
        end

        def allow_access_to_managers
            (@authorized_entities ||= []) << "Project managers"
            @authorized = true if manager_logged_in?
        end
        ...

How I use the AC in controllers :

class ProjectController < ApplicationController
  # In reverse because `prepend_` is LIFO
  prepend_before_action(except: [:show, :index, :new, :create]) do |c|
    c.allow_access_to_manager(@manager.administrateur)
  end
  prepend_before_action :find_manager, except: [:index, :new, :create]

Upvotes: 0

Cyril Duchon-Doris
Cyril Duchon-Doris

Reputation: 13949

I didn't like my previous solution using prepend_before_action, here is a nice implementation using ActionController callbacks

module AccessControl
  extend ActiveSupport::Concern

  class UnauthorizedException < Exception
  end

  class_methods do
    define_method :access_control do |*names, &blk|
      _insert_callbacks(names, blk) do |name, options|
        set_callback(:access_control, :before, name, options)
      end
    end
  end

  included do

    define_callbacks :access_control

    before_action :deny_by_default
    around_action :perform_if_access_granted

    def perform_if_access_granted
      run_callbacks :access_control do
        if @access_denied and not @access_authorized
          @request_authentication = true unless user_signed_in?
          render(
            file: File.join(Rails.root, 'app/views/errors/403.html'),
            status: 403,
            layout: 'error')
        else
          yield
        end
      end
    end

    def deny_by_default
      @access_denied ||= true
    end

    def allow_access
      @access_authorized = true
    end
  end
end

Then you can add your own allow_access_to_x methods (for example in the same AccessControl concern) :

def allow_access_to_participants_of(project)
  return unless user_signed_in?
  allow_access if current_user.in?(project.executants)
end

Use it in your controllers the following way (don't forget to include AccessControl in your ApplicationController

class ProjectsController < ApplicationController
  access_control(only: [:show, :edit, :update]) do
    set_project
    allow_access_to_participants_of(@project)
    allow_access_to_project_managers
  end

  def index; ...; end;
  def show; ...; end;
  def edit; ...; end;
  def update; ...; end;

  def set_project
    @project = Project.find(params[:project_id])
  end
end

Upvotes: 0

Mat&#237;as Gagliano
Mat&#237;as Gagliano

Reputation: 73

Rolling your own implementation isn't necessarily bad as long as you're committed to it.

It won't get tested and maintained by the community so you must be willing to maintain it yourself in the long run, and if it compromises security you need to be really sure of what you're doing and take extra care. If you have that covered and the existing alternatives don't really fit your needs, making your own isn't such a bad idea. And generally it's an incredibly good learning experience.

I rolled my own with ActionAccess and I couldn't be happier with the results.

  • Locked by default aproach:

    class ApplicationController < ActionController::Base
      lock_access
    
      # ...
    end
    
  • Per-action access control:

    class ArticlesController < ApplicationController
      let :admins, :all
      let :editors, [:index, :show, :edit, :update]
      let :all, [:index, :show]
    
      def index
        # ...
      end
    
      # ...
    end
    
  • Really lighweight implementation.

I encourage you not to use it but to check out the source code, it has a fare share of comments and should be a good source of inspiration. ControllerAdditions might be a good place to start.

ActionAccess follows a different approach internally, but you can refactor your answer to mimic it's API with something like this:

module AccessControl
  extend ActiveSupport::Concern

  included do
    before_filter :lock_access
  end

  module ClassMethods
    def lock_access
      unless @authorized
        # Redirect user...
      end
    end

    def allow_manager_to(actions = [])
      prepend_before_action only: actions do
        @authorized = true if current_user_is_a_manager?
      end
    end
  end
end

class ApplicationController < ActionController::Base
  include AccessControl  # Locked by default

  # ...
end

class ProjectController < ApplicationController
  allow_managers_to [:edit, :update]  # Per-action access control

  # ...
end

Take this example as pseudo-code, I haven't tested it.

Hope this helps.

Upvotes: 1

Jens
Jens

Reputation: 1154

I would really advise using a proper battle tested gem for authentication & authorisation instead of rolling your own. These gems have enormous test suites and aren't really all that hard to setup.

I've recently implemented an action based authorization using roles with Pundit & Devise

Devise is changeable as long as the gem you are using provides a current_user method if you don't want to further configure pundit.

# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  include Pundit

  rescue_from Pundit::NotAuthorizedError, with: :rescue_unauthorized

  # Lock actions untill authorization is performed
  before_action :authorize_user

  # Fallback when not authorized
  def rescue_unauthorized(exception)
    policy_name = exception.policy.class.to_s.underscore
    flash[:notice] = t(
      "#{policy_name}.#{exception.query}",
      scope: "pundit",
      default: :default
    )
    redirect_to(request.referrer || root_path)
  end
end

# app/models/user.rb
class User < ActiveRecord::Base
  has_many :roles, through: :memberships

  def authorized?(action)
    claim = String(action)
    roles.pluck(:claim).any? { |role_claim| role_claim == claim }
  end
end

# app/policies/user_policy.rb => maps to user_controller#actions
class UserPolicy < ApplicationPolicy
  class Scope < Scope
    attr_reader :user, :scope

    # user is automagically set to current_user
    def initialize(user, scope)
      @user = user
      @scope = scope
    end

    def resolve
      scope.all
    end
  end

  def index?
    # If user has a role which has the claim :view_users
    # Allow this user to use the user#index action
    @user.authorized? :view_users 
  end

  def new?
    @user.authorized? :new_users
  end

  def edit?
    @user.authorized? :edit_users
  end

  def create?
    new?
  end

  def update?
    edit?
  end

  def destroy?
    @user.authorized? :destroy_users
  end
end

Long story short:

If you configure pundit to force authorization on each request which is described in detail on the github page, the controller evaluates a policy based on the used controller.

UserController -> UserPolicy

Actions get defined with a question mark, even non restful routes.

def index?
  # authorization is done inside the method.
  # true = authorization succes
  # false = authorization failure
end

This is my solution to action based authorization hope it helps you out.

Optimisations & feedback are welcome !

Upvotes: 3

Related Questions