hoodoo 1.0.2

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 (216) hide show
  1. checksums.yaml +7 -0
  2. data/bin/hoodoo +5 -0
  3. data/lib/hoodoo.rb +27 -0
  4. data/lib/hoodoo/active.rb +32 -0
  5. data/lib/hoodoo/active/active_model/uuid_validator.rb +45 -0
  6. data/lib/hoodoo/active/active_record/base.rb +81 -0
  7. data/lib/hoodoo/active/active_record/creator.rb +134 -0
  8. data/lib/hoodoo/active/active_record/dated.rb +343 -0
  9. data/lib/hoodoo/active/active_record/error_mapping.rb +351 -0
  10. data/lib/hoodoo/active/active_record/finder.rb +606 -0
  11. data/lib/hoodoo/active/active_record/search_helper.rb +189 -0
  12. data/lib/hoodoo/active/active_record/secure.rb +431 -0
  13. data/lib/hoodoo/active/active_record/support.rb +106 -0
  14. data/lib/hoodoo/active/active_record/translated.rb +87 -0
  15. data/lib/hoodoo/active/active_record/uuid.rb +80 -0
  16. data/lib/hoodoo/active/active_record/writer.rb +321 -0
  17. data/lib/hoodoo/client.rb +23 -0
  18. data/lib/hoodoo/client/augmented_array.rb +29 -0
  19. data/lib/hoodoo/client/augmented_base.rb +168 -0
  20. data/lib/hoodoo/client/augmented_hash.rb +23 -0
  21. data/lib/hoodoo/client/client.rb +354 -0
  22. data/lib/hoodoo/client/endpoint/endpoint.rb +427 -0
  23. data/lib/hoodoo/client/endpoint/endpoints/amqp.rb +180 -0
  24. data/lib/hoodoo/client/endpoint/endpoints/auto_session.rb +194 -0
  25. data/lib/hoodoo/client/endpoint/endpoints/http.rb +203 -0
  26. data/lib/hoodoo/client/endpoint/endpoints/http_based.rb +367 -0
  27. data/lib/hoodoo/client/endpoint/endpoints/not_found.rb +59 -0
  28. data/lib/hoodoo/client/headers.rb +269 -0
  29. data/lib/hoodoo/communicators.rb +23 -0
  30. data/lib/hoodoo/communicators/fast.rb +44 -0
  31. data/lib/hoodoo/communicators/pool.rb +601 -0
  32. data/lib/hoodoo/communicators/slow.rb +84 -0
  33. data/lib/hoodoo/data.rb +51 -0
  34. data/lib/hoodoo/data/resources/caller.rb +39 -0
  35. data/lib/hoodoo/data/resources/errors.rb +28 -0
  36. data/lib/hoodoo/data/resources/log.rb +31 -0
  37. data/lib/hoodoo/data/resources/session.rb +26 -0
  38. data/lib/hoodoo/data/types/error_primitive.rb +27 -0
  39. data/lib/hoodoo/data/types/permissions.rb +40 -0
  40. data/lib/hoodoo/data/types/permissions_defaults.rb +32 -0
  41. data/lib/hoodoo/data/types/permissions_full.rb +28 -0
  42. data/lib/hoodoo/data/types/permissions_resources.rb +31 -0
  43. data/lib/hoodoo/discovery.rb +20 -0
  44. data/lib/hoodoo/errors.rb +19 -0
  45. data/lib/hoodoo/errors/error_descriptions.rb +229 -0
  46. data/lib/hoodoo/errors/errors.rb +322 -0
  47. data/lib/hoodoo/generator.rb +139 -0
  48. data/lib/hoodoo/logger.rb +23 -0
  49. data/lib/hoodoo/logger/fast_writer.rb +27 -0
  50. data/lib/hoodoo/logger/flattener_mixin.rb +36 -0
  51. data/lib/hoodoo/logger/logger.rb +387 -0
  52. data/lib/hoodoo/logger/slow_writer.rb +49 -0
  53. data/lib/hoodoo/logger/writer_mixin.rb +52 -0
  54. data/lib/hoodoo/logger/writers/file_writer.rb +45 -0
  55. data/lib/hoodoo/logger/writers/log_entries_dot_com_writer.rb +64 -0
  56. data/lib/hoodoo/logger/writers/stream_writer.rb +43 -0
  57. data/lib/hoodoo/middleware.rb +33 -0
  58. data/lib/hoodoo/presenters.rb +45 -0
  59. data/lib/hoodoo/presenters/base.rb +281 -0
  60. data/lib/hoodoo/presenters/base_dsl.rb +519 -0
  61. data/lib/hoodoo/presenters/common_resource_fields.rb +31 -0
  62. data/lib/hoodoo/presenters/embedding.rb +232 -0
  63. data/lib/hoodoo/presenters/types/array.rb +118 -0
  64. data/lib/hoodoo/presenters/types/boolean.rb +26 -0
  65. data/lib/hoodoo/presenters/types/date.rb +26 -0
  66. data/lib/hoodoo/presenters/types/date_time.rb +26 -0
  67. data/lib/hoodoo/presenters/types/decimal.rb +47 -0
  68. data/lib/hoodoo/presenters/types/enum.rb +55 -0
  69. data/lib/hoodoo/presenters/types/field.rb +158 -0
  70. data/lib/hoodoo/presenters/types/float.rb +26 -0
  71. data/lib/hoodoo/presenters/types/hash.rb +361 -0
  72. data/lib/hoodoo/presenters/types/integer.rb +26 -0
  73. data/lib/hoodoo/presenters/types/object.rb +117 -0
  74. data/lib/hoodoo/presenters/types/string.rb +53 -0
  75. data/lib/hoodoo/presenters/types/tags.rb +24 -0
  76. data/lib/hoodoo/presenters/types/text.rb +26 -0
  77. data/lib/hoodoo/presenters/types/uuid.rb +54 -0
  78. data/lib/hoodoo/services.rb +34 -0
  79. data/lib/hoodoo/services/discovery/discoverers/by_consul.rb +66 -0
  80. data/lib/hoodoo/services/discovery/discoverers/by_convention.rb +173 -0
  81. data/lib/hoodoo/services/discovery/discoverers/by_drb/by_drb.rb +195 -0
  82. data/lib/hoodoo/services/discovery/discoverers/by_drb/drb_server.rb +166 -0
  83. data/lib/hoodoo/services/discovery/discoverers/by_drb/drb_server_start.rb +37 -0
  84. data/lib/hoodoo/services/discovery/discovery.rb +186 -0
  85. data/lib/hoodoo/services/discovery/results/for_amqp.rb +58 -0
  86. data/lib/hoodoo/services/discovery/results/for_http.rb +85 -0
  87. data/lib/hoodoo/services/discovery/results/for_local.rb +85 -0
  88. data/lib/hoodoo/services/discovery/results/for_remote.rb +57 -0
  89. data/lib/hoodoo/services/middleware/amqp_log_message.rb +186 -0
  90. data/lib/hoodoo/services/middleware/amqp_log_writer.rb +119 -0
  91. data/lib/hoodoo/services/middleware/endpoints/inter_resource_local.rb +130 -0
  92. data/lib/hoodoo/services/middleware/endpoints/inter_resource_remote.rb +202 -0
  93. data/lib/hoodoo/services/middleware/exception_reporting/base_reporter.rb +105 -0
  94. data/lib/hoodoo/services/middleware/exception_reporting/exception_reporting.rb +115 -0
  95. data/lib/hoodoo/services/middleware/exception_reporting/reporters/airbrake_reporter.rb +64 -0
  96. data/lib/hoodoo/services/middleware/exception_reporting/reporters/raygun_reporter.rb +63 -0
  97. data/lib/hoodoo/services/middleware/interaction.rb +127 -0
  98. data/lib/hoodoo/services/middleware/middleware.rb +2705 -0
  99. data/lib/hoodoo/services/middleware/rack_monkey_patch.rb +73 -0
  100. data/lib/hoodoo/services/services/context.rb +153 -0
  101. data/lib/hoodoo/services/services/implementation.rb +132 -0
  102. data/lib/hoodoo/services/services/interface.rb +934 -0
  103. data/lib/hoodoo/services/services/permissions.rb +250 -0
  104. data/lib/hoodoo/services/services/request.rb +189 -0
  105. data/lib/hoodoo/services/services/response.rb +316 -0
  106. data/lib/hoodoo/services/services/service.rb +141 -0
  107. data/lib/hoodoo/services/services/session.rb +729 -0
  108. data/lib/hoodoo/utilities.rb +12 -0
  109. data/lib/hoodoo/utilities/string_inquirer.rb +54 -0
  110. data/lib/hoodoo/utilities/utilities.rb +380 -0
  111. data/lib/hoodoo/utilities/uuid.rb +44 -0
  112. data/lib/hoodoo/version.rb +17 -0
  113. data/spec/active/active_record/base_spec.rb +57 -0
  114. data/spec/active/active_record/creator_spec.rb +88 -0
  115. data/spec/active/active_record/dated_spec.rb +248 -0
  116. data/spec/active/active_record/error_mapping_spec.rb +360 -0
  117. data/spec/active/active_record/finder_spec.rb +744 -0
  118. data/spec/active/active_record/search_helper_spec.rb +384 -0
  119. data/spec/active/active_record/secure_spec.rb +435 -0
  120. data/spec/active/active_record/support_spec.rb +225 -0
  121. data/spec/active/active_record/translated_spec.rb +19 -0
  122. data/spec/active/active_record/uuid_spec.rb +72 -0
  123. data/spec/active/active_record/writer_spec.rb +272 -0
  124. data/spec/alchemy/alchemy-amq.rb +33 -0
  125. data/spec/client/augmented_array_spec.rb +15 -0
  126. data/spec/client/augmented_base_spec.rb +50 -0
  127. data/spec/client/augmented_hash_spec.rb +15 -0
  128. data/spec/client/client_spec.rb +955 -0
  129. data/spec/client/endpoint/endpoint_spec.rb +70 -0
  130. data/spec/client/endpoint/endpoints/amqp_spec.rb +16 -0
  131. data/spec/client/endpoint/endpoints/auto_session_spec.rb +9 -0
  132. data/spec/client/endpoint/endpoints/http_based_spec.rb +9 -0
  133. data/spec/client/endpoint/endpoints/http_spec.rb +103 -0
  134. data/spec/client/endpoint/endpoints/not_found_spec.rb +35 -0
  135. data/spec/client/headers_spec.rb +172 -0
  136. data/spec/communicators/fast_spec.rb +9 -0
  137. data/spec/communicators/pool_spec.rb +339 -0
  138. data/spec/communicators/slow_spec.rb +15 -0
  139. data/spec/data/resources/caller_spec.rb +156 -0
  140. data/spec/data/resources/errors_spec.rb +22 -0
  141. data/spec/data/resources/log_spec.rb +20 -0
  142. data/spec/data/resources/session_spec.rb +15 -0
  143. data/spec/data/types/error_primitive_spec.rb +15 -0
  144. data/spec/data/types/permissions_defaults_spec.rb +25 -0
  145. data/spec/data/types/permissions_full_spec.rb +44 -0
  146. data/spec/data/types/permissions_resources_spec.rb +34 -0
  147. data/spec/data/types/permissions_spec.rb +37 -0
  148. data/spec/errors/error_descriptions_spec.rb +98 -0
  149. data/spec/errors/errors_spec.rb +346 -0
  150. data/spec/integration/service_actions_spec.rb +112 -0
  151. data/spec/logger/fast_writer_spec.rb +18 -0
  152. data/spec/logger/logger_spec.rb +259 -0
  153. data/spec/logger/slow_writer_spec.rb +144 -0
  154. data/spec/logger/writers/file_writer_spec.rb +37 -0
  155. data/spec/logger/writers/log_entries_dot_com_writer_spec.rb +29 -0
  156. data/spec/logger/writers/stream_writer_spec.rb +38 -0
  157. data/spec/presenters/base_dsl_spec.rb +111 -0
  158. data/spec/presenters/base_spec.rb +871 -0
  159. data/spec/presenters/common_resource_fields_spec.rb +30 -0
  160. data/spec/presenters/embedding_spec.rb +87 -0
  161. data/spec/presenters/types/array_spec.rb +249 -0
  162. data/spec/presenters/types/boolean_spec.rb +51 -0
  163. data/spec/presenters/types/date_spec.rb +57 -0
  164. data/spec/presenters/types/date_time_spec.rb +59 -0
  165. data/spec/presenters/types/decimal_spec.rb +58 -0
  166. data/spec/presenters/types/enum_spec.rb +71 -0
  167. data/spec/presenters/types/field_spec.rb +77 -0
  168. data/spec/presenters/types/float_spec.rb +50 -0
  169. data/spec/presenters/types/hash_spec.rb +1069 -0
  170. data/spec/presenters/types/integer_spec.rb +50 -0
  171. data/spec/presenters/types/object_spec.rb +177 -0
  172. data/spec/presenters/types/string_spec.rb +65 -0
  173. data/spec/presenters/types/tags_spec.rb +56 -0
  174. data/spec/presenters/types/text_spec.rb +50 -0
  175. data/spec/presenters/types/uuid_spec.rb +46 -0
  176. data/spec/presenters/walk_spec.rb +198 -0
  177. data/spec/services/discovery/discoverers/by_consul_spec.rb +29 -0
  178. data/spec/services/discovery/discoverers/by_convention_spec.rb +67 -0
  179. data/spec/services/discovery/discoverers/by_drb/by_drb_spec.rb +80 -0
  180. data/spec/services/discovery/discoverers/by_drb/drb_server_spec.rb +205 -0
  181. data/spec/services/discovery/discovery_spec.rb +73 -0
  182. data/spec/services/discovery/results/for_amqp_spec.rb +17 -0
  183. data/spec/services/discovery/results/for_http_spec.rb +37 -0
  184. data/spec/services/discovery/results/for_local_spec.rb +21 -0
  185. data/spec/services/discovery/results/for_remote_spec.rb +15 -0
  186. data/spec/services/middleware/amqp_log_message_spec.rb +60 -0
  187. data/spec/services/middleware/amqp_log_writer_spec.rb +95 -0
  188. data/spec/services/middleware/endpoints/inter_resource_local_spec.rb +9 -0
  189. data/spec/services/middleware/endpoints/inter_resource_remote_spec.rb +9 -0
  190. data/spec/services/middleware/exception_reporting/base_reporter_spec.rb +16 -0
  191. data/spec/services/middleware/exception_reporting/exception_reporting_spec.rb +92 -0
  192. data/spec/services/middleware/exception_reporting/reporters/airbrake_reporter_spec.rb +24 -0
  193. data/spec/services/middleware/exception_reporting/reporters/raygun_reporter_spec.rb +23 -0
  194. data/spec/services/middleware/middleware_cors_spec.rb +93 -0
  195. data/spec/services/middleware/middleware_create_update_spec.rb +489 -0
  196. data/spec/services/middleware/middleware_dated_at_spec.rb +186 -0
  197. data/spec/services/middleware/middleware_exotic_communication_spec.rb +560 -0
  198. data/spec/services/middleware/middleware_logging_spec.rb +356 -0
  199. data/spec/services/middleware/middleware_multi_local_spec.rb +1094 -0
  200. data/spec/services/middleware/middleware_multi_remote_spec.rb +1440 -0
  201. data/spec/services/middleware/middleware_permissions_spec.rb +1014 -0
  202. data/spec/services/middleware/middleware_public_spec.rb +238 -0
  203. data/spec/services/middleware/middleware_spec.rb +1569 -0
  204. data/spec/services/middleware/string_inquirer_spec.rb +30 -0
  205. data/spec/services/services/application_spec.rb +74 -0
  206. data/spec/services/services/context_spec.rb +48 -0
  207. data/spec/services/services/implementation_spec.rb +45 -0
  208. data/spec/services/services/interface_spec.rb +262 -0
  209. data/spec/services/services/permissions_spec.rb +249 -0
  210. data/spec/services/services/request_spec.rb +95 -0
  211. data/spec/services/services/response_spec.rb +250 -0
  212. data/spec/services/services/session_spec.rb +432 -0
  213. data/spec/spec_helper.rb +298 -0
  214. data/spec/utilities/utilities_spec.rb +537 -0
  215. data/spec/utilities/uuid_spec.rb +20 -0
  216. metadata +615 -0
@@ -0,0 +1,2705 @@
1
+ ########################################################################
2
+ # File:: service_middleware.rb
3
+ # (C):: Loyalty New Zealand 2014
4
+ #
5
+ # Purpose:: Rack middleware, declared in a +config.ru+ file in the usual
6
+ # way - "use( Hoodoo::Services::Middleware )".
7
+ # ----------------------------------------------------------------------
8
+ # 22-Sep-2014 (ADH): Created.
9
+ # 16-Oct-2014 (TC): Added Session Code.
10
+ # 11-Nov-2014 (ADH): Some internal classes split out into
11
+ # their own files to reduce file size here.
12
+ ########################################################################
13
+
14
+ require 'set'
15
+ require 'uri'
16
+ require 'json'
17
+ require 'benchmark'
18
+
19
+ require 'hoodoo/services/services/permissions'
20
+ require 'hoodoo/services/services/session'
21
+ require 'hoodoo/discovery'
22
+ require 'hoodoo/client'
23
+
24
+ module Hoodoo; module Services
25
+
26
+ # Rack middleware, declared in (e.g.) a +config.ru+ file in the usual way:
27
+ #
28
+ # use( Hoodoo::Services::Middleware )
29
+ #
30
+ # This is the core of the common service implementation on the Rack
31
+ # client-request-handling side. It is run in the context of an
32
+ # Hoodoo::Services::Service subclass that's been given to Rack as the Rack
33
+ # endpoint application; it looks at the component interfaces supported by the
34
+ # service and routes requests to the correct one (or raises a 404).
35
+ #
36
+ # Lots of preprocessing and postprocessing gets done to set up things like
37
+ # locale information, enforce content types and so-forth. Request data is
38
+ # assembled in a parsed, structured format for passing to service
39
+ # implementations and a response object built so that services have a
40
+ # consistent way to return results, which can be post-processed further by
41
+ # the middleware before returning the data to Rack.
42
+ #
43
+ # The middleware supports structured logging through Hoodoo::Logger via the
44
+ # custom Hoodoo::Services::Middleware::AMQPLogWriter class. Access the logger
45
+ # instance with Hoodoo::Services::Middleware::logger. Call +report+ on this
46
+ # (see Hoodoo::Logger::WriterMixin#report) to make structured log entries.
47
+ # The middleware's own entries use component +Middleware+ for general data.
48
+ # It also logs essential essential information about successful and failed
49
+ # interactions with resource endpoints using the resource name as the
50
+ # component. In such cases, the codes it uses are always prefixed by
51
+ # +middleware_+ and service applications must consider codes with this prefix
52
+ # reserved - do not use such codes yourself.
53
+ #
54
+ # The middleware adds a STDERR stream writer logger by default and an AMQP
55
+ # log writer on the first Rack +call+ should the Rack environment provide an
56
+ # Alchemy endpoint (see the AlchemyAMQ gem).
57
+ #
58
+ class Middleware
59
+
60
+ # The "category" directive below is required to work around an RDoc
61
+ # bug where Middleware is viewed as a namespace rather than a class in
62
+ # its own right, with documentation of constants otherwise entirely
63
+ # omitted. By putting one constant in its own category, RDoc ends up
64
+ # making them all visible.
65
+
66
+ # :category: Public constants
67
+ #
68
+ # All allowed action names in implementations, used for internal checks.
69
+ # This is also the default supported set of actions. Symbols.
70
+ #
71
+ ALLOWED_ACTIONS = [
72
+ :list,
73
+ :show,
74
+ :create,
75
+ :update,
76
+ :delete,
77
+ ]
78
+
79
+ # All allowed HTTP methods, related to ALLOWED_ACTIONS.
80
+ #
81
+ ALLOWED_HTTP_METHODS = Set.new( %w( GET POST PATCH DELETE ) )
82
+
83
+ # Allowed common fields in query strings (list actions only). Strings.
84
+ #
85
+ # Only ever *add* to this list. As the API evolves, legacy clients will
86
+ # be calling with previously documented query strings and removing any
87
+ # entries from the list below could cause their requests to be rejected
88
+ # with a 'platform.malformed' error.
89
+ #
90
+ ALLOWED_QUERIES_LIST = [
91
+ 'offset',
92
+ 'limit',
93
+ 'sort',
94
+ 'direction',
95
+ 'search',
96
+ 'filter'
97
+ ]
98
+
99
+ # Allowed common fields in query strings (all actions). Strings. Adds to
100
+ # the ::ALLOWED_QUERIES_LIST for list actions.
101
+ #
102
+ # Only ever *add* to this list. As the API evolves, legacy clients will
103
+ # be calling with previously documented query strings and removing any
104
+ # entries from the list below could cause their requests to be rejected
105
+ # with a 'platform.malformed' error.
106
+ #
107
+ ALLOWED_QUERIES_ALL = [
108
+ '_embed',
109
+ '_reference'
110
+ ]
111
+
112
+ # Allowed media types in Content-Type headers.
113
+ #
114
+ SUPPORTED_MEDIA_TYPES = [ 'application/json' ]
115
+
116
+ # Allowed (required) charsets in Content-Type headers.
117
+ #
118
+ SUPPORTED_ENCODINGS = [ 'utf-8' ]
119
+
120
+ # Prohibited fields in creations or updates - these are the common fields
121
+ # specified in the API, which are emergent in the platform or are set via
122
+ # other routes (e.g. "language" comes from HTTP headers in requests). This
123
+ # is obtained via the Hoodoo::Presenters::CommonResourceFields class and
124
+ # its described field schema, so see that for details.
125
+ #
126
+ PROHIBITED_INBOUND_FIELDS = Hoodoo::Presenters::CommonResourceFields.get_schema().properties.keys
127
+
128
+ # Somewhat arbitrary maximum incoming payload size to prevent ham-fisted
129
+ # DOS attempts to consume RAM.
130
+ #
131
+ MAXIMUM_PAYLOAD_SIZE = 1048576 # 1MB Should Be Enough For Anyone
132
+
133
+ # Maximum *logged* payload (inbound data) size.
134
+ #
135
+ MAXIMUM_LOGGED_PAYLOAD_SIZE = 1024
136
+
137
+ # Maximum *logged* response (outbound data) size.
138
+ #
139
+ MAXIMUM_LOGGED_RESPONSE_SIZE = 1024
140
+
141
+ # The default test session; a Hoodoo::Services::Session instance with the
142
+ # following characteristics:
143
+ #
144
+ # Session ID:: +01234567890123456789012345678901+
145
+ # Caller ID:: +c5ea12fb7f414a46850e73ee1bf6d95e+
146
+ # Caller Version:: 1
147
+ # Permissions:: Default/else/"allow" to allow all actions
148
+ # Identity:: Has +caller_id+ as its only field
149
+ # Scoping:: All secured HTTP headers are allowed
150
+ # Expires at: Now plus 2 days
151
+ #
152
+ # See also ::test_session and ::set_test_session.
153
+ #
154
+ DEFAULT_TEST_SESSION = Hoodoo::Services::Session.new
155
+
156
+ # This is NOT a canonical way to construct a session! Both the Permissions
157
+ # object and Session object should be put together using defined methods,
158
+ # not by assuming hash layout. This is done *purely internally* within the
159
+ # middleware for simplicity/speed at parse time.
160
+ #
161
+ DEFAULT_TEST_SESSION.from_h!( {
162
+ 'session_id' => '01234567890123456789012345678901',
163
+ 'expires_at' => ( Time.now + 172800 ).utc.iso8601,
164
+ 'caller_version' => 1,
165
+ 'caller_id' => 'c5ea12fb7f414a46850e73ee1bf6d95e',
166
+ 'identity' => { 'caller_id' => 'c5ea12fb7f414a46850e73ee1bf6d95e' },
167
+ 'permissions' => Hoodoo::Services::Permissions.new( {
168
+ 'default' => { 'else' => Hoodoo::Services::Permissions::ALLOW }
169
+ } ).to_h,
170
+ 'scoping' => {
171
+ 'authorised_http_headers' => Hoodoo::Client::Headers::HEADER_TO_PROPERTY.map() { | key, sub_hash |
172
+ sub_hash[ :header ] if sub_hash[ :secured ] === true
173
+ }.compact
174
+ }
175
+ } )
176
+
177
+ # Utility - returns the execution environment as a Rails-like environment
178
+ # object which answers queries like +production?+ or +staging?+ with +true+
179
+ # or +false+ according to the +RACK_ENV+ environment variable setting.
180
+ #
181
+ # Example:
182
+ #
183
+ # if Hoodoo::Services::Middleware.environment.production?
184
+ # # ...do something only if RACK_ENV="production"
185
+ # end
186
+ #
187
+ def self.environment
188
+ @@environment ||= Hoodoo::StringInquirer.new( ENV[ 'RACK_ENV' ] || 'development' )
189
+ end
190
+
191
+ # Do we have Memcached available? If not, assume local development with
192
+ # higher level queue services not available. Most service authors should
193
+ # not ever need to check this.
194
+ #
195
+ def self.has_memcached?
196
+ m = self.memcached_host()
197
+ m.nil? == false && m.empty? == false
198
+ end
199
+
200
+ # Return a Memcached host (IP address/port combination) as a String if
201
+ # defined in environment variable MEMCACHED_HOST (with MEMCACHE_URL also
202
+ # accepted as a legacy fallback).
203
+ #
204
+ # If this returns +nil+ or an empty string, there's no defined Memcached
205
+ # host available.
206
+ #
207
+ def self.memcached_host
208
+ ENV[ 'MEMCACHED_HOST' ] || ENV[ 'MEMCACHE_URL' ]
209
+ end
210
+
211
+ # Are we running on the queue, else (implied) a local HTTP server?
212
+ #
213
+ def self.on_queue?
214
+ q = ENV[ 'AMQ_ENDPOINT' ]
215
+ q.nil? == false && q.empty? == false
216
+ end
217
+
218
+ # Access the middleware's logging instance. Call +report+ on this to make
219
+ # structured log entries. See Hoodoo::Logger::WriterMixin#report along
220
+ # with Hoodoo::Logger for other calls you can use.
221
+ #
222
+ # The logging system 'wakes up' in stages. Initially, only console based
223
+ # output is added, as the Middleware Ruby code is parsed and configures
224
+ # a basic logger. If you call ::set_log_folder, file-based logging may be
225
+ # available. In AMQP based environments, queue based logging will become
226
+ # automatically available via Rack and the Alchemy gem once the middleware
227
+ # starts handling its very first request, but not before.
228
+ #
229
+ # With this in mind, the logger is ultimately configured with a set of
230
+ # writers as follows:
231
+ #
232
+ # * If off queue:
233
+ # * All RACK_ENV values (including "test"):
234
+ # * File "log/{environment}.log"
235
+ # * RACK_ENV "development"
236
+ # * Also to $stdout
237
+ #
238
+ # * If on queue:
239
+ # * RACK ENV "test"
240
+ # * File "log/test.log"
241
+ # * All other RACK_ENV values
242
+ # * AMQP writer (see below)
243
+ # * RACK_ENV "development"
244
+ # * Also to $stdout
245
+ #
246
+ # Or to put it another way, in test mode only file output to 'test.log'
247
+ # happens; in development mode $stdout always happens; and in addition
248
+ # for non-test environment, you'll get a queue-based or file-based
249
+ # logger depending on whether or not a queue is available.
250
+ #
251
+ # See Hoodoo::Services::Interface#secure_logs_for for information about
252
+ # security considerations when using logs.
253
+ #
254
+ def self.logger
255
+ @@logger # See self.set_up_basic_logging and self.set_logger
256
+ end
257
+
258
+ # The middleware sets up a logger itself (see ::logger) with various log
259
+ # mechanisms set up (mostly) without service author intervention.
260
+ #
261
+ # If you want to completely override the middleware's logger and replace
262
+ # it with your own at any time (not recommended), call here.
263
+ #
264
+ # See Hoodoo::Services::Interface#secure_logs_for for information about
265
+ # security considerations when using logs.
266
+ #
267
+ # +logger+:: Alternative Hoodoo::Logger instance to use for all
268
+ # middleware logging from this point onwards. The value will
269
+ # subsequently be returned by the ::logger class method.
270
+ #
271
+ def self.set_logger( logger )
272
+ unless logger.is_a?( Hoodoo::Logger )
273
+ raise "Hoodoo::Communicators::set_logger must be called with an instance of Hoodoo::Logger only"
274
+ end
275
+
276
+ @@external_logger = true
277
+ @@logger = logger
278
+ end
279
+
280
+ # If using the middleware logger (see ::logger) with no external custom
281
+ # logger set up (see ::set_logger), call here to configure the folder
282
+ # used for logs when file output is active.
283
+ #
284
+ # If you don't do this at least once, no log file output can occur.
285
+ #
286
+ # You can call more than once to output to more than one log folder.
287
+ #
288
+ # See Hoodoo::Services::Interface#secure_logs_for for information about
289
+ # security considerations when using logs.
290
+ #
291
+ # +base_path+:: Path to folder to use for logs; file "#{environment}.log"
292
+ # may be written inside (see ::environment).
293
+ #
294
+ def self.set_log_folder( base_path )
295
+ self.send( :add_file_logging, base_path )
296
+ end
297
+
298
+ # A Hoodoo::Services::Session instance to use for tests or when no
299
+ # local Memcached instance is known about (environment variable
300
+ # +MEMCACHED_HOST+ is not set). The session is (eventually) read each
301
+ # time a request is made via Rack (through #call).
302
+ #
303
+ # "Out of the box", DEFAULT_TEST_SESSION is used.
304
+ #
305
+ def self.test_session
306
+ @@test_session
307
+ end
308
+
309
+ # Set the test session instance. See ::test_session for details.
310
+ #
311
+ # +session+:: A Hoodoo::Services::Session instance to use as the test
312
+ # session instance for any subsequently-made requests. If
313
+ # +nil+, the test session system acts as if an invalid or
314
+ # missing session ID had been supplied.
315
+ #
316
+ def self.set_test_session( session )
317
+ @@test_session = session
318
+ end
319
+
320
+ self.set_test_session( DEFAULT_TEST_SESSION )
321
+
322
+ # Record internally the HTTP host and port during local development via
323
+ # e.g +rackup+ or testing with rspec. This is usually not called directly
324
+ # except via the Rack startup monkey patch code in
325
+ # +rack_monkey_patch.rb+.
326
+ #
327
+ # Options hash +:Host+ and +:Port+ entries are recorded.
328
+ #
329
+ def self.record_host_and_port( options = {} )
330
+ @@recorded_host = options[ :Host ]
331
+ @@recorded_port = options[ :Port ]
332
+ end
333
+
334
+ # For test purposes, dump the internal service records and flush the DRb
335
+ # service, if it is running. Existing middleware
336
+ # instances will be invalidated. New instances must be created to re-scan
337
+ # their services internally and (where required) inform the DRb process
338
+ # of the endpoints.
339
+ #
340
+ def self.flush_services_for_test
341
+ @@services = []
342
+
343
+ ObjectSpace.each_object( self ) do | middleware_instance |
344
+ discoverer = middleware_instance.instance_variable_get( '@discoverer' )
345
+ discoverer.flush_services_for_test() if discoverer.respond_to?( :flush_services_for_test )
346
+ end
347
+ end
348
+
349
+ # Initialize the middleware instance.
350
+ #
351
+ # +app+ Rack app instance to which calls should be passed.
352
+ #
353
+ def initialize( app )
354
+
355
+ service_container = app
356
+
357
+ if defined?( NewRelic ) &&
358
+ defined?( NewRelic::Agent ) &&
359
+ defined?( NewRelic::Agent::Instrumentation ) &&
360
+ defined?( NewRelic::Agent::Instrumentation::MiddlewareProxy ) &&
361
+ service_container.is_a?( NewRelic::Agent::Instrumentation::MiddlewareProxy )
362
+
363
+ if service_container.respond_to?( :target )
364
+ newrelic_wrapper = service_container
365
+ service_container = service_container.target()
366
+ else
367
+ raise "Hoodoo::Services::Middleware instance created with NewRelic-wrapped Service entity, but NewRelic API is not as expected by Hoodoo; incompatible NewRelic version."
368
+ end
369
+ end
370
+
371
+ unless service_container.is_a?( Hoodoo::Services::Service )
372
+ raise "Hoodoo::Services::Middleware instance created with non-Service entity of class '#{ service_container.class }' - is this the last middleware in the chain via 'use()' and is Rack 'run()'-ing the correct thing?"
373
+ end
374
+
375
+ # Collect together the implementation instances and the matching regexps
376
+ # for endpoints. An array of hashes.
377
+ #
378
+ # Key Value
379
+ # =======================================================================
380
+ # regexp Regexp for +String#match+ on the URI path component
381
+ # interface Hoodoo::Services::Interface subclass associated with
382
+ # the endpoint regular expression in +regexp+
383
+ # actions Set of symbols naming allowed actions
384
+ # implementation Hoodoo::Services::Implementation subclass *instance* to
385
+ # use on match
386
+
387
+ @@services = service_container.component_interfaces.map do | interface |
388
+
389
+ if interface.nil? || interface.endpoint.nil? || interface.implementation.nil?
390
+ raise "Hoodoo::Services::Middleware encountered invalid interface class #{ interface } via service class #{ service_container.class }"
391
+ end
392
+
393
+ # If anything uses a public interface, we need to tell ourselves that
394
+ # the early exit session check can't be done.
395
+ #
396
+ interfaces_have_public_methods() unless interface.public_actions.empty?
397
+
398
+ # Regexp explanation:
399
+ #
400
+ # Match "/", the version text, "/", the endpoint text, then either
401
+ # another "/", a "." or the end of the string, followed by capturing
402
+ # everything else. Match data index 1 will be whatever character (if
403
+ # any) followed after the endpoint ("/" or ".") while index 2 contains
404
+ # everything else.
405
+
406
+ Hoodoo::Services::Discovery::ForLocal.new(
407
+ :resource => interface.resource,
408
+ :version => interface.version,
409
+ :base_path => "/v#{ interface.version }/#{ interface.endpoint }",
410
+ :routing_regexp => /\/v#{ interface.version }\/#{ interface.endpoint }(\.|\/|$)(.*)/,
411
+ :interface_class => interface,
412
+ :implementation_instance => interface.implementation.new
413
+ )
414
+ end
415
+
416
+ announce_presence_of( @@services )
417
+ end
418
+
419
+ # Run a Rack request, returning the [status, headers, body-array] data as
420
+ # per the Rack protocol requirements.
421
+ #
422
+ # +env+ Rack environment.
423
+ #
424
+ def call( env )
425
+
426
+ # Global exception handler - catch problems in service implementations
427
+ # and send back a 500 response as per API documentation (if possible).
428
+ #
429
+ begin
430
+
431
+ enable_alchemy_logging_from( env )
432
+
433
+ interaction = Hoodoo::Services::Middleware::Interaction.new( env, self )
434
+ debug_log( interaction )
435
+
436
+ early_response = preprocess( interaction )
437
+ return early_response unless early_response.nil?
438
+
439
+ response = interaction.context.response
440
+
441
+ process( interaction ) unless response.halt_processing?
442
+ postprocess( interaction ) unless response.halt_processing?
443
+
444
+ return respond_for( interaction )
445
+
446
+ rescue => exception
447
+ begin
448
+
449
+ ExceptionReporting.report( exception, env )
450
+ record_exception( interaction, exception )
451
+
452
+ return respond_for( interaction )
453
+
454
+ rescue => inner_exception
455
+ begin
456
+ backtrace = ''
457
+ inner_backtrace = ''
458
+
459
+ if self.class.environment.test? || self.class.environment.development?
460
+ backtrace = exception.backtrace
461
+ inner_backtrace = inner_exception.backtrace
462
+ else
463
+ ''
464
+ end
465
+
466
+ @@logger.error(
467
+ 'Hoodoo::Services::Middleware#call',
468
+ 'Middleware exception in exception handler',
469
+ inner_exception.to_s,
470
+ inner_backtrace.to_s,
471
+ '...while handling...',
472
+ exception.to_s,
473
+ backtrace.to_s
474
+ )
475
+ rescue
476
+ # Ignore logger exceptions. Can't do anything about them. Just
477
+ # try and get the response back to the client now.
478
+ end
479
+
480
+ # An exception in the exception handler! Oh dear.
481
+ #
482
+ rack_response = Rack::Response.new
483
+ rack_response.status = 500
484
+ rack_response.write( 'Middleware exception in exception handler' )
485
+ return rack_response.finish
486
+
487
+ end
488
+ end
489
+ end
490
+
491
+ # Return something that behaves like a Hoodoo::Client::Endpoint subclass
492
+ # instance which can be used for inter-resource communication, whether
493
+ # the target endpoint implementation is local or remote.
494
+ #
495
+ # +resource+:: Resource name for the endpoint, e.g. +:Purchase+.
496
+ # String or symbol.
497
+ #
498
+ # +version+:: Required implemented version for the endpoint. Integer.
499
+ #
500
+ # +interaction+:: The Hoodoo::Services::Interaction instance describing
501
+ # the inbound call, the processing of which is leading to
502
+ # a request for an inter-resource call by an endpoint
503
+ # implementation.
504
+ #
505
+ def inter_resource_endpoint_for( resource, version, interaction )
506
+ resource = resource.to_sym
507
+ version = version.to_i
508
+
509
+ # Build a Hash of any options which should be transferred from one
510
+ # endpoint to another for inter-resource calls, along with other
511
+ # options common to local and remote endpoints.
512
+
513
+ endpoint_options = {
514
+ :interaction => interaction,
515
+ :locale => interaction.context.request.locale,
516
+ }
517
+
518
+ Hoodoo::Client::Headers::HEADER_TO_PROPERTY.each do | rack_header, description |
519
+ property = description[ :property ]
520
+
521
+ if description[ :auto_transfer ] == true
522
+ endpoint_options[ property ] = interaction.context.request.send( property )
523
+ end
524
+ end
525
+
526
+ if @discoverer.is_local?( resource, version )
527
+
528
+ # For local inter-resource calls, return the middleware's endpoint
529
+ # for that. In turn, if used this calls into #inter_resource_local.
530
+
531
+ discovery_result = @@services.find do | entry |
532
+ interface = entry.interface_class
533
+ interface.resource == resource && interface.version == version
534
+ end
535
+
536
+ if discovery_result.nil?
537
+ raise "Hoodoo::Services::Middleware\#inter_resource_endpoint_for: Internal error - version #{ version } of resource #{ resource } endpoint is local according to the discovery engine, but no local service discovery record can be found"
538
+ end
539
+
540
+ endpoint_options[ :discovery_result ] = discovery_result
541
+
542
+ return Hoodoo::Services::Middleware::InterResourceLocal.new(
543
+ resource,
544
+ version,
545
+ endpoint_options
546
+ )
547
+
548
+ else
549
+
550
+ # For remote inter-resource calls, use Hoodoo::Client's endpoint
551
+ # factory to get a (say) HTTP or AMQP contact endpoint, but then
552
+ # wrap it with the middleware's remote call endpoint, since the
553
+ # request requires extra processing before it goes to the Client
554
+ # (e.g. session permission augmentation) and the result needs
555
+ # extra processing before it is returned to the caller (e.g.
556
+ # delete an augmented session, annotate any errors from call).
557
+
558
+ endpoint_options[ :discoverer ] = @discoverer
559
+ endpoint_options[ :session ] = interaction.context.session
560
+
561
+ wrapped_endpoint = Hoodoo::Client::Endpoint.endpoint_for(
562
+ resource,
563
+ version,
564
+ endpoint_options
565
+ )
566
+
567
+ if wrapped_endpoint.is_a?( Hoodoo::Client::Endpoint::AMQP ) && defined?( @@alchemy )
568
+ wrapped_endpoint.alchemy = @@alchemy
569
+ end
570
+
571
+ # Using "ForRemote" here is redundant - we could just as well
572
+ # pass wrapped_endpoint directly to an option in the
573
+ # InterResourceRemote class - but keeping "with the pattern"
574
+ # just sort of 'seems right' and might be useful in future.
575
+
576
+ discovery_result = Hoodoo::Services::Discovery::ForRemote.new(
577
+ :resource => resource,
578
+ :version => version,
579
+ :wrapped_endpoint => wrapped_endpoint
580
+ )
581
+
582
+ return Hoodoo::Services::Middleware::InterResourceRemote.new(
583
+ resource,
584
+ version,
585
+ {
586
+ :interaction => interaction,
587
+ :discovery_result => discovery_result
588
+ }
589
+ )
590
+ end
591
+ end
592
+
593
+ # Make a local (non-HTTP local Ruby method call) inter-resource call. This
594
+ # is fast compared to any remote resource call, even though there is still
595
+ # a lot of overhead involved in setting up data so that the target
596
+ # resource "sees" the call in the same way as any other.
597
+ #
598
+ # Named parameters are as follows:
599
+ #
600
+ # +source_interaction+:: A Hoodoo::Services::Interaction instance for the
601
+ # inbound call which is being processed right now
602
+ # by some resource endpoint implementation and this
603
+ # implementation is now making an inter-resource
604
+ # call as part of its processing;
605
+ #
606
+ # +discovery_result+:: A Hoodoo::Services::Discovery::ForLocal instance
607
+ # describing the target of the inter-resource call;
608
+ #
609
+ # +endpoint+:: The calling Hoodoo::Client::Endpoint subclass
610
+ # instance (used for e.g. locale, dated-at);
611
+ #
612
+ # +action+:: A Hoodoo::Services::Middleware::ALLOWED_ACTIONS
613
+ # entry;
614
+ #
615
+ # +ident+:: UUID or other unique identifier of a resource
616
+ # instance. Required for +show+, +update+ and
617
+ # +delete+ actions, ignored for others;
618
+ #
619
+ # +query_hash+:: Optional Hash of query data to be turned into a
620
+ # query string - applicable to any action;
621
+ #
622
+ # +body_hash+:: Hash of data to convert to a body string using
623
+ # the source interaction's described content type.
624
+ # Required for +create+ and +update+ actions,
625
+ # ignored for others.
626
+ #
627
+ # A Hoodoo::Client::AugmentedArray or Hoodoo::Client::AugmentedHash is
628
+ # returned from these methods; @response or the wider processing context
629
+ # is not automatically modified. Callers MUST use the methods provided by
630
+ # Hoodoo::Client::AugmentedBase to detect and handle error conditions,
631
+ # unless for some reason they wish to ignore inter-resource call errors.
632
+ #
633
+ def inter_resource_local( source_interaction:,
634
+ discovery_result:,
635
+ endpoint:,
636
+ action:,
637
+ ident: nil,
638
+ body_hash: nil,
639
+ query_hash: nil )
640
+
641
+ # We must construct a call context for the local service. This means
642
+ # a local request object which we fill in with data just as if we'd
643
+ # parsed an inbound HTTP request and a response object that contains
644
+ # the usual default data.
645
+
646
+ interface = discovery_result.interface_class
647
+ implementation = discovery_result.implementation_instance
648
+
649
+ # Need to possibly augment the caller's session - same rationale
650
+ # as #local_service_remote, so see that for details.
651
+
652
+ session = source_interaction.context.session
653
+
654
+ unless session.nil? || source_interaction.using_test_session?
655
+ session = session.augment_with_permissions_for( source_interaction )
656
+ end
657
+
658
+ if session == false
659
+ hash = Hoodoo::Client::AugmentedHash.new
660
+ hash.platform_errors.add_error( 'platform.invalid_session' )
661
+ return hash
662
+ end
663
+
664
+ mock_rack_env = {
665
+ 'HTTP_X_INTERACTION_ID' => source_interaction.interaction_id
666
+ }
667
+
668
+ local_interaction = Hoodoo::Services::Middleware::Interaction.new(
669
+ mock_rack_env,
670
+ self,
671
+ session
672
+ )
673
+
674
+ local_interaction.target_interface = interface
675
+ local_interaction.target_implementation = implementation
676
+ local_interaction.requested_content_type = source_interaction.requested_content_type
677
+ local_interaction.requested_content_encoding = source_interaction.requested_content_encoding
678
+
679
+ # For convenience...
680
+
681
+ local_request = local_interaction.context.request
682
+ local_response = local_interaction.context.response
683
+
684
+ # Carry through any endpoint-specified request orientated attributes.
685
+
686
+ local_request.locale = endpoint.locale
687
+
688
+ Hoodoo::Client::Headers::HEADER_TO_PROPERTY.each do | rack_header, description |
689
+ property = description[ :property ]
690
+ property_writer = description[ :property_writer ]
691
+
692
+ value = endpoint.send( property )
693
+
694
+ local_request.send( property_writer, value ) unless value.nil?
695
+ end
696
+
697
+ # Initialise the response data.
698
+
699
+ set_common_response_headers( local_interaction )
700
+ update_response_for( local_response, interface )
701
+
702
+ # Work out what kind of result the caller is expecting.
703
+
704
+ result_class = if action == :list
705
+ Hoodoo::Client::AugmentedArray
706
+ else
707
+ Hoodoo::Client::AugmentedHash
708
+ end
709
+
710
+ # Add errors from the local service response into an augmented object
711
+ # for responding early (via a Proc for internal reuse later).
712
+
713
+ add_local_errors = Proc.new {
714
+ result = result_class.new
715
+ result.response_options = Hoodoo::Client::Headers.x_header_to_options(
716
+ local_response.headers
717
+ )
718
+
719
+ result.platform_errors.merge!( local_response.errors )
720
+ result
721
+ }
722
+
723
+ # Figure out initial action / authorisation results for this request.
724
+ # We may still have to construct a context and ask the service after.
725
+
726
+ upc = []
727
+ upc << ident unless ident.nil? || ident.empty?
728
+
729
+ local_interaction.requested_action = action
730
+ authorisation = determine_authorisation( local_interaction )
731
+
732
+ # In addition, check security on any would-have-been-a-secured-header
733
+ # property.
734
+
735
+ return add_local_errors.call() if local_response.halt_processing?
736
+
737
+ Hoodoo::Client::Headers::HEADER_TO_PROPERTY.each do | rack_header, description |
738
+
739
+ next if description[ :secured ] != true
740
+ next if endpoint.send( description[ :property ] ).nil?
741
+
742
+ real_header = description[ :header ]
743
+
744
+ if (
745
+ session.respond_to?( :scoping ) == false ||
746
+ session.scoping.respond_to?( :authorised_http_headers ) == false ||
747
+ session.scoping.authorised_http_headers.respond_to?( :include? ) == false ||
748
+ (
749
+ session.scoping.authorised_http_headers.include?( rack_header ) == false &&
750
+ session.scoping.authorised_http_headers.include?( real_header ) == false
751
+ )
752
+ )
753
+
754
+ local_response.errors.add_error( 'platform.forbidden' )
755
+ break
756
+ end
757
+ end
758
+
759
+ return add_local_errors.call() if local_response.halt_processing?
760
+
761
+ # Construct the local request details.
762
+
763
+ local_request.uri_path_components = upc
764
+ local_request.uri_path_extension = ''
765
+
766
+ unless query_hash.nil?
767
+ query_hash = Hoodoo::Utilities.stringify( query_hash )
768
+
769
+ # This is for inter-resource local calls where a service author
770
+ # specifies ":_embed => 'foo'" accidentally, forgetting that it
771
+ # should be a single element array. It's such a common mistake
772
+ # that we tolerate it here. Same for "_reference".
773
+
774
+ data = query_hash[ '_embed' ]
775
+ query_hash[ '_embed' ] = [ data ] if data.is_a?( ::String ) || data.is_a?( ::Symbol )
776
+
777
+ data = query_hash[ '_reference' ]
778
+ query_hash[ '_reference' ] = [ data ] if data.is_a?( ::String ) || data.is_a?( ::Symbol )
779
+
780
+ # Regardless, make sure embed/reference array data contains strings.
781
+
782
+ query_hash[ '_embed' ].map!( &:to_s ) unless query_hash[ '_embed' ].nil?
783
+ query_hash[ '_reference' ].map!( &:to_s ) unless query_hash[ '_reference' ].nil?
784
+
785
+ process_query_hash( local_interaction, query_hash )
786
+ end
787
+
788
+ local_request.body = body_hash
789
+
790
+ # The inter-resource local backend does not accept or process the
791
+ # equivalent of the X-Resource-UUID "set the ID to <this>" HTTP
792
+ # header, so we do not call "maybe_update_body_data_for()" here;
793
+ # we only need to validate it.
794
+ #
795
+ if ( action == :create || action == :update )
796
+ validate_body_data_for( local_interaction )
797
+ end
798
+
799
+ return add_local_errors.call() if local_response.halt_processing?
800
+
801
+ # Can now, if necessary, do a final check with the target endpoint
802
+ # for authorisation.
803
+
804
+ if authorisation == Hoodoo::Services::Permissions::ASK
805
+ ask_for_authorisation( local_interaction )
806
+ return add_local_errors.call() if local_response.halt_processing?
807
+ end
808
+
809
+ # Dispatch the call.
810
+
811
+ debug_log( local_interaction, 'Dispatching local inter-resource call', local_request.body )
812
+ dispatch( local_interaction )
813
+
814
+ # If we get this far the interim session isn't needed. We might have
815
+ # exited early due to errors above and left this behind, but that's not
816
+ # the end of the world - it'll expire out of Memcached eventually.
817
+
818
+ if session &&
819
+ source_interaction.context &&
820
+ source_interaction.context.session &&
821
+ session.session_id != source_interaction.context.session.session_id
822
+
823
+ # Ignore errors, there's nothing much we can do about them and in
824
+ # the worst case we just have to wait for this to expire naturally.
825
+
826
+ session.delete_from_memcached()
827
+ end
828
+
829
+ # Extract the returned data, handling error conditions.
830
+
831
+ if local_response.halt_processing?
832
+ result = result_class.new
833
+ result.set_platform_errors(
834
+ annotate_errors_from_other_resource( local_response.errors )
835
+ )
836
+
837
+ else
838
+ body = local_response.body
839
+
840
+ if action == :list && body.is_a?( ::Array )
841
+ result = Hoodoo::Client::AugmentedArray.new( body )
842
+ result.dataset_size = local_response.dataset_size
843
+
844
+ elsif action != :list && body.is_a?( ::Hash )
845
+ result = Hoodoo::Client::AugmentedHash[ body ]
846
+
847
+ elsif local_request.deja_vu && body == ''
848
+ result = result_class.new
849
+
850
+ else
851
+ raise "Hoodoo::Services::Middleware: Unexpected response type '#{ body.class.name }' received from a local inter-resource call for action '#{ action }'"
852
+
853
+ end
854
+
855
+ end
856
+
857
+ result.response_options = Hoodoo::Client::Headers.x_header_to_options(
858
+ local_response.headers
859
+ )
860
+
861
+ return result
862
+ end
863
+
864
+ private
865
+
866
+ @@interfaces_have_public_methods = false
867
+
868
+ # Note internally that at least one interface in this Ruby process has
869
+ # a public interface. This means that the normal early exit for invalid
870
+ # or missing session keys cannot be performed; we have to continue down
871
+ # the processing chain as far as determining the target interface and
872
+ # action in order to find out if it's public or not and *then* check
873
+ # the session, if necessary. This is clearly less efficient and maybe a
874
+ # bit more risky.
875
+ #
876
+ def interfaces_have_public_methods
877
+ @@interfaces_have_public_methods = true
878
+ end
879
+
880
+ # Do any interfaces in this Ruby process have public methods, requiring
881
+ # no session data? If so returns +true+, else +false+.
882
+ #
883
+ def interfaces_have_public_methods?
884
+ @@interfaces_have_public_methods
885
+ end
886
+
887
+ # Private class method.
888
+ #
889
+ # Sets up a logger instance with appropriate log level for the environment
890
+ # (see ::environment) and any logger writers appropriate for that
891
+ # environment which can be constructed with no extra configuration data.
892
+ # In practice this just means a $stdout stream writer in development mode.
893
+ #
894
+ def self.set_up_basic_logging
895
+
896
+ @@external_logger = false
897
+ @@logger = Hoodoo::Logger.new
898
+
899
+ # RACK_ENV "test" and "development" environments have debug level
900
+ # logging. Other environments have info-level logging.
901
+
902
+ if self.environment.test? || self.environment.development?
903
+ @@logger.level = :debug
904
+ else
905
+ @@logger.level = :info
906
+ end
907
+
908
+ # The only environment that gets a simple writer we can create right
909
+ # now is "development", which always logs to stdout.
910
+
911
+ if self.environment.development?
912
+ @@logger.add( Hoodoo::Logger::StreamWriter.new( $stdout ) )
913
+ end
914
+ end
915
+
916
+ private_class_method( :set_up_basic_logging )
917
+
918
+ # Private class method.
919
+ #
920
+ # Assuming ::set_up_basic_logging has previously run, add in a file
921
+ # writer if in a test environment, or if in any other environment
922
+ # without an AMQP based queue available.
923
+ #
924
+ # The method does nothing if an external logger is in use.
925
+ #
926
+ # +base_path+:: Path to folder to use for logs; file "{environment}.log"
927
+ # may be written inside.
928
+ #
929
+ def self.add_file_logging( base_path )
930
+ return if @@external_logger == true
931
+
932
+ if self.environment.test? || self.on_queue? == false
933
+ log_path = File.join( base_path, "#{ self.environment }.log" )
934
+ file_writer = Hoodoo::Logger::FileWriter.new( log_path )
935
+
936
+ @@logger.add( file_writer )
937
+ end
938
+ end
939
+
940
+ private_class_method( :add_file_logging )
941
+
942
+ # Private class method.
943
+ #
944
+ # Assuming ::set_up_basic_logging has previously run, add in an Alchemy
945
+ # based queue writer if in a non-test environment with an AMQP based queue
946
+ # available.
947
+ #
948
+ # The method does nothing if an external logger is in use.
949
+ #
950
+ # +alchemy+:: A valid Alchemy endpoint instance upon which #send_message
951
+ # will be invoked, to send logging messages on to the queue.
952
+ #
953
+ def self.add_queue_logging( alchemy )
954
+ return if @@external_logger == true
955
+
956
+ if self.environment.test? == false && self.on_queue?
957
+ alchemy_queue_writer = Hoodoo::Services::Middleware::AMQPLogWriter.new( alchemy )
958
+
959
+ @@logger.add( alchemy_queue_writer )
960
+ end
961
+ end
962
+
963
+ private_class_method( :add_queue_logging )
964
+
965
+ # Given a Rack environment, find the Alchemy endpoint and if there is one,
966
+ # use this to initialize queue logging.
967
+ #
968
+ # +env+:: Rack 'env' parameter from e.g. Rack's invocation of #call.
969
+ #
970
+ def enable_alchemy_logging_from( env )
971
+ alchemy = env[ 'rack.alchemy' ]
972
+ unless alchemy.nil? || defined?( @@alchemy )
973
+ @@alchemy = alchemy
974
+ self.class.send( :add_queue_logging, @@alchemy ) unless @@alchemy.nil?
975
+ end
976
+ end
977
+
978
+ # Make an "inbound" call log based on the given interaction.
979
+ #
980
+ # +interaction+:: Hoodoo::Services::Interaction describing the inbound
981
+ # request. The +interaction_id+, +rack_request+ and
982
+ # +session+ data is used (the latter being optional).
983
+ # If +target_interface+ and +requested_action+ are
984
+ # available, body data _might_ be logged according to
985
+ # secure log settings in the interface; if these
986
+ # values are unset, body data is _not_ logged.
987
+ #
988
+ def log_inbound_request( interaction )
989
+
990
+ # Annoying dance required to extract all HTTP header data from Rack.
991
+
992
+ env = interaction.rack_request.env
993
+ headers = env.select do | key, value |
994
+ key.to_s.match( /^HTTP_/ )
995
+ end
996
+
997
+ headers[ 'CONTENT_TYPE' ] = env[ 'CONTENT_TYPE' ]
998
+ headers[ 'CONTENT_LENGTH' ] = env[ 'CONTENT_LENGTH' ]
999
+
1000
+ data = {
1001
+ :interaction_id => interaction.interaction_id,
1002
+ :payload => {
1003
+ :method => env[ 'REQUEST_METHOD', ],
1004
+ :scheme => env[ 'rack.url_scheme' ],
1005
+ :host => env[ 'SERVER_NAME' ],
1006
+ :port => env[ 'SERVER_PORT' ],
1007
+ :script => env[ 'SCRIPT_NAME' ],
1008
+ :path => env[ 'PATH_INFO' ],
1009
+ :query => env[ 'QUERY_STRING' ],
1010
+ :headers => headers
1011
+ }
1012
+ }
1013
+
1014
+ # Deal with body data and security issues.
1015
+
1016
+ secure = true
1017
+
1018
+ unless interaction.target_interface.nil? || interaction.requested_action.nil?
1019
+ secure_log_actions = interaction.target_interface.secure_log_for()
1020
+ secure_type = secure_log_actions[ interaction.requested_action ]
1021
+
1022
+ # Allow body logging if there's no security specified for this action
1023
+ # or the security is specified for the response only (since we log the
1024
+ # request here).
1025
+ #
1026
+ # This means values of :both or :request will leave "secure" unchanged,
1027
+ # as will any other unexpected value that might get specified.
1028
+
1029
+ secure = false if secure_type.nil? || secure_type == :response
1030
+
1031
+ # Fill in unrelated useful data since we know it is available here.
1032
+
1033
+ data[ :target ] = {
1034
+ :resource => ( interaction.target_interface.resource || '' ).to_s,
1035
+ :version => interaction.target_interface.version,
1036
+ :action => ( interaction.requested_action || '' ).to_s
1037
+ }
1038
+ end
1039
+
1040
+ # Compile the remaining log payload and send it.
1041
+
1042
+ unless secure
1043
+ body = interaction.rack_request.body.read( MAXIMUM_LOGGED_PAYLOAD_SIZE )
1044
+ interaction.rack_request.body.rewind()
1045
+
1046
+ data[ :payload ][ :body ] = body
1047
+ end
1048
+
1049
+ data[ :session ] = interaction.context.session.to_h unless interaction.context.session.nil?
1050
+
1051
+ @@logger.report(
1052
+ :info,
1053
+ :Middleware,
1054
+ :inbound,
1055
+ data
1056
+ )
1057
+
1058
+ return nil
1059
+ end
1060
+
1061
+ # This is part of the formalised structured logging interface upon which
1062
+ # external entites might depend. Change with care.
1063
+ #
1064
+ # For a given interaction, log the response *after the fact* of calling
1065
+ # a resource implementation, using the target interface's resource name
1066
+ # for the structured log entry's "component" field.
1067
+ #
1068
+ # +interaction+:: Hoodoo::Services::Middleware::Interaction instance for
1069
+ # the interaction currently being logged.
1070
+ #
1071
+ def log_call_result( interaction )
1072
+
1073
+ context = interaction.context
1074
+ interface = interaction.target_interface
1075
+ action = interaction.requested_action
1076
+
1077
+ # In #respond_for, error logging is handled. Since we generate a UUID
1078
+ # up front for errors (since a UUID is returned), we must not log under
1079
+ # that UUID twice. So in auto-log, only log success cases. Leave the
1080
+ # last-bastion-of-response that is #respond_for to deal with the rest.
1081
+ #
1082
+ return if ( context.response.halt_processing? )
1083
+
1084
+ # Data as per Hoodoo::Logger.
1085
+
1086
+ data = {
1087
+ :interaction_id => interaction.interaction_id,
1088
+ :session => ( interaction.context.session || {} ).to_h,
1089
+ :target => {
1090
+ :resource => ( interface.resource || '' ).to_s,
1091
+ :version => interface.version,
1092
+ :action => ( action || '' ).to_s,
1093
+ }
1094
+ }
1095
+
1096
+ # Don't bother logging list responses - they could be huge - instead
1097
+ # log all list-related parameters from the inbound request. At least
1098
+ # we don't have to worry about security in that case.
1099
+ #
1100
+ # For other kinds of data, check the secure actions to see if the body
1101
+ # should be included.
1102
+
1103
+ if context.response.body.is_a?( ::Array )
1104
+ attributes = %i( list_offset list_limit list_sort_data list_search_data list_filter_data embeds references )
1105
+ data[ :payload ] = {}
1106
+
1107
+ attributes.each do | attribute |
1108
+ data[ attribute ] = context.request.send( attribute )
1109
+ end
1110
+ else
1111
+ secure = true
1112
+
1113
+ unless interface.nil? || action.nil?
1114
+ secure_log_actions = interface.secure_log_for()
1115
+ secure_type = secure_log_actions[ action ]
1116
+
1117
+ # Allow body logging if there's no security specified for this action
1118
+ # or the security is specified for the request only (since we log the
1119
+ # response here).
1120
+ #
1121
+ # That means values of :both or :response will leave secure untouched,
1122
+ # as will any other unexpected value that might get specified.
1123
+
1124
+ secure = false if secure_type.nil? || secure_type == :request
1125
+ end
1126
+
1127
+ unless secure
1128
+ data[ :payload ] = context.response.body
1129
+ end
1130
+ end
1131
+
1132
+ @@logger.report(
1133
+ :info,
1134
+ interface.resource,
1135
+ "middleware_#{ action }",
1136
+ data
1137
+ )
1138
+ end
1139
+
1140
+ # This is part of the formalised structured logging interface upon which
1141
+ # external entites might depend. Change with care.
1142
+ #
1143
+ # For a given service interface, an implementation of which is receiving
1144
+ # a given action under the given request context, log the response *after
1145
+ # the fact* of calling the implementation, using the target interface's
1146
+ # resource name for the structured log entry's "component" field.
1147
+ #
1148
+ # +interaction+:: Hoodoo::Services::Middleware::Interaction instance for
1149
+ # the interaction currently being logged.
1150
+ #
1151
+ def log_outbound_response( interaction, rack_data )
1152
+ secure = true
1153
+ id = nil
1154
+ level = if interaction.context.response.halt_processing?
1155
+ :error
1156
+ else
1157
+ :info
1158
+ end
1159
+
1160
+ data = {
1161
+ :interaction_id => interaction.interaction_id,
1162
+ :payload => {
1163
+ :http_status_code => rack_data[ 0 ].to_i,
1164
+ :http_headers => rack_data[ 1 ]
1165
+ }
1166
+ }
1167
+
1168
+ unless interaction.target_interface.nil? || interaction.requested_action.nil?
1169
+ secure_log_actions = interaction.target_interface.secure_log_for()
1170
+ secure_type = secure_log_actions[ interaction.requested_action ]
1171
+
1172
+ # Allow body logging if there's no security specified for this action
1173
+ # or the security is specified for the request only (since we log the
1174
+ # response here).
1175
+ #
1176
+ # That means values of :both or :response will leave secure untouched,
1177
+ # as will any other unexpected value that might get specified.
1178
+
1179
+ secure = false if secure_type.nil? || secure_type == :request
1180
+
1181
+ data[ :target ] = {
1182
+ :resource => ( interaction.target_interface.resource || '' ).to_s,
1183
+ :version => interaction.target_interface.version,
1184
+ :action => ( interaction.requested_action || '' ).to_s
1185
+ }
1186
+ end
1187
+
1188
+ if secure == false || level == :error
1189
+ body = ''
1190
+ rack_data[ 2 ].each { | thing | body << thing.to_s }
1191
+
1192
+ if interaction.context.response.halt_processing?
1193
+ begin
1194
+ # Error cases should be infrequent, so we can "be nice" and re-parse
1195
+ # the returned body for structured logging only. We don't do this for
1196
+ # successful responses as we assume those will be much more frequent
1197
+ # and the extra parsing step would be heavy overkill for a log.
1198
+ #
1199
+ # This also means we can (in theory) extract the intended resource
1200
+ # UUID and include that in structured log data to make sure any
1201
+ # persistence layers store the item as an error with the correct ID.
1202
+
1203
+ body = ::JSON.parse( body )
1204
+ id = body[ 'id' ]
1205
+ rescue
1206
+ end
1207
+
1208
+ else
1209
+ body = body[ 0 .. ( MAXIMUM_LOGGED_RESPONSE_SIZE - 1 ) ] << '...' if ( body.size > MAXIMUM_LOGGED_RESPONSE_SIZE )
1210
+
1211
+ end
1212
+
1213
+ data[ :payload ][ :response_body ] = body
1214
+ end
1215
+
1216
+ data[ :id ] = id unless id.nil?
1217
+ data[ :session ] = interaction.context.session.to_h unless interaction.context.session.nil?
1218
+
1219
+ @@logger.report(
1220
+ level,
1221
+ :Middleware,
1222
+ :outbound,
1223
+ data
1224
+ )
1225
+ end
1226
+
1227
+ # Log a debug message. Pass optional extra arguments which will be used as
1228
+ # strings that get appended to the log message.
1229
+ #
1230
+ # THIS IS INSCURE. Sensitive data might be logged. DO NOT USE IN DEPLOYED
1231
+ # ENVIRONMENTS. At the time of writing, Hoodoo ensures this by only using
1232
+ # debug logging in 'development' or 'test' environments.
1233
+ #
1234
+ # Before calling, +@rack_request+ must be set up with the Rack::Request
1235
+ # instance for the call environment.
1236
+ #
1237
+ # +interaction+:: Hoodoo::Services::Middleware::Interaction instance for
1238
+ # the interaction currently being logged.
1239
+ #
1240
+ # *args:: Optional extra arguments used as strings to add to the
1241
+ # log message.
1242
+ #
1243
+ def debug_log( interaction, *args )
1244
+
1245
+ # Even though the logger itself would check this itself, check and exit
1246
+ # early here to avoid object creation and string composition overheads
1247
+ # from the code that would otherwise run even in non-debug environments.
1248
+
1249
+ return unless @@logger.report?( :debug )
1250
+
1251
+ scheme = interaction.rack_request.scheme || 'unknown_scheme'
1252
+ host_with_port = interaction.rack_request.host_with_port || 'unknown_host'
1253
+ full_path = interaction.rack_request.fullpath || '/unknown_path'
1254
+
1255
+ data = {
1256
+ :full_uri => "#{ scheme }://#{ host_with_port }#{ full_path }",
1257
+ :interaction_id => interaction.interaction_id,
1258
+ :payload => { 'args' => args }
1259
+ }
1260
+
1261
+ data[ :session ] = interaction.context.session.to_h unless interaction.context.session.nil?
1262
+
1263
+ @@logger.report(
1264
+ :debug,
1265
+ :Middleware,
1266
+ :log,
1267
+ data
1268
+ )
1269
+ end
1270
+
1271
+ # Handle responding to Rack for a given interaction. Logs that we're
1272
+ # responding and returns #for_rack data so that in (e.g.) #call the
1273
+ # idiom can be:
1274
+ #
1275
+ # return respond_for( ... )
1276
+ #
1277
+ # ...to log the response and return data to Rack all in one go.
1278
+ #
1279
+ # +interaction+:: Hoodoo::Services::Middleware::Interaction instance
1280
+ # describing the current interaction, including a
1281
+ # valid 'response' object if you're using that for
1282
+ # the response data.
1283
+ #
1284
+ # +preflight+:: Optional. If +true+, this is a CORS preflight
1285
+ # requires and should contain no body data; else it
1286
+ # is a normal response and must contain body data.
1287
+ # Default is +false+.
1288
+ #
1289
+ # Returns data suitable for giving directly back to Rack.
1290
+ #
1291
+ def respond_for( interaction, preflight = false )
1292
+ interaction.context.response.body = '' if preflight
1293
+
1294
+ rack_data = interaction.context.response.for_rack()
1295
+ log_outbound_response( interaction, rack_data )
1296
+
1297
+ return rack_data
1298
+ end
1299
+
1300
+ # When a request includes an <tt>X-Deja-Vu</tt> header and a service
1301
+ # returns a result that includes any errors for creation or deletion
1302
+ # events, we detect any in the collection without a given code;
1303
+ # +generic.invalid_duplication+ for creation, or
1304
+ # +generic.not_found+ for deletion.
1305
+ #
1306
+ # If we find any then normal error handling continues, otherwise the
1307
+ # errors are cleared, an HTTP response code of 204 is setup in the
1308
+ # +response+ object and body data is cleared. <tt>X-Deja-Vu</tt> is
1309
+ # set in the response too, with a +confirmed+ value.
1310
+ #
1311
+ # +interaction+:: Hoodoo::Services::Middleware::Interaction instance
1312
+ # describing the current interaction. May be updated
1313
+ # on exit with new response status code, body etc.
1314
+ #
1315
+ def remove_expected_errors_when_experiencing_deja_vu( interaction )
1316
+ interesting_code = case interaction.requested_action
1317
+ when :create
1318
+ 'generic.invalid_duplication'
1319
+ when :delete
1320
+ 'generic.not_found'
1321
+ else
1322
+ return
1323
+ end
1324
+
1325
+ other_errors = interaction.context.response.errors.errors.detect do | error_hash |
1326
+ error_hash[ 'code' ] != interesting_code
1327
+ end
1328
+
1329
+ if other_errors.nil?
1330
+ interaction.context.response.errors = Hoodoo::Errors.new()
1331
+ interaction.context.response.http_status_code = 204
1332
+ interaction.context.response.body = ''
1333
+
1334
+ interaction.context.response.add_header(
1335
+ 'X-Deja-Vu',
1336
+ "confirmed",
1337
+ true # Overwrite
1338
+ )
1339
+ end
1340
+ end
1341
+
1342
+ # Announce the presence of the service endpoints to known interested
1343
+ # parties.
1344
+ #
1345
+ # ONLY CALL AS PART OF INSTANCE CREATION (from #initialize).
1346
+ #
1347
+ # +services+:: Array of Hashes describing service information.
1348
+ #
1349
+ # Hash keys/values are as follows:
1350
+ #
1351
+ # +regexp+:: A regular expression for a URI path which, if matched,
1352
+ # means that this service endpoint is being called.
1353
+ # +path+:: The endpoint path that the regexp would match, with
1354
+ # leading "/" (e.g. "/v1/products")
1355
+ # +interface+:: The Interface subclass for this endpoint.
1356
+ # +implementation+:: The Implementation subclass instance for this
1357
+ # endpoint.
1358
+ #
1359
+ def announce_presence_of( services )
1360
+
1361
+ # Note the RARE LEGITIMATE USE of an instance variable here. It will
1362
+ # be shared across potentially many threads with the same instance
1363
+ # driven through Rack. Presence announcements to the Discoverer are
1364
+ # made only upon object initialisation and remain valid (and
1365
+ # unchanged) for its lifetime.
1366
+ #
1367
+ # A class variable is wrong, as entirely new instances of the service
1368
+ # middleware might be stood up in one process and could potentially
1369
+ # be handling different services. This is typically only the case for
1370
+ # running tests, but *might* happen elsewhere too. In any event, we
1371
+ # don't want announcements in one instance to pollute the discovery
1372
+ # data in another (especially the records of which services were
1373
+ # announced by, and therefore must be local to, an instance).
1374
+
1375
+ if self.class.on_queue?
1376
+
1377
+ @discoverer ||= Hoodoo::Services::Discovery::ByConsul.new
1378
+
1379
+ services.each do | service |
1380
+ interface = service.interface_class
1381
+
1382
+ @discoverer.announce(
1383
+ interface.resource,
1384
+ interface.version
1385
+ )
1386
+ end
1387
+
1388
+ else
1389
+
1390
+ @discoverer ||= Hoodoo::Services::Discovery::ByDRb.new
1391
+
1392
+ # Rack provides no formal way to find out our host or port before a
1393
+ # request arrives, because in part it might change due to clustering.
1394
+ # For local development on an assumed single instance server, we can
1395
+ # ask Ruby itself for all Rack::Server instances, expecting just one.
1396
+ # If there isn't just one, we rely on the Rack monkey patch or a
1397
+ # hard coded default.
1398
+
1399
+ host = nil
1400
+ port = nil
1401
+
1402
+ if defined?( ::Rack ) && defined?( ::Rack::Server )
1403
+ servers = ObjectSpace.each_object( ::Rack::Server )
1404
+
1405
+ if servers.count == 1
1406
+ server = servers.first
1407
+ host = server.options[ :Host ]
1408
+ port = server.options[ :Port ]
1409
+ end
1410
+ end
1411
+
1412
+ host = @@recorded_host if host.nil? && defined?( @@recorded_host )
1413
+ port = @@recorded_port if port.nil? && defined?( @@recorded_port )
1414
+
1415
+ # Under test, ensure a simulation of an available host and port is
1416
+ # always available for discovery-related tests.
1417
+
1418
+ if ( self.class.environment.test? )
1419
+ host ||= '127.0.0.1'
1420
+ port ||= '9292'
1421
+ end
1422
+
1423
+ # Announce the resource endpoints unless we are still missing a host
1424
+ # or port. Implication is 'racksh'.
1425
+
1426
+ unless host.nil? || port.nil?
1427
+ services.each do | service |
1428
+ interface = service.interface_class
1429
+
1430
+ @discoverer.announce(
1431
+ interface.resource,
1432
+ interface.version,
1433
+ {
1434
+ :host => host,
1435
+ :port => port,
1436
+ :path => service.base_path
1437
+ }
1438
+ )
1439
+ end
1440
+ end
1441
+ end
1442
+ end
1443
+
1444
+ # Load a session from Memcached on the basis of a session ID header
1445
+ # in the current interaction's Rack request data.
1446
+ #
1447
+ # On exit, the interaction context may have been updated. Be sure to
1448
+ # check +interaction.context.response.halt_processing?+ to see if
1449
+ # processing should abort and return immediately.
1450
+ #
1451
+ # +interaction+:: Hoodoo::Services::Middleware::Interaction instance
1452
+ # describing the current interaction.
1453
+ #
1454
+ def load_session_into( interaction )
1455
+
1456
+ test_session = self.class.test_session()
1457
+ session = nil
1458
+ session_id = interaction.rack_request.env[ 'HTTP_X_SESSION_ID' ]
1459
+
1460
+ if session_id != nil && ( test_session.nil? || test_session.session_id != session_id )
1461
+ session = Hoodoo::Services::Session.new(
1462
+ :memcached_host => self.class.memcached_host(),
1463
+ :session_id => session_id
1464
+ )
1465
+
1466
+ result = session.load_from_memcached!( session_id )
1467
+ session = nil if result != :ok
1468
+ elsif ( self.class.environment.test? || self.class.environment.development? )
1469
+ interaction.using_test_session()
1470
+ session = self.class.test_session()
1471
+ end
1472
+
1473
+ # If there's no session and no local interfaces have any public
1474
+ # methods (everything is protected) then bail out early, as the
1475
+ # request can't possibly succeed.
1476
+
1477
+ if session.nil? && interfaces_have_public_methods? == false
1478
+ return interaction.context.response.add_error( 'platform.invalid_session' )
1479
+ end
1480
+
1481
+ # Update the interaction's context with the new session. Since
1482
+ # the context data is exposed to service implementations, the
1483
+ # session reference is read-only; don't break that protection;
1484
+ # instead build and use a replacement context.
1485
+
1486
+ if session != interaction.context.session
1487
+ updated_context = Hoodoo::Services::Context.new(
1488
+ session,
1489
+ interaction.context.request,
1490
+ interaction.context.response,
1491
+ interaction
1492
+ )
1493
+ interaction.context = updated_context
1494
+ end
1495
+ end
1496
+
1497
+ # Run request preprocessing - common actions that occur prior to any
1498
+ # service instance selection or service-specific processing.
1499
+ #
1500
+ # Returns +nil+ if successful.
1501
+ #
1502
+ # If the method returns something else, it's a Rack response; an early
1503
+ # and immediate response has been created. Return this (through whatever
1504
+ # call chain is necessary) as the return value for #call.
1505
+ #
1506
+ # After calling, be sure to check +@response.halt_processing?+ to
1507
+ # see if processing should abort and return immediately.
1508
+ #
1509
+ # An "inbound" code log entry is generated *without* body data for CORS
1510
+ # requests or for requests which have already generated errors. In the
1511
+ # event the request so far looks good, no inbound log entry is made in
1512
+ # order to give later processing stages a chance to determine if the
1513
+ # body data could be safely logged (since it's useful to have). Thus,
1514
+ # later processing stages will still need to make a call to
1515
+ # "log_inbound_request".
1516
+ #
1517
+ # +interaction+:: Hoodoo::Services::Middleware::Interaction instance
1518
+ # describing the current interaction. Parts of this may
1519
+ # be updated on exit.
1520
+ #
1521
+ def preprocess( interaction )
1522
+
1523
+
1524
+ # =======================================================================
1525
+ # Additions here may require corresponding additions to the
1526
+ # inter-resource local call code.
1527
+ # =======================================================================
1528
+
1529
+
1530
+ # Always log the inbound request early, in case of exceptions. Body data
1531
+ # will not be logged as the interaction contains no information on the
1532
+ # target resource or action, so we won't accidentally log secure data in
1533
+ # the inbound payload (if any).
1534
+
1535
+ log_inbound_request( interaction )
1536
+ set_common_response_headers( interaction )
1537
+
1538
+ # Potential special-case early exit for CORS preflight.
1539
+
1540
+ early_exit = deal_with_cors( interaction )
1541
+ return early_exit unless early_exit.nil?
1542
+
1543
+ # If we reach here it's a normal request, not CORS preflight.
1544
+
1545
+ deal_with_content_type_header( interaction )
1546
+ deal_with_language_headers( interaction )
1547
+
1548
+ # Load the session and then, in the context of a loaded session, process
1549
+ # any remaining extension ("X-...") HTTP headers, checking up on secured
1550
+ # headers in passing.
1551
+
1552
+ load_session_into( interaction )
1553
+ deal_with_x_headers( interaction )
1554
+
1555
+ return nil
1556
+ end
1557
+
1558
+ # Process the client's call. The heart of service routing and application
1559
+ # invocation. Relies entirely on data assembled during initialisation of
1560
+ # this middleware instance or during handling in #call.
1561
+ #
1562
+ # +interaction+:: Hoodoo::Services::Middleware::Interaction instance
1563
+ # describing the current interaction.
1564
+ #
1565
+ def process( interaction )
1566
+
1567
+
1568
+ # =======================================================================
1569
+ # Additions here may require corresponding additions to the
1570
+ # inter-resource local call code.
1571
+ # =======================================================================
1572
+
1573
+
1574
+ response = interaction.context.response # Convenience
1575
+
1576
+ # Select a service based on the escaped URI's path. If we find none,
1577
+ # then there's no matching endpoint; badly routed request; 404. If we
1578
+ # find many, raise an exception and rely on the exception handler to
1579
+ # send back a 500.
1580
+
1581
+ uri_path = CGI.unescape( interaction.rack_request.path() )
1582
+
1583
+ selected_path_data = nil
1584
+ selected_services = @@services.select do | service_data |
1585
+ path_data = process_uri_path( uri_path, service_data.routing_regexp )
1586
+
1587
+ if path_data.nil?
1588
+ false
1589
+ else
1590
+ selected_path_data = path_data
1591
+ true
1592
+ end
1593
+ end
1594
+
1595
+ if selected_services.size == 0
1596
+ return response.add_error(
1597
+ 'platform.not_found',
1598
+ 'reference' => { :entity_name => '' }
1599
+ )
1600
+ elsif selected_services.size > 1
1601
+ raise( 'Multiple service endpoint matches - internal server configuration fault' )
1602
+ else
1603
+ selected_service = selected_services[ 0 ]
1604
+ end
1605
+
1606
+ # Otherwise, update the interaction data and response data in light
1607
+ # of the chosen service's information.
1608
+
1609
+ uri_path_components, uri_path_extension = selected_path_data
1610
+ interface = selected_service.interface_class
1611
+ implementation = selected_service.implementation_instance
1612
+
1613
+ interaction.target_interface = interface
1614
+ interaction.target_implementation = implementation
1615
+
1616
+ update_response_for( interaction.context.response, interface )
1617
+
1618
+ # Check for a supported, session-accessible action.
1619
+
1620
+ http_method = interaction.rack_request.request_method
1621
+ action = determine_action( http_method, uri_path_components.empty? )
1622
+ interaction.requested_action = action
1623
+
1624
+ # We finally have enough data to log the inbound call again, with body
1625
+ # data included if allowed by the target resource.
1626
+
1627
+ log_inbound_request( interaction )
1628
+
1629
+ authorisation = determine_authorisation( interaction )
1630
+ return if response.halt_processing?
1631
+
1632
+ # Looks good so far, so start filling in request details.
1633
+
1634
+ request = interaction.context.request # Convenience
1635
+ request.uri_path_components = uri_path_components
1636
+ request.uri_path_extension = uri_path_extension
1637
+
1638
+ process_query_string( interaction )
1639
+
1640
+ return if response.halt_processing?
1641
+
1642
+ # There should be no spurious path data for "list" or "create" actions -
1643
+ # only "show", "update" and "delete" take extra data via the URL's path.
1644
+ # Conversely, other actions require it.
1645
+
1646
+ if action == :list || action == :create
1647
+ return response.add_error( 'platform.malformed',
1648
+ 'message' => 'Unexpected path components for this action',
1649
+ 'reference' => { :action => action } ) unless uri_path_components.empty?
1650
+ else
1651
+ return response.add_error( 'platform.malformed',
1652
+ 'message' => 'Expected path components identifying target resource instance for this action',
1653
+ 'reference' => { :action => action } ) if uri_path_components.empty?
1654
+ end
1655
+
1656
+ # There should be no spurious body data for anything other than "create"
1657
+ # or "update" actions. This is one of the last things we do as it is
1658
+ # potentially very heavyweight.
1659
+ #
1660
+ # To try and be helpful to clients which may use HTTP libraries that
1661
+ # always write body data of some kind, we permit white space; so always
1662
+ # read the body, then strip the white space from it.
1663
+ #
1664
+ # Start by reading only a limited amount of data. Then try to read more.
1665
+ # According the input stream documentation of the Rack specification:
1666
+ #
1667
+ # http://rubydoc.info/github/rack/rack/master/file/SPEC
1668
+ #
1669
+ # ...then when we call "read" with a length value and there's no more
1670
+ # data to read, it should return nil. If it doesn't, the payload is
1671
+ # too big. Reject it.
1672
+
1673
+ body = interaction.rack_request.body.read( MAXIMUM_PAYLOAD_SIZE )
1674
+
1675
+ unless ( body.nil? || body.is_a?( ::String ) ) && interaction.rack_request.body.read( MAXIMUM_PAYLOAD_SIZE ).nil?
1676
+ return response.add_error( 'platform.malformed',
1677
+ 'message' => 'Body data exceeds configured maximum size for platform' )
1678
+ end
1679
+
1680
+ debug_log( interaction, 'Raw body data read successfully', body )
1681
+
1682
+ if action == :create || action == :update
1683
+
1684
+ parse_body_string_into( interaction, body )
1685
+ return if response.halt_processing?
1686
+
1687
+ validate_body_data_for( interaction )
1688
+ return if response.halt_processing?
1689
+
1690
+ if action == :create # Important! For-create-only.
1691
+ maybe_update_body_data_for( interaction )
1692
+ return if response.halt_processing?
1693
+ end
1694
+
1695
+ elsif body.nil? == false && body.to_s.strip.length > 0
1696
+
1697
+ return response.add_error( 'platform.malformed',
1698
+ 'message' => 'Unexpected body data for this action',
1699
+ 'reference' => { :action => action } )
1700
+
1701
+ end
1702
+
1703
+ debug_log( interaction, 'Dispatching with parsed body data', request.body )
1704
+
1705
+ # Can now, if necessary, do a final check with the resource endpoint
1706
+ # for authorisation because the request data is fully populated so
1707
+ # the resource implementation's "verify" method has something to use.
1708
+
1709
+ if authorisation == Hoodoo::Services::Permissions::ASK
1710
+ ask_for_authorisation( interaction )
1711
+ return if response.halt_processing?
1712
+ end
1713
+
1714
+ # Finally - dispatch to service.
1715
+
1716
+ dispatch( interaction )
1717
+ end
1718
+
1719
+ # Dispatch a call to the given implementation, with before/after actions.
1720
+ #
1721
+ # +interaction+:: Hoodoo::Services::Middleware::Interaction instance
1722
+ # describing the current interaction.
1723
+ #
1724
+ def dispatch( interaction )
1725
+
1726
+ # Set up some convenience variables
1727
+
1728
+ interface = interaction.target_interface
1729
+ implementation = interaction.target_implementation
1730
+ action = interaction.requested_action
1731
+ context = interaction.context
1732
+
1733
+ # Benchmark the "inner" dispatch call
1734
+
1735
+ dispatch_time = ::Benchmark.realtime do
1736
+
1737
+ block = Proc.new do
1738
+
1739
+ # Before/after callbacks are invoked always, even if errors are
1740
+ # added to the response object during processing. If this matters
1741
+ # to 'after' code, it must check "context.response.halt_processing?"
1742
+ # itself.
1743
+
1744
+ implementation.before( context ) if implementation.respond_to?( :before )
1745
+ implementation.send( action, context ) unless context.response.halt_processing?
1746
+ implementation.after( context ) if implementation.respond_to?( :after )
1747
+
1748
+ end
1749
+
1750
+ if ( defined?( ::ActiveRecord ) && defined?( ::ActiveRecord::Base ) )
1751
+ ::ActiveRecord::Base.connection_pool.with_connection( &block )
1752
+ else
1753
+ block.call
1754
+ end
1755
+
1756
+ if context.request.deja_vu && context.response.halt_processing?
1757
+ remove_expected_errors_when_experiencing_deja_vu( interaction )
1758
+ end
1759
+
1760
+ log_call_result( interaction )
1761
+
1762
+ end # "Benchmark.realtime do"
1763
+
1764
+ context.response.add_header(
1765
+ 'X-Service-Response-Time',
1766
+ "#{ dispatch_time.inspect } seconds",
1767
+ true # Overwrite
1768
+ )
1769
+ end
1770
+
1771
+ # Run request preprocessing - common actions that occur after service
1772
+ # instance selection and service-specific processing.
1773
+ #
1774
+ # On exit, interaction data may have been updated.
1775
+ #
1776
+ # +interaction+:: Hoodoo::Services::Middleware::Interaction instance
1777
+ # describing the current interaction.
1778
+ #
1779
+ def postprocess( interaction )
1780
+
1781
+
1782
+ # =======================================================================
1783
+ # Additions here may require corresponding additions to the
1784
+ # inter-resource local call code.
1785
+ # =======================================================================
1786
+
1787
+
1788
+ # TODO: Nothing?
1789
+ #
1790
+ # This is only called on service *success*. Potentially we can hook in
1791
+ # the validation of the service's output (internal self-check) according
1792
+ # the expected returned Resource that the interface class defines (see
1793
+ # the "interface.resource" property), so long as it's defined in the
1794
+ # Hoodoo::Data::Resources collection (or extend the DSL to take a Class).
1795
+ #
1796
+ # The outgoing response body in the service response object is an Array
1797
+ # or Hash. We can check, for known resource types, the "language" of the
1798
+ # first item & set Content-Language, assuming an internationalised type.
1799
+ #
1800
+ # Can certainly make sure that we enforce all-call-resource-representation
1801
+ # here - for 200 cases, *all* calls should be returning a representation
1802
+ # or a list (even if the list is empty). That includes 'delete' ("here
1803
+ # is what I just deleted" - aids stack-like coding in clients).
1804
+
1805
+ end
1806
+
1807
+ # Check the client's +Content-Type+ header and if it doesn't ask for the
1808
+ # supported content types or text encodings, set a response error to force
1809
+ # a halt of any further processing (subject to the caller checking for
1810
+ # response errors afterwards).
1811
+ #
1812
+ # If successful, updates the given interaction data with the request
1813
+ # content type and encoding.
1814
+ #
1815
+ # +interaction+:: Hoodoo::Services::Middleware::Interaction instance
1816
+ # describing the current interaction. Updated on exit.
1817
+ #
1818
+ def deal_with_content_type_header( interaction )
1819
+ content_type = interaction.rack_request.media_type
1820
+ content_encoding = interaction.rack_request.content_charset
1821
+
1822
+ content_type.downcase! unless content_type.nil?
1823
+ content_encoding.downcase! unless content_encoding.nil?
1824
+
1825
+ unless SUPPORTED_MEDIA_TYPES.include?( content_type ) &&
1826
+ SUPPORTED_ENCODINGS.include?( content_encoding )
1827
+
1828
+ interaction.context.response.errors.add_error(
1829
+ 'platform.malformed',
1830
+ 'message' => "Content-Type '#{ interaction.rack_request.content_type || "<unknown>" }' does not match supported types '#{ SUPPORTED_MEDIA_TYPES }' and/or encodings '#{ SUPPORTED_ENCODINGS }'"
1831
+ )
1832
+
1833
+ # Avoid incorrect Content-Type in responses, which otherwise "inherits"
1834
+ # from inbound type and encoding.
1835
+ #
1836
+ content_type = content_encoding = nil
1837
+
1838
+ end
1839
+
1840
+ interaction.requested_content_type = content_type
1841
+ interaction.requested_content_encoding = content_encoding
1842
+ end
1843
+
1844
+ # Extract the +Content-Language+ header value from the client, or if that
1845
+ # is missing, +Accept-Language+. Uses it, or a default of "en-nz",
1846
+ # converts to lower case and sets the value as the interaction's request's
1847
+ # locale value.
1848
+ #
1849
+ # We support neither a list of preferences nor "qvalues", so if there is
1850
+ # a list, we only take the first item; if there is a qvalue, we strip it
1851
+ # leaving just the language part, e.g. "en-gb".
1852
+ #
1853
+ # +interaction+:: Hoodoo::Services::Middleware::Interaction instance
1854
+ # describing the current interaction. Updated on exit.
1855
+ #
1856
+ def deal_with_language_headers( interaction )
1857
+ lang = interaction.rack_request.env[ 'HTTP_CONTENT_LANGUAGE' ]
1858
+ lang = interaction.rack_request.env[ 'HTTP_ACCEPT_LANGUAGE' ] if lang.nil? || lang.empty?
1859
+
1860
+ unless lang.nil? || lang.empty?
1861
+ # E.g. "Accept-Language: da, en-gb;q=0.8, en;q=0.7" => 'da'
1862
+ lang = lang.split( ',' )[ 0 ]
1863
+ lang = lang.split( ';' )[ 0 ]
1864
+ end
1865
+
1866
+ lang = 'en-nz' if lang.nil? || lang.empty?
1867
+ interaction.context.request.locale = lang.downcase
1868
+ end
1869
+
1870
+ # Extract all +X-Foo+ headers from Hoodoo::Client::Headers'
1871
+ # +HEADER_TO_PROPERTY+ and store relevant information in the request data
1872
+ # based on the header mappings. Security checks are done for secured
1873
+ # headers. Validation is performed according to validation Procs in the
1874
+ # mappings.
1875
+ #
1876
+ # +interaction+:: Hoodoo::Services::Middleware::Interaction instance
1877
+ # describing the current interaction. Updated on exit.
1878
+ #
1879
+ def deal_with_x_headers( interaction )
1880
+
1881
+ # Set up some convenience variables
1882
+
1883
+ session = interaction.context.session
1884
+ rack_env = interaction.rack_request.env
1885
+ request = interaction.context.request
1886
+
1887
+ Hoodoo::Client::Headers::HEADER_TO_PROPERTY.each do | rack_header, description |
1888
+
1889
+ header_value = rack_env[ rack_header ]
1890
+ next if header_value.nil?
1891
+
1892
+ # Don't do anything else if this header is secured but prohibited.
1893
+
1894
+ real_header = description[ :header ]
1895
+
1896
+ if description[ :secured ] == true &&
1897
+ (
1898
+ session.respond_to?( :scoping ) == false ||
1899
+ session.scoping.respond_to?( :authorised_http_headers ) == false ||
1900
+ session.scoping.authorised_http_headers.respond_to?( :include? ) == false ||
1901
+ (
1902
+ session.scoping.authorised_http_headers.include?( rack_header ) == false &&
1903
+ session.scoping.authorised_http_headers.include?( real_header ) == false
1904
+ )
1905
+ )
1906
+
1907
+ interaction.context.response.errors.add_error( 'platform.forbidden' )
1908
+
1909
+ return # EARLY EXIT
1910
+ end
1911
+
1912
+ # If we reach here the header is either not secured, or is permitted.
1913
+ # Check to see if the value is OK.
1914
+
1915
+ property_writer = description[ :property_writer ]
1916
+ property_value = description[ :property_proc ].call( header_value )
1917
+
1918
+ if property_value.nil?
1919
+ interaction.context.response.errors.add_error(
1920
+ 'generic.malformed',
1921
+ {
1922
+ :message => "#{ real_header } header value '#{ header_value }' is invalid",
1923
+ :reference => { :header_name => real_header }
1924
+ }
1925
+ )
1926
+
1927
+ return # EARLY EXIT
1928
+ end
1929
+
1930
+ # All good!
1931
+
1932
+ request.send( property_writer, property_value )
1933
+
1934
+ end
1935
+ end
1936
+
1937
+ # Preprocessing stage that sets up common headers required in any response.
1938
+ # May vary according to inbound content type requested. If processing was
1939
+ # aborted early (e.g. missing inbound Content-Type) we may fall to defaults.
1940
+ #
1941
+ # (At the time of writing, platform documentations say we're JSON only - but
1942
+ # there's an strong chance of e.g. XML representation being demanded later).
1943
+ #
1944
+ # +response+:: Hoodoo::Services::Response instance to update.
1945
+ #
1946
+ def set_common_response_headers( interaction )
1947
+ interaction.context.response.add_header( 'X-Interaction-ID', interaction.interaction_id )
1948
+ interaction.context.response.add_header( 'Content-Type', "#{ interaction.requested_content_type || 'application/json' }; charset=#{ interaction.requested_content_encoding || 'utf-8' }" )
1949
+ end
1950
+
1951
+ # Simplisitic CORS preflight handler.
1952
+ #
1953
+ # * http://www.w3.org/TR/cors/
1954
+ # * http://www.w3.org/TR/cors/#preflight-request
1955
+ # * http://enable-cors.org
1956
+ # * https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS
1957
+ #
1958
+ # +interaction+:: Hoodoo::Services::Middleware::Interaction instance
1959
+ # describing the current interaction.
1960
+ #
1961
+ # Returns +nil+ if the request can continue to be processed, else an early
1962
+ # exit CORS response has already been generated; processing should stop now.
1963
+ #
1964
+ def deal_with_cors( interaction )
1965
+ headers = interaction.rack_request.env
1966
+ origin = headers[ 'HTTP_ORIGIN' ]
1967
+
1968
+ unless ( origin.nil? )
1969
+ if interaction.rack_request.request_method == 'OPTIONS'
1970
+ requested_method = headers[ 'HTTP_ACCESS_CONTROL_REQUEST_METHOD' ]
1971
+ requested_headers = headers[ 'HTTP_ACCESS_CONTROL_REQUEST_HEADERS' ]
1972
+
1973
+ if ALLOWED_HTTP_METHODS.include?( requested_method )
1974
+
1975
+ # We just parrot back the origin and requested headers as
1976
+ # any are theoretically possible. Other security layers
1977
+ # deal with, ignore, or reject interesting HTTP headers.
1978
+
1979
+ set_cors_preflight_response_headers( interaction, origin, requested_headers )
1980
+
1981
+ else
1982
+ interaction.context.response.errors.add_error( 'platform.method_not_allowed' )
1983
+
1984
+ end
1985
+
1986
+ # The early exit means only secure logging (earlier) is
1987
+ # done. Insecure logging with body data is not performed,
1988
+ # just in case the CORS inbound request contains anything
1989
+ # daft which could count as secure information.
1990
+
1991
+ return respond_for( interaction, true )
1992
+
1993
+ else
1994
+ set_cors_normal_response_headers( interaction, origin )
1995
+
1996
+ end
1997
+ end
1998
+
1999
+ return nil
2000
+ end
2001
+
2002
+ # Preprocessing stage that sets up CORS response headers in response to a
2003
+ # normal (or preflight) CORS response.
2004
+ #
2005
+ # +interaction+:: Hoodoo::Services::Middleware::Interaction instance
2006
+ # describing the current interaction.
2007
+ #
2008
+ # +origin+:: Value of inbound request's "Origin" HTTP header.
2009
+ #
2010
+ def set_cors_normal_response_headers( interaction, origin )
2011
+ interaction.context.response.add_header( 'Access-Control-Allow-Origin', origin )
2012
+ end
2013
+
2014
+ # Preprocessing stage that sets up CORS response headers in response to a
2015
+ # preflight CORS response, based on given inbound headers.
2016
+ #
2017
+ # +interaction+:: Hoodoo::Services::Middleware::Interaction instance
2018
+ # describing the current interaction.
2019
+ #
2020
+ # +origin+:: Value of inbound request's "Origin" HTTP header.
2021
+ #
2022
+ # +headers+:: Value of inbound request's
2023
+ # "Access-Control-Request-Headers" HTTP header.
2024
+ #
2025
+ def set_cors_preflight_response_headers( interaction, origin, headers )
2026
+
2027
+ set_cors_normal_response_headers( interaction, origin )
2028
+
2029
+ # We don't try and figure out a target resource interface and give back
2030
+ # just the verbs it supports in preflight; too much trouble; just list
2031
+ # all *possible* supported methods.
2032
+
2033
+ interaction.context.response.add_header(
2034
+ 'Access-Control-Allow-Methods',
2035
+ ALLOWED_HTTP_METHODS.to_a.join( ', ' )
2036
+ )
2037
+
2038
+ # Same for HTTP headers. Just allow whatever was requested. Other layers
2039
+ # will read, ignore, or reject interesting HTTP headers.
2040
+
2041
+ interaction.context.response.add_header(
2042
+ 'Access-Control-Allow-Headers',
2043
+ headers
2044
+ )
2045
+
2046
+ # No "Access-Control-Expose-Headers" is set. We don't expose *any* of
2047
+ # the custom response headers to untrusted JavaScript code - not even
2048
+ # the Interaction ID.
2049
+
2050
+ end
2051
+
2052
+ # Match a URI string against a service endpoint regexp and return broken
2053
+ # down path components and extension if there's a match, else nil.
2054
+ #
2055
+ # +uri_path+:: Path component of URI, percent-*unescaped*.
2056
+ # +regexp+:: A regexp that should return the separator between service
2057
+ # endpoint and any other path data in match data index 1 and
2058
+ # the rest of the URI path, if any, in match data 2.
2059
+ #
2060
+ # Returns an array with two elements. The first is the array of pure path
2061
+ # components, with no empty strings; it may be empty. The second is the
2062
+ # filename extension if present, else an empty string.
2063
+ #
2064
+ # Returns nil if there's no endpoint match at all.
2065
+ #
2066
+ # Example - assuming the regexp matched a service endpoint of "/members"
2067
+ # then URI paths yield example return values as follows:
2068
+ #
2069
+ # /members
2070
+ # => [ [], '' ]
2071
+ #
2072
+ # /members.json
2073
+ # => [ [], 'json' ]
2074
+ #
2075
+ # /members/
2076
+ # => [ [], '' ]
2077
+ #
2078
+ # /members/1234.json
2079
+ # => [ [ '1234' ], 'json' ]
2080
+ #
2081
+ # /members/1234/hello.tar.gz
2082
+ # => [ [ '1234', 'hello' ], 'tar.gz' ]
2083
+ #
2084
+ def process_uri_path( uri_path, regexp )
2085
+ match_data = uri_path.match( regexp )
2086
+ return nil if match_data.nil?
2087
+
2088
+ # Split the path into array entries and examine the last one for a
2089
+ # filename extension, extracting it if found.
2090
+
2091
+ remaining_path_components = []
2092
+ extension = ''
2093
+
2094
+ if ( match_data[ 1 ] == '.' )
2095
+ extension = match_data[ 2 ]
2096
+
2097
+ elsif ( match_data[ 1 ] == '/' )
2098
+ remaining_path_components = match_data[ 2 ].split( '/' ).reject { | str | str === '' }
2099
+ last_item = remaining_path_components.last
2100
+
2101
+ unless ( last_item.nil? )
2102
+ path, extension = last_item.split( '.', 2 )
2103
+
2104
+ if ( path == '' )
2105
+ remaining_path_components.pop()
2106
+ else
2107
+ remaining_path_components[ -1 ] = path
2108
+ end
2109
+ end
2110
+ end
2111
+
2112
+ [ remaining_path_components, extension || '' ]
2113
+ end
2114
+
2115
+ # Determine the action to call in a service for the given inbound HTTP
2116
+ # method. This doesn't say anything about whether or not a particular
2117
+ # endpoint happens to support that action - it just maps HTTP verb to
2118
+ # action.
2119
+ #
2120
+ # See also #determine_authorisation.
2121
+ #
2122
+ # +http_method+:: Inbound method as a string, e.g. +'POST'+. Upper or
2123
+ # lower case.
2124
+ #
2125
+ # +get_is_list+:: If +true+, treat GET methods as +:list+, else as
2126
+ # +:show+. This is often determined on the basis of e.g.
2127
+ # path components after the endpoint part of the URI path
2128
+ # being absent or present.
2129
+ #
2130
+ # Returns the action as a Symbol - see ALLOWED_ACTIONS.
2131
+ #
2132
+ def determine_action( http_method, get_is_list )
2133
+ http_method = ( http_method || '' ).upcase
2134
+
2135
+ # Clumsy code because there is no 1:1 map from HTTP method to action
2136
+ # (e.g. GET can be :show or :list).
2137
+ #
2138
+ action = case http_method
2139
+ when 'POST'
2140
+ :create
2141
+ when 'PATCH'
2142
+ :update
2143
+ when 'DELETE'
2144
+ :delete
2145
+ when 'GET'
2146
+ get_is_list ? :list : :show
2147
+ end
2148
+
2149
+ return action
2150
+ end
2151
+
2152
+ # Determine the authorisation / permission to perform a particular
2153
+ # action.
2154
+ #
2155
+ # +interaction+:: Hoodoo::Services::Middleware::Interaction instance
2156
+ # describing the current interaction.
2157
+ #
2158
+ # Returns:
2159
+ #
2160
+ # * Hoodoo::Services::Permissions::ALLOW - the action is allowed.
2161
+ #
2162
+ # * +nil+ - the action was prohibited. The given +response+ was
2163
+ # with an appropriate error message (e.g. "invalid session",
2164
+ # "forbidden" etc.).
2165
+ #
2166
+ # * Hoodoo::Services::Permissions::ASK - the caller MUST check with
2167
+ # the target endpoint's implementation to see if the action is
2168
+ # allowed by calling #ask_for_authorisation at some point later in
2169
+ # processing when the information this needs is available.
2170
+ #
2171
+ def determine_authorisation( interaction )
2172
+
2173
+ # Set up some convenience variables
2174
+
2175
+ interface = interaction.target_interface
2176
+ action = interaction.requested_action
2177
+ session = interaction.context.session
2178
+ response = interaction.context.response
2179
+
2180
+ # Check authorisation
2181
+
2182
+ result = nil
2183
+
2184
+ if interface.public_actions.include?( action )
2185
+
2186
+ # Public action; no need to check anything else, it's allowed,
2187
+ # session or no session.
2188
+
2189
+ result = Hoodoo::Services::Permissions::ALLOW
2190
+
2191
+ else
2192
+
2193
+ # The action isn't public; so unless it is declared as a protected
2194
+ # action, it isn't supported by this endpoint. If supported, check
2195
+ # session and permissions.
2196
+
2197
+ if interface.actions.include?( action )
2198
+
2199
+ if session.nil?
2200
+ response.add_error( 'platform.invalid_session' )
2201
+ elsif session.permissions.nil?
2202
+ response.add_error( 'platform.forbidden' )
2203
+ else
2204
+ permission = session.permissions.permitted?( interface.resource, action )
2205
+
2206
+ if permission == Hoodoo::Services::Permissions::DENY
2207
+ response.add_error( 'platform.forbidden' )
2208
+ else
2209
+ result = permission
2210
+ end
2211
+ end
2212
+
2213
+ else
2214
+
2215
+ http_method = interaction.rack_request.request_method
2216
+
2217
+ response.add_error(
2218
+ 'platform.method_not_allowed',
2219
+ 'message' => "Service endpoint '/v#{ interface.version }/#{ interface.endpoint }' does not support HTTP method '#{ ( http_method || '<unknown>' ).upcase }' yielding action '#{ action }'"
2220
+ )
2221
+
2222
+ end
2223
+ end
2224
+
2225
+ return result
2226
+ end
2227
+
2228
+ # As a service for authorisation for a particular action given
2229
+ # the request and session context data provided.
2230
+ #
2231
+ # Calls the Hoodoo::Services::Implementation#verify method in the
2232
+ # target implementation. Expects a conforming response; anything
2233
+ # that isn't Hoodoo::Services::Permissions::ALLOW is treated as
2234
+ # Hoodoo::Services::Permissions::DENY.
2235
+ #
2236
+ # Parameters:
2237
+ #
2238
+ # +interaction+:: Hoodoo::Services::Middleware::Interaction instance
2239
+ # describing the current interaction.
2240
+ #
2241
+ # Returns:
2242
+ #
2243
+ # * Hoodoo::Services::Permissions::ALLOW - the action is allowed.
2244
+ #
2245
+ # * +nil+ - the action was prohibited. The given +response+ was
2246
+ # with an appropriate error message (e.g. "invalid session",
2247
+ # "forbidden" etc.).
2248
+ #
2249
+ def ask_for_authorisation( interaction )
2250
+
2251
+ permission = interaction.target_implementation.verify(
2252
+ interaction.context,
2253
+ interaction.requested_action
2254
+ )
2255
+
2256
+ if permission == Hoodoo::Services::Permissions::ALLOW
2257
+ return permission
2258
+ else
2259
+ interaction.context.response.add_error( 'platform.forbidden' )
2260
+ return nil
2261
+ end
2262
+ end
2263
+
2264
+ # Update a Hoodoo::Services::Response instance for making a call to
2265
+ # the given Hoodoo::Services::Interface, setting up error description
2266
+ # information. Other initialisation is left to the caller.
2267
+ #
2268
+ # +response+:: Hoodoo::Services::Response instance to update.
2269
+ # +interface+:: Hoodoo::Services::Interface for which the request is being
2270
+ # constructed. Custom error descriptions from that
2271
+ # interface, if any, are included in the response object's
2272
+ # error collection data.
2273
+ #
2274
+ def update_response_for( response, interface )
2275
+ unless interface.errors_for.nil?
2276
+ response.errors = Hoodoo::Errors.new( interface.errors_for )
2277
+ end
2278
+ end
2279
+
2280
+ # Process query string data for list actions. Only call if there's a list
2281
+ # action being requested.
2282
+ #
2283
+ # +interaction+:: Hoodoo::Services::Middleware::Interaction instance
2284
+ # describing the current interaction.
2285
+ #
2286
+ # The interaction's request data will be updated with list parameter
2287
+ # information if successul. The interaction's response data will be
2288
+ # updated with error information if anything is wrong.
2289
+ #
2290
+ def process_query_string( interaction )
2291
+
2292
+ query_string = interaction.rack_request.query_string
2293
+
2294
+ # The 'decode' call produces an array of two-element arrays, the first
2295
+ # being the key and next being the value, already CGI unescaped once.
2296
+ #
2297
+ # On some Ruby versions bad data here can cause an exception, so there's
2298
+ # a catch-all "rescue" at the end of the function to return a 'malformed'
2299
+ # response if necessary.
2300
+
2301
+ query_data = URI.decode_www_form( query_string )
2302
+
2303
+ # Convert to a unified Hash of non-duplicated keys yielding Arrays of
2304
+ # *not* unique values ('true' in second parameter => allow duplicates).
2305
+
2306
+ query_hash = Hoodoo::Utilities.collated_hash_from( query_data, true )
2307
+
2308
+ # Some query hash entries accept either multiple repeats in the query
2309
+ # string, or comma-separated values in a single query string entry.
2310
+ # For example:
2311
+ #
2312
+ # &sort=name&sort=created_at&direction=asc&direction=asc
2313
+ #
2314
+ # ...versus:
2315
+ #
2316
+ # &sort=name,created_at&direction=asc,desc
2317
+ #
2318
+ # Further, search and filter strings should have been double-encoded
2319
+ # so still require a decode pass before we can process things as above.
2320
+
2321
+ # First, split any input query strings on "," for supported keys.
2322
+
2323
+ %w{ sort direction search filter _embed _reference }.each do | key |
2324
+ value = query_hash[ key ]
2325
+ unless value.nil?
2326
+ value.map! do | possible_csv_to_split |
2327
+ possible_csv_to_split.split( ',' )
2328
+ end
2329
+ end
2330
+ end
2331
+
2332
+ # Flatten the resulting sub-arrays and make sure values are unique.
2333
+
2334
+ query_hash.each do | key, value |
2335
+ value.flatten!
2336
+ value.uniq!
2337
+ end
2338
+
2339
+ # For search and filter strings, decode the key/value pairs as a
2340
+ # unified string with the Hash-converted result written back.
2341
+
2342
+ %w{ search filter }.each do | key |
2343
+ value = query_hash[ key ]
2344
+ unless value.nil?
2345
+ query_hash[ key ] = Hash[ URI::decode_www_form( value.join( '&' ) ) ]
2346
+ end
2347
+ end
2348
+
2349
+ # For some other parameters, array values just don't make sense, so
2350
+ # take the *last* of these, so that subsequently-specified values are
2351
+ # overriding previously-specified values, as a caller might expect.
2352
+
2353
+ %w{ offset limit }.each do | key |
2354
+ value = query_hash[ key ]
2355
+ unless value.nil?
2356
+ query_hash[ key ] = value.last
2357
+ end
2358
+ end
2359
+
2360
+ return process_query_hash( interaction, query_hash )
2361
+ end
2362
+
2363
+ # Process a hash of URI-decoded form data in the same way as
2364
+ # #process_query_string (and used as a back-end for that). Nested search
2365
+ # and filter strings should be decoded as nested hashes. Nested _embed and
2366
+ # _reference lists should be stored as arrays. Other values may be Strings
2367
+ # or Arrays. All Hash keys must be Strings.
2368
+ #
2369
+ # +interaction+:: Hoodoo::Services::Middleware::Interaction instance
2370
+ # describing the current interaction.
2371
+ #
2372
+ # +query_hash+:: Hash of data derived from query string - see
2373
+ # #process_query_string.
2374
+ #
2375
+ # The interaction's request data will be updated with list parameter
2376
+ # information if successul. The interaction's response data will be
2377
+ # updated with error information if anything is wrong.
2378
+ #
2379
+ def process_query_hash( interaction, query_hash )
2380
+
2381
+ # Set up some convenience variables
2382
+
2383
+ interface = interaction.target_interface
2384
+ request = interaction.context.request
2385
+ response = interaction.context.response
2386
+
2387
+ # Process the query hash
2388
+
2389
+ allowed = ALLOWED_QUERIES_ALL
2390
+ allowed += ALLOWED_QUERIES_LIST if interaction.requested_action == :list
2391
+
2392
+ unrecognised_query_keys = query_hash.keys - allowed
2393
+ malformed = unrecognised_query_keys
2394
+
2395
+ limit = Hoodoo::Utilities::to_integer?( query_hash[ 'limit' ] || interface.to_list.limit )
2396
+ malformed << :limit if limit.nil? || limit < 1
2397
+
2398
+ offset = Hoodoo::Utilities::to_integer?( query_hash[ 'offset' ] || 0 )
2399
+ malformed << :offset if offset.nil? || offset < 0
2400
+
2401
+ # In essence the code below is rationalising sort and direction lists as
2402
+ # follows:
2403
+ #
2404
+ # SORT DIRECTION
2405
+ # ==================
2406
+ # 0 created
2407
+ # => created with default direction for sort key 'created'
2408
+ #
2409
+ # 0 desc
2410
+ # => default sort key with 'desc' order
2411
+ #
2412
+ # 0 created asc
2413
+ # 1 name desc
2414
+ # 2 title
2415
+ # => as specified with default direction for sort key 'title'
2416
+ #
2417
+ # 0 created asc
2418
+ # 1 desc
2419
+ # => error as there's no way to guess the sort key for the 'desc'
2420
+
2421
+ sort_keys = query_hash[ 'sort' ] || [ interface.to_list.default_sort_key ]
2422
+ sort_directions = query_hash[ 'direction' ] || []
2423
+
2424
+ # For inter-resource calls, historical callers might provide a sort key
2425
+ # and/or direction as a String not Array, so promote and flatten.
2426
+
2427
+ sort_keys = [ sort_keys ] if sort_keys.is_a?( String )
2428
+ sort_directions = [ sort_directions ] if sort_directions.is_a?( String )
2429
+
2430
+ # 2015-07-03 (ADH): This used to just read "sort_directions.size >
2431
+ # sort_keys.size", to match the big comment a few lines above. During
2432
+ # pull request review though it was decided that we'd remove the
2433
+ # ambiguity in mismatched sort key or direction lists entirely and
2434
+ # require them both (when there's more than one) in equal numbers.
2435
+ #
2436
+ # We have to allow someone to just specify a direction without the sort
2437
+ # key for the single-use case (i.e. change to "created_at asc") else the
2438
+ # change would break any clients that already use such parameters.
2439
+ #
2440
+ # The originally intended, more permissive, default-orientated behaviour
2441
+ # can of course be restored by just changing this "if" back.
2442
+ #
2443
+ if ( sort_keys.size > 1 || sort_directions.size > 1 ) && sort_directions.size != sort_keys.size
2444
+ malformed << :direction
2445
+ else
2446
+ sort_keys.each_with_index do | sort_key, index |
2447
+ unless interface.to_list.sort[ sort_key ].is_a?( Set )
2448
+ malformed << :sort
2449
+ break
2450
+ end
2451
+
2452
+ sort_direction = sort_directions[ index ] || interface.to_list.sort[ sort_key ].first
2453
+
2454
+ unless interface.to_list.sort[ sort_key ].include?( sort_direction )
2455
+ malformed << :direction
2456
+ break
2457
+ end
2458
+
2459
+ sort_directions[ index ] = sort_direction
2460
+ end
2461
+ end
2462
+
2463
+ search = query_hash[ 'search' ] || {}
2464
+ unrecognised_search_keys = search.keys - interface.to_list.search
2465
+ malformed << "search: #{ unrecognised_search_keys.join(', ') }" unless unrecognised_search_keys.empty?
2466
+
2467
+ filter = query_hash[ 'filter' ] || {}
2468
+ unrecognised_filter_keys = filter.keys - interface.to_list.filter
2469
+ malformed << "filter: #{ unrecognised_filter_keys.join(', ') }" unless unrecognised_filter_keys.empty?
2470
+
2471
+ embeds = query_hash[ '_embed' ] || []
2472
+ unrecognised_embeds = embeds - interface.embeds
2473
+ malformed << "_embed: #{ unrecognised_embeds.join(', ') }" unless unrecognised_embeds.empty?
2474
+
2475
+ references = query_hash[ '_reference' ] || []
2476
+ unrecognised_references = references - interface.embeds # (sic.)
2477
+ malformed << "_reference: #{ unrecognised_references.join(', ') }" unless unrecognised_references.empty?
2478
+
2479
+ return response.add_error(
2480
+ 'platform.malformed',
2481
+ 'message' => "One or more malformed or invalid query string parameters",
2482
+ 'reference' => { :including => malformed.join( ', ' ) }
2483
+ ) unless malformed.empty?
2484
+
2485
+ sort_data = {}
2486
+ sort_keys.each_with_index do | sort_key, index |
2487
+ sort_data[ sort_key ] = sort_directions[ index ]
2488
+ end
2489
+
2490
+ request.list.offset = offset
2491
+ request.list.limit = limit
2492
+ request.list.sort_data = sort_data
2493
+ request.list.search_data = search
2494
+ request.list.filter_data = filter
2495
+ request.embeds = embeds
2496
+ request.references = references
2497
+ end
2498
+
2499
+ # Safely parse the client payload in the context of the defined content
2500
+ # type (#deal_with_content_type_header must have been run first).
2501
+ #
2502
+ # +interaction+:: Hoodoo::Services::Middleware::Interaction instance
2503
+ # describing the current interaction. Response may be
2504
+ # updated on exit with an error, or request may be
2505
+ # updated with the parsed body data as a Hash.
2506
+ #
2507
+ # +body+:: Calling client request's body payload as a String.
2508
+ #
2509
+ def parse_body_string_into( interaction, body )
2510
+
2511
+ content_type = interaction.requested_content_type
2512
+
2513
+ begin
2514
+ case content_type
2515
+ when 'application/json'
2516
+
2517
+ # Hoodoo requires Ruby 2.1 or later, else:
2518
+ # https://www.ruby-lang.org/en/news/2013/02/22/json-dos-cve-2013-0269/
2519
+ #
2520
+ payload_hash = ::JSON.parse( body )
2521
+
2522
+ end
2523
+
2524
+ rescue => e
2525
+ payload_hash = {}
2526
+ interaction.context.response.errors.add_error( 'generic.malformed' )
2527
+
2528
+ end
2529
+
2530
+ if payload_hash.nil?
2531
+ raise "Internal error - content type '#{ interaction.requested_content_type }' is not supported here; \#deal_with_content_type_header() should have caught that"
2532
+ end
2533
+
2534
+ interaction.context.request.body = payload_hash
2535
+ end
2536
+
2537
+ # For the given action and service interface, verify the given body data
2538
+ # via to-update / to-create DSL data where available. On exit, the given
2539
+ # response data may have errors added.
2540
+ #
2541
+ # +interaction+:: Hoodoo::Services::Middleware::Interaction instance
2542
+ # describing the current interaction. Response may be
2543
+ # updated on exit with an error, or request may be
2544
+ # updated with the parsed body data as a Hash.
2545
+ #
2546
+ def validate_body_data_for( interaction )
2547
+
2548
+ # Set up some convenience variables
2549
+
2550
+ interface = interaction.target_interface
2551
+ action = interaction.requested_action
2552
+ response = interaction.context.response
2553
+ body = interaction.context.request.body
2554
+
2555
+ # Work out which verification schema to use
2556
+
2557
+ verification_object = if ( action == :create )
2558
+ interface.to_create()
2559
+ else
2560
+ interface.to_update()
2561
+ end
2562
+
2563
+ # Verify the inbound parameters either via to-create/to-update schema
2564
+ # or, if absent, at least make sure prohibited fields are absent.
2565
+
2566
+ if ( verification_object.nil? )
2567
+
2568
+ requested_fields = body.keys
2569
+ union = PROHIBITED_INBOUND_FIELDS & requested_fields
2570
+
2571
+ unless union.empty?
2572
+ response.errors.add_error(
2573
+ 'generic.invalid_parameters',
2574
+ 'message' => 'Body data contains unrecognised or prohibited fields',
2575
+ 'reference' => { :fields => union.join( ', ' ) }
2576
+ )
2577
+ end
2578
+
2579
+ else
2580
+
2581
+ # 'false' => validate as type-only, not a resource (no ID, kind etc.)
2582
+ #
2583
+ result = verification_object.validate( body, false )
2584
+
2585
+ if result.has_errors?
2586
+ response.errors.merge!( result )
2587
+ else
2588
+ # Strip out unexpected/unrecognised fields and sanitise the input
2589
+ # in addition to general validation.
2590
+ #
2591
+ # At the time of writing, it makes more sense to warn callers if
2592
+ # they send stuff that is not recognised; e.g. they might have
2593
+ # misread the API and be trying to patch field "id", or change a
2594
+ # field that's in the resource representation and maybe used for
2595
+ # a "create" but can't be subsequently modified; or they might be
2596
+ # using fields that are defined in a newer version of the API,
2597
+ # but are talking to a service that doesn't implement it.
2598
+ #
2599
+ # Thus, complain if the sanitised body differs from the input.
2600
+
2601
+ rendered = verification_object.render( body ) # May add default fields
2602
+ merged = body.merge( rendered )
2603
+
2604
+ if ( merged != rendered )
2605
+ deep_dup = Hoodoo::Utilities.deep_dup( body )
2606
+ deep_merged = Hoodoo::Utilities.deep_merge_into( deep_dup, rendered )
2607
+ diff = Hoodoo::Utilities.hash_diff( deep_merged, rendered )
2608
+ paths = Hoodoo::Utilities.hash_key_paths( diff )
2609
+
2610
+ response.errors.add_error(
2611
+ 'generic.invalid_parameters',
2612
+ 'message' => 'Body data contains unrecognised or prohibited fields',
2613
+ 'reference' => { :fields => paths.join( ', ' ) }
2614
+ )
2615
+ end
2616
+ end
2617
+ end
2618
+ end
2619
+
2620
+ # Some HTTP headers or other request features may give us reason to modify
2621
+ # inbound body data.
2622
+ #
2623
+ # * Currently, this is only ever done for a "create" action (POST); never
2624
+ # call here for any other action / HTTP method.
2625
+ #
2626
+ # * Secured header checking must already have taken place before calling.
2627
+ #
2628
+ # At present this involves just the X-Resource-UUID header. If this is
2629
+ # present and the value is non-empty, it's validated as UUID and written as
2630
+ # the body's "id" field at the top-level if OK.
2631
+ #
2632
+ # By the time this method is called, validation of the header value must
2633
+ # already have taken place (see "deal_with_x_headers").
2634
+ #
2635
+ # On exit, the interaction's request's body may be updated, or the
2636
+ # response's error collection may be updated rejecting the request on
2637
+ # the grounds of an invalid UUID.
2638
+ #
2639
+ # +interaction+:: Hoodoo::Services::Middleware::Interaction instance
2640
+ # describing the current interaction.
2641
+ #
2642
+ def maybe_update_body_data_for( interaction )
2643
+ return unless interaction.rack_request.env.has_key?( 'HTTP_X_RESOURCE_UUID' )
2644
+
2645
+ required_item_uuid = interaction.rack_request.env[ 'HTTP_X_RESOURCE_UUID' ]
2646
+ interaction.context.request.body[ 'id' ] = required_item_uuid
2647
+ end
2648
+
2649
+ # Record an exception in a given response object, overwriting any previous
2650
+ # error data if present.
2651
+ #
2652
+ # +interaction+:: Hoodoo::Services::Middleware::Interaction instance
2653
+ # describing the current interaction. Response will be
2654
+ # updated on exit with just the exception error.
2655
+ #
2656
+ # +exception+:: The Exception instance to record.
2657
+ #
2658
+ # Returns a "for Rack" representation of the whole response.
2659
+ #
2660
+ def record_exception( interaction, exception )
2661
+ reference = {
2662
+ :exception => exception.message
2663
+ }
2664
+
2665
+ unless self.class.environment.production? || self.class.environment.red?
2666
+ reference[ :backtrace ] = exception.backtrace.join( " | " )
2667
+ end
2668
+
2669
+ # A service can rewrite this field with a different object, leading
2670
+ # to an exception within the exception handler; so use a new one!
2671
+ #
2672
+ interaction.context.response.errors = Hoodoo::Errors.new()
2673
+
2674
+ return interaction.context.response.add_error(
2675
+ 'platform.fault',
2676
+ 'message' => exception.message,
2677
+ 'reference' => reference
2678
+ )
2679
+ end
2680
+
2681
+ # Take a Hoodoo::Errors instance constructed from, or obtained via
2682
+ # a call to another service (inter-resource local or remote call) and
2683
+ # translate the contents to make sense when those errors are reported
2684
+ # in the context of an outer resource's response to a request.
2685
+ #
2686
+ # For example, if one resource tries to look up a reference to another
2687
+ # as part of a +show+ action, but that _referred_ resource is not found,
2688
+ # internally that would be reported via HTTP 404. This would confuse
2689
+ # callers if returned verbatim as it implies the target, outermost
2690
+ # resource wasn't found, even though it was. Instead, the 404 is turned
2691
+ # into a 422 with code/message/reference data describing the equivalent
2692
+ # "inner reference not found" condition.
2693
+ #
2694
+ def annotate_errors_from_other_resource( errors )
2695
+ # TODO - Move to an accessible shared location, e.g. Errors itself
2696
+ # The inter-resource remote endpoint code duplicates this
2697
+ return errors
2698
+ end
2699
+
2700
+ # The following must appear at the end of this class definition.
2701
+
2702
+ set_up_basic_logging()
2703
+
2704
+ end # 'class Middleware'
2705
+ end; end # 'module Hoodoo; module Services'