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,158 @@
1
+ require 'active_support/concern'
2
+
3
+ module Eaco
4
+
5
+ ##
6
+ # An ActionController extension to verify authorization in Rails applications.
7
+ #
8
+ # Tested on Rails 3.2 and up on Ruby 2.0 and up.
9
+ #
10
+ module Controller
11
+ extend ActiveSupport::Concern
12
+
13
+ ##
14
+ # Controller authorization DSL.
15
+ #
16
+ module ClassMethods
17
+
18
+ ##
19
+ # Defines the ability required to access a given controller action.
20
+ #
21
+ # Example:
22
+ #
23
+ # class DocumentsController < ApplicationController
24
+ # authorize :index, [:folder, :index]
25
+ # authorize :show, [:folder, :read]
26
+ # authorize :create, :update, [:folder, :write]
27
+ # end
28
+ #
29
+ # Here +@folder+ is expected to be an authorized +Resource+, and for the
30
+ # +index+ action the +current_user+ is checked to +can?(:index, @folder)+
31
+ # while for +show+, +can?(:read, @folder)+ and for +create+ and +update+
32
+ # checks that it +can?(:write, @folder)+.
33
+ #
34
+ # The special +:all+ action name requires the given ability on the given
35
+ # Resource for all actions.
36
+ #
37
+ # If an action has no authorization defined, access is granted.
38
+ #
39
+ # Adds {Controller#confront_eaco} as a +before_filter+.
40
+ #
41
+ # @param actions [Variadic] see above.
42
+ #
43
+ # @return void
44
+ #
45
+ def authorize(*actions)
46
+ target = actions.pop
47
+
48
+ actions.each {|action| authorization_permissions.update(action => target)}
49
+
50
+ @_eaco_filter_installed ||= begin
51
+ before_filter :confront_eaco
52
+ true
53
+ end
54
+ end
55
+
56
+ ##
57
+ # @return [Symbol] the permission required to access the given action.
58
+ #
59
+ def permission_for(action)
60
+ authorization_permissions[action] || authorization_permissions[:all]
61
+ end
62
+
63
+ protected
64
+ ##
65
+ # Permission requirements configured on this controller.
66
+ #
67
+ def authorization_permissions
68
+ @_authorization_permissions ||= {}
69
+ end
70
+ end
71
+
72
+ ##
73
+ # Asks Eaco whether thou shalt pass or not.
74
+ #
75
+ # The implementation is left in this method's body, despite a bit long for
76
+ # many's taste, as it is pretty imperative and simple code. Moreover, the
77
+ # less we pollute ActionController's namespace, the better.
78
+ #
79
+ # @return [void]
80
+ #
81
+ # @raise [Error] if the instance variable configured in {.authorize} is not found
82
+ # @raise [Forbidden] if the +current_user+ is not granted access.
83
+ #
84
+ #
85
+ # == La Guardiana
86
+ # /\
87
+ # .-_-. / \
88
+ # || .-.( .' .-. // \ /
89
+ # \\\/ (((\ /))) \ / // )(
90
+ # ) '._ ,-. ___. )/ //(__)
91
+ # \_((( ( :) \)))/ , / ||
92
+ # \_ \ '-' /_ /| ),// ||
93
+ # \ (_._.'_ \ (o__// _||_
94
+ # \ )\ .(/ / __) \ \
95
+ # ( \ '_ .' /( |-. \
96
+ # \_'._'.\__/)))) (__)'.'.
97
+ # _._ | | _.-._ || \ '.
98
+ # / //--' / '--//'-'/\||____\ '.
99
+ # \---.\ .----.// // ||// '\ \
100
+ # / ' \/ ' \\__\\ ,||\\_______.'
101
+ # \\___//\\____//\____\ ||
102
+ # _.-'''---. /\___/ \____/ \\/ ||
103
+ # ..'_.''''---.| /. \ / ||
104
+ # .'.-'O __ / _/ )_.--.____( ||
105
+ # / / / \__/ /' /\ \(__.--._____) ||
106
+ # | | /\ \ \_.' | | \ | ||
107
+ # \ '.__\,_.'.__/./ / ) . |\ ||
108
+ # '..__ O --' ___..' /\ /|'. ||
109
+ # ''----' | \/\.' / /'. ||
110
+ # |\(()).' / \ ||
111
+ # _/ \ \/ / \||
112
+ # __..--'' '. | |||
113
+ # .-'' / '._|/ |||
114
+ # / __.- / /||
115
+ # \ ____..-----'' / | ||
116
+ # '. )). | / ||
117
+ # ''._// \ .-----./ ||
118
+ # '. \ (.-----.) ||
119
+ # '. \ | / ||
120
+ # )_ \ | | ||
121
+ # /__'O\ ( ) ( ||
122
+ # _______mrf,-'____/|/__ |\ \ ||
123
+ # | | ||
124
+ # |____) (__)
125
+ # '-----' ||
126
+ # \ | ||
127
+ # \ | ||
128
+ # \ | ||
129
+ # | \ ||
130
+ # |_ \ ||
131
+ # /_'O\||
132
+ # .-'___/(__)
133
+ #
134
+ # http://ascii.co.uk/art/guardiana
135
+ #
136
+ def confront_eaco
137
+ action = params[:action].intern
138
+ resource_ivar, permission = self.class.permission_for(action)
139
+
140
+ if resource_ivar && permission
141
+ resource = instance_variable_get(['@', resource_ivar].join.intern)
142
+
143
+ if resource.nil?
144
+ raise Error, <<-EOF
145
+ @#{resource_ivar} is not set, can't authorize #{self}##{action}
146
+ EOF
147
+ end
148
+
149
+ unless current_user.can? permission, resource
150
+ raise Forbidden, <<-EOF
151
+ `#{current_user}' not authorized to `#{action}' on `#{resource}'
152
+ EOF
153
+ end
154
+ end
155
+ end
156
+ end
157
+
158
+ end
@@ -0,0 +1,11 @@
1
+ module Eaco
2
+
3
+ ##
4
+ # Namespace that holds all cucumber-related helpers.
5
+ #
6
+ module Cucumber
7
+ autoload :ActiveRecord, 'eaco/cucumber/active_record.rb'
8
+ autoload :World, 'eaco/cucumber/world.rb'
9
+ end
10
+
11
+ end
@@ -0,0 +1,163 @@
1
+ begin
2
+ require 'active_record'
3
+ rescue LoadError
4
+ abort "ActiveRecord requires the rails appraisal. Try `appraisal cucumber`"
5
+ end
6
+
7
+ require 'yaml'
8
+
9
+ module Eaco
10
+ module Cucumber
11
+
12
+ ##
13
+ # +ActiveRecord+ configuration and connection.
14
+ #
15
+ # Database configuration is looked up in +features/active_record.yml+ by
16
+ # default. Logs are sent to +features/active_record.log+, truncating the
17
+ # file at each run.
18
+ #
19
+ # Environment variables:
20
+ #
21
+ # * +EACO_AR_CONFIG+ specify a different +ActiveRecord+ configuration file
22
+ # * +VERBOSE+ log to +stderr+
23
+ #
24
+ module ActiveRecord
25
+ autoload :Document, 'eaco/cucumber/active_record/document' # Resource
26
+ autoload :User, 'eaco/cucumber/active_record/user' # Actor
27
+ autoload :Department, 'eaco/cucumber/active_record/department' # Designator source
28
+ autoload :Position, 'eaco/cucumber/active_record/position' # Designator source
29
+
30
+ extend self
31
+
32
+ ##
33
+ # Looks up ActiveRecord and sets the +logger+.
34
+ #
35
+ # @return [Class] +ActiveRecord::Base+
36
+ #
37
+ def active_record
38
+ @_active_record ||= ::ActiveRecord::Base.tap do |active_record|
39
+ active_record.logger = ::Logger.new(active_record_log).tap {|l| l.level = 0}
40
+ end
41
+ end
42
+
43
+ ##
44
+ # Log to stderr if +VERBOSE+ is given, else log to
45
+ # +features/active_record.log+
46
+ #
47
+ # @return [IO] the log destination
48
+ #
49
+ def active_record_log
50
+ @_active_record_log ||= ENV['VERBOSE'] ? $stderr :
51
+ 'features/active_record.log'.tap {|f| File.open(f, "w+")}
52
+ end
53
+
54
+ ##
55
+ # @return [Logger] the logger configured, logging to {.active_record_log}.
56
+ #
57
+ def logger
58
+ active_record.logger
59
+ end
60
+
61
+ ##
62
+ # @return [ActiveRecord::Connection] the current +ActiveRecord+ connection
63
+ # object.
64
+ #
65
+ def connection
66
+ active_record.connection
67
+ end
68
+ alias adapter connection
69
+
70
+ ##
71
+ # Returns an Hash wit the database configuration.
72
+ #
73
+ # Caveat:the returned +Hash+ has a custom +.to_s+ method that formats
74
+ # the configuration as a +pgsql://+ URL.
75
+ #
76
+ # @return [Hash] the current database configuration
77
+ #
78
+ # @see {#config_file}
79
+ #
80
+ def configuration
81
+ @_config ||= YAML.load(config_file.read).tap do |conf|
82
+ def conf.to_s
83
+ 'pgsql://%s:%s@%s/%s' % values_at(
84
+ :username, :password, :hostname, :database
85
+ )
86
+ end
87
+ end
88
+ end
89
+
90
+ ##
91
+ # @return [Pathname] the currently configured configuration file. Override
92
+ # using the +EACO_AR_CONFIG' envinronment variable.
93
+ #
94
+ def config_file
95
+ Pathname.new(ENV['EACO_AR_CONFIG'] || default_config_file)
96
+ end
97
+
98
+ ##
99
+ # @return [String] +active_record.yml+ relative to this source file.
100
+ #
101
+ # @raise [Errno::ENOENT] if the configuration file is not found.
102
+ #
103
+ def default_config_file
104
+ Pathname.new('features/active_record.yml').realpath
105
+
106
+ rescue Errno::ENOENT => error
107
+ raise error.class.new, <<-EOF.squeeze(' ')
108
+
109
+ #{error.message}.
110
+
111
+ Please define your Active Record database configuration in the
112
+ default location, or specify your configuration file location by
113
+ passing the `EACO_AR_CONFIG' environment variable.
114
+ EOF
115
+ end
116
+
117
+ ##
118
+ # Establish ActiveRecord connection using the given configuration hash
119
+ #
120
+ # @param config [Hash] the configuration to use, {#configuration} by default.
121
+ #
122
+ # @return [ActiveRecord::ConnectionAdapters::ConnectionPool]
123
+ #
124
+ # @raise [ActiveRecord::ActiveRecordError] if cannot connect
125
+ #
126
+ def connect!(config = self.configuration)
127
+ unless ENV['VERBOSE']
128
+ config = config.merge(min_messages: 'WARNING')
129
+ end
130
+
131
+ active_record.establish_connection(config)
132
+ end
133
+
134
+ ##
135
+ # Loads the defined {ActiveRecord#schema}
136
+ #
137
+ # @return [nil]
138
+ #
139
+ def define_schema!
140
+ load 'eaco/cucumber/active_record/schema.rb'
141
+ end
142
+
143
+ ##
144
+ # Drops and recreates the database specified in the {#configuration}.
145
+ #
146
+ # TODO untangle from postgres
147
+ #
148
+ # @return [void]
149
+ #
150
+ def recreate_database!
151
+ database = config.fetch(:database)
152
+ connect! config.merge(database: :postgres) # FIXME
153
+
154
+ connection.drop_database database
155
+ connection.create_database database
156
+ connect! config
157
+
158
+ logger.info "Connected to #{config}"
159
+ end
160
+ end
161
+
162
+ end
163
+ end
@@ -0,0 +1,19 @@
1
+ module Eaco
2
+ module Cucumber
3
+ module ActiveRecord
4
+
5
+ ##
6
+ # A department holds many {Position}s.
7
+ #
8
+ # For the background story, see {Eaco::Cucumber::World}.
9
+ #
10
+ # @see Position
11
+ # @see Eaco::Actor
12
+ # @see Eaco::Cucumber::World
13
+ #
14
+ class Department < ::ActiveRecord::Base
15
+ end
16
+
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,18 @@
1
+ module Eaco
2
+ module Cucumber
3
+ module ActiveRecord
4
+
5
+ ##
6
+ # This is an example of a {Eaco::Resource} that can be protected by an
7
+ # {Eaco::ACL}. For the background story, see {Eaco::Cucumber::World}.
8
+ #
9
+ # @see User
10
+ # @see Eaco::Resource
11
+ # @see Eaco::Cucumber::World
12
+ #
13
+ class Document < ::ActiveRecord::Base
14
+ end
15
+
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,21 @@
1
+ module Eaco
2
+ module Cucumber
3
+ module ActiveRecord
4
+
5
+ ##
6
+ # A Position is occupied by an {User} in a {Department}.
7
+ #
8
+ # For the background story, see {Eaco::Cucumber::World}.
9
+ #
10
+ # @see User
11
+ # @see Department
12
+ # @see Eaco::Cucumber::World
13
+ #
14
+ class Position < ::ActiveRecord::Base
15
+ belongs_to :user
16
+ belongs_to :department
17
+ end
18
+
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,36 @@
1
+ module Eaco
2
+ module Cucumber
3
+ module ActiveRecord
4
+
5
+ # @!method schema
6
+ #
7
+ # Defines the database schema for the {Eaco::Cucumber::World} scenario.
8
+ #
9
+ # @see Eaco::Cucumber::World
10
+ #
11
+ ::ActiveRecord::Schema.define(version: '2015022301') do
12
+ create_table 'documents', force: true do |t|
13
+ t.string :name
14
+ t.text :contents
15
+ t.column :acl, :jsonb
16
+ end
17
+
18
+ create_table 'users', force: true do |t|
19
+ t.string :name
20
+ end
21
+
22
+ create_table 'departments', force: true do |t|
23
+ t.string :abbr
24
+ end
25
+
26
+ create_table 'positions', force: true do |t|
27
+ t.string :job_title
28
+
29
+ t.references :user
30
+ t.references :department
31
+ end
32
+ end
33
+
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,24 @@
1
+ module Eaco
2
+ module Cucumber
3
+ module ActiveRecord
4
+
5
+ ##
6
+ # This is an example of a {Eaco::Actor} that can be authorized against
7
+ # the ACLs in a resource, such as the example {Document}.
8
+ #
9
+ # For the background story, see {Eaco::Cucumber::World}.
10
+ #
11
+ # @see Document
12
+ # @see Eaco::Actor
13
+ # @see Eaco::Cucumber::World
14
+ #
15
+ class User < ::ActiveRecord::Base
16
+ autoload :Designators, 'lib/eaco/cucumber/designators.rb'
17
+
18
+ has_many :positions
19
+ has_many :departments, through: :positions
20
+ end
21
+
22
+ end
23
+ end
24
+ end