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,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