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
@@ -1,107 +1,164 @@
1
- module Aegis
2
- class Permissions
3
-
4
- def self.inherited(base)
5
- base.class_eval do
6
- @roles_by_name = {}
7
- @permission_blocks = Hash.new { |hash, key| hash[key] = [] }
8
- extend ClassMethods
9
- end
10
- end
11
-
12
- module ClassMethods
13
-
14
- def role(role_name, options = {})
15
- role_name = role_name.to_sym
16
- role_name != Aegis::Constants::EVERYONE_ROLE_NAME or raise "Cannot define a role named: #{Aegis::Constants::EVERYONE_ROLE_NAME}"
17
- @roles_by_name[role_name] = Aegis::Role.new(role_name, self, options)
18
- end
19
-
20
- def find_all_role_names
21
- @roles_by_name.keys
22
- end
23
-
24
- def find_all_roles
25
- @roles_by_name.values.sort
26
- end
27
-
28
- def find_role_by_name(name)
29
- # cannot call :to_sym on nil or an empty string
30
- if name.blank?
31
- nil
32
- else
33
- @roles_by_name[name.to_sym]
34
- end
35
- end
36
-
37
- def find_role_by_name!(name)
38
- find_role_by_name(name) or raise "Undefined role: #{name}"
39
- end
40
-
41
- def permission(*permission_name_or_names, &block)
42
- permission_names = Array(permission_name_or_names).map(&:to_s)
43
- permission_names.each do |permission_name|
44
- add_split_crud_permission(permission_name, &block)
45
- end
46
- end
47
-
48
- def may?(role_or_role_name, permission, *args)
49
- role = role_or_role_name.is_a?(Aegis::Role) ? role_or_role_name : find_role_by_name(role_or_role_name)
50
- blocks = @permission_blocks[permission.to_sym]
51
- evaluate_permission_blocks(role, blocks, *args)
52
- end
53
-
54
- def evaluate_permission_blocks(role, blocks, *args)
55
- evaluator = Aegis::PermissionEvaluator.new(role)
56
- evaluator.evaluate(blocks, args)
57
- end
58
-
59
- def denied?(*args)
60
- !allowed?(*args)
61
- end
62
-
63
- private
64
-
65
- def add_split_crud_permission(permission_name, &block)
66
- if permission_name =~ /^crud_(.+?)$/
67
- target = $1
68
- Aegis::Constants::CRUD_VERBS.each do |verb|
69
- add_normalized_permission("#{verb}_#{target}", &block)
70
- end
71
- else
72
- add_normalized_permission(permission_name, &block)
73
- end
74
- end
75
-
76
- def add_normalized_permission(permission_name, &block)
77
- normalized_permission_name = Aegis::Normalization.normalize_permission(permission_name)
78
- add_singularized_permission(normalized_permission_name, &block)
79
- end
80
-
81
- def add_singularized_permission(permission_name, &block)
82
- if permission_name =~ /^([^_]+?)_(.+?)$/
83
- verb = $1
84
- target = $2
85
- singular_target = target.singularize
86
- if singular_target.length < target.length
87
- singular_block = lambda do |*args|
88
- args.delete_at 1
89
- instance_exec(*args, &block)
90
- end
91
- singular_permission_name = "#{verb}_#{singular_target}"
92
- add_permission(singular_permission_name, &singular_block)
93
- end
94
- end
95
- add_permission(permission_name, &block)
96
- end
97
-
98
- def add_permission(permission_name, &block)
99
- permission_name = permission_name.to_sym
100
- @permission_blocks[permission_name] << block
101
- end
102
-
103
- end # module ClassMethods
104
-
105
- end # class Permissions
106
- end # module Aegis
107
-
1
+ module Aegis
2
+ class Permissions
3
+ class << self
4
+
5
+ MISSING_ACTION_STRATEGIES = [
6
+ :allow, :deny, :default_permission, :error
7
+ ]
8
+
9
+ def missing_action_means(strategy)
10
+ prepare
11
+ MISSING_ACTION_STRATEGIES.include?(strategy) or raise ArgumentError, "missing_action_means must be one of #{MISSING_ACTION_STRATEGIES.inspect}"
12
+ @missing_action_strategy = strategy
13
+ end
14
+
15
+ def missing_user_means(&strategy)
16
+ prepare
17
+ @missing_user_strategy = strategy
18
+ end
19
+
20
+ def alias_action(aliases)
21
+ prepare
22
+ aliases.each do |key, value|
23
+ @action_aliases[key.to_s] = value.to_s
24
+ end
25
+ end
26
+
27
+ def permission(*args)
28
+ raise "The Aegis API has changed. See http://wiki.github.com/makandra/aegis/upgrading-to-aegis-2 for migration instructions."
29
+ end
30
+
31
+ def action(*args, &block)
32
+ prepare
33
+ @parser.action(*args, &block)
34
+ end
35
+
36
+ def resource(*args, &block)
37
+ prepare
38
+ @parser.resource(*args, &block)
39
+ end
40
+
41
+ def namespace(*args, &block)
42
+ prepare
43
+ @parser.namespace(*args, &block)
44
+ end
45
+
46
+ def resources(*args, &block)
47
+ prepare
48
+ @parser.resources(*args, &block)
49
+ end
50
+
51
+ def may?(user, path, *args)
52
+ query_action(:may?, user, path, *args)
53
+ end
54
+
55
+ def may!(user, path, *args)
56
+ query_action(:may!, user, path, *args)
57
+ end
58
+
59
+ def role(role_name, options = {})
60
+ role_name = role_name.to_s
61
+ role_name != 'everyone' or raise "Cannot define a role named: #{role_name}"
62
+ @roles_by_name ||= {}
63
+ @roles_by_name[role_name] = Aegis::Role.new(role_name, options)
64
+ end
65
+
66
+ def roles
67
+ @roles_by_name.values.sort
68
+ end
69
+
70
+ def find_role_by_name(name)
71
+ @roles_by_name[name.to_s]
72
+ end
73
+
74
+ def guess_action(resource_name, action_name, map = {})
75
+ compile
76
+ action = nil
77
+ action_name = action_name.to_s
78
+ guess_action_paths(resource_name, action_name, map).detect do |path|
79
+ action = find_action_by_path(path, false)
80
+ end
81
+ handle_missing_action(action)
82
+ end
83
+
84
+ def find_action_by_path(path, handle_missing = true)
85
+ compile
86
+ action = @actions_by_path[path.to_s]
87
+ action = handle_missing_action(action) if handle_missing
88
+ action
89
+ end
90
+
91
+ def app_permissions(option)
92
+ if option.is_a?(Class)
93
+ option
94
+ else
95
+ (option || '::Permissions').constantize
96
+ end
97
+ end
98
+
99
+ def inspect
100
+ compile
101
+ "Permissions(#{@root_resource.inspect})"
102
+ end
103
+
104
+ private
105
+
106
+ def query_action(verb, user, path, *args)
107
+ user = handle_missing_user(user)
108
+ action = find_action_by_path(path)
109
+ action.send(verb, user, *args)
110
+ end
111
+
112
+ def handle_missing_user(possibly_missing_user)
113
+ possibly_missing_user ||= case @missing_user_strategy
114
+ when :error then raise "Cannot check permission without a user"
115
+ when Proc then @missing_user_strategy.call
116
+ end
117
+ end
118
+
119
+ def handle_missing_action(possibly_missing_action)
120
+ possibly_missing_action ||= case @missing_action_strategy
121
+ when :default_permission then Aegis::Action.undefined
122
+ when :allow then Aegis::Action.allow_to_all
123
+ when :deny then Aegis::Action.deny_to_all
124
+ when :error then raise "Undefined Aegis action: #{action}"
125
+ end
126
+ end
127
+
128
+ def guess_action_paths(resource_name, action_name, map)
129
+ if mapped = map[action_name]
130
+ [ mapped.to_s ]
131
+ else
132
+ [ "#{action_name}_#{resource_name.to_s.singularize}",
133
+ "#{action_name}_#{resource_name.to_s.pluralize}" ]
134
+ end
135
+ end
136
+
137
+ def prepare
138
+ unless @parser
139
+ @parser = Aegis::Parser.new
140
+ @missing_user_strategy = :error
141
+ @missing_action_strategy = :default_permission
142
+ @action_aliases = {
143
+ 'new' => 'create',
144
+ 'edit' => 'update'
145
+ }
146
+ end
147
+ end
148
+
149
+ def compile
150
+ unless @root_resource
151
+ prepare
152
+ @root_resource = Aegis::Resource.new(nil, nil, :root, {})
153
+ Aegis::Compiler.compile(@root_resource, @parser.atoms)
154
+ index_actions
155
+ end
156
+ end
157
+
158
+ def index_actions
159
+ @actions_by_path = @root_resource.index_actions_by_path(@action_aliases)
160
+ end
161
+
162
+ end
163
+ end
164
+ end
@@ -0,0 +1,158 @@
1
+ module Aegis
2
+ class Resource
3
+
4
+ attr_reader :parent, :children, :name, :type, :never_takes_object, :actions
5
+
6
+ def initialize(parent, name, type, options)
7
+ @parent = parent
8
+ @children = []
9
+ @name = name
10
+ @type = type
11
+ @actions = initial_actions(options)
12
+ # @never_takes_object = options[:object] == false
13
+ end
14
+
15
+ def inspect
16
+ "Resource(#{{:name => name || 'root', :actions => actions, :children => children, :parent => parent}.inspect})"
17
+ end
18
+
19
+ def find_action_by_name(name)
20
+ name = name.to_s
21
+ @actions.detect { |action| action.name == name }
22
+ end
23
+
24
+ def create_or_update_action(name, create_options, update_options)
25
+ action = nil
26
+ if action = find_action_by_name(name)
27
+ action.update(update_options)
28
+ else
29
+ action = Action.new(name, create_options)
30
+ @actions << action
31
+ end
32
+ action
33
+ end
34
+
35
+ def root?
36
+ type == :root
37
+ end
38
+
39
+ def singleton?
40
+ type == :singleton
41
+ end
42
+
43
+ def collection?
44
+ type == :collection
45
+ end
46
+
47
+ def reading_actions
48
+ actions.reject(&:writing)
49
+ end
50
+
51
+ def writing_actions
52
+ actions.select(&:writing)
53
+ end
54
+
55
+ def action_paths(action, aliases)
56
+ if root?
57
+ action_names_with_aliases(action.name, aliases)
58
+ else
59
+ action_names_with_aliases(action.name, aliases).collect do |action_name|
60
+ build_path(
61
+ action_name,
62
+ parent && parent.path(false),
63
+ action.pluralize_resource ? name.pluralize : name.singularize
64
+ )
65
+ end
66
+ end
67
+ end
68
+
69
+ def action_names_with_aliases(native_name, aliases)
70
+ names = [native_name]
71
+ aliases.each do |key, value|
72
+ names << key if value == native_name
73
+ end
74
+ names
75
+ end
76
+
77
+ def index_actions_by_path(aliases)
78
+ index = {}
79
+ actions.each do |action|
80
+ action_paths(action, aliases).each do |path|
81
+ index[path] = action
82
+ end
83
+ end
84
+ children.each do |child|
85
+ index.merge! child.index_actions_by_path(aliases)
86
+ end
87
+ index
88
+ end
89
+
90
+ def new_action_takes_object?(action_options = {})
91
+ collection? && action_options[:collection] != true # && !never_takes_object
92
+ end
93
+
94
+ def new_action_takes_parent_object?(action_options = {})
95
+ parent && parent.collection? # && !parent.never_takes_object
96
+ end
97
+
98
+ protected
99
+
100
+ def path(pluralize = true)
101
+ parent_path = parent && parent.path(false)
102
+ pluralized_name = name ? (pluralize ? name.pluralize : name.singularize) : nil
103
+ build_path(parent_path, pluralized_name)
104
+ end
105
+
106
+ private
107
+
108
+ def build_path(*args)
109
+ args.select(&:present?).join("_")
110
+ end
111
+
112
+ def filter_actions(actions, options)
113
+ if options[:only]
114
+ actions = actions.select(&action_name_filter(options[:only]))
115
+ elsif options[:except]
116
+ actions = actions.reject(&action_name_filter(options[:except]))
117
+ end
118
+ actions
119
+ end
120
+
121
+ def action_name_filter(whitelist)
122
+ whitelist = whitelist.collect(&:to_s)
123
+ lambda { |action| whitelist.include?(action.name) }
124
+ end
125
+
126
+ def initial_actions(options)
127
+ send("initial_actions_for_#{type}", options)
128
+ end
129
+
130
+ def initial_actions_for_collection(options = {})
131
+ filter_actions([
132
+ Aegis::Action.index(initial_action_options),
133
+ Aegis::Action.show(initial_action_options),
134
+ Aegis::Action.update(initial_action_options),
135
+ Aegis::Action.create(initial_action_options),
136
+ Aegis::Action.destroy(initial_action_options),
137
+ ], options)
138
+ end
139
+
140
+ def initial_actions_for_singleton(options = {})
141
+ filter_actions([
142
+ Aegis::Action.show(initial_action_options(:takes_object => false)),
143
+ Aegis::Action.update(initial_action_options(:takes_object => false)),
144
+ Aegis::Action.create(initial_action_options(:takes_object => false)),
145
+ Aegis::Action.destroy(initial_action_options(:takes_object => false))
146
+ ], options)
147
+ end
148
+
149
+ def initial_action_options(options = {})
150
+ { :takes_parent_object => new_action_takes_parent_object? }.merge(options)
151
+ end
152
+
153
+ def initial_actions_for_root(options = {})
154
+ []
155
+ end
156
+
157
+ end
158
+ end
data/lib/aegis/role.rb CHANGED
@@ -1,55 +1,25 @@
1
- module Aegis
2
- class Role
3
-
4
- attr_reader :name, :default_permission
5
-
6
- # permissions is a hash like: permissions[:edit_user] = lambda { |user| ... }
7
- def initialize(name, permissions, options)
8
- @name = name
9
- @permissions = permissions
10
- @default_permission = options[:default_permission] == :allow ? :allow : :deny
11
- freeze
12
- end
13
-
14
- def allow_by_default?
15
- @default_permission == :allow
16
- end
17
-
18
- def may?(permission, *args)
19
- # puts "may? #{permission}, #{args}"
20
- @permissions.may?(self, permission, *args)
21
- end
22
-
23
- def <=>(other)
24
- name.to_s <=> other.name.to_s
25
- end
26
-
27
- def to_s
28
- name.to_s.humanize
29
- end
30
-
31
- def id
32
- name.to_s
33
- end
34
-
35
- private
36
-
37
- def method_missing(symb, *args)
38
- method_name = symb.to_s
39
- if method_name =~ /^may_(.+)(\?|\!)$/
40
- permission, severity = $1, $2
41
- permission = Aegis::Normalization.normalize_permission(permission)
42
- may = may?(permission, *args)
43
- if severity == '!' && !may
44
- raise PermissionError, "Access denied: #{permission}"
45
- else
46
- may
47
- end
48
- else
49
- super
50
- end
51
- end
52
-
53
-
54
- end
55
- end
1
+ module Aegis
2
+ class Role
3
+
4
+ attr_reader :name, :default_permission
5
+
6
+ def initialize(name, options)
7
+ @name = name
8
+ @default_permission = options[:default_permission] == :allow ? :allow : :deny
9
+ freeze
10
+ end
11
+
12
+ def may_by_default?
13
+ @default_permission == :allow
14
+ end
15
+
16
+ def <=>(other)
17
+ name <=> other.name
18
+ end
19
+
20
+ def to_s
21
+ name.to_s.humanize
22
+ end
23
+
24
+ end
25
+ end