acl9 0.11.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/Rakefile ADDED
@@ -0,0 +1,40 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+ require 'rake/testtask'
4
+
5
+ desc 'Default: run tests.'
6
+ task :default => :test
7
+
8
+ begin
9
+ require 'jeweler'
10
+ Jeweler::Tasks.new do |s|
11
+ s.name = "acl9"
12
+ s.summary = "Yet another role-based authorization system for Rails"
13
+ s.email = "olegdashevskii@gmail.com"
14
+ s.homepage = "http://github.com/be9/acl9"
15
+ s.description = "Role-based authorization system for Rails with a nice DSL for access control lists"
16
+ s.authors = ["oleg dashevskii"]
17
+ s.files = FileList["[A-Z]*", "{lib,test}/**/*.rb"]
18
+ s.add_development_dependency "jeremymcanally-context", ">= 0.5.5"
19
+ s.add_development_dependency "jnunemaker-matchy", ">= 0.4.0"
20
+ end
21
+ Jeweler::GemcutterTasks.new
22
+ rescue LoadError
23
+ puts "Jeweler not available. Install it with: sudo gem install jeweler"
24
+ end
25
+
26
+ Rake::TestTask.new(:test) do |test|
27
+ test.libs << 'lib' << 'test'
28
+ test.pattern = 'test/**/*_test.rb'
29
+ test.verbose = false
30
+ end
31
+
32
+ begin
33
+ require 'yard'
34
+
35
+ YARD::Rake::YardocTask.new do |t|
36
+ t.files = ['lib/**/*.rb']
37
+ #t.options = ['--any', '--extra', '--opts'] # optional
38
+ end
39
+ rescue LoadError
40
+ end
data/TODO ADDED
@@ -0,0 +1,42 @@
1
+ * Complex roles with ANDing.
2
+
3
+ Something like:
4
+
5
+ access_control do
6
+ allow all, :except => :destroy
7
+ allow complex_role, :to => :destroy do
8
+ is :strong
9
+ is :decisive
10
+ is :owner, :of => :object
11
+ is_not :banned
12
+ is_not :fake
13
+ end
14
+ end
15
+
16
+ * Acl9-based menu generator.
17
+
18
+ If you get Access Denied on /secrets/index, probably you shouldn't see "Secrets" item
19
+ in the menu at all.
20
+
21
+ It can be very DRY. Say, we introduce :menu => true option to access_control method which
22
+ will make it register a lambda (can see/cannot see) in some global hash (indexed by controller name).
23
+
24
+ Then, given an URL, you'll be able to check it against this hash. /secrets/index is mapped to
25
+ SecretsController#index, so you run access_control_hash['SecretsController'].call('index') and
26
+ show the link only if true is returned.
27
+
28
+ The problem here is with objects. SecretsController's access_control block can reference instance
29
+ variables during the permission check, but we have only current instantiated controller which can be any.
30
+
31
+ Another option is to distinguish visible part from access control part.
32
+
33
+ menu do
34
+ item 'Home', home_path
35
+ item 'Secrets', secrets_path do
36
+ allow :trusted
37
+ end
38
+
39
+ # ...
40
+ end
41
+
42
+ Here only "trusted" users will see "Secrets" item.
data/VERSION.yml ADDED
@@ -0,0 +1,4 @@
1
+ ---
2
+ :minor: 11
3
+ :patch: 0
4
+ :major: 0
data/lib/acl9.rb ADDED
@@ -0,0 +1,16 @@
1
+ require File.join(File.dirname(__FILE__), 'acl9', 'config')
2
+
3
+ if defined? ActiveRecord::Base
4
+ require File.join(File.dirname(__FILE__), 'acl9', 'model_extensions')
5
+
6
+ ActiveRecord::Base.send(:include, Acl9::ModelExtensions)
7
+ end
8
+
9
+
10
+ if defined? ActionController::Base
11
+ require File.join(File.dirname(__FILE__), 'acl9', 'controller_extensions')
12
+ require File.join(File.dirname(__FILE__), 'acl9', 'helpers')
13
+
14
+ ActionController::Base.send(:include, Acl9::ControllerExtensions)
15
+ Acl9Helpers = Acl9::Helpers unless defined?(Acl9Helpers)
16
+ end
@@ -0,0 +1,10 @@
1
+ module Acl9
2
+ @@config = {
3
+ :default_role_class_name => 'Role',
4
+ :default_subject_class_name => 'User',
5
+ :default_subject_method => :current_user,
6
+ :protect_global_roles => false,
7
+ }
8
+
9
+ mattr_reader :config
10
+ end
@@ -0,0 +1,85 @@
1
+ require File.join(File.dirname(__FILE__), 'controller_extensions', 'generators')
2
+
3
+ module Acl9
4
+ module ControllerExtensions
5
+ def self.included(base)
6
+ base.extend(ClassMethods)
7
+ end
8
+
9
+ module ClassMethods
10
+ def access_control(*args, &block)
11
+ opts = args.extract_options!
12
+
13
+ case args.size
14
+ when 0 then true
15
+ when 1
16
+ meth = args.first
17
+
18
+ if meth.is_a? Symbol
19
+ opts[:as_method] = meth
20
+ else
21
+ raise ArgumentError, "access_control argument must be a :symbol!"
22
+ end
23
+ else
24
+ raise ArgumentError, "Invalid arguments for access_control"
25
+ end
26
+
27
+ subject_method = opts[:subject_method] || Acl9::config[:default_subject_method]
28
+
29
+ raise ArgumentError, "Block must be supplied to access_control" unless block
30
+
31
+ filter = opts[:filter]
32
+ filter = true if filter.nil?
33
+
34
+ case helper = opts[:helper]
35
+ when true
36
+ raise ArgumentError, "you should specify :helper => :method_name" if !opts[:as_method]
37
+ when nil then nil
38
+ else
39
+ if opts[:as_method]
40
+ raise ArgumentError, "you can't specify both method name and helper name"
41
+ else
42
+ opts[:as_method] = helper
43
+ filter = false
44
+ end
45
+ end
46
+
47
+ method = opts[:as_method]
48
+
49
+ query_method_available = true
50
+ generator = case
51
+ when method && filter
52
+ Acl9::Dsl::Generators::FilterMethod.new(subject_method, method)
53
+ when method && !filter
54
+ query_method_available = false
55
+ Acl9::Dsl::Generators::BooleanMethod.new(subject_method, method)
56
+ else
57
+ Acl9::Dsl::Generators::FilterLambda.new(subject_method)
58
+ end
59
+
60
+ generator.acl_block!(&block)
61
+
62
+ generator.install_on(self, opts)
63
+
64
+ if query_method_available && (query_method = opts.delete(:query_method))
65
+ case query_method
66
+ when true
67
+ if method
68
+ query_method = "#{method}?"
69
+ else
70
+ raise ArgumentError, "You must specify :query_method as Symbol"
71
+ end
72
+ when Symbol, String
73
+ # okay here
74
+ else
75
+ raise ArgumentError, "Invalid value for :query_method"
76
+ end
77
+
78
+ second_generator = Acl9::Dsl::Generators::BooleanMethod.new(subject_method, query_method)
79
+ second_generator.acl_block!(&block)
80
+ second_generator.install_on(self, opts)
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,229 @@
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
+ instance_eval(&acl_block)
17
+ end
18
+
19
+ def default_action
20
+ if @default_action.nil? then :deny else @default_action end
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 logged_in; false end
99
+ def anonymous; nil end
100
+ def all; true end
101
+
102
+ alias everyone all
103
+ alias everybody all
104
+ alias anyone all
105
+
106
+ def _parse_and_add_rule(*args)
107
+ options = args.extract_options!
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 anonymous() then "#{_subject_ref}.nil?"
116
+ when logged_in() then "!#{_subject_ref}.nil?"
117
+ when all() then "true"
118
+ else
119
+ "!#{_subject_ref}.nil? && #{_subject_ref}.has_role?('#{who.to_s.singularize}', #{object})"
120
+ end
121
+ end
122
+
123
+ [:if, :unless].each do |cond|
124
+ val = options[cond]
125
+ raise ArgumentError, "#{cond} option must be a Symbol" if val && !val.is_a?(Symbol)
126
+ end
127
+
128
+ condition = [
129
+ (_method_ref(options[:if]) if options[:if]),
130
+ ("!#{_method_ref(options[:unless])}" if options[:unless])
131
+ ].compact.join(' && ')
132
+
133
+ condition = nil if condition.blank?
134
+
135
+ _add_rule(case role_checks.size
136
+ when 0
137
+ raise ArgumentError, "allow/deny should have at least 1 argument"
138
+ when 1 then role_checks.first
139
+ else
140
+ _either_of(role_checks)
141
+ end, condition)
142
+ end
143
+
144
+ def _either_of(exprs)
145
+ exprs.map { |expr| "(#{expr})" }.join(' || ')
146
+ end
147
+
148
+ def _add_rule(what, condition)
149
+ anded = [what] + [@action_clause, condition].compact
150
+ anded[0] = "(#{anded[0]})" if anded.size > 1
151
+
152
+ (@current_rule == :allow ? @allows : @denys) << anded.join(' && ')
153
+ end
154
+
155
+ def _set_action_clause(to, except)
156
+ raise ArgumentError, "both :to and :except cannot be specified in the rule" if to && except
157
+
158
+ @action_clause = nil
159
+
160
+ action_list = to || except
161
+ return unless action_list
162
+
163
+ expr = _action_check_expression(action_list)
164
+
165
+ @action_clause = if to
166
+ "#{expr}"
167
+ else
168
+ "!#{expr}"
169
+ end
170
+ end
171
+
172
+ def _action_check_expression(action_list)
173
+ unless action_list.is_a?(Array)
174
+ action_list = [ action_list.to_s ]
175
+ end
176
+
177
+ case action_list.size
178
+ when 0 then "true"
179
+ when 1 then "(#{_action_ref} == '#{action_list.first}')"
180
+ else
181
+ set_of_actions = "Set.new([" + action_list.map { |act| "'#{act}'"}.join(',') + "])"
182
+
183
+ "#{set_of_actions}.include?(#{_action_ref})"
184
+ end
185
+ end
186
+
187
+ VALID_PREPOSITIONS = %w(of for in on at by).freeze unless defined? VALID_PREPOSITIONS
188
+
189
+ def _role_object(options)
190
+ object = nil
191
+
192
+ VALID_PREPOSITIONS.each do |prep|
193
+ if options[prep.to_sym]
194
+ raise ArgumentError, "You may only use one preposition to specify object" if object
195
+
196
+ object = options[prep.to_sym]
197
+ end
198
+ end
199
+
200
+ case object
201
+ when Class
202
+ object.to_s
203
+ when Symbol
204
+ _object_ref object
205
+ when nil
206
+ "nil"
207
+ else
208
+ raise ArgumentError, "object specified by preposition can only be a Class or a Symbol"
209
+ end
210
+ end
211
+
212
+ def _subject_ref
213
+ raise
214
+ end
215
+
216
+ def _object_ref(object)
217
+ raise
218
+ end
219
+
220
+ def _action_ref
221
+ raise
222
+ end
223
+
224
+ def _method_ref(method)
225
+ raise
226
+ end
227
+ end
228
+ end
229
+ end
@@ -0,0 +1,197 @@
1
+ require File.join(File.dirname(__FILE__), 'dsl_base')
2
+
3
+ module Acl9
4
+ ##
5
+ # This exception is raised whenever ACL block finds that the current user
6
+ # is not authorized for the controller action he wants to execute.
7
+ # @example How to catch this exception in ApplicationController
8
+ # class ApplicationController < ActionController::Base
9
+ # rescue_from 'Acl9::AccessDenied', :with => :access_denied
10
+ #
11
+ # # ...other stuff...
12
+ # private
13
+ #
14
+ # def access_denied
15
+ # if current_user
16
+ # # It's presumed you have a template with words of pity and regret
17
+ # # for unhappy user who is not authorized to do what he wanted
18
+ # render :template => 'home/access_denied'
19
+ # else
20
+ # # In this case user has not even logged in. Might be OK after login.
21
+ # flash[:notice] = 'Access denied. Try to log in first.'
22
+ # redirect_to login_path
23
+ # end
24
+ # end
25
+ # end
26
+ #
27
+ class AccessDenied < StandardError; end
28
+
29
+ ##
30
+ # This exception is raised when acl9 has generated invalid code for the
31
+ # filtering method or block. Should never happen, and it's a bug when it
32
+ # happens.
33
+ class FilterSyntaxError < StandardError; end
34
+
35
+ module Dsl
36
+ module Generators
37
+ class BaseGenerator < Acl9::Dsl::Base
38
+ def initialize(*args)
39
+ @subject_method = args[0]
40
+
41
+ super
42
+ end
43
+
44
+ protected
45
+
46
+ def _access_denied
47
+ "raise Acl9::AccessDenied"
48
+ end
49
+
50
+ def _subject_ref
51
+ "#{_controller_ref}send(:#{@subject_method})"
52
+ end
53
+
54
+ def _object_ref(object)
55
+ "#{_controller_ref}instance_variable_get('@#{object}')"
56
+ end
57
+
58
+ def _action_ref
59
+ "#{_controller_ref}action_name"
60
+ end
61
+
62
+ def _method_ref(method)
63
+ "#{_controller_ref}send(:#{method})"
64
+ end
65
+
66
+ def _controller_ref
67
+ @controller ? "#{@controller}." : ''
68
+ end
69
+
70
+ def install_on(controller_class, options)
71
+ debug_dump(controller_class) if options[:debug]
72
+ end
73
+
74
+ def debug_dump(klass)
75
+ return unless logger
76
+ logger.debug "=== Acl9 access_control expression dump (#{klass.to_s})"
77
+ logger.debug self.to_s
78
+ logger.debug "======"
79
+ end
80
+
81
+ def logger
82
+ ActionController::Base.logger
83
+ end
84
+ end
85
+
86
+ class FilterLambda < BaseGenerator
87
+ def initialize(subject_method)
88
+ super
89
+
90
+ @controller = 'controller'
91
+ end
92
+
93
+ def install_on(controller_class, options)
94
+ super
95
+
96
+ controller_class.send(:before_filter, options, &self.to_proc)
97
+ end
98
+
99
+ def to_proc
100
+ code = <<-RUBY
101
+ lambda do |controller|
102
+ unless #{allowance_expression}
103
+ #{_access_denied}
104
+ end
105
+ end
106
+ RUBY
107
+
108
+ self.instance_eval(code, __FILE__, __LINE__)
109
+ rescue SyntaxError
110
+ raise FilterSyntaxError, code
111
+ end
112
+ end
113
+
114
+ ################################################################
115
+
116
+ class FilterMethod < BaseGenerator
117
+ def initialize(subject_method, method_name)
118
+ super
119
+
120
+ @method_name = method_name
121
+ @controller = nil
122
+ end
123
+
124
+ def install_on(controller_class, options)
125
+ super
126
+ _add_method(controller_class)
127
+ controller_class.send(:before_filter, @method_name, options)
128
+ end
129
+
130
+ protected
131
+
132
+ def _add_method(controller_class)
133
+ code = self.to_method_code
134
+ controller_class.send(:class_eval, code, __FILE__, __LINE__)
135
+ rescue SyntaxError
136
+ raise FilterSyntaxError, code
137
+ end
138
+
139
+ def to_method_code
140
+ <<-RUBY
141
+ def #{@method_name}
142
+ unless #{allowance_expression}
143
+ #{_access_denied}
144
+ end
145
+ end
146
+ RUBY
147
+ end
148
+ end
149
+
150
+ ################################################################
151
+
152
+ class BooleanMethod < FilterMethod
153
+ def install_on(controller_class, opts)
154
+ debug_dump(controller_class) if opts[:debug]
155
+
156
+ _add_method(controller_class)
157
+
158
+ if opts[:helper]
159
+ controller_class.send(:helper_method, @method_name)
160
+ end
161
+ end
162
+
163
+ protected
164
+
165
+ def to_method_code
166
+ <<-RUBY
167
+ def #{@method_name}(*args)
168
+ options = args.extract_options!
169
+
170
+ unless args.size <= 1
171
+ raise ArgumentError, "call #{@method_name} with 0, 1 or 2 arguments"
172
+ end
173
+
174
+ action_name = args.empty? ? self.action_name : args.first.to_s
175
+
176
+ return #{allowance_expression}
177
+ end
178
+ RUBY
179
+ end
180
+
181
+ def _object_ref(object)
182
+ "(options[:#{object}] || #{super})"
183
+ end
184
+ end
185
+
186
+ ################################################################
187
+
188
+ class HelperMethod < BooleanMethod
189
+ def initialize(subject_method, method)
190
+ super
191
+
192
+ @controller = 'controller'
193
+ end
194
+ end
195
+ end
196
+ end
197
+ end