eaco 0.5.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 (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