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/MIT-LICENSE +20 -0
- data/Manifest +19 -0
- data/README.textile +765 -0
- data/Rakefile +37 -0
- data/acl9.gemspec +37 -0
- data/init.rb +1 -0
- data/lib/acl9.rb +14 -0
- data/lib/acl9/config.rb +9 -0
- data/lib/acl9/controller_extensions.rb +37 -0
- data/lib/acl9/controller_extensions/filter_producer.rb +244 -0
- data/lib/acl9/model_extensions.rb +58 -0
- data/lib/acl9/model_extensions/object.rb +27 -0
- data/lib/acl9/model_extensions/subject.rb +107 -0
- data/lib/acl9/version.rb +54 -0
- data/spec/access_control_spec.rb +185 -0
- data/spec/db/schema.rb +47 -0
- data/spec/filter_producer_spec.rb +707 -0
- data/spec/models.rb +27 -0
- data/spec/roles_spec.rb +259 -0
- data/spec/spec_helper.rb +34 -0
- metadata +102 -0
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
|
data/lib/acl9/config.rb
ADDED
@@ -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
|