aws-sdk-core 3.131.0 → 3.158.1
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 +177 -1
- data/VERSION +1 -1
- data/lib/aws-sdk-core/errors.rb +13 -0
- data/lib/aws-sdk-core/pageable_response.rb +7 -0
- data/lib/aws-sdk-core/plugins/bearer_authorization.rb +67 -0
- data/lib/aws-sdk-core/plugins/jsonvalue_converter.rb +34 -6
- data/lib/aws-sdk-core/plugins/recursion_detection.rb +14 -3
- data/lib/aws-sdk-core/plugins/signature_v4.rb +12 -7
- data/lib/aws-sdk-core/refreshing_token.rb +71 -0
- data/lib/aws-sdk-core/rest/handler.rb +1 -1
- data/lib/aws-sdk-core/shared_config.rb +43 -0
- data/lib/aws-sdk-core/sso_token_provider.rb +134 -0
- data/lib/aws-sdk-core/static_token_provider.rb +14 -0
- data/lib/aws-sdk-core/structure.rb +6 -4
- data/lib/aws-sdk-core/token.rb +31 -0
- data/lib/aws-sdk-core/token_provider.rb +15 -0
- data/lib/aws-sdk-core/token_provider_chain.rb +51 -0
- data/lib/aws-sdk-core/xml/error_handler.rb +7 -0
- data/lib/aws-sdk-core.rb +10 -0
- data/lib/aws-sdk-sso/client.rb +32 -9
- data/lib/aws-sdk-sso/types.rb +8 -8
- data/lib/aws-sdk-sso.rb +1 -1
- data/lib/aws-sdk-ssooidc/client.rb +589 -0
- data/lib/aws-sdk-ssooidc/client_api.rb +216 -0
- data/lib/aws-sdk-ssooidc/customizations.rb +1 -0
- data/lib/aws-sdk-ssooidc/errors.rb +290 -0
- data/lib/aws-sdk-ssooidc/resource.rb +26 -0
- data/lib/aws-sdk-ssooidc/types.rb +534 -0
- data/lib/aws-sdk-ssooidc.rb +55 -0
- data/lib/aws-sdk-sts/client.rb +1 -1
- data/lib/aws-sdk-sts.rb +1 -1
- data/lib/seahorse/client/plugins/request_callback.rb +9 -9
- metadata +24 -4
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Aws
|
|
4
|
+
class SSOTokenProvider
|
|
5
|
+
|
|
6
|
+
include TokenProvider
|
|
7
|
+
include RefreshingToken
|
|
8
|
+
|
|
9
|
+
# @api private
|
|
10
|
+
SSO_REQUIRED_OPTS = [:sso_region, :sso_session].freeze
|
|
11
|
+
|
|
12
|
+
# @api private
|
|
13
|
+
SSO_LOGIN_GUIDANCE = 'The SSO session associated with this profile has '\
|
|
14
|
+
'expired or is otherwise invalid. To refresh this SSO session run '\
|
|
15
|
+
'aws sso login with the corresponding profile.'.freeze
|
|
16
|
+
|
|
17
|
+
# @option options [required, String] :sso_region The AWS region where the
|
|
18
|
+
# SSO directory for the given sso_start_url is hosted.
|
|
19
|
+
#
|
|
20
|
+
# @option options [required, String] :sso_session The SSO Session used to
|
|
21
|
+
# for fetching this token.
|
|
22
|
+
#
|
|
23
|
+
# @option options [SSOOIDC::Client] :client Optional `SSOOIDC::Client`. If not
|
|
24
|
+
# provided, a client will be constructed.
|
|
25
|
+
#
|
|
26
|
+
# @option options [Callable] before_refresh Proc called before
|
|
27
|
+
# credentials are refreshed. `before_refresh` is called
|
|
28
|
+
# with an instance of this object when
|
|
29
|
+
# AWS credentials are required and need to be refreshed.
|
|
30
|
+
def initialize(options = {})
|
|
31
|
+
|
|
32
|
+
missing_keys = SSO_REQUIRED_OPTS.select { |k| options[k].nil? }
|
|
33
|
+
unless missing_keys.empty?
|
|
34
|
+
raise ArgumentError, "Missing required keys: #{missing_keys}"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
@sso_session = options.delete(:sso_session)
|
|
38
|
+
@sso_region = options.delete(:sso_region)
|
|
39
|
+
|
|
40
|
+
options[:region] = @sso_region
|
|
41
|
+
options[:credentials] = nil
|
|
42
|
+
@client = options[:client] || Aws::SSOOIDC::Client.new(options)
|
|
43
|
+
|
|
44
|
+
super
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# @return [SSO::Client]
|
|
48
|
+
attr_reader :client
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def refresh
|
|
53
|
+
# token is valid and not in refresh window - do not refresh it.
|
|
54
|
+
return if @token && @token.expiration && !near_expiration?
|
|
55
|
+
|
|
56
|
+
# token may not exist or is out of the expiration window
|
|
57
|
+
# attempt to refresh from disk first (another process/application may have refreshed already)
|
|
58
|
+
token_json = read_cached_token
|
|
59
|
+
@token = Token.new(token_json['accessToken'], token_json['expiresAt'])
|
|
60
|
+
return if @token && @token.expiration && !near_expiration?
|
|
61
|
+
|
|
62
|
+
# The token is expired and needs to be refreshed
|
|
63
|
+
if can_refresh_token?(token_json)
|
|
64
|
+
begin
|
|
65
|
+
current_time = Time.now
|
|
66
|
+
resp = @client.create_token(
|
|
67
|
+
grant_type: 'refresh_token',
|
|
68
|
+
client_id: token_json['clientId'],
|
|
69
|
+
client_secret: token_json['client_secret'],
|
|
70
|
+
refresh_token: token_json['refreshToken']
|
|
71
|
+
)
|
|
72
|
+
token_json['accessToken'] = resp.access_token
|
|
73
|
+
token_json['expiresAt'] = current_time + resp.expires_in
|
|
74
|
+
@token = Token.new(token_json['accessToken'], token_json['expiresAt'])
|
|
75
|
+
|
|
76
|
+
if resp.refresh_token
|
|
77
|
+
token_json['refreshToken'] = resp.refresh_token
|
|
78
|
+
else
|
|
79
|
+
token_json.delete('refreshToken')
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
update_token_cache(token_json)
|
|
83
|
+
rescue
|
|
84
|
+
# refresh has failed, continue attempting to use the token if its not hard expired
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
if !@token.expiration || @token.expiration < Time.now
|
|
89
|
+
# Token is hard expired, raise an exception
|
|
90
|
+
raise Errors::InvalidSSOToken, 'Token is invalid and failed to refresh.'
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def read_cached_token
|
|
95
|
+
cached_token = Json.load(File.read(sso_cache_file))
|
|
96
|
+
# validation
|
|
97
|
+
unless cached_token['accessToken'] && cached_token['expiresAt']
|
|
98
|
+
raise ArgumentError, 'Missing required field(s)'
|
|
99
|
+
end
|
|
100
|
+
cached_token['expiresAt'] = Time.parse(cached_token['expiresAt'])
|
|
101
|
+
cached_token
|
|
102
|
+
rescue Errno::ENOENT, Aws::Json::ParseError, ArgumentError
|
|
103
|
+
raise Errors::InvalidSSOToken, SSO_LOGIN_GUIDANCE
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def update_token_cache(token_json)
|
|
107
|
+
cached_token = token_json.dup
|
|
108
|
+
cached_token['expiresAt'] = cached_token['expiresAt'].iso8601
|
|
109
|
+
File.write(sso_cache_file, Json.dump(cached_token))
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def sso_cache_file
|
|
113
|
+
sso_session_sha1 = OpenSSL::Digest::SHA1.hexdigest(@sso_session.encode('utf-8'))
|
|
114
|
+
File.join(Dir.home, '.aws', 'sso', 'cache', "#{sso_session_sha1}.json")
|
|
115
|
+
rescue ArgumentError
|
|
116
|
+
# Dir.home raises ArgumentError when ENV['home'] is not set
|
|
117
|
+
raise ArgumentError, "Unable to load sso_cache_file: ENV['HOME'] is not set."
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# return true if all required fields are present
|
|
121
|
+
# return false if registrationExpiresAt exists and is later than now
|
|
122
|
+
def can_refresh_token?(token_json)
|
|
123
|
+
if token_json['clientId'] &&
|
|
124
|
+
token_json['clientSecret'] &&
|
|
125
|
+
token_json['refreshToken']
|
|
126
|
+
|
|
127
|
+
return !token_json['registrationExpiresAt'] ||
|
|
128
|
+
Time.parse(token_json['registrationExpiresAt']) > Time.now
|
|
129
|
+
else
|
|
130
|
+
false
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Aws
|
|
4
|
+
class StaticTokenProvider
|
|
5
|
+
|
|
6
|
+
include TokenProvider
|
|
7
|
+
|
|
8
|
+
# @param [String] token
|
|
9
|
+
# @param [Time] expiration
|
|
10
|
+
def initialize(token, expiration=nil)
|
|
11
|
+
@token = Token.new(token, expiration)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -28,18 +28,20 @@ module Aws
|
|
|
28
28
|
# in stdlib Struct.
|
|
29
29
|
#
|
|
30
30
|
# @return [Hash]
|
|
31
|
-
def to_h(obj = self)
|
|
31
|
+
def to_h(obj = self, options = {})
|
|
32
32
|
case obj
|
|
33
33
|
when Struct
|
|
34
34
|
obj.each_pair.with_object({}) do |(member, value), hash|
|
|
35
|
-
|
|
35
|
+
member = member.to_s if options[:as_json]
|
|
36
|
+
hash[member] = to_hash(value, options) unless value.nil?
|
|
36
37
|
end
|
|
37
38
|
when Hash
|
|
38
39
|
obj.each.with_object({}) do |(key, value), hash|
|
|
39
|
-
|
|
40
|
+
key = key.to_s if options[:as_json]
|
|
41
|
+
hash[key] = to_hash(value, options)
|
|
40
42
|
end
|
|
41
43
|
when Array
|
|
42
|
-
obj.collect { |value| to_hash(value) }
|
|
44
|
+
obj.collect { |value| to_hash(value, options) }
|
|
43
45
|
else
|
|
44
46
|
obj
|
|
45
47
|
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Aws
|
|
4
|
+
class Token
|
|
5
|
+
|
|
6
|
+
# @param [String] token
|
|
7
|
+
# @param [Time] expiration
|
|
8
|
+
def initialize(token, expiration=nil)
|
|
9
|
+
@token = token
|
|
10
|
+
@expiration = expiration
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# @return [String, nil]
|
|
14
|
+
attr_reader :token
|
|
15
|
+
|
|
16
|
+
# @return [Time, nil]
|
|
17
|
+
attr_reader :expiration
|
|
18
|
+
|
|
19
|
+
# @return [Boolean] Returns `true` if token is set
|
|
20
|
+
def set?
|
|
21
|
+
!token.nil? && !token.empty?
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Removing the token from the default inspect string.
|
|
25
|
+
# @api private
|
|
26
|
+
def inspect
|
|
27
|
+
"#<#{self.class.name} token=[FILTERED]> expiration=#{expiration}>"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Aws
|
|
4
|
+
# @api private
|
|
5
|
+
class TokenProviderChain
|
|
6
|
+
def initialize(config = nil)
|
|
7
|
+
@config = config
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
# @return [TokenProvider, nil]
|
|
11
|
+
def resolve
|
|
12
|
+
providers.each do |method_name, options|
|
|
13
|
+
provider = send(method_name, options.merge(config: @config))
|
|
14
|
+
return provider if provider && provider.set?
|
|
15
|
+
end
|
|
16
|
+
nil
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def providers
|
|
22
|
+
[
|
|
23
|
+
[:static_profile_sso_token, {}],
|
|
24
|
+
[:sso_token, {}]
|
|
25
|
+
]
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def static_profile_sso_token(options)
|
|
29
|
+
if Aws.shared_config.config_enabled? && options[:config] && options[:config].profile
|
|
30
|
+
Aws.shared_config.sso_token_from_config(
|
|
31
|
+
profile: options[:config].profile
|
|
32
|
+
)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def sso_token(options)
|
|
38
|
+
profile_name = determine_profile_name(options)
|
|
39
|
+
if Aws.shared_config.config_enabled?
|
|
40
|
+
Aws.shared_config.sso_token_from_config(profile: profile_name)
|
|
41
|
+
end
|
|
42
|
+
rescue Errors::NoSuchProfileError
|
|
43
|
+
nil
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def determine_profile_name(options)
|
|
47
|
+
(options[:config] && options[:config].profile) || ENV['AWS_PROFILE'] || ENV['AWS_DEFAULT_PROFILE'] || 'default'
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -24,6 +24,7 @@ module Aws
|
|
|
24
24
|
else
|
|
25
25
|
code, message, data = extract_error(body, context)
|
|
26
26
|
end
|
|
27
|
+
context[:request_id] = request_id(body)
|
|
27
28
|
errors_module = context.client.class.errors_module
|
|
28
29
|
error_class = errors_module.error_class(code).new(context, message, data)
|
|
29
30
|
error_class
|
|
@@ -94,6 +95,12 @@ module Aws
|
|
|
94
95
|
end
|
|
95
96
|
end
|
|
96
97
|
|
|
98
|
+
def request_id(body)
|
|
99
|
+
if matches = body.match(/<RequestId>(.+?)<\/RequestId>/m)
|
|
100
|
+
matches[1]
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
97
104
|
def unescape(str)
|
|
98
105
|
CGI.unescapeHTML(str)
|
|
99
106
|
end
|
data/lib/aws-sdk-core.rb
CHANGED
|
@@ -20,6 +20,15 @@ require_relative 'aws-sdk-core/shared_credentials'
|
|
|
20
20
|
require_relative 'aws-sdk-core/process_credentials'
|
|
21
21
|
require_relative 'aws-sdk-core/sso_credentials'
|
|
22
22
|
|
|
23
|
+
# tokens and token providers
|
|
24
|
+
require_relative 'aws-sdk-core/token'
|
|
25
|
+
require_relative 'aws-sdk-core/token_provider'
|
|
26
|
+
require_relative 'aws-sdk-core/static_token_provider'
|
|
27
|
+
require_relative 'aws-sdk-core/refreshing_token'
|
|
28
|
+
require_relative 'aws-sdk-core/sso_token_provider'
|
|
29
|
+
require_relative 'aws-sdk-core/token_provider_chain'
|
|
30
|
+
require_relative 'aws-sdk-core/plugins/bearer_authorization'
|
|
31
|
+
|
|
23
32
|
# client modules
|
|
24
33
|
|
|
25
34
|
require_relative 'aws-sdk-core/client_stubs'
|
|
@@ -99,6 +108,7 @@ require_relative 'aws-sdk-sts'
|
|
|
99
108
|
|
|
100
109
|
# aws-sdk-sso is included to support Aws::SSOCredentials
|
|
101
110
|
require_relative 'aws-sdk-sso'
|
|
111
|
+
require_relative 'aws-sdk-ssooidc'
|
|
102
112
|
|
|
103
113
|
module Aws
|
|
104
114
|
|
data/lib/aws-sdk-sso/client.rb
CHANGED
|
@@ -362,7 +362,8 @@ module Aws::SSO
|
|
|
362
362
|
#
|
|
363
363
|
# @option params [required, String] :access_token
|
|
364
364
|
# The token issued by the `CreateToken` API call. For more information,
|
|
365
|
-
# see [CreateToken][1] in the *
|
|
365
|
+
# see [CreateToken][1] in the *IAM Identity Center OIDC API Reference
|
|
366
|
+
# Guide*.
|
|
366
367
|
#
|
|
367
368
|
#
|
|
368
369
|
#
|
|
@@ -407,7 +408,8 @@ module Aws::SSO
|
|
|
407
408
|
#
|
|
408
409
|
# @option params [required, String] :access_token
|
|
409
410
|
# The token issued by the `CreateToken` API call. For more information,
|
|
410
|
-
# see [CreateToken][1] in the *
|
|
411
|
+
# see [CreateToken][1] in the *IAM Identity Center OIDC API Reference
|
|
412
|
+
# Guide*.
|
|
411
413
|
#
|
|
412
414
|
#
|
|
413
415
|
#
|
|
@@ -450,8 +452,8 @@ module Aws::SSO
|
|
|
450
452
|
|
|
451
453
|
# Lists all AWS accounts assigned to the user. These AWS accounts are
|
|
452
454
|
# assigned by the administrator of the account. For more information,
|
|
453
|
-
# see [Assign User Access][1] in the *
|
|
454
|
-
# operation returns a paginated response.
|
|
455
|
+
# see [Assign User Access][1] in the *IAM Identity Center User Guide*.
|
|
456
|
+
# This operation returns a paginated response.
|
|
455
457
|
#
|
|
456
458
|
#
|
|
457
459
|
#
|
|
@@ -466,7 +468,8 @@ module Aws::SSO
|
|
|
466
468
|
#
|
|
467
469
|
# @option params [required, String] :access_token
|
|
468
470
|
# The token issued by the `CreateToken` API call. For more information,
|
|
469
|
-
# see [CreateToken][1] in the *
|
|
471
|
+
# see [CreateToken][1] in the *IAM Identity Center OIDC API Reference
|
|
472
|
+
# Guide*.
|
|
470
473
|
#
|
|
471
474
|
#
|
|
472
475
|
#
|
|
@@ -504,12 +507,32 @@ module Aws::SSO
|
|
|
504
507
|
req.send_request(options)
|
|
505
508
|
end
|
|
506
509
|
|
|
507
|
-
# Removes the
|
|
508
|
-
# the
|
|
510
|
+
# Removes the locally stored SSO tokens from the client-side cache and
|
|
511
|
+
# sends an API call to the IAM Identity Center service to invalidate the
|
|
512
|
+
# corresponding server-side IAM Identity Center sign in session.
|
|
513
|
+
#
|
|
514
|
+
# <note markdown="1"> If a user uses IAM Identity Center to access the AWS CLI, the user’s
|
|
515
|
+
# IAM Identity Center sign in session is used to obtain an IAM session,
|
|
516
|
+
# as specified in the corresponding IAM Identity Center permission set.
|
|
517
|
+
# More specifically, IAM Identity Center assumes an IAM role in the
|
|
518
|
+
# target account on behalf of the user, and the corresponding temporary
|
|
519
|
+
# AWS credentials are returned to the client.
|
|
520
|
+
#
|
|
521
|
+
# After user logout, any existing IAM role sessions that were created by
|
|
522
|
+
# using IAM Identity Center permission sets continue based on the
|
|
523
|
+
# duration configured in the permission set. For more information, see
|
|
524
|
+
# [User authentications][1] in the *IAM Identity Center User Guide*.
|
|
525
|
+
#
|
|
526
|
+
# </note>
|
|
527
|
+
#
|
|
528
|
+
#
|
|
529
|
+
#
|
|
530
|
+
# [1]: https://docs.aws.amazon.com/singlesignon/latest/userguide/authconcept.html
|
|
509
531
|
#
|
|
510
532
|
# @option params [required, String] :access_token
|
|
511
533
|
# The token issued by the `CreateToken` API call. For more information,
|
|
512
|
-
# see [CreateToken][1] in the *
|
|
534
|
+
# see [CreateToken][1] in the *IAM Identity Center OIDC API Reference
|
|
535
|
+
# Guide*.
|
|
513
536
|
#
|
|
514
537
|
#
|
|
515
538
|
#
|
|
@@ -545,7 +568,7 @@ module Aws::SSO
|
|
|
545
568
|
params: params,
|
|
546
569
|
config: config)
|
|
547
570
|
context[:gem_name] = 'aws-sdk-core'
|
|
548
|
-
context[:gem_version] = '3.
|
|
571
|
+
context[:gem_version] = '3.158.1'
|
|
549
572
|
Seahorse::Client::Request.new(handlers, context)
|
|
550
573
|
end
|
|
551
574
|
|
data/lib/aws-sdk-sso/types.rb
CHANGED
|
@@ -53,8 +53,8 @@ module Aws::SSO
|
|
|
53
53
|
#
|
|
54
54
|
# @!attribute [rw] access_token
|
|
55
55
|
# The token issued by the `CreateToken` API call. For more
|
|
56
|
-
# information, see [CreateToken][1] in the *
|
|
57
|
-
# Guide*.
|
|
56
|
+
# information, see [CreateToken][1] in the *IAM Identity Center OIDC
|
|
57
|
+
# API Reference Guide*.
|
|
58
58
|
#
|
|
59
59
|
#
|
|
60
60
|
#
|
|
@@ -118,8 +118,8 @@ module Aws::SSO
|
|
|
118
118
|
#
|
|
119
119
|
# @!attribute [rw] access_token
|
|
120
120
|
# The token issued by the `CreateToken` API call. For more
|
|
121
|
-
# information, see [CreateToken][1] in the *
|
|
122
|
-
# Guide*.
|
|
121
|
+
# information, see [CreateToken][1] in the *IAM Identity Center OIDC
|
|
122
|
+
# API Reference Guide*.
|
|
123
123
|
#
|
|
124
124
|
#
|
|
125
125
|
#
|
|
@@ -179,8 +179,8 @@ module Aws::SSO
|
|
|
179
179
|
#
|
|
180
180
|
# @!attribute [rw] access_token
|
|
181
181
|
# The token issued by the `CreateToken` API call. For more
|
|
182
|
-
# information, see [CreateToken][1] in the *
|
|
183
|
-
# Guide*.
|
|
182
|
+
# information, see [CreateToken][1] in the *IAM Identity Center OIDC
|
|
183
|
+
# API Reference Guide*.
|
|
184
184
|
#
|
|
185
185
|
#
|
|
186
186
|
#
|
|
@@ -224,8 +224,8 @@ module Aws::SSO
|
|
|
224
224
|
#
|
|
225
225
|
# @!attribute [rw] access_token
|
|
226
226
|
# The token issued by the `CreateToken` API call. For more
|
|
227
|
-
# information, see [CreateToken][1] in the *
|
|
228
|
-
# Guide*.
|
|
227
|
+
# information, see [CreateToken][1] in the *IAM Identity Center OIDC
|
|
228
|
+
# API Reference Guide*.
|
|
229
229
|
#
|
|
230
230
|
#
|
|
231
231
|
#
|