aegis 1.1.8 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (65) hide show
  1. data/.gitignore +4 -0
  2. data/README.rdoc +58 -165
  3. data/Rakefile +20 -12
  4. data/VERSION +1 -1
  5. data/aegis.gemspec +85 -56
  6. data/lib/aegis.rb +9 -6
  7. data/lib/aegis/access_denied.rb +4 -0
  8. data/lib/aegis/action.rb +99 -0
  9. data/lib/aegis/compiler.rb +113 -0
  10. data/lib/aegis/has_role.rb +89 -110
  11. data/lib/aegis/parser.rb +110 -0
  12. data/lib/aegis/permissions.rb +164 -107
  13. data/lib/aegis/resource.rb +158 -0
  14. data/lib/aegis/role.rb +25 -55
  15. data/lib/aegis/sieve.rb +39 -0
  16. data/lib/rails/action_controller.rb +38 -0
  17. data/lib/rails/active_record.rb +1 -5
  18. data/spec/action_controller_spec.rb +100 -0
  19. data/spec/app_root/app/controllers/application_controller.rb +7 -0
  20. data/spec/app_root/app/controllers/reviews_controller.rb +36 -0
  21. data/spec/app_root/app/models/permissions.rb +14 -0
  22. data/spec/app_root/app/models/property.rb +5 -0
  23. data/spec/app_root/app/models/review.rb +5 -0
  24. data/{test → spec}/app_root/app/models/user.rb +1 -2
  25. data/{test → spec}/app_root/config/boot.rb +0 -0
  26. data/{test → spec}/app_root/config/database.yml +0 -0
  27. data/{test → spec}/app_root/config/environment.rb +0 -0
  28. data/{test → spec}/app_root/config/environments/in_memory.rb +0 -0
  29. data/{test → spec}/app_root/config/environments/mysql.rb +0 -0
  30. data/{test → spec}/app_root/config/environments/postgresql.rb +0 -0
  31. data/{test → spec}/app_root/config/environments/sqlite.rb +0 -0
  32. data/{test → spec}/app_root/config/environments/sqlite3.rb +0 -0
  33. data/spec/app_root/config/routes.rb +7 -0
  34. data/{test/app_root/db/migrate/20090408115228_create_users.rb → spec/app_root/db/migrate/001_create_users.rb} +2 -1
  35. data/spec/app_root/db/migrate/002_create_properties.rb +13 -0
  36. data/spec/app_root/db/migrate/003_create_reviews.rb +14 -0
  37. data/{test → spec}/app_root/lib/console_with_fixtures.rb +0 -0
  38. data/{test → spec}/app_root/log/.gitignore +0 -0
  39. data/{test → spec}/app_root/script/console +0 -0
  40. data/spec/controllers/reviews_controller_spec.rb +19 -0
  41. data/spec/has_role_spec.rb +177 -0
  42. data/spec/permissions_spec.rb +550 -0
  43. data/spec/rcov.opts +2 -0
  44. data/spec/spec.opts +4 -0
  45. data/{test/test_helper.rb → spec/spec_helper.rb} +6 -9
  46. metadata +73 -57
  47. data/lib/aegis/constants.rb +0 -6
  48. data/lib/aegis/normalization.rb +0 -26
  49. data/lib/aegis/permission_error.rb +0 -5
  50. data/lib/aegis/permission_evaluator.rb +0 -34
  51. data/test/app_root/app/controllers/application_controller.rb +0 -2
  52. data/test/app_root/app/models/old_soldier.rb +0 -6
  53. data/test/app_root/app/models/permissions.rb +0 -49
  54. data/test/app_root/app/models/soldier.rb +0 -5
  55. data/test/app_root/app/models/trust_fund_kid.rb +0 -5
  56. data/test/app_root/app/models/user_subclass.rb +0 -2
  57. data/test/app_root/app/models/veteran_soldier.rb +0 -6
  58. data/test/app_root/config/routes.rb +0 -4
  59. data/test/app_root/db/migrate/20090429075648_create_soldiers.rb +0 -14
  60. data/test/app_root/db/migrate/20091110075648_create_veteran_soldiers.rb +0 -14
  61. data/test/app_root/db/migrate/20091110075649_create_trust_fund_kids.rb +0 -15
  62. data/test/has_role_options_test.rb +0 -64
  63. data/test/has_role_test.rb +0 -54
  64. data/test/permissions_test.rb +0 -109
  65. data/test/validation_test.rb +0 -55
data/lib/aegis.rb CHANGED
@@ -1,10 +1,13 @@
1
- # Include hook code here
2
- require 'aegis/constants'
1
+ require 'ostruct'
2
+
3
+ require 'aegis/access_denied'
4
+ require 'aegis/action'
5
+ require 'aegis/compiler'
3
6
  require 'aegis/has_role'
4
- require 'aegis/normalization'
5
- require 'aegis/permission_error'
6
- require 'aegis/permission_evaluator'
7
+ require 'aegis/parser'
7
8
  require 'aegis/permissions'
8
9
  require 'aegis/role'
9
- require 'rails/active_record'
10
+ require 'aegis/sieve'
10
11
 
12
+ require 'rails/action_controller'
13
+ require 'rails/active_record'
@@ -0,0 +1,4 @@
1
+ module Aegis
2
+ class AccessDenied < StandardError
3
+ end
4
+ end
@@ -0,0 +1,99 @@
1
+ module Aegis
2
+ class Action
3
+
4
+ attr_reader :name, :takes_object, :takes_parent_object, :writing, :sieves, :pluralize_resource
5
+
6
+ def initialize(name, options)
7
+ @name = name.to_s
8
+ @sieves = []
9
+ update(options, true)
10
+ end
11
+
12
+ def update(options, use_defaults = false)
13
+ update_attribute(options, :takes_object, use_defaults, true)
14
+ update_attribute(options, :takes_parent_object, use_defaults, false)
15
+ update_attribute(options, :writing, use_defaults, true)
16
+ update_attribute(options, :pluralize_resource, use_defaults, false)
17
+ end
18
+
19
+ def update_attribute(options, key, use_defaults, default)
20
+ value = options[key]
21
+ value = default if value.nil? && use_defaults
22
+ instance_variable_set("@#{key}", value) unless value.nil?
23
+ end
24
+
25
+ def may?(user, *args)
26
+ context = extract_context(user, args)
27
+ may = user.role.may_by_default?
28
+ for sieve in sieves
29
+ opinion = sieve.may?(context, *args)
30
+ may = opinion unless opinion.nil?
31
+ end
32
+ may
33
+ end
34
+
35
+ def may!(user, *args)
36
+ may?(user, *args) or raise Aegis::AccessDenied, "Access denied: #{args.inspect}"
37
+ end
38
+
39
+ def self.index(options = {})
40
+ new('index', options.reverse_merge(:takes_object => false, :pluralize_resource => true, :writing => false))
41
+ end
42
+
43
+ def self.show(options = {})
44
+ new('show', options.reverse_merge(:takes_object => true, :writing => false))
45
+ end
46
+
47
+ def self.update(options = {})
48
+ new('update', options.reverse_merge(:takes_object => true, :writing => true))
49
+ end
50
+
51
+ def self.create(options = {})
52
+ new('create', options.reverse_merge(:takes_object => false, :writing => true))
53
+ end
54
+
55
+ def self.destroy(options = {})
56
+ new('destroy', options.reverse_merge(:takes_object => true, :writing => true))
57
+ end
58
+
59
+ def self.undefined
60
+ new(nil, :takes_object => false, :writing => true)
61
+ end
62
+
63
+ def self.allow_to_all
64
+ action = undefined
65
+ action.sieves << Aegis::Sieve.allow_to_all
66
+ action
67
+ end
68
+
69
+ def self.deny_to_all
70
+ action = undefined
71
+ action.sieves << Aegis::Sieve.deny_to_all
72
+ action
73
+ end
74
+
75
+ def abstract?
76
+ name.blank?
77
+ end
78
+
79
+ def inspect
80
+ "Action(#{{ :name => name, :takes_object => takes_object, :takes_parent_object => takes_parent_object, :sieves => sieves }.inspect})"
81
+ end
82
+
83
+ private
84
+
85
+ # not *args so we can change the array reference
86
+ def extract_context(user, args)
87
+ context = {}
88
+ context[:user] = user
89
+ if takes_parent_object
90
+ context[:parent_object] = args.shift or raise ArgumentError, "No parent object given"
91
+ end
92
+ if takes_object
93
+ context[:object] = args.shift or raise ArgumentError, "No object given"
94
+ end
95
+ OpenStruct.new(context)
96
+ end
97
+
98
+ end
99
+ end
@@ -0,0 +1,113 @@
1
+ module Aegis
2
+ class Compiler
3
+
4
+ ATOM_GROUPS = {
5
+ :namespace => :structure,
6
+ :resource => :structure,
7
+ :resources => :structure,
8
+ :action => :structure,
9
+ :allow => :sieve,
10
+ :deny => :sieve,
11
+ :reading => :sieve,
12
+ :writing => :sieve
13
+ }
14
+
15
+ def initialize(resource)
16
+ @resource = resource
17
+ end
18
+
19
+ def compile(atoms)
20
+ grouped_atoms = group_atoms(atoms)
21
+ for atom in grouped_atoms[:structure] || []
22
+ compile_structure(atom)
23
+ end
24
+ for atom in grouped_atoms[:sieve] || []
25
+ compile_sieve(atom)
26
+ end
27
+ end
28
+
29
+ def self.compile(resource, atoms)
30
+ new(resource).compile(atoms)
31
+ end
32
+
33
+ private
34
+
35
+ def compile_structure(atom)
36
+ case atom[:type]
37
+ when :action
38
+ compile_action(atom)
39
+ when :namespace
40
+ compile_namespace(atom)
41
+ when :resource
42
+ compile_child_resource(atom, :singleton)
43
+ when :resources
44
+ compile_child_resource(atom, :collection)
45
+ else
46
+ "Unexpected atom type: #{atom[:type]}"
47
+ end
48
+ end
49
+
50
+ def compile_namespace(atom)
51
+ atom[:options].merge!(:only => [])
52
+ compile_child_resource(atom, :singleton)
53
+ end
54
+
55
+ def compile_action(atom)
56
+ action = @resource.create_or_update_action(
57
+ atom[:name],
58
+ create_action_options(atom[:options]),
59
+ update_action_options(atom[:options])
60
+ )
61
+ for sieve_atom in atom[:children]
62
+ compile_sieve(sieve_atom, [action])
63
+ end
64
+ end
65
+
66
+ def compile_sieve(atom, affected_actions = @resource.actions)
67
+ case atom[:type]
68
+ when :allow
69
+ for action in affected_actions
70
+ action.sieves << Aegis::Sieve.new(atom[:role_name], true, atom[:block])
71
+ end
72
+ when :deny
73
+ for action in affected_actions
74
+ action.sieves << Aegis::Sieve.new(atom[:role_name], false, atom[:block])
75
+ end
76
+ when :reading
77
+ for child in atom[:children]
78
+ compile_sieve(child, @resource.reading_actions)
79
+ end
80
+ when :writing
81
+ for child in atom[:children]
82
+ compile_sieve(child, @resource.writing_actions)
83
+ end
84
+ else
85
+ "Unexpected atom type: #{atom[:type]}"
86
+ end
87
+ end
88
+
89
+ def compile_child_resource(atom, type)
90
+ child = Aegis::Resource.new(@resource, atom[:name], type, atom[:options])
91
+ @resource.children << child
92
+ Aegis::Compiler.compile(child, atom[:children])
93
+ end
94
+
95
+ def create_action_options(options)
96
+ { :takes_object => @resource.new_action_takes_object?(options),
97
+ :takes_parent_object => @resource.new_action_takes_parent_object?(options)
98
+ }.merge(update_action_options(options))
99
+ end
100
+
101
+ def update_action_options(options)
102
+ { :writing => options[:writing],
103
+ :pluralize_resource => options[:collection] }
104
+ end
105
+
106
+ def group_atoms(atoms)
107
+ atoms.group_by do |atom|
108
+ ATOM_GROUPS[atom[:type]]
109
+ end
110
+ end
111
+
112
+ end
113
+ end
@@ -1,110 +1,89 @@
1
- module Aegis
2
- module HasRole
3
-
4
- def validates_role_name(options = {})
5
- validates_each :role_name do |record, attr, value|
6
- options[:message] ||= I18n.translate('activerecord.errors.messages.inclusion')
7
- role = ::Permissions.find_role_by_name(value)
8
- record.errors.add attr, options[:message] if role.nil?
9
- end
10
- end
11
-
12
- alias_method :validates_role, :validates_role_name
13
-
14
- def has_role(options = {})
15
-
16
- # Legacy parameter names
17
- options[:accessor] ||= options.delete(:name_accessor)
18
- options[:reader] ||= options.delete(:name_reader)
19
- options[:writer] ||= options.delete(:name_writer)
20
-
21
- if options[:accessor]
22
- options[:reader] = "#{options[:accessor]}"
23
- options[:writer] = "#{options[:accessor]}="
24
- options.delete(:accessor)
25
- end
26
-
27
- self.class_eval do
28
-
29
- class_attribute :aegis_role_name_reader, :aegis_role_name_writer, :aegis_default_role_name
30
-
31
- unless method_defined?(:after_initialize)
32
- def after_initialize
33
- end
34
- end
35
-
36
- if options[:default]
37
- self.aegis_default_role_name = options[:default].to_s
38
- after_initialize :set_default_aegis_role_name
39
- end
40
-
41
- self.aegis_role_name_reader = (options[:reader] || "role_name").to_sym
42
- self.aegis_role_name_writer = (options[:writer] || "role_name=").to_sym
43
-
44
- def aegis_role_name_reader
45
- self.class.class_eval{ aegis_role_name_reader }
46
- end
47
-
48
- def aegis_role_name_writer
49
- self.class.class_eval{ aegis_role_name_writer }
50
- end
51
-
52
- def aegis_role_name
53
- send(aegis_role_name_reader)
54
- end
55
-
56
- def aegis_role_name=(value)
57
- send(aegis_role_name_writer, value)
58
- end
59
-
60
- def role
61
- ::Permissions.find_role_by_name!(aegis_role_name)
62
- end
63
-
64
- def role=(role_or_name)
65
- self.aegis_role_name = if role_or_name.is_a?(Aegis::Role)
66
- role_or_name.name
67
- else
68
- role_or_name.to_s
69
- end
70
- end
71
-
72
- private
73
-
74
- # Delegate may_...? and may_...! methods to the user's role.
75
- def method_missing_with_aegis_permissions(symb, *args)
76
- method_name = symb.to_s
77
- if method_name =~ /^may_(.+?)[\!\?]$/
78
- role.send(symb, self, *args)
79
- elsif method_name =~ /^(.*?)\?$/ && queried_role = ::Permissions.find_role_by_name($1)
80
- role == queried_role
81
- else
82
- method_missing_without_aegis_permissions(symb, *args)
83
- end
84
- end
85
-
86
- alias_method_chain :method_missing, :aegis_permissions
87
-
88
- def respond_to_with_aegis_permissions?(symb, include_private = false)
89
- if symb.to_s =~ /^may_(.+?)[\!\?]$/
90
- true
91
- else
92
- respond_to_without_aegis_permissions?(symb, include_private)
93
- end
94
- end
95
-
96
- alias_method_chain :respond_to?, :aegis_permissions
97
-
98
- def set_default_aegis_role_name
99
- if new_record? && self.aegis_role_name.blank?
100
- self.aegis_role_name = self.class.aegis_default_role_name
101
- end
102
- end
103
-
104
- end
105
-
106
- end
107
-
108
- end
109
-
110
- end
1
+ module Aegis
2
+ module HasRole
3
+
4
+ def has_role(options = {})
5
+
6
+ if options[:accessor]
7
+ options[:reader] = "#{options[:accessor]}"
8
+ options[:writer] = "#{options[:accessor]}="
9
+ options.delete(:accessor)
10
+ end
11
+
12
+ get_role_name = (options[:reader] || "role_name").to_sym
13
+ set_role_name = (options[:writer] || "role_name=").to_sym
14
+
15
+ permissions = lambda { Aegis::Permissions.app_permissions(options[:permissions]) }
16
+
17
+ may_pattern = /^may_(.+?)([\!\?])$/
18
+
19
+ send :define_method, :role do
20
+ permissions.call.find_role_by_name(send(get_role_name))
21
+ end
22
+
23
+ send :define_method, :role= do |role|
24
+ send(set_role_name, role.name)
25
+ end
26
+
27
+ metaclass.send :define_method, :validates_role do |*validate_options|
28
+ validate_options = validate_options[0] || {}
29
+
30
+ send :define_method, :validate_role do
31
+ role = permissions.call.find_role_by_name(send(get_role_name))
32
+ unless role
33
+ message = validate_options[:message] || I18n.translate('activerecord.errors.messages.inclusion')
34
+ errors.add get_role_name, message
35
+ end
36
+ end
37
+
38
+ validate :validate_role
39
+ end
40
+
41
+ if options[:default]
42
+
43
+ unless method_defined?(:after_initialize)
44
+ send :define_method, :after_initialize do
45
+ end
46
+ end
47
+
48
+ send :define_method, :set_default_role_name do
49
+ if new_record? && send(get_role_name).blank?
50
+ send(set_role_name, options[:default])
51
+ end
52
+ end
53
+
54
+ after_initialize :set_default_role_name
55
+
56
+ end
57
+
58
+ unless method_defined?(:method_missing_with_aegis_permissions)
59
+
60
+ # Delegate may_...? and may_...! methods to the permissions class.
61
+ send :define_method, :method_missing_with_aegis_permissions do |symb, *args|
62
+ method_name = symb.to_s
63
+ if method_name =~ may_pattern
64
+ action_path = $1
65
+ severity = $2
66
+ permissions.call.send("may#{severity}", self, action_path, *args)
67
+ else
68
+ method_missing_without_aegis_permissions(symb, *args)
69
+ end
70
+ end
71
+
72
+ alias_method_chain :method_missing, :aegis_permissions
73
+
74
+ send :define_method, :respond_to_with_aegis_permissions? do |symb, *args|
75
+ if symb.to_s =~ may_pattern
76
+ true
77
+ else
78
+ include_private = args.first.nil? ? false : args.first
79
+ respond_to_without_aegis_permissions?(symb, include_private)
80
+ end
81
+ end
82
+
83
+ alias_method_chain :respond_to?, :aegis_permissions
84
+
85
+ end
86
+ end
87
+
88
+ end
89
+ end
@@ -0,0 +1,110 @@
1
+ module Aegis
2
+ class Parser
3
+
4
+ attr_reader :atoms
5
+
6
+ def self.parse(&block)
7
+ Aegis::Parser.new.parse(&block)
8
+ end
9
+
10
+ def initialize
11
+ @atoms = []
12
+ end
13
+
14
+ def parse(&block)
15
+ instance_eval(&block) if block
16
+ atoms
17
+ end
18
+
19
+ def action(*args, &block)
20
+ split_definitions(*args) do |name, options|
21
+ @atoms.push({
22
+ :type => :action,
23
+ :name => name.to_s,
24
+ :options => options,
25
+ :children => Aegis::Parser.parse(&block)
26
+ })
27
+ end
28
+ end
29
+
30
+ def namespace(*args, &block)
31
+ split_definitions(*args) do |name, options|
32
+ @atoms.push({
33
+ :type => :namespace,
34
+ :name => name.to_s,
35
+ :options => options,
36
+ :children => Aegis::Parser.parse(&block)
37
+ })
38
+ end
39
+ end
40
+
41
+ def resource(*args, &block)
42
+ split_definitions(*args) do |name, options|
43
+ @atoms.push({
44
+ :type => :resource,
45
+ :name => name.to_s,
46
+ :options => options,
47
+ :children => Aegis::Parser.parse(&block)
48
+ })
49
+ end
50
+ end
51
+
52
+ def resources(*args, &block)
53
+ split_definitions(*args) do |name, options|
54
+ @atoms.push({
55
+ :type => :resources,
56
+ :name => name.to_s,
57
+ :options => options,
58
+ :children => Aegis::Parser.parse(&block)
59
+ })
60
+ end
61
+ end
62
+
63
+ def allow(*args, &block)
64
+ split_definitions(*args) do |role_name, options|
65
+ @atoms.push({
66
+ :type => :allow,
67
+ :role_name => role_name.to_s,
68
+ :block => block
69
+ })
70
+ end
71
+ end
72
+
73
+ def deny(*args, &block)
74
+ split_definitions(*args) do |role_name, options|
75
+ @atoms.push({
76
+ :type => :deny,
77
+ :role_name => role_name.to_s,
78
+ :block => block
79
+ })
80
+ end
81
+ end
82
+
83
+ def reading(&block)
84
+ block or raise "missing block"
85
+ @atoms.push({
86
+ :type => :reading,
87
+ :children => Aegis::Parser.parse(&block)
88
+ })
89
+ end
90
+
91
+ def writing(&block)
92
+ block or raise "missing block"
93
+ @atoms.push({
94
+ :type => :writing,
95
+ :children => Aegis::Parser.parse(&block)
96
+ })
97
+ end
98
+
99
+ private
100
+
101
+ def split_definitions(*args, &definition)
102
+ options = args.extract_options!
103
+ args = [nil] if args.empty?
104
+ for name in args
105
+ definition.call(name, options)
106
+ end
107
+ end
108
+
109
+ end
110
+ end