Skip to content

Commit 26b2888

Browse files
committed
test(idp): cached temporary AWS credentials used if present and valid
1 parent 8b8e432 commit 26b2888

File tree

2 files changed

+182
-15
lines changed

2 files changed

+182
-15
lines changed

test/integration/plugin/test_credentials_providers.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,3 +140,23 @@ def testWrongCredentialsProvider(idp_arg):
140140
idp_arg["credentials_provider"] = "WrongProvider"
141141
with pytest.raises(redshift_connector.InterfaceError, match="Invalid credentials provider WrongProvider"):
142142
redshift_connector.connect(**idp_arg)
143+
144+
145+
@pytest.mark.parametrize("idp_arg", NON_BROWSER_IDP, indirect=True)
146+
def use_cached_temporary_credentials(idp_arg):
147+
# ensure nothing is in the credential cache
148+
redshift_connector.IamHelper.credentials_cache.clear()
149+
150+
with redshift_connector.connect(**idp_arg):
151+
pass
152+
153+
assert len(redshift_connector.IamHelper.credentials_cache) == 1
154+
first_cred_cache_entry = redshift_connector.IamHelper.credentials_cache.popitem()
155+
156+
with redshift_connector.connect(**idp_arg):
157+
pass
158+
159+
# we should have used the temporary credentials retrieved in first AWS API call, verify cache still
160+
# holds these
161+
assert len(redshift_connector.IamHelper.credentials_cache) == 1
162+
assert first_cred_cache_entry == redshift_connector.IamHelper.credentials_cache.popitem()

test/unit/test_iam_helper.py

Lines changed: 162 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
1+
import datetime
12
import typing
23
from test.unit import MockCredentialsProvider
4+
from unittest import mock
5+
from unittest.mock import MagicMock, call
36

47
import pytest # type: ignore
8+
from dateutil.tz import tzutc
59
from pytest_mock import mocker
610

7-
from redshift_connector import InterfaceError, RedshiftProperty, set_iam_properties
11+
from redshift_connector import InterfaceError, RedshiftProperty
812
from redshift_connector.auth import AWSCredentialsProvider
913
from redshift_connector.config import ClientProtocolVersion
10-
from redshift_connector.iam_helper import set_iam_credentials
14+
from redshift_connector.iam_helper import IamHelper
1115
from redshift_connector.plugin import (
1216
AdfsCredentialsProvider,
1317
AzureCredentialsProvider,
@@ -20,12 +24,12 @@
2024

2125
@pytest.fixture
2226
def mock_set_iam_credentials(mocker):
23-
mocker.patch("redshift_connector.iam_helper.set_iam_credentials", return_value=None)
27+
mocker.patch("redshift_connector.iam_helper.IamHelper.set_iam_credentials", return_value=None)
2428

2529

2630
@pytest.fixture
2731
def mock_set_cluster_credentials(mocker):
28-
mocker.patch("redshift_connector.iam_helper.set_cluster_credentials", return_value=None)
32+
mocker.patch("redshift_connector.iam_helper.IamHelper.set_cluster_credentials", return_value=None)
2933

3034

3135
@pytest.fixture
@@ -99,7 +103,7 @@ def get_set_iam_properties_args(**kwargs) -> typing.Dict[str, typing.Any]:
99103
def test_set_iam_properties_fails_when_info_is_none(missing_param):
100104
keywords: typing.Dict = {missing_param: None}
101105
with pytest.raises(InterfaceError) as excinfo:
102-
set_iam_properties(**get_set_iam_properties_args(**keywords))
106+
IamHelper.set_iam_properties(**get_set_iam_properties_args(**keywords))
103107
assert "Invalid connection property setting" in str(excinfo.value)
104108

105109

@@ -123,7 +127,7 @@ def test_set_iam_properties_enforce_min_ssl_mode(ssl_param):
123127
all_params: typing.Dict = get_set_iam_properties_args(**keywords)
124128
assert all_params["sslmode"] == test_input
125129

126-
set_iam_properties(**all_params)
130+
IamHelper.set_iam_properties(**all_params)
127131
assert all_params["info"].sslmode == expected_mode
128132

129133

@@ -136,7 +140,7 @@ def test_set_iam_properties_enforce_client_protocol_version(_input):
136140
all_params: typing.Dict = get_set_iam_properties_args(**keywords)
137141
assert all_params["client_protocol_version"] == _input
138142

139-
set_iam_properties(**all_params)
143+
IamHelper.set_iam_properties(**all_params)
140144
assert all_params["info"].client_protocol_version == _input
141145

142146

@@ -223,7 +227,7 @@ def test_set_iam_properties_enforce_setting_compatibility(mocker, joint_params):
223227
test_input, expected_exception_msg = joint_params
224228

225229
with pytest.raises(InterfaceError) as excinfo:
226-
set_iam_properties(**get_set_iam_properties_args(**test_input))
230+
IamHelper.set_iam_properties(**get_set_iam_properties_args(**test_input))
227231
assert expected_exception_msg in str(excinfo.value)
228232

229233

@@ -241,6 +245,8 @@ def make_redshift_property() -> RedshiftProperty:
241245
rp: RedshiftProperty = RedshiftProperty()
242246
rp.user_name = "mario@luigi.com"
243247
rp.password = "bowser"
248+
rp.db_name = "dev"
249+
rp.cluster_identifier = "something"
244250
rp.idp_host = "8000"
245251
rp.duration = 100
246252
rp.preferred_role = "analyst"
@@ -270,7 +276,7 @@ def test_set_iam_properties_provider_assigned(mocker, provider):
270276

271277
spy = mocker.spy(expectedProvider, "add_parameter")
272278

273-
set_iam_credentials(rp)
279+
IamHelper.set_iam_credentials(rp)
274280
assert spy.called
275281
assert spy.call_count == 1
276282
# ensure call to add_Parameter was made on the expected Provider class
@@ -293,8 +299,8 @@ def test_set_iam_properties_via_aws_credentials(mocker, test_input):
293299
info_obj["iam"] = True
294300
info_obj["cluster_identifier"] = "blah"
295301

296-
mocker.patch("redshift_connector.iam_helper.set_iam_credentials", return_value=None)
297-
set_iam_properties(**info_obj)
302+
mocker.patch("redshift_connector.iam_helper.IamHelper.set_iam_credentials", return_value=None)
303+
IamHelper.set_iam_properties(**info_obj)
298304

299305
for aws_cred_key, aws_cred_val in enumerate(test_input):
300306
if aws_cred_key == "profile":
@@ -316,10 +322,10 @@ def test_set_iam_credentials_via_aws_credentials(mocker):
316322
redshift_property.secret_access_key = "secret_val"
317323
redshift_property.session_token = "session_val"
318324

319-
mocker.patch("redshift_connector.iam_helper.set_cluster_credentials", return_value=None)
325+
mocker.patch("redshift_connector.iam_helper.IamHelper.set_cluster_credentials", return_value=None)
320326
spy = mocker.spy(AWSCredentialsProvider, "add_parameter")
321327

322-
set_iam_credentials(redshift_property)
328+
IamHelper.set_iam_credentials(redshift_property)
323329
assert spy.called is True
324330
assert spy.call_count == 1
325331
assert spy.call_args[0][1] == redshift_property
@@ -328,14 +334,155 @@ def test_set_iam_credentials_via_aws_credentials(mocker):
328334
def test_dynamically_loading_credential_holder(mocker):
329335
external_class_name: str = "test.unit.MockCredentialsProvider"
330336
mocker.patch("{}.get_credentials".format(external_class_name))
331-
mocker.patch("redshift_connector.iam_helper.set_cluster_credentials", return_value=None)
337+
mocker.patch("redshift_connector.iam_helper.IamHelper.set_cluster_credentials", return_value=None)
332338
rp: RedshiftProperty = make_redshift_property()
333339
rp.credentials_provider = external_class_name
334340

335341
spy = mocker.spy(MockCredentialsProvider, "add_parameter")
336342

337-
set_iam_credentials(rp)
343+
IamHelper.set_iam_credentials(rp)
338344
assert spy.called
339345
assert spy.call_count == 1
340346
# ensure call to add_Parameter was made on the expected Provider class
341347
assert isinstance(spy.call_args[0][0], MockCredentialsProvider) is True
348+
349+
350+
def test_get_credentials_cache_key():
351+
rp: RedshiftProperty = RedshiftProperty()
352+
rp.db_user = "2"
353+
rp.db_name = "1"
354+
rp.db_groups = ["4", "3", "5"]
355+
rp.cluster_identifier = "6"
356+
rp.auto_create = False
357+
358+
res_cache_key: str = IamHelper.get_credentials_cache_key(rp)
359+
assert res_cache_key is not None
360+
assert res_cache_key == "2;1;3,4,5;6;False"
361+
362+
363+
def test_get_credentials_cache_key_no_db_groups():
364+
rp: RedshiftProperty = RedshiftProperty()
365+
rp.db_user = "2"
366+
rp.db_name = "1"
367+
rp.cluster_identifier = "6"
368+
rp.auto_create = False
369+
370+
res_cache_key: str = IamHelper.get_credentials_cache_key(rp)
371+
assert res_cache_key is not None
372+
assert res_cache_key == "2;1;;6;False"
373+
374+
375+
@mock.patch("boto3.client.get_cluster_credentials")
376+
@mock.patch("boto3.client.describe_clusters")
377+
@mock.patch("boto3.client")
378+
def test_set_cluster_credentials_caches_credentials(
379+
mock_boto_client, mock_describe_clusters, mock_get_cluster_credentials
380+
):
381+
mock_cred_provider = MagicMock()
382+
mock_cred_holder = MagicMock()
383+
mock_cred_provider.get_credentials.return_value = mock_cred_holder
384+
mock_cred_holder.has_associated_session = False
385+
386+
rp: RedshiftProperty = make_redshift_property()
387+
388+
IamHelper.credentials_cache.clear()
389+
390+
IamHelper.set_cluster_credentials(mock_cred_provider, rp)
391+
assert len(IamHelper.credentials_cache) == 1
392+
393+
assert mock_boto_client.called is True
394+
mock_boto_client.assert_has_calls(
395+
[
396+
call().get_cluster_credentials(
397+
AutoCreate=rp.auto_create,
398+
ClusterIdentifier=rp.cluster_identifier,
399+
DbGroups=rp.db_groups,
400+
DbName=rp.db_name,
401+
DbUser=rp.db_user,
402+
)
403+
]
404+
)
405+
406+
407+
@mock.patch("boto3.client.get_cluster_credentials")
408+
@mock.patch("boto3.client.describe_clusters")
409+
@mock.patch("boto3.client")
410+
def test_set_cluster_credentials_uses_cache_if_possible(
411+
mock_boto_client, mock_describe_clusters, mock_get_cluster_credentials
412+
):
413+
mock_cred_provider = MagicMock()
414+
mock_cred_holder = MagicMock()
415+
mock_cred_provider.get_credentials.return_value = mock_cred_holder
416+
mock_cred_holder.has_associated_session = False
417+
418+
rp: RedshiftProperty = make_redshift_property()
419+
# mock out the boto3 response temporary credentials stored from prior auth
420+
mock_cred_obj: typing.Dict[str, typing.Union[str, datetime.datetime]] = {
421+
"DbUser": "xyz",
422+
"DbPassword": "turtle",
423+
"Expiration": datetime.datetime(9999, 1, 1, tzinfo=tzutc()),
424+
}
425+
# populate the cache
426+
IamHelper.credentials_cache.clear()
427+
IamHelper.credentials_cache[IamHelper.get_credentials_cache_key(rp)] = mock_cred_obj
428+
429+
IamHelper.set_cluster_credentials(mock_cred_provider, rp)
430+
assert len(IamHelper.credentials_cache) == 1
431+
assert IamHelper.credentials_cache[IamHelper.get_credentials_cache_key(rp)] is mock_cred_obj
432+
assert mock_boto_client.called is True
433+
434+
assert rp.user_name == mock_cred_obj["DbUser"]
435+
assert rp.password == mock_cred_obj["DbPassword"]
436+
437+
assert (
438+
call().get_cluster_credentials(
439+
AutoCreate=rp.auto_create,
440+
ClusterIdentifier=rp.cluster_identifier,
441+
DbGroups=rp.db_groups,
442+
DbName=rp.db_name,
443+
DbUser=rp.db_user,
444+
)
445+
not in mock_boto_client.mock_calls
446+
)
447+
448+
449+
@mock.patch("boto3.client.get_cluster_credentials")
450+
@mock.patch("boto3.client.describe_clusters")
451+
@mock.patch("boto3.client")
452+
def test_set_cluster_credentials_refreshes_stale_credentials(
453+
mock_boto_client, mock_describe_clusters, mock_get_cluster_credentials
454+
):
455+
mock_cred_provider = MagicMock()
456+
mock_cred_holder = MagicMock()
457+
mock_cred_provider.get_credentials.return_value = mock_cred_holder
458+
mock_cred_holder.has_associated_session = False
459+
460+
rp: RedshiftProperty = make_redshift_property()
461+
# mock out the boto3 response temporary credentials stored from prior auth (now stale)
462+
mock_cred_obj: typing.Dict[str, typing.Union[str, datetime.datetime]] = {
463+
"DbUser": "xyz",
464+
"DbPassword": "turtle",
465+
"Expiration": datetime.datetime(1, 1, 1, tzinfo=tzutc()),
466+
}
467+
# populate the cache
468+
IamHelper.credentials_cache.clear()
469+
IamHelper.credentials_cache[IamHelper.get_credentials_cache_key(rp)] = mock_cred_obj
470+
471+
IamHelper.set_cluster_credentials(mock_cred_provider, rp)
472+
assert len(IamHelper.credentials_cache) == 1
473+
# ensure new temporary credentials have been replaced in cache
474+
assert IamHelper.get_credentials_cache_key(rp) in IamHelper.credentials_cache
475+
assert IamHelper.credentials_cache[IamHelper.get_credentials_cache_key(rp)] is not mock_cred_obj
476+
assert mock_boto_client.called is True
477+
478+
mock_boto_client.assert_has_calls(
479+
[
480+
call().get_cluster_credentials(
481+
AutoCreate=rp.auto_create,
482+
ClusterIdentifier=rp.cluster_identifier,
483+
DbGroups=rp.db_groups,
484+
DbName=rp.db_name,
485+
DbUser=rp.db_user,
486+
)
487+
]
488+
)

0 commit comments

Comments
 (0)