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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +23 -0
- data/LICENSE +175 -0
- data/NOTICE +1 -0
- data/README.md +168 -0
- data/THIRD-PARTY-LICENSES +473 -0
- data/aws_advanced_ruby_driver_wrapper.gemspec +73 -0
- data/lib/aws_advanced_ruby_driver_wrapper/active_record/aws_mysql2_adapter.rb +73 -0
- data/lib/aws_advanced_ruby_driver_wrapper/active_record/aws_postgresql_adapter.rb +95 -0
- data/lib/aws_advanced_ruby_driver_wrapper/custom_configuration.rb +58 -0
- data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/aurora_mysql_dialect.rb +103 -0
- data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/aurora_pg_dialect.rb +124 -0
- data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/dialect_codes.rb +38 -0
- data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/global_mysql_dialect.rb +91 -0
- data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/global_pg_dialect.rb +92 -0
- data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/multi_az_cluster_mysql_dialect.rb +95 -0
- data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/multi_az_cluster_pg_dialect.rb +86 -0
- data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/mysql_dialect.rb +98 -0
- data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/pg_dialect.rb +95 -0
- data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/rds_mysql_dialect.rb +88 -0
- data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/rds_pg_dialect.rb +86 -0
- data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/unknown_dialect.rb +72 -0
- data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/utils/dialect_utils.rb +71 -0
- data/lib/aws_advanced_ruby_driver_wrapper/driver_dialects/driver_dialect.rb +154 -0
- data/lib/aws_advanced_ruby_driver_wrapper/driver_dialects/driver_dialect_manager.rb +55 -0
- data/lib/aws_advanced_ruby_driver_wrapper/driver_dialects/mysql_driver_dialect.rb +165 -0
- data/lib/aws_advanced_ruby_driver_wrapper/driver_dialects/pg_driver_dialect.rb +201 -0
- data/lib/aws_advanced_ruby_driver_wrapper/errors/error_handler.rb +62 -0
- data/lib/aws_advanced_ruby_driver_wrapper/errors/mysql_error_handler.rb +80 -0
- data/lib/aws_advanced_ruby_driver_wrapper/errors/pg_error_handler.rb +126 -0
- data/lib/aws_advanced_ruby_driver_wrapper/errors.rb +59 -0
- data/lib/aws_advanced_ruby_driver_wrapper/host/connection_string_host_list_provider.rb +95 -0
- data/lib/aws_advanced_ruby_driver_wrapper/host/global_aurora_host_list_provider.rb +65 -0
- data/lib/aws_advanced_ruby_driver_wrapper/host/host_availability.rb +24 -0
- data/lib/aws_advanced_ruby_driver_wrapper/host/host_availability_strategy.rb +27 -0
- data/lib/aws_advanced_ruby_driver_wrapper/host/host_info.rb +137 -0
- data/lib/aws_advanced_ruby_driver_wrapper/host/host_role.rb +25 -0
- data/lib/aws_advanced_ruby_driver_wrapper/host/random_host_selector.rb +40 -0
- data/lib/aws_advanced_ruby_driver_wrapper/host/rds_host_list_provider.rb +206 -0
- data/lib/aws_advanced_ruby_driver_wrapper/logging.rb +110 -0
- data/lib/aws_advanced_ruby_driver_wrapper/monitoring/cluster_topology_monitor.rb +709 -0
- data/lib/aws_advanced_ruby_driver_wrapper/monitoring/global_cluster_topology_monitor.rb +72 -0
- data/lib/aws_advanced_ruby_driver_wrapper/monitoring/monitor.rb +99 -0
- data/lib/aws_advanced_ruby_driver_wrapper/monitoring/monitor_connection.rb +57 -0
- data/lib/aws_advanced_ruby_driver_wrapper/monitoring/monitor_state.rb +25 -0
- data/lib/aws_advanced_ruby_driver_wrapper/mysql.rb +429 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/blue_green_plugin.rb +205 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/host_mapper.rb +132 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/iam_host_tracker.rb +84 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/interim_status.rb +92 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/interval_rate.rb +27 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/phase.rb +69 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/phase_event_log.rb +85 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/phase_time_info.rb +25 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/role.rb +38 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/routing/base_routing.rb +83 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/routing/reject_connect_routing.rb +40 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/routing/substitute_connect_routing.rb +136 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/routing/suspend_connect_routing.rb +53 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/routing/suspend_execute_routing.rb +52 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/routing/suspend_until_corresponding_host_found_connect_routing.rb +83 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/status.rb +68 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/status_builder.rb +244 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/status_info.rb +30 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/status_monitor.rb +564 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/status_provider.rb +414 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/switchover_state.rb +98 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/switchover_timer.rb +46 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/custom_endpoint/custom_endpoint_monitor.rb +266 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/custom_endpoint/custom_endpoint_plugin.rb +158 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/custom_endpoint/info.rb +111 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/custom_endpoint/member_list_type.rb +31 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/custom_endpoint/role.rb +45 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/default_plugin.rb +108 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/failover_mode.rb +43 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/failover_plugin.rb +467 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/gdb/gdb_failover_mode.rb +68 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/gdb/gdb_failover_plugin.rb +403 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/iam_auth_plugin.rb +159 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/initial_connection_strategy_plugin.rb +485 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/audit_logger.rb +157 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/column_cipher.rb +159 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/column_encryption_config.rb +61 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/connection_source.rb +91 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/data_key_cache.rb +220 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/encryption_algorithm.rb +75 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/encryption_config.rb +146 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/encryption_service.rb +391 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/error_context.rb +198 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/errors.rb +259 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/key_management_utility.rb +435 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/key_manager.rb +378 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/key_metadata.rb +86 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/kms_encryption_plugin.rb +890 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/kms_encryption_utility.rb +281 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/metadata_manager.rb +332 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/sanitizer.rb +147 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/schema_name.rb +70 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/schema_validator.rb +211 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/sql_runner.rb +147 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/type_marker.rb +109 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/secrets_manager_plugin.rb +358 -0
- data/lib/aws_advanced_ruby_driver_wrapper/postgresql.rb +659 -0
- data/lib/aws_advanced_ruby_driver_wrapper/property_definition.rb +409 -0
- data/lib/aws_advanced_ruby_driver_wrapper/ruby_method.rb +122 -0
- data/lib/aws_advanced_ruby_driver_wrapper/services/connection_service.rb +143 -0
- data/lib/aws_advanced_ruby_driver_wrapper/services/dialect_service.rb +267 -0
- data/lib/aws_advanced_ruby_driver_wrapper/services/host_service.rb +199 -0
- data/lib/aws_advanced_ruby_driver_wrapper/services/monitor_service.rb +186 -0
- data/lib/aws_advanced_ruby_driver_wrapper/services/plugin_call_context.rb +63 -0
- data/lib/aws_advanced_ruby_driver_wrapper/services/plugin_manager.rb +273 -0
- data/lib/aws_advanced_ruby_driver_wrapper/services/service_container.rb +30 -0
- data/lib/aws_advanced_ruby_driver_wrapper/services/service_utility.rb +78 -0
- data/lib/aws_advanced_ruby_driver_wrapper/services/session_state_service.rb +56 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/accessible_regions.rb +52 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/ar_constants.rb +25 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/aurora_topology_utils.rb +99 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/aws_credentials_utils.rb +62 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/connection_config.rb +91 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/connection_config_parser.rb +368 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/conversion_utils.rb +51 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/events/batching_event_publisher.rb +119 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/events/data_access_event.rb +26 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/events/monitor_reset_event.rb +26 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/global_aurora_topology_utils.rb +185 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/host_list_utils.rb +27 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/iam_auth_utils.rb +112 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/multi_az_topology_utils.rb +117 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/encryption_annotation_parser.rb +99 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/mysql_statement_analyzer.rb +641 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/pg_statement_analyzer.rb +502 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/query_analysis.rb +63 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/query_type.rb +35 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/routing_hint.rb +27 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/routing_hint_parser.rb +50 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/sql_parser.rb +139 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/rds_url_type.rb +71 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/rds_utils.rb +575 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/retry_util.rb +153 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/sql_encoding.rb +56 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/sql_method_analyzer.rb +195 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/storage/cache_entry.rb +56 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/storage/expiration_cache.rb +108 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/storage/sliding_expiration_cache.rb +137 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/storage/storage_service.rb +172 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/topology_utils.rb +127 -0
- data/lib/aws_advanced_ruby_driver_wrapper/version.rb +19 -0
- data/lib/aws_advanced_ruby_driver_wrapper/wrapper_property.rb +64 -0
- data/lib/aws_advanced_ruby_driver_wrapper.rb +116 -0
- metadata +227 -0
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
4
|
+
#
|
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License").
|
|
6
|
+
# You may not use this file except in compliance with the License.
|
|
7
|
+
# You may obtain a copy of the License at
|
|
8
|
+
#
|
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
#
|
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
# See the License for the specific language governing permissions and
|
|
15
|
+
# limitations under the License.
|
|
16
|
+
|
|
17
|
+
require 'concurrent'
|
|
18
|
+
require_relative '../errors'
|
|
19
|
+
require_relative '../logging'
|
|
20
|
+
require_relative '../property_definition'
|
|
21
|
+
require_relative '../utils/aws_credentials_utils'
|
|
22
|
+
|
|
23
|
+
module AwsAdvancedRubyDriverWrapper
|
|
24
|
+
module Plugins
|
|
25
|
+
class SecretsManagerPlugin
|
|
26
|
+
include Logging
|
|
27
|
+
|
|
28
|
+
SUBSCRIBED_METHODS = Set['connect', 'internal_connect'].freeze
|
|
29
|
+
SECRETS_MANAGER_CACHE_NAME = :secrets_manager
|
|
30
|
+
MIN_EXPIRATION_SEC = 300
|
|
31
|
+
SYNC_FETCH_TIMEOUT_SEC = 60
|
|
32
|
+
# The partition segment of an ARN varies across AWS partitions, so match any of them.
|
|
33
|
+
SECRETS_ARN_PATTERN = %r{\Aarn:aws(?:-[a-z]+)*:secretsmanager:(?<region>[^:\n]+):[^:\n]*:(?:[^:/\n]*[:/])?}
|
|
34
|
+
MAX_RETRY_DELAY_SEC = 8
|
|
35
|
+
|
|
36
|
+
# The maximum time a fetched secret stays in the shared cache, measured from when it was
|
|
37
|
+
# fetched (the cache anchors expiry at store time and does not renew on read). Until its
|
|
38
|
+
# logical expiration (@expiration_sec, default 14.5 min) the entry is served fresh;
|
|
39
|
+
# between logical expiration and this disposal time it is served stale while a background
|
|
40
|
+
# refresh runs (SWR); after this it is physically removed and the next use re-fetches from
|
|
41
|
+
# Secrets Manager. This is the hard cap on how long a cached secret can live.
|
|
42
|
+
SECRET_CACHE_DISPOSAL_SEC = 20 * 60
|
|
43
|
+
|
|
44
|
+
# The window reserved between logical expiration and physical disposal for a background
|
|
45
|
+
# refresh to complete. The logical expiration is clamped so that at least this much time
|
|
46
|
+
# remains for stale-while-revalidate. Sized to the synchronous fetch timeout so a refresh
|
|
47
|
+
# has a full fetch's worth of time to land.
|
|
48
|
+
SWR_REVALIDATION_BUDGET_SEC = SYNC_FETCH_TIMEOUT_SEC
|
|
49
|
+
|
|
50
|
+
# The largest logical expiration we allow, leaving room for the SWR refresh window.
|
|
51
|
+
MAX_EXPIRATION_SEC = SECRET_CACHE_DISPOSAL_SEC - SWR_REVALIDATION_BUDGET_SEC
|
|
52
|
+
|
|
53
|
+
SecretEntry = Data.define(:username, :password, :expires_at) do
|
|
54
|
+
def expired?(now = Process.clock_gettime(Process::CLOCK_MONOTONIC))
|
|
55
|
+
expires_at && now >= expires_at
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Redact the password so the secret is never exposed if an instance is
|
|
59
|
+
# logged, interpolated, or rendered in a backtrace.
|
|
60
|
+
def inspect
|
|
61
|
+
"#<data SecretEntry username=#{username.inspect}, " \
|
|
62
|
+
"password=#{AwsAdvancedRubyDriverWrapper::REDACTED.inspect}, expires_at=#{expires_at.inspect}>"
|
|
63
|
+
end
|
|
64
|
+
alias_method :to_s, :inspect
|
|
65
|
+
|
|
66
|
+
# `pp` / PrettyPrint does not call #inspect; route them through the
|
|
67
|
+
# redacted representation so `pp entry` cannot leak the password.
|
|
68
|
+
def pretty_print(pp)
|
|
69
|
+
pp.text(inspect)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
@pending_refreshes = Concurrent::Map.new
|
|
74
|
+
|
|
75
|
+
class << self
|
|
76
|
+
attr_reader :pending_refreshes
|
|
77
|
+
|
|
78
|
+
def clear_cache(storage_service)
|
|
79
|
+
storage_service.clear(SECRETS_MANAGER_CACHE_NAME)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# The secret's value depends on which secret is read, from which region and endpoint, and
|
|
83
|
+
# with which AWS credentials, so all of them are part of the key. Connections that differ in
|
|
84
|
+
# any of them neither share a cached secret nor wait on each other's fetch.
|
|
85
|
+
#
|
|
86
|
+
# @param endpoint [String, nil] the custom Secrets Manager endpoint, if any
|
|
87
|
+
# @param credentials_identity [String] from {Utils::AwsCredentialsUtils.identity}
|
|
88
|
+
def cache_key(secret_id, region, endpoint, credentials_identity)
|
|
89
|
+
"#{secret_id}:#{region}:#{endpoint}:#{credentials_identity}"
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
attr_reader :subscribed_methods
|
|
94
|
+
|
|
95
|
+
def initialize(service_container, props = ::Concurrent::Map.new)
|
|
96
|
+
ensure_sdk!
|
|
97
|
+
@service_container = service_container
|
|
98
|
+
@wrapper_props = props
|
|
99
|
+
@credentials_provider = PropertyDefinition::AWS_CREDENTIALS_PROVIDER.get(props) ||
|
|
100
|
+
Aws::CredentialProviderChain.new.resolve
|
|
101
|
+
@secret_id = PropertyDefinition::SECRET_ID.get(props)
|
|
102
|
+
raise Errors::SecretsManagerAuthError, 'secret_id is required' unless @secret_id
|
|
103
|
+
|
|
104
|
+
@region = resolve_region(props)
|
|
105
|
+
unless @region
|
|
106
|
+
raise Errors::SecretsManagerAuthError,
|
|
107
|
+
'Unable to determine region; set :secret_region or use a Secrets Manager ARN'
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
@username_key = PropertyDefinition::SECRET_USERNAME_KEY.get_string(props)
|
|
111
|
+
@password_key = PropertyDefinition::SECRET_PASSWORD_KEY.get_string(props)
|
|
112
|
+
@expiration_sec = resolve_expiration(props)
|
|
113
|
+
@endpoint = PropertyDefinition::SECRET_ENDPOINT.get(props)
|
|
114
|
+
@rotation_retry_timeout_sec = PropertyDefinition::SECRET_ROTATION_RETRY_TIMEOUT_SEC.get_float(props)
|
|
115
|
+
@rotation_retry_base_delay_sec = PropertyDefinition::SECRET_ROTATION_RETRY_BASE_DELAY_SEC.get_float(props)
|
|
116
|
+
|
|
117
|
+
service_container.storage_service.register(
|
|
118
|
+
SECRETS_MANAGER_CACHE_NAME,
|
|
119
|
+
ttl: SECRET_CACHE_DISPOSAL_SEC
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
@subscribed_methods = SUBSCRIBED_METHODS
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def connect(_host_info, driver_props, _is_initial_connection, pipeline_callable)
|
|
126
|
+
secrets_connect(driver_props, pipeline_callable)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def internal_connect(_host_info, driver_props, _, _is_initial_connection, pipeline_callable)
|
|
130
|
+
secrets_connect(driver_props, pipeline_callable)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
private
|
|
134
|
+
|
|
135
|
+
def secrets_connect(driver_props, pipeline_callable)
|
|
136
|
+
if @rotation_retry_timeout_sec.positive?
|
|
137
|
+
connect_with_rotation_budget(driver_props, pipeline_callable)
|
|
138
|
+
else
|
|
139
|
+
connect_with_single_retry(driver_props, pipeline_callable)
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Default behavior (budget disabled): at most one forced re-fetch, and only when
|
|
144
|
+
# the *cached* secret failed to log in.
|
|
145
|
+
def connect_with_single_retry(driver_props, pipeline_callable)
|
|
146
|
+
secret_is_fresh = fetch_secret_and_report_if_fresh?(force: false)
|
|
147
|
+
apply_secret(driver_props)
|
|
148
|
+
|
|
149
|
+
begin
|
|
150
|
+
return pipeline_callable.call
|
|
151
|
+
rescue StandardError => e
|
|
152
|
+
raise unless !secret_is_fresh && @service_container.dialect_service.login_error?(e)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
fetch_secret_and_report_if_fresh?(force: true)
|
|
156
|
+
apply_secret(driver_props)
|
|
157
|
+
pipeline_callable.call
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Budgeted behavior: force a re-fetch + reconnect with capped exponential backoff
|
|
161
|
+
# until login succeeds or the time budget is exhausted. Bridges the rotation window
|
|
162
|
+
# including the cold-cache first connection, and tolerates transient fetch failures.
|
|
163
|
+
def connect_with_rotation_budget(driver_props, pipeline_callable)
|
|
164
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + @rotation_retry_timeout_sec
|
|
165
|
+
delay_sec = @rotation_retry_base_delay_sec
|
|
166
|
+
last_login_error = nil
|
|
167
|
+
attempt = 0
|
|
168
|
+
|
|
169
|
+
loop do
|
|
170
|
+
attempt += 1
|
|
171
|
+
|
|
172
|
+
begin
|
|
173
|
+
# Attempt 1 may use the cache; every later attempt forces a re-fetch to pick up
|
|
174
|
+
# a version promoted in the meantime.
|
|
175
|
+
fetch_secret_and_report_if_fresh?(force: attempt > 1)
|
|
176
|
+
rescue StandardError => e
|
|
177
|
+
raise e if last_login_error.nil?
|
|
178
|
+
|
|
179
|
+
# Otherwise a transient Secrets Manager failure must not consume the budget;
|
|
180
|
+
# keep the login error as the reported cause and try again.
|
|
181
|
+
logger.debug("SecretsManagerPlugin: re-fetch failed mid-retry (#{e.class}: #{e.message}); keeping login error")
|
|
182
|
+
else
|
|
183
|
+
apply_secret(driver_props)
|
|
184
|
+
begin
|
|
185
|
+
connection = pipeline_callable.call
|
|
186
|
+
logger.info("SecretsManagerPlugin: connection succeeded on attempt #{attempt}") if attempt > 1
|
|
187
|
+
return connection
|
|
188
|
+
rescue StandardError => e
|
|
189
|
+
# Not a credentials problem -> re-fetching would not help.
|
|
190
|
+
raise unless @service_container.dialect_service.login_error?(e)
|
|
191
|
+
|
|
192
|
+
last_login_error = e
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
remaining = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
197
|
+
if remaining <= 0
|
|
198
|
+
logger.info("SecretsManagerPlugin: rotation retry budget exhausted after #{attempt} attempt(s)")
|
|
199
|
+
raise last_login_error
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
sleep([delay_sec, remaining].min)
|
|
203
|
+
delay_sec = [delay_sec * 2, MAX_RETRY_DELAY_SEC].min
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# Returns true if credentials were freshly fetched from the service (suppresses login-error retry).
|
|
208
|
+
def fetch_secret_and_report_if_fresh?(force:)
|
|
209
|
+
credentials = Utils::AwsCredentialsUtils.snapshot(@credentials_provider)
|
|
210
|
+
cache_key = secret_cache_key(credentials)
|
|
211
|
+
entry = cache_key && @service_container.storage_service.get(SECRETS_MANAGER_CACHE_NAME, cache_key)
|
|
212
|
+
|
|
213
|
+
if entry.nil? || force
|
|
214
|
+
@secret = fetch_synchronously(cache_key, credentials)
|
|
215
|
+
return !@secret.nil?
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
if entry.expired?
|
|
219
|
+
logger.debug('SecretsManagerPlugin: serving stale credentials, refreshing in background')
|
|
220
|
+
@secret = entry
|
|
221
|
+
trigger_async_refresh(cache_key, credentials)
|
|
222
|
+
return false
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
@secret = entry
|
|
226
|
+
false
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# The cache key for a secret read with the given credentials snapshot. Built per fetch, so it
|
|
230
|
+
# follows credentials that refresh to a new access key. Nil when there are no credentials:
|
|
231
|
+
# without them there is nothing to tell one connection's secret from another's, so such a
|
|
232
|
+
# connection neither reads nor writes the shared cache, and does not join another connection's
|
|
233
|
+
# in-flight fetch.
|
|
234
|
+
def secret_cache_key(credentials)
|
|
235
|
+
return nil if credentials.nil?
|
|
236
|
+
|
|
237
|
+
self.class.cache_key(@secret_id, @region, @endpoint, Utils::AwsCredentialsUtils.identity(credentials))
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def fetch_synchronously(cache_key, credentials)
|
|
241
|
+
future =
|
|
242
|
+
if cache_key
|
|
243
|
+
trigger_async_refresh(cache_key, credentials)
|
|
244
|
+
else
|
|
245
|
+
Concurrent::Promises.future_on(:io) { fetch_and_store_secret(nil, credentials) }
|
|
246
|
+
end
|
|
247
|
+
future.value!(SYNC_FETCH_TIMEOUT_SEC)
|
|
248
|
+
rescue Concurrent::CancelledOperationError, Timeout::Error
|
|
249
|
+
raise Errors::SecretsManagerAuthError,
|
|
250
|
+
"Timed out fetching secret after #{SYNC_FETCH_TIMEOUT_SEC}s"
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def trigger_async_refresh(cache_key, credentials)
|
|
254
|
+
pending = self.class.pending_refreshes
|
|
255
|
+
|
|
256
|
+
existing = pending[cache_key]
|
|
257
|
+
return existing if existing && !existing.resolved?
|
|
258
|
+
|
|
259
|
+
future_candidate = Concurrent::Promises.delay_on(:io) { fetch_and_store_secret(cache_key, credentials) }
|
|
260
|
+
stored = pending.put_if_absent(cache_key, future_candidate)
|
|
261
|
+
return stored if stored
|
|
262
|
+
|
|
263
|
+
future_candidate.touch
|
|
264
|
+
|
|
265
|
+
future_candidate.on_resolution! do |_fulfilled, _value, reason|
|
|
266
|
+
pending.delete_pair(cache_key, future_candidate)
|
|
267
|
+
logger.debug("SecretsManagerPlugin: async refresh failed: #{reason}") if reason
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
future_candidate
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
# Fetches the secret with the same credentials snapshot its cache key was built from and, when
|
|
274
|
+
# there is a cache key, caches it.
|
|
275
|
+
def fetch_and_store_secret(cache_key, credentials)
|
|
276
|
+
response = build_secrets_client(credentials).get_secret_value(secret_id: @secret_id)
|
|
277
|
+
parsed = parse_secret_string(response.secret_string)
|
|
278
|
+
|
|
279
|
+
unless parsed.is_a?(Hash) && parsed.key?(@username_key) && parsed.key?(@password_key)
|
|
280
|
+
raise Errors::SecretsManagerAuthError,
|
|
281
|
+
"Secret JSON missing required keys: '#{@username_key}' and/or '#{@password_key}'"
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
entry = SecretEntry.new(
|
|
285
|
+
username: parsed[@username_key],
|
|
286
|
+
password: parsed[@password_key],
|
|
287
|
+
expires_at: Process.clock_gettime(Process::CLOCK_MONOTONIC) + @expiration_sec
|
|
288
|
+
)
|
|
289
|
+
@service_container.storage_service.set(SECRETS_MANAGER_CACHE_NAME, cache_key, entry) if cache_key
|
|
290
|
+
entry
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
# Parse the raw secret string as JSON. A plaintext (non-JSON) secret makes
|
|
294
|
+
# +JSON.parse+ raise a +JSON::ParserError+. Catch that error and re-raise a
|
|
295
|
+
# +SecretsManagerAuthError+ with a message that never includes the secret.
|
|
296
|
+
def parse_secret_string(secret_string)
|
|
297
|
+
JSON.parse(secret_string)
|
|
298
|
+
rescue JSON::ParserError
|
|
299
|
+
raise Errors::SecretsManagerAuthError,
|
|
300
|
+
'The secret is not in the expected JSON format. Ensure the secret stored in AWS ' \
|
|
301
|
+
'Secrets Manager is a JSON object containing the configured username and password keys.'
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
def apply_secret(driver_props)
|
|
305
|
+
raise Errors::SecretsManagerAuthError, 'Failed to fetch database credentials from AWS Secrets Manager' unless @secret
|
|
306
|
+
|
|
307
|
+
user_key = @service_container.dialect_service.driver_dialect.user_property_key
|
|
308
|
+
driver_props[user_key] = @secret.username
|
|
309
|
+
driver_props[:password] = @secret.password
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
# A client for one fetch, signing with the given credentials snapshot. Without a snapshot the
|
|
313
|
+
# provider is passed as is, and the request fails for lack of credentials. Fetches only happen
|
|
314
|
+
# on a cache miss or refresh, so building a client for each one costs little.
|
|
315
|
+
def build_secrets_client(credentials)
|
|
316
|
+
opts = { region: @region, credentials: credentials || @credentials_provider }
|
|
317
|
+
opts[:endpoint] = @endpoint if @endpoint
|
|
318
|
+
Aws::SecretsManager::Client.new(**opts)
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
def resolve_region(props)
|
|
322
|
+
explicit = PropertyDefinition::SECRET_REGION.get(props)
|
|
323
|
+
return explicit if explicit
|
|
324
|
+
|
|
325
|
+
match = SECRETS_ARN_PATTERN.match(@secret_id.to_s)
|
|
326
|
+
match[:region] if match
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
# The logical expiration must leave room for the stale-while-revalidate window: an entry is
|
|
330
|
+
# physically removed SECRET_CACHE_DISPOSAL_SEC after it was fetched, so once it expires it can
|
|
331
|
+
# only be served stale until then. Clamp it to the range [MIN_EXPIRATION_SEC, MAX_EXPIRATION_SEC],
|
|
332
|
+
# where MAX_EXPIRATION_SEC reserves SWR_REVALIDATION_BUDGET_SEC before disposal for the refresh to complete.
|
|
333
|
+
def resolve_expiration(props)
|
|
334
|
+
configured = PropertyDefinition::SECRET_EXPIRATION_SEC.get_float(props)
|
|
335
|
+
if configured < MIN_EXPIRATION_SEC
|
|
336
|
+
logger.warn("SecretsManagerPlugin: expiration #{configured}s below minimum #{MIN_EXPIRATION_SEC}s, clamping")
|
|
337
|
+
MIN_EXPIRATION_SEC
|
|
338
|
+
elsif configured > MAX_EXPIRATION_SEC
|
|
339
|
+
logger.warn("SecretsManagerPlugin: expiration #{configured}s exceeds the #{MAX_EXPIRATION_SEC}s maximum " \
|
|
340
|
+
"(leaving #{SWR_REVALIDATION_BUDGET_SEC}s before the #{SECRET_CACHE_DISPOSAL_SEC}s cache " \
|
|
341
|
+
'lifetime cap for stale-while-revalidate), clamping')
|
|
342
|
+
MAX_EXPIRATION_SEC
|
|
343
|
+
else
|
|
344
|
+
configured
|
|
345
|
+
end
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
def ensure_sdk!
|
|
349
|
+
require 'aws-sdk-secretsmanager'
|
|
350
|
+
require 'json'
|
|
351
|
+
rescue LoadError
|
|
352
|
+
raise LoadError,
|
|
353
|
+
"The Secrets Manager plugin requires 'aws-sdk-secretsmanager'. " \
|
|
354
|
+
"Add it to your Gemfile: gem 'aws-sdk-secretsmanager'"
|
|
355
|
+
end
|
|
356
|
+
end
|
|
357
|
+
end
|
|
358
|
+
end
|