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,281 @@
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_relative '../../logging'
18
+ require_relative '../../property_definition'
19
+ require_relative 'audit_logger'
20
+ require_relative 'data_key_cache'
21
+ require_relative 'encryption_config'
22
+ require_relative 'errors'
23
+ require_relative 'key_manager'
24
+ require_relative 'metadata_manager'
25
+ require_relative 'schema_validator'
26
+ require_relative 'sql_runner'
27
+
28
+ module AwsAdvancedRubyDriverWrapper
29
+ module Plugins
30
+ module Encryption
31
+ # Wires up everything the kms_encryption plugin needs and owns its lifecycle.
32
+ #
33
+ # Construction is split in two, because the two halves become available at different times:
34
+ #
35
+ # * Everything that only needs properties (the configuration, the audit logger, the data key
36
+ # cache, the KMS client) is built up front, so that a misconfiguration is reported when the
37
+ # connection is opened rather than in the middle of a query.
38
+ # * Everything that needs to talk to the database (the metadata manager and the key manager,
39
+ # both of which read the +encryption_metadata+ and +key_storage+ tables) is built the first
40
+ # time a statement could touch an encrypted column, through {#ensure_initialized}. At that
41
+ # point the connect pipeline has finished, so the plugin can open its own connections.
42
+ #
43
+ # The KMS client is created lazily as well, so that an application that never touches an
44
+ # encrypted column never has to reach KMS.
45
+ class KmsEncryptionUtility
46
+ include Logging
47
+
48
+ PLUGIN_NAME = 'KmsEncryptionPlugin'
49
+
50
+ # Logged at most once per process, so disabling the metadata cache does not warn on every
51
+ # connection.
52
+ @metadata_cache_warning_logged = false
53
+ class << self
54
+ attr_accessor :metadata_cache_warning_logged
55
+ end
56
+
57
+ attr_reader :config, :audit_logger, :data_key_cache
58
+
59
+ # @param service_container [Services::ServiceContainer]
60
+ # @param props [Concurrent::Map, Hash] the wrapper properties
61
+ # @param kms_client [Aws::KMS::Client, nil] a client to use instead of building one
62
+ # @raise [ArgumentError] if the kms_encryption properties are invalid, or no master keys are allowed
63
+ def initialize(service_container, props, kms_client: nil)
64
+ raise ArgumentError, 'service_container is required' if service_container.nil?
65
+
66
+ @service_container = service_container
67
+ @props = props
68
+ @config = EncryptionConfig.from_props(props)
69
+ require_master_key_allow_list!
70
+ @kms_client = kms_client
71
+ @lock = Mutex.new
72
+ @initialized = false
73
+ @closed = false
74
+
75
+ @audit_logger = AuditLogger.new(@config.audit_logging_enabled)
76
+ @data_key_cache = DataKeyCache.new(
77
+ max_size: @config.data_key_cache_max_size,
78
+ ttl_sec: @config.data_key_cache_expiration_sec,
79
+ enabled: @config.data_key_cache_enabled
80
+ )
81
+
82
+ logger.debug do
83
+ "Loaded the kms_encryption configuration: region=#{@config.kms_region}, " \
84
+ "schema=#{@config.metadata_schema}, metadata cache=#{@config.metadata_cache_enabled}, " \
85
+ "max retries=#{@config.key_management_max_retries}"
86
+ end
87
+
88
+ warn_if_metadata_cache_disabled
89
+ end
90
+
91
+ # @return [String] the name the plugin is known by
92
+ def plugin_name
93
+ PLUGIN_NAME
94
+ end
95
+
96
+ # @return [Boolean] whether the database backed components have been built
97
+ def initialized?
98
+ @lock.synchronize { @initialized }
99
+ end
100
+
101
+ # @return [Boolean] whether {#cleanup} has run
102
+ def closed?
103
+ @lock.synchronize { @closed }
104
+ end
105
+
106
+ # Builds the components that need a database connection, the first time one is needed.
107
+ #
108
+ # @return [void]
109
+ # @raise [Errors::MetadataError] if the initial metadata load fails
110
+ # @raise [Errors::EncryptionPluginError] if the components cannot be built
111
+ def ensure_initialized
112
+ return if @lock.synchronize { @initialized || @closed }
113
+
114
+ @lock.synchronize do
115
+ next if @initialized || @closed
116
+
117
+ build_database_components
118
+ @initialized = true
119
+ end
120
+ nil
121
+ end
122
+
123
+ # @return [MetadataManager, nil] nil until {#ensure_initialized} has run
124
+ def metadata_manager
125
+ @lock.synchronize { @metadata_manager }
126
+ end
127
+
128
+ # @return [KeyManager, nil] nil until {#ensure_initialized} has run
129
+ def key_manager
130
+ @lock.synchronize { @key_manager }
131
+ end
132
+
133
+ # @return [SqlRunner, nil] nil until {#ensure_initialized} has run
134
+ def sql_runner
135
+ @lock.synchronize { @sql_runner }
136
+ end
137
+
138
+ # Checks that the +encryption_metadata+ and +key_storage+ tables look the way the plugin
139
+ # expects. Nothing calls this automatically: it is meant for setup scripts and diagnostics.
140
+ #
141
+ # @return [SchemaValidator::ValidationResult]
142
+ # @raise [Errors::EncryptionPluginError] if the components cannot be built
143
+ def validate_schema
144
+ ensure_initialized
145
+ validator, metadata_manager = @lock.synchronize { [@schema_validator, @metadata_manager] }
146
+
147
+ metadata_manager.with_connection(operation: 'VALIDATE_SCHEMA') { |connection| validator.validate(connection) }
148
+ end
149
+
150
+ # The KMS client, created on first use.
151
+ #
152
+ # @return [Aws::KMS::Client]
153
+ def kms_client
154
+ @lock.synchronize { @kms_client ||= create_kms_client }
155
+ end
156
+
157
+ # @return [String] a description of how metadata is being read
158
+ def connection_mode_status
159
+ if initialized?
160
+ 'The kms_encryption plugin is reading its metadata over its own short-lived connections'
161
+ else
162
+ 'The kms_encryption plugin has not opened a metadata connection yet'
163
+ end
164
+ end
165
+
166
+ # Logs the connection mode, for troubleshooting.
167
+ # @return [void]
168
+ def log_current_status
169
+ logger.info("#{PLUGIN_NAME} status report")
170
+ logger.info(connection_mode_status)
171
+ nil
172
+ end
173
+
174
+ # Releases everything the plugin holds: the background metadata refresh, the cached data
175
+ # keys, and the KMS client.
176
+ #
177
+ # @return [void]
178
+ def cleanup
179
+ return if @lock.synchronize { @closed }
180
+
181
+ logger.debug("Cleaning up #{PLUGIN_NAME}")
182
+
183
+ metadata_manager, data_key_cache, kms_client = @lock.synchronize do
184
+ @closed = true
185
+ @initialized = false
186
+ [@metadata_manager, @data_key_cache, @kms_client]
187
+ end
188
+
189
+ quietly('stop the metadata refresh') { metadata_manager&.shutdown }
190
+ quietly('clear the data key cache') { data_key_cache&.shutdown }
191
+ quietly('close the KMS client') { kms_client.close if kms_client.respond_to?(:close) }
192
+
193
+ logger.debug("Finished cleaning up #{PLUGIN_NAME}")
194
+ nil
195
+ end
196
+
197
+ private
198
+
199
+ def require_master_key_allow_list!
200
+ return if @config.restricts_master_keys?
201
+
202
+ raise ArgumentError,
203
+ "#{PropertyDefinition::ENCRYPTION_ALLOWED_MASTER_KEY_ARNS.name} is required: list the KMS master key " \
204
+ 'ARNs the kms_encryption plugin may use'
205
+ end
206
+
207
+ # Runs under @lock.
208
+ def build_database_components
209
+ @sql_runner = SqlRunner.new(@service_container.dialect_service.driver_dialect)
210
+ @audit_logger.log_connection_parameter_extraction(
211
+ strategy: 'ServiceContainer', connection_type: 'INDEPENDENT_CONNECTION'
212
+ )
213
+
214
+ @kms_client ||= create_kms_client
215
+ @key_manager = KeyManager.new(
216
+ kms_client: @kms_client,
217
+ service_container: @service_container,
218
+ sql_runner: @sql_runner,
219
+ config: @config,
220
+ data_key_cache: @data_key_cache,
221
+ audit_logger: @audit_logger
222
+ )
223
+ @metadata_manager = MetadataManager.new(
224
+ service_container: @service_container,
225
+ sql_runner: @sql_runner,
226
+ config: @config,
227
+ audit_logger: @audit_logger
228
+ )
229
+ @schema_validator = SchemaValidator.new(@config.metadata_schema, @sql_runner)
230
+
231
+ @metadata_manager.start
232
+ logger.debug('The kms_encryption plugin is ready to encrypt and decrypt column values')
233
+ end
234
+
235
+ # Warns once per process when the metadata cache is off, since that makes the plugin open a
236
+ # short-lived metadata connection for every statement that touches an encrypted column.
237
+ def warn_if_metadata_cache_disabled
238
+ return if @config.metadata_cache_enabled
239
+ return if self.class.metadata_cache_warning_logged
240
+
241
+ self.class.metadata_cache_warning_logged = true
242
+ logger.warn(
243
+ 'The kms_encryption metadata cache is disabled (encryption_metadata_cache_enabled=false): the ' \
244
+ 'plugin opens a short-lived metadata connection for every statement that touches an encrypted ' \
245
+ 'column. Enable it in production and lower encryption_metadata_cache_refresh_interval_sec if ' \
246
+ 'you need fresher metadata.'
247
+ )
248
+ end
249
+
250
+ def create_kms_client
251
+ ensure_sdk!
252
+ logger.debug { "Creating a KMS client for region #{@config.kms_region}" }
253
+
254
+ options = {
255
+ region: @config.kms_region,
256
+ credentials: PropertyDefinition::AWS_CREDENTIALS_PROVIDER.get(@props) ||
257
+ Aws::CredentialProviderChain.new.resolve
258
+ }
259
+ options[:endpoint] = @config.kms_endpoint unless @config.kms_endpoint.nil?
260
+
261
+ Aws::KMS::Client.new(**options)
262
+ end
263
+
264
+ def ensure_sdk!
265
+ require 'aws-sdk-kms'
266
+ rescue LoadError
267
+ raise LoadError,
268
+ "The KMS kms_encryption plugin requires 'aws-sdk-kms'. " \
269
+ "Add it to your Gemfile: gem 'aws-sdk-kms'"
270
+ end
271
+
272
+ # Cleanup must release everything it can, so one failing step cannot stop the others.
273
+ def quietly(description)
274
+ yield
275
+ rescue StandardError => e
276
+ logger.warn("Failed to #{description} while cleaning up the kms_encryption plugin: #{e.message}")
277
+ end
278
+ end
279
+ end
280
+ end
281
+ end
@@ -0,0 +1,332 @@
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_relative '../../logging'
18
+ require_relative '../../utils/conversion_utils'
19
+ require_relative '../../utils/sql_encoding'
20
+ require_relative 'column_encryption_config'
21
+ require_relative 'connection_source'
22
+ require_relative 'encryption_algorithm'
23
+ require_relative 'errors'
24
+ require_relative 'key_metadata'
25
+
26
+ module AwsAdvancedRubyDriverWrapper
27
+ module Plugins
28
+ module Encryption
29
+ # Reads the +encryption_metadata+ table, which says which columns are encrypted and with
30
+ # which key, and keeps the answers in a cache.
31
+ #
32
+ # The cache is loaded once at startup and then refreshed on a background thread every
33
+ # +encryption_metadata_cache_refresh_interval_sec+, so that a column can be added to or
34
+ # removed from the kms_encryption configuration without restarting the application. Lookups
35
+ # fall back to querying the database directly when caching is disabled or the cache has
36
+ # expired.
37
+ class MetadataManager
38
+ include Logging
39
+ include Utils::ConversionUtils
40
+ include ConnectionSource
41
+
42
+ # How long {shutdown} waits for the refresh thread to finish.
43
+ SHUTDOWN_TIMEOUT_SEC = 5
44
+
45
+ # Exactly one of +connection+ or +service_container+ must be given (see {ConnectionSource}).
46
+ #
47
+ # @param sql_runner [SqlRunner]
48
+ # @param config [EncryptionConfig]
49
+ # @param connection [Object, nil] a caller-owned connection used for every operation
50
+ # @param service_container [Services::ServiceContainer, nil] opens a short-lived connection per operation
51
+ # @param audit_logger [AuditLogger, nil]
52
+ def initialize(sql_runner:, config:, connection: nil, service_container: nil, audit_logger: nil)
53
+ use_connection_source(connection: connection, service_container: service_container)
54
+ @sql = sql_runner
55
+ @config = config
56
+ @audit_logger = audit_logger
57
+ @cache = {}
58
+ @lock = Mutex.new
59
+ @last_refresh_time = nil
60
+ @running = false
61
+ @refresh_thread = nil
62
+ end
63
+
64
+ # Loads the cache and starts the background refresh.
65
+ #
66
+ # @return [void]
67
+ # @raise [Errors::MetadataError] if the initial load fails
68
+ def start
69
+ logger.debug('Initializing the kms_encryption metadata manager')
70
+ refresh if @config.metadata_cache_enabled
71
+ start_refresh_thread if @config.background_refresh_enabled?
72
+ nil
73
+ end
74
+
75
+ # Reloads every column configuration from the database and replaces the cache.
76
+ #
77
+ # @return [Integer] the number of cached column configurations
78
+ # @raise [Errors::MetadataError] if the load fails
79
+ def refresh
80
+ metadata = load_metadata
81
+
82
+ @lock.synchronize do
83
+ @cache = metadata
84
+ @last_refresh_time = Time.now
85
+ end
86
+
87
+ logger.debug { "Refreshed the kms_encryption metadata cache with #{metadata.size} column configuration(s)" }
88
+ @audit_logger&.log_metadata_operation(operation: 'refresh', success: true)
89
+ metadata.size
90
+ rescue Errors::MetadataError => e
91
+ @audit_logger&.log_metadata_operation(operation: 'refresh', success: false, error_message: e.message)
92
+ raise
93
+ end
94
+
95
+ # Reads every column configuration from the database, without touching the cache.
96
+ #
97
+ # @return [Hash{String => ColumnEncryptionConfig}] keyed by +"table.column"+
98
+ # @raise [Errors::MetadataError] if the load fails
99
+ def load_metadata
100
+ rows = with_connection(operation: 'LOAD_ENCRYPTION_METADATA') do |connection|
101
+ @sql.query(connection, load_metadata_sql)
102
+ end
103
+
104
+ rows.each_with_object({}) do |row, metadata|
105
+ column_config = to_column_config(row)
106
+ metadata[column_config.column_identifier] = column_config
107
+ end
108
+ rescue StandardError => e
109
+ raise e if e.is_a?(Errors::MetadataError)
110
+
111
+ raise Errors::MetadataError.load_failed("Failed to load kms_encryption metadata: #{e.message}")
112
+ end
113
+
114
+ # @param table_name [String, nil]
115
+ # @param column_name [String, nil]
116
+ # @return [Boolean] whether the column is configured for kms_encryption
117
+ # @raise [Errors::MetadataError] if the lookup has to hit the database and that fails
118
+ def column_encrypted?(table_name, column_name)
119
+ return false if table_name.nil? || column_name.nil?
120
+
121
+ if cache_usable?
122
+ hit = @lock.synchronize { @cache.key?(column_identifier(table_name, column_name)) }
123
+ return hit
124
+ end
125
+
126
+ column_encrypted_in_database?(table_name, column_name)
127
+ end
128
+
129
+ # @param table_name [String, nil]
130
+ # @param column_name [String, nil]
131
+ # @return [ColumnEncryptionConfig, nil] the column's configuration, nil when not encrypted
132
+ # @raise [Errors::MetadataError] if the lookup has to hit the database and that fails
133
+ def column_config(table_name, column_name)
134
+ return nil if table_name.nil? || column_name.nil?
135
+
136
+ return @lock.synchronize { @cache[column_identifier(table_name, column_name)] } if cache_usable?
137
+
138
+ column_config_from_database(table_name, column_name)
139
+ end
140
+
141
+ # Every encrypted column of one table.
142
+ #
143
+ # This is what a read is planned from: the statement says which tables it touches, and the
144
+ # configuration says which of their columns will come back encrypted.
145
+ #
146
+ # @param table_name [String, nil]
147
+ # @return [Array<ColumnEncryptionConfig>] empty when no column of the table is encrypted
148
+ # @raise [Errors::MetadataError] if the lookup has to hit the database and that fails
149
+ def table_configs(table_name)
150
+ return [] if table_name.nil?
151
+
152
+ return @lock.synchronize { cached_table_configs(table_name) } if cache_usable?
153
+
154
+ table_configs_from_database(table_name)
155
+ end
156
+
157
+ # @return [Time, nil] when the cache was last refreshed, nil when it never was
158
+ def last_refresh_time
159
+ @lock.synchronize { @last_refresh_time }
160
+ end
161
+
162
+ # @return [Integer] the number of cached column configurations
163
+ def cache_size
164
+ @lock.synchronize { @cache.size }
165
+ end
166
+
167
+ # Stops the background refresh and empties the cache.
168
+ # @return [void]
169
+ def shutdown
170
+ @running = false
171
+ thread = @refresh_thread
172
+ if thread
173
+ thread.wakeup if thread.alive?
174
+ thread.join(SHUTDOWN_TIMEOUT_SEC)
175
+ @refresh_thread = nil
176
+ end
177
+
178
+ @lock.synchronize do
179
+ @cache = {}
180
+ @last_refresh_time = nil
181
+ end
182
+ nil
183
+ end
184
+
185
+ private
186
+
187
+ def column_identifier(table_name, column_name)
188
+ "#{table_name}.#{column_name}"
189
+ end
190
+
191
+ # The cache can only answer a lookup when it is enabled, has been loaded, and has not
192
+ # expired.
193
+ def cache_usable?
194
+ return false unless @config.metadata_cache_enabled
195
+
196
+ last_refresh = @lock.synchronize { @last_refresh_time }
197
+ !last_refresh.nil? && (Time.now - last_refresh) < @config.metadata_cache_expiration_sec
198
+ end
199
+
200
+ def column_encrypted_in_database?(table_name, column_name)
201
+ row = with_connection(operation: 'CHECK_COLUMN_ENCRYPTED') do |connection|
202
+ @sql.query(connection, check_column_encrypted_sql, [table_name, column_name]).first
203
+ end
204
+ !row.nil?
205
+ rescue StandardError => e
206
+ raise lookup_error(e, table_name, column_name)
207
+ end
208
+
209
+ def column_config_from_database(table_name, column_name)
210
+ row = with_connection(operation: 'GET_COLUMN_CONFIG') do |connection|
211
+ @sql.query(connection, column_config_sql, [table_name, column_name]).first
212
+ end
213
+ row && to_column_config(row)
214
+ rescue StandardError => e
215
+ raise lookup_error(e, table_name, column_name)
216
+ end
217
+
218
+ # Callers hold {@lock}.
219
+ def cached_table_configs(table_name)
220
+ @cache.each_value.select { |config| config.table_name == table_name }
221
+ end
222
+
223
+ def table_configs_from_database(table_name)
224
+ rows = with_connection(operation: 'GET_TABLE_CONFIGS') do |connection|
225
+ @sql.query(connection, table_configs_sql, [table_name])
226
+ end
227
+ rows.map { |row| to_column_config(row) }
228
+ rescue StandardError => e
229
+ raise lookup_error(e, table_name, nil)
230
+ end
231
+
232
+ def lookup_error(error, table_name, column_name)
233
+ return error if error.is_a?(Errors::MetadataError)
234
+
235
+ Errors::MetadataError
236
+ .lookup_failed("Failed to load the kms_encryption configuration: #{error.message}")
237
+ .with_table(table_name)
238
+ .with_column(column_name)
239
+ end
240
+
241
+ # Builds a {ColumnEncryptionConfig}, and the {KeyMetadata} it points at, from one joined row.
242
+ #
243
+ # The row names its table and column in the connection's encoding, while the names they are
244
+ # looked up by are read from the application's SQL as UTF-8. They are kept as UTF-8 so the two
245
+ # compare equal whatever the connection's encoding is.
246
+ def to_column_config(row)
247
+ key_metadata = KeyMetadata.new(
248
+ id: row['key_id']&.to_i,
249
+ key_id: row['key_uuid'],
250
+ key_name: row['name'],
251
+ master_key_arn: row['master_key_arn'],
252
+ encrypted_data_key: row['encrypted_data_key'],
253
+ hmac_key: @sql.read_binary(row['hmac_key']),
254
+ key_spec: row['key_spec'] || KeyMetadata::DEFAULT_KEY_SPEC,
255
+ created_at: row['key_created_at'] && to_time(row['key_created_at']),
256
+ last_used_at: row['last_used_at'] && to_time(row['last_used_at'])
257
+ )
258
+
259
+ ColumnEncryptionConfig.new(
260
+ table_name: Utils::SqlEncoding.inspectable(row['table_name']),
261
+ column_name: Utils::SqlEncoding.inspectable(row['column_name']),
262
+ algorithm: row['encryption_algorithm'] || EncryptionAlgorithm::DEFAULT,
263
+ key_id: row['key_id']&.to_i,
264
+ key_metadata: key_metadata,
265
+ created_at: row['created_at'] && to_time(row['created_at']),
266
+ updated_at: row['updated_at'] && to_time(row['updated_at'])
267
+ )
268
+ end
269
+
270
+ def load_metadata_sql
271
+ "#{joined_columns_sql} ORDER BY em.table_name, em.column_name"
272
+ end
273
+
274
+ def column_config_sql
275
+ "#{joined_columns_sql} WHERE em.table_name #{eq} ? AND em.column_name #{eq} ?"
276
+ end
277
+
278
+ def table_configs_sql
279
+ "#{joined_columns_sql} WHERE em.table_name #{eq} ?"
280
+ end
281
+
282
+ def check_column_encrypted_sql
283
+ "SELECT 1 FROM #{@config.metadata_schema}.encryption_metadata " \
284
+ "WHERE table_name #{eq} ? AND column_name #{eq} ?"
285
+ end
286
+
287
+ # +ks.key_id+ is aliased because +em.key_id+ already occupies that name in the row: the
288
+ # metadata table's key_id is the integer foreign key into key_storage.id, while the key
289
+ # storage table's own key_id is the external identifier of the key.
290
+ #
291
+ # The join is an outer one so that a column whose key row is missing is still reported as
292
+ # configured for kms_encryption. An inner join would drop it, the column would look like an
293
+ # ordinary one, and a write would store the plaintext. Kept this way, its key metadata comes
294
+ # back empty, which fails validation and so fails the write instead.
295
+ def joined_columns_sql
296
+ schema = @config.metadata_schema
297
+ 'SELECT em.table_name, em.column_name, em.encryption_algorithm, em.key_id, ' \
298
+ 'em.created_at, em.updated_at, ' \
299
+ 'ks.key_id AS key_uuid, ks.name, ks.master_key_arn, ks.encrypted_data_key, ks.hmac_key, ks.key_spec, ' \
300
+ 'ks.created_at AS key_created_at, ks.last_used_at ' \
301
+ "FROM #{schema}.encryption_metadata em " \
302
+ "LEFT JOIN #{schema}.key_storage ks ON em.key_id #{eq} ks.id"
303
+ end
304
+
305
+ def eq
306
+ @sql.equals_operator
307
+ end
308
+
309
+ def start_refresh_thread
310
+ @running = true
311
+ interval = @config.metadata_cache_refresh_interval_sec
312
+
313
+ thread = Thread.new do
314
+ while @running
315
+ sleep(interval)
316
+ break unless @running
317
+
318
+ begin
319
+ refresh
320
+ rescue StandardError => e
321
+ logger.warn("Failed to refresh the kms_encryption metadata cache: #{e.message}")
322
+ end
323
+ end
324
+ end
325
+ thread.name = 'kms_encryption-metadata-refresh'
326
+ @refresh_thread = thread
327
+ logger.debug { "Started the kms_encryption metadata refresh thread with a #{interval}s interval" }
328
+ end
329
+ end
330
+ end
331
+ end
332
+ end