aws_advanced_ruby_driver_wrapper 1.0.0 → 1.1.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 +4 -4
- data/CHANGELOG.md +48 -2
- data/README.md +115 -6
- data/THIRD-PARTY-LICENSES +70 -11
- data/aws_advanced_ruby_driver_wrapper.gemspec +9 -0
- data/lib/aws_advanced_ruby_driver_wrapper/active_record/aws_action_cable_postgresql_support.rb +40 -0
- data/lib/aws_advanced_ruby_driver_wrapper/active_record/aws_connection_handler.rb +47 -0
- data/lib/aws_advanced_ruby_driver_wrapper/active_record/aws_mysql2_adapter.rb +23 -9
- data/lib/aws_advanced_ruby_driver_wrapper/active_record/aws_postgresql_adapter.rb +65 -10
- data/lib/aws_advanced_ruby_driver_wrapper/active_record/aws_trilogy_adapter.rb +80 -0
- data/lib/aws_advanced_ruby_driver_wrapper/active_record/type_adapter_alias.rb +61 -0
- data/lib/aws_advanced_ruby_driver_wrapper/driver_dialects/driver_dialect.rb +48 -0
- data/lib/aws_advanced_ruby_driver_wrapper/driver_dialects/driver_dialect_manager.rb +11 -4
- data/lib/aws_advanced_ruby_driver_wrapper/driver_dialects/mysql2_driver_dialect.rb +129 -0
- data/lib/aws_advanced_ruby_driver_wrapper/driver_dialects/mysql_driver_dialect.rb +9 -81
- data/lib/aws_advanced_ruby_driver_wrapper/driver_dialects/pg_driver_dialect.rb +28 -0
- data/lib/aws_advanced_ruby_driver_wrapper/driver_dialects/trilogy_driver_dialect.rb +118 -0
- data/lib/aws_advanced_ruby_driver_wrapper/errors/error_handler.rb +16 -0
- data/lib/aws_advanced_ruby_driver_wrapper/errors/mysql_error_handler.rb +10 -5
- data/lib/aws_advanced_ruby_driver_wrapper/errors/trilogy_error_handler.rb +127 -0
- data/lib/aws_advanced_ruby_driver_wrapper/errors.rb +1 -0
- data/lib/aws_advanced_ruby_driver_wrapper/monitoring/cluster_topology_monitor.rb +11 -0
- data/lib/aws_advanced_ruby_driver_wrapper/monitoring/monitor.rb +14 -0
- data/lib/aws_advanced_ruby_driver_wrapper/monitoring/monitor_connection.rb +6 -0
- data/lib/aws_advanced_ruby_driver_wrapper/mysql.rb +27 -9
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/blue_green_plugin.rb +125 -7
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/drop_blue_connections_marker.rb +84 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/host_mapper.rb +3 -1
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/routing/base_routing.rb +7 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/routing/substitute_connect_routing.rb +68 -41
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/routing/suspend_connect_routing.rb +4 -2
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/routing/suspend_execute_routing.rb +5 -3
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/routing/suspend_until_corresponding_host_found_connect_routing.rb +7 -1
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/status_builder.rb +1 -1
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/status_monitor.rb +10 -13
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/status_provider.rb +95 -3
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/custom_endpoint/custom_endpoint_monitor.rb +11 -1
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/custom_endpoint/custom_endpoint_plugin.rb +4 -1
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/default_plugin.rb +14 -3
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/failover_plugin.rb +68 -3
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/federated_auth/base_saml_auth_plugin.rb +266 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/federated_auth/federated_auth_plugin.rb +134 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/federated_auth/okta_auth_plugin.rb +113 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/gdb/gdb_failover_plugin.rb +30 -9
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/iam_auth_plugin.rb +16 -2
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/initial_connection_strategy_plugin.rb +4 -1
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/key_management_utility.rb +6 -3
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/key_manager.rb +13 -4
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/kms_encryption_plugin.rb +29 -6
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/sql/trigger_mysql.sql +108 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/sql/trigger_pg.sql +54 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/secrets_manager_plugin.rb +23 -1
- data/lib/aws_advanced_ruby_driver_wrapper/postgresql.rb +49 -23
- data/lib/aws_advanced_ruby_driver_wrapper/property_definition.rb +63 -1
- data/lib/aws_advanced_ruby_driver_wrapper/ruby_method.rb +3 -0
- data/lib/aws_advanced_ruby_driver_wrapper/services/dialect_service.rb +20 -7
- data/lib/aws_advanced_ruby_driver_wrapper/services/host_service.rb +34 -8
- data/lib/aws_advanced_ruby_driver_wrapper/services/monitor_service.rb +23 -4
- data/lib/aws_advanced_ruby_driver_wrapper/services/plugin_manager.rb +80 -29
- data/lib/aws_advanced_ruby_driver_wrapper/services/service_container.rb +10 -2
- data/lib/aws_advanced_ruby_driver_wrapper/services/service_utility.rb +4 -1
- data/lib/aws_advanced_ruby_driver_wrapper/trilogy.rb +167 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/connection_config.rb +1 -1
- data/lib/aws_advanced_ruby_driver_wrapper/utils/connection_config_parser.rb +25 -15
- data/lib/aws_advanced_ruby_driver_wrapper/utils/events/batching_event_publisher.rb +18 -7
- data/lib/aws_advanced_ruby_driver_wrapper/utils/global_aurora_topology_utils.rb +2 -2
- data/lib/aws_advanced_ruby_driver_wrapper/utils/iam_auth_utils.rb +13 -4
- data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/encryption_annotation_parser.rb +15 -31
- data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/mysql_statement_analyzer.rb +55 -76
- data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/mysql_text.rb +215 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/pg_statement_analyzer.rb +11 -20
- data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/query_analysis.rb +2 -3
- data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/sql_parser.rb +21 -14
- data/lib/aws_advanced_ruby_driver_wrapper/utils/retry_util.rb +2 -1
- data/lib/aws_advanced_ruby_driver_wrapper/utils/saml_utils.rb +133 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/storage/storage_service.rb +29 -8
- data/lib/aws_advanced_ruby_driver_wrapper/utils/telemetry/null_telemetry.rb +89 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/telemetry/open_telemetry_context.rb +124 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/{ar_constants.rb → telemetry/open_telemetry_counter.rb} +19 -5
- data/lib/aws_advanced_ruby_driver_wrapper/utils/telemetry/open_telemetry_factory.rb +122 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/telemetry/open_telemetry_gauge.rb +36 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/telemetry/telemetry_const.rb +50 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/telemetry/telemetry_factory.rb +143 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/topology_utils.rb +9 -2
- data/lib/aws_advanced_ruby_driver_wrapper/version.rb +1 -1
- data/lib/aws_advanced_ruby_driver_wrapper.rb +47 -2
- metadata +57 -3
|
@@ -20,9 +20,11 @@ require_relative '../host/host_info'
|
|
|
20
20
|
require_relative '../host/host_role'
|
|
21
21
|
require_relative '../host/host_availability'
|
|
22
22
|
require_relative '../logging'
|
|
23
|
+
require_relative '../services/host_service'
|
|
23
24
|
require_relative '../utils/rds_utils'
|
|
24
25
|
require_relative '../utils/rds_url_type'
|
|
25
26
|
require_relative '../utils/retry_util'
|
|
27
|
+
require_relative '../utils/telemetry/telemetry_const'
|
|
26
28
|
require_relative '../wrapper_property'
|
|
27
29
|
require_relative 'failover_mode'
|
|
28
30
|
|
|
@@ -32,14 +34,25 @@ module AwsAdvancedRubyDriverWrapper
|
|
|
32
34
|
include Logging
|
|
33
35
|
|
|
34
36
|
ReaderFailoverResult = Data.define(:connection, :host_info)
|
|
37
|
+
FailoverCounters = Data.define(:triggered, :succeeded, :failed)
|
|
38
|
+
|
|
39
|
+
SUCCESSFUL_FAILOVER_ERRORS = [Errors::FailoverSuccessError, Errors::TransactionStateUnknownError].freeze
|
|
40
|
+
|
|
41
|
+
TELEMETRY_WRITER_FAILOVER = 'failover to writer host'
|
|
42
|
+
TELEMETRY_READER_FAILOVER = 'failover to replica'
|
|
35
43
|
|
|
36
44
|
def initialize(service_container, props = ::Concurrent::Map.new)
|
|
37
45
|
@service_container = service_container
|
|
38
46
|
@wrapper_props = props
|
|
47
|
+
@telemetry_factory = service_container.telemetry_factory
|
|
48
|
+
@telemetry_failover_additional_top_trace = PropertyDefinition::TELEMETRY_FAILOVER_ADDITIONAL_TOP_TRACE.get_bool(props)
|
|
49
|
+
@writer_failover_counters = failover_counters('writer_failover')
|
|
50
|
+
@reader_failover_counters = failover_counters('reader_failover')
|
|
39
51
|
|
|
40
52
|
@retry_util = Utils::RetryUtil.new(service_container)
|
|
41
53
|
@failover_timeout = PropertyDefinition::FAILOVER_TIMEOUT_SEC.get_float(props)
|
|
42
54
|
@reader_selector_strategy = PropertyDefinition::FAILOVER_READER_HOST_SELECTOR_STRATEGY.get(props)
|
|
55
|
+
Services::HostService.validate_strategy!(@reader_selector_strategy)
|
|
43
56
|
@failover_mode = nil
|
|
44
57
|
@rds_url_type = nil
|
|
45
58
|
@closed_explicitly = false
|
|
@@ -235,9 +248,60 @@ module AwsAdvancedRubyDriverWrapper
|
|
|
235
248
|
end
|
|
236
249
|
|
|
237
250
|
if @failover_mode == FailoverMode::STRICT_WRITER
|
|
238
|
-
failover_writer
|
|
251
|
+
with_failover_telemetry(TELEMETRY_WRITER_FAILOVER) { count_failover(@writer_failover_counters) { failover_writer } }
|
|
239
252
|
else
|
|
240
|
-
failover_reader
|
|
253
|
+
with_failover_telemetry(TELEMETRY_READER_FAILOVER) { count_failover(@reader_failover_counters) { failover_reader } }
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def failover_counters(prefix)
|
|
258
|
+
FailoverCounters.new(
|
|
259
|
+
triggered: @telemetry_factory.create_counter("#{prefix}.triggered.count"),
|
|
260
|
+
succeeded: @telemetry_factory.create_counter("#{prefix}.completed.success.count"),
|
|
261
|
+
failed: @telemetry_factory.create_counter("#{prefix}.completed.failed.count")
|
|
262
|
+
)
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
# Records a failover as a span. Failover always ends in an error: a successful one ends in one
|
|
266
|
+
# of SUCCESSFUL_FAILOVER_ERRORS, which marks the span as succeeded, and any other error marks it
|
|
267
|
+
# as failed. With telemetry_failover_additional_top_trace enabled, the span is also submitted as
|
|
268
|
+
# a trace of its own.
|
|
269
|
+
def with_failover_telemetry(span_name)
|
|
270
|
+
telemetry_context = nil
|
|
271
|
+
@telemetry_factory.in_context(span_name, Utils::Telemetry::TraceLevel::NESTED) do |context|
|
|
272
|
+
telemetry_context = context
|
|
273
|
+
yield
|
|
274
|
+
rescue *SUCCESSFUL_FAILOVER_ERRORS => e
|
|
275
|
+
context.success = true
|
|
276
|
+
context.record_exception(e)
|
|
277
|
+
raise
|
|
278
|
+
rescue StandardError => e
|
|
279
|
+
context.success = false
|
|
280
|
+
context.record_exception(e)
|
|
281
|
+
raise
|
|
282
|
+
end
|
|
283
|
+
ensure
|
|
284
|
+
submit_failover_copy(telemetry_context) if telemetry_context && @telemetry_failover_additional_top_trace
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
# Runs while the failover's outcome is propagating, so an error here must not replace it.
|
|
288
|
+
def submit_failover_copy(telemetry_context)
|
|
289
|
+
@telemetry_factory.post_copy(telemetry_context, Utils::Telemetry::TraceLevel::FORCE_TOP_LEVEL)
|
|
290
|
+
rescue StandardError => e
|
|
291
|
+
logger.debug { "Unable to submit the failover trace as a trace of its own: #{e.message}" }
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
# Counts a failover as triggered, and then as succeeded or failed according to how it ends.
|
|
295
|
+
def count_failover(counters)
|
|
296
|
+
counters.triggered.inc
|
|
297
|
+
begin
|
|
298
|
+
yield
|
|
299
|
+
rescue *SUCCESSFUL_FAILOVER_ERRORS
|
|
300
|
+
counters.succeeded.inc
|
|
301
|
+
raise
|
|
302
|
+
rescue StandardError
|
|
303
|
+
counters.failed.inc
|
|
304
|
+
raise
|
|
241
305
|
end
|
|
242
306
|
end
|
|
243
307
|
|
|
@@ -414,7 +478,8 @@ module AwsAdvancedRubyDriverWrapper
|
|
|
414
478
|
Host::HostRole::READER,
|
|
415
479
|
@reader_selector_strategy
|
|
416
480
|
)
|
|
417
|
-
rescue StandardError
|
|
481
|
+
rescue StandardError => e
|
|
482
|
+
logger.warn("Failed to select a reader using the '#{@reader_selector_strategy}' host selection strategy: #{e.message}")
|
|
418
483
|
nil
|
|
419
484
|
end
|
|
420
485
|
|
|
@@ -0,0 +1,266 @@
|
|
|
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 '../../property_definition'
|
|
20
|
+
require_relative '../iam_auth_plugin'
|
|
21
|
+
require_relative '../../utils/iam_auth_utils'
|
|
22
|
+
require_relative '../../utils/rds_utils'
|
|
23
|
+
require_relative '../../utils/rds_url_type'
|
|
24
|
+
require_relative '../../utils/saml_utils'
|
|
25
|
+
require_relative '../../utils/telemetry/telemetry_const'
|
|
26
|
+
|
|
27
|
+
module AwsAdvancedRubyDriverWrapper
|
|
28
|
+
module Plugins
|
|
29
|
+
module FederatedAuth
|
|
30
|
+
# Shared connect flow for the SAML-based authentication plugins (AD FS and
|
|
31
|
+
# Okta).
|
|
32
|
+
#
|
|
33
|
+
# A subclass defines +self.fetch_saml_assertion+, +TELEMETRY_PREFIX+ (the prefix of its metric
|
|
34
|
+
# names) and +TELEMETRY_FETCH_SAML+ (the name of the span its SAML assertion fetch is recorded as).
|
|
35
|
+
class BaseSamlAuthPlugin
|
|
36
|
+
SUBSCRIBED_METHODS = Set['connect', 'internal_connect'].freeze
|
|
37
|
+
# Reuse the IAM plugin's cache name.
|
|
38
|
+
IAM_TOKEN_CACHE_NAME = AwsAdvancedRubyDriverWrapper::Plugins::IamAuthPlugin::IAM_TOKEN_CACHE_NAME
|
|
39
|
+
|
|
40
|
+
attr_reader :subscribed_methods
|
|
41
|
+
|
|
42
|
+
def initialize(service_container, props = ::Concurrent::Map.new)
|
|
43
|
+
ensure_aws_sdk!
|
|
44
|
+
@service_container = service_container
|
|
45
|
+
@wrapper_props = props
|
|
46
|
+
expiration = PropertyDefinition::IAM_EXPIRATION_SEC.get_float(props)
|
|
47
|
+
PropertyDefinition::IAM_EXPIRATION_SEC.validate!(expiration)
|
|
48
|
+
service_container.storage_service.register(IAM_TOKEN_CACHE_NAME, ttl: expiration)
|
|
49
|
+
@subscribed_methods = SUBSCRIBED_METHODS
|
|
50
|
+
|
|
51
|
+
# Each subclass names its metrics after itself, and its SAML assertion span after its IdP.
|
|
52
|
+
@telemetry_factory = service_container.telemetry_factory
|
|
53
|
+
@fetch_token_counter = @telemetry_factory.create_counter("#{self.class::TELEMETRY_PREFIX}.fetch_token.count")
|
|
54
|
+
@telemetry_factory.create_gauge(
|
|
55
|
+
"#{self.class::TELEMETRY_PREFIX}.token_cache.size",
|
|
56
|
+
&IamAuthPlugin.token_cache_size(service_container.storage_service)
|
|
57
|
+
)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def connect(host_info, driver_props, _is_initial_connection, pipeline_callable)
|
|
61
|
+
saml_connect(host_info, driver_props, pipeline_callable)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def internal_connect(host_info, driver_props, wrapper_props_override, _is_initial_connection, pipeline_callable)
|
|
65
|
+
saml_connect(host_info, driver_props, pipeline_callable, wrapper_props_override || @wrapper_props)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def self.clear_cache(storage_service)
|
|
69
|
+
storage_service.clear(IAM_TOKEN_CACHE_NAME)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Exchanges a SAML assertion for temporary AWS credentials via STS
|
|
73
|
+
# AssumeRoleWithSAML.
|
|
74
|
+
#
|
|
75
|
+
# @return [Aws::Credentials]
|
|
76
|
+
def self.credentials_from_saml(saml_assertion, role_arn:, idp_arn:, region:)
|
|
77
|
+
require 'aws-sdk-core'
|
|
78
|
+
# AssumeRoleWithSAML is unsigned, so no AWS credentials are needed. Passing
|
|
79
|
+
# credentials: nil keeps the SDK from running the default credential chain (which
|
|
80
|
+
# on a credential-less, non-EC2 host wastes ~1s probing IMDS and logs a misleading
|
|
81
|
+
# "Error retrieving instance profile credentials").
|
|
82
|
+
client_opts = { credentials: nil }
|
|
83
|
+
client_opts[:region] = region if region && !region.to_s.empty?
|
|
84
|
+
resp = Aws::STS::Client.new(**client_opts).assume_role_with_saml(
|
|
85
|
+
role_arn:, principal_arn: idp_arn, saml_assertion:
|
|
86
|
+
)
|
|
87
|
+
creds = resp.credentials
|
|
88
|
+
Aws::Credentials.new(creds.access_key_id, creds.secret_access_key, creds.session_token)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
private
|
|
92
|
+
|
|
93
|
+
def saml_connect(host_info, driver_props, pipeline_callable, wrapper_props_override = nil)
|
|
94
|
+
wrapper_props_override ||= @wrapper_props
|
|
95
|
+
saml_props = wrapper_props_override
|
|
96
|
+
|
|
97
|
+
validate_required!(saml_props)
|
|
98
|
+
|
|
99
|
+
# The connection's driver_props :user/:password double as the fallback IdP login when
|
|
100
|
+
# idp_username / idp_password are not set. We overwrite driver_props with db_user and the
|
|
101
|
+
# RDS token further down, and that hash is reused across reconnects (failover, retry,
|
|
102
|
+
# Blue/Green rerouting), so capture the fallback into the wrapper props once, up front.
|
|
103
|
+
# A later reconnect then still logs in to the IdP with the original credentials rather
|
|
104
|
+
# than db_user and a stale token.
|
|
105
|
+
persist_idp_credentials_fallback(saml_props, driver_props)
|
|
106
|
+
db_user = PropertyDefinition::DB_USER.get(saml_props)
|
|
107
|
+
idp_username = PropertyDefinition::IDP_USERNAME.get(saml_props)
|
|
108
|
+
idp_password = PropertyDefinition::IDP_PASSWORD.get(saml_props)
|
|
109
|
+
|
|
110
|
+
host = Utils::IamAuthUtils.resolve_host(
|
|
111
|
+
PropertyDefinition::IAM_HOST.get(wrapper_props_override), host_info
|
|
112
|
+
)
|
|
113
|
+
port = Utils::IamAuthUtils.resolve_port(
|
|
114
|
+
PropertyDefinition::IAM_PORT.get(wrapper_props_override),
|
|
115
|
+
host_info,
|
|
116
|
+
@service_container.dialect_service.db_dialect.default_port
|
|
117
|
+
)
|
|
118
|
+
rds_type = Utils::RdsUtils.identify_rds_type(host)
|
|
119
|
+
|
|
120
|
+
# Only a global writer cluster with no explicit iam_region needs AWS credentials to look
|
|
121
|
+
# its region up. In that one case the SAML exchange has to happen before the region lookup
|
|
122
|
+
# (and so before the token-cache check); every other case - including a global cluster
|
|
123
|
+
# whose iam_region is set - defers the IdP round-trip to the token-cache miss path, so a
|
|
124
|
+
# pool or failover storm does not trigger one IdP login per connection.
|
|
125
|
+
explicit_region = PropertyDefinition::IAM_REGION.get(wrapper_props_override)
|
|
126
|
+
preassumed_credentials = nil
|
|
127
|
+
if rds_type == Utils::RdsUrlType::RDS_GLOBAL_WRITER_CLUSTER && (explicit_region.nil? || explicit_region.to_s.empty?)
|
|
128
|
+
preassumed_credentials = obtain_credentials(saml_props, nil, idp_username, idp_password)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
region = Utils::IamAuthUtils.region_for(
|
|
132
|
+
host:, props: wrapper_props_override, rds_type:,
|
|
133
|
+
credentials_provider: preassumed_credentials,
|
|
134
|
+
rds_client_func: preassumed_credentials ? -> { Aws::RDS::Client.new(credentials: preassumed_credentials) } : nil
|
|
135
|
+
)
|
|
136
|
+
unless region
|
|
137
|
+
raise Errors::SamlAuthError,
|
|
138
|
+
"#{self.class.name}: unable to determine connection region. " \
|
|
139
|
+
"If you are using a non-standard RDS URL, please set the 'iam_region' property."
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
fingerprint = Utils::SamlUtils.principal_fingerprint(
|
|
143
|
+
PropertyDefinition::IAM_ROLE_ARN.get(saml_props),
|
|
144
|
+
PropertyDefinition::IAM_IDP_ARN.get(saml_props)
|
|
145
|
+
)
|
|
146
|
+
cache_key = Utils::IamAuthUtils.cache_key(region, host, port, db_user, fingerprint)
|
|
147
|
+
entry = @service_container.storage_service.get(IAM_TOKEN_CACHE_NAME, cache_key)
|
|
148
|
+
expiration = PropertyDefinition::IAM_EXPIRATION_SEC.get_float(wrapper_props_override)
|
|
149
|
+
PropertyDefinition::IAM_EXPIRATION_SEC.validate!(expiration)
|
|
150
|
+
|
|
151
|
+
if Utils::IamAuthUtils.valid_entry?(entry)
|
|
152
|
+
token = entry.token
|
|
153
|
+
is_cached_token = true
|
|
154
|
+
else
|
|
155
|
+
token = fetch_and_cache_token(saml_props, region, host, port, db_user, cache_key, expiration,
|
|
156
|
+
idp_username, idp_password, preassumed_credentials)
|
|
157
|
+
is_cached_token = false
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
user_key = @service_container.dialect_service.driver_dialect.user_property_key
|
|
161
|
+
driver_props[user_key] = db_user
|
|
162
|
+
driver_props[:password] = token
|
|
163
|
+
|
|
164
|
+
begin
|
|
165
|
+
pipeline_callable.call
|
|
166
|
+
rescue StandardError => e
|
|
167
|
+
raise unless is_cached_token && @service_container.dialect_service.login_error?(e)
|
|
168
|
+
|
|
169
|
+
driver_props[:password] = fetch_and_cache_token(saml_props, region, host, port, db_user, cache_key,
|
|
170
|
+
expiration, idp_username, idp_password, nil)
|
|
171
|
+
pipeline_callable.call
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Mints a fresh RDS token signed with SAML-derived credentials and caches it.
|
|
176
|
+
def fetch_and_cache_token(saml_props, region, host, port, db_user, cache_key, expiration,
|
|
177
|
+
idp_username, idp_password, credentials)
|
|
178
|
+
credentials ||= obtain_credentials(saml_props, region, idp_username, idp_password)
|
|
179
|
+
@fetch_token_counter.inc
|
|
180
|
+
token = Utils::IamAuthUtils.generate_token(
|
|
181
|
+
region:, hostname: host, port:, user: db_user, credentials_provider: credentials,
|
|
182
|
+
telemetry_factory: @telemetry_factory
|
|
183
|
+
)
|
|
184
|
+
@service_container.storage_service.set(
|
|
185
|
+
IAM_TOKEN_CACHE_NAME, cache_key, Utils::IamAuthUtils.build_token_entry(token, expiration)
|
|
186
|
+
)
|
|
187
|
+
token
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# Captures the connection's user/password as the IdP login when idp_username /
|
|
191
|
+
# idp_password are unset, persisting them into the wrapper props so the value survives
|
|
192
|
+
# the driver_props overwrite later in the flow and any subsequent reconnect. Never stores
|
|
193
|
+
# a nil (Concurrent::Map rejects nil values), so a connection with no fallback simply
|
|
194
|
+
# keeps idp_username / idp_password unset.
|
|
195
|
+
def persist_idp_credentials_fallback(saml_props, driver_props)
|
|
196
|
+
if PropertyDefinition::IDP_USERNAME.get(saml_props).nil?
|
|
197
|
+
fallback_user = driver_props[:user] || driver_props[:username]
|
|
198
|
+
PropertyDefinition::IDP_USERNAME.set(saml_props, fallback_user) unless fallback_user.nil?
|
|
199
|
+
end
|
|
200
|
+
return unless PropertyDefinition::IDP_PASSWORD.get(saml_props).nil?
|
|
201
|
+
|
|
202
|
+
fallback_password = driver_props[:password]
|
|
203
|
+
PropertyDefinition::IDP_PASSWORD.set(saml_props, fallback_password) unless fallback_password.nil?
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Fetches a SAML assertion from the IdP and exchanges it for AWS credentials.
|
|
207
|
+
def obtain_credentials(saml_props, region, idp_username, idp_password)
|
|
208
|
+
assertion = @telemetry_factory.in_context(self.class::TELEMETRY_FETCH_SAML, Utils::Telemetry::TraceLevel::NESTED,
|
|
209
|
+
record_failure: true) do
|
|
210
|
+
self.class.fetch_saml_assertion(**saml_fetch_options(saml_props, idp_username, idp_password))
|
|
211
|
+
end
|
|
212
|
+
self.class.credentials_from_saml(
|
|
213
|
+
assertion,
|
|
214
|
+
role_arn: PropertyDefinition::IAM_ROLE_ARN.get(saml_props),
|
|
215
|
+
idp_arn: PropertyDefinition::IAM_IDP_ARN.get(saml_props),
|
|
216
|
+
region:
|
|
217
|
+
)
|
|
218
|
+
rescue Errors::SamlAuthError
|
|
219
|
+
raise
|
|
220
|
+
rescue StandardError => e
|
|
221
|
+
# Surface IdP/STS failures (DNS, TLS, STS errors) as a SamlAuthError.
|
|
222
|
+
raise Errors::SamlAuthError, "#{self.class.name}: failed to obtain credentials via SAML: #{e.message}"
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def validate_required!(saml_props)
|
|
226
|
+
missing = required_identity(saml_props).select { |_name, value| value.nil? || value.to_s.empty? }
|
|
227
|
+
return if missing.empty?
|
|
228
|
+
|
|
229
|
+
names = missing.map(&:first)
|
|
230
|
+
raise Errors::SamlAuthError,
|
|
231
|
+
"#{self.class.name}: #{names.join(', ')} #{names.length > 1 ? 'are' : 'is'} required"
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
# The properties that must be present for this IdP. Subclasses override to
|
|
235
|
+
# append their own (e.g. Okta's app_id).
|
|
236
|
+
def required_identity(saml_props)
|
|
237
|
+
[
|
|
238
|
+
['db_user', PropertyDefinition::DB_USER.get(saml_props)],
|
|
239
|
+
['idp_endpoint', PropertyDefinition::IDP_ENDPOINT.get(saml_props)],
|
|
240
|
+
['iam_role_arn', PropertyDefinition::IAM_ROLE_ARN.get(saml_props)],
|
|
241
|
+
['iam_idp_arn', PropertyDefinition::IAM_IDP_ARN.get(saml_props)]
|
|
242
|
+
]
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
# The keyword arguments passed to .fetch_saml_assertion. Subclasses override.
|
|
246
|
+
def saml_fetch_options(saml_props, idp_username, idp_password)
|
|
247
|
+
{
|
|
248
|
+
idp_endpoint: PropertyDefinition::IDP_ENDPOINT.get(saml_props),
|
|
249
|
+
idp_username:,
|
|
250
|
+
idp_password:,
|
|
251
|
+
ssl_insecure: PropertyDefinition::SSL_INSECURE.get_bool(saml_props),
|
|
252
|
+
socket_timeout_sec: PropertyDefinition::HTTP_CLIENT_SOCKET_TIMEOUT_SEC.get_float(saml_props),
|
|
253
|
+
connect_timeout_sec: PropertyDefinition::HTTP_CLIENT_CONNECT_TIMEOUT_SEC.get_float(saml_props)
|
|
254
|
+
}
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def ensure_aws_sdk!
|
|
258
|
+
require 'aws-sdk-rds'
|
|
259
|
+
rescue LoadError
|
|
260
|
+
raise LoadError,
|
|
261
|
+
"The SAML auth plugins require 'aws-sdk-rds'. Add it to your Gemfile: gem 'aws-sdk-rds'"
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
end
|
|
@@ -0,0 +1,134 @@
|
|
|
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 'cgi/escape'
|
|
18
|
+
require 'uri'
|
|
19
|
+
require_relative 'base_saml_auth_plugin'
|
|
20
|
+
|
|
21
|
+
module AwsAdvancedRubyDriverWrapper
|
|
22
|
+
module Plugins
|
|
23
|
+
module FederatedAuth
|
|
24
|
+
# Federated authentication against Active Directory Federation Services
|
|
25
|
+
# (AD FS). Scrapes the IdP-initiated sign-on page for its form, posts the
|
|
26
|
+
# federated credentials, and extracts the SAML assertion from the response.
|
|
27
|
+
# Everything else (STS exchange, token minting, caching, retry) is handled by
|
|
28
|
+
# BaseSamlAuthPlugin.
|
|
29
|
+
class FederatedAuthPlugin < BaseSamlAuthPlugin
|
|
30
|
+
TELEMETRY_PREFIX = 'federated'
|
|
31
|
+
TELEMETRY_FETCH_SAML = 'Fetch ADFS SAML Assertion'
|
|
32
|
+
INPUT_TAG_PATTERN = /<input([^<>]*)>/
|
|
33
|
+
FORM_ACTION_PATTERN = /<form[^<>]*?\baction="([^"]+)"/
|
|
34
|
+
|
|
35
|
+
# Fetches a SAML assertion from AD FS.
|
|
36
|
+
#
|
|
37
|
+
# @return [String] the base64 SAML assertion
|
|
38
|
+
def self.fetch_saml_assertion(idp_endpoint:, idp_port:, idp_username:, idp_password:,
|
|
39
|
+
rp_identifier:, ssl_insecure:, socket_timeout_sec:, connect_timeout_sec:, **_rest)
|
|
40
|
+
# One cookie jar shared by the sign-on GET and the credential POST, so cookies AD FS
|
|
41
|
+
# (or a load balancer in front of it) sets on the sign-on page are returned with the POST.
|
|
42
|
+
cookies = {}
|
|
43
|
+
http = lambda do |method, url, body: nil, headers: {}|
|
|
44
|
+
Utils::SamlUtils.http_request(
|
|
45
|
+
method:, url:, body:, headers:, ssl_insecure:, cookies:,
|
|
46
|
+
open_timeout_sec: connect_timeout_sec, read_timeout_sec: socket_timeout_sec
|
|
47
|
+
)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
sign_in_url = "https://#{idp_endpoint}:#{idp_port}/adfs/ls/IdpInitiatedSignOn.aspx?loginToRp=#{rp_identifier}"
|
|
51
|
+
sign_in_body = require_body!(http.call(:get, sign_in_url), 'sign-on page')
|
|
52
|
+
|
|
53
|
+
action = form_action(sign_in_body)
|
|
54
|
+
post_url = action&.start_with?('/') ? "https://#{idp_endpoint}:#{idp_port}#{action}" : sign_in_url
|
|
55
|
+
|
|
56
|
+
params = form_parameters(sign_in_body, idp_username, idp_password)
|
|
57
|
+
response = http.call(:post, post_url,
|
|
58
|
+
body: URI.encode_www_form(params),
|
|
59
|
+
headers: { 'Content-Type' => 'application/x-www-form-urlencoded' })
|
|
60
|
+
content = require_body!(response, 'sign-on POST')
|
|
61
|
+
|
|
62
|
+
match = Utils::SamlUtils::SAML_RESPONSE_PATTERN.match(content)
|
|
63
|
+
unless match
|
|
64
|
+
# AD FS answers a rejected login with the sign-on page again (no SAMLResponse); the
|
|
65
|
+
# body is not included as it may contain sensitive markup.
|
|
66
|
+
raise Errors::SamlAuthError,
|
|
67
|
+
'FederatedAuthPlugin: no SAML assertion in the AD FS response - check idp_username, ' \
|
|
68
|
+
'idp_password, and idp_endpoint'
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
match[1]
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def self.require_body!(response, what)
|
|
75
|
+
return response.body if response.is_a?(Net::HTTPSuccess)
|
|
76
|
+
|
|
77
|
+
raise Errors::SamlAuthError,
|
|
78
|
+
"FederatedAuthPlugin: AD FS #{what} request failed (#{response.code} #{response.message})"
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def self.form_action(body)
|
|
82
|
+
match = FORM_ACTION_PATTERN.match(body)
|
|
83
|
+
match && unescape_html(match[1])
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Builds the POST parameters from the sign-on form's input tags, filling in
|
|
87
|
+
# the federated username/password and preserving every other hidden field.
|
|
88
|
+
def self.form_parameters(body, idp_username, idp_password)
|
|
89
|
+
params = []
|
|
90
|
+
seen = {}
|
|
91
|
+
body.scan(INPUT_TAG_PATTERN).flatten.each do |tag|
|
|
92
|
+
name = value_by_key(tag, 'name')
|
|
93
|
+
next if name.empty?
|
|
94
|
+
|
|
95
|
+
lower = name.downcase
|
|
96
|
+
next if seen[lower]
|
|
97
|
+
|
|
98
|
+
seen[lower] = true
|
|
99
|
+
value = value_by_key(tag, 'value')
|
|
100
|
+
|
|
101
|
+
if lower.include?('username')
|
|
102
|
+
params << [name, idp_username]
|
|
103
|
+
elsif lower.include?('authmethod')
|
|
104
|
+
params << [name, value] unless value.empty?
|
|
105
|
+
elsif lower.include?('password')
|
|
106
|
+
params << [name, idp_password]
|
|
107
|
+
else
|
|
108
|
+
params << [name, value]
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
params
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def self.value_by_key(input, key)
|
|
115
|
+
match = /#{Regexp.quote(key)}\s*=\s*"(.*?)"/.match(input)
|
|
116
|
+
match ? unescape_html(match[1]) : ''
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def self.unescape_html(html)
|
|
120
|
+
CGI.unescapeHTML(html)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
private
|
|
124
|
+
|
|
125
|
+
def saml_fetch_options(saml_props, idp_username, idp_password)
|
|
126
|
+
super.merge(
|
|
127
|
+
idp_port: PropertyDefinition::IDP_PORT.get(saml_props),
|
|
128
|
+
rp_identifier: PropertyDefinition::RP_IDENTIFIER.get(saml_props)
|
|
129
|
+
)
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
@@ -0,0 +1,113 @@
|
|
|
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 'cgi/escape'
|
|
18
|
+
require 'json'
|
|
19
|
+
require_relative 'base_saml_auth_plugin'
|
|
20
|
+
|
|
21
|
+
module AwsAdvancedRubyDriverWrapper
|
|
22
|
+
module Plugins
|
|
23
|
+
module FederatedAuth
|
|
24
|
+
# Federated authentication against Okta. Authenticates to Okta's /authn API
|
|
25
|
+
# for a one-time session token, redeems it at the AWS app's SSO SAML endpoint
|
|
26
|
+
# for a SAML assertion, and then defers to BaseSamlAuthPlugin for the STS
|
|
27
|
+
# exchange, token minting, caching, and retry. Okta differs from AD FS only in
|
|
28
|
+
# configuration (it uses app_id, the AWS app configured on Okta) and in the
|
|
29
|
+
# shape of the IdP HTTP flow.
|
|
30
|
+
class OktaAuthPlugin < AwsAdvancedRubyDriverWrapper::Plugins::FederatedAuth::BaseSamlAuthPlugin
|
|
31
|
+
TELEMETRY_PREFIX = 'okta'
|
|
32
|
+
TELEMETRY_FETCH_SAML = 'Fetch OKTA SAML Assertion'
|
|
33
|
+
OKTA_AWS_APP_NAME = 'amazon_aws'
|
|
34
|
+
# Extracts the value attribute of the hidden SAMLResponse input regardless
|
|
35
|
+
# of attribute order.
|
|
36
|
+
SAML_INPUT_PATTERN = /<input[^>]*\bname="SAMLResponse"[^>]*>/i
|
|
37
|
+
VALUE_ATTR_PATTERN = /\bvalue="([^"]+)"/i
|
|
38
|
+
|
|
39
|
+
# Fetches a SAML assertion from Okta.
|
|
40
|
+
#
|
|
41
|
+
# @return [String] the base64 SAML assertion
|
|
42
|
+
def self.fetch_saml_assertion(idp_endpoint:, app_id:, idp_username:, idp_password:,
|
|
43
|
+
ssl_insecure:, socket_timeout_sec:, connect_timeout_sec:, **_rest)
|
|
44
|
+
http = lambda do |method, url, body: nil, headers: {}|
|
|
45
|
+
Utils::SamlUtils.http_request(
|
|
46
|
+
method:, url:, body:, headers:, ssl_insecure:,
|
|
47
|
+
open_timeout_sec: connect_timeout_sec, read_timeout_sec: socket_timeout_sec
|
|
48
|
+
)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
session_token = session_token(http, idp_endpoint, idp_username, idp_password)
|
|
52
|
+
saml_url = "https://#{idp_endpoint}/app/#{OKTA_AWS_APP_NAME}/#{CGI.escape(app_id)}/sso/saml" \
|
|
53
|
+
"?onetimetoken=#{CGI.escape(session_token)}"
|
|
54
|
+
response = http.call(:get, saml_url)
|
|
55
|
+
unless response.is_a?(Net::HTTPSuccess)
|
|
56
|
+
raise Errors::SamlAuthError,
|
|
57
|
+
"OktaAuthPlugin: SAML request failed (#{response.code} #{response.message})"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
tag = SAML_INPUT_PATTERN.match(response.body)
|
|
61
|
+
value = tag && VALUE_ATTR_PATTERN.match(tag[0])
|
|
62
|
+
raise Errors::SamlAuthError, 'OktaAuthPlugin: invalid SAML response from Okta' unless value
|
|
63
|
+
|
|
64
|
+
# The value attribute is HTML-entity-encoded (Okta encodes base64's trailing
|
|
65
|
+
# "=" padding, and "+" if present, as e.g. = / +); decode before
|
|
66
|
+
# handing the assertion to STS, matching FederatedAuthPlugin's AD FS flow.
|
|
67
|
+
CGI.unescapeHTML(value[1])
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Authenticates to Okta's /authn API and returns the one-time session token.
|
|
71
|
+
def self.session_token(http, idp_endpoint, idp_username, idp_password)
|
|
72
|
+
response = http.call(
|
|
73
|
+
:post, "https://#{idp_endpoint}/api/v1/authn",
|
|
74
|
+
body: JSON.generate(username: idp_username, password: idp_password),
|
|
75
|
+
headers: { 'Accept' => 'application/json', 'Content-Type' => 'application/json' }
|
|
76
|
+
)
|
|
77
|
+
unless response.is_a?(Net::HTTPSuccess)
|
|
78
|
+
raise Errors::SamlAuthError,
|
|
79
|
+
"OktaAuthPlugin: Okta session token request failed (#{response.code} #{response.message})"
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
parsed = begin
|
|
83
|
+
JSON.parse(response.body)
|
|
84
|
+
rescue JSON::ParserError
|
|
85
|
+
nil
|
|
86
|
+
end
|
|
87
|
+
parsed = nil unless parsed.is_a?(Hash)
|
|
88
|
+
token = parsed && parsed['sessionToken']
|
|
89
|
+
if token.nil? || token.empty?
|
|
90
|
+
# Okta's `status` is a non-sensitive auth-state enum (e.g. MFA_REQUIRED,
|
|
91
|
+
# PASSWORD_EXPIRED, LOCKED_OUT); surface it for clarity. It never carries
|
|
92
|
+
# credentials or the session token itself.
|
|
93
|
+
status = parsed && parsed['status']
|
|
94
|
+
detail = status.is_a?(String) && !status.empty? ? " (status: #{status})" : ''
|
|
95
|
+
raise Errors::SamlAuthError, "OktaAuthPlugin: Okta did not return a session token#{detail}"
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
token
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
private
|
|
102
|
+
|
|
103
|
+
def required_identity(saml_props)
|
|
104
|
+
super + [['app_id', PropertyDefinition::APP_ID.get(saml_props)]]
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def saml_fetch_options(saml_props, idp_username, idp_password)
|
|
108
|
+
super.merge(app_id: PropertyDefinition::APP_ID.get(saml_props))
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|