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
@@ -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,7 @@
1
+ Given(/I am connected to the database/) do
2
+ Eaco::Cucumber::ActiveRecord.connect!
3
+ end
4
+
5
+ Given(/I have a schema defined/) do
6
+ Eaco::Cucumber::ActiveRecord.define_schema!
7
+ end
@@ -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
@@ -0,0 +1,9 @@
1
+ require 'bundler/setup'
2
+ require 'byebug'
3
+ require 'eaco'
4
+ require 'eaco/cucumber'
5
+
6
+ # Create a whole new world
7
+ World do
8
+ Eaco::Cucumber::World.new
9
+ end
@@ -0,0 +1,9 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "rails", "~> 3.2.0"
6
+ gem "pg"
7
+ gem "activerecord-postgres-json", :require => false
8
+
9
+ gemspec :path => "../"
@@ -0,0 +1,8 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "rails", "~> 4.0.0"
6
+ gem "pg"
7
+
8
+ gemspec :path => "../"
@@ -0,0 +1,8 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "rails", "~> 4.1.0"
6
+ gem "pg"
7
+
8
+ gemspec :path => "../"
@@ -0,0 +1,8 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "rails", "~> 4.2.0"
6
+ gem "pg"
7
+
8
+ gemspec :path => "../"
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