be9-acl9 0.9.1

Sign up to get free protection for your applications and to get access to all the features.
data/Rakefile ADDED
@@ -0,0 +1,37 @@
1
+ require 'rubygems'
2
+ require File.join(File.dirname(__FILE__), 'lib', 'acl9', 'version')
3
+ require 'rake'
4
+ require 'spec/rake/spectask'
5
+
6
+ desc 'Default: run specs.'
7
+ task :default => :spec
8
+
9
+ begin
10
+ require 'echoe'
11
+
12
+ Echoe.new 'acl9' do |p|
13
+ p.version = Acl9::Version::STRING
14
+ p.author = "Oleg Dashevskii"
15
+ p.email = 'olegdashevskii@gmail.com'
16
+ p.project = 'acl9'
17
+ p.summary = "Yet another role-based authorization system for Rails with a nice DSL for access control lists."
18
+ p.url = "http://github.com/be9/acl9"
19
+ p.ignore_pattern = ["spec/db/*.sqlite3", "spec/debug.log"]
20
+ p.development_dependencies = ["rspec >=1.1.11", "rspec-rails >=1.1.11"]
21
+ end
22
+ rescue LoadError => boom
23
+ puts "You are missing a dependency required for meta-operations on this gem."
24
+ puts "#{boom.to_s.capitalize}."
25
+ end
26
+
27
+ desc 'Run the specs'
28
+ Spec::Rake::SpecTask.new(:spec) do |t|
29
+ t.spec_opts = ['--colour --format progress --loadby mtime --reverse']
30
+ t.spec_files = FileList['spec/**/*_spec.rb']
31
+ end
32
+
33
+ desc 'Regenerate the .gemspec'
34
+ task :gemspec => :package do
35
+ gemspec = Dir["pkg/**/*.gemspec"].first
36
+ FileUtils.cp gemspec, "."
37
+ end
data/acl9.gemspec ADDED
@@ -0,0 +1,37 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = %q{acl9}
5
+ s.version = "0.9.1"
6
+
7
+ s.required_rubygems_version = Gem::Requirement.new(">= 1.2") if s.respond_to? :required_rubygems_version=
8
+ s.authors = ["Oleg Dashevskii"]
9
+ s.date = %q{2009-01-03}
10
+ s.description = %q{Yet another role-based authorization system for Rails with a nice DSL for access control lists.}
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"]
14
+ s.has_rdoc = true
15
+ s.homepage = %q{http://github.com/be9/acl9}
16
+ s.rdoc_options = ["--line-numbers", "--inline-source", "--title", "Acl9", "--main", "README.textile"]
17
+ s.require_paths = ["lib"]
18
+ s.rubyforge_project = %q{acl9}
19
+ s.rubygems_version = %q{1.3.1}
20
+ s.summary = %q{Yet another role-based authorization system for Rails with a nice DSL for access control lists.}
21
+
22
+ if s.respond_to? :specification_version then
23
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
24
+ s.specification_version = 2
25
+
26
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
27
+ s.add_development_dependency(%q<rspec>, [">= 1.1.11"])
28
+ s.add_development_dependency(%q<rspec-rails>, [">= 1.1.11"])
29
+ else
30
+ s.add_dependency(%q<rspec>, [">= 1.1.11"])
31
+ s.add_dependency(%q<rspec-rails>, [">= 1.1.11"])
32
+ end
33
+ else
34
+ s.add_dependency(%q<rspec>, [">= 1.1.11"])
35
+ s.add_dependency(%q<rspec-rails>, [">= 1.1.11"])
36
+ end
37
+ end
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require 'acl9'
data/lib/acl9.rb ADDED
@@ -0,0 +1,14 @@
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
+
13
+ ActionController::Base.send(:include, Acl9::ControllerExtensions)
14
+ end
@@ -0,0 +1,9 @@
1
+ module Acl9
2
+ @@config = {
3
+ :default_role_class_name => 'Role',
4
+ :default_subject_class_name => 'User',
5
+ :default_subject_method => :current_user,
6
+ }
7
+
8
+ mattr_reader :config
9
+ end
@@ -0,0 +1,37 @@
1
+ require File.join(File.dirname(__FILE__), 'controller_extensions', 'filter_producer')
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(opts = {}, &block)
11
+ subject_method = opts.delete(:subject_method) || Acl9::config[:default_subject_method]
12
+
13
+ raise ArgumentError, "Block must be supplied to access_control" unless block
14
+
15
+ producer = Acl9::FilterProducer.new(subject_method)
16
+ producer.acl(&block)
17
+
18
+ filter = opts.delete(:filter)
19
+ filter = true if filter.nil?
20
+
21
+ if opts.delete(:debug)
22
+ Rails::logger.debug "=== Acl9 access_control expression dump (#{self.to_s})"
23
+ Rails::logger.debug producer.to_s
24
+ Rails::logger.debug "======"
25
+ end
26
+
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
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,244 @@
1
+ require 'set'
2
+
3
+ module Acl9
4
+ class AccessDenied < Exception; end
5
+ class FilterSyntaxError < Exception; end
6
+
7
+ class FilterProducer
8
+ attr_reader :allows, :denys
9
+
10
+ def initialize(subject_method)
11
+ @subject_method = subject_method
12
+ @default_action = nil
13
+ @allows = []
14
+ @denys = []
15
+
16
+ @subject = "controller.send(:#{subject_method})"
17
+ end
18
+
19
+ def acl(&acl_block)
20
+ self.instance_eval(&acl_block)
21
+ end
22
+
23
+ def to_s
24
+ _allowance_check_expression
25
+ end
26
+
27
+ def to_proc
28
+ code = <<-RUBY
29
+ lambda do |controller|
30
+ unless #{self.to_s}
31
+ raise Acl9::AccessDenied
32
+ end
33
+ end
34
+ RUBY
35
+
36
+ self.instance_eval(code, __FILE__, __LINE__)
37
+ rescue SyntaxError
38
+ raise FilterSyntaxError, code
39
+ end
40
+
41
+ def to_method_code(method_name, filter = true)
42
+ body = if filter
43
+ "unless #{self.to_s}; raise Acl9::AccessDenied; end"
44
+ else
45
+ self.to_s
46
+ end
47
+
48
+ <<-RUBY
49
+ def #{method_name}
50
+ controller = self
51
+ #{body}
52
+ end
53
+ RUBY
54
+ end
55
+
56
+ def default_action
57
+ @default_action.nil? ? :deny : @default_action
58
+ end
59
+
60
+ protected
61
+
62
+ def default(default_action)
63
+ raise ArgumentError, "default can only be called once in access_control block" if @default_action
64
+
65
+ unless [:allow, :deny].include? default_action
66
+ raise ArgumentError, "invalid value for default (can be :allow or :deny)"
67
+ end
68
+
69
+ @default_action = default_action
70
+ end
71
+
72
+ def allow(*args)
73
+ @current_rule = :allow
74
+ _parse_and_add_rule(*args)
75
+ end
76
+
77
+ def deny(*args)
78
+ @current_rule = :deny
79
+ _parse_and_add_rule(*args)
80
+ end
81
+
82
+ def actions(*args, &block)
83
+ raise ArgumentError, "actions should receive at least 1 action as argument" if args.size < 1
84
+
85
+ subsidiary = FilterProducer.new(@subject_method)
86
+
87
+ class <<subsidiary
88
+ def actions(*args)
89
+ raise ArgumentError, "You cannot use actions inside another actions block"
90
+ end
91
+
92
+ def default(*args)
93
+ raise ArgumentError, "You cannot use default inside an actions block"
94
+ end
95
+
96
+ def _set_action_clause(to, except)
97
+ raise ArgumentError, "You cannot use :to/:except inside actions block" if to || except
98
+ end
99
+ end
100
+
101
+ subsidiary.acl(&block)
102
+
103
+ action_check = _action_check_expression(args)
104
+
105
+ squash = lambda do |rules|
106
+ _either_of(rules) + ' && ' + action_check
107
+ end
108
+
109
+ @allows << squash.call(subsidiary.allows) if subsidiary.allows.size > 0
110
+ @denys << squash.call(subsidiary.denys) if subsidiary.denys.size > 0
111
+ end
112
+
113
+ alias action actions
114
+
115
+ def anonymous
116
+ nil
117
+ end
118
+
119
+ def all
120
+ true
121
+ end
122
+
123
+ def logged_in
124
+ false
125
+ end
126
+
127
+ private
128
+
129
+ def _parse_and_add_rule(*args)
130
+ options = if args.last.is_a? Hash
131
+ args.pop
132
+ else
133
+ {}
134
+ end
135
+
136
+ _set_action_clause(options.delete(:to), options.delete(:except))
137
+
138
+ object = _role_object(options)
139
+
140
+ role_checks = args.map do |who|
141
+ case who
142
+ when nil then "#{@subject}.nil?" # anonymous
143
+ when false then "!#{@subject}.nil?" # logged_in
144
+ when true then "true" # all
145
+ else
146
+ "!#{@subject}.nil? && #{@subject}.has_role?('#{who.to_s.singularize}', #{object})"
147
+ end
148
+ end
149
+
150
+ _add_rule case role_checks.size
151
+ when 0
152
+ raise ArgumentError, "allow/deny should have at least 1 argument"
153
+ when 1 then role_checks.first
154
+ else
155
+ _either_of(role_checks)
156
+ end
157
+ end
158
+
159
+ def _either_of(exprs)
160
+ exprs.map { |expr| "(#{expr})" }.join(' || ')
161
+ end
162
+
163
+ def _add_rule(what)
164
+ what = "(#{what}) && #{@action_clause}" if @action_clause
165
+
166
+ (@current_rule == :allow ? @allows : @denys) << what
167
+ end
168
+
169
+ def _set_action_clause(to, except)
170
+ raise ArgumentError, "both :to and :except cannot be specified in the rule" if to && except
171
+
172
+ @action_clause = nil
173
+
174
+ action_list = to || except
175
+ return unless action_list
176
+
177
+ expr = _action_check_expression(action_list)
178
+
179
+ @action_clause = if to
180
+ "#{expr}"
181
+ else
182
+ "!#{expr}"
183
+ end
184
+ end
185
+
186
+ def _action_check_expression(action_list)
187
+ unless action_list.is_a?(Array)
188
+ action_list = [ action_list.to_s ]
189
+ end
190
+
191
+ case action_list.size
192
+ when 0 then "true"
193
+ when 1 then "(controller.action_name == '#{action_list.first}')"
194
+ else
195
+ set_of_actions = "Set.new([" + action_list.map { |act| "'#{act}'"}.join(',') + "])"
196
+
197
+ "#{set_of_actions}.include?(controller.action_name)"
198
+ end
199
+ end
200
+
201
+ VALID_PREPOSITIONS = %w(of for in on at by).freeze unless defined? VALID_PREPOSITIONS
202
+
203
+ def _role_object(options)
204
+ object = nil
205
+
206
+ VALID_PREPOSITIONS.each do |prep|
207
+ if options[prep.to_sym]
208
+ raise ArgumentError, "You may only use one preposition to specify object" if object
209
+
210
+ object = options[prep.to_sym]
211
+ end
212
+ end
213
+
214
+ case object
215
+ when Class
216
+ object.to_s
217
+ when Symbol
218
+ "controller.instance_variable_get('@#{object}')"
219
+ when nil
220
+ "nil"
221
+ else
222
+ raise ArgumentError, "object specified by preposition can only be a Class or a Symbol"
223
+ end
224
+ end
225
+
226
+ def _allowance_check_expression
227
+ allowed_expr = if @allows.size > 0
228
+ @allows.map { |clause| "(#{clause})" }.join(' || ')
229
+ else
230
+ "false"
231
+ end
232
+
233
+ not_denied_expr = if @denys.size > 0
234
+ @denys.map { |clause| "!(#{clause})" }.join(' && ')
235
+ else
236
+ "true"
237
+ end
238
+
239
+ [allowed_expr, not_denied_expr].
240
+ map { |expr| "(#{expr})" }.
241
+ join(default_action == :deny ? ' && ' : ' || ')
242
+ end
243
+ end
244
+ end
@@ -0,0 +1,58 @@
1
+ require File.join(File.dirname(__FILE__), 'model_extensions', 'subject')
2
+ require File.join(File.dirname(__FILE__), 'model_extensions', 'object')
3
+
4
+ module Acl9
5
+ module ModelExtensions
6
+ def self.included(base)
7
+ base.extend(ClassMethods)
8
+ end
9
+
10
+ module ClassMethods
11
+ def acts_as_authorization_subject(options = {})
12
+ role = options[:role_class_name] || Acl9::config[:default_role_class_name]
13
+ has_and_belongs_to_many :roles, :class_name => role
14
+
15
+ cattr_accessor :_auth_role_class_name
16
+ self._auth_role_class_name = role
17
+
18
+ include Acl9::ModelExtensions::Subject
19
+ end
20
+
21
+ def acts_as_authorization_object(options = {})
22
+ subject = options[:subject_class_name] || Acl9::config[:default_subject_class_name]
23
+ subj_table = subject.tableize
24
+ subj_col = subject.underscore
25
+
26
+ role = options[:role_class_name] || Acl9::config[:default_role_class_name]
27
+ role_table = role.tableize
28
+
29
+ sql_tables = <<-EOS
30
+ FROM #{subj_table}
31
+ INNER JOIN #{role_table}_#{subj_table} ON #{subj_col}_id = #{subj_table}.id
32
+ INNER JOIN #{role_table} ON #{role_table}.id = #{role.underscore}_id
33
+ EOS
34
+
35
+ sql_where = <<-'EOS'
36
+ WHERE authorizable_type = '#{self.class.base_class.to_s}'
37
+ AND authorizable_id = #{id}"
38
+ EOS
39
+
40
+ has_many :accepted_roles, :as => :authorizable, :class_name => role, :dependent => :destroy
41
+
42
+ has_many :"#{subj_table}",
43
+ :finder_sql => ("SELECT DISTINCT #{subj_table}.*" + sql_tables + sql_where),
44
+ :counter_sql => ("SELECT COUNT(DISTINCT #{subj_table}.id)" + sql_tables + sql_where),
45
+ :readonly => true
46
+
47
+ include Acl9::ModelExtensions::Object
48
+ end
49
+
50
+ def acts_as_authorization_role(options = {})
51
+ subject = options[:subject_class_name] || Acl9::config[:default_subject_class_name]
52
+
53
+ has_and_belongs_to_many subject.tableize.to_sym
54
+ belongs_to :authorizable, :polymorphic => true
55
+ end
56
+ end
57
+ end
58
+ end