ixtlan-guard 0.7.2 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/README.md +104 -0
- data/lib/ixtlan/guard.rb +1 -1
- data/lib/ixtlan/guard/{guard_ng.rb → guard.rb} +50 -34
- data/lib/ixtlan/guard/guard.rb~ +195 -0
- data/lib/ixtlan/guard/guard_config.rb +1 -2
- data/lib/ixtlan/guard/guard_rails.rb +181 -35
- data/lib/ixtlan/guard/railtie.rb +8 -5
- data/spec/guard_cache_spec.rb +4 -4
- data/spec/guard_export_spec.rb +48 -20
- data/spec/guard_rails_spec.rb +150 -0
- data/spec/guard_rails_spec.rb~ +150 -0
- data/spec/guard_spec.rb +17 -11
- data/spec/guard_with_associations_spec.rb +91 -75
- data/spec/guards/re_users_guard.yaml~ +4 -0
- metadata +10 -8
- data/features/step_definitions/ruby_maven.rb +0 -170
data/README.md
ADDED
@@ -0,0 +1,104 @@
|
|
1
|
+
# ixtlan guard #
|
2
|
+
|
3
|
+
it is an simple authorization framework for restful rails especially using rails as API server.
|
4
|
+
|
5
|
+
the idea is simple:
|
6
|
+
|
7
|
+
* each user belongs to set of groups
|
8
|
+
* each controller/action pair permits a set of groups to execute it
|
9
|
+
* the guard class checks if the user has any group which is allowed by the controller/action pair
|
10
|
+
|
11
|
+
## current\_user\_groups method ##
|
12
|
+
|
13
|
+
this is similar to the **current_user** method common on authentication. the **current_user_groups** method is an array of object which responds to __:name__. call these objects groups which have name. the name is used in the permission config of the controller.
|
14
|
+
|
15
|
+
having something like PosixAccounts and PosixGroups (as know from ldap) would lead to an implementation like (which is the default when there is no such method)
|
16
|
+
|
17
|
+
def current_user_groups
|
18
|
+
current_user.groups
|
19
|
+
end
|
20
|
+
|
21
|
+
## config for a controller
|
22
|
+
|
23
|
+
this is a yaml file in **RAILS_ROOT/app/guards/my\_users\_guard.yml**. for example
|
24
|
+
|
25
|
+
my_users:
|
26
|
+
index:
|
27
|
+
- root
|
28
|
+
- user-admin
|
29
|
+
- app-admin
|
30
|
+
show: [root,app-admin,guest]
|
31
|
+
new: [root]
|
32
|
+
create: [root]
|
33
|
+
edit: [root,app-admin]
|
34
|
+
update: [root,app-admin]
|
35
|
+
destroy: [root]
|
36
|
+
|
37
|
+
with the special action **defaults** this can be reduced to
|
38
|
+
|
39
|
+
my_users:
|
40
|
+
defaults: [root]
|
41
|
+
index:
|
42
|
+
- root
|
43
|
+
- user-admin
|
44
|
+
- app-admin
|
45
|
+
show: [root,app-admin,guest]
|
46
|
+
edit: [root,app-admin]
|
47
|
+
update: [root,app-admin]
|
48
|
+
|
49
|
+
and since **root** is handle by the guard anyways it can be further reduced to
|
50
|
+
|
51
|
+
my_users:
|
52
|
+
defaults: []
|
53
|
+
index:
|
54
|
+
- user-admin
|
55
|
+
- app-admin
|
56
|
+
show: [app-admin,guest]
|
57
|
+
edit: [app-admin]
|
58
|
+
update: [app-admin]
|
59
|
+
|
60
|
+
## rails helper methods
|
61
|
+
|
62
|
+
### authorize method of controller
|
63
|
+
|
64
|
+
the authorize method asked the Guard if a certain action on a controller is allowed by the current_user, if not the method raises an Error. this method is registered as before-filter on the application-contrller. so **skip-before-filter :authorize** will disable the guard.
|
65
|
+
|
66
|
+
### allowed? method of controller
|
67
|
+
|
68
|
+
the call `allowed?(:destroy)` will give the permissions for the given action on the current controller.
|
69
|
+
|
70
|
+
### allowed? method of views
|
71
|
+
|
72
|
+
it takes two arguments since the controller name (or resource name) is needed as well. the call `allowed?(:users, :destroy)` will give the permissions for the given action controller pair.
|
73
|
+
|
74
|
+
### getting the Guard instance
|
75
|
+
|
76
|
+
to get an instance of the **Guard** on the controller itself just call `guard`. otherwise `Rails.application.config.guard` will give you such an instance.
|
77
|
+
|
78
|
+
# more advanced
|
79
|
+
|
80
|
+
sometimes you want to bind resource to a user/group pair, i.e. given an organizations which have report-writers and report-readers. example as rails before-filter:
|
81
|
+
|
82
|
+
skip_before-filter :authorize
|
83
|
+
guard_filter :authorize_organization_reader, :only => [:show]
|
84
|
+
guard_filter :authorize_organization_writer, :only => [:edit, :update]
|
85
|
+
|
86
|
+
def authorize_organization_writer(groups)
|
87
|
+
groups.select { |g| g.writer?(current_user) }
|
88
|
+
end
|
89
|
+
|
90
|
+
def authorize_organization_reader
|
91
|
+
groups.select { |g| g.writer?(current_user) || org.writer?(current_user)|}
|
92
|
+
end
|
93
|
+
|
94
|
+
of course you can organize such relations also like that
|
95
|
+
|
96
|
+
skip_before_filter :authorize
|
97
|
+
guard_filter :authorize_organization
|
98
|
+
|
99
|
+
def authorize_organization(groups)
|
100
|
+
gou = GroupsOrganizationsUser.where(:org_id => params(:org_id),
|
101
|
+
:user_id => current_user.id)
|
102
|
+
ids = gou.collect { |i| i.group_id }
|
103
|
+
groups.select { |g| ids.include?(g.id) }
|
104
|
+
end
|
data/lib/ixtlan/guard.rb
CHANGED
@@ -1 +1 @@
|
|
1
|
-
require 'ixtlan/guard/
|
1
|
+
require 'ixtlan/guard/guard'
|
@@ -2,7 +2,7 @@ require 'ixtlan/guard/guard_config'
|
|
2
2
|
|
3
3
|
module Ixtlan
|
4
4
|
module Guard
|
5
|
-
class
|
5
|
+
class Guard
|
6
6
|
|
7
7
|
attr_reader :superuser
|
8
8
|
|
@@ -13,6 +13,10 @@ module Ixtlan
|
|
13
13
|
@logger = options[:logger]
|
14
14
|
end
|
15
15
|
|
16
|
+
def superuser_name
|
17
|
+
@superuser[0]
|
18
|
+
end
|
19
|
+
|
16
20
|
def block_groups(groups)
|
17
21
|
@blocked_groups = (groups || []).collect { |g| g.to_s}
|
18
22
|
@blocked_groups.delete(@superuser)
|
@@ -33,8 +37,11 @@ module Ixtlan
|
|
33
37
|
end
|
34
38
|
end
|
35
39
|
|
36
|
-
def allowed_groups(resource_name,
|
37
|
-
|
40
|
+
def allowed_groups(resource_name,
|
41
|
+
action,
|
42
|
+
current_group_names)
|
43
|
+
allowed = @config.allowed_groups(resource_name, action)
|
44
|
+
allowed = allowed - blocked_groups + @superuser
|
38
45
|
if allowed.member?('*')
|
39
46
|
# keep superuser in current_groups if in there
|
40
47
|
current_group_names - (blocked_groups - @superuser)
|
@@ -59,37 +66,35 @@ module Ixtlan
|
|
59
66
|
end
|
60
67
|
private :group_map
|
61
68
|
|
62
|
-
def
|
69
|
+
def check(resource_name, action, current_groups, &block)
|
70
|
+
action = action.to_s
|
63
71
|
group_map = group_map(current_groups)
|
64
72
|
allowed_group_names = allowed_groups(resource_name, action, group_map.keys)
|
65
|
-
|
73
|
+
|
66
74
|
if allowed_group_names.size > 0
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
75
|
+
groups = allowed_group_names.collect { |name| group_map[name] }
|
76
|
+
# call block to filter groups unless we are superuser
|
77
|
+
if block && !allowed_group_names.member?(superuser_name)
|
78
|
+
groups = block.call(groups)
|
71
79
|
end
|
80
|
+
|
81
|
+
logger.debug { "guard #{resource_name}##{action}: #{groups.size > 0}" }
|
82
|
+
|
83
|
+
# nil means 'access denied', i.e. there are no allowed groups
|
84
|
+
groups if groups.size > 0
|
72
85
|
else
|
73
86
|
unless @config.has_guard?(resource_name)
|
74
87
|
raise ::Ixtlan::Guard::GuardException.new("no guard config for '#{resource_name}'")
|
75
88
|
else
|
76
|
-
|
89
|
+
logger.debug { "guard #{resource_name}##{action}: #{allowed_group_names.size > 0}" }
|
90
|
+
# nil means 'access denied', i.e. there are no allowed groups
|
91
|
+
nil
|
77
92
|
end
|
78
93
|
end
|
79
94
|
end
|
80
95
|
|
81
|
-
def
|
82
|
-
|
83
|
-
block.call(group_map[group_name], association)
|
84
|
-
end if association && block
|
85
|
-
logger.debug do
|
86
|
-
if g
|
87
|
-
"found group #{g} for #{association}"
|
88
|
-
else
|
89
|
-
"no group found for #{association}"
|
90
|
-
end
|
91
|
-
end
|
92
|
-
g != nil
|
96
|
+
def allowed?(resource, action, groups, &block)
|
97
|
+
check(resource, action, groups, &block) != nil
|
93
98
|
end
|
94
99
|
|
95
100
|
def permissions(current_groups, &block)
|
@@ -101,20 +106,24 @@ module Ixtlan
|
|
101
106
|
perm = Node.new(:permission)
|
102
107
|
perm[:resource] = resource
|
103
108
|
perm[:actions] = nodes
|
104
|
-
|
105
|
-
|
109
|
+
|
110
|
+
# setup default_groups
|
111
|
+
default_groups = actions.delete('defaults') || []
|
112
|
+
default_groups = group_map.keys & (default_groups + @superuser) unless default_groups.member?('*')
|
113
|
+
|
106
114
|
deny = if actions.size == 0
|
107
115
|
# no actions
|
108
|
-
# deny = false: !
|
109
|
-
# deny = true:
|
110
|
-
|
116
|
+
# deny = false: !default_groups.member?('*')
|
117
|
+
# deny = true: default_groups.member?('*') || current_group_names.member?(@superuser[0])
|
118
|
+
default_groups.member?('*') || group_map.keys.member?(@superuser[0]) || !group_map.keys.detect {|g| default_groups.member? g }.nil?
|
111
119
|
else
|
112
120
|
# actions
|
113
|
-
# deny = false :
|
114
|
-
# deny = true :
|
115
|
-
|
121
|
+
# deny = false : default_groups == []
|
122
|
+
# deny = true : default_groups.member?('*')
|
123
|
+
default_groups.size != 0 || default_groups.member?('*')
|
116
124
|
end
|
117
125
|
perm[:deny] = deny
|
126
|
+
|
118
127
|
actions.each do |action, groups|
|
119
128
|
group_names = groups.collect { |g| g.is_a?(Hash) ? g.keys : g }.flatten if groups
|
120
129
|
node = Node.new(:action)
|
@@ -125,22 +134,29 @@ module Ixtlan
|
|
125
134
|
names = group_map.keys & ((group_names || []) + @superuser)
|
126
135
|
names.collect { |name| group_map[name] }
|
127
136
|
end
|
137
|
+
|
128
138
|
if (deny && allowed_groups.size == 0) || (!deny && allowed_groups.size > 0)
|
129
139
|
node[:name] = action
|
130
140
|
if block
|
131
141
|
if allowed_groups.size > 0
|
132
|
-
|
142
|
+
assos = block.call(resource, allowed_groups)
|
143
|
+
node[:associations] = assos if assos && assos.size > 0
|
133
144
|
else
|
134
|
-
|
145
|
+
assos = block.call(resource, group_map.values)
|
146
|
+
perm[:associations] = assos if assos && assos.size > 0
|
135
147
|
end
|
136
148
|
end
|
137
149
|
nodes << node
|
150
|
+
elsif deny && allowed_groups.size > 0 && block
|
151
|
+
assos = block.call(resource, group_map.values)
|
152
|
+
perm[:associations] = assos if assos && assos.size > 0
|
138
153
|
end
|
139
154
|
end
|
140
155
|
# TODO is that right like this ?
|
141
|
-
# only
|
156
|
+
# only default_groups, i.e. no actions !!!
|
142
157
|
if block && actions.size == 0 && deny
|
143
|
-
|
158
|
+
assos = block.call(resource, group_map.values)
|
159
|
+
perm[:associations] = assos if assos && assos.size > 0
|
144
160
|
end
|
145
161
|
perms << perm
|
146
162
|
end
|
@@ -0,0 +1,195 @@
|
|
1
|
+
require 'ixtlan/guard/guard_config'
|
2
|
+
|
3
|
+
module Ixtlan
|
4
|
+
module Guard
|
5
|
+
class Guard
|
6
|
+
|
7
|
+
attr_reader :superuser
|
8
|
+
|
9
|
+
def initialize(options = {})
|
10
|
+
options[:guards_dir] ||= File.expand_path(".")
|
11
|
+
@superuser = [(options[:superuser] || "root").to_s]
|
12
|
+
@config = Config.new(options)
|
13
|
+
@logger = options[:logger]
|
14
|
+
end
|
15
|
+
|
16
|
+
def superuser_name
|
17
|
+
@superuser[0]
|
18
|
+
end
|
19
|
+
|
20
|
+
def block_groups(groups)
|
21
|
+
@blocked_groups = (groups || []).collect { |g| g.to_s}
|
22
|
+
@blocked_groups.delete(@superuser)
|
23
|
+
@blocked_groups
|
24
|
+
end
|
25
|
+
|
26
|
+
def blocked_groups
|
27
|
+
@blocked_groups ||= []
|
28
|
+
end
|
29
|
+
|
30
|
+
def logger
|
31
|
+
@logger ||=
|
32
|
+
if defined?(Slf4r::LoggerFactory)
|
33
|
+
Slf4r::LoggerFactory.new(Ixtlan::Guard)
|
34
|
+
else
|
35
|
+
require 'logger'
|
36
|
+
Logger.new(STDOUT)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def allowed_groups_and_restricted(resource_name,
|
41
|
+
action,
|
42
|
+
current_group_names)
|
43
|
+
allowed, restricted =
|
44
|
+
@config.allowed_groups_and_restricted(resource_name, action)
|
45
|
+
allowed = allowed - blocked_groups + @superuser
|
46
|
+
result = if allowed.member?('*')
|
47
|
+
# keep superuser in current_groups if in there
|
48
|
+
current_group_names - (blocked_groups - @superuser)
|
49
|
+
else
|
50
|
+
allowed & current_group_names
|
51
|
+
end
|
52
|
+
[result, restricted]
|
53
|
+
end
|
54
|
+
|
55
|
+
def group_map(current_groups)
|
56
|
+
names = current_groups.collect do |g|
|
57
|
+
key = case g
|
58
|
+
when String
|
59
|
+
g
|
60
|
+
when Symbol
|
61
|
+
g.to_s
|
62
|
+
else
|
63
|
+
g.name.to_s
|
64
|
+
end
|
65
|
+
[key, g]
|
66
|
+
end
|
67
|
+
Hash[*(names.flatten)]
|
68
|
+
end
|
69
|
+
private :group_map
|
70
|
+
|
71
|
+
def check(resource_name, action, current_groups, &block)
|
72
|
+
action = action.to_s
|
73
|
+
group_map = group_map(current_groups)
|
74
|
+
allowed_group_names, restricted =
|
75
|
+
allowed_groups_and_restricted(resource_name, action, group_map.keys)
|
76
|
+
|
77
|
+
logger.debug { "guard #{resource_name}##{action}: #{allowed_group_names.size > 0}" }
|
78
|
+
|
79
|
+
if allowed_group_names.size > 0
|
80
|
+
groups = allowed_group_names.collect { |name| group_map[name] }
|
81
|
+
# call block to filter groups if restricted applies
|
82
|
+
if restricted && !allowed_group_names.member?(superuser_name)
|
83
|
+
raise "no block given to filter groups" unless block
|
84
|
+
except = restricted['except'] || []
|
85
|
+
only = restricted['only'] || [action]
|
86
|
+
if !except.member?(action) && only.member?(action)
|
87
|
+
groups = block.call(groups)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
# nil means 'access denied', i.e. there are no allowed groups
|
92
|
+
groups if groups.size > 0
|
93
|
+
else
|
94
|
+
unless @config.has_guard?(resource_name)
|
95
|
+
raise ::Ixtlan::Guard::GuardException.new("no guard config for '#{resource_name}'")
|
96
|
+
else
|
97
|
+
# nil means 'access denied', i.e. there are no allowed groups
|
98
|
+
nil
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def allowed?(resource, action, groups, &block)
|
104
|
+
check(resource, action, groups, &block) != nil
|
105
|
+
end
|
106
|
+
|
107
|
+
def permissions(current_groups, &block)
|
108
|
+
group_map = group_map(current_groups)
|
109
|
+
perms = []
|
110
|
+
m = @config.map_of_all
|
111
|
+
m.each do |resource, actions|
|
112
|
+
nodes = []
|
113
|
+
perm = Node.new(:permission)
|
114
|
+
perm[:resource] = resource
|
115
|
+
perm[:actions] = nodes
|
116
|
+
|
117
|
+
restricted = actions.delete('restricted')
|
118
|
+
|
119
|
+
# setup default_groups
|
120
|
+
default_groups = actions.delete('defaults') || []
|
121
|
+
default_groups = group_map.keys & (default_groups + @superuser) unless default_groups.member?('*')
|
122
|
+
|
123
|
+
deny = if actions.size == 0
|
124
|
+
# no actions
|
125
|
+
# deny = false: !default_groups.member?('*')
|
126
|
+
# deny = true: default_groups.member?('*') || current_group_names.member?(@superuser[0])
|
127
|
+
default_groups.member?('*') || group_map.keys.member?(@superuser[0]) || !group_map.keys.detect {|g| default_groups.member? g }.nil?
|
128
|
+
else
|
129
|
+
# actions
|
130
|
+
# deny = false : default_groups == []
|
131
|
+
# deny = true : default_groups.member?('*')
|
132
|
+
default_groups.size != 0 || default_groups.member?('*')
|
133
|
+
end
|
134
|
+
perm[:deny] = deny
|
135
|
+
|
136
|
+
actions.each do |action, groups|
|
137
|
+
group_names = groups.collect { |g| g.is_a?(Hash) ? g.keys : g }.flatten if groups
|
138
|
+
node = Node.new(:action)
|
139
|
+
allowed_groups =
|
140
|
+
if groups && group_names.member?('*')
|
141
|
+
group_map.values
|
142
|
+
else
|
143
|
+
names = group_map.keys & ((group_names || []) + @superuser)
|
144
|
+
names.collect { |name| group_map[name] }
|
145
|
+
end
|
146
|
+
|
147
|
+
if (deny && allowed_groups.size == 0) || (!deny && allowed_groups.size > 0)
|
148
|
+
node[:name] = action
|
149
|
+
if block
|
150
|
+
if allowed_groups.size > 0
|
151
|
+
assos = block.call(resource, allowed_groups)
|
152
|
+
node[:associations] = assos if assos && assos.size > 0
|
153
|
+
else
|
154
|
+
assos = block.call(resource, group_map.values)
|
155
|
+
perm[:associations] = assos if assos && assos.size > 0
|
156
|
+
end
|
157
|
+
end
|
158
|
+
nodes << node
|
159
|
+
elsif deny && allowed_groups.size > 0 #block
|
160
|
+
assos = block.call(resource, group_map.values)
|
161
|
+
perm[:associations] = assos if assos && assos.size > 0
|
162
|
+
end
|
163
|
+
end
|
164
|
+
# TODO is that right like this ?
|
165
|
+
# only default_groups, i.e. no actions !!!
|
166
|
+
if block && actions.size == 0 && deny
|
167
|
+
assos = block.call(resource, group_map.values)
|
168
|
+
perm[:associations] = assos if assos && assos.size > 0
|
169
|
+
end
|
170
|
+
perms << perm
|
171
|
+
end
|
172
|
+
perms
|
173
|
+
end
|
174
|
+
end
|
175
|
+
class Node < Hash
|
176
|
+
|
177
|
+
attr_reader :content
|
178
|
+
|
179
|
+
def initialize(name)
|
180
|
+
map = super
|
181
|
+
@content = {}
|
182
|
+
merge!({ name => @content })
|
183
|
+
end
|
184
|
+
|
185
|
+
def []=(k,v)
|
186
|
+
@content[k] = v
|
187
|
+
end
|
188
|
+
def [](k)
|
189
|
+
@content[k]
|
190
|
+
end
|
191
|
+
end
|
192
|
+
class GuardException < Exception; end
|
193
|
+
class PermissionDenied < GuardException; end
|
194
|
+
end
|
195
|
+
end
|