aegis 1.1.8 → 2.0.0

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