nathanl / authority

*CURRENTLY UNMAINTAINED*. Authority helps you authorize actions in your Rails app. It's ORM-neutral and has very little fancy syntax; just group your models under one or more Authorizer classes and write plain Ruby methods on them.
MIT License
1.21k stars 67 forks source link

More complex layering authorization #63

Closed radeno closed 10 years ago

radeno commented 11 years ago

Hi,

i'm migrate from CanCan to Authority and i stuck on one thing.

My App has one more layer than only user and role authorizing. It is Account authorizing.

So every user should belongs to more accounts with different roles.

Account, User, Role
  has_many    :account_user_roles
end

AccountUserRole
  belongs_to :account
  belongs_to :user
  belongs_to :role
end

Account is layer, that separate clients against each other. Like

For example: user 1 belongs to account 1 with role admin user 1 belongs to account 2 with role author

User must be switched only in one account. So if i'm switched in Account 2 i have permissions only as author for that account.

Can you please hint me, how to do this?

In my controller i have helpers current_account, current_role

With CanCan it was ease, because there should be initialize with more arguments:

Ability.new(current_user, current_account, current_role, ...etc)

and in ability.rb do for example

can :manage, Media::Document, user_id: @current_user.id, account_id: @current_account.id

Thank you for help.

R.

nathanl commented 11 years ago

@radeno,

Let me see if I understand correctly.

I think you're saying that, for class-level checks, we need to specify the account. In other words, we can't just say "can the current user create documents?", but rather, "can the current user create documents for the current account?"

Using Authority, you'd pose this question in your views as:

current_user.can_create? Media::Document, account: current_account

In your authorizer, you might do something like:

class DocumentAuthorizer
  def self.creatable_by?(user, options = {})
    account = options.fetch(:account) { raise "You must specify an account" }
    [:admin, :author].any? { |role| user.roles_for_account(account).include?(role) }
  end
end

For instance-level checks, I'm guessing that you can look up the resource's account? For example:

current_user.can_edit? @document

and in the authorizer:

def editable_by?(user)
  account = resource.account # we know which account this document belongs to
  # ... etc
end

The only tricky part is the controller. authorize_actions_for sets up a before_filter for you, but any options you pass are considered options for the filter itself (like except: :create or whatever).

To also pass in the current_account, you'll need to roll your own call to before_filter (or before_action, since Rails 4), like this:

class Media::DocumentsController < ApplicationController

  before_filter except: :create { authorize_action_for Media::Document, account: current_account }

  # ...
end

Finally, since you won't be using authorize_actions_for, if you need to specify any special controller action mappings, use authority_actions. For example:

authority_actions :breed => 'create', :vaporize => 'delete`

Let me know if I've misunderstood something or if you have any more questions.

radeno commented 10 years ago

@nathanl

thank you for you response and aim me to do this!

Yes i mean current user have permisionios defined by role for specific account (account is like namespace).

I'm so much influenced by simplicity of CanCan that takes me some time to implement this solution with Authority and is really great that is possible.

Please give me some time to try it. I believe that this will be new core gem of all my new projects.

R.

radeno commented 10 years ago

Hi again.

It seems there should be a way to use this authorization instead CanCan. Great! :) It is little handy to define class and instance of this class separately. And if is system more complex (Accounts, Roles, Users, Licenses, Groups) it is more harder to maintenance. CanCan was great that unified it (but bad, is abandoned). I find in yours TODO that want to improve it. Good info ;)

radeno commented 10 years ago

One question,

is there any solution how to filter on database layer within Authority? (not necessary if SQL, NoSQL or flat file)

For example to reduce of non necessary data. I know that it is possible to filter data in ruby, but it is more faster to do this first step in database.

Or it is only possible in own controller actions?

CanCan has database adapters. Do you plan add some like this? I think there is many projects which want to migrate from CanCan to another one with easiness of original solution.

R.

nathanl commented 10 years ago

is there any solution how to filter on database layer within Authority?

There's currently no built-in way to filter records with Authority. What you can do is define scopes on your model and use them in your authorizer. For instance:

class Article
  # Note that this is pseudocode; I haven't tested it
  scope :accessible_to, lambda { |user|
   return self.all if user.admin?
    { :conditions => { :user_id => user.id }
  }
end

In your controller:

class ArticlesController < ApplicationController
  def index
    @articles = Article.accessible_to(current_user)
  end
end

and in your authorizer:

class ArticleAuthorizer < ApplicationAuthorizer
  def editable_by?(user)
    # See http://stackoverflow.com/a/1255929/4376
    resource.class.accessible_to(user).exists?(resource)
  end
end

CanCan has database adapters. Do you plan add some like this?

No. One of the goals of Authority is to be neutral about what database or ORM you use. It works with all of them, but the tradeoff is that it requires you to do the work of looking up records.

I'd consider adding support for scopes, but only in the ORM-neutral way that, for example, Searchlight uses them (https://github.com/nathanl/searchlight)

radeno commented 10 years ago

@nathanl

Thank you for answer. So i must write database filter layer by own. Maybe it is disadvantage cause vast majority of all permission restrictions are with model attributes defined by ORM and their associations to another models. e.g. One user (editor) can update all Articles and another (author) can update only own, third one (contributor) can update only own and not published yet (state draft). Or specific editor can update articles only from defined categories, not all (ORM association, database join). And let imagine that there are thousands of articles with rich count of attributes and. It is performance issue. So nearly same logic defined in authority permission model must be duplicated to controller and it very complicated to maintain. I see authorization as integrated process.

I know that Authority is database agnostic and you can't agree with me.


And of course, i found very helpful way how to automatically push information about current_role and current_account to user. It is not necessary to use more parameters. Put to User model this:

cattr_accessor :current_role
cattr_accessor :current_account

And in Application controller: current_role and current_account are controller methods returns active account and role of user for that account.

  before_action :initialize_user_role
  before_action :initialize_user_account

  private

  def initialize_user_role
    Access::User.current_role ||= current_role
  end

  def initialize_user_account
    Access::User.current_account ||= current_account
  end

so in user instance are accessible both methods.

If this will work, I will write short wiki for users with same problem as i have.

nathanl commented 10 years ago

I know that Authority is database agnostic and you can't agree with me.

Actually, see #65; I think maybe we can do something that will be helpful after all.

Regarding this:

def initialize_user_role
    Access::User.current_role ||= current_role
  end

Two things I would caution: 1) it looks like you're modifying the Access::User class itself. If so, that's not threadsafe; you want to do that on an instance, not the class. 2) If this setup is required for all authorization, it's fine for your controllers to set it up during the request cycle, but be aware that this means you can't do authorization during, say, a rake task without some additional work there.

radeno commented 10 years ago

Sorry for my time delay. I currently migrate quite large project from Rails 3 to Rails 4, and its little bit confused :)

  1. You are right about that i must write define it to instance not class.
  2. I know, it must be defined or activity fails. Its like required attributes in model.