acl9 0.11.0

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