aws_advanced_ruby_driver_wrapper 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +23 -0
  3. data/LICENSE +175 -0
  4. data/NOTICE +1 -0
  5. data/README.md +168 -0
  6. data/THIRD-PARTY-LICENSES +473 -0
  7. data/aws_advanced_ruby_driver_wrapper.gemspec +73 -0
  8. data/lib/aws_advanced_ruby_driver_wrapper/active_record/aws_mysql2_adapter.rb +73 -0
  9. data/lib/aws_advanced_ruby_driver_wrapper/active_record/aws_postgresql_adapter.rb +95 -0
  10. data/lib/aws_advanced_ruby_driver_wrapper/custom_configuration.rb +58 -0
  11. data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/aurora_mysql_dialect.rb +103 -0
  12. data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/aurora_pg_dialect.rb +124 -0
  13. data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/dialect_codes.rb +38 -0
  14. data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/global_mysql_dialect.rb +91 -0
  15. data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/global_pg_dialect.rb +92 -0
  16. data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/multi_az_cluster_mysql_dialect.rb +95 -0
  17. data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/multi_az_cluster_pg_dialect.rb +86 -0
  18. data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/mysql_dialect.rb +98 -0
  19. data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/pg_dialect.rb +95 -0
  20. data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/rds_mysql_dialect.rb +88 -0
  21. data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/rds_pg_dialect.rb +86 -0
  22. data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/unknown_dialect.rb +72 -0
  23. data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/utils/dialect_utils.rb +71 -0
  24. data/lib/aws_advanced_ruby_driver_wrapper/driver_dialects/driver_dialect.rb +154 -0
  25. data/lib/aws_advanced_ruby_driver_wrapper/driver_dialects/driver_dialect_manager.rb +55 -0
  26. data/lib/aws_advanced_ruby_driver_wrapper/driver_dialects/mysql_driver_dialect.rb +165 -0
  27. data/lib/aws_advanced_ruby_driver_wrapper/driver_dialects/pg_driver_dialect.rb +201 -0
  28. data/lib/aws_advanced_ruby_driver_wrapper/errors/error_handler.rb +62 -0
  29. data/lib/aws_advanced_ruby_driver_wrapper/errors/mysql_error_handler.rb +80 -0
  30. data/lib/aws_advanced_ruby_driver_wrapper/errors/pg_error_handler.rb +126 -0
  31. data/lib/aws_advanced_ruby_driver_wrapper/errors.rb +59 -0
  32. data/lib/aws_advanced_ruby_driver_wrapper/host/connection_string_host_list_provider.rb +95 -0
  33. data/lib/aws_advanced_ruby_driver_wrapper/host/global_aurora_host_list_provider.rb +65 -0
  34. data/lib/aws_advanced_ruby_driver_wrapper/host/host_availability.rb +24 -0
  35. data/lib/aws_advanced_ruby_driver_wrapper/host/host_availability_strategy.rb +27 -0
  36. data/lib/aws_advanced_ruby_driver_wrapper/host/host_info.rb +137 -0
  37. data/lib/aws_advanced_ruby_driver_wrapper/host/host_role.rb +25 -0
  38. data/lib/aws_advanced_ruby_driver_wrapper/host/random_host_selector.rb +40 -0
  39. data/lib/aws_advanced_ruby_driver_wrapper/host/rds_host_list_provider.rb +206 -0
  40. data/lib/aws_advanced_ruby_driver_wrapper/logging.rb +110 -0
  41. data/lib/aws_advanced_ruby_driver_wrapper/monitoring/cluster_topology_monitor.rb +709 -0
  42. data/lib/aws_advanced_ruby_driver_wrapper/monitoring/global_cluster_topology_monitor.rb +72 -0
  43. data/lib/aws_advanced_ruby_driver_wrapper/monitoring/monitor.rb +99 -0
  44. data/lib/aws_advanced_ruby_driver_wrapper/monitoring/monitor_connection.rb +57 -0
  45. data/lib/aws_advanced_ruby_driver_wrapper/monitoring/monitor_state.rb +25 -0
  46. data/lib/aws_advanced_ruby_driver_wrapper/mysql.rb +429 -0
  47. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/blue_green_plugin.rb +205 -0
  48. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/host_mapper.rb +132 -0
  49. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/iam_host_tracker.rb +84 -0
  50. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/interim_status.rb +92 -0
  51. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/interval_rate.rb +27 -0
  52. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/phase.rb +69 -0
  53. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/phase_event_log.rb +85 -0
  54. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/phase_time_info.rb +25 -0
  55. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/role.rb +38 -0
  56. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/routing/base_routing.rb +83 -0
  57. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/routing/reject_connect_routing.rb +40 -0
  58. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/routing/substitute_connect_routing.rb +136 -0
  59. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/routing/suspend_connect_routing.rb +53 -0
  60. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/routing/suspend_execute_routing.rb +52 -0
  61. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/routing/suspend_until_corresponding_host_found_connect_routing.rb +83 -0
  62. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/status.rb +68 -0
  63. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/status_builder.rb +244 -0
  64. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/status_info.rb +30 -0
  65. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/status_monitor.rb +564 -0
  66. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/status_provider.rb +414 -0
  67. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/switchover_state.rb +98 -0
  68. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/switchover_timer.rb +46 -0
  69. data/lib/aws_advanced_ruby_driver_wrapper/plugins/custom_endpoint/custom_endpoint_monitor.rb +266 -0
  70. data/lib/aws_advanced_ruby_driver_wrapper/plugins/custom_endpoint/custom_endpoint_plugin.rb +158 -0
  71. data/lib/aws_advanced_ruby_driver_wrapper/plugins/custom_endpoint/info.rb +111 -0
  72. data/lib/aws_advanced_ruby_driver_wrapper/plugins/custom_endpoint/member_list_type.rb +31 -0
  73. data/lib/aws_advanced_ruby_driver_wrapper/plugins/custom_endpoint/role.rb +45 -0
  74. data/lib/aws_advanced_ruby_driver_wrapper/plugins/default_plugin.rb +108 -0
  75. data/lib/aws_advanced_ruby_driver_wrapper/plugins/failover_mode.rb +43 -0
  76. data/lib/aws_advanced_ruby_driver_wrapper/plugins/failover_plugin.rb +467 -0
  77. data/lib/aws_advanced_ruby_driver_wrapper/plugins/gdb/gdb_failover_mode.rb +68 -0
  78. data/lib/aws_advanced_ruby_driver_wrapper/plugins/gdb/gdb_failover_plugin.rb +403 -0
  79. data/lib/aws_advanced_ruby_driver_wrapper/plugins/iam_auth_plugin.rb +159 -0
  80. data/lib/aws_advanced_ruby_driver_wrapper/plugins/initial_connection_strategy_plugin.rb +485 -0
  81. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/audit_logger.rb +157 -0
  82. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/column_cipher.rb +159 -0
  83. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/column_encryption_config.rb +61 -0
  84. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/connection_source.rb +91 -0
  85. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/data_key_cache.rb +220 -0
  86. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/encryption_algorithm.rb +75 -0
  87. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/encryption_config.rb +146 -0
  88. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/encryption_service.rb +391 -0
  89. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/error_context.rb +198 -0
  90. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/errors.rb +259 -0
  91. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/key_management_utility.rb +435 -0
  92. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/key_manager.rb +378 -0
  93. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/key_metadata.rb +86 -0
  94. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/kms_encryption_plugin.rb +890 -0
  95. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/kms_encryption_utility.rb +281 -0
  96. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/metadata_manager.rb +332 -0
  97. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/sanitizer.rb +147 -0
  98. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/schema_name.rb +70 -0
  99. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/schema_validator.rb +211 -0
  100. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/sql_runner.rb +147 -0
  101. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/type_marker.rb +109 -0
  102. data/lib/aws_advanced_ruby_driver_wrapper/plugins/secrets_manager_plugin.rb +358 -0
  103. data/lib/aws_advanced_ruby_driver_wrapper/postgresql.rb +659 -0
  104. data/lib/aws_advanced_ruby_driver_wrapper/property_definition.rb +409 -0
  105. data/lib/aws_advanced_ruby_driver_wrapper/ruby_method.rb +122 -0
  106. data/lib/aws_advanced_ruby_driver_wrapper/services/connection_service.rb +143 -0
  107. data/lib/aws_advanced_ruby_driver_wrapper/services/dialect_service.rb +267 -0
  108. data/lib/aws_advanced_ruby_driver_wrapper/services/host_service.rb +199 -0
  109. data/lib/aws_advanced_ruby_driver_wrapper/services/monitor_service.rb +186 -0
  110. data/lib/aws_advanced_ruby_driver_wrapper/services/plugin_call_context.rb +63 -0
  111. data/lib/aws_advanced_ruby_driver_wrapper/services/plugin_manager.rb +273 -0
  112. data/lib/aws_advanced_ruby_driver_wrapper/services/service_container.rb +30 -0
  113. data/lib/aws_advanced_ruby_driver_wrapper/services/service_utility.rb +78 -0
  114. data/lib/aws_advanced_ruby_driver_wrapper/services/session_state_service.rb +56 -0
  115. data/lib/aws_advanced_ruby_driver_wrapper/utils/accessible_regions.rb +52 -0
  116. data/lib/aws_advanced_ruby_driver_wrapper/utils/ar_constants.rb +25 -0
  117. data/lib/aws_advanced_ruby_driver_wrapper/utils/aurora_topology_utils.rb +99 -0
  118. data/lib/aws_advanced_ruby_driver_wrapper/utils/aws_credentials_utils.rb +62 -0
  119. data/lib/aws_advanced_ruby_driver_wrapper/utils/connection_config.rb +91 -0
  120. data/lib/aws_advanced_ruby_driver_wrapper/utils/connection_config_parser.rb +368 -0
  121. data/lib/aws_advanced_ruby_driver_wrapper/utils/conversion_utils.rb +51 -0
  122. data/lib/aws_advanced_ruby_driver_wrapper/utils/events/batching_event_publisher.rb +119 -0
  123. data/lib/aws_advanced_ruby_driver_wrapper/utils/events/data_access_event.rb +26 -0
  124. data/lib/aws_advanced_ruby_driver_wrapper/utils/events/monitor_reset_event.rb +26 -0
  125. data/lib/aws_advanced_ruby_driver_wrapper/utils/global_aurora_topology_utils.rb +185 -0
  126. data/lib/aws_advanced_ruby_driver_wrapper/utils/host_list_utils.rb +27 -0
  127. data/lib/aws_advanced_ruby_driver_wrapper/utils/iam_auth_utils.rb +112 -0
  128. data/lib/aws_advanced_ruby_driver_wrapper/utils/multi_az_topology_utils.rb +117 -0
  129. data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/encryption_annotation_parser.rb +99 -0
  130. data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/mysql_statement_analyzer.rb +641 -0
  131. data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/pg_statement_analyzer.rb +502 -0
  132. data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/query_analysis.rb +63 -0
  133. data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/query_type.rb +35 -0
  134. data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/routing_hint.rb +27 -0
  135. data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/routing_hint_parser.rb +50 -0
  136. data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/sql_parser.rb +139 -0
  137. data/lib/aws_advanced_ruby_driver_wrapper/utils/rds_url_type.rb +71 -0
  138. data/lib/aws_advanced_ruby_driver_wrapper/utils/rds_utils.rb +575 -0
  139. data/lib/aws_advanced_ruby_driver_wrapper/utils/retry_util.rb +153 -0
  140. data/lib/aws_advanced_ruby_driver_wrapper/utils/sql_encoding.rb +56 -0
  141. data/lib/aws_advanced_ruby_driver_wrapper/utils/sql_method_analyzer.rb +195 -0
  142. data/lib/aws_advanced_ruby_driver_wrapper/utils/storage/cache_entry.rb +56 -0
  143. data/lib/aws_advanced_ruby_driver_wrapper/utils/storage/expiration_cache.rb +108 -0
  144. data/lib/aws_advanced_ruby_driver_wrapper/utils/storage/sliding_expiration_cache.rb +137 -0
  145. data/lib/aws_advanced_ruby_driver_wrapper/utils/storage/storage_service.rb +172 -0
  146. data/lib/aws_advanced_ruby_driver_wrapper/utils/topology_utils.rb +127 -0
  147. data/lib/aws_advanced_ruby_driver_wrapper/version.rb +19 -0
  148. data/lib/aws_advanced_ruby_driver_wrapper/wrapper_property.rb +64 -0
  149. data/lib/aws_advanced_ruby_driver_wrapper.rb +116 -0
  150. metadata +227 -0
@@ -0,0 +1,890 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License").
6
+ # You may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ require 'concurrent'
18
+ require_relative '../../logging'
19
+ require_relative '../../ruby_method'
20
+ require_relative '../../utils/parser/encryption_annotation_parser'
21
+ require_relative '../../utils/parser/query_type'
22
+ require_relative '../../utils/parser/sql_parser'
23
+ require_relative '../../utils/sql_encoding'
24
+ require_relative 'column_cipher'
25
+ require_relative 'errors'
26
+ require_relative 'kms_encryption_utility'
27
+
28
+ module AwsAdvancedRubyDriverWrapper
29
+ module Plugins
30
+ # Encrypts and decrypts individual table columns with keys held in AWS KMS, without the
31
+ # application having to know about it.
32
+ #
33
+ # Which columns are encrypted is configured in the database rather than in application code, so
34
+ # it can change without redeploying the application. That configuration is managed through
35
+ # {Encryption::KeyManagementUtility} rather than by editing the tables by hand. When a
36
+ # statement writes to one of those columns the plugin encrypts the bind parameter on its way to
37
+ # the server, and when a statement reads one back it decrypts the value on its way to the
38
+ # application. The plaintext never reaches the server, and the data keys never reach it either:
39
+ # only a KMS encrypted copy of each data key is stored, in +key_storage+.
40
+ #
41
+ # Encryption applies to bind parameters, so a value can only be encrypted when it is bound
42
+ # rather than written into the SQL text:
43
+ #
44
+ # # pg
45
+ # conn.exec_params('INSERT INTO users (name, ssn) VALUES ($1, $2)', ['Jo', '123-45-6789'])
46
+ # conn.exec_params('SELECT ssn FROM users WHERE name = $1', ['Jo']).each { |row| row['ssn'] }
47
+ #
48
+ # # mysql2
49
+ # client.prepare('INSERT INTO users (name, ssn) VALUES (?, ?)').execute('Jo', '123-45-6789')
50
+ #
51
+ # The plugin works out which parameter belongs to which column by parsing the statement. When
52
+ # the statement is too involved for that, the column can be named explicitly with an
53
+ # annotation, which takes precedence over anything the parser found:
54
+ #
55
+ # conn.exec_params('INSERT INTO users (name, ssn) VALUES ($1, /*@encrypt:users.ssn*/ $2)', ...)
56
+ #
57
+ # Decrypted values are always returned as strings, whatever type the value had when it was
58
+ # written. An encrypted column is a binary column (+bytea+ or +VARBINARY+), which is what both
59
+ # drivers return as a string, so a string keeps the read consistent with the column's real type
60
+ # and with how ActiveRecord treats it. Cast the value on read when the application needs another
61
+ # type, for example +row['age'].to_i+.
62
+ #
63
+ # A row is decrypted however it is read. A row read as a hash is matched to its columns by name;
64
+ # a row (or single cell) read as bare values is matched by position, through the field list the
65
+ # result reports, which is what lets ActiveRecord's reads decrypt even though it fetches rows as
66
+ # arrays. This covers +PG::Result+'s +#each+, +#each_row+, +#to_a+, +#[]+, +#values+,
67
+ # +#field_values+, +#column_values+, +#tuple+, +#tuple_values+ and +#getvalue+, its single-row
68
+ # streaming +#stream_each+ / +#stream_each_row+ / +#stream_each_tuple+, and mysql2's hash and
69
+ # array results alike. A +COPY ... TO+ is the exception: its rows are a stream rather than values
70
+ # the plugin can replace, so it hands out what the column holds.
71
+ #
72
+ # The plugin does not try to guarantee that an encrypted column never holds a plaintext - it
73
+ # cannot, since it only sees the statements this wrapper sends over a connection that has it
74
+ # enabled, and only the ones it can read that far. That guarantee is the database's to make, with
75
+ # a trigger that checks a value's integrity tag as it goes in (it can do so without the data key,
76
+ # because the HMAC key is stored in +key_storage+), and installing one is required; see the
77
+ # plugin docs. The plugin's job is to encrypt every value it can confidently place into an
78
+ # encrypted column and to stay out of the way otherwise.
79
+ #
80
+ # So when the plugin cannot read a statement well enough to be sure - the tables it writes cannot
81
+ # be established, or a table has encrypted columns but which of them this statement writes cannot
82
+ # be enumerated, or a value is bound to a prepared statement whose text the connection never saw -
83
+ # it passes the statement through rather than refusing it, and leaves a plaintext for the database
84
+ # trigger to reject. Refusing here would reject legitimate statements that never touch an
85
+ # encrypted column at all.
86
+ #
87
+ # The one write-side case it still fails closed on is the one it can be certain about: a column it
88
+ # has confirmed is encrypted, written with something other than a bind parameter (a literal, an
89
+ # expression, a DEFAULT, a +COPY+ stream), which cannot be encrypted. That is almost always a
90
+ # mistake, so it is refused with advice to bind or annotate the value. An annotation naming the
91
+ # column takes it off the plugin's hands; a +COPY+ has no parameter for one to name, so it is
92
+ # steered to +INSERT+ instead.
93
+ #
94
+ # A prepared statement is run by name, so the plugin reads the statement the connection remembers
95
+ # preparing under that name, whether it was prepared by the driver's own +prepare+ or by a
96
+ # +PREPARE+ sent as a statement. A +PREPARE+ is also checked as it is sent, and not only when its
97
+ # name is later run, since a value written into the statement it carries rather than left as a
98
+ # parameter is only in hand while the +PREPARE+ itself is.
99
+ class KmsEncryptionPlugin
100
+ include Logging
101
+
102
+ # Statement methods whose bind parameters may have to be encrypted, mapped to the position of
103
+ # the parameter array in the argument list. A nil position means the arguments are themselves
104
+ # the parameters.
105
+ PARAMETER_METHODS = {
106
+ RubyMethod::CONNECTION_EXEC.name => 1,
107
+ RubyMethod::CONNECTION_ASYNC_EXEC.name => 1,
108
+ RubyMethod::CONNECTION_EXEC_PARAMS.name => 1,
109
+ RubyMethod::CONNECTION_SEND_QUERY.name => 1,
110
+ RubyMethod::CONNECTION_SEND_QUERY_PARAMS.name => 1,
111
+ RubyMethod::CONNECTION_EXEC_PREPARED.name => 1,
112
+ RubyMethod::CONNECTION_SEND_QUERY_PREPARED.name => 1,
113
+ RubyMethod::STATEMENT_EXECUTE.name => nil
114
+ }.freeze
115
+
116
+ # Statement methods that take no bind parameters. There is nothing to encrypt for these, but a
117
+ # statement that carries its values outside its bind parameters is exactly the one that could
118
+ # write a confirmed encrypted column unencryptably, so they are still run through the write
119
+ # check.
120
+ #
121
+ # mysql2's +query+ covers its asynchronous path as well, since that is the same call with
122
+ # +async: true+ passed to it, and the statement is inspected when it is sent either way.
123
+ #
124
+ # pg's +copy_data+ is here because it opens its COPY on the driver's own connection rather than
125
+ # through the wrapper, so the statement would otherwise never be seen. Checking it when the COPY
126
+ # is opened is what makes checking the calls that feed it unnecessary: a COPY that names a
127
+ # confirmed encrypted column is refused before there is anywhere to put a row.
128
+ WRITE_CHECK_METHODS = Set[
129
+ RubyMethod::CONNECTION_QUERY.name,
130
+ RubyMethod::CONNECTION_COPY_DATA.name
131
+ ].freeze
132
+
133
+ # Result methods that yield rows to a block, one at a time. The +stream_+ variants are the
134
+ # single-row-mode iterators, which read rows off the wire one at a time but hand out the same
135
+ # row shapes.
136
+ ROW_BLOCK_METHODS = Set[
137
+ RubyMethod::RESULT_EACH.name,
138
+ RubyMethod::RESULT_EACH_ROW.name,
139
+ RubyMethod::RESULT_STREAM_EACH.name,
140
+ RubyMethod::RESULT_STREAM_EACH_ROW.name,
141
+ RubyMethod::RESULT_STREAM_EACH_TUPLE.name
142
+ ].freeze
143
+
144
+ # Result methods that return every row at once, as an array of rows.
145
+ ROW_COLLECTION_METHODS = Set[
146
+ RubyMethod::RESULT_TO_A.name,
147
+ RubyMethod::RESULT_VALUES.name
148
+ ].freeze
149
+
150
+ # Result methods that return a single row.
151
+ ROW_SINGLE_METHODS = Set[
152
+ RubyMethod::RESULT_BRACKET.name,
153
+ RubyMethod::RESULT_TUPLE.name,
154
+ RubyMethod::RESULT_TUPLE_VALUES.name
155
+ ].freeze
156
+
157
+ # Every result method that hands out whole rows, however it does so. A row may arrive as a hash
158
+ # keyed by column name or as an array of bare values, and either is decrypted: a hash by name,
159
+ # an array by matching each position to a column through the result's field list.
160
+ ROW_METHODS = (ROW_BLOCK_METHODS + ROW_COLLECTION_METHODS + ROW_SINGLE_METHODS).freeze
161
+
162
+ # Result methods that hand out every value of one column. One names the column; the other gives
163
+ # its position in the result, which is matched to a name through the field list.
164
+ COLUMN_BY_NAME_METHOD = RubyMethod::RESULT_FIELD_VALUES.name
165
+ COLUMN_BY_INDEX_METHOD = RubyMethod::RESULT_COLUMN_VALUES.name
166
+
167
+ # The result method that hands out a single cell by row and column position.
168
+ VALUE_BY_INDEX_METHOD = RubyMethod::RESULT_GETVALUE.name
169
+
170
+ # Statement types that store values. These are the ones run through the write check, which
171
+ # refuses a statement that writes a confirmed encrypted column with a value it cannot encrypt
172
+ # and otherwise leaves the statement to the database's own enforcement.
173
+ WRITE_QUERY_TYPES = Set[
174
+ Utils::Parser::QueryType::INSERT,
175
+ Utils::Parser::QueryType::UPDATE,
176
+ Utils::Parser::QueryType::COPY
177
+ ].freeze
178
+
179
+ # A statement that stores values, as far as its keywords go. The keywords are looked at as well
180
+ # as the parse, because a statement the parser could not read has no query type, and that is
181
+ # precisely the case that must not be mistaken for a read.
182
+ #
183
+ # The keyword is not always the first thing in the text. Query instrumentation prepends a
184
+ # comment routinely, and a common table expression can come in front of a statement that
185
+ # writes, so both are stepped over before the keyword is looked for.
186
+ WRITE_KEYWORDS = %r{
187
+ \A(?:\s|\(|/\*.*?\*/|--[^\n]*|\#[^\n]*)* # comments and whitespace in front of it
188
+ (?:WITH\s.*?\s)? # a common table expression in front of it
189
+ (?:INSERT|UPDATE|REPLACE|UPSERT|MERGE)\b
190
+ }imx
191
+
192
+ # What a character is replaced with in the copy of the SQL the plugin reads when the SQL's
193
+ # encoding has no UTF-8 form for it (see {Utils::SqlEncoding.inspectable}).
194
+ UNREADABLE_CHARACTER = "\uFFFD"
195
+
196
+ SUBSCRIBED_METHODS = (
197
+ Set[RubyMethod::CONNECTION_CLOSE.name, COLUMN_BY_NAME_METHOD, COLUMN_BY_INDEX_METHOD, VALUE_BY_INDEX_METHOD] +
198
+ PARAMETER_METHODS.keys + WRITE_CHECK_METHODS + ROW_METHODS
199
+ ).freeze
200
+
201
+ attr_reader :subscribed_methods, :encryption_utility
202
+
203
+ # @param service_container [Services::ServiceContainer]
204
+ # @param props [Concurrent::Map, Hash] the wrapper properties
205
+ # @param encryption_utility [Encryption::KmsEncryptionUtility, nil] a utility to use instead
206
+ # of building one
207
+ def initialize(service_container, props = ::Concurrent::Map.new, encryption_utility: nil)
208
+ @service_container = service_container
209
+ @encryption_utility = encryption_utility || Encryption::KmsEncryptionUtility.new(service_container, props)
210
+ # Built up front so that a parser dependency the application has not installed (pg_query, on
211
+ # PostgreSQL) fails the connection as it is set up rather than its first statement.
212
+ @sql_parser = Utils::Parser::SqlParser.new(service_container.dialect_service.driver_dialect)
213
+ @subscribed_methods = SUBSCRIBED_METHODS
214
+ # The names already warned about by {#unreadable_name?}, which is asked on every lookup and so
215
+ # would otherwise repeat the same warning for every row and statement that touches the name.
216
+ @unreadable_names = ::Concurrent::Set.new
217
+ end
218
+
219
+ # The call's block is taken from the call context rather than from a block parameter, since
220
+ # that is where the pipeline reads the block it passes on.
221
+ def execute(method_name, pipeline_callable, *args, **_kwargs)
222
+ return close_connection(pipeline_callable) if method_name == RubyMethod::CONNECTION_CLOSE.name
223
+
224
+ context = @service_container.plugin_manager.current_call_context
225
+ sql = context&.sql
226
+
227
+ if PARAMETER_METHODS.key?(method_name)
228
+ encrypt_parameters(method_name, args, context, sql)
229
+ pipeline_callable.call
230
+ elsif WRITE_CHECK_METHODS.include?(method_name)
231
+ verify_statement(sql)
232
+ pipeline_callable.call
233
+ elsif ROW_METHODS.include?(method_name)
234
+ read_rows(method_name, pipeline_callable, context, sql)
235
+ elsif method_name == COLUMN_BY_NAME_METHOD
236
+ read_named_column(pipeline_callable, args.first, sql)
237
+ elsif method_name == COLUMN_BY_INDEX_METHOD
238
+ read_indexed_column(pipeline_callable, args.first, context, sql)
239
+ elsif method_name == VALUE_BY_INDEX_METHOD
240
+ read_indexed_value(pipeline_callable, args, context, sql)
241
+ else
242
+ pipeline_callable.call
243
+ end
244
+ end
245
+
246
+ private
247
+
248
+ def close_connection(pipeline_callable)
249
+ @encryption_utility.cleanup
250
+ pipeline_callable.call
251
+ end
252
+
253
+ # -- Writing --
254
+
255
+ # Replaces every bind parameter that belongs to an encrypted column with its encrypted form.
256
+ # The parameters are handed back through the call context, so that the application's own
257
+ # array is left as it was.
258
+ def encrypt_parameters(method_name, args, context, sql)
259
+ return if context.nil?
260
+
261
+ position = PARAMETER_METHODS[method_name]
262
+ parameters = position.nil? ? args : args[position]
263
+ parameters = [] unless parameters.is_a?(Array)
264
+ return note_unknown_statement(parameters) if sql.nil?
265
+
266
+ # Asked for even when there is nothing to bind, since that is what says whether the
267
+ # statement is storing a value the plugin cannot reach.
268
+ columns = parameter_columns(sql)
269
+ return if columns.empty? || parameters.empty?
270
+
271
+ encrypted = encrypt_each(parameters, columns)
272
+ return if encrypted.nil?
273
+
274
+ if position.nil?
275
+ context.args = encrypted
276
+ else
277
+ rewritten = args.dup
278
+ rewritten[position] = encrypted
279
+ context.args = rewritten
280
+ end
281
+ end
282
+
283
+ # A call whose statement the wrapper never saw: a prepared statement run by name after something
284
+ # other than a +prepare+ or a +PREPARE+ brought it into being (one prepared inside a
285
+ # multi-statement string, on the driver's connection directly, or by a client library of its
286
+ # own devising). The plugin cannot tell which column each bound value fills, so it leaves them
287
+ # to the database rather than refusing the call; if any targets an encrypted column, the
288
+ # required server-side enforcement is what catches a plaintext bound this way. Logged at debug
289
+ # since the great majority of such statements touch no encrypted column at all.
290
+ def note_unknown_statement(parameters)
291
+ return if parameters.empty?
292
+
293
+ logger.debug { 'The kms_encryption plugin is binding values to a statement it never saw; leaving them to the database' }
294
+ end
295
+
296
+ # A statement with no bind parameters has nothing to encrypt, so only its safety is at stake.
297
+ def verify_statement(sql)
298
+ return if sql.nil?
299
+
300
+ parameter_columns(sql) # nothing to bind; called for the checks it makes
301
+ end
302
+
303
+ # @return [Array, nil] the parameters with the encrypted ones replaced, nil when none were
304
+ def encrypt_each(parameters, columns)
305
+ cipher = new_cipher
306
+ encrypted = nil
307
+
308
+ begin
309
+ parameters.each_with_index do |parameter, index|
310
+ config = columns[index + 1]
311
+ value = parameter_value(parameter)
312
+ next if config.nil? || value.nil?
313
+
314
+ encrypted ||= parameters.dup
315
+ encrypted[index] = bind_value(encrypt_value(value, config, cipher))
316
+ end
317
+ ensure
318
+ cipher.release
319
+ end
320
+
321
+ encrypted
322
+ end
323
+
324
+ # pg also takes a parameter as a hash that carries its value along with its format and type, and
325
+ # ActiveRecord binds every binary column that way. Only the value is encrypted: the ciphertext
326
+ # is bound as binary whatever format the hash named, and a hash with no value stays null.
327
+ def parameter_value(parameter)
328
+ parameter.is_a?(Hash) && sql_runner.pg? ? parameter[:value] : parameter
329
+ end
330
+
331
+ def encrypt_value(value, config, cipher)
332
+ cipher.encrypt(value, config)
333
+ rescue StandardError => e
334
+ audit_logger.log_encryption(
335
+ table_name: config.table_name, column_name: config.column_name, key_id: config.key_id,
336
+ success: false, error_message: e.message
337
+ )
338
+ raise
339
+ end
340
+
341
+ # An encrypted value is binary, and neither driver would send a Ruby string as binary on its
342
+ # own: pg needs to be told the parameter format, and mysql2 needs the encoding.
343
+ def bind_value(encrypted)
344
+ sql_runner.binary_param(encrypted)
345
+ end
346
+
347
+ # -- Reading --
348
+
349
+ def read_rows(method_name, pipeline_callable, context, sql)
350
+ columns = column_configs(sql)
351
+ return pipeline_callable.call if columns.empty?
352
+
353
+ cipher = new_cipher
354
+ field_names = context&.field_names
355
+ columns = keyed_by_field_names(columns, field_names)
356
+ # A row read as an array of values is matched to its columns by position; a row read as a
357
+ # hash is matched by name and never needs this. It is worked out once for the whole result.
358
+ positions = encrypted_positions(columns, field_names)
359
+ caller_block = context&.block
360
+
361
+ begin
362
+ if ROW_BLOCK_METHODS.include?(method_name) && caller_block
363
+ # each and each_row yield the rows rather than returning them, so the block is what has
364
+ # to be decrypted through. Replacing it in the call context leaves the caller's own block
365
+ # untouched. A lambda is used rather than a proc so that a row yielded as an array of
366
+ # values is passed on whole, instead of being splatted across the block's parameters.
367
+ context.block = ->(row, *rest) { caller_block.call(decrypt_row(row, columns, positions, cipher), *rest) }
368
+ pipeline_callable.call
369
+ elsif ROW_BLOCK_METHODS.include?(method_name)
370
+ # Called without a block, each and each_row return an Enumerator over the rows. The rows
371
+ # are decrypted eagerly and handed back as an enumerator over the results, because the
372
+ # cipher is released as soon as this method returns and a lazy wrapper would decrypt with
373
+ # a spent cipher.
374
+ result = pipeline_callable.call
375
+ result.respond_to?(:map) ? result.map { |row| decrypt_row(row, columns, positions, cipher) }.each : result
376
+ elsif ROW_COLLECTION_METHODS.include?(method_name)
377
+ rows = pipeline_callable.call
378
+ rows.is_a?(Array) ? rows.map { |row| decrypt_row(row, columns, positions, cipher) } : rows
379
+ else
380
+ decrypt_row(pipeline_callable.call, columns, positions, cipher)
381
+ end
382
+ ensure
383
+ cipher.release
384
+ end
385
+ end
386
+
387
+ # field_values hands back one named column's values, so the column is looked up by name.
388
+ def read_named_column(pipeline_callable, field_name, sql)
389
+ config = field_name.nil? ? nil : config_named(column_configs(sql), field_name)
390
+ decrypt_column(pipeline_callable, config)
391
+ end
392
+
393
+ # column_values hands back one column's values by position, so the position is matched to a
394
+ # column name through the result's field list before the values are decrypted.
395
+ def read_indexed_column(pipeline_callable, index, context, sql)
396
+ columns = column_configs(sql)
397
+ config = columns.empty? ? nil : column_at(index, context&.field_names, columns)
398
+ decrypt_column(pipeline_callable, config)
399
+ end
400
+
401
+ # getvalue hands back a single cell by row and column position (+args+ is +[row, column]+), so
402
+ # the column position is matched to a name through the field list before the value is decrypted.
403
+ def read_indexed_value(pipeline_callable, args, context, sql)
404
+ columns = column_configs(sql)
405
+ config = columns.empty? ? nil : column_at(args[1], context&.field_names, columns)
406
+ return pipeline_callable.call if config.nil?
407
+
408
+ cipher = new_cipher
409
+
410
+ begin
411
+ decrypt_value(pipeline_callable.call, config, cipher)
412
+ ensure
413
+ cipher.release
414
+ end
415
+ end
416
+
417
+ def decrypt_column(pipeline_callable, config)
418
+ return pipeline_callable.call if config.nil?
419
+
420
+ cipher = new_cipher
421
+
422
+ begin
423
+ values = pipeline_callable.call
424
+ values.is_a?(Array) ? values.map { |value| decrypt_value(value, config, cipher) } : values
425
+ ensure
426
+ cipher.release
427
+ end
428
+ end
429
+
430
+ # @param columns [Hash{String => ColumnEncryptionConfig}] the encrypted columns of the
431
+ # statement, by name, for a row read as a hash
432
+ # @param positions [Array<Array(Integer, ColumnEncryptionConfig)>] the encrypted columns by
433
+ # position, for a row read as an array of values
434
+ # @return [Object] the row, with a new hash or array substituted only when something was
435
+ # decrypted
436
+ def decrypt_row(row, columns, positions, cipher)
437
+ case row
438
+ when Hash then decrypt_named_row(row, columns, cipher)
439
+ when Array then decrypt_indexed_row(row, positions, cipher)
440
+ else pg_tuple?(row) ? decrypt_tuple(row, columns, cipher) : row
441
+ end
442
+ end
443
+
444
+ # A row read as a hash: the encrypted columns are found by name.
445
+ def decrypt_named_row(row, columns, cipher)
446
+ decrypted = nil
447
+ columns.each do |column_name, config|
448
+ next unless row.key?(column_name)
449
+
450
+ raw = row[column_name]
451
+ value = decrypt_value(raw, config, cipher)
452
+ next if value.equal?(raw)
453
+
454
+ decrypted ||= row.dup
455
+ decrypted[column_name] = value
456
+ end
457
+
458
+ decrypted || row
459
+ end
460
+
461
+ # A row read as an array of values: the encrypted columns are found by position.
462
+ def decrypt_indexed_row(row, positions, cipher)
463
+ decrypted = nil
464
+ positions.each do |index, config|
465
+ raw = row[index]
466
+ value = decrypt_value(raw, config, cipher)
467
+ next if value.equal?(raw)
468
+
469
+ decrypted ||= row.dup
470
+ decrypted[index] = value
471
+ end
472
+
473
+ decrypted || row
474
+ end
475
+
476
+ # A pg tuple is read-only and keyed by column name, so a decrypted copy is handed back as a
477
+ # plain hash, and only when a column was actually decrypted so an unaffected tuple keeps its
478
+ # type.
479
+ def decrypt_tuple(row, columns, cipher)
480
+ keys = row.keys
481
+ copy = nil
482
+ columns.each do |column_name, config|
483
+ next unless keys.include?(column_name)
484
+
485
+ raw = row[column_name]
486
+ value = decrypt_value(raw, config, cipher)
487
+ next if value.equal?(raw)
488
+
489
+ copy ||= keys.zip(row.values).to_h
490
+ copy[column_name] = value
491
+ end
492
+
493
+ copy || row
494
+ end
495
+
496
+ # Matches each encrypted column to its position in a result read as arrays of values, so a row
497
+ # can be decrypted by index. Empty when the call carried no field list (a row read as a hash
498
+ # never needs it) or none of the result's columns is encrypted.
499
+ #
500
+ # @param columns [Hash{String => ColumnEncryptionConfig}] encrypted columns by name
501
+ # @param field_names [Array<String>, nil] the result's columns in order
502
+ # @return [Array<Array(Integer, ColumnEncryptionConfig)>]
503
+ def encrypted_positions(columns, field_names)
504
+ return [] if field_names.nil? || columns.empty?
505
+
506
+ field_names.each_with_index.with_object([]) do |(name, index), positions|
507
+ config = columns[name.to_s]
508
+ positions << [index, config] if config
509
+ end
510
+ end
511
+
512
+ # @return [ColumnEncryptionConfig, nil] the encrypted column at a position in the result, nil
513
+ # when the position is out of range or the column there is not encrypted
514
+ def column_at(index, field_names, columns)
515
+ return nil unless index.is_a?(Integer) && field_names
516
+
517
+ name = field_names[index]
518
+ name.nil? ? nil : config_named(columns, name)
519
+ end
520
+
521
+ # A result names its columns in the connection's encoding, while the configuration names them in
522
+ # UTF-8, so on a connection that is not UTF-8 a column whose name is not ASCII is named
523
+ # differently by each. The columns are keyed by the names the result uses as well, once for the
524
+ # whole result, so that each row is looked up by the name it actually carries. The columns are
525
+ # only copied when a name differs, so a result on a UTF-8 connection gets them as they are.
526
+ #
527
+ # @param columns [Hash{String => ColumnEncryptionConfig}] encrypted columns by UTF-8 name
528
+ # @param field_names [Array<String>, nil] the result's columns in order
529
+ # @return [Hash{String => ColumnEncryptionConfig}]
530
+ def keyed_by_field_names(columns, field_names)
531
+ return columns if field_names.nil?
532
+
533
+ keyed = nil
534
+ field_names.each do |name|
535
+ name = name.to_s
536
+ next if columns.key?(name)
537
+
538
+ config = columns[Utils::SqlEncoding.inspectable(name)]
539
+ (keyed ||= columns.dup)[name] = config if config
540
+ end
541
+ keyed || columns
542
+ end
543
+
544
+ # @return [ColumnEncryptionConfig, nil] the encrypted column a result or the application names,
545
+ # whatever the encoding of the name
546
+ def config_named(columns, name)
547
+ name = name.to_s
548
+ columns[name] || columns[Utils::SqlEncoding.inspectable(name)]
549
+ end
550
+
551
+ def pg_tuple?(row)
552
+ defined?(PG::Tuple) && row.is_a?(PG::Tuple)
553
+ end
554
+
555
+ def decrypt_value(raw, config, cipher)
556
+ cipher.decrypt(raw, config)
557
+ rescue StandardError => e
558
+ # In lenient mode a value that cannot be confirmed to be this column's encrypted data - too
559
+ # short to be a payload, or a failed HMAC - is returned as it is stored, so a column holding
560
+ # values written before kms_encryption was enabled still reads back. A value that verifies
561
+ # but will not decrypt (a GCM/data-key failure) is never returned unverified: it signals a
562
+ # real key problem, so only an integrity-check failure is eligible. Returning +raw+ is how
563
+ # the row readers see "not decrypted, leave as-is".
564
+ lenient = return_unverified_data? && e.is_a?(Errors::EncryptionError) &&
565
+ e.code == Errors::EncryptionError::INTEGRITY_CHECK_FAILED
566
+ audit_logger.log_decryption(
567
+ table_name: config.table_name, column_name: config.column_name, key_id: config.key_id,
568
+ success: false, error_message: lenient ? "returned unverified: #{e.message}" : e.message
569
+ )
570
+ return raw if lenient
571
+
572
+ raise
573
+ end
574
+
575
+ # @return [Boolean] whether a value that cannot be verified on read is returned as it is stored
576
+ # rather than raised on. Off by default; not for production - see the property's documentation.
577
+ def return_unverified_data?
578
+ @encryption_utility.config.return_unverified_data
579
+ end
580
+
581
+ # -- Statement analysis --
582
+
583
+ # The encrypted columns a statement's bind parameters write to.
584
+ #
585
+ # @return [Hash{Integer => ColumnEncryptionConfig}] by 1-based parameter index
586
+ # @raise [Errors::MetadataError] when the statement writes a column the plugin has confirmed is
587
+ # encrypted with a value it cannot encrypt (see {#check_write})
588
+ def parameter_columns(sql)
589
+ annotations = Utils::Parser::EncryptionAnnotationParser.parse_annotations(sql)
590
+ stripped = stripped_sql(sql)
591
+ analysis = analysis_of(stripped)
592
+ write = write_statement?(analysis, stripped)
593
+ inferred = analysis.parameter_column_names
594
+ return {} if !write && annotations.empty? && inferred.empty?
595
+ return {} unless ready_for_statement?(sql)
596
+
597
+ tables = statement_tables(analysis, annotations)
598
+ check_write(analysis, tables, annotations) if write
599
+
600
+ inferred.merge(annotations).each_with_object({}) do |(index, reference), columns|
601
+ config = resolve_column(reference, tables)
602
+ columns[index] = config if config
603
+ end
604
+ end
605
+
606
+ # The statement as far as it could be read. A parse that fails is not the same as a statement
607
+ # that writes nothing, so it is reported as nothing having been read rather than as nothing
608
+ # being there to read.
609
+ #
610
+ # @return [Utils::Parser::SqlParser::SqlAnalysisResult]
611
+ def analysis_of(sql)
612
+ sql_parser.analyze_sql(sql)
613
+ rescue StandardError => e
614
+ logger.warn("The statement could not be analysed: #{e.message}")
615
+ Utils::Parser::SqlParser::SqlAnalysisResult.new(
616
+ query_type: Utils::Parser::QueryType::UNKNOWN,
617
+ affected_tables: Set.new,
618
+ write_columns_complete: false
619
+ )
620
+ end
621
+
622
+ def write_statement?(analysis, sql)
623
+ WRITE_QUERY_TYPES.include?(analysis.query_type) || WRITE_KEYWORDS.match?(sql.to_s)
624
+ end
625
+
626
+ # The one write-side check the plugin fails closed on: a column it has confirmed is
627
+ # encrypted, but that this statement writes with something other than a bind parameter (a
628
+ # literal, an expression, a DEFAULT, a COPY stream), which cannot be encrypted. That is almost
629
+ # always a mistake, and storing a plaintext in an encrypted column reads back clean ever after,
630
+ # so it is refused with advice to bind or annotate the value. An annotation naming the column
631
+ # takes it off the plugin's hands; a COPY has no parameter for one to name.
632
+ #
633
+ # Everything else about a write the plugin cannot fully read is left to the database rather
634
+ # than refused. When the tables cannot be established at all, or when a table has encrypted
635
+ # columns but which of them this statement writes cannot be enumerated, the plugin cannot be
636
+ # sure a plaintext is at stake, and guessing wrong by refusing would reject legitimate
637
+ # statements that never touch an encrypted column. The required server-side enforcement is what
638
+ # actually guarantees no plaintext reaches an encrypted column; see the plugin docs. The
639
+ # "columns could not be enumerated" case is warned about (the table does hold encrypted
640
+ # columns, so it is worth a line), the "tables unknown" case is left to debug (it usually is
641
+ # not relevant at all).
642
+ #
643
+ # @param tables [Array<String>] the tables the statement writes to
644
+ # @raise [Errors::MetadataError] only for the confirmed-encrypted, cannot-encrypt case above
645
+ def check_write(analysis, tables, annotations)
646
+ copy = analysis.query_type == Utils::Parser::QueryType::COPY
647
+
648
+ if tables.empty?
649
+ logger.debug { 'The kms_encryption plugin could not establish the tables a write targets; leaving it to the database' }
650
+ return
651
+ end
652
+
653
+ # A COPY has no bind parameter for an annotation to name, so an annotation cannot make one of
654
+ # its columns encryptable and does not excuse it.
655
+ named = copy ? Set.new : annotated_columns(annotations, tables)
656
+ # An unbound column with no table of its own is matched against the statement's tables in
657
+ # order (see resolve_column). In the rare multi-table write where the same bare column name is
658
+ # encrypted in one table but written unbound in another, this can refuse a write that never
659
+ # touches the encrypted column. That is fail-closed and the annotation is the way out of it, so
660
+ # it is left as is rather than complicated further.
661
+ unencryptable = analysis.unbound_write_columns.find do |column|
662
+ config = encrypted_column(column, tables)
663
+ config && !named.include?(config.column_identifier)
664
+ end
665
+ raise unencryptable_write_error(unencryptable, copy: copy) if unencryptable
666
+ return if analysis.write_columns_complete || (annotations.any? && !copy)
667
+
668
+ hidden = tables.find { |table| encrypted_columns_of(table).any? }
669
+ logger.warn(unreadable_columns_warning(hidden)) if hidden
670
+ end
671
+
672
+ # The encrypted columns the statement's own annotations name.
673
+ #
674
+ # @return [Set<String>] column identifiers, empty when nothing was annotated
675
+ def annotated_columns(annotations, tables)
676
+ annotations.each_value.with_object(Set.new) do |reference, named|
677
+ config = resolve_column(reference, tables)
678
+ named << config.column_identifier if config
679
+ end
680
+ end
681
+
682
+ # @param column [Utils::Parser::ColumnInfo]
683
+ # @return [ColumnEncryptionConfig, nil] nil when the column is not encrypted
684
+ def encrypted_column(column, tables)
685
+ reference = column.table_name.nil? ? column.column_name : "#{column.table_name}.#{column.column_name}"
686
+ resolve_column(reference, tables)
687
+ end
688
+
689
+ # The encrypted columns a statement reads. Always lenient: a column whose configuration
690
+ # cannot be read is simply not decrypted.
691
+ #
692
+ # @return [Hash{String => ColumnEncryptionConfig}] by column name
693
+ def column_configs(sql)
694
+ return {} unless ready_for_statement?(sql)
695
+
696
+ annotations = Utils::Parser::EncryptionAnnotationParser.parse_annotations(sql)
697
+ tables = statement_tables(analysis_of(stripped_sql(sql)), annotations)
698
+ return {} if tables.empty?
699
+
700
+ # When two of the statement's tables encrypt a column of the same name, the first of them
701
+ # wins: without the column's table qualifier in the row there is nothing better to go on.
702
+ tables.each_with_object({}) do |table, columns|
703
+ encrypted_columns_of(table).each do |config|
704
+ next unless usable?(config)
705
+
706
+ columns[config.column_name] ||= config
707
+ end
708
+ end
709
+ end
710
+
711
+ # The tables a statement could be encrypting a column of. An annotation's table qualifier is
712
+ # included as well, since it may name a table the parser did not report.
713
+ #
714
+ # @param analysis [Utils::Parser::SqlParser::SqlAnalysisResult]
715
+ # @return [Array<String>]
716
+ def statement_tables(analysis, annotations)
717
+ tables = analysis.affected_tables.to_a
718
+ annotations.each_value do |reference|
719
+ table = reference.to_s.rpartition('.').first
720
+ next if table.empty?
721
+
722
+ table = table.split('.').last
723
+ tables << table unless tables.include?(table)
724
+ end
725
+
726
+ tables
727
+ end
728
+
729
+ # @param reference [String] either +"column"+ or +"table.column"+
730
+ # @param tables [Array<String>] the tables the statement touches, tried in order
731
+ # @return [ColumnEncryptionConfig, nil] nil when the column is not encrypted
732
+ def resolve_column(reference, tables)
733
+ table, _, column = reference.to_s.rpartition('.')
734
+ return nil if column.empty?
735
+
736
+ # What qualifies a column is tried as a table first, and the statement's own tables after
737
+ # it, because the qualifier may be an alias: +/*@encrypt:u.ssn*/+ on a statement that says
738
+ # +UPDATE users u+ names a real column of a real table, and looking only for a table called
739
+ # +u+ would find nothing and leave the value unencrypted.
740
+ candidates = table.empty? ? tables : [table.split('.').last, *tables].uniq
741
+ candidates.each do |candidate|
742
+ config = column_config(candidate, column)
743
+ return config if config
744
+ end
745
+
746
+ nil
747
+ end
748
+
749
+ def column_config(table, column)
750
+ return nil if unreadable_name?("#{table}.#{column}")
751
+
752
+ config = metadata_lookup("#{table}.#{column}") { |manager| manager.column_config(table, column) }
753
+ usable?(config) ? config : nil
754
+ end
755
+
756
+ def encrypted_columns_of(table)
757
+ return [] if unreadable_name?(table)
758
+
759
+ metadata_lookup(table) { |manager| manager.table_configs(table) } || []
760
+ end
761
+
762
+ # A name with a character the SQL's encoding had no UTF-8 form for cannot be matched against the
763
+ # configuration reliably: looking it up would miss, and the column would be treated as though it
764
+ # were not encrypted without anyone knowing. So it is not looked up. The column is left as the
765
+ # database holds it, which is where the required server-side enforcement stops a plaintext being
766
+ # stored, and a warning says why, once for each name, since it only happens when the connection's
767
+ # encoding is one Ruby cannot fully read.
768
+ #
769
+ # @param name [String] a table, or a +"table.column"+ reference
770
+ def unreadable_name?(name)
771
+ return false unless name.include?(UNREADABLE_CHARACTER)
772
+ return true unless @unreadable_names.add?(name)
773
+
774
+ logger.warn(
775
+ "The kms_encryption plugin cannot read the name #{name} in the connection's encoding, so it cannot " \
776
+ 'tell whether it is encrypted; leaving it to the database. Use a UTF-8 connection, or ASCII names ' \
777
+ 'for encrypted tables and columns.'
778
+ )
779
+ true
780
+ end
781
+
782
+ # A lookup that fails is never allowed to take the application's statement down with it: the
783
+ # column is left as the database holds it, which is what the application would have got without
784
+ # the plugin, and the required server-side enforcement is what stops a plaintext being stored.
785
+ #
786
+ # @param described [String] what was being looked up, named in the log message
787
+ def metadata_lookup(described)
788
+ manager = metadata_manager
789
+ return nil if manager.nil?
790
+
791
+ yield(manager)
792
+ rescue Errors::MetadataError => e
793
+ logger.warn("Could not read the kms_encryption configuration of #{described}: #{e.message}")
794
+ nil
795
+ end
796
+
797
+ def usable?(config)
798
+ return false if config.nil?
799
+ return true if config.usable?
800
+
801
+ logger.warn("Skipping #{config.column_identifier}: its kms_encryption configuration is incomplete")
802
+ false
803
+ end
804
+
805
+ # A column the plugin confirmed is encrypted but that this statement writes with something
806
+ # other than a bind parameter. The value cannot be encrypted, so the write is refused rather
807
+ # than storing a plaintext in a column configured to be encrypted - the one write-side case the
808
+ # plugin fails closed on, since it has positively identified the problem.
809
+ #
810
+ # @param column [Utils::Parser::ColumnInfo] the confirmed-encrypted column written unencryptably
811
+ # @param copy [Boolean] whether the statement is a COPY, which no annotation can rescue
812
+ def unencryptable_write_error(column, copy:)
813
+ advice = if copy
814
+ 'a COPY sends its rows to the server as a stream rather than as bind parameters, which ' \
815
+ 'cannot be encrypted. Write the rows with INSERT and bind the values instead.'
816
+ else
817
+ 'this statement writes it with something other than a bind parameter, which cannot be ' \
818
+ 'encrypted. Bind the value, or name the parameter it belongs to with an ' \
819
+ '/*@encrypt:table.column*/ annotation.'
820
+ end
821
+ Errors::MetadataError
822
+ .validation_failed("#{column.column_name} is configured for kms_encryption, but #{advice}")
823
+ .with_table(column.table_name)
824
+ .with_column(column.column_name)
825
+ end
826
+
827
+ # @param table [String] a table of the statement that has encrypted columns
828
+ # @return [String] the warning logged when a write's columns cannot be enumerated
829
+ def unreadable_columns_warning(table)
830
+ "#{table} has columns configured for kms_encryption and which of them this statement writes could " \
831
+ 'not be established, so the plugin cannot encrypt them; relying on the database to reject a ' \
832
+ 'plaintext. Name the columns the statement writes, or annotate the parameters, to have the plugin ' \
833
+ 'encrypt them.'
834
+ end
835
+
836
+ def stripped_sql(sql)
837
+ Utils::Parser::EncryptionAnnotationParser.strip_annotations(sql)
838
+ end
839
+
840
+ # -- Lazily built collaborators --
841
+
842
+ # Builds the parts of the plugin that need a database connection, the first time a statement
843
+ # could touch an encrypted column.
844
+ #
845
+ # @return [Boolean] whether the plugin is ready to encrypt and decrypt
846
+ def ready_for_statement?(sql)
847
+ return false if sql.nil? || sql.to_s.strip.empty?
848
+
849
+ error = readiness_error
850
+ return true if error.nil?
851
+
852
+ # The application's statement is never the place to report that the kms_encryption tables
853
+ # cannot be read: every column stays as the database holds it until they can, and the
854
+ # required server-side enforcement is what stops a plaintext being stored meanwhile.
855
+ logger.warn("The kms_encryption plugin is not ready, leaving columns as they are: #{error.message}")
856
+ false
857
+ end
858
+
859
+ # @return [Errors::MetadataError, nil] nil once the plugin is ready to encrypt and decrypt
860
+ def readiness_error
861
+ @encryption_utility.ensure_initialized
862
+ return nil unless @encryption_utility.metadata_manager.nil?
863
+
864
+ Errors::MetadataError.load_failed('The kms_encryption metadata manager could not be built')
865
+ rescue Errors::MetadataError => e
866
+ e
867
+ rescue StandardError => e
868
+ Errors::MetadataError.load_failed("The kms_encryption plugin is not ready: #{e.message}")
869
+ end
870
+
871
+ def new_cipher
872
+ Encryption::ColumnCipher.new(key_manager: @encryption_utility.key_manager, sql_runner: sql_runner)
873
+ end
874
+
875
+ def metadata_manager
876
+ @encryption_utility.metadata_manager
877
+ end
878
+
879
+ def audit_logger
880
+ @encryption_utility.audit_logger
881
+ end
882
+
883
+ def sql_runner
884
+ @encryption_utility.sql_runner
885
+ end
886
+
887
+ attr_reader :sql_parser
888
+ end
889
+ end
890
+ end