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 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