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