eaco 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|