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,139 @@
1
+ module Eaco
2
+ module DSL
3
+
4
+ ##
5
+ # Parses the Actor DSL, that describes how to harvest {Designator}s from
6
+ # an {Actor} and how to identify it as an +admin+, or superuser.
7
+ #
8
+ # actor User do
9
+ # admin do |user|
10
+ # user.admin?
11
+ # end
12
+ #
13
+ # designators do
14
+ # authenticated from: :class
15
+ # user from: :id
16
+ # group from: :group_ids
17
+ # end
18
+ # end
19
+ #
20
+ class Actor < Base
21
+ autoload :Designators, 'eaco/dsl/actor/designators'
22
+
23
+ ##
24
+ # Initializes an Actor class.
25
+ #
26
+ # @see Eaco::Actor
27
+ #
28
+ def initialize(*)
29
+ super
30
+
31
+ target_eval do
32
+ include Eaco::Actor
33
+
34
+ def designators
35
+ @_designators
36
+ end
37
+
38
+ def admin_logic
39
+ @_admin_logic
40
+ end
41
+ end
42
+ end
43
+
44
+ ##
45
+ # Defines the designators that apply to this {Actor}.
46
+ #
47
+ # Example:
48
+ #
49
+ # actor User do
50
+ # designators do
51
+ # authenticated from: :class
52
+ # user from: :id
53
+ # group from: :group_ids
54
+ # end
55
+ # end
56
+ #
57
+ # {Designator} names are collected using +method_missing+, and are
58
+ # named after the method name. Implementations are looked up in
59
+ # a +Designators+ module in the {Actor}'s class.
60
+ #
61
+ # Each designator implementation is expected to be named after the
62
+ # designator's name, camelized, and inherit from {Eaco::Designator}.
63
+ #
64
+ # TODO all designators share the same namespace. This is due to the
65
+ # fact that designator string representations aren't scoped by the
66
+ # Actor model they belong to. As such when instantiating a designator
67
+ # from +Eaco::Designator.make+ the registry is consulted to find the
68
+ # designator implementation.
69
+ #
70
+ # @see DSL::Actor::Designators
71
+ #
72
+ def designators(&block)
73
+ new_designators = target_eval do
74
+ @_designators = Designators.eval(self, &block).result.freeze
75
+ end
76
+
77
+ Actor.register_designators(new_designators)
78
+ end
79
+
80
+ ##
81
+ # Defines the boolean logic that determines whether an {Actor} is an
82
+ # admin. Usually you'll have an +admin+ method on your model, that you
83
+ # can call from here. Or, feel free to just return +false+ to disable
84
+ # this functionality.
85
+ #
86
+ # Example:
87
+ #
88
+ # actor User do
89
+ # admin do |user|
90
+ # user.admin?
91
+ # end
92
+ # end
93
+ #
94
+ def admin(&block)
95
+ target_eval do
96
+ @admin_logic = block
97
+ end
98
+ end
99
+
100
+ class << self
101
+ ##
102
+ # Looks up the given designator implementation by its +name+.
103
+ #
104
+ # @param name [Symbol] the designator name.
105
+ #
106
+ # @raise [Eaco::Malformed] if the designator is not found.
107
+ #
108
+ # @return [Class]
109
+ #
110
+ def find_designator(name)
111
+ all_designators.fetch(name.intern)
112
+
113
+ rescue KeyError
114
+ raise Malformed, "Designator not found: #{name.inspect}"
115
+ end
116
+
117
+ ##
118
+ # Saves the given designators in the global designators registry.
119
+ #
120
+ # @param new_designators [Hash]
121
+ #
122
+ # @return [Hash] the designators registry.
123
+ #
124
+ def register_designators(new_designators)
125
+ all_designators.update(new_designators)
126
+ end
127
+
128
+ private
129
+ ##
130
+ # @return [Hash] a registry of all the defined designators.
131
+ #
132
+ def all_designators
133
+ @_all_designators ||= {}
134
+ end
135
+ end
136
+ end
137
+
138
+ end
139
+ end
@@ -0,0 +1,110 @@
1
+ module Eaco
2
+ module DSL
3
+ class Actor < Base
4
+
5
+ ##
6
+ # Designators collector using +method_missing+.
7
+ #
8
+ # Parses the following DSL:
9
+ #
10
+ # actor User do
11
+ # designators do
12
+ # authenticated from: :class
13
+ # user from: :id
14
+ # group from: :group_ids
15
+ # end
16
+ # end
17
+ #
18
+ # and looks up within the Designators namespace of the Actor model the
19
+ # concrete implementations of the described designators.
20
+ #
21
+ # Here the User model is expected to define an User::Designators module
22
+ # and to implement within it a +class Authenticated < Eaco::Designator+
23
+ #
24
+ # @see Designator
25
+ #
26
+ class Designators < Base
27
+ ##
28
+ # Sets up the designators registry.
29
+ #
30
+ def initialize(*)
31
+ super
32
+
33
+ @designators = {}
34
+ end
35
+
36
+ ##
37
+ # The parsed designators, keyed by type symbol and with concrete
38
+ # implementations as values.
39
+ #
40
+ # @return [Hash]
41
+ #
42
+ attr_reader :designators
43
+ alias result designators
44
+
45
+ private
46
+ ##
47
+ # Looks up the implementation for the designator of the given
48
+ # +name+, configures it with the given +options+ and saves it in
49
+ # the designators registry.
50
+ #
51
+ # @param name [Symbol]
52
+ # @param options [Hash]
53
+ #
54
+ # @return [Class]
55
+ #
56
+ # @see #implementation_for
57
+ #
58
+ def define_designator(name, options)
59
+ designators[name] = implementation_for(name).configure!(options)
60
+ end
61
+ alias method_missing define_designator
62
+
63
+ ##
64
+ # Looks up the +name+ designator implementation in the {Actor}'s
65
+ # +Designators+ namespace.
66
+ #
67
+ # @param name [Symbol]
68
+ #
69
+ # @return [Class]
70
+ #
71
+ # @raise [Malformed] if the implementation class is not found.
72
+ #
73
+ # @see #container
74
+ # @see Designator.type
75
+ #
76
+ def implementation_for(name)
77
+ impl = name.to_s.camelize.intern
78
+
79
+ unless container.const_defined?(impl)
80
+ raise Malformed, <<-EOF
81
+ Implementation #{container}::#{impl} for Designator #{name} not found.
82
+ EOF
83
+ end
84
+
85
+ container.const_get(impl)
86
+ end
87
+
88
+ ##
89
+ # Looks up the +Designators+ namespace within the {Actor}'s class.
90
+ #
91
+ # @return [Class]
92
+ #
93
+ # @raise Malformed if the +Designators+ module cannot be found.
94
+ #
95
+ # @see #implementation_for
96
+ #
97
+ def container
98
+ @_container ||= begin
99
+ unless target.const_defined?(:Designators)
100
+ raise Malformed, "Please put designators implementations in #{target}::Designators"
101
+ end
102
+
103
+ target.const_get(:Designators)
104
+ end
105
+ end
106
+ end
107
+
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,52 @@
1
+ module Eaco
2
+ module DSL
3
+
4
+ ##
5
+ # Base DSL class. Provides handy access to the +target+ class being
6
+ # manipulated, DSL-specific options, and a {#target_eval} helper to do
7
+ # +instance_eval+ on the +target+.
8
+ #
9
+ # Nothing too fancy.
10
+ #
11
+ class Base
12
+
13
+ ##
14
+ # Executes a DSL block in the context of a DSL manipulator.
15
+ #
16
+ # @see DSL::ACL
17
+ # @see DSL::Actor
18
+ # @see DSL::Resource
19
+ #
20
+ # @return [Base]
21
+ #
22
+ def self.eval(klass, options = {}, &block)
23
+ new(klass, options).tap do |dsl|
24
+ dsl.instance_eval(&block) if block
25
+ end
26
+ end
27
+
28
+ # The target class of the manipulation
29
+ attr_reader :target
30
+
31
+ # DSL-specific options
32
+ attr_reader :options
33
+
34
+ ##
35
+ # @param target [Class]
36
+ # @param options [Hash]
37
+ #
38
+ def initialize(target, options)
39
+ @target, @options = target, options
40
+ end
41
+
42
+ protected
43
+ ##
44
+ # Evaluates the given block in the context of the target class
45
+ #
46
+ def target_eval(&block)
47
+ target.instance_eval(&block)
48
+ end
49
+ end
50
+
51
+ end
52
+ end
@@ -0,0 +1,129 @@
1
+ module Eaco
2
+ module DSL
3
+
4
+ ##
5
+ # Parses the Resource definition DSL.
6
+ #
7
+ # Example:
8
+ #
9
+ # authorize Document do
10
+ # roles :owner, :editor, :reader
11
+ #
12
+ # role :owner, 'Author'
13
+ #
14
+ # permissions do
15
+ # reader :read
16
+ # editor reader, :edit
17
+ # owner editor, :destroy
18
+ # end
19
+ # end
20
+ #
21
+ # The DSL installs authorization in your +Document+ model,
22
+ # defining three access roles.
23
+ #
24
+ # The +owner+ role is given a label of "Author".
25
+ #
26
+ # Each role has then different abilities, defined in the
27
+ # permissions block.
28
+ #
29
+ # @see DSL::Resource::Permissions
30
+ #
31
+ class Resource < Base
32
+ autoload :Permissions, 'eaco/dsl/resource/permissions'
33
+
34
+ ##
35
+ # Sets up an authorized resource. The only required API
36
+ # is +accessible_by+. For available implementations, see
37
+ # the {Adapters} module.
38
+ #
39
+ # @see Resource
40
+ #
41
+ def initialize(*)
42
+ super
43
+
44
+ target_eval do
45
+ include Eaco::Resource
46
+
47
+ def permissions
48
+ @_permissions
49
+ end
50
+
51
+ def roles
52
+ @_roles || []
53
+ end
54
+
55
+ def roles_priority
56
+ @_roles_priority ||= {}.tap do |priorities|
57
+ roles.each_with_index {|role, idx| priorities[role] = idx }
58
+ end.freeze
59
+ end
60
+
61
+ def roles_with_labels
62
+ @_roles_with_labels ||= roles.inject({}) do |labels, role|
63
+ labels.update(role => role.to_s.humanize)
64
+ end
65
+ end
66
+
67
+ # Reset memoizations when this method is called on the target class,
68
+ # so that reloading the authorizations configuration file will
69
+ # refresh the models' configuration.
70
+ @_roles_priority = nil
71
+ @_roles_with_labels = nil
72
+ end
73
+ end
74
+
75
+ ##
76
+ # Defines the permissions on this resource.
77
+ # The evaluated registries are memoized in the target class.
78
+ #
79
+ # @return [void]
80
+ #
81
+ def permissions(&block)
82
+ target_eval do
83
+ @_permissions = Permissions.eval(self, &block).result.freeze
84
+ end
85
+ end
86
+
87
+ ##
88
+ # Defines the roles valid for this resource. e.g.
89
+ #
90
+ # authorize Foobar do
91
+ # roles :owner, :editor, :reader
92
+ # end
93
+ #
94
+ # Roles defined first have higher priority.
95
+ #
96
+ # If the same user is at the same time +reader+ and +editor+, the
97
+ # resulting role is +editor+.
98
+ #
99
+ # @param keys [Variadic]
100
+ #
101
+ # @return [void]
102
+ #
103
+ def roles(*keys)
104
+ target_eval do
105
+ @_roles = keys.flatten.freeze
106
+ end
107
+ end
108
+
109
+ ##
110
+ # Sets the given label on the given role.
111
+ #
112
+ # TODO rename this method, or use it to pass options
113
+ # to improve readability of the DSL and to store more
114
+ # metadata with each role for future extensibility.
115
+ #
116
+ # @param role [Symbol]
117
+ # @param label [String]
118
+ #
119
+ # @return [void]
120
+ #
121
+ def role(role, label)
122
+ target_eval do
123
+ roles_with_labels[role] = label
124
+ end
125
+ end
126
+ end
127
+
128
+ end
129
+ end
@@ -0,0 +1,131 @@
1
+ module Eaco
2
+ module DSL
3
+ class Resource < Base
4
+
5
+ ##
6
+ # Permission collector, based on +method_missing+.
7
+ #
8
+ # Example:
9
+ #
10
+ # 1 authorize Foobar do
11
+ # 2 permissions do
12
+ # 3 reader :read_foo, :read_bar
13
+ # 4 editor reader, :edit_foo, :edit_bar
14
+ # 5 owner editor, :destroy
15
+ # 6 end
16
+ # 7 end
17
+ #
18
+ # Within the block, each undefined method call defines a new
19
+ # method that returns the given arguments.
20
+ #
21
+ # After evaluating line 3 above:
22
+ #
23
+ # >> reader
24
+ # => #<Set{ :read_foo, :read_bar }>
25
+ #
26
+ # The method is used then on line 4, giving the +editor+ role the
27
+ # same set of permissions granted to the +reader+, plus its own
28
+ # set of permissions:
29
+ #
30
+ # >> editor
31
+ # => #<Set{ :read_foo, :read_bar, :edit_foo, :edit_bar }>
32
+ #
33
+ class Permissions < Base
34
+
35
+ ##
36
+ # Evaluates the given block in the context of a new collector
37
+ #
38
+ # Returns an Hash of permissions, keyed by role.
39
+ #
40
+ # >> Permissions.eval do
41
+ # | permissions do
42
+ # | reader :read
43
+ # | editor reader, :edit
44
+ # | end
45
+ # | end
46
+ #
47
+ # => {
48
+ # | reader: #<Set{ :read },
49
+ # | editor: #<Set{ :read, :edit }
50
+ # | }
51
+ #
52
+ # @return [Permissions]
53
+ #
54
+ def self.eval(*, &block)
55
+ super
56
+ end
57
+
58
+ ##
59
+ # Sets up an hash with a default value of a new Set.
60
+ #
61
+ def initialize(*)
62
+ super
63
+
64
+ @permissions = Hash.new {|hsh, key| hsh[key] = Set.new}
65
+ end
66
+
67
+ ##
68
+ # Returns the collected permissions in a plain Hash, lacking the
69
+ # default block used by the collector's internals - to give to
70
+ # the outside an Hash with a predictable behaviour :-).
71
+ #
72
+ # @return [Hash]
73
+ #
74
+ def result
75
+ Hash.new.merge(@permissions)
76
+ end
77
+
78
+ private
79
+ ##
80
+ # Here the method name is the role code. If we already have defined
81
+ # permissions for the given role, those are returned.
82
+ #
83
+ # Else, {#save_permission} is called to memoize the given permissions
84
+ # for the +role+.
85
+ #
86
+ # @param role [Symbol]
87
+ # @param permissions [Array] permissions to grant to the given +role+.
88
+ #
89
+ # @return [Set]
90
+ #
91
+ def method_missing(role, *permissions)
92
+ if @permissions.key?(role)
93
+ @permissions[role]
94
+ else
95
+ save_permission(role, permissions)
96
+ end
97
+ end
98
+
99
+ ##
100
+ # Memoizes the given set of permissions for the given role.
101
+ #
102
+ # @param role [Symbol]
103
+ # @param permissions [Array]
104
+ #
105
+ # @return [Set]
106
+ #
107
+ # @raise [Malformed] if the syntax is not valid.
108
+ #
109
+ def save_permission(role, permissions)
110
+ permissions = permissions.inject(Set.new) do |set, perm|
111
+ if perm.is_a?(Symbol)
112
+ set.add perm
113
+
114
+ elsif perm.is_a?(Set)
115
+ set.merge perm
116
+
117
+ else
118
+ raise Malformed, <<-EOF
119
+ Invalid #{role} permission definition: #{perm.inspect}.
120
+ Permissions can be defined only by plain symbols.
121
+ EOF
122
+ end
123
+ end
124
+
125
+ @permissions[role].merge(permissions)
126
+ end
127
+ end
128
+
129
+ end
130
+ end
131
+ end