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,147 @@
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
+ module AwsAdvancedRubyDriverWrapper
18
+ module Plugins
19
+ module Encryption
20
+ # Redaction helpers shared by the audit logger, the error context builder, and the
21
+ # kms_encryption error classes. Every method returns nil when given nil so that callers
22
+ # can decide how to render a missing value.
23
+ module Sanitizer
24
+ # Matches "password=...", "secret=...", "key=...", "token=..." assignments.
25
+ SENSITIVE_ASSIGNMENT_PATTERN = /(password|secret|key|token)=\S+/i
26
+ # Same as above, plus "credential=...", and stops at commas and closing braces so that
27
+ # it works on rendered hashes.
28
+ CREDENTIAL_ASSIGNMENT_PATTERN = /(password|secret|key|token|credential)=[^\s,}]+/i
29
+ # The region and account exclude '*' so that an ARN masked by {.arn} is not matched again,
30
+ # which would garble its partly masked key id. An unmasked ARN never contains a '*'.
31
+ KMS_ARN_PATTERN = %r{arn:aws:kms:[^:*]+:[^:*]+:key/[a-f0-9-]+}i
32
+ URL_PASSWORD_PATTERN = /[?&]password=[^&]*/i
33
+ URL_PWD_PATTERN = /[?&]pwd=[^&]*/i
34
+ # The password run excludes '/' and whitespace as well as '@', so it cannot scan past the
35
+ # start of the next authority; without that a string of "://x:" segments is quadratic to
36
+ # match. A URL's userinfo cannot contain an unencoded '/' anyway.
37
+ URL_USER_INFO_PATTERN = %r{://[^:/@\s]+:[^@/\s]+@}
38
+ SQL_STRING_LITERAL_PATTERN = /'[^']*'/
39
+ SQL_NUMERIC_LITERAL_PATTERN = /\b\d+\b/
40
+
41
+ MAX_NAME_LENGTH = 50
42
+ MAX_DESCRIPTION_LENGTH = 100
43
+ MAX_CONFIG_DETAILS_LENGTH = 150
44
+ MAX_ERROR_MESSAGE_LENGTH = 200
45
+ MAX_SQL_LENGTH = 100
46
+
47
+ module_function
48
+
49
+ # Masks the account and region of a KMS key ARN, and masks the middle of the trailing key id
50
+ # the same way {.key_id} does, so a key id is redacted consistently wherever it appears.
51
+ # @param value [String, nil]
52
+ # @return [String, nil]
53
+ def arn(value)
54
+ return nil if value.nil?
55
+
56
+ str = value.to_s
57
+ last_slash = str.rindex('/')
58
+ return 'arn:aws:kms:***:***:key/***' if last_slash.nil? || last_slash.zero? || last_slash == str.length - 1
59
+
60
+ "arn:aws:kms:***:***:key/#{key_id(str[(last_slash + 1)..])}"
61
+ end
62
+
63
+ # Keeps the first and last four characters of a key id, masking the middle.
64
+ # @param value [String, nil]
65
+ # @return [String, nil]
66
+ def key_id(value)
67
+ return nil if value.nil?
68
+
69
+ str = value.to_s
70
+ str.length > 8 ? "#{str[0, 4]}***#{str[-4..]}" : '***'
71
+ end
72
+
73
+ # @param value [String, nil]
74
+ # @return [String, nil]
75
+ def table_name(value)
76
+ truncate(value, MAX_NAME_LENGTH)
77
+ end
78
+
79
+ # @param value [String, nil]
80
+ # @return [String, nil]
81
+ def column_name(value)
82
+ truncate(value, MAX_NAME_LENGTH)
83
+ end
84
+
85
+ # @param value [String, nil]
86
+ # @return [String, nil]
87
+ def description(value)
88
+ return nil if value.nil?
89
+
90
+ truncate(value.gsub(SENSITIVE_ASSIGNMENT_PATTERN, '\1=***'), MAX_DESCRIPTION_LENGTH)
91
+ end
92
+
93
+ # @param value [String, nil]
94
+ # @return [String, nil]
95
+ def error_message(value)
96
+ return nil if value.nil?
97
+
98
+ masked = value.gsub(SENSITIVE_ASSIGNMENT_PATTERN, '\1=***')
99
+ .gsub(KMS_ARN_PATTERN, 'arn:aws:kms:***:***:key/***')
100
+ truncate(masked, MAX_ERROR_MESSAGE_LENGTH)
101
+ end
102
+
103
+ # @param value [String, nil]
104
+ # @return [String, nil]
105
+ def config_details(value)
106
+ return nil if value.nil?
107
+
108
+ masked = value.gsub(CREDENTIAL_ASSIGNMENT_PATTERN, '\1=***')
109
+ .gsub(KMS_ARN_PATTERN, 'arn:aws:kms:***:***:key/***')
110
+ truncate(masked, MAX_CONFIG_DETAILS_LENGTH)
111
+ end
112
+
113
+ # Masks credentials embedded in a connection URL, both as query parameters and as user info.
114
+ # @param value [String, nil]
115
+ # @return [String, nil]
116
+ def connection_url(value)
117
+ return nil if value.nil?
118
+
119
+ value.gsub(URL_PASSWORD_PATTERN, '?password=***')
120
+ .gsub(URL_PWD_PATTERN, '?pwd=***')
121
+ .gsub(URL_USER_INFO_PATTERN, '://***:***@')
122
+ end
123
+
124
+ # Replaces string and numeric literals so that SQL can be logged without leaking values.
125
+ # @param value [String, nil]
126
+ # @return [String, nil]
127
+ def sql(value)
128
+ return nil if value.nil?
129
+
130
+ masked = value.gsub(SQL_STRING_LITERAL_PATTERN, "'***'")
131
+ .gsub(SQL_NUMERIC_LITERAL_PATTERN, '***')
132
+ truncate(masked, MAX_SQL_LENGTH)
133
+ end
134
+
135
+ # @param value [String, nil]
136
+ # @param limit [Integer]
137
+ # @return [String, nil]
138
+ def truncate(value, limit)
139
+ return nil if value.nil?
140
+
141
+ str = value.to_s
142
+ str.length > limit ? "#{str[0, limit - 3]}..." : str
143
+ end
144
+ end
145
+ end
146
+ end
147
+ end
@@ -0,0 +1,70 @@
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
+ module AwsAdvancedRubyDriverWrapper
18
+ module Plugins
19
+ module Encryption
20
+ # A validated database schema name.
21
+ #
22
+ # Schema names cannot be bound as statement parameters, so the plugin has to
23
+ # interpolate them into the metadata queries. Wrapping the name in this type keeps
24
+ # that interpolation safe: only plain identifiers are accepted, which rules out
25
+ # quoting, whitespace, comments, and statement separators.
26
+ class SchemaName
27
+ VALID_PATTERN = /\A[a-zA-Z_][a-zA-Z0-9_]*\z/
28
+
29
+ attr_reader :value
30
+
31
+ # @param name [String, Symbol, SchemaName] the schema name to validate
32
+ # @return [SchemaName]
33
+ # @raise [ArgumentError] if the name is empty or is not a plain SQL identifier
34
+ def self.of(name)
35
+ name.is_a?(SchemaName) ? name : new(name)
36
+ end
37
+
38
+ # @param value [String, Symbol]
39
+ # @raise [ArgumentError] if the name is empty or is not a plain SQL identifier
40
+ def initialize(value)
41
+ name = value.to_s
42
+ raise ArgumentError, 'Schema name cannot be empty' if value.nil? || name.strip.empty?
43
+
44
+ unless VALID_PATTERN.match?(name)
45
+ raise ArgumentError,
46
+ "Invalid schema name: #{name.inspect}. Schema names must start with a letter or underscore and " \
47
+ 'contain only letters, digits, and underscores.'
48
+ end
49
+
50
+ @value = name.freeze
51
+ freeze
52
+ end
53
+
54
+ def to_s
55
+ @value
56
+ end
57
+ alias to_str to_s
58
+
59
+ def ==(other)
60
+ other.is_a?(SchemaName) && other.value == @value
61
+ end
62
+ alias eql? ==
63
+
64
+ def hash
65
+ [self.class, @value].hash
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,211 @@
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 'schema_name'
18
+
19
+ module AwsAdvancedRubyDriverWrapper
20
+ module Plugins
21
+ module Encryption
22
+ # Checks that the +encryption_metadata+ and +key_storage+ tables exist and look the way the
23
+ # plugin expects before any kms_encryption is attempted.
24
+ #
25
+ # Everything is read from +information_schema+, which both PostgreSQL and MySQL provide, so
26
+ # the same queries work for either driver. Only the foreign key lookup differs: MySQL keeps
27
+ # the referenced table on +key_column_usage+, while PostgreSQL exposes it through
28
+ # +constraint_column_usage+.
29
+ class SchemaValidator
30
+ REQUIRED_ENCRYPTION_METADATA_COLUMNS = %w[
31
+ table_name column_name encryption_algorithm key_id created_at updated_at
32
+ ].freeze
33
+
34
+ REQUIRED_KEY_STORAGE_COLUMNS = %w[
35
+ id key_id name master_key_arn encrypted_data_key hmac_key key_spec created_at last_used_at
36
+ ].freeze
37
+
38
+ ENCRYPTION_METADATA_TABLE = 'encryption_metadata'
39
+ KEY_STORAGE_TABLE = 'key_storage'
40
+ UNIQUE_METADATA_COLUMNS = %w[table_name column_name].freeze
41
+
42
+ # The outcome of a validation run.
43
+ ValidationResult = Data.define(:valid, :issues)
44
+
45
+ # Reopened so that the constant below is scoped to the result class.
46
+ class ValidationResult
47
+ def initialize(valid: nil, issues: [])
48
+ frozen_issues = issues.freeze
49
+ super(valid: valid.nil? ? frozen_issues.empty? : valid, issues: frozen_issues)
50
+ end
51
+
52
+ # @return [Boolean]
53
+ def valid?
54
+ valid
55
+ end
56
+
57
+ def to_s
58
+ valid? ? 'Schema validation passed' : "Schema validation failed: #{issues.join(', ')}"
59
+ end
60
+ alias inspect to_s
61
+ end
62
+
63
+ # @param metadata_schema [SchemaName, String] the schema holding the two tables
64
+ # @param sql_runner [SqlRunner]
65
+ def initialize(metadata_schema, sql_runner)
66
+ raise ArgumentError, 'metadata_schema is required' if metadata_schema.nil?
67
+
68
+ @schema = SchemaName.of(metadata_schema)
69
+ @sql = sql_runner
70
+ end
71
+
72
+ # Validates the schema over an existing connection.
73
+ #
74
+ # @param connection [Object] a pg or mysql2 connection
75
+ # @return [ValidationResult]
76
+ def validate(connection)
77
+ issues = []
78
+
79
+ issues.concat(validate_table(connection, ENCRYPTION_METADATA_TABLE, REQUIRED_ENCRYPTION_METADATA_COLUMNS) do
80
+ unique_constraint_issues(connection)
81
+ end)
82
+
83
+ issues.concat(validate_table(connection, KEY_STORAGE_TABLE, REQUIRED_KEY_STORAGE_COLUMNS) do
84
+ primary_key_issues(connection)
85
+ end)
86
+
87
+ # Only worth checking once both tables are known to be sound.
88
+ issues.concat(foreign_key_issues(connection)) if issues.empty?
89
+
90
+ ValidationResult.new(issues: issues)
91
+ end
92
+
93
+ private
94
+
95
+ # Checks that a table exists and has every required column, then runs the table's own
96
+ # constraint checks.
97
+ def validate_table(connection, table, required_columns)
98
+ qualified = qualify(table)
99
+ return ["Table '#{qualified}' does not exist"] unless table_exists?(connection, table)
100
+
101
+ issues = missing_columns(connection, table, required_columns).map do |column|
102
+ "Table '#{qualified}' is missing required column '#{column}'"
103
+ end
104
+ issues.concat(yield)
105
+ end
106
+
107
+ def table_exists?(connection, table)
108
+ rows = @sql.query(
109
+ connection,
110
+ "SELECT 1 AS present FROM information_schema.tables WHERE table_schema #{eq} ? AND table_name #{eq} ?",
111
+ [@schema.to_s, table]
112
+ )
113
+ !rows.empty?
114
+ end
115
+
116
+ def missing_columns(connection, table, required_columns)
117
+ rows = @sql.query(
118
+ connection,
119
+ 'SELECT column_name AS name FROM information_schema.columns ' \
120
+ "WHERE table_schema #{eq} ? AND table_name #{eq} ?",
121
+ [@schema.to_s, table]
122
+ )
123
+ existing = rows.to_set { |row| value(row, 'name').to_s.downcase }
124
+
125
+ required_columns.reject { |column| existing.include?(column.downcase) }
126
+ end
127
+
128
+ def unique_constraint_issues(connection)
129
+ columns = constrained_columns(connection, ENCRYPTION_METADATA_TABLE, ['PRIMARY KEY', 'UNIQUE'])
130
+ expected = UNIQUE_METADATA_COLUMNS.to_set
131
+ return [] if columns.include?(expected)
132
+
133
+ ["Table '#{qualify(ENCRYPTION_METADATA_TABLE)}' is missing a unique constraint on " \
134
+ "(#{UNIQUE_METADATA_COLUMNS.join(', ')})"]
135
+ end
136
+
137
+ def primary_key_issues(connection)
138
+ columns = constrained_columns(connection, KEY_STORAGE_TABLE, ['PRIMARY KEY'])
139
+ return [] if columns.any? { |constraint_columns| constraint_columns.include?('id') }
140
+
141
+ ["Table '#{qualify(KEY_STORAGE_TABLE)}' is missing a primary key on 'id'"]
142
+ end
143
+
144
+ def foreign_key_issues(connection)
145
+ references = foreign_keys(connection, ENCRYPTION_METADATA_TABLE)
146
+ matched = references.any? do |reference|
147
+ reference[:from_column] == 'key_id' &&
148
+ reference[:to_table] == KEY_STORAGE_TABLE &&
149
+ reference[:to_column] == 'id'
150
+ end
151
+ return [] if matched
152
+
153
+ ["Missing foreign key constraint from #{qualify(ENCRYPTION_METADATA_TABLE)}.key_id " \
154
+ "to #{qualify(KEY_STORAGE_TABLE)}.id"]
155
+ end
156
+
157
+ # The column sets of every constraint of the given types, one set per constraint.
158
+ #
159
+ # @return [Array<Set<String>>]
160
+ def constrained_columns(connection, table, constraint_types)
161
+ # Spelled out as ORs rather than IN, since IN compares with an unqualified operator.
162
+ type_matches = Array.new(constraint_types.size, "tc.constraint_type #{eq} ?").join(' OR ')
163
+ rows = @sql.query(
164
+ connection,
165
+ 'SELECT tc.constraint_name AS constraint_name, kcu.column_name AS column_name ' \
166
+ 'FROM information_schema.table_constraints tc ' \
167
+ 'JOIN information_schema.key_column_usage kcu ' \
168
+ "ON tc.constraint_name #{eq} kcu.constraint_name AND tc.table_schema #{eq} kcu.table_schema " \
169
+ "AND tc.table_name #{eq} kcu.table_name WHERE (#{type_matches}) " \
170
+ "AND tc.table_schema #{eq} ? AND tc.table_name #{eq} ?",
171
+ constraint_types + [@schema.to_s, table]
172
+ )
173
+
174
+ grouped = rows.group_by { |row| value(row, 'constraint_name') }
175
+ grouped.values.map { |group| group.to_set { |row| value(row, 'column_name').to_s.downcase } }
176
+ end
177
+
178
+ # @return [Array<Hash>] one entry per foreign key column of the table
179
+ def foreign_keys(connection, table)
180
+ rows = @sql.query(connection, foreign_key_sql, [@schema.to_s, table])
181
+
182
+ rows.map do |row|
183
+ {
184
+ from_column: value(row, 'from_column').to_s.downcase,
185
+ to_table: value(row, 'to_table').to_s.downcase,
186
+ to_column: value(row, 'to_column').to_s.downcase
187
+ }
188
+ end
189
+ end
190
+
191
+ def foreign_key_sql
192
+ @sql.foreign_key_query
193
+ end
194
+
195
+ # MySQL reports information_schema column names in upper case on some versions, so every
196
+ # lookup accepts either case.
197
+ def value(row, key)
198
+ row[key] || row[key.upcase]
199
+ end
200
+
201
+ def qualify(table)
202
+ "#{@schema}.#{table}"
203
+ end
204
+
205
+ def eq
206
+ @sql.equals_operator
207
+ end
208
+ end
209
+ end
210
+ end
211
+ end
@@ -0,0 +1,147 @@
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/pg_driver_dialect'
18
+
19
+ module AwsAdvancedRubyDriverWrapper
20
+ module Plugins
21
+ module Encryption
22
+ # Runs the plugin's own metadata and key storage queries against either driver.
23
+ #
24
+ # The plugin needs to talk to +encryption_metadata+ and +key_storage+ itself, and the two
25
+ # drivers disagree on almost every detail of how to do that: pg wants +$1+ placeholders
26
+ # and hex escaped bytea, mysql2 wants +?+ placeholders and raw binary, and only pg can
27
+ # return a generated id from an INSERT. This isolates those differences so that the
28
+ # managers above it can be written once.
29
+ #
30
+ # SQL is always written with +?+ placeholders and translated here.
31
+ class SqlRunner
32
+ # @param driver_dialect [DriverDialects::DriverDialect] the dialect of the connection in use
33
+ def initialize(driver_dialect)
34
+ @driver_dialect = driver_dialect
35
+ @pg = driver_dialect.is_a?(DriverDialects::PgDriverDialect)
36
+ end
37
+
38
+ # @return [Boolean] true when the underlying driver is pg
39
+ def pg?
40
+ @pg
41
+ end
42
+
43
+ # Runs a query and returns its rows.
44
+ #
45
+ # @param connection [Object] a pg or mysql2 connection
46
+ # @param template [String] SQL using +?+ placeholders
47
+ # @param params [Array] the bind parameters
48
+ # @return [Array<Hash{String => Object}>] the result rows, empty for statements without one
49
+ def query(connection, template, params = [])
50
+ rows(raw_execute(connection, template, params))
51
+ end
52
+ alias execute query
53
+
54
+ # Runs an INSERT, UPDATE, or DELETE and returns how many rows it changed.
55
+ #
56
+ # @param connection [Object] a pg or mysql2 connection
57
+ # @param template [String] SQL using +?+ placeholders
58
+ # @param params [Array] the bind parameters
59
+ # @return [Integer] the number of affected rows
60
+ def update(connection, template, params = [])
61
+ result = raw_execute(connection, template, params)
62
+ @driver_dialect.affected_rows(connection, result)
63
+ end
64
+
65
+ # Runs an INSERT and returns the generated +id+.
66
+ #
67
+ # @param connection [Object] a pg or mysql2 connection
68
+ # @param template [String] an INSERT using +?+ placeholders, without a RETURNING clause
69
+ # @param params [Array] the bind parameters
70
+ # @param id_column [String] the generated column to return
71
+ # @return [Integer, nil] the generated id
72
+ def insert_returning_id(connection, template, params, id_column: 'id')
73
+ @driver_dialect.insert_returning_id(connection, translate(template), params, id_column)
74
+ end
75
+
76
+ # Wraps a binary value so that it can be bound to a bytea or blob parameter.
77
+ #
78
+ # @param bytes [String, nil] binary data
79
+ # @return [Object, nil] the driver specific bind value
80
+ def binary_param(bytes)
81
+ return nil if bytes.nil?
82
+
83
+ @driver_dialect.binary_param(bytes)
84
+ end
85
+
86
+ # Reads a bytea or blob column back into binary data.
87
+ #
88
+ # @param value [String, nil] the raw column value
89
+ # @return [String, nil] binary data
90
+ def read_binary(value)
91
+ return nil if value.nil?
92
+
93
+ @driver_dialect.read_binary(value)
94
+ end
95
+
96
+ # Translates +?+ placeholders into the driver's own placeholder syntax.
97
+ #
98
+ # @param template [String]
99
+ # @return [String]
100
+ def translate(template)
101
+ @driver_dialect.translate_placeholders(template)
102
+ end
103
+
104
+ # The driver-specific trailing upsert clause for an INSERT.
105
+ #
106
+ # @param conflict_columns [Array<String>] the columns whose conflict triggers the update
107
+ # @param update_columns [Array<String>] the columns to overwrite from the incoming row
108
+ # @return [String]
109
+ def upsert_clause(conflict_columns, update_columns)
110
+ @driver_dialect.upsert_clause(conflict_columns, update_columns)
111
+ end
112
+
113
+ # The driver-specific equality operator. Every comparison in the plugin's own SQL uses it,
114
+ # so that on pg a user-defined operator earlier in the search_path cannot take its place.
115
+ #
116
+ # @return [String]
117
+ def equals_operator
118
+ @driver_dialect.equals_operator
119
+ end
120
+
121
+ # The driver-specific query for a table's foreign keys, using +?+ placeholders for the
122
+ # schema and table names.
123
+ #
124
+ # @return [String]
125
+ def foreign_key_query
126
+ @driver_dialect.foreign_key_query
127
+ end
128
+
129
+ private
130
+
131
+ def raw_execute(connection, template, params)
132
+ if params.empty?
133
+ @driver_dialect.execute(connection, translate(template))
134
+ else
135
+ @driver_dialect.execute_with_params(connection, translate(template), params)
136
+ end
137
+ end
138
+
139
+ def rows(result)
140
+ return [] if result.nil? || !result.respond_to?(:each)
141
+
142
+ result.to_a
143
+ end
144
+ end
145
+ end
146
+ end
147
+ end
@@ -0,0 +1,109 @@
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 'bigdecimal'
18
+ require 'date'
19
+
20
+ module AwsAdvancedRubyDriverWrapper
21
+ module Plugins
22
+ module Encryption
23
+ # The single byte written ahead of the ciphertext that records how the plaintext was
24
+ # serialized, so that a decrypted payload can be turned back into a Ruby value.
25
+ module TypeMarker
26
+ STRING = 1
27
+ INTEGER = 2
28
+ LONG = 3
29
+ DOUBLE = 4
30
+ FLOAT = 5
31
+ BOOLEAN = 6
32
+ BIG_DECIMAL = 7
33
+ DATE = 8
34
+ TIME = 9
35
+ TIMESTAMP = 10
36
+ LOCAL_DATE = 11
37
+ LOCAL_TIME = 12
38
+ LOCAL_DATE_TIME = 13
39
+ BYTE_ARRAY = 14
40
+ GENERIC = 99
41
+
42
+ NAMES = {
43
+ STRING => 'STRING',
44
+ INTEGER => 'INTEGER',
45
+ LONG => 'LONG',
46
+ DOUBLE => 'DOUBLE',
47
+ FLOAT => 'FLOAT',
48
+ BOOLEAN => 'BOOLEAN',
49
+ BIG_DECIMAL => 'BIG_DECIMAL',
50
+ DATE => 'DATE',
51
+ TIME => 'TIME',
52
+ TIMESTAMP => 'TIMESTAMP',
53
+ LOCAL_DATE => 'LOCAL_DATE',
54
+ LOCAL_TIME => 'LOCAL_TIME',
55
+ LOCAL_DATE_TIME => 'LOCAL_DATE_TIME',
56
+ BYTE_ARRAY => 'BYTE_ARRAY',
57
+ GENERIC => 'GENERIC'
58
+ }.freeze
59
+
60
+ ALL = NAMES.keys.freeze
61
+
62
+ module_function
63
+
64
+ # @param value [Integer] the marker byte read from an encrypted payload
65
+ # @return [Integer] the same marker, once validated
66
+ # @raise [ArgumentError] if the byte is not a known marker
67
+ def from_value(value)
68
+ raise ArgumentError, "Unknown type marker: #{value.inspect}" unless NAMES.key?(value)
69
+
70
+ value
71
+ end
72
+
73
+ # Picks the marker used to serialize a Ruby value. Unknown classes fall back to
74
+ # {GENERIC}, which is serialized through +to_s+.
75
+ #
76
+ # Ruby has a single unbounded Integer type, so integers are always written as
77
+ # {LONG}. {INTEGER} is still understood on read for payloads written by the wrapper.
78
+ #
79
+ # @param value [Object]
80
+ # @return [Integer]
81
+ def from_object(value)
82
+ case value
83
+ when String then value.encoding == ::Encoding::BINARY ? BYTE_ARRAY : STRING
84
+ when Integer then LONG
85
+ when Float then DOUBLE
86
+ when true, false then BOOLEAN
87
+ when BigDecimal then BIG_DECIMAL
88
+ when ::Time then TIMESTAMP
89
+ when ::DateTime then LOCAL_DATE_TIME
90
+ when ::Date then LOCAL_DATE
91
+ else GENERIC
92
+ end
93
+ end
94
+
95
+ # @param marker [Integer]
96
+ # @return [String, nil] the human-readable name of the marker
97
+ def name_for(marker)
98
+ NAMES[marker]
99
+ end
100
+
101
+ # @param marker [Integer]
102
+ # @return [Boolean]
103
+ def known?(marker)
104
+ NAMES.key?(marker)
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end