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 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/guard_ng'
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 GuardNG
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, action, current_group_names)
37
- allowed = @config.allowed_groups(resource_name, action) - blocked_groups + @superuser
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 allowed?(resource_name, action, current_groups, association = nil, &block)
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
- logger.debug { "guard #{resource_name}##{action}: #{allowed_group_names.size > 0}" }
73
+
66
74
  if allowed_group_names.size > 0
67
- if block || association
68
- group_allowed?(group_map, allowed_group_names, association, &block)
69
- else
70
- true
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
- false
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 group_allowed?(group_map, allowed_group_names, association, &block)
82
- g = allowed_group_names.detect do |group_name|
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
- default_actions = actions.delete('defaults') || []
105
- default_actions = group_map.keys & (default_actions + @superuser) unless default_actions.member?('*')
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: !default_actions.member?('*')
109
- # deny = true: default_actions.member?('*') || current_group_names.member?(@superuser[0])
110
- default_actions.member?('*') || group_map.keys.member?(@superuser[0]) || !group_map.keys.detect {|g| default_actions.member? g }.nil?
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 : default_actions == []
114
- # deny = true : default_actions.member?('*')
115
- default_actions.size != 0 || default_actions.member?('*')
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
- node.content.merge!(block.call(resource, action, allowed_groups) || {})
142
+ assos = block.call(resource, allowed_groups)
143
+ node[:associations] = assos if assos && assos.size > 0
133
144
  else
134
- perm.content.merge!(block.call(resource, action, group_map.values) || {})
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 default_actions, i.e. no actions !!!
156
+ # only default_groups, i.e. no actions !!!
142
157
  if block && actions.size == 0 && deny
143
- perm.content.merge!(block.call(resource, nil, group_map.values) || {})
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