Git Product home page Git Product logo

canard's Introduction

Canard

Build Status

Canard brings CanCan and RoleModel together to make role-based authorization in Rails easy. Your ability definitions gain their own folder and a little structure. The easiest way to get started is with the Canard generator. Canard progressively enhances the abilities of the model by applying role abilities on top of the model's base abilities. A User model with :admin and :manager roles would be defined:

class User < ActiveRecord::Base

  acts_as_user :roles => [ :manager, :admin ]

end

If a User has both the :manager and :admin roles, Canard looks first for user abilities. Then it will look for other roles in the order that they are defined:

app/abilities/users.rb
app/abilities/manager.rb
app/abilities/admin.rb

Therefore each of the later abilities can build on its predecessor.

Usage

To generate some abilities for the User:

$ rails g canard:ability user can:[read,create]:[account,statement] cannot:destroy:account
create  app/abilities/users.rb
invoke  rspec
create    spec/abilities/user_spec.rb

This action generates an ability folder in Rails root and an associated spec:

app.abilities/
  users.rb
spec/abilities/
  users_spec.rb

The resulting app/abilities/users.rb will look something like this:

Canard::Abilities.for(:user) do

  can     [:read, :create], Account
  cannot  [:destroy], Account
  can     [:read, :create], Statement

end

And its associated test spec/abilities/users_spec.rb will look something like this:

require_relative '../spec_helper'
require "cancan/matchers"

describe Ability, "for :user" do

  before do
    @user = Factory.create(:user_user)
  end

  subject { Ability.new(@user) }

  describe 'on Account' do

    before do
      @account = Factory.create(:account)
    end

    it { should be_able_to( :read,      @account ) }
    it { should be_able_to( :create,    @account ) }
    it { should_not be_able_to( :destroy,   @account ) }

  end
  # on Account

  describe 'on Statement' do

    before do
      @statement = Factory.create(:statement)
    end

    it { should be_able_to( :read,      @statement ) }
    it { should be_able_to( :create,    @statement ) }

  end
  # on Statement

end

You can also re-use abilities defined for one role in another. This allows you to 'inherit' abilities without having to assign all of the roles to the user. To do this, pass a list of role names to the includes_abilities_of method:

Canard::Abilities.for(:writer) do

  can     [:create], Post
  can     [:read], Post, user_id: user.id

end

Canard::Abilities.for(:reviewer) do

  can     [:read, :update], Post

end

Canard::Abilities.for(:admin) do

  includes_abilities_of :writer, :reviewer

  can     [:delete], Post

end

A user assigned the :admin role will have all of the abilities of the :writer and :reviewer, along with their own abilities, without having to have those individual roles assigned to them.

Now let's generate some abilities for the manager and admin:

$ rails g canard:ability admin can:manage:[account,statement]
$ rails g canard:ability manager can:edit:statement

This generates two new sets of abilities in the abilities folder. Canard will apply these abilities by first loading the ability for the User model and then applying the abilities for each of the current user's roles.

If there is no user (i.e. logged out), Canard creates a guest and looks for a guest ability to apply:

$ rails g canard:ability guest can:create:user

This would generate a signup ability for a user who was not logged in.

Obviously the generators are just a starting point and should be used only to get you going. I strongly suggest that you add each new model to the abilities because the specs are easy to write and CanCan definitions are very clear and simple.

Scopes

The :acts_as_user method will automatically define some named scopes for each role. For the User model above it will define the following scopes:

Scope Returns
User.admins all the users with the admin role
User.non_admins all the users without the admin role
User.managers all the users with the manager role
User.non_managers all the users without the manager role

In addition to the role specific scopes it also adds some general scopes:

Scope Returns
User.with_any_role(roles) all the users with any of the specified roles
User.with_all_roles(roles) only the users with all the specified roles

Installation

Rails 3.x, 4.x & 5.x

Add the canard gem to your Gemfile:

gem "canard"

Add the roles_mask field to your user table:

rails g migration add_roles_mask_to_users roles_mask:integer
rake db:migrate

That's it!

Rails 2.x

Sorry, you are out of luck. Canard has only been written and tested with Rails 3 and above.

Supported ORMs

Canard is ORM agnostic. ActiveRecord and Mongoid (thanks David Butler) adapters are currently implemented. New adapters can easily be added, but you'd need to check to see if CanCan can also support your adapter.

Further reading

Canard stands on the shoulders of Ryan Bates' CanCan and Martin Rehfeld's RoleModel. You can read more about defining abilities on the CanCan wiki (https://github.com/ryanb/cancan/wiki). Canard implements the Ability class for you so you don't need the boilerplate code from Ryan's example:

class Ability
  include CanCan::Ability

  def initialize(user)
    user ||= User.new # guest user (not logged in)
    if user.admin?
      can :manage, :all
    else
      can :read, :all
    end
  end
end

The Canard equivalent for non-admins would be:

Canard::Abilities.for(:user) do
  can :read, :all
end

And for admins:

Canard::Abilities.for(:admin) do
  can :manage, :all
end

Under the covers Canard uses RoleModel (https://github.com/martinrehfeld/role_model) to define roles. RoleModel is based on Ryan Bates' suggested approach to role based authorization which is documented in the CanCan wiki (https://github.com/ryanb/cancan/wiki/role-based-authorization).

Note on Patches/Pull Request

  • Fork the project.
  • Make your feature addition or bug fix.
  • Add tests for it (when I have some). This is important so I don't break it in a future version unintentionally.
  • Commit. Do not mess with rakefile, version, or history. (If you want to have your own version, that is fine but bump version in a commit by itself so I can ignore it when I pull.)
  • Send me a pull request. Bonus points for topic branches.

Contributors

git log | grep Author | sort | uniq
  • Alessandro Dal Grande
  • David Butler
  • Dmitriy Molodtsov
  • Dmytro Salko
  • James McCarthy
  • Jesse McGinnis
  • Joey Geiger
  • Jon Kinney
  • Justin Buchanan
  • Morton Jonuschat
  • Piotr Kuczynski
  • Thomas Hoen
  • Travis Berry

If you feel like contributing there is a TODO list in the root with a few ideas and opportunities!

Credits

Thanks to Ryan Bates for creating the awesome CanCan (http://wiki.github.com/ryanb/cancan) and Martin Rehfeld for implementing Role Based Authorization in the form of RoleModel (http://github.com/martinrehfeld/role_model).

Copyright

Copyright (c) 2011-2017 James McCarthy, released under the MIT license

canard's People

Contributors

aledalgrande avatar dsalko avatar dwbutler avatar james2m avatar jgeiger avatar jondkinney avatar justbuchanan avatar kevthedev avatar kipkosek avatar ninetwentyfour avatar pkuczynski avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

canard's Issues

Multiple Abilities?

It would be awesome to have multiple ability definitions for applications that have multiple "apps" with different permissions (primary example being an admin app vs the standard app).

I know this is a relatively common use-case in plain old CanCan(Can), but was wondering how you'd suggest doing so with Canard?

I have a rudimentary solution working, but it requires me to create a custom Ability class from yours (partly due to the methods being private over protected, partly to provide a hook)

class CustomAbility < Ability
  def initialize(object=nil)
    @user = object.respond_to?(:user) ? object.user : object
    setup_abilities
  end

protected

  def setup_abilities
  end

  def add_role_abilities(roles)
    roles = [roles] unless roles.is_a? Array
    roles.each{ |role| self.send(:append_abilities, role) }
  end

  def get_ability_key(class_name)
    self.send(:ability_key, class_name)
  end
end

and then for my "custom" ablities, inheriting from the new class, and adjusting as necessary (currently by just simply appending admin_ to the front, just to see if it'd work)

class AdminAbility < CustomAbility
protected
  def setup_abilities
    return unless @user

    user_class_name = String(@user.class.name)
    add_role_abilities get_ability_key("admin_#{user_class_name}") unless user_class_name.empty?

    # If user has roles get those abilities
    add_role_abilities @user.roles.map{ |role| role.to_s.prepend('admin_').to_sym } if @user.respond_to?(:roles)
  end
end

Get All Available Roles

I'm trying to get the available roles for the user model defined, so I can display them in my view. I know with the role model gem I can call User.valid_roles to return all the available roles declared. Is there a way to do this with canard? If I do use ".valid_roles" on my user object it gives me an undefined method error.

Help with this will be appreciated

EDIT - my mistake, saw I was calling it on the user object instead of on the User model - is working now.

Heroku, devise, and canard fails on rake assets:precompile

With Rails 4.0 beta1, devise loads the user model which, in turn, loads canard which checks it for a role matrix. This is fine if the database exists, but as many of us know from painful experience Heroku does not bring the database up before running the precompile. As a result you see:

Preparing app for Rails asset pipeline
       Running: rake assets:precompile
       rake aborted!
       could not connect to server: Connection refused
       Is the server running on host "127.0.0.1" and accepting
       TCP/IP connections on port 5432?
       /tmp/build_2sfjhhr6toicw/vendor/bundle/ruby/2.0.0/gems/activerecord-4.0.0.beta1/lib/active_record/connection_adapters/postgresql_adapter.rb:771:in `initialize'
       /tmp/build_2sfjhhr6toicw/vendor/bundle/ruby/2.0.0/gems/activerecord-4.0.0.beta1/lib/active_record/connection_adapters/postgresql_adapter.rb:771:in `new'
       /tmp/build_2sfjhhr6toicw/vendor/bundle/ruby/2.0.0/gems/activerecord-4.0.0.beta1/lib/active_record/connection_adapters/postgresql_adapter.rb:771:in `connect'
       /tmp/build_2sfjhhr6toicw/vendor/bundle/ruby/2.0.0/gems/activerecord-4.0.0.beta1/lib/active_record/connection_adapters/postgresql_adapter.rb:493:in `initialize'
       /tmp/build_2sfjhhr6toicw/vendor/bundle/ruby/2.0.0/gems/activerecord-4.0.0.beta1/lib/active_record/connection_adapters/postgresql_adapter.rb:41:in `new'
       /tmp/build_2sfjhhr6toicw/vendor/bundle/ruby/2.0.0/gems/activerecord-4.0.0.beta1/lib/active_record/connection_adapters/postgresql_adapter.rb:41:in `postgresql_c
onnection'
       /tmp/build_2sfjhhr6toicw/vendor/bundle/ruby/2.0.0/gems/activerecord-4.0.0.beta1/lib/active_record/connection_adapters/abstract/connection_pool.rb:446:in `new_c
onnection'
       /tmp/build_2sfjhhr6toicw/vendor/bundle/ruby/2.0.0/gems/activerecord-4.0.0.beta1/lib/active_record/connection_adapters/abstract/connection_pool.rb:456:in `check
out_new_connection'
       /tmp/build_2sfjhhr6toicw/vendor/bundle/ruby/2.0.0/gems/activerecord-4.0.0.beta1/lib/active_record/connection_adapters/abstract/connection_pool.rb:427:in `acqui
re_connection'
       /tmp/build_2sfjhhr6toicw/vendor/bundle/ruby/2.0.0/gems/activerecord-4.0.0.beta1/lib/active_record/connection_adapters/abstract/connection_pool.rb:364:in `block
 in checkout'
       /tmp/build_2sfjhhr6toicw/vendor/bundle/ruby/2.0.0/gems/activerecord-4.0.0.beta1/lib/active_record/connection_adapters/abstract/connection_pool.rb:363:in `check
out'
       /tmp/build_2sfjhhr6toicw/vendor/bundle/ruby/2.0.0/gems/activerecord-4.0.0.beta1/lib/active_record/connection_adapters/abstract/connection_pool.rb:273:in `block
 in connection'
       /tmp/build_2sfjhhr6toicw/vendor/bundle/ruby/2.0.0/gems/activerecord-4.0.0.beta1/lib/active_record/connection_adapters/abstract/connection_pool.rb:272:in `conne
ction'
       /tmp/build_2sfjhhr6toicw/vendor/bundle/ruby/2.0.0/gems/activerecord-4.0.0.beta1/lib/active_record/connection_adapters/abstract/connection_pool.rb:552:in `retri
eve_connection'
       /tmp/build_2sfjhhr6toicw/vendor/bundle/ruby/2.0.0/gems/activerecord-4.0.0.beta1/lib/active_record/connection_handling.rb:79:in `retrieve_connection'
       /tmp/build_2sfjhhr6toicw/vendor/bundle/ruby/2.0.0/gems/activerecord-4.0.0.beta1/lib/active_record/connection_handling.rb:53:in `connection'
       /tmp/build_2sfjhhr6toicw/vendor/bundle/ruby/2.0.0/gems/activerecord-4.0.0.beta1/lib/active_record/model_schema.rb:203:in `table_exists?'
       /tmp/build_2sfjhhr6toicw/vendor/bundle/ruby/2.0.0/gems/canard-0.4.1/lib/canard/adapters/active_record.rb:30:in `active_record_table?'
       /tmp/build_2sfjhhr6toicw/vendor/bundle/ruby/2.0.0/gems/canard-0.4.1/lib/canard/adapters/active_record.rb:35:in `has_roles_mask_accessors?'
       /tmp/build_2sfjhhr6toicw/vendor/bundle/ruby/2.0.0/gems/canard-0.4.1/lib/canard/user_model.rb:74:in `acts_as_user'
       /tmp/build_2sfjhhr6toicw/app/models/user.rb:12:in `<class:User>'
       /tmp/build_2sfjhhr6toicw/app/models/user.rb:1:in `<top (required)>'

The workaround that got Heroku back up and running was to only run the canard helper if the connection is present.

  # must test for the connection because of a precompile problem on Heroku 
  acts_as_user :roles => [:timekeeper, :admin] if ActiveRecord::Base.connected?

Not sure if anything can be done on the code side but if it was built in to the matrix check that would help. Possible? Necessary? Thanks so much for the wonderful work on this gem. I rolled my own at first and then appreciated the hard work that went into this gem all the more.

Instalation

HI,
One question about instalation. Should I define any column for storing role attributes? I could not find any details about this in readme and sourcecode.

How to stub the ability in the step_definitions

Hi,
I am having a code like can? :access, :project in my views. Based on that I am showing the link.
So that is defined in the abilities file as usual which is working fine.

i.e., eg:
app/abilities/admin.rb
I am using can :access, :project

app/abilities/employee.rb
I am using cannot :access, :project

Now I wanted to test in the following way in cucumber with the help of step_definition:

  • Login as User (Any User Admin/Employee)
  • In one test case I wanted to pass the value as true for :access, :project and test the project link
  • In another test case I wanted to pass the value as false for :access, :project and test the project link

So this means I wanted to dynamically change the ability and ensure the test pass.
How DO I do this?
I have done something like this which is not working.

@user.ability.stub(:can?).with(:access, :project).and_return(false)

The reason behind this is tomorrow we might change employee ability also to can :access, :project hence i want this dynamic behavior.

User abilities don't include guest abilities

I ended up not using Canard for a recent project because the "additive" nature of abilities does not include guest abilities.

I think the behavior should be, for example: guest, user, manager, admin
Instead it is: user, manager, admin

So any abilities defined for guest have to be duplicated in user. Not very DRY. While I realize this is a breaking change, to be perfectly honest, I can't think of any real world examples where a user would be denied from doing something that a guest can do.

How to use cancard with separate context?

Hello,
First of all I would like to say thank you for great and well tested open source project.
I have trouble with getting out of my problem so I decided to ask for help. Currently I am trying to make system with couple separate interfaces but with one User model. There is Admin, Partner and User interface. User could have many global roles (cancard fit there very good) but User could have many roles as PartnerUser (model which contains relation user_id, partner_id, role). I dont know how to set up before_filter in my Partner base namespace to add this separate context (role of user for selected partner).

Please checkout my relations:

class PartnerUser < ActiveRecord::Base
  ROLES = {
    owner: 0,
    employee: 1 
  }

  belongs_to :user
  belongs_to :partner

  has_many :transactions, class_name: "Transaction"

  validate :role, inclussion: { in: ROLES }
class Partner < ActiveRecord::Base

  has_many :partner_users, include: :user
  has_many :users, through: :partner_users
class User < ActiveRecord::Base
  ALLOWED_ROLES = [:manager, :admin, :reporter]
  acts_as_user roles: ALLOWED_ROLES

  # Partner
  has_many :partner_users
  has_many :partners, through: :partner_users

Partner base controller

# -*- encoding : utf-8 -*-
class Partner::BaseController < InheritedResources::Base
  layout 'admin/index'

  before_filter :authenticate_user!
  before_filter :authorize_for_subdomain!

  helper_method :current_user_role
  helper_method :current_partner

  rescue_from ActiveRecord::RecordNotFound, with: :partner_not_found

  add_crumb(I18n.t('partner.dashboard.singular')) { |instance| instance.send :partner_root_path }

  def authorize_for_subdomain!
    @subdomain = request.subdomain.to_s.split('.').first
    @current_partner = Partner.find_by_url(@subdomain)

    raise ActiveRecord::RecordNotFound unless @current_partner.present?
    @current_partner_user = @current_partner.partner_users.where(user_id: @current_user.id).try(:first)

    raise CanCan::AccessDenied unless @current_partner_user.present?

    @current_user_role = @current_partner_user.current_role || :employee
    current_user
  end

  private

  def current_user_role
    authorize_for_subdomain! unless @current_user_role.present?
    @current_user_role
  end

  def current_partner
    authorize_for_subdomain! unless @current_partner.present?
    @current_partner
  end

  protected

  def partner_not_found
    flash[:error] = "Poszukiwana strona nie istnieje"
    redirect_to root_path
  end

  def begin_of_association_chain
    current_partner
  end

  def set_current_user_for_acl
    @current_ability ||= Ability.new(current_admin)
  end


end

Thanks in advantage for any suggestions :).
Have a nice day!

test_unit[not found]

I'm using the canard gem to set up user roles and am running into a test_unit[not found] error. I tried adding test-unit to the Gemfile but it still fails. Disclaimer: I'm still relatively novice at Rails.

 $ rails g canard:ability user can:create:post
    conflict  app/abilities/users.rb
Overwrite /home/ubuntu/project/app/abilities/users.rb? (enter "h" for help) [Ynaqdh] Y
       force  app/abilities/users.rb
       error  test_unit [not found]

Bundler won't install canard unless Rails is already installed

This stems from the require 'rails' on line 4 of the gemspec. I see why you're doing that, so you can check whether to use cancan or cancancan, but it doesn't seem like this is safe. I even added a runtime_dependency to the gemspec for rails and it still didn't try to install rails before trying to install canard. I'm sure there is a way, but I'm not sure what it is. Can you help?

There was a LoadError while loading canard.gemspec:
cannot load such file -- rails from
  /rails/github/canard/canard.gemspec:4:in `<main>'

This is from my local branch, which is why the path is not maybe what you'd expect.

Thanks!

cancancan support

Hi there,

Thanks for this gem, I like it quite a bit. I'm curious about support for cancancan. I noticed that it seems to be in the gemspec on github, but perhaps the latest code is not released as a gem? For now I've specified the master branch of this repo on my gemfile and then cancancan was installed as a dependency of canard. Any plans to release those 6 month old changes to gem land? Or perhaps I'm just doing something wrong since I was still seeing cancan installed when just using gem 'canard' in my gemfile.

Thanks!

Support for rolify?

I really like how canard simplifies authorization and roles. However, for my particular application I have been agonizing over role_model's inability to scope a role to a particular resource. I also have nightmares in my sleep thinking about when I need to add a new role and migrate all the bitmasks over. shudder

I'd like to try out rolify but I don't want to give up canard!

After looking things over, I think it might be possible to support rolify in canard. For example, most of the stuff in ::acts_as_user and the Adapters aren't necessary, because rolify already defines scopes for ActiveRecord and Mongoid. This stuff could conditionally be loaded only if the model is_a? RoleModel or perhaps via a configuration option.

If this sounds plausible/interesting to you, I'd be willing to give it a shot.

.has_role? :user returns false

Great job wrapping up role_model! This is actually a role_model suggestion but I guess I want to point that the example:

class User < ActiveRecord::Base

acts_as_user :roles => :manager, :admin

end

suggests that abilities are checked in the following order:

app/abilities/users.rb
app/abilities/manager.rb
app/abilities/admin.rb

And indeed, I confirmed that /users.rb is called for users with empty roles, and yet of course has_role? :user returns false. Since, of course, you're keeping canard consistent with role_model, then an end-user work around would explicitly specify the :user role in the list of roles and explicitly set each new user to be a :user,

acts_as_user :roles => :user, :manager, :admin

Of course, while it's easy to add a :user role later and add the :user role to every existing user if someone really feels the need to do this, maybe it would be worth documenting a sentence or two about this inconsistency.

Non-inherited Roles

Is it possible to create roles that don't inherit abilities from "lesser" roles? I need two separate roles that are disjoint, and it seems that's not possible atm.

Allow one role to inherit abilities of other roles

Though one can assign multiple roles to user, to try to keep the abilities DRY, we would prefer to get the same affect when assigning a single role to a user. For example, if I had the following roles

  • User - access only to login and own profile
  • Writer - Can do everything a User can do, and create content
  • Editor - Can do everything a writer can do, can assign stories, can publish
  • Manager - Can do everything an editor can do, can agree to jobs, can remove published stories, etc
  • Supervisor - etc

Instead of making sure that the Supervisor user also was assigned to all of the preceding roles, I would like to define the Supervisor's abilities so that it inherited from all of the preceding abilities.

I am thinking that at the top of an abilities definition, I include a call like:

includes_abilities_of :user, :writer, :editor, :manager

Then add the following to the Ability class:

  def includes_abilities_of(*other_roles)
    other_roles.each { |other_role| append_abilities(other_role) }
  end

If this makes sense to you, i am happy to issue a pull request

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.