be9-acl9 0.9.1

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