Skip to content

Commit cda5480

Browse files
DmitryLukyanovdnickless
authored andcommitted
CSHARP-4273: Cache AWS Credentials Where Possible. (mongodb#894)
CSHARP-4273: Cache AWS Credentials Where Possible.
1 parent e154014 commit cda5480

File tree

11 files changed

+404
-46
lines changed

11 files changed

+404
-46
lines changed

evergreen/evergreen.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -514,6 +514,7 @@ functions:
514514
cat <<'EOF' > "${PROJECT_DIRECTORY}/prepare_mongodb_aws.sh"
515515
MONGODB_URI="mongodb://localhost"
516516
EOF
517+
export AWS_EC2_ENABLED=true
517518
PROJECT_DIRECTORY=${PROJECT_DIRECTORY} evergreen/run-mongodb-aws-test.sh
518519
519520
run-aws-auth-test-with-aws-credentials-as-environment-variables:
@@ -1008,7 +1009,7 @@ tasks:
10081009
- func: run-aws-auth-test-with-aws-credentials-as-environment-variables
10091010
- func: run-aws-auth-test-with-aws-credentials-and-session-token-as-environment-variables
10101011
- func: run-aws-auth-test-with-aws-EC2-credentials
1011-
# ECS test is skipped untill testing on Linux becomes possible
1012+
# ECS test is skipped until testing on Linux becomes possible
10121013

10131014
- name: stable-api-tests-net472
10141015
commands:

src/MongoDB.Driver.Core/Core/Authentication/External/AwsAuthenticationCredentialsProvider.cs

Lines changed: 42 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,20 +25,27 @@ namespace MongoDB.Driver.Core.Authentication.External
2525
{
2626
internal class AwsCredentials : IExternalCredentials
2727
{
28+
// credentials are considered expired when: Expiration - now < 5 mins
29+
private static readonly TimeSpan __overlapWhereExpired = TimeSpan.FromMinutes(5);
30+
2831
private readonly string _accessKeyId;
32+
private readonly DateTime? _expiration;
2933
private readonly SecureString _secretAccessKey;
3034
private readonly string _sessionToken;
3135

32-
public AwsCredentials(string accessKeyId, SecureString secretAccessKey, string sessionToken)
36+
public AwsCredentials(string accessKeyId, SecureString secretAccessKey, string sessionToken, DateTime? expiration)
3337
{
3438
_accessKeyId = Ensure.IsNotNull(accessKeyId, nameof(accessKeyId));
39+
_expiration = expiration; // can be null
3540
_secretAccessKey = Ensure.IsNotNull(secretAccessKey, nameof(secretAccessKey));
3641
_sessionToken = sessionToken; // can be null
3742
}
3843

3944
public string AccessKeyId => _accessKeyId;
45+
public DateTime? Expiration => _expiration;
4046
public SecureString SecretAccessKey => _secretAccessKey;
4147
public string SessionToken => _sessionToken;
48+
public bool IsExpired => _expiration.HasValue ? (_expiration.Value - DateTime.UtcNow) < __overlapWhereExpired : false;
4249

4350
public BsonDocument GetKmsCredentials()
4451
=> new BsonDocument
@@ -93,7 +100,7 @@ private AwsCredentials CreateAwsCredentialsFromEnvironmentVariables()
93100
throw new InvalidOperationException($"When using AWS authentication if a session token is provided via environment variables then an access key ID and a secret access key must be provided also.");
94101
}
95102

96-
return new AwsCredentials(accessKeyId, SecureStringHelper.ToSecureString(secretAccessKey), sessionToken);
103+
return new AwsCredentials(accessKeyId, SecureStringHelper.ToSecureString(secretAccessKey), sessionToken, expiration: null);
97104
}
98105

99106
private async Task<AwsCredentials> CreateAwsCredentialsFromEcsResponseAsync(CancellationToken cancellationToken)
@@ -105,23 +112,52 @@ private async Task<AwsCredentials> CreateAwsCredentialsFromEcsResponseAsync(Canc
105112
}
106113

107114
var response = await _awsHttpClientHelper.GetECSResponseAsync(relativeUri, cancellationToken).ConfigureAwait(false);
108-
return CreateAwsCreadentialsFromAwsResponse(response);
115+
return CreateAwsCredentialsFromAwsResponse(response);
109116
}
110117

111118
private async Task<AwsCredentials> CreateAwsCredentialsFromEc2ResponseAsync(CancellationToken cancellationToken)
112119
{
113120
var response = await _awsHttpClientHelper.GetEC2ResponseAsync(cancellationToken).ConfigureAwait(false);
114-
return CreateAwsCreadentialsFromAwsResponse(response);
121+
return CreateAwsCredentialsFromAwsResponse(response);
115122
}
116123

117-
private AwsCredentials CreateAwsCreadentialsFromAwsResponse(string awsResponse)
124+
private AwsCredentials CreateAwsCredentialsFromAwsResponse(string awsResponse)
118125
{
126+
// Response template:
127+
//{
128+
// "Code": "Success",
129+
// "LastUpdated": "..",
130+
// "Type": "AWS-HMAC",
131+
// "AccessKeyId": "..",
132+
// "SecretAccessKey": "..",
133+
// "Token": "",
134+
// "Expiration": "YYYY-mm-ddThh:mm:ssZ"
135+
//}
119136
var parsedResponse = BsonDocument.Parse(awsResponse);
120137
var accessKeyId = parsedResponse.GetValue("AccessKeyId", null)?.AsString;
121138
var secretAccessKey = parsedResponse.GetValue("SecretAccessKey", null)?.AsString;
122139
var sessionToken = parsedResponse.GetValue("Token", null)?.AsString;
140+
var expiration = parsedResponse.GetValue("Expiration", null)?.AsString;
141+
if (!TryParseDateTime(expiration, out var expirationDateTime))
142+
{
143+
if (expiration != null)
144+
{
145+
throw new InvalidOperationException($"Expiration in AWS response is in invalid datetime format: {expiration}.");
146+
}
147+
}
123148

124-
return new AwsCredentials(accessKeyId, SecureStringHelper.ToSecureString(secretAccessKey), sessionToken);
149+
return new AwsCredentials(accessKeyId, SecureStringHelper.ToSecureString(secretAccessKey), sessionToken, expirationDateTime);
150+
151+
bool TryParseDateTime(string value, out DateTime? result)
152+
{
153+
result = null;
154+
if (DateTime.TryParse(value, out var dateTime))
155+
{
156+
result = dateTime;
157+
return true;
158+
}
159+
return false;
160+
}
125161
}
126162

127163
// nested types
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/* Copyright 2010-present MongoDB Inc.
2+
*
3+
* Licensed under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License.
5+
* You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*/
15+
16+
using System.Threading;
17+
using System.Threading.Tasks;
18+
using MongoDB.Driver.Core.Misc;
19+
20+
namespace MongoDB.Driver.Core.Authentication.External
21+
{
22+
internal interface ICredentialsCache<TCredentials> where TCredentials : IExternalCredentials
23+
{
24+
TCredentials Credentials { get; }
25+
void Clear();
26+
}
27+
28+
internal class CacheableCredentialsProvider<TCredentials> : IExternalAuthenticationCredentialsProvider<TCredentials>, ICredentialsCache<TCredentials>
29+
where TCredentials : IExternalCredentials
30+
{
31+
private TCredentials _cachedCredentials;
32+
private readonly IExternalAuthenticationCredentialsProvider<TCredentials> _provider;
33+
34+
public CacheableCredentialsProvider(IExternalAuthenticationCredentialsProvider<TCredentials> provider)
35+
{
36+
_provider = Ensure.IsNotNull(provider, nameof(provider));
37+
}
38+
39+
public TCredentials Credentials => _cachedCredentials;
40+
41+
public TCredentials CreateCredentialsFromExternalSource(CancellationToken cancellationToken = default)
42+
{
43+
var cachedCredentials = _cachedCredentials;
44+
if (IsValidCache(cachedCredentials))
45+
{
46+
return cachedCredentials;
47+
}
48+
else
49+
{
50+
Clear();
51+
try
52+
{
53+
cachedCredentials = _provider.CreateCredentialsFromExternalSource(cancellationToken);
54+
_cachedCredentials = cachedCredentials;
55+
return cachedCredentials;
56+
}
57+
catch
58+
{
59+
Clear();
60+
throw;
61+
}
62+
}
63+
}
64+
65+
public async Task<TCredentials> CreateCredentialsFromExternalSourceAsync(CancellationToken cancellationToken = default)
66+
{
67+
var cachedCredentials = _cachedCredentials;
68+
if (IsValidCache(cachedCredentials))
69+
{
70+
return cachedCredentials;
71+
}
72+
else
73+
{
74+
Clear();
75+
try
76+
{
77+
cachedCredentials = await _provider.CreateCredentialsFromExternalSourceAsync(cancellationToken).ConfigureAwait(false);
78+
_cachedCredentials = cachedCredentials;
79+
return cachedCredentials;
80+
}
81+
catch
82+
{
83+
Clear();
84+
throw;
85+
}
86+
}
87+
}
88+
89+
// private methods
90+
private bool IsValidCache(TCredentials credentials) => credentials != null && !credentials.IsExpired;
91+
public void Clear() => _cachedCredentials = default;
92+
}
93+
}

src/MongoDB.Driver.Core/Core/Authentication/External/ExternalCredentialsAuthenticators.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
*/
1515

1616
using System;
17-
using System.Net.Http;
1817
using MongoDB.Driver.Core.Misc;
1918

2019
namespace MongoDB.Driver.Core.Authentication.External
@@ -38,7 +37,7 @@ internal ExternalCredentialsAuthenticators() : this(new HttpClientHelper())
3837
internal ExternalCredentialsAuthenticators(HttpClientHelper httpClientHelper)
3938
{
4039
_httpClientHelper = Ensure.IsNotNull(httpClientHelper, nameof(httpClientHelper));
41-
_awsExternalAuthenticationCredentialsProvider = new Lazy<IExternalAuthenticationCredentialsProvider<AwsCredentials>>(() => new AwsAuthenticationCredentialsProvider(_httpClientHelper), isThreadSafe: true);
40+
_awsExternalAuthenticationCredentialsProvider = new Lazy<IExternalAuthenticationCredentialsProvider<AwsCredentials>>(() => new CacheableCredentialsProvider<AwsCredentials>(new AwsAuthenticationCredentialsProvider(_httpClientHelper)), isThreadSafe: true);
4241
_gcpExternalAuthenticationCredentialsProvider = new Lazy<IExternalAuthenticationCredentialsProvider<GcpCredentials>>(() => new GcpAuthenticationCredentialsProvider(_httpClientHelper), isThreadSafe: true);
4342
}
4443

src/MongoDB.Driver.Core/Core/Authentication/External/GcpAuthenticationCredentialsProvider.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ internal class GcpCredentials : IExternalCredentials
3030

3131
public string AccessToken => _accessToken;
3232

33+
public DateTime? Expiration => null;
34+
35+
public bool IsExpired => false;
36+
3337
public BsonDocument GetKmsCredentials() => new BsonDocument("accessToken", _accessToken);
3438
}
3539

src/MongoDB.Driver.Core/Core/Authentication/External/IExternalCredentials.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,15 @@
1313
* limitations under the License.
1414
*/
1515

16+
using System;
1617
using MongoDB.Bson;
1718

1819
namespace MongoDB.Driver.Core.Authentication.External
1920
{
2021
internal interface IExternalCredentials
2122
{
23+
DateTime? Expiration { get; }
24+
bool IsExpired { get; }
2225
BsonDocument GetKmsCredentials();
2326
}
2427
}

0 commit comments

Comments
 (0)