be9-acl9 0.9.1 → 0.9.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,9 @@
1
+ h2. 0.9.2 (11-Jan-2009)
2
+
3
+ * @access_control :method do end@ as shorter form for @access_control :as_method => :method do end@.
4
+ * Boolean method can now receive an objects hash which will be looked into first before taking
5
+ an instance variable.
6
+
7
+ h2. 0.9.1 (03-Jan-2009)
8
+
9
+ Initial release.
data/Manifest CHANGED
@@ -1,19 +1,22 @@
1
- lib/acl9/config.rb
1
+ init.rb
2
+ Manifest
3
+ lib/acl9/model_extensions.rb
4
+ lib/acl9/version.rb
2
5
  lib/acl9/model_extensions/subject.rb
3
6
  lib/acl9/model_extensions/object.rb
7
+ lib/acl9/controller_extensions/generators.rb
8
+ lib/acl9/controller_extensions/dsl_base.rb
9
+ lib/acl9/config.rb
4
10
  lib/acl9/controller_extensions.rb
5
- lib/acl9/controller_extensions/filter_producer.rb
6
- lib/acl9/version.rb
7
- lib/acl9/model_extensions.rb
8
11
  lib/acl9.rb
12
+ README.textile
13
+ acl9.gemspec
14
+ CHANGELOG.textile
15
+ MIT-LICENSE
16
+ Rakefile
9
17
  spec/db/schema.rb
10
- spec/filter_producer_spec.rb
18
+ spec/dsl_base_spec.rb
11
19
  spec/spec_helper.rb
12
- spec/models.rb
13
20
  spec/access_control_spec.rb
14
21
  spec/roles_spec.rb
15
- Manifest
16
- MIT-LICENSE
17
- Rakefile
18
- README.textile
19
- init.rb
22
+ spec/models.rb
@@ -45,7 +45,7 @@ Acl9 can be installed as a gem from "GitHub":http://github.com.
45
45
  Add the following line to your @config/environment.rb@:
46
46
 
47
47
  <pre><code>
48
- config.gem "be9-acl9", :source => "http://gems.github.com"
48
+ config.gem "be9-acl9", :source => "http://gems.github.com", :lib => "acl9"
49
49
  </pre></code>
50
50
 
51
51
  Then run @rake gems:install@ (with possible @rake gems:unpack@ thereafter) and you're done!
@@ -118,7 +118,7 @@ Role control subsystem has been lifted from
118
118
  but undergone some modifications.
119
119
 
120
120
  It's based on two tables in the database. First, role table, which stores pairs @[role_name, object]@
121
- where object is a polymorphic link or a class. Second, join table, which joins users and roles.
121
+ where object is a polymorphic model instance or a class. Second, join table, which joins users and roles.
122
122
 
123
123
  To use this subsystem, you should define a @Role@ model.
124
124
 
@@ -142,7 +142,7 @@ The structure of @roles@ table is as follows:
142
142
  end
143
143
  </code></pre>
144
144
 
145
- Note that you will never use the @Role@ class directly.
145
+ Note that you will almost never use the @Role@ class directly.
146
146
 
147
147
  h2. Subject model
148
148
 
@@ -333,7 +333,7 @@ In other words, you are _manager_ if you manage at least one @foo@ (or a @bar@..
333
333
  user.has_role? :manager # => true
334
334
  </code></pre>
335
335
 
336
- Our @user@ is no more manager of @foo@, but is now a manager of @bar@.
336
+ Our @user@ is no more manager of @foo@, but has become a manager of @bar@.
337
337
 
338
338
  <pre><code>
339
339
  user.has_no_roles!
@@ -343,7 +343,7 @@ Our @user@ is no more manager of @foo@, but is now a manager of @bar@.
343
343
  user.roles # => []
344
344
  </code></pre>
345
345
 
346
- Now @user@ has no roles in the system.
346
+ At this time @user@ has no roles in the system.
347
347
 
348
348
  h2. Coming up with your own role implementation
349
349
 
@@ -381,7 +381,7 @@ from unauthorized access. Acl9 provides a nice DSL for writing access rules.
381
381
 
382
382
  h2. Allow and deny
383
383
 
384
- Access control is mostly about allowing and denying. So there two
384
+ Access control is mostly about allowing and denying. So there are two
385
385
  basic methods: @allow@ and @deny@. They have the same syntax:
386
386
 
387
387
  <pre><code>
@@ -397,8 +397,8 @@ ROLE_LIST is a list of roles (at least 1 role should be there). So,
397
397
  allow :manager, :admin
398
398
  deny :banned
399
399
  </code></pre>
400
- will match holders of global role _manager_ *and* holders of global role _admin_ as allowed and
401
- _banned_ as denied.
400
+ will match holders of global role _manager_ *and* holders of global role _admin_ as allowed.
401
+ On the contrary, holders of _banned_ role will match as denied.
402
402
 
403
403
  Basically this snippet is equivalent to
404
404
 
@@ -407,13 +407,13 @@ Basically this snippet is equivalent to
407
407
  allow :admin
408
408
  deny :banned
409
409
  </code></pre>
410
- which means that roles in argument list is OR'ed for a match, and not AND'ed.
410
+ which means that roles in argument list are OR'ed for a match, and not AND'ed.
411
411
 
412
412
  Also note that:
413
413
  * You may use both strings and :symbols to specify roles (the latter get converted into strings).
414
414
  * Role names are singularized before check.
415
415
 
416
- The mentioned snippet can thus be written as
416
+ Thus the snippet above can also be written as
417
417
 
418
418
  <pre><code>
419
419
  allow :managers, :admins
@@ -440,7 +440,7 @@ use object and class roles in the ACL block.
440
440
  deny :hated, :by => :us
441
441
  </code></pre>
442
442
 
443
- So, to specify an object you use one of the 6 preposition options:
443
+ To specify an object you use one of the 6 preposition options:
444
444
  * :of
445
445
  * :at
446
446
  * :on
@@ -482,8 +482,6 @@ Checking against an instance variable has sense when you have another _before fi
482
482
  end
483
483
  </code></pre>
484
484
 
485
-
486
-
487
485
  Note that the object option is applied to all of the roles you specify in the argument list.
488
486
  As such,
489
487
 
@@ -589,7 +587,7 @@ Same result as a table:
589
587
  | None of the @allow@ rules matched, some of the @deny@ rules matched. | Access is denied. | Access is denied. |
590
588
  | Some of the @allow@ rules matched, some of the @deny@ rules matched. | Access is allowed. | Access is denied. |
591
589
 
592
- Apparently _default deny_ mode is stricter, that's because it's on by default.
590
+ Apparently _default deny_ mode is more strict, and that's because it's on by default.
593
591
 
594
592
  h2. Actions block
595
593
 
@@ -644,11 +642,12 @@ By calling @access_control@ in your controller you can get your ACL block transl
644
642
  First case is by default. You can catch the exception with @rescue_from@ call and do something
645
643
  you like: make a redirect, or render "Access denied" template, or whatever.
646
644
 
647
- Second case is obtained with @:as_method@ option (see below) and may be helpful if you want
648
- to use @skip_before_filter@ somewhere on the derived controller.
645
+ Second case is obtained with specifying method name as an argument to
646
+ @access_control@ (or using @:as_method@ option, see below) and may be helpful
647
+ if you want to use @skip_before_filter@ somewhere in the derived controller.
649
648
 
650
- Third case will take place if you use @:as_method@ with @:filter => false@. You'll get an ordinary
651
- method which you can call anywhere you want.
649
+ Third case will take place if you supply @:filter => false@ along with method
650
+ name. You'll get an ordinary method which you can call anywhere you want.
652
651
 
653
652
  h3. :subject_method
654
653
 
@@ -694,14 +693,30 @@ In the case
694
693
 
695
694
  access control checks will be added as @acl@ method onto MyController, with @before_filter :acl@ call thereafter.
696
695
 
696
+ Instead of using @:as_method@ you may specify the name of the method as a positional argument
697
+ to @access_control@:
698
+
699
+ <pre><code>
700
+ class MyController < ApplicationController
701
+ access_control :acl do
702
+ # ...
703
+ end
704
+
705
+ # ...
706
+ end
707
+ </code></pre>
708
+
709
+
697
710
  h3. :filter
698
711
 
699
- If you set @:filter@ to @false@ (it's @true@ by default) and also use @:as_method@, you'll get a method
700
- which won't raise @Acl9::AccessDenied@ exception, but rather return @true@ or @false@ (access allowed/denied).
712
+ If you set @:filter@ to @false@ (it's @true@ by default) and also use
713
+ @:as_method@ (or method name as 1st argument to @access_control@, you'll get a
714
+ method which won't raise @Acl9::AccessDenied@ exception, but rather return
715
+ @true@ or @false@ (access allowed/denied).
701
716
 
702
717
  <pre><code>
703
718
  class SecretController < ApplicationController
704
- access_control :as_method => :secret_access?, :filter => false do
719
+ access_control :secret_access?, :filter => false do
705
720
  allow :james_bond
706
721
  # ...
707
722
  end
@@ -728,6 +743,26 @@ which won't raise @Acl9::AccessDenied@ exception, but rather return @true@ or @f
728
743
  end
729
744
  </code></pre>
730
745
 
746
+ The generated method can receive an objects hash as an argument. In this example,
747
+
748
+ <pre><code>
749
+ class LolController < ApplicationController
750
+ access_control :lolcats?, :filter => false do
751
+ allow :cats, :by => :lol
752
+ # ...
753
+ end
754
+ end
755
+ </code></pre>
756
+
757
+ you may not only call @lolcats?@ with no arguments, which will basically return
758
+
759
+ <pre><code>
760
+ current_user.has_role?('cats', @lol)
761
+ </code></pre>
762
+
763
+ but also as @lolcats?(:lol => Lol.find(params[:lol]))@. The hash will be looked into first,
764
+ even if you have an instance variable @lol@.
765
+
731
766
  h3. Other options
732
767
 
733
768
  Other options will be passed to @before_filter@. As such, you may use @:only@ and @:except@ to narrow
@@ -762,4 +797,6 @@ is basically equivalent to
762
797
  </code></pre>
763
798
 
764
799
 
765
- Copyright (c) 2009 Oleg Dashevskii, released under the MIT license
800
+ Copyright (c) 2009 Oleg Dashevskii, released under the MIT license.
801
+
802
+ Contact me at "#{%w(Oleg Dashevskii).join('').downcase}@gmail.com"
@@ -2,15 +2,15 @@
2
2
 
3
3
  Gem::Specification.new do |s|
4
4
  s.name = %q{acl9}
5
- s.version = "0.9.1"
5
+ s.version = "0.9.2"
6
6
 
7
7
  s.required_rubygems_version = Gem::Requirement.new(">= 1.2") if s.respond_to? :required_rubygems_version=
8
8
  s.authors = ["Oleg Dashevskii"]
9
- s.date = %q{2009-01-03}
9
+ s.date = %q{2009-01-14}
10
10
  s.description = %q{Yet another role-based authorization system for Rails with a nice DSL for access control lists.}
11
11
  s.email = %q{olegdashevskii@gmail.com}
12
- s.extra_rdoc_files = ["lib/acl9/config.rb", "lib/acl9/model_extensions/subject.rb", "lib/acl9/model_extensions/object.rb", "lib/acl9/controller_extensions.rb", "lib/acl9/controller_extensions/filter_producer.rb", "lib/acl9/version.rb", "lib/acl9/model_extensions.rb", "lib/acl9.rb", "README.textile"]
13
- s.files = ["lib/acl9/config.rb", "lib/acl9/model_extensions/subject.rb", "lib/acl9/model_extensions/object.rb", "lib/acl9/controller_extensions.rb", "lib/acl9/controller_extensions/filter_producer.rb", "lib/acl9/version.rb", "lib/acl9/model_extensions.rb", "lib/acl9.rb", "spec/db/schema.rb", "spec/filter_producer_spec.rb", "spec/spec_helper.rb", "spec/models.rb", "spec/access_control_spec.rb", "spec/roles_spec.rb", "Manifest", "MIT-LICENSE", "Rakefile", "README.textile", "init.rb", "acl9.gemspec"]
12
+ s.extra_rdoc_files = ["lib/acl9/model_extensions.rb", "lib/acl9/version.rb", "lib/acl9/model_extensions/subject.rb", "lib/acl9/model_extensions/object.rb", "lib/acl9/controller_extensions/generators.rb", "lib/acl9/controller_extensions/dsl_base.rb", "lib/acl9/config.rb", "lib/acl9/controller_extensions.rb", "lib/acl9.rb", "README.textile", "CHANGELOG.textile"]
13
+ s.files = ["init.rb", "Manifest", "lib/acl9/model_extensions.rb", "lib/acl9/version.rb", "lib/acl9/model_extensions/subject.rb", "lib/acl9/model_extensions/object.rb", "lib/acl9/controller_extensions/generators.rb", "lib/acl9/controller_extensions/dsl_base.rb", "lib/acl9/config.rb", "lib/acl9/controller_extensions.rb", "lib/acl9.rb", "README.textile", "acl9.gemspec", "CHANGELOG.textile", "MIT-LICENSE", "Rakefile", "spec/db/schema.rb", "spec/dsl_base_spec.rb", "spec/spec_helper.rb", "spec/access_control_spec.rb", "spec/roles_spec.rb", "spec/models.rb"]
14
14
  s.has_rdoc = true
15
15
  s.homepage = %q{http://github.com/be9/acl9}
16
16
  s.rdoc_options = ["--line-numbers", "--inline-source", "--title", "Acl9", "--main", "README.textile"]
@@ -1,4 +1,4 @@
1
- require File.join(File.dirname(__FILE__), 'controller_extensions', 'filter_producer')
1
+ require File.join(File.dirname(__FILE__), 'controller_extensions', 'generators')
2
2
 
3
3
  module Acl9
4
4
  module ControllerExtensions
@@ -7,30 +7,54 @@ module Acl9
7
7
  end
8
8
 
9
9
  module ClassMethods
10
- def access_control(opts = {}, &block)
10
+ def access_control(*args, &block)
11
+ opts = if args.last.is_a? Hash
12
+ args.pop
13
+ else
14
+ {}
15
+ end
16
+
17
+ case args.size
18
+ when 0 then true
19
+ when 1
20
+ meth = args.first
21
+
22
+ if meth.is_a? Symbol
23
+ opts[:as_method] = meth
24
+ else
25
+ raise ArgumentError, "access control argument must be a :symbol!"
26
+ end
27
+ else
28
+ raise ArgumentError, "Invalid arguments for access_control"
29
+ end
30
+
11
31
  subject_method = opts.delete(:subject_method) || Acl9::config[:default_subject_method]
12
32
 
13
33
  raise ArgumentError, "Block must be supplied to access_control" unless block
14
34
 
15
- producer = Acl9::FilterProducer.new(subject_method)
16
- producer.acl(&block)
17
-
18
35
  filter = opts.delete(:filter)
19
36
  filter = true if filter.nil?
20
37
 
38
+ method = opts.delete(:as_method)
39
+
40
+ generator = case
41
+ when method && filter
42
+ Acl9::Dsl::Generators::FilterMethod.new(subject_method, method)
43
+ when method && !filter
44
+ Acl9::Dsl::Generators::BooleanMethod.new(subject_method, method)
45
+ else
46
+ Acl9::Dsl::Generators::FilterLambda.new(subject_method)
47
+ end
48
+
49
+ generator.acl_block!(&block)
50
+
21
51
  if opts.delete(:debug)
22
52
  Rails::logger.debug "=== Acl9 access_control expression dump (#{self.to_s})"
23
- Rails::logger.debug producer.to_s
53
+ Rails::logger.debug generator.to_s
24
54
  Rails::logger.debug "======"
25
55
  end
26
56
 
27
- if method = opts.delete(:as_method)
28
- class_eval producer.to_method_code(method, filter)
29
-
30
- before_filter(method, opts) if filter
31
- else
32
- before_filter(opts, &producer.to_proc)
33
- end
57
+ generator.install_on(self, opts)
34
58
  end
35
59
  end
36
60
  end
@@ -0,0 +1,212 @@
1
+ module Acl9
2
+ module Dsl
3
+ class Base
4
+ attr_reader :allows, :denys
5
+
6
+ def initialize(*args)
7
+ @default_action = nil
8
+
9
+ @allows = []
10
+ @denys = []
11
+
12
+ @original_args = args
13
+ end
14
+
15
+ def acl_block!(&acl_block)
16
+ self.instance_eval(&acl_block)
17
+ end
18
+
19
+ def default_action
20
+ @default_action.nil? ? :deny : @default_action
21
+ end
22
+
23
+ def allowance_expression
24
+ allowed_expr = if @allows.size > 0
25
+ @allows.map { |clause| "(#{clause})" }.join(' || ')
26
+ else
27
+ "false"
28
+ end
29
+
30
+ not_denied_expr = if @denys.size > 0
31
+ @denys.map { |clause| "!(#{clause})" }.join(' && ')
32
+ else
33
+ "true"
34
+ end
35
+
36
+ [allowed_expr, not_denied_expr].
37
+ map { |expr| "(#{expr})" }.
38
+ join(default_action == :deny ? ' && ' : ' || ')
39
+ end
40
+
41
+ alias to_s allowance_expression
42
+
43
+ protected
44
+
45
+ def default(default_action)
46
+ raise ArgumentError, "default can only be called once in access_control block" if @default_action
47
+
48
+ unless [:allow, :deny].include? default_action
49
+ raise ArgumentError, "invalid value for default (can be :allow or :deny)"
50
+ end
51
+
52
+ @default_action = default_action
53
+ end
54
+
55
+ def allow(*args)
56
+ @current_rule = :allow
57
+ _parse_and_add_rule(*args)
58
+ end
59
+
60
+ def deny(*args)
61
+ @current_rule = :deny
62
+ _parse_and_add_rule(*args)
63
+ end
64
+
65
+ def actions(*args, &block)
66
+ raise ArgumentError, "actions should receive at least 1 action as argument" if args.size < 1
67
+
68
+ subsidiary = self.class.new(*@original_args)
69
+
70
+ class <<subsidiary
71
+ def actions(*args)
72
+ raise ArgumentError, "You cannot use actions inside another actions block"
73
+ end
74
+
75
+ def default(*args)
76
+ raise ArgumentError, "You cannot use default inside an actions block"
77
+ end
78
+
79
+ def _set_action_clause(to, except)
80
+ raise ArgumentError, "You cannot use :to/:except inside actions block" if to || except
81
+ end
82
+ end
83
+
84
+ subsidiary.acl_block!(&block)
85
+
86
+ action_check = _action_check_expression(args)
87
+
88
+ squash = lambda do |rules|
89
+ _either_of(rules) + ' && ' + action_check
90
+ end
91
+
92
+ @allows << squash.call(subsidiary.allows) if subsidiary.allows.size > 0
93
+ @denys << squash.call(subsidiary.denys) if subsidiary.denys.size > 0
94
+ end
95
+
96
+ alias action actions
97
+
98
+ def anonymous; nil end
99
+ def all; true end
100
+ def logged_in; false end
101
+
102
+ def _parse_and_add_rule(*args)
103
+ options = if args.last.is_a? Hash
104
+ args.pop
105
+ else
106
+ {}
107
+ end
108
+
109
+ _set_action_clause(options.delete(:to), options.delete(:except))
110
+
111
+ object = _role_object(options)
112
+
113
+ role_checks = args.map do |who|
114
+ case who
115
+ when nil then "#{_subject_ref}.nil?" # anonymous
116
+ when false then "!#{_subject_ref}.nil?" # logged_in
117
+ when true then "true" # all
118
+ else
119
+ "!#{_subject_ref}.nil? && #{_subject_ref}.has_role?('#{who.to_s.singularize}', #{object})"
120
+ end
121
+ end
122
+
123
+ _add_rule case role_checks.size
124
+ when 0
125
+ raise ArgumentError, "allow/deny should have at least 1 argument"
126
+ when 1 then role_checks.first
127
+ else
128
+ _either_of(role_checks)
129
+ end
130
+ end
131
+
132
+ def _either_of(exprs)
133
+ exprs.map { |expr| "(#{expr})" }.join(' || ')
134
+ end
135
+
136
+ def _add_rule(what)
137
+ what = "(#{what}) && #{@action_clause}" if @action_clause
138
+
139
+ (@current_rule == :allow ? @allows : @denys) << what
140
+ end
141
+
142
+ def _set_action_clause(to, except)
143
+ raise ArgumentError, "both :to and :except cannot be specified in the rule" if to && except
144
+
145
+ @action_clause = nil
146
+
147
+ action_list = to || except
148
+ return unless action_list
149
+
150
+ expr = _action_check_expression(action_list)
151
+
152
+ @action_clause = if to
153
+ "#{expr}"
154
+ else
155
+ "!#{expr}"
156
+ end
157
+ end
158
+
159
+ def _action_check_expression(action_list)
160
+ unless action_list.is_a?(Array)
161
+ action_list = [ action_list.to_s ]
162
+ end
163
+
164
+ case action_list.size
165
+ when 0 then "true"
166
+ when 1 then "(#{_action_ref} == '#{action_list.first}')"
167
+ else
168
+ set_of_actions = "Set.new([" + action_list.map { |act| "'#{act}'"}.join(',') + "])"
169
+
170
+ "#{set_of_actions}.include?(#{_action_ref})"
171
+ end
172
+ end
173
+
174
+ VALID_PREPOSITIONS = %w(of for in on at by).freeze unless defined? VALID_PREPOSITIONS
175
+
176
+ def _role_object(options)
177
+ object = nil
178
+
179
+ VALID_PREPOSITIONS.each do |prep|
180
+ if options[prep.to_sym]
181
+ raise ArgumentError, "You may only use one preposition to specify object" if object
182
+
183
+ object = options[prep.to_sym]
184
+ end
185
+ end
186
+
187
+ case object
188
+ when Class
189
+ object.to_s
190
+ when Symbol
191
+ _object_ref object
192
+ when nil
193
+ "nil"
194
+ else
195
+ raise ArgumentError, "object specified by preposition can only be a Class or a Symbol"
196
+ end
197
+ end
198
+
199
+ def _subject_ref
200
+ raise
201
+ end
202
+
203
+ def _object_ref(object)
204
+ raise
205
+ end
206
+
207
+ def _action_ref
208
+ raise
209
+ end
210
+ end
211
+ end
212
+ end