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
@@ -0,0 +1,10 @@
|
|
1
|
+
Feature: Rails integration
|
2
|
+
The framework should play nice with the most recent major Rails version
|
3
|
+
|
4
|
+
Background:
|
5
|
+
Given I am connected to the database
|
6
|
+
And I have a schema defined
|
7
|
+
|
8
|
+
Scenario:
|
9
|
+
When I authorize the Document model
|
10
|
+
Then I should be able to set an ACL on it
|
@@ -0,0 +1,15 @@
|
|
1
|
+
When(/I authorize the (\w+) model/) do |model_name|
|
2
|
+
@model = Eaco::Cucumber::ActiveRecord.const_get(model_name)
|
3
|
+
|
4
|
+
Eaco::DSL.authorize @model, using: :pg_jsonb
|
5
|
+
end
|
6
|
+
|
7
|
+
Then(/I should be able to set an ACL on it/) do
|
8
|
+
instance = @model.new
|
9
|
+
|
10
|
+
instance.acl = {foo: :bar}
|
11
|
+
instance.save!
|
12
|
+
instance = @model.find(instance.id)
|
13
|
+
|
14
|
+
instance.acl == {foo: :bar} && instance.acl.class.kind_of?(Eaco::ACL)
|
15
|
+
end
|
data/lib/eaco.rb
ADDED
@@ -0,0 +1,93 @@
|
|
1
|
+
require 'eaco/error'
|
2
|
+
require 'eaco/version'
|
3
|
+
|
4
|
+
if defined? Rails
|
5
|
+
require 'eaco/railtie'
|
6
|
+
end
|
7
|
+
|
8
|
+
require 'pathname'
|
9
|
+
|
10
|
+
############################################################################
|
11
|
+
#
|
12
|
+
# Welcome to Eaco!
|
13
|
+
#
|
14
|
+
# Eaco is a full-fledged authorization framework for Ruby that allows you to
|
15
|
+
# describe which actions are allowed on your resources, how to identify your
|
16
|
+
# users as having a particular privilege and which privileges are granted to
|
17
|
+
# a specific resource through the usage of ACLs.
|
18
|
+
#
|
19
|
+
module Eaco
|
20
|
+
autoload :ACL, 'eaco/acl'
|
21
|
+
autoload :Actor, 'eaco/actor'
|
22
|
+
autoload :Adapters, 'eaco/adapters'
|
23
|
+
autoload :DSL, 'eaco/dsl'
|
24
|
+
autoload :Designator, 'eaco/designator'
|
25
|
+
autoload :Resource, 'eaco/resource'
|
26
|
+
|
27
|
+
# The location of the default rules file
|
28
|
+
DEFAULT_RULES = Pathname('./config/authorization.rb')
|
29
|
+
|
30
|
+
##
|
31
|
+
# Parses and evaluates the authorization rules from the {DEFAULT_RULES}.
|
32
|
+
#
|
33
|
+
# The authorization rules define all the authorization framework behaviour
|
34
|
+
# through the {DSL}
|
35
|
+
#
|
36
|
+
# @return (see .eval!)
|
37
|
+
#
|
38
|
+
def self.parse_default_rules_file!
|
39
|
+
parse_rules! DEFAULT_RULES
|
40
|
+
end
|
41
|
+
|
42
|
+
##
|
43
|
+
# Parses the given +rules+ file.
|
44
|
+
#
|
45
|
+
# @param rules [Pathname]
|
46
|
+
#
|
47
|
+
# @return (see .eval!)
|
48
|
+
#
|
49
|
+
# @raise [Malformed] if the +rules+ file does not exist.
|
50
|
+
#
|
51
|
+
def self.parse_rules!(rules)
|
52
|
+
unless rules.exist?
|
53
|
+
path = rules.realpath rescue rules.to_s
|
54
|
+
raise Malformed, "Please create #{path} with Eaco authorization rules"
|
55
|
+
end
|
56
|
+
|
57
|
+
eval! rules.read, rules.realpath.to_s
|
58
|
+
end
|
59
|
+
|
60
|
+
##
|
61
|
+
# Evaluates the given authorization rules +source+, orignally found on
|
62
|
+
# +path+.
|
63
|
+
#
|
64
|
+
# @param source [String] {DSL} source code
|
65
|
+
# @param path [String] Source code origin, for better backtraces.
|
66
|
+
#
|
67
|
+
# @return true
|
68
|
+
#
|
69
|
+
# @raise [Error] if something goes wrong while evaluating the DSL.
|
70
|
+
#
|
71
|
+
# @see DSL
|
72
|
+
#
|
73
|
+
def self.eval!(source, path)
|
74
|
+
DSL.send :eval, source, nil, path, 1
|
75
|
+
|
76
|
+
true
|
77
|
+
rescue => e
|
78
|
+
raise Error, <<-EOF
|
79
|
+
|
80
|
+
=== EACO === Error while evaluating rules
|
81
|
+
|
82
|
+
#{e.message}
|
83
|
+
|
84
|
+
+--------- -- -
|
85
|
+
| #{e.backtrace.join("\n | ")}
|
86
|
+
+-
|
87
|
+
|
88
|
+
=== EACO ===
|
89
|
+
|
90
|
+
EOF
|
91
|
+
end
|
92
|
+
|
93
|
+
end
|
data/lib/eaco/acl.rb
ADDED
@@ -0,0 +1,206 @@
|
|
1
|
+
module Eaco
|
2
|
+
|
3
|
+
##
|
4
|
+
# An ACL is an Hash whose keys are Designator string representations and
|
5
|
+
# values are the role symbols defined in the Resource permissions
|
6
|
+
# configuration.
|
7
|
+
#
|
8
|
+
# Example:
|
9
|
+
#
|
10
|
+
# authorize Document do
|
11
|
+
# roles :reader, :editor
|
12
|
+
# end
|
13
|
+
#
|
14
|
+
# @see Actor
|
15
|
+
# @see Resource
|
16
|
+
#
|
17
|
+
class ACL < Hash
|
18
|
+
|
19
|
+
##
|
20
|
+
# Builds a new ACL object from the given Hash representation with strings
|
21
|
+
# as keys and values.
|
22
|
+
#
|
23
|
+
# @param definition [Hash] the ACL hash
|
24
|
+
#
|
25
|
+
# @return [ACL] this ACL
|
26
|
+
#
|
27
|
+
def initialize(definition = {})
|
28
|
+
definition.each do |designator, role|
|
29
|
+
self[designator] = role.intern
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
##
|
34
|
+
# Gives the given Designator access as the given +role+.
|
35
|
+
#
|
36
|
+
# @param role [Symbol] the role to grant
|
37
|
+
# @param designator [Variadic] (see {#identify})
|
38
|
+
#
|
39
|
+
# @return [ACL] this ACL
|
40
|
+
#
|
41
|
+
def add(role, *designator)
|
42
|
+
identify(*designator).each do |key|
|
43
|
+
self[key] = role
|
44
|
+
end
|
45
|
+
|
46
|
+
self
|
47
|
+
end
|
48
|
+
|
49
|
+
##
|
50
|
+
# Removes access from the given Designator.
|
51
|
+
#
|
52
|
+
# @param designator [Variadic] (see {#identify})
|
53
|
+
#
|
54
|
+
# @return [ACL] this ACL
|
55
|
+
#
|
56
|
+
# @see Designator
|
57
|
+
#
|
58
|
+
def del(*designator)
|
59
|
+
identify(*designator).each do |key|
|
60
|
+
self.delete(key)
|
61
|
+
end
|
62
|
+
|
63
|
+
self
|
64
|
+
end
|
65
|
+
|
66
|
+
##
|
67
|
+
# @param name [Symbol] The role name
|
68
|
+
#
|
69
|
+
# @return [Set] A set of Designators having the given +role+.
|
70
|
+
#
|
71
|
+
# @see Designator
|
72
|
+
# @see Resource
|
73
|
+
#
|
74
|
+
def find_by_role(name)
|
75
|
+
self.inject(Set.new) do |ret, (designator, role)|
|
76
|
+
ret.tap { ret.add Designator.parse(designator) if role == name }
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
##
|
81
|
+
# @return [Set] all Designators in the ACL, regardless of their role.
|
82
|
+
#
|
83
|
+
def all
|
84
|
+
self.inject(Set.new) do |ret, (designator,_)|
|
85
|
+
ret.add Designator.parse(designator)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
##
|
90
|
+
# Gets a map of Actors in the ACL having the given +role+.
|
91
|
+
#
|
92
|
+
# This is a useful starting point for an Enterprise summary page of who is
|
93
|
+
# granted to access a resource. Given that actor resolution is dynamic and
|
94
|
+
# handled by the application's Designators implementation, you can rely on
|
95
|
+
# your internal organigram APIs to resolve actual people out of positions,
|
96
|
+
# groups, department of assignment, etc.
|
97
|
+
#
|
98
|
+
# @param name [Symbol] The role name
|
99
|
+
#
|
100
|
+
# @return [Hash] keyed by designator with Set of Actors as values
|
101
|
+
#
|
102
|
+
# @see Actor
|
103
|
+
# @see Resource
|
104
|
+
#
|
105
|
+
def designators_map_for_role(name)
|
106
|
+
find_by_role(name).inject({}) do |ret, designator|
|
107
|
+
actors = designator.resolve
|
108
|
+
|
109
|
+
ret.tap do
|
110
|
+
ret[designator] ||= Set.new
|
111
|
+
ret[designator].merge Array.new(actors)
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
##
|
117
|
+
# @param name [Symbol] the role name
|
118
|
+
#
|
119
|
+
# @return [Set] Actors having the given +role+.
|
120
|
+
#
|
121
|
+
# @see Actor
|
122
|
+
# @see Resource
|
123
|
+
#
|
124
|
+
def actors_by_role(name)
|
125
|
+
find_by_role(name).inject(Set.new) do |set, designator|
|
126
|
+
set |= Array.new(designator.resolve)
|
127
|
+
end.to_a
|
128
|
+
end
|
129
|
+
|
130
|
+
##
|
131
|
+
# Pretty prints this ACL in your console.
|
132
|
+
#
|
133
|
+
def inspect
|
134
|
+
"#<#{self.class.name}: #{super}>"
|
135
|
+
end
|
136
|
+
alias pretty_print_inspect inspect
|
137
|
+
|
138
|
+
##
|
139
|
+
# Pretty print for +pry+.
|
140
|
+
#
|
141
|
+
def pretty_inspect
|
142
|
+
"#{self.class.name}\n#{super}"
|
143
|
+
end
|
144
|
+
|
145
|
+
protected
|
146
|
+
|
147
|
+
##
|
148
|
+
# There are three ways of specifying designators:
|
149
|
+
#
|
150
|
+
# * Passing an +Designator+ instance obtained from somewhere else:
|
151
|
+
#
|
152
|
+
# >> designator
|
153
|
+
# => #<Designator(User) value:42>
|
154
|
+
#
|
155
|
+
# >> resource.acl.add :reader, designator
|
156
|
+
# => #<Resource::ACL {"user:42"=>:reader}>
|
157
|
+
#
|
158
|
+
# * Passing a designator type and an unique ID valid in the designator's
|
159
|
+
# namespace:
|
160
|
+
#
|
161
|
+
# >> resource.acl.add :reader, :user, 42
|
162
|
+
# => #<Resource::ACL {"user:42"=>:reader}>
|
163
|
+
#
|
164
|
+
# * Passing a designator type and an Actor instance, will add all
|
165
|
+
# designators of the given type owned by the Actor.
|
166
|
+
#
|
167
|
+
# >> actor
|
168
|
+
# => #<User id:42 name:"Ethan Siegel">
|
169
|
+
#
|
170
|
+
# >> actor.designators
|
171
|
+
# => #<Set:{
|
172
|
+
# | #<Designator(User) value:42>,
|
173
|
+
# | #<Designator(Group) value:"astrophysicists">,
|
174
|
+
# | #<Designator(Group) value:"medium bloggers">
|
175
|
+
# | }>
|
176
|
+
#
|
177
|
+
# >> resource.acl.add :editor, :group, actor
|
178
|
+
# => #<Resource::ACL {
|
179
|
+
# | "group:astrophysicists"=>:editor,
|
180
|
+
# | "group:medium bloggers"=>:editor
|
181
|
+
# | }
|
182
|
+
#
|
183
|
+
# @param designator [Designator] the designator to grant
|
184
|
+
# @param actor_or_id [Actor] or [String] the actor
|
185
|
+
#
|
186
|
+
def identify(designator, actor_or_id = nil)
|
187
|
+
if designator.is_a?(Eaco::Designator)
|
188
|
+
[designator]
|
189
|
+
|
190
|
+
elsif designator && actor_or_id.respond_to?(:designators)
|
191
|
+
actor_or_id.designators.select {|d| d.type == designator}
|
192
|
+
|
193
|
+
elsif designator.is_a?(Symbol)
|
194
|
+
[Eaco::Designator.make(designator, actor_or_id)]
|
195
|
+
|
196
|
+
else
|
197
|
+
raise Error, <<-EOF
|
198
|
+
Cannot infer designator
|
199
|
+
from #{designator.inspect}
|
200
|
+
and #{actor_or_id.inspect}
|
201
|
+
EOF
|
202
|
+
end
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
end
|
data/lib/eaco/actor.rb
ADDED
@@ -0,0 +1,86 @@
|
|
1
|
+
module Eaco
|
2
|
+
|
3
|
+
##
|
4
|
+
# An Actor is an entity whose access to Resources is discretionary,
|
5
|
+
# depending on the Role this actor has in the ACL.
|
6
|
+
#
|
7
|
+
# The role of this +Actor+ is calculated from the +Designator+ that
|
8
|
+
# the actor instance has, and the +ACL+ instance attached to the
|
9
|
+
# +Resource+.
|
10
|
+
#
|
11
|
+
# @see ACL
|
12
|
+
# @see Resource
|
13
|
+
# @see DSL::Actor
|
14
|
+
#
|
15
|
+
module Actor
|
16
|
+
|
17
|
+
# @private
|
18
|
+
def self.included(base)
|
19
|
+
base.extend ClassMethods
|
20
|
+
end
|
21
|
+
|
22
|
+
##
|
23
|
+
# Singleton methods added to Actor classes.
|
24
|
+
#
|
25
|
+
module ClassMethods
|
26
|
+
##
|
27
|
+
# The designators implementations defined for this Actor as an Hash
|
28
|
+
# keyed by designator type symbol and with the concrete Designator
|
29
|
+
# implementations as values.
|
30
|
+
#
|
31
|
+
# @see DSL::Actor#initialize
|
32
|
+
#
|
33
|
+
def designators
|
34
|
+
end
|
35
|
+
|
36
|
+
##
|
37
|
+
# The logic that evaluates whether an Actor instance is an admin.
|
38
|
+
#
|
39
|
+
# @see DSL::Actor#initialize
|
40
|
+
#
|
41
|
+
def admin_logic
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
##
|
46
|
+
# @return [Set] the designators granted to this Actor.
|
47
|
+
#
|
48
|
+
# @see Designator
|
49
|
+
#
|
50
|
+
def designators
|
51
|
+
@_designators ||= Set.new.tap do |ret|
|
52
|
+
self.class.designators.each do |_, designator|
|
53
|
+
ret.merge designator.harvest(self)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
##
|
59
|
+
# Checks whether this Actor fulfills the admin logic.
|
60
|
+
#
|
61
|
+
# This logic is called by +Resource+ Adapters' +accessible_by+, that
|
62
|
+
# returns the full collection, and by {Resource#allows?}, that bypassess
|
63
|
+
# access checks always returning true.
|
64
|
+
#
|
65
|
+
# @return [Boolean] True or False if admin logic is defined, nil if not.
|
66
|
+
#
|
67
|
+
def is_admin?
|
68
|
+
return unless self.class.admin_logic
|
69
|
+
|
70
|
+
instance_exec(self, &self.class.admin_logic)
|
71
|
+
end
|
72
|
+
|
73
|
+
##
|
74
|
+
# Checks wether the given Resource allows this Actor to perform the given action.
|
75
|
+
#
|
76
|
+
# @param action [Symbol] a valid action for this Resource (see {DSL::Resource})
|
77
|
+
# @param resource [Resource] an authorized resource
|
78
|
+
#
|
79
|
+
# @see Resource
|
80
|
+
#
|
81
|
+
def can?(action, resource)
|
82
|
+
resource.allows?(action, self)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
end
|