ixtlan-guard 0.7.2 → 0.8.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|