eaco 0.5.0

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