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.
- checksums.yaml +7 -0
- data/bin/hoodoo +5 -0
- data/lib/hoodoo.rb +27 -0
- data/lib/hoodoo/active.rb +32 -0
- data/lib/hoodoo/active/active_model/uuid_validator.rb +45 -0
- data/lib/hoodoo/active/active_record/base.rb +81 -0
- data/lib/hoodoo/active/active_record/creator.rb +134 -0
- data/lib/hoodoo/active/active_record/dated.rb +343 -0
- data/lib/hoodoo/active/active_record/error_mapping.rb +351 -0
- data/lib/hoodoo/active/active_record/finder.rb +606 -0
- data/lib/hoodoo/active/active_record/search_helper.rb +189 -0
- data/lib/hoodoo/active/active_record/secure.rb +431 -0
- data/lib/hoodoo/active/active_record/support.rb +106 -0
- data/lib/hoodoo/active/active_record/translated.rb +87 -0
- data/lib/hoodoo/active/active_record/uuid.rb +80 -0
- data/lib/hoodoo/active/active_record/writer.rb +321 -0
- data/lib/hoodoo/client.rb +23 -0
- data/lib/hoodoo/client/augmented_array.rb +29 -0
- data/lib/hoodoo/client/augmented_base.rb +168 -0
- data/lib/hoodoo/client/augmented_hash.rb +23 -0
- data/lib/hoodoo/client/client.rb +354 -0
- data/lib/hoodoo/client/endpoint/endpoint.rb +427 -0
- data/lib/hoodoo/client/endpoint/endpoints/amqp.rb +180 -0
- data/lib/hoodoo/client/endpoint/endpoints/auto_session.rb +194 -0
- data/lib/hoodoo/client/endpoint/endpoints/http.rb +203 -0
- data/lib/hoodoo/client/endpoint/endpoints/http_based.rb +367 -0
- data/lib/hoodoo/client/endpoint/endpoints/not_found.rb +59 -0
- data/lib/hoodoo/client/headers.rb +269 -0
- data/lib/hoodoo/communicators.rb +23 -0
- data/lib/hoodoo/communicators/fast.rb +44 -0
- data/lib/hoodoo/communicators/pool.rb +601 -0
- data/lib/hoodoo/communicators/slow.rb +84 -0
- data/lib/hoodoo/data.rb +51 -0
- data/lib/hoodoo/data/resources/caller.rb +39 -0
- data/lib/hoodoo/data/resources/errors.rb +28 -0
- data/lib/hoodoo/data/resources/log.rb +31 -0
- data/lib/hoodoo/data/resources/session.rb +26 -0
- data/lib/hoodoo/data/types/error_primitive.rb +27 -0
- data/lib/hoodoo/data/types/permissions.rb +40 -0
- data/lib/hoodoo/data/types/permissions_defaults.rb +32 -0
- data/lib/hoodoo/data/types/permissions_full.rb +28 -0
- data/lib/hoodoo/data/types/permissions_resources.rb +31 -0
- data/lib/hoodoo/discovery.rb +20 -0
- data/lib/hoodoo/errors.rb +19 -0
- data/lib/hoodoo/errors/error_descriptions.rb +229 -0
- data/lib/hoodoo/errors/errors.rb +322 -0
- data/lib/hoodoo/generator.rb +139 -0
- data/lib/hoodoo/logger.rb +23 -0
- data/lib/hoodoo/logger/fast_writer.rb +27 -0
- data/lib/hoodoo/logger/flattener_mixin.rb +36 -0
- data/lib/hoodoo/logger/logger.rb +387 -0
- data/lib/hoodoo/logger/slow_writer.rb +49 -0
- data/lib/hoodoo/logger/writer_mixin.rb +52 -0
- data/lib/hoodoo/logger/writers/file_writer.rb +45 -0
- data/lib/hoodoo/logger/writers/log_entries_dot_com_writer.rb +64 -0
- data/lib/hoodoo/logger/writers/stream_writer.rb +43 -0
- data/lib/hoodoo/middleware.rb +33 -0
- data/lib/hoodoo/presenters.rb +45 -0
- data/lib/hoodoo/presenters/base.rb +281 -0
- data/lib/hoodoo/presenters/base_dsl.rb +519 -0
- data/lib/hoodoo/presenters/common_resource_fields.rb +31 -0
- data/lib/hoodoo/presenters/embedding.rb +232 -0
- data/lib/hoodoo/presenters/types/array.rb +118 -0
- data/lib/hoodoo/presenters/types/boolean.rb +26 -0
- data/lib/hoodoo/presenters/types/date.rb +26 -0
- data/lib/hoodoo/presenters/types/date_time.rb +26 -0
- data/lib/hoodoo/presenters/types/decimal.rb +47 -0
- data/lib/hoodoo/presenters/types/enum.rb +55 -0
- data/lib/hoodoo/presenters/types/field.rb +158 -0
- data/lib/hoodoo/presenters/types/float.rb +26 -0
- data/lib/hoodoo/presenters/types/hash.rb +361 -0
- data/lib/hoodoo/presenters/types/integer.rb +26 -0
- data/lib/hoodoo/presenters/types/object.rb +117 -0
- data/lib/hoodoo/presenters/types/string.rb +53 -0
- data/lib/hoodoo/presenters/types/tags.rb +24 -0
- data/lib/hoodoo/presenters/types/text.rb +26 -0
- data/lib/hoodoo/presenters/types/uuid.rb +54 -0
- data/lib/hoodoo/services.rb +34 -0
- data/lib/hoodoo/services/discovery/discoverers/by_consul.rb +66 -0
- data/lib/hoodoo/services/discovery/discoverers/by_convention.rb +173 -0
- data/lib/hoodoo/services/discovery/discoverers/by_drb/by_drb.rb +195 -0
- data/lib/hoodoo/services/discovery/discoverers/by_drb/drb_server.rb +166 -0
- data/lib/hoodoo/services/discovery/discoverers/by_drb/drb_server_start.rb +37 -0
- data/lib/hoodoo/services/discovery/discovery.rb +186 -0
- data/lib/hoodoo/services/discovery/results/for_amqp.rb +58 -0
- data/lib/hoodoo/services/discovery/results/for_http.rb +85 -0
- data/lib/hoodoo/services/discovery/results/for_local.rb +85 -0
- data/lib/hoodoo/services/discovery/results/for_remote.rb +57 -0
- data/lib/hoodoo/services/middleware/amqp_log_message.rb +186 -0
- data/lib/hoodoo/services/middleware/amqp_log_writer.rb +119 -0
- data/lib/hoodoo/services/middleware/endpoints/inter_resource_local.rb +130 -0
- data/lib/hoodoo/services/middleware/endpoints/inter_resource_remote.rb +202 -0
- data/lib/hoodoo/services/middleware/exception_reporting/base_reporter.rb +105 -0
- data/lib/hoodoo/services/middleware/exception_reporting/exception_reporting.rb +115 -0
- data/lib/hoodoo/services/middleware/exception_reporting/reporters/airbrake_reporter.rb +64 -0
- data/lib/hoodoo/services/middleware/exception_reporting/reporters/raygun_reporter.rb +63 -0
- data/lib/hoodoo/services/middleware/interaction.rb +127 -0
- data/lib/hoodoo/services/middleware/middleware.rb +2705 -0
- data/lib/hoodoo/services/middleware/rack_monkey_patch.rb +73 -0
- data/lib/hoodoo/services/services/context.rb +153 -0
- data/lib/hoodoo/services/services/implementation.rb +132 -0
- data/lib/hoodoo/services/services/interface.rb +934 -0
- data/lib/hoodoo/services/services/permissions.rb +250 -0
- data/lib/hoodoo/services/services/request.rb +189 -0
- data/lib/hoodoo/services/services/response.rb +316 -0
- data/lib/hoodoo/services/services/service.rb +141 -0
- data/lib/hoodoo/services/services/session.rb +729 -0
- data/lib/hoodoo/utilities.rb +12 -0
- data/lib/hoodoo/utilities/string_inquirer.rb +54 -0
- data/lib/hoodoo/utilities/utilities.rb +380 -0
- data/lib/hoodoo/utilities/uuid.rb +44 -0
- data/lib/hoodoo/version.rb +17 -0
- data/spec/active/active_record/base_spec.rb +57 -0
- data/spec/active/active_record/creator_spec.rb +88 -0
- data/spec/active/active_record/dated_spec.rb +248 -0
- data/spec/active/active_record/error_mapping_spec.rb +360 -0
- data/spec/active/active_record/finder_spec.rb +744 -0
- data/spec/active/active_record/search_helper_spec.rb +384 -0
- data/spec/active/active_record/secure_spec.rb +435 -0
- data/spec/active/active_record/support_spec.rb +225 -0
- data/spec/active/active_record/translated_spec.rb +19 -0
- data/spec/active/active_record/uuid_spec.rb +72 -0
- data/spec/active/active_record/writer_spec.rb +272 -0
- data/spec/alchemy/alchemy-amq.rb +33 -0
- data/spec/client/augmented_array_spec.rb +15 -0
- data/spec/client/augmented_base_spec.rb +50 -0
- data/spec/client/augmented_hash_spec.rb +15 -0
- data/spec/client/client_spec.rb +955 -0
- data/spec/client/endpoint/endpoint_spec.rb +70 -0
- data/spec/client/endpoint/endpoints/amqp_spec.rb +16 -0
- data/spec/client/endpoint/endpoints/auto_session_spec.rb +9 -0
- data/spec/client/endpoint/endpoints/http_based_spec.rb +9 -0
- data/spec/client/endpoint/endpoints/http_spec.rb +103 -0
- data/spec/client/endpoint/endpoints/not_found_spec.rb +35 -0
- data/spec/client/headers_spec.rb +172 -0
- data/spec/communicators/fast_spec.rb +9 -0
- data/spec/communicators/pool_spec.rb +339 -0
- data/spec/communicators/slow_spec.rb +15 -0
- data/spec/data/resources/caller_spec.rb +156 -0
- data/spec/data/resources/errors_spec.rb +22 -0
- data/spec/data/resources/log_spec.rb +20 -0
- data/spec/data/resources/session_spec.rb +15 -0
- data/spec/data/types/error_primitive_spec.rb +15 -0
- data/spec/data/types/permissions_defaults_spec.rb +25 -0
- data/spec/data/types/permissions_full_spec.rb +44 -0
- data/spec/data/types/permissions_resources_spec.rb +34 -0
- data/spec/data/types/permissions_spec.rb +37 -0
- data/spec/errors/error_descriptions_spec.rb +98 -0
- data/spec/errors/errors_spec.rb +346 -0
- data/spec/integration/service_actions_spec.rb +112 -0
- data/spec/logger/fast_writer_spec.rb +18 -0
- data/spec/logger/logger_spec.rb +259 -0
- data/spec/logger/slow_writer_spec.rb +144 -0
- data/spec/logger/writers/file_writer_spec.rb +37 -0
- data/spec/logger/writers/log_entries_dot_com_writer_spec.rb +29 -0
- data/spec/logger/writers/stream_writer_spec.rb +38 -0
- data/spec/presenters/base_dsl_spec.rb +111 -0
- data/spec/presenters/base_spec.rb +871 -0
- data/spec/presenters/common_resource_fields_spec.rb +30 -0
- data/spec/presenters/embedding_spec.rb +87 -0
- data/spec/presenters/types/array_spec.rb +249 -0
- data/spec/presenters/types/boolean_spec.rb +51 -0
- data/spec/presenters/types/date_spec.rb +57 -0
- data/spec/presenters/types/date_time_spec.rb +59 -0
- data/spec/presenters/types/decimal_spec.rb +58 -0
- data/spec/presenters/types/enum_spec.rb +71 -0
- data/spec/presenters/types/field_spec.rb +77 -0
- data/spec/presenters/types/float_spec.rb +50 -0
- data/spec/presenters/types/hash_spec.rb +1069 -0
- data/spec/presenters/types/integer_spec.rb +50 -0
- data/spec/presenters/types/object_spec.rb +177 -0
- data/spec/presenters/types/string_spec.rb +65 -0
- data/spec/presenters/types/tags_spec.rb +56 -0
- data/spec/presenters/types/text_spec.rb +50 -0
- data/spec/presenters/types/uuid_spec.rb +46 -0
- data/spec/presenters/walk_spec.rb +198 -0
- data/spec/services/discovery/discoverers/by_consul_spec.rb +29 -0
- data/spec/services/discovery/discoverers/by_convention_spec.rb +67 -0
- data/spec/services/discovery/discoverers/by_drb/by_drb_spec.rb +80 -0
- data/spec/services/discovery/discoverers/by_drb/drb_server_spec.rb +205 -0
- data/spec/services/discovery/discovery_spec.rb +73 -0
- data/spec/services/discovery/results/for_amqp_spec.rb +17 -0
- data/spec/services/discovery/results/for_http_spec.rb +37 -0
- data/spec/services/discovery/results/for_local_spec.rb +21 -0
- data/spec/services/discovery/results/for_remote_spec.rb +15 -0
- data/spec/services/middleware/amqp_log_message_spec.rb +60 -0
- data/spec/services/middleware/amqp_log_writer_spec.rb +95 -0
- data/spec/services/middleware/endpoints/inter_resource_local_spec.rb +9 -0
- data/spec/services/middleware/endpoints/inter_resource_remote_spec.rb +9 -0
- data/spec/services/middleware/exception_reporting/base_reporter_spec.rb +16 -0
- data/spec/services/middleware/exception_reporting/exception_reporting_spec.rb +92 -0
- data/spec/services/middleware/exception_reporting/reporters/airbrake_reporter_spec.rb +24 -0
- data/spec/services/middleware/exception_reporting/reporters/raygun_reporter_spec.rb +23 -0
- data/spec/services/middleware/middleware_cors_spec.rb +93 -0
- data/spec/services/middleware/middleware_create_update_spec.rb +489 -0
- data/spec/services/middleware/middleware_dated_at_spec.rb +186 -0
- data/spec/services/middleware/middleware_exotic_communication_spec.rb +560 -0
- data/spec/services/middleware/middleware_logging_spec.rb +356 -0
- data/spec/services/middleware/middleware_multi_local_spec.rb +1094 -0
- data/spec/services/middleware/middleware_multi_remote_spec.rb +1440 -0
- data/spec/services/middleware/middleware_permissions_spec.rb +1014 -0
- data/spec/services/middleware/middleware_public_spec.rb +238 -0
- data/spec/services/middleware/middleware_spec.rb +1569 -0
- data/spec/services/middleware/string_inquirer_spec.rb +30 -0
- data/spec/services/services/application_spec.rb +74 -0
- data/spec/services/services/context_spec.rb +48 -0
- data/spec/services/services/implementation_spec.rb +45 -0
- data/spec/services/services/interface_spec.rb +262 -0
- data/spec/services/services/permissions_spec.rb +249 -0
- data/spec/services/services/request_spec.rb +95 -0
- data/spec/services/services/response_spec.rb +250 -0
- data/spec/services/services/session_spec.rb +432 -0
- data/spec/spec_helper.rb +298 -0
- data/spec/utilities/utilities_spec.rb +537 -0
- data/spec/utilities/uuid_spec.rb +20 -0
- 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'
|