Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add allow_with_resource #8

Merged
merged 13 commits into from
Dec 9, 2016
Merged
40 changes: 40 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,46 @@ class ProfilesController < ApplicationController
end
```

# Allow roles with a resource
`allow_with_resource` acts similarly to `allow` but requires a resource check to access the endpoint.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, as we discussed, it doesn't really require a resource check. It will check resources against an empty role_resource.


This requires a method to be defined on `SomeCustomRoleObject` that checks if the role_resource object defined on the controller matches the user role.


```ruby
class SomeCustomRoleObject
def resource?(resource)
self.resources.includes?(resource)
end
end

class ProfilesController < ApplicationController
allow_with_resource(:admin).to_access(:index)
allow_with_resource(:owner).to_access(:edit)
publicize(:show)

def index
current_roles # => [#<SomeCustomRoleObject to_role_string: "admin", resource?: true >]
end

def edit # Raises permission error before entering this
current_roles # => []
end

def show
current_roles # => []
end

private def current_user_roles
current_user.roles # => [#<SomeCustomRoleObject to_role_string: "admin", resource?: true>, #<SomeCustomRoleObject to_role_string: "scorekeeper", resource?: false>]
end

private def role_resource
{ resource: params[:resource_id] }
end
end
```

## Contributing

1. Fork it
Expand Down
18 changes: 13 additions & 5 deletions lib/rolypoly/controller_role_dsl.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ def self.included(sub)
unless sub.method_defined? :current_user_roles
define_method(:current_user_roles) { [] }
end

unless sub.method_defined? :role_resource
define_method(:role_resource) { [] }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, you wanna change this to an empty hash.

end
sub.send :extend, ClassMethods
end

Expand All @@ -31,8 +35,8 @@ def failed_role_check!
def current_roles
return [] if rolypoly_gatekeepers.empty?
current_gatekeepers.reduce([]) { |array, gatekeeper|
if gatekeeper.role? current_user_roles
array += Array(gatekeeper.allowed_roles(current_user_roles, action_name))
if gatekeeper.role?(current_user_roles, role_resource)
array += Array(gatekeeper.allowed_roles(current_user_roles, action_name, role_resource))
end
array
}
Expand All @@ -52,7 +56,7 @@ def current_gatekeepers
def rolypoly_role_access?
rolypoly_gatekeepers.empty? ||
rolypoly_gatekeepers.any? { |gatekeeper|
gatekeeper.allow? current_roles, action_name
gatekeeper.allow?(current_roles, action_name, role_resource)
}
end
private :rolypoly_role_access?
Expand All @@ -75,6 +79,10 @@ def allow(*roles)
build_gatekeeper roles, nil
end

def allow_with_resource(*roles)
build_gatekeeper roles, nil, true
end

def publicize(*actions)
restrict(*actions).to_none
end
Expand All @@ -90,8 +98,8 @@ def try_super(mname)
end
end

def build_gatekeeper(roles, actions)
RoleGatekeeper.new(roles, actions).tap { |gatekeeper|
def build_gatekeeper(roles, actions, require_resource = false)
RoleGatekeeper.new(roles, actions, require_resource).tap { |gatekeeper|
rolypoly_gatekeepers << gatekeeper
}
end
Expand Down
26 changes: 19 additions & 7 deletions lib/rolypoly/role_gatekeeper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@
module Rolypoly
class RoleGatekeeper
attr_reader :roles
def initialize(roles, actions)
def initialize(roles, actions, require_resource)
self.roles = Set.new Array(roles).map(&:to_s)
self.actions = Set.new Array(actions).map(&:to_s)
self.require_resource = require_resource
self.all_actions = false
self.public = false
end
Expand All @@ -31,22 +32,23 @@ def to_all
self.all_actions = true
end

def allow?(current_roles, action)
def allow?(current_roles, action, resource)
action?(action) &&
role?(current_roles)
role?(current_roles, resource)
end

def allowed_roles(current_roles, action)
def allowed_roles(current_roles, action, resource)
return [] if public? || !action?(action)
match_roles(current_roles)
match_roles(current_roles, resource)
end

def all_public
self.public = true
self.all_actions = true
end

def role?(check_roles)
def role?(check_roles, resource)
check_roles = filter_roles_by_resource(check_roles, resource)
check_roles = Set.new sanitize_role_input(check_roles)
public? || !(check_roles & roles).empty?
end
Expand All @@ -65,15 +67,25 @@ def public?
attr_accessor :actions
attr_accessor :all_actions
attr_accessor :public
attr_accessor :require_resource

def match_roles(check_roles)
def match_roles(check_roles, resource)
check_roles = filter_roles_by_resource(check_roles, resource)
check_roles.reduce([]) { |array, role_object|
array << role_object if roles.include?(sanitize_role_object(role_object))
array
}
end
private :match_roles

def filter_roles_by_resource(check_roles, resource)
return check_roles if check_roles.nil? || !require_resource
check_roles.select do |check_role|
check_role.respond_to?(:resource?) && check_role.resource?(resource)
end
end
private :filter_roles_by_resource

def sanitize_role_input(role_objects)
Array(role_objects).map { |r| sanitize_role_object(r) }
end
Expand Down
47 changes: 47 additions & 0 deletions spec/lib/rolypoly/controller_role_dsl_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ module Rolypoly
subject { example_controller }
it { should respond_to :restrict }
it { should respond_to :allow }
it { should respond_to :allow_with_resource }

describe "setting up with DSL" do
describe "from allow side" do
Expand Down Expand Up @@ -76,6 +77,52 @@ module Rolypoly
end
end
end

describe "from allow_with_resource side" do
let(:controller_instance) { subject.new }
let(:admin_role) { RoleObject.new(:admin) }
let(:scorekeeper_role) { RoleObject.new(:scorekeeper) }
let(:current_user_roles) { [admin_role, scorekeeper_role] }
let(:role_resource) { {resource: 123} }
let(:check_access!) { controller_instance.rolypoly_check_role_access! }

before do
subject.allow_with_resource(:admin).to_access(:index)
subject.publicize(:landing)
allow(admin_role).to receive(:resource?).and_return true
allow(controller_instance).to receive(:current_user_roles).and_return(current_user_roles)
allow(controller_instance).to receive(:action_name).and_return(action_name)
allow(controller_instance).to receive(:role_resource).and_return(role_resource)
end

describe "#index" do
let(:action_name) { "index" }

it { expect(controller_instance).to_not be_public }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a be_private?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it { expect{ check_access! }.not_to raise_error }
it { expect(controller_instance.current_roles).to eq([RoleObject.new(:admin)])}
end

describe "#show" do
let(:action_name) { "show" }

it { expect{ check_access! }.to raise_error(Rolypoly::FailedRoleCheckError)}
it { expect(controller_instance).to_not be_public }
end

describe "#landing" do
let(:action_name) { "landing" }

it { expect{ check_access! }.not_to raise_error }

describe "with no role" do
let(:current_roles) { [] }

it { expect { check_access! }.not_to raise_error }
it { expect(controller_instance).to be_public }
end
end
end
end
end
end
Loading