chaps-io / access-granted

Multi-role and whitelist based authorization gem for Rails (and not only Rails!)
MIT License
775 stars 41 forks source link

Support for introspection #42

Open lastdays opened 7 years ago

lastdays commented 7 years ago

I propose to add an interface for introspection uses. It can help in different situations. For example, if you need to serialize some object along with current_user's privileges on it now I should carefully copy it from policy one by one. Getting this list dynamically in run-time make things a lot of easier and less error-prone.

pokonski commented 7 years ago

Yeah this is an idea I wanted to implement for a longer while now. Some sort of a debug mode where you can see why any given permission was granted or denied.

technodrome commented 6 years ago

Hi @pokonski, first of all, thanks for your work, your gem is a gem! :)

Do you have any update concerning introspection? Having a method where one could clearly see which roles apply to an user would be extremely helpful. I've just spent an afternoon trying to figure out which roles an user had and still couldn't figure it out completely.

There is a method current_policy but it is quite confusing. There's always currently logged-in user listed in each role with granted: true which is really confusing as many of the roles listed in that output are actually not granted to that user. I tried with a test user with low authorization and even my own admin role had granted: true in that hash even though that low-privileged user actually did not get admin role from the gem. So I do not know how this is constructed and what that granted: true means for roles which are actually not granted to low-privileged user, but it would be mighty helpful to get some introspection for exactly this reason.

Could you maybe whip up something really simple to at least list the assigned roles in the upcoming months to help us users out? I mean something along the lines of a new method assigned_roles in AccessGranted::Rails::ControllerMethods would be really, really great.

Thank you very much for your future contributions to this much-needed gem!

pokonski commented 6 years ago

hey @technodrome! There's actually an internal method called applicable_roles. I haven't had time to work on the debug mode mentioned earlier but it's still something we could all use.

For now you can access that list of roles by doing something like this:

current_policy.send(:applicable_roles)

I will make it public API soon :)

technodrome commented 6 years ago

Thanks, @pokonski for your help. Based on that I created a method I put in application_controller to get list of all available resources and all actions grouped by roles. Someone might find that useful until your solution is available so I am putting it here to make life easier for fellow developers.

  # show applicable AccessGranted roles for current user
  # a temporary fix until AccessGranted API is available
  # https://github.com/chaps-io/access-granted/issues/42
  #
  # applicable_roles returns an array of roles where each index is one role with nested objects it applies (grants access) to
  # and available CRUD actions on these objects
  def current_roles
    policy_roles = current_policy.send(:applicable_roles)

    # new unknown hash keys (:gues, :admin) will be holding nested hashes
    roles = Hash.new { |hash, key| hash[key] = {} }
    # for rights, new unknown hash keys will be holding arrays of rights (CRUD actions: create, read, ...) by our default set here
    rights = Hash.new { |hash, key| hash[key] = [] }

    policy_roles.each do |role|
      # clear the hash holding rights (CRUD) for the next role (:admin, :guest), otherwise they'd get added to the rights
      # of the previously processed role, i.e. we'd have two 'read' for :admin. One for the admin role itself, the other 'read'
      # for :guest role if both roles apply to one user. We need 'deep_dup' to preserve 'roles' hash and clear 'rights' hash
      # to get only rights (create, read, etc) for the next role
      rights.clear

      role.permissions.each do |permission|
        # rights[:Comment] will contain [:create, :read, ...]
        rights[permission.subject.to_s.to_sym] << permission.action

        # do a deep dup to preserve roles as we need to clear 'rights' hash for the next role if user has more than one
        roles[role.name] = rights.deep_dup
      end
    end

    roles
  end

The result is:

{
    :editor => {
               :Image => [
            [0] :read,
            [1] :create,
            [2] :update,
            [3] :destroy
        ],
             :Comment => [
            [0] :read,
            [1] :create,
            [2] :update,
            [3] :destroy
        ]
    },
     :guest => {
               :Image => [
            [0] :read
        ],
             :Comment => [
            [0] :read
        ],
    }
}
pokonski commented 6 years ago

@technodrome but keep in mind you are not actually resolving the permissions so even if :read is present for Image it does not mean the user can perform that action. This is because permissions can have blocks of code which need to be executed against a specific target.

You are simply listing all defined permissions. I assume it's what you want but worth noting for any other users.

technodrome commented 6 years ago

Thanks for mentioning that, @pokonski. I know, I am also using blocks to check whether a blogpost.author is identical with current_user etc. But I do not know how I could abstract away those checks to make it available as a controller method as blogpost is not defined in Image controller. I am not that well versed in the inner workings of your gem. I just took what your private method applicable_roles gave me and parsed it to offer a high level overview.

I do not know if the gem sets granted = false or something similar in case those procs resolve to false. If you can maybe shed some more light on this, I'd be easy to take that boolean into consideration when building my final hash. Or, just take it for folks who need it and keep that block caveat in mind.

pokonski commented 6 years ago

I do not know if the gem sets granted = false or something similar in case those procs resolve to false.

It doesn't. Simply put, by reading role.permissions you are actually reading just permission definitions, not actual result of can? method. As you can imagine when you write a permission like this:

can :read, Post do |post|
   post.published == true
end

it can only resolve that permission when you supply an actual instance of the Post. You cannot dump a single hash of all permission resolutions without providing subjects. I hope I made it clear with the example :)

But if you don't use blocks to define permissions, you can use a snippet I developed for one of my projects which dumps what user can do for "subject-less" permissions (ones not using any block or hash and which don't rely on actual instances of objects), as a hash of permissions:

class AccessPolicy
  include AccessGranted::Policy

  def permissions_map
    result = {}

    applicable_roles.each do |role|
      role.permissions.map do |permission|
        next if permission.block
        result[permission.subject.name] ||= []

        if can?(permission.action, permission.subject)
          result[permission.subject.name].push(permission.action)
        end
      end
    end

    result
  end
  # (...)
end

As you can see, it iterates over applicable_roles and their permissions but checks if user can perform action using can? and only for permissions without a block, as you can probably guess why now.


tl;dr introspection in the way you wanted to achieve is not possible

technodrome commented 6 years ago

Thanks a bunch for a detailed response, Piotr. I like it when people are detailed with their answers :)

What I basically wanted was to get a list of all applicable roles, not actions in a quick way. That was my main goal and my code luckily achieves that. The task was not to check the actual applicable actions within those roles, it is more of a reminder what a member, admin, guest should be able to do, although I got curious about how you do stuff when I started digging in a bit.

As I see it, specific can? and cannot? are in this case better left for tests. So that was not the goal of the introspection. I was really after something rather simplistic - to answer the question: "which roles apply to me as a user currently viewing this page?" - because in case of my projects, several roles apply to my user simultaneously (guest + editor with editor overrides as seen in my example output). I just wanted that.

Having said that, I understand what you said and we are on the same page. I will take some time to review the code you wrote just to get to know your gem a bit more in my free time. It is like reading a story and yours really is useful.

Thanks for your help, it is much appreciated and all the best to you.

pokonski commented 6 years ago

Great, I'm glad I was wrong, makes the solution much easier :D

In that case you can simply extract just the names from applicable_roles :)

The snippet I shared I personally use to extract some top level permissions from API to the frontend client. (For hiding menu options mostly :)). Resource specific actions are checked when it is serialized.

Crisfole commented 2 years ago

@pokonski A small update to your permissions_map method:

  def permissions_map
    result = {}

    applicable_roles.each do |role|
      role.permissions.each do |permission|
        if permission.subject
          permission_set = result[permission.subject.name] ||= {}

          if permission.block
            permission_set[permission.action] = 'check'
          else
            permission_set[permission.action] = can?(permission.action, permission.subject)
          end
        else
          if permission.block
            result[permission.action] = 'check'
          else
            result[permission.action] = can?(permission.action)
          end
        end
      end
    end

    result
  end

This results in something like this:

{
  "administrate": false,
  "fidget": true,
  "wiggle": "check",
  "WikiPage": {
    "read": true, "create": true, "update": false, "destroy": true, "custom": false
   },
  "Posts": {
    "read": true,
    "create": true,
    "update": "check",
    "destroy": "check"
  }
}

If you want to know on the front end if you have permission for something you can 100% trust a boolean to tell you the truth. If it's the string "check" you'll have to ask the server to verify because it's dependent on which object you're asking about.

Crisfole commented 2 years ago

It slightly less than ideal since negation is just 'absence' of true but that works nicely in javascript anyway.