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,435 @@
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 '../../driver_dialects/mysql_driver_dialect'
18
+ require_relative '../../driver_dialects/pg_driver_dialect'
19
+ require_relative '../../logging'
20
+ require_relative '../../property_definition'
21
+ require_relative 'connection_source'
22
+ require_relative 'data_key_cache'
23
+ require_relative 'encryption_algorithm'
24
+ require_relative 'encryption_service'
25
+ require_relative 'errors'
26
+ require_relative 'key_manager'
27
+ require_relative 'key_metadata'
28
+ require_relative 'metadata_manager'
29
+ require_relative 'sanitizer'
30
+ require_relative 'sql_runner'
31
+
32
+ module AwsAdvancedRubyDriverWrapper
33
+ module Plugins
34
+ module Encryption
35
+ # The administrative side of the plugin: creates master keys, turns kms_encryption on or off for a
36
+ # column, rotates data keys, and reports which columns a key is used by.
37
+ #
38
+ # None of this runs during normal query execution. It is meant to be called once, from a
39
+ # migration or a setup script, by whoever administers the kms_encryption configuration. Build
40
+ # it from a plain database connection you already have:
41
+ #
42
+ # kms = Aws::KMS::Client.new(region: 'us-east-1')
43
+ # config = EncryptionConfig.from_props(encryption_kms_region: 'us-east-1',
44
+ # encryption_metadata_schema: 'encrypt')
45
+ # utility = KeyManagementUtility.new(connection: conn, kms_client: kms, config: config)
46
+ # arn = utility.create_master_key('application column kms_encryption')
47
+ # utility.initialize_encryption_for_column('users', 'ssn', arn)
48
+ #
49
+ # Every operation uses the connection you pass, and that connection is never closed here - its
50
+ # lifecycle stays yours.
51
+ #
52
+ # Unlike the plugin, the utility does not require +encryption_allowed_master_key_arns+, so a new
53
+ # master key can be created and put to use in the same script. When the allow-list is configured
54
+ # it is enforced here too, so data keys can only be generated or decrypted under listed master
55
+ # keys. Without it, {#rotate_data_key} must be told which master key to rotate onto, since
56
+ # keeping the current one would mean trusting the ARN recorded in +key_storage+. Either way,
57
+ # add every master key a column uses to the application's allow-list, or the plugin will
58
+ # refuse to read or write that column.
59
+ #
60
+ # Rotating a data key only changes the key that new writes use. Values already written with
61
+ # the previous key stay readable, because each stored value records the id of the key it was
62
+ # written with and the read path resolves that key from +key_storage+ (which keeps the old
63
+ # key). Re-encrypting old values under the new key is optional, and is the application's job;
64
+ # until it is done, retiring the old key from +key_storage+ would make them unreadable.
65
+ class KeyManagementUtility
66
+ include Logging
67
+ include ConnectionSource
68
+
69
+ KEY_SPEC = 'AES_256'
70
+ ALIAS_PREFIX = 'alias/ruby-kms_encryption-'
71
+
72
+ # Builds a utility that runs every operation on the connection you supply, assembling the
73
+ # KeyManager, MetadataManager, and SqlRunner it needs internally. This is purely a user-facing
74
+ # administrative tool, so it takes a plain connection and never closes that connection; its
75
+ # lifecycle stays yours.
76
+ #
77
+ # @param connection [Object] a pg or mysql2 connection, used for every operation
78
+ # @param kms_client [Aws::KMS::Client]
79
+ # @param config [EncryptionConfig]
80
+ # @param driver [Symbol, nil] +:postgresql+ or +:mysql2+; inferred from +connection+ when nil
81
+ # @param audit_logger [AuditLogger, nil]
82
+ # @raise [ArgumentError] if a required argument is missing or the driver cannot be determined
83
+ def initialize(connection:, kms_client:, config:, driver: nil, audit_logger: nil)
84
+ raise ArgumentError, 'connection is required' if connection.nil?
85
+ raise ArgumentError, 'kms_client is required' if kms_client.nil?
86
+ raise ArgumentError, 'config is required' if config.nil?
87
+
88
+ dialect = driver.nil? ? dialect_for_connection(connection) : dialect_for_driver(driver)
89
+ @sql = SqlRunner.new(dialect)
90
+ data_key_cache = DataKeyCache.new(
91
+ max_size: config.data_key_cache_max_size,
92
+ ttl_sec: config.data_key_cache_expiration_sec,
93
+ enabled: config.data_key_cache_enabled
94
+ )
95
+ @key_manager = KeyManager.new(kms_client: kms_client, connection: connection, sql_runner: @sql,
96
+ config: config, data_key_cache: data_key_cache, audit_logger: audit_logger)
97
+ @metadata_manager = MetadataManager.new(connection: connection, sql_runner: @sql,
98
+ config: config, audit_logger: audit_logger)
99
+ use_connection_source(connection: connection, service_container: nil)
100
+ @kms_client = kms_client
101
+ @config = config
102
+ @audit_logger = audit_logger
103
+ end
104
+
105
+ # Creates a KMS master key for column kms_encryption and gives it an alias.
106
+ #
107
+ # @param description [String] the key description
108
+ # @param key_policy [String, nil] a key policy document; KMS applies its default when omitted
109
+ # @param create_alias [Boolean] whether to also create an alias for the new key
110
+ # @return [String] the ARN of the new master key
111
+ # @raise [Errors::KeyManagementError] if the key cannot be created
112
+ def create_master_key(description, key_policy: nil, create_alias: true)
113
+ raise ArgumentError, 'description is required' if description.nil?
114
+
115
+ logger.info("Creating a KMS master key: #{Sanitizer.description(description)}")
116
+
117
+ request = { description: description, key_usage: 'ENCRYPT_DECRYPT', key_spec: 'SYMMETRIC_DEFAULT' }
118
+ request[:policy] = key_policy unless key_policy.nil? || key_policy.strip.empty?
119
+
120
+ arn = @kms_client.create_key(**request).key_metadata.arn
121
+ add_alias(arn) if create_alias
122
+
123
+ @audit_logger&.log_key_creation(master_key_arn: arn, description: description, success: true)
124
+ arn
125
+ rescue StandardError => e
126
+ raise e if e.is_a?(ArgumentError)
127
+
128
+ @audit_logger&.log_key_creation(master_key_arn: nil, description: description, success: false,
129
+ error_message: e.message)
130
+ raise Errors::KeyManagementError.key_creation_failed("Failed to create the master key: #{e.message}")
131
+ end
132
+
133
+ # Sets up kms_encryption for a column that is not encrypted yet: generates a data key, stores it,
134
+ # and records the column in +encryption_metadata+.
135
+ #
136
+ # @param table_name [String]
137
+ # @param column_name [String]
138
+ # @param master_key_arn [String]
139
+ # @param algorithm [String] one of {EncryptionAlgorithm::ALL}
140
+ # @return [Integer] the +key_storage.id+ of the new data key
141
+ # @raise [Errors::KeyManagementError] if the column is already encrypted or the setup fails
142
+ def initialize_encryption_for_column(table_name, column_name, master_key_arn,
143
+ algorithm = EncryptionAlgorithm::DEFAULT)
144
+ logger.info("Initializing kms_encryption for #{table_name}.#{column_name}")
145
+
146
+ begin
147
+ already_encrypted = @metadata_manager.column_encrypted?(table_name, column_name)
148
+ rescue Errors::MetadataError => e
149
+ raise Errors::KeyManagementError
150
+ .key_creation_failed("Failed to check the kms_encryption status of the column: #{e.message}")
151
+ .with_context(:table, table_name)
152
+ .with_context(:column, column_name)
153
+ end
154
+
155
+ if already_encrypted
156
+ raise Errors::KeyManagementError
157
+ .key_creation_failed("Column #{table_name}.#{column_name} is already encrypted")
158
+ .with_context(:table, table_name)
159
+ .with_context(:column, column_name)
160
+ end
161
+
162
+ generate_and_store_data_key(table_name, column_name, master_key_arn, algorithm)
163
+ end
164
+
165
+ # Generates a data key for a column and records it, replacing any configuration the column
166
+ # already has.
167
+ #
168
+ # @param table_name [String]
169
+ # @param column_name [String]
170
+ # @param master_key_arn [String]
171
+ # @param algorithm [String]
172
+ # @return [Integer] the +key_storage.id+ of the new data key
173
+ # @raise [Errors::KeyManagementError] if the key cannot be generated or stored
174
+ def generate_and_store_data_key(table_name, column_name, master_key_arn,
175
+ algorithm = EncryptionAlgorithm::DEFAULT)
176
+ raise ArgumentError, 'table_name is required' if table_name.nil?
177
+ raise ArgumentError, 'column_name is required' if column_name.nil?
178
+ raise ArgumentError, 'master_key_arn is required' if master_key_arn.nil?
179
+
180
+ algorithm = EncryptionAlgorithm::DEFAULT if algorithm.nil? || algorithm.to_s.strip.empty?
181
+ raise EncryptionAlgorithm.unsupported(algorithm) unless EncryptionAlgorithm.supported?(algorithm)
182
+
183
+ stored = store_new_data_key(table_name, column_name, master_key_arn)
184
+ store_encryption_metadata(table_name, column_name, algorithm, stored.id)
185
+ @metadata_manager.refresh if @config.metadata_cache_enabled
186
+
187
+ logger.info("Stored a data key for #{table_name}.#{column_name} as key_storage id #{stored.id}")
188
+ stored.id
189
+ rescue StandardError => e
190
+ raise e if e.is_a?(ArgumentError) || e.is_a?(Errors::EncryptionPluginError)
191
+
192
+ raise Errors::KeyManagementError
193
+ .key_creation_failed("Failed to generate and store the data key: #{e.message}")
194
+ .with_context(:table, table_name)
195
+ .with_context(:column, column_name)
196
+ end
197
+
198
+ # Rotates the data key of an already encrypted column. New writes use the new key; values
199
+ # written with the previous key remain readable.
200
+ #
201
+ # Keeping the current master key means reusing the ARN recorded in +key_storage+, so it is
202
+ # only allowed when +encryption_allowed_master_key_arns+ is configured to check that ARN
203
+ # against. Without the allow-list, a row repointed at another key would have the new data key
204
+ # generated under whatever key it names.
205
+ #
206
+ # @param table_name [String]
207
+ # @param column_name [String]
208
+ # @param new_master_key_arn [String, nil] the master key to wrap the new data key with, or nil
209
+ # to keep the current one (requires the allow-list)
210
+ # @return [Integer] the +key_storage.id+ of the new data key
211
+ # @raise [ArgumentError] if no master key is given and no allow-list is configured
212
+ # @raise [Errors::KeyManagementError] if the column is not encrypted, the master key is not
213
+ # allowed, or the rotation fails
214
+ def rotate_data_key(table_name, column_name, new_master_key_arn = nil)
215
+ raise ArgumentError, 'table_name is required' if table_name.nil?
216
+ raise ArgumentError, 'column_name is required' if column_name.nil?
217
+
218
+ if new_master_key_arn.nil? && !@config.restricts_master_keys?
219
+ raise ArgumentError,
220
+ 'Pass the master key ARN to rotate onto, or configure ' \
221
+ "#{PropertyDefinition::ENCRYPTION_ALLOWED_MASTER_KEY_ARNS.name} so the current one can be checked"
222
+ end
223
+
224
+ logger.info("Rotating the data key for #{table_name}.#{column_name}")
225
+
226
+ current = @metadata_manager.column_config(table_name, column_name)
227
+ if current.nil?
228
+ raise Errors::KeyManagementError
229
+ .key_creation_failed("No kms_encryption configuration exists for #{table_name}.#{column_name}")
230
+ .with_context(:table, table_name)
231
+ .with_context(:column, column_name)
232
+ end
233
+
234
+ master_key_arn = new_master_key_arn || current.key_metadata&.master_key_arn
235
+ stored = store_new_data_key(table_name, column_name, master_key_arn)
236
+ update_encryption_metadata_key(table_name, column_name, stored.id)
237
+ @metadata_manager.refresh if @config.metadata_cache_enabled
238
+
239
+ logger.info(
240
+ "Rotated the data key for #{table_name}.#{column_name} from key #{current.key_id} to #{stored.id}"
241
+ )
242
+ stored.id
243
+ rescue StandardError => e
244
+ raise e if e.is_a?(ArgumentError) || e.is_a?(Errors::EncryptionPluginError)
245
+
246
+ raise Errors::KeyManagementError
247
+ .key_creation_failed("Failed to rotate the data key: #{e.message}")
248
+ .with_context(:table, table_name)
249
+ .with_context(:column, column_name)
250
+ end
251
+
252
+ # Stops encrypting a column by deleting its row from +encryption_metadata+.
253
+ #
254
+ # The key itself is deliberately left in +key_storage+, so that values already written can
255
+ # still be decrypted.
256
+ #
257
+ # @param table_name [String]
258
+ # @param column_name [String]
259
+ # @return [Boolean] true when a configuration row was removed
260
+ # @raise [Errors::KeyManagementError] if the row cannot be removed
261
+ def remove_encryption_for_column(table_name, column_name)
262
+ raise ArgumentError, 'table_name is required' if table_name.nil?
263
+ raise ArgumentError, 'column_name is required' if column_name.nil?
264
+
265
+ logger.info("Removing the kms_encryption configuration for #{table_name}.#{column_name}")
266
+
267
+ affected = with_connection(operation: 'DELETE_ENCRYPTION_METADATA') do |connection|
268
+ @sql.update(connection, delete_encryption_metadata_sql, [table_name, column_name])
269
+ end
270
+
271
+ if affected.zero?
272
+ logger.warn("No kms_encryption configuration existed for #{table_name}.#{column_name}")
273
+ else
274
+ logger.info("Removed the kms_encryption configuration for #{table_name}.#{column_name}")
275
+ end
276
+
277
+ @metadata_manager.refresh if @config.metadata_cache_enabled
278
+ @audit_logger&.log_metadata_operation(operation: 'remove', table_name: table_name, column_name: column_name)
279
+ affected.positive?
280
+ rescue StandardError => e
281
+ raise e if e.is_a?(ArgumentError)
282
+
283
+ raise Errors::KeyManagementError
284
+ .key_storage_failed("Failed to remove the kms_encryption configuration: #{e.message}")
285
+ .with_context(:table, table_name)
286
+ .with_context(:column, column_name)
287
+ end
288
+
289
+ # Lists the columns that a stored key is used by, so that the effect of rotating or
290
+ # retiring it can be seen up front.
291
+ #
292
+ # @param key_id [Integer] a +key_storage.id+ value
293
+ # @return [Array<String>] +"table.column"+ identifiers
294
+ # @raise [Errors::KeyManagementError] if the query fails
295
+ def columns_using_key(key_id)
296
+ raise ArgumentError, 'key_id is required' if key_id.nil?
297
+
298
+ rows = with_connection(operation: 'SELECT_COLUMNS_USING_KEY') do |connection|
299
+ @sql.query(connection, select_columns_with_key_sql, [key_id])
300
+ end
301
+
302
+ rows.map { |row| "#{row['table_name']}.#{row['column_name']}" }
303
+ rescue StandardError => e
304
+ raise e if e.is_a?(ArgumentError)
305
+
306
+ raise Errors::KeyManagementError
307
+ .key_retrieval_failed("Failed to find the columns using the key: #{e.message}")
308
+ .with_key_id(key_id.to_s)
309
+ end
310
+
311
+ # @param master_key_arn [String]
312
+ # @return [Boolean] whether the master key exists, is enabled, and can encrypt and decrypt
313
+ def validate_master_key(master_key_arn)
314
+ raise ArgumentError, 'master_key_arn is required' if master_key_arn.nil?
315
+
316
+ @key_manager.validate_master_key(master_key_arn)
317
+ end
318
+
319
+ private
320
+
321
+ # Picks a driver dialect from a connection's class without loading the driver gems. Both a raw
322
+ # driver connection (PG::Connection / Mysql2::Client) and a wrapper connection
323
+ # (WrapperPgConnection / WrapperMysql2Client) are recognized.
324
+ def dialect_for_connection(connection)
325
+ name = connection.class.name.to_s
326
+ return DriverDialects::PgDriverDialect.new if name.start_with?('PG::') || name.end_with?('WrapperPgConnection')
327
+ return DriverDialects::MysqlDriverDialect.new if name.start_with?('Mysql2::') || name.end_with?('WrapperMysql2Client')
328
+
329
+ raise ArgumentError,
330
+ "Cannot infer the driver dialect from #{name.empty? ? connection.class : name}; " \
331
+ 'pass driver: :postgresql or :mysql2'
332
+ end
333
+
334
+ def dialect_for_driver(driver)
335
+ case driver.to_sym
336
+ when :postgresql, :pg then DriverDialects::PgDriverDialect.new
337
+ when :mysql2, :mysql then DriverDialects::MysqlDriverDialect.new
338
+ else raise ArgumentError, "Unknown driver #{driver.inspect}; use :postgresql or :mysql2"
339
+ end
340
+ end
341
+
342
+ # Generates a data key through KMS and writes it to +key_storage+. The plaintext key is
343
+ # wiped again immediately: nothing here needs to encrypt with it.
344
+ #
345
+ # @return [KeyMetadata] the stored data key, with its +id+ filled in
346
+ def store_new_data_key(table_name, column_name, master_key_arn)
347
+ generated = @key_manager.generate_data_key(master_key_arn)
348
+
349
+ begin
350
+ @key_manager.store_key_metadata(
351
+ KeyMetadata.new(
352
+ key_name: key_name_for(table_name, column_name),
353
+ master_key_arn: master_key_arn,
354
+ encrypted_data_key: generated.encrypted_data_key,
355
+ hmac_key: generated.hmac_key,
356
+ key_spec: KEY_SPEC
357
+ )
358
+ )
359
+ ensure
360
+ EncryptionService.wipe(generated.plaintext)
361
+ end
362
+ end
363
+
364
+ def key_name_for(table_name, column_name)
365
+ "key-#{table_name}-#{column_name}-#{(Time.now.to_f * 1000).to_i}"
366
+ end
367
+
368
+ def add_alias(key_arn)
369
+ @kms_client.create_alias(
370
+ alias_name: "#{ALIAS_PREFIX}#{(Time.now.to_f * 1000).to_i}",
371
+ target_key_id: key_arn
372
+ )
373
+ rescue StandardError => e
374
+ # An alias is a convenience, not a requirement: the key is usable by ARN either way.
375
+ logger.warn("Created the master key but could not create an alias for it: #{e.message}")
376
+ end
377
+
378
+ def store_encryption_metadata(table_name, column_name, algorithm, key_id)
379
+ now = Time.now
380
+ with_connection(operation: 'STORE_ENCRYPTION_METADATA') do |connection|
381
+ @sql.update(connection, insert_encryption_metadata_sql,
382
+ [table_name, column_name, algorithm, key_id, now, now])
383
+ end
384
+
385
+ @audit_logger&.log_metadata_operation(operation: 'store', table_name: table_name, column_name: column_name)
386
+ nil
387
+ end
388
+
389
+ def update_encryption_metadata_key(table_name, column_name, key_id)
390
+ affected = with_connection(operation: 'UPDATE_ENCRYPTION_METADATA') do |connection|
391
+ @sql.update(connection, update_encryption_metadata_key_sql, [key_id, Time.now, table_name, column_name])
392
+ end
393
+
394
+ if affected.zero?
395
+ raise Errors::KeyManagementError
396
+ .key_storage_failed("No kms_encryption configuration row was updated for #{table_name}.#{column_name}")
397
+ .with_context(:table, table_name)
398
+ .with_context(:column, column_name)
399
+ end
400
+
401
+ @audit_logger&.log_metadata_operation(operation: 'update', table_name: table_name, column_name: column_name)
402
+ nil
403
+ end
404
+
405
+ # Upserting keeps this idempotent, so re-running a setup script does not fail on a column
406
+ # that is already configured. The driver dialect supplies its own upsert grammar.
407
+ def insert_encryption_metadata_sql
408
+ base = "INSERT INTO #{@config.metadata_schema}.encryption_metadata " \
409
+ '(table_name, column_name, encryption_algorithm, key_id, created_at, updated_at) ' \
410
+ 'VALUES (?, ?, ?, ?, ?, ?)'
411
+ clause = @sql.upsert_clause(%w[table_name column_name], %w[encryption_algorithm key_id updated_at])
412
+ "#{base} #{clause}"
413
+ end
414
+
415
+ def update_encryption_metadata_key_sql
416
+ "UPDATE #{@config.metadata_schema}.encryption_metadata SET key_id = ?, updated_at = ? " \
417
+ "WHERE table_name #{eq} ? AND column_name #{eq} ?"
418
+ end
419
+
420
+ def select_columns_with_key_sql
421
+ "SELECT table_name, column_name FROM #{@config.metadata_schema}.encryption_metadata WHERE key_id #{eq} ?"
422
+ end
423
+
424
+ def delete_encryption_metadata_sql
425
+ "DELETE FROM #{@config.metadata_schema}.encryption_metadata " \
426
+ "WHERE table_name #{eq} ? AND column_name #{eq} ?"
427
+ end
428
+
429
+ def eq
430
+ @sql.equals_operator
431
+ end
432
+ end
433
+ end
434
+ end
435
+ end