rolypoly 1.0.4 → 1.3.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of rolypoly might be problematic. Click here for more details.
- checksums.yaml +5 -5
- data/.ruby-version +1 -1
- data/README.md +2 -0
- data/lib/rolypoly.rb +1 -0
- data/lib/rolypoly/controller_role_dsl.rb +7 -3
- data/lib/rolypoly/index_role_dsl.rb +86 -0
- data/lib/rolypoly/resource_index_query_builder.rb +22 -0
- data/lib/rolypoly/role_gatekeeper.rb +11 -3
- data/lib/rolypoly/role_scope.rb +92 -0
- data/lib/rolypoly/role_scopes.rb +51 -0
- data/lib/rolypoly/version.rb +1 -1
- data/spec/lib/rolypoly/role_gatekeeper_spec.rb +42 -0
- metadata +10 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 8e4740fb3c4333e32b0fa17cd46c6069ac7f4e5a9fb8fd0d634f37a372b1cb58
|
4
|
+
data.tar.gz: f95e52673fc7211cc17ec683f5727bfd5b4d3b4b653c1db571e7eace3ce91a39
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6f34a19e2704d544039bcbe4c8ec95647a3a2498be2bac9d8d91fe9e97222b164f026dd171ba7f3fc7019d445297d11a465835c17efe4e63a9712be11d788d3b
|
7
|
+
data.tar.gz: ccd41a27601cb2b182cbd4363da214f6c63dd54864a148a6753f2cbd07257daf78d3e72d34b77a017793a6422d85804ee67011c832b7e033bcf6234301dc6de4
|
data/.ruby-version
CHANGED
@@ -1 +1 @@
|
|
1
|
-
2.
|
1
|
+
2.6.5
|
data/README.md
CHANGED
data/lib/rolypoly.rb
CHANGED
@@ -5,11 +5,15 @@ module Rolypoly
|
|
5
5
|
module ControllerRoleDSL
|
6
6
|
|
7
7
|
def self.included(sub)
|
8
|
-
|
9
|
-
|
8
|
+
if sub.respond_to? :before_filter
|
9
|
+
sub.before_filter(:rolypoly_check_role_access!)
|
10
|
+
elsif sub.respond_to? :before_action
|
11
|
+
sub.before_action(:rolypoly_check_role_access!)
|
12
|
+
end
|
13
|
+
if sub.respond_to? :rescue_from
|
10
14
|
sub.rescue_from(FailedRoleCheckError) do
|
11
15
|
respond_to do |f|
|
12
|
-
f.html { render
|
16
|
+
f.html { render plain: "Not Authorized", status: 401 }
|
13
17
|
f.json { render json: { error: "Not Authorized" }, status: 401 }
|
14
18
|
f.xml { render xml: { error: "Not Authorized" }, status: 401 }
|
15
19
|
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
require 'rolypoly/role_scopes'
|
3
|
+
|
4
|
+
module Rolypoly
|
5
|
+
module IndexRoleDSL
|
6
|
+
|
7
|
+
def self.included(base)
|
8
|
+
base.extend(ClassMethods)
|
9
|
+
base.send(:include, InstanceMethods)
|
10
|
+
end
|
11
|
+
|
12
|
+
module InstanceMethods
|
13
|
+
extend Forwardable
|
14
|
+
|
15
|
+
def self.included(base)
|
16
|
+
unless base.method_defined?(:current_user_roles)
|
17
|
+
base.send(:define_method, :current_user_roles) do
|
18
|
+
raise NotImplementedError
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def_delegators 'self.class', :role_scopes
|
24
|
+
def_delegators :role_scopes, :public?
|
25
|
+
|
26
|
+
def apply_scopes
|
27
|
+
return query if role_scopes.all_access?(current_user_roles)
|
28
|
+
return query.none if scope_hash.empty?
|
29
|
+
if scope_hash.keys.length == 1
|
30
|
+
scope_hash.inject(query) { |query, (scope_name, ids)| query.public_send(scope_name, ids) }
|
31
|
+
else
|
32
|
+
# This block is designed to handle cases of more than one scope.
|
33
|
+
# The following code has been demonstrated to perform on most "simple"...
|
34
|
+
#...objects and scopes. However, there are certain complex objects...
|
35
|
+
#...and scopes that cause permission checks to fail.
|
36
|
+
object_query = query
|
37
|
+
object_query = join_tables.inject(object_query) do |q, join_table|
|
38
|
+
q.joins(join_table)
|
39
|
+
end
|
40
|
+
|
41
|
+
scope_hash.inject(object_query) do |object_query, (scope_name, ids)|
|
42
|
+
object_query.or(query.public_send(scope_name, ids))
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def join_tables
|
48
|
+
scope_hash.map do |scope_name, ids|
|
49
|
+
query.public_send(scope_name, ids).values[:joins]
|
50
|
+
end
|
51
|
+
.flatten
|
52
|
+
.uniq
|
53
|
+
.reject { |join_table| query_join_tables.include?(join_table) }
|
54
|
+
end
|
55
|
+
|
56
|
+
def query_join_tables
|
57
|
+
values = query.try(:values) || {}
|
58
|
+
values.fetch(:joins, [])
|
59
|
+
end
|
60
|
+
|
61
|
+
|
62
|
+
def scope_hash
|
63
|
+
@scope_hash ||= role_scopes.scope_hash(current_user_roles)
|
64
|
+
end
|
65
|
+
|
66
|
+
def allowed_roles(scope_name)
|
67
|
+
role_scopes.allowed_roles(current_user_roles, scope_name)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
module ClassMethods
|
72
|
+
extend Forwardable
|
73
|
+
|
74
|
+
def inherited(subclass)
|
75
|
+
super
|
76
|
+
subclass.instance_variable_set('@role_scopes', role_scopes.dup)
|
77
|
+
end
|
78
|
+
|
79
|
+
def_delegators :role_scopes, :all_public, :allow, :allowed_roles, :on
|
80
|
+
|
81
|
+
def role_scopes
|
82
|
+
@role_scopes ||= Rolypoly::RoleScopes.new
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'rolypoly/index_role_dsl'
|
2
|
+
|
3
|
+
module Rolypoly
|
4
|
+
class ResourceIndexQueryBuilder
|
5
|
+
include Rolypoly::IndexRoleDSL
|
6
|
+
|
7
|
+
attr_reader :query, :user
|
8
|
+
|
9
|
+
allow(:third_north).to_all
|
10
|
+
allow(:org_admin).on(:organization).to_access(:org_id)
|
11
|
+
allow(:tournament_director).on(:organization).to_access(:org_id)
|
12
|
+
|
13
|
+
def initialize(query, user)
|
14
|
+
@query = query
|
15
|
+
@user = user
|
16
|
+
end
|
17
|
+
|
18
|
+
private def current_user_roles
|
19
|
+
user.role_assignments
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -84,10 +84,18 @@ module Rolypoly
|
|
84
84
|
|
85
85
|
private def allowed_resource?(check_role, required_resource)
|
86
86
|
return true unless require_resource?
|
87
|
-
return false unless check_role.respond_to?(:resource?)
|
88
87
|
|
89
|
-
|
90
|
-
|
88
|
+
case required_resource
|
89
|
+
when ->(r) { r.is_a?(Hash) && r.keys == [:resource_type] }
|
90
|
+
check_role.respond_to?(:resource_type) &&
|
91
|
+
check_role.resource_type == required_resource[:resource_type]
|
92
|
+
when Hash, ->(r) { type_id_resource?(r) }
|
93
|
+
check_role.respond_to?(:resource?) &&
|
94
|
+
check_role.resource?(required_resource)
|
95
|
+
else
|
96
|
+
check_role.respond_to?(:resource?) &&
|
97
|
+
Array(required_resource).any? {|r| check_role.resource?(r) }
|
98
|
+
end
|
91
99
|
end
|
92
100
|
|
93
101
|
private def type_id_resource?(required_resource)
|
@@ -0,0 +1,92 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
3
|
+
module Rolypoly
|
4
|
+
class RoleScope
|
5
|
+
attr_reader :roles, :actions
|
6
|
+
def initialize(roles, actions, resource = nil)
|
7
|
+
self.roles = Set.new Array(roles).map(&:to_s)
|
8
|
+
self.actions = Set.new Array(actions).map(&:to_s)
|
9
|
+
self.resource = resource
|
10
|
+
self.all_actions = false
|
11
|
+
end
|
12
|
+
|
13
|
+
def initialize_copy(other)
|
14
|
+
@roles = @roles.dup
|
15
|
+
@actions = @actions.dup
|
16
|
+
end
|
17
|
+
|
18
|
+
# on(resource).allow(*roles).to_access(*actions)
|
19
|
+
def allow(*roles)
|
20
|
+
self.roles = self.roles.merge(roles.flatten.compact.map(&:to_s))
|
21
|
+
self
|
22
|
+
end
|
23
|
+
|
24
|
+
# allow(*roles).on(resource).to_access(*actions)
|
25
|
+
def on(resource)
|
26
|
+
self.resource = resource
|
27
|
+
self
|
28
|
+
end
|
29
|
+
|
30
|
+
# allow(*roles).to_access *actions
|
31
|
+
def to_access(*actions)
|
32
|
+
self.actions = self.actions.merge actions.flatten.compact.map(&:to_s)
|
33
|
+
end
|
34
|
+
|
35
|
+
# allow role access to all actions
|
36
|
+
# allow(*roles).to_all
|
37
|
+
def to_all
|
38
|
+
self.all_actions = true
|
39
|
+
end
|
40
|
+
|
41
|
+
def allow?(current_roles, action)
|
42
|
+
action?(action) && role?(current_roles)
|
43
|
+
end
|
44
|
+
|
45
|
+
def allowed_roles(current_roles, action)
|
46
|
+
return [] unless action?(action)
|
47
|
+
match_roles(current_roles)
|
48
|
+
end
|
49
|
+
|
50
|
+
def role?(check_roles)
|
51
|
+
Array(check_roles).any? { |check_role| allowed_role?(check_role) }
|
52
|
+
end
|
53
|
+
|
54
|
+
def action?(check_actions)
|
55
|
+
check_actions = Set.new Array(check_actions).map(&:to_s)
|
56
|
+
all_actions? || !(check_actions & actions).empty?
|
57
|
+
end
|
58
|
+
|
59
|
+
def all_actions?
|
60
|
+
!!all_actions
|
61
|
+
end
|
62
|
+
|
63
|
+
private def allowed_resource?(check_role, required_resource)
|
64
|
+
return false unless check_role.respond_to?(:resource?)
|
65
|
+
|
66
|
+
required_resources = type_id_resource?(required_resource) ? [required_resource] : Array(required_resource)
|
67
|
+
required_resources.any? { |r| check_role.resource?(r) }
|
68
|
+
end
|
69
|
+
|
70
|
+
private def type_id_resource?(required_resource)
|
71
|
+
required_resource.is_a?(Array) && %w(String Symbol).include?(required_resource.first.class.name)
|
72
|
+
end
|
73
|
+
|
74
|
+
protected
|
75
|
+
attr_writer :roles
|
76
|
+
attr_writer :actions
|
77
|
+
attr_accessor :all_actions
|
78
|
+
attr_accessor :resource
|
79
|
+
|
80
|
+
private def match_roles(check_roles)
|
81
|
+
Array(check_roles).select do |check_role|
|
82
|
+
allowed_role?(check_role)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
private def allowed_role?(role_object)
|
87
|
+
role_string = role_object.respond_to?(:to_role_string) ? role_object.to_role_string : role_object.to_s
|
88
|
+
|
89
|
+
roles.include?(role_string.to_s)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
require 'rolypoly/role_scope'
|
3
|
+
|
4
|
+
module Rolypoly
|
5
|
+
class RoleScopes
|
6
|
+
extend Forwardable
|
7
|
+
include Enumerable
|
8
|
+
|
9
|
+
def_delegators :build_role_scope, :allow, :on
|
10
|
+
def_delegators :@role_scopes, :clear, :each, :empty?
|
11
|
+
|
12
|
+
def initialize(role_scopes = [])
|
13
|
+
@role_scopes = Array(role_scopes)
|
14
|
+
end
|
15
|
+
|
16
|
+
def initialize_copy(other)
|
17
|
+
@role_scopes = @role_scopes.map(&:dup)
|
18
|
+
end
|
19
|
+
|
20
|
+
def allowed_roles(user_role_objects, action)
|
21
|
+
return [] if empty?
|
22
|
+
|
23
|
+
reduce([]) do |allowed_role_objects, role_scope|
|
24
|
+
allowed_role_objects | role_scope.allowed_roles(user_role_objects, action)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def scope_hash(user_role_objects)
|
29
|
+
actions.each_with_object({}) do |action, memo|
|
30
|
+
roles = allowed_roles(user_role_objects, action)
|
31
|
+
ids = roles.map(&:resource_id).compact.uniq
|
32
|
+
memo[action] = ids if ids.any?
|
33
|
+
memo
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def all_access?(current_user_roles)
|
38
|
+
any? { |role_scope| role_scope.allow?(current_user_roles, nil) }
|
39
|
+
end
|
40
|
+
|
41
|
+
def actions
|
42
|
+
map(&:actions).each_with_object(Set.new) { |action_set, memo| memo.merge(action_set) }
|
43
|
+
end
|
44
|
+
|
45
|
+
private def build_role_scope(roles = nil, actions = nil, resource = nil)
|
46
|
+
new_role_scope = Rolypoly::RoleScope.new(roles, actions, resource)
|
47
|
+
@role_scopes << new_role_scope
|
48
|
+
new_role_scope
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
data/lib/rolypoly/version.rb
CHANGED
@@ -189,5 +189,47 @@ module Rolypoly
|
|
189
189
|
end
|
190
190
|
|
191
191
|
end
|
192
|
+
|
193
|
+
context 'allowed_roles' do
|
194
|
+
let(:roles) do
|
195
|
+
[double('admin', to_role_string: 'admin', resource?: nil),
|
196
|
+
double('org 1 admin', resource_id: '1', resource_type: 'Organization', to_role_string: 'org_admin', resource?: nil),
|
197
|
+
double('org 2 admin', resource_id: '2', resource_type: 'Organization', to_role_string: 'org_admin', resource?: nil),
|
198
|
+
double('tenant admin', resource_id: '3', resource_type: 'Tenant', to_role_string: 'org_admin', resource?: nil)]
|
199
|
+
end
|
200
|
+
|
201
|
+
context 'with unscoped gatekeeper' do
|
202
|
+
subject { described_class.new(['admin'], ['index']) }
|
203
|
+
|
204
|
+
it 'returns the unscoped role' do
|
205
|
+
expect(subject.allowed_roles(roles, 'index')).to eq([roles[0]])
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
context 'with resource-scoped gatekeeper' do
|
210
|
+
subject { described_class.new(['org_admin'], ['index'], :organization) }
|
211
|
+
|
212
|
+
it 'returns no roles' do
|
213
|
+
expect(subject.allowed_roles(roles, 'index')).to be_empty
|
214
|
+
end
|
215
|
+
|
216
|
+
it 'returns the matching role' do
|
217
|
+
query = ['Organization', '1']
|
218
|
+
expect(roles[1]).to receive(:resource?).with(query).and_return(true)
|
219
|
+
expect(subject.allowed_roles(roles, 'index', organization: query)).to eq([roles[1]])
|
220
|
+
end
|
221
|
+
|
222
|
+
it 'returns all roles with the same resource_type' do
|
223
|
+
query = { resource_type: 'Organization' }
|
224
|
+
expect(subject.allowed_roles(roles, 'index', organization: query)).to eq([roles[1], roles[2]])
|
225
|
+
end
|
226
|
+
|
227
|
+
it 'returns only the role matching the resource? call' do
|
228
|
+
query = { resource_id: '1', resource_type: 'Organization' }
|
229
|
+
expect(roles[1]).to receive(:resource?).with(query).and_return(true)
|
230
|
+
expect(subject.allowed_roles(roles, 'index', organization: query)).to eq([roles[1]])
|
231
|
+
end
|
232
|
+
end
|
233
|
+
end
|
192
234
|
end
|
193
235
|
end
|
metadata
CHANGED
@@ -1,15 +1,15 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rolypoly
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0
|
4
|
+
version: 1.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jon Phenow
|
8
8
|
- Jake Waletzko
|
9
|
-
autorequire:
|
9
|
+
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date:
|
12
|
+
date: 2020-10-29 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rake
|
@@ -58,9 +58,13 @@ files:
|
|
58
58
|
- Rakefile
|
59
59
|
- lib/rolypoly.rb
|
60
60
|
- lib/rolypoly/controller_role_dsl.rb
|
61
|
+
- lib/rolypoly/index_role_dsl.rb
|
62
|
+
- lib/rolypoly/resource_index_query_builder.rb
|
61
63
|
- lib/rolypoly/role_dsl.rb
|
62
64
|
- lib/rolypoly/role_gatekeeper.rb
|
63
65
|
- lib/rolypoly/role_gatekeepers.rb
|
66
|
+
- lib/rolypoly/role_scope.rb
|
67
|
+
- lib/rolypoly/role_scopes.rb
|
64
68
|
- lib/rolypoly/version.rb
|
65
69
|
- rolypoly.gemspec
|
66
70
|
- spec/lib/rolypoly/controller_role_dsl_spec.rb
|
@@ -72,7 +76,7 @@ homepage: https://github.com/sportngin/rolypoly
|
|
72
76
|
licenses:
|
73
77
|
- MIT
|
74
78
|
metadata: {}
|
75
|
-
post_install_message:
|
79
|
+
post_install_message:
|
76
80
|
rdoc_options: []
|
77
81
|
require_paths:
|
78
82
|
- lib
|
@@ -87,9 +91,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
87
91
|
- !ruby/object:Gem::Version
|
88
92
|
version: '0'
|
89
93
|
requirements: []
|
90
|
-
|
91
|
-
|
92
|
-
signing_key:
|
94
|
+
rubygems_version: 3.0.3
|
95
|
+
signing_key:
|
93
96
|
specification_version: 4
|
94
97
|
summary: Tools for handling per-action and per-app Role authorization
|
95
98
|
test_files:
|