eaco 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (76) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +21 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +18 -0
  5. data/.yardopts +1 -0
  6. data/Appraisals +22 -0
  7. data/Gemfile +4 -0
  8. data/Guardfile +34 -0
  9. data/LICENSE.txt +23 -0
  10. data/README.md +225 -0
  11. data/Rakefile +26 -0
  12. data/eaco.gemspec +27 -0
  13. data/features/active_record.example.yml +8 -0
  14. data/features/active_record.travis.yml +7 -0
  15. data/features/rails_integration.feature +10 -0
  16. data/features/step_definitions/database.rb +7 -0
  17. data/features/step_definitions/resource_authorization.rb +15 -0
  18. data/features/support/env.rb +9 -0
  19. data/gemfiles/rails_3.2.gemfile +9 -0
  20. data/gemfiles/rails_4.0.gemfile +8 -0
  21. data/gemfiles/rails_4.1.gemfile +8 -0
  22. data/gemfiles/rails_4.2.gemfile +8 -0
  23. data/lib/eaco.rb +93 -0
  24. data/lib/eaco/acl.rb +206 -0
  25. data/lib/eaco/actor.rb +86 -0
  26. data/lib/eaco/adapters.rb +14 -0
  27. data/lib/eaco/adapters/active_record.rb +70 -0
  28. data/lib/eaco/adapters/active_record/compatibility.rb +83 -0
  29. data/lib/eaco/adapters/active_record/compatibility/v32.rb +27 -0
  30. data/lib/eaco/adapters/active_record/compatibility/v40.rb +59 -0
  31. data/lib/eaco/adapters/active_record/compatibility/v41.rb +16 -0
  32. data/lib/eaco/adapters/active_record/compatibility/v42.rb +17 -0
  33. data/lib/eaco/adapters/active_record/postgres_jsonb.rb +36 -0
  34. data/lib/eaco/adapters/couchrest_model.rb +37 -0
  35. data/lib/eaco/adapters/couchrest_model/couchdb_lucene.rb +71 -0
  36. data/lib/eaco/controller.rb +158 -0
  37. data/lib/eaco/cucumber.rb +11 -0
  38. data/lib/eaco/cucumber/active_record.rb +163 -0
  39. data/lib/eaco/cucumber/active_record/department.rb +19 -0
  40. data/lib/eaco/cucumber/active_record/document.rb +18 -0
  41. data/lib/eaco/cucumber/active_record/position.rb +21 -0
  42. data/lib/eaco/cucumber/active_record/schema.rb +36 -0
  43. data/lib/eaco/cucumber/active_record/user.rb +24 -0
  44. data/lib/eaco/cucumber/world.rb +136 -0
  45. data/lib/eaco/designator.rb +264 -0
  46. data/lib/eaco/dsl.rb +40 -0
  47. data/lib/eaco/dsl/acl.rb +163 -0
  48. data/lib/eaco/dsl/actor.rb +139 -0
  49. data/lib/eaco/dsl/actor/designators.rb +110 -0
  50. data/lib/eaco/dsl/base.rb +52 -0
  51. data/lib/eaco/dsl/resource.rb +129 -0
  52. data/lib/eaco/dsl/resource/permissions.rb +131 -0
  53. data/lib/eaco/error.rb +36 -0
  54. data/lib/eaco/railtie.rb +46 -0
  55. data/lib/eaco/rake.rb +10 -0
  56. data/lib/eaco/rake/default_task.rb +164 -0
  57. data/lib/eaco/resource.rb +234 -0
  58. data/lib/eaco/version.rb +7 -0
  59. data/spec/eaco/acl_spec.rb +147 -0
  60. data/spec/eaco/actor_spec.rb +13 -0
  61. data/spec/eaco/adapters/active_record/postgres_jsonb_spec.rb +9 -0
  62. data/spec/eaco/adapters/active_record_spec.rb +13 -0
  63. data/spec/eaco/adapters/couchrest_model/couchdb_lucene_spec.rb +9 -0
  64. data/spec/eaco/adapters/couchrest_model_spec.rb +9 -0
  65. data/spec/eaco/controller_spec.rb +12 -0
  66. data/spec/eaco/designator_spec.rb +25 -0
  67. data/spec/eaco/dsl/acl_spec.rb +9 -0
  68. data/spec/eaco/dsl/actor/designators_spec.rb +7 -0
  69. data/spec/eaco/dsl/actor_spec.rb +15 -0
  70. data/spec/eaco/dsl/resource/permissions_spec.rb +7 -0
  71. data/spec/eaco/dsl/resource_spec.rb +17 -0
  72. data/spec/eaco/error_spec.rb +9 -0
  73. data/spec/eaco/resource_spec.rb +31 -0
  74. data/spec/eaco_spec.rb +49 -0
  75. data/spec/spec_helper.rb +71 -0
  76. metadata +296 -0
data/lib/eaco/error.rb ADDED
@@ -0,0 +1,36 @@
1
+ module Eaco
2
+
3
+ ##
4
+ # An Eaco Runtime Error.
5
+ #
6
+ class Error < StandardError
7
+ # As we make use of heredoc for long error messages, squeeze subsequent
8
+ # spaces and remove newlines.
9
+ #
10
+ def initialize(message)
11
+ unless message =~ %r{EACO.+Error}
12
+ message = message.squeeze(' ').gsub("\n", '')
13
+ end
14
+
15
+ super message
16
+ end
17
+ end
18
+
19
+ # Raised when an Actor attempts an unauthorized access to a controller
20
+ # action that deals with a protected Resource.
21
+ #
22
+ # @see Actor
23
+ # @see Resource
24
+ # @see Controller
25
+ #
26
+ class Forbidden < Error; end
27
+
28
+ # Represents a configuration error of the Eaco framework, whether wrong
29
+ # options or wrong usage of the DSL, or invalid storage options for the
30
+ # ACL objects and authorized collection extraction strategy.
31
+ #
32
+ # @see DSL
33
+ #
34
+ class Malformed < Error; end
35
+
36
+ end
@@ -0,0 +1,46 @@
1
+ module Eaco
2
+
3
+ autoload :Controller, 'eaco/controller'
4
+
5
+ ##
6
+ # Initializer for Rails 3 and up.
7
+ #
8
+ # * Parses the configuration rules upon startup and, in development, after a
9
+ # console +reload!+.
10
+ #
11
+ # * Installs {Controller} authorization filters in +ActionController::Base+.
12
+ #
13
+ class Railtie < ::Rails::Railtie
14
+
15
+ ##
16
+ # Calls {Eaco.parse_default_rules_file!}
17
+ #
18
+ # @!method parse_rules
19
+ #
20
+ initializer 'eaco.parse_rules' do
21
+ Eaco.parse_default_rules_file!
22
+
23
+ unless Rails.configuration.cache_classes
24
+ ActionDispatch::Reloader.to_prepare do
25
+ Eaco.parse_default_rules_file!
26
+ end
27
+ end
28
+ end
29
+
30
+ ##
31
+ # Adds {Controller} to +ActionController::Base+
32
+ #
33
+ # @!method install_controller_runtime
34
+ #
35
+ initializer 'eaco.install_controller_runtime' do
36
+ ActiveSupport.on_load :action_controller do
37
+
38
+ ActionController::Base.instance_eval do
39
+ include Eaco::Controller
40
+ end
41
+
42
+ end
43
+ end
44
+ end
45
+
46
+ end
data/lib/eaco/rake.rb ADDED
@@ -0,0 +1,10 @@
1
+ module Eaco
2
+
3
+ ##
4
+ # Namespace to all rake-related functionality.
5
+ #
6
+ module Rake
7
+ autoload :DefaultTask, 'eaco/rake/default_task.rb'
8
+ end
9
+
10
+ end
@@ -0,0 +1,164 @@
1
+ module Eaco
2
+ module Rake
3
+
4
+ ##
5
+ # Defines the default Eaco rake task. It runs tests and generates the docs.
6
+ #
7
+ # Usage:
8
+ #
9
+ # Eaco::Rake::DefaultTask.new
10
+ #
11
+ class DefaultTask
12
+ include ::Rake::DSL if defined?(::Rake::DSL)
13
+
14
+ ##
15
+ # Main +Eaco+ rake task.
16
+ #
17
+ # If running appraisals or running within Travis CI, run all specs and
18
+ # cucumber features.
19
+ #
20
+ # The concept here is to prepare the environment with the gems set we
21
+ # are testing against, and this is done by Appraisals and Travis, albeit
22
+ # in a different way. The first uses the +Appraisals+ file, the second
23
+ # instead relies on the +.travis.yml+ configuration.
24
+ #
25
+ # Documentation is generated at the end, once if running locally, but
26
+ # multiple times, once for each appraisal, on Travis.
27
+ #
28
+ def initialize
29
+ if running_appraisals?
30
+ task :default do
31
+ run_specs
32
+ run_cucumber
33
+ end
34
+
35
+ elsif running_in_travis?
36
+ task :default do
37
+ run_specs
38
+ run_cucumber
39
+ generate_documentation
40
+ end
41
+
42
+ else
43
+ desc 'Appraises specs and cucumber, generates documentation'
44
+ task :default do
45
+ run_appraisals
46
+ generate_documentation
47
+ end
48
+
49
+ end
50
+ end
51
+
52
+ ##
53
+ # Runs all appraisals (see +Appraisals+ in the source root)
54
+ # against the defined Rails version and generates the source
55
+ # documentation using Yard.
56
+ #
57
+ # Runs them in a subprocess as the appraisals gem makes use
58
+ # of fork/exec hijacking the process session root.
59
+ #
60
+ # @raise [RuntimeError] if the appraisals run fails.
61
+ #
62
+ # @return [void]
63
+ #
64
+ def run_appraisals
65
+ croak 'Running all appraisals'
66
+
67
+ pid = fork { invoke :appraisal }
68
+ _, status = Process.wait2(pid)
69
+ unless status.exitstatus == 0
70
+ bail "Appraisals failed with status #{status.exitstatus}"
71
+ end
72
+ end
73
+
74
+ ##
75
+ # Generate the documentation using +Yard+.
76
+ #
77
+ def generate_documentation
78
+ croak 'Generating documentation'
79
+
80
+ invoke :yard
81
+ end
82
+
83
+ ##
84
+ # Runs all specs under the +spec/+ directory
85
+ #
86
+ # @return [void]
87
+ #
88
+ def run_specs
89
+ croak 'Running specs'
90
+
91
+ invoke :spec
92
+ end
93
+
94
+ ##
95
+ # Runs all cucumber features in the +features/+ directory
96
+ #
97
+ # @return [void]
98
+ #
99
+ def run_cucumber
100
+ croak 'Evaluating cucumber features'
101
+
102
+ invoke :cucumber
103
+ end
104
+
105
+ private
106
+
107
+ ##
108
+ # Invokes the given rake task.
109
+ #
110
+ # @param task [Symbol] the task to invoke.
111
+ # @return [void]
112
+ #
113
+ def invoke(task)
114
+ ::Rake::Task[task].invoke
115
+ end
116
+
117
+ ##
118
+ # Fancily logs the given +msg+ to +$stderr+.
119
+ #
120
+ # @param msg [String] the message to bail out.
121
+ #
122
+ # @return [nil]
123
+ #
124
+ def croak(msg)
125
+ $stderr.puts fancy(msg)
126
+ end
127
+
128
+ ##
129
+ # Bails out the given error message.
130
+ #
131
+ # @param msg [String] the message to bail
132
+ # @raise [RuntimeError]
133
+ #
134
+ def bail(msg)
135
+ raise RuntimeError, fancy(msg)
136
+ end
137
+
138
+ ##
139
+ # Makes +msg+ fancy.
140
+ #
141
+ # @param msg [String]
142
+ # @return [String]
143
+ #
144
+ def fancy(msg)
145
+ ">>>\n>>> EACO: #{msg}\n>>>\n"
146
+ end
147
+
148
+ ##
149
+ # @return [Boolean] Are we running appraisals?
150
+ #
151
+ def running_appraisals?
152
+ ENV["APPRAISAL_INITIALIZED"]
153
+ end
154
+
155
+ ##
156
+ # @return [Boolean] Are we running on Travis CI?
157
+ #
158
+ def running_in_travis?
159
+ ENV["TRAVIS"]
160
+ end
161
+ end
162
+
163
+ end
164
+ end
@@ -0,0 +1,234 @@
1
+ module Eaco
2
+
3
+ ##
4
+ # A Resource is an object that can be authorized. It has an {ACL}, that
5
+ # defines the access levels of {Designator}s. {Actor}s have many designators
6
+ # and the highest priority ones that matches the {ACL} yields the access
7
+ # level of the {Actor} to this {Resource}.
8
+ #
9
+ # If there is no match between the {Actor}'s designators and the {ACL}, then
10
+ # access is denied.
11
+ #
12
+ # Authorized resources are defined through the DSL, see {DSL::Resource}.
13
+ #
14
+ # TODO Negative authorizations
15
+ #
16
+ # @see ACL
17
+ # @see Actor
18
+ # @see Designator
19
+ #
20
+ # @see DSL::Resource
21
+ #
22
+ module Resource
23
+
24
+ # @private
25
+ def self.included(base)
26
+ base.extend ClassMethods
27
+ end
28
+
29
+ ##
30
+ # Singleton methods added to authorized Resources.
31
+ #
32
+ module ClassMethods
33
+ ##
34
+ # @return [Boolean] checks whether the given +role+ is valid in the
35
+ # context of this Resource.
36
+ #
37
+ # @param role [Symbol] role name.
38
+ #
39
+ def role?(role)
40
+ role.to_sym.in?(roles)
41
+ end
42
+
43
+ ##
44
+ # Checks whether the {ACL} and permissions defined on this Resource
45
+ # allow the given +actor+ to perform the given +action+ on it, that
46
+ # depends on the +role+ the user has on the resource, calculated from
47
+ # the {ACL}.
48
+ #
49
+ # @param action [Symbol]
50
+ # @param actor [Actor]
51
+ # @param resource [Resource]
52
+ #
53
+ # @return [Boolean]
54
+ #
55
+ def allows?(action, actor, resource)
56
+ return true if actor.is_admin?
57
+
58
+ role = role_of(actor, resource)
59
+ return false unless role
60
+
61
+ perms = permissions[role]
62
+ return false unless perms
63
+
64
+ perms.include?(action)
65
+ end
66
+
67
+ ##
68
+ # @return [Symbol] the given +actor+ role in the given resource, or +nil+ if no
69
+ # access is granted.
70
+ #
71
+ # @param actor_or_designator [Actor or Designator]
72
+ # @param resource [Resource]
73
+ #
74
+ def role_of(actor_or_designator, resource)
75
+ designators = if actor_or_designator.is_a?(Eaco::Designator)
76
+ [actor_or_designator]
77
+
78
+ elsif actor_or_designator.respond_to?(:designators)
79
+ actor_or_designator.designators
80
+
81
+ else
82
+ raise Error, <<-EOF
83
+ #{__method__} expects #{actor_or_designator.inspect}
84
+ to be a Designator or to `respond_to?(:designators)`
85
+ EOF
86
+ end
87
+
88
+ role_priority = nil
89
+ resource.acl.each do |designator, role|
90
+ if designators.include?(designator)
91
+ priority = roles_priority[role]
92
+ end
93
+
94
+ if priority && (role_priority.nil? || priority < role_priority)
95
+ role_priority = priority
96
+ break if role_priority == 0
97
+ end
98
+ end
99
+
100
+ roles[role_priority] if role_priority
101
+ end
102
+
103
+ ##
104
+ # The permissions defined for each role.
105
+ #
106
+ # @see DSL::Resource#initialize
107
+ #
108
+ def permissions
109
+ end
110
+
111
+ # The defined roles.
112
+ #
113
+ # @see DSL::Resource#initialize
114
+ #
115
+ def roles
116
+ end
117
+
118
+ # Roles' priority map keyed by role symbol.
119
+ #
120
+ # @see DSL::Resource#initialize
121
+ #
122
+ def roles_priority
123
+ end
124
+
125
+ # Role labels map keyed by role symbol
126
+ #
127
+ # @see DSL::Resource#initialize
128
+ #
129
+ def roles_with_labels
130
+ end
131
+ end
132
+
133
+ ##
134
+ # @return [Boolean] whether the given +action+ is allowed to the given +actor+.
135
+ #
136
+ # @param action [Symbol]
137
+ # @param actor [Actor]
138
+ #
139
+ def allows?(action, actor)
140
+ self.class.allows?(action, actor, self)
141
+ end
142
+
143
+ ##
144
+ # @return [Symbol] the role of the given +actor+
145
+ #
146
+ # @param actor [Actor]
147
+ #
148
+ def role_of(actor)
149
+ self.class.role_of(actor, self)
150
+ end
151
+
152
+ ##
153
+ # Grants the given +designator+ access to this Resource as the given +role+.
154
+ #
155
+ # @param role [Symbol]
156
+ # @param designator [Variadic], see {ACL#add}
157
+ #
158
+ # @return [ACL]
159
+ #
160
+ # @see #change_acl
161
+ #
162
+ def grant(role, *designator)
163
+ self.check_role!(role)
164
+
165
+ change_acl {|acl| acl.add(role, *designator) }
166
+ end
167
+
168
+ ##
169
+ # Revokes the given +designator+ access to this Resource.
170
+ #
171
+ # @param designator [Variadic], see {ACL#del}
172
+ #
173
+ # @return [ACL]
174
+ #
175
+ # @see #change_acl
176
+ #
177
+ def revoke(*designator)
178
+ change_acl {|acl| acl.del(*designator) }
179
+ end
180
+
181
+ # Grants the given set of +designators+ access as to this Resource as the
182
+ # given +role+.
183
+ #
184
+ # @param role [Symbol]
185
+ # @param designators [Array] of {Designator}, see {ACL#add}
186
+ #
187
+ # @return [ACL]
188
+ #
189
+ # @see #change_acl
190
+ #
191
+ def batch_grant(role, designators)
192
+ self.check_role!(role)
193
+
194
+ change_acl do |acl|
195
+ designators.each do |designator|
196
+ acl.add(role, designator)
197
+ end
198
+ acl
199
+ end
200
+ end
201
+
202
+ protected
203
+ ##
204
+ # Changes the ACL, calling the persistance setter if it changes.
205
+ #
206
+ # @yield [ACL] the current ACL or a new one if no ACL is set
207
+ #
208
+ # @return [ACL] the new ACL
209
+ #
210
+ def change_acl
211
+ acl = yield self.acl.try(:dup) || self.class.acl.new
212
+
213
+ self.acl = acl unless acl == self.acl
214
+
215
+ return self.acl
216
+ end
217
+
218
+ ##
219
+ # Checks whether the given +role+ is valid for this Resource.
220
+ #
221
+ # @param role [Symbol] the role name.
222
+ #
223
+ # @raise [Eaco::Error] if not valid.
224
+ #
225
+ def check_role!(role)
226
+ unless self.class.role?(role)
227
+ raise Error,
228
+ "The `#{role}' role is not valid for `#{self.class.name}' objects. " \
229
+ "Valid roles are: `#{self.class.roles.join(', ')}'"
230
+ end
231
+ end
232
+ end
233
+
234
+ end