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.
- checksums.yaml +7 -0
- data/.gitignore +21 -0
- data/.rspec +2 -0
- data/.travis.yml +18 -0
- data/.yardopts +1 -0
- data/Appraisals +22 -0
- data/Gemfile +4 -0
- data/Guardfile +34 -0
- data/LICENSE.txt +23 -0
- data/README.md +225 -0
- data/Rakefile +26 -0
- data/eaco.gemspec +27 -0
- data/features/active_record.example.yml +8 -0
- data/features/active_record.travis.yml +7 -0
- data/features/rails_integration.feature +10 -0
- data/features/step_definitions/database.rb +7 -0
- data/features/step_definitions/resource_authorization.rb +15 -0
- data/features/support/env.rb +9 -0
- data/gemfiles/rails_3.2.gemfile +9 -0
- data/gemfiles/rails_4.0.gemfile +8 -0
- data/gemfiles/rails_4.1.gemfile +8 -0
- data/gemfiles/rails_4.2.gemfile +8 -0
- data/lib/eaco.rb +93 -0
- data/lib/eaco/acl.rb +206 -0
- data/lib/eaco/actor.rb +86 -0
- data/lib/eaco/adapters.rb +14 -0
- data/lib/eaco/adapters/active_record.rb +70 -0
- data/lib/eaco/adapters/active_record/compatibility.rb +83 -0
- data/lib/eaco/adapters/active_record/compatibility/v32.rb +27 -0
- data/lib/eaco/adapters/active_record/compatibility/v40.rb +59 -0
- data/lib/eaco/adapters/active_record/compatibility/v41.rb +16 -0
- data/lib/eaco/adapters/active_record/compatibility/v42.rb +17 -0
- data/lib/eaco/adapters/active_record/postgres_jsonb.rb +36 -0
- data/lib/eaco/adapters/couchrest_model.rb +37 -0
- data/lib/eaco/adapters/couchrest_model/couchdb_lucene.rb +71 -0
- data/lib/eaco/controller.rb +158 -0
- data/lib/eaco/cucumber.rb +11 -0
- data/lib/eaco/cucumber/active_record.rb +163 -0
- data/lib/eaco/cucumber/active_record/department.rb +19 -0
- data/lib/eaco/cucumber/active_record/document.rb +18 -0
- data/lib/eaco/cucumber/active_record/position.rb +21 -0
- data/lib/eaco/cucumber/active_record/schema.rb +36 -0
- data/lib/eaco/cucumber/active_record/user.rb +24 -0
- data/lib/eaco/cucumber/world.rb +136 -0
- data/lib/eaco/designator.rb +264 -0
- data/lib/eaco/dsl.rb +40 -0
- data/lib/eaco/dsl/acl.rb +163 -0
- data/lib/eaco/dsl/actor.rb +139 -0
- data/lib/eaco/dsl/actor/designators.rb +110 -0
- data/lib/eaco/dsl/base.rb +52 -0
- data/lib/eaco/dsl/resource.rb +129 -0
- data/lib/eaco/dsl/resource/permissions.rb +131 -0
- data/lib/eaco/error.rb +36 -0
- data/lib/eaco/railtie.rb +46 -0
- data/lib/eaco/rake.rb +10 -0
- data/lib/eaco/rake/default_task.rb +164 -0
- data/lib/eaco/resource.rb +234 -0
- data/lib/eaco/version.rb +7 -0
- data/spec/eaco/acl_spec.rb +147 -0
- data/spec/eaco/actor_spec.rb +13 -0
- data/spec/eaco/adapters/active_record/postgres_jsonb_spec.rb +9 -0
- data/spec/eaco/adapters/active_record_spec.rb +13 -0
- data/spec/eaco/adapters/couchrest_model/couchdb_lucene_spec.rb +9 -0
- data/spec/eaco/adapters/couchrest_model_spec.rb +9 -0
- data/spec/eaco/controller_spec.rb +12 -0
- data/spec/eaco/designator_spec.rb +25 -0
- data/spec/eaco/dsl/acl_spec.rb +9 -0
- data/spec/eaco/dsl/actor/designators_spec.rb +7 -0
- data/spec/eaco/dsl/actor_spec.rb +15 -0
- data/spec/eaco/dsl/resource/permissions_spec.rb +7 -0
- data/spec/eaco/dsl/resource_spec.rb +17 -0
- data/spec/eaco/error_spec.rb +9 -0
- data/spec/eaco/resource_spec.rb +31 -0
- data/spec/eaco_spec.rb +49 -0
- data/spec/spec_helper.rb +71 -0
- 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
|
data/lib/eaco/railtie.rb
ADDED
@@ -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,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
|