Skip to content

Commit e2ca743

Browse files
committed
test(auth): IAM credential authentication
1 parent 6af4225 commit e2ca743

6 files changed

+306
-12
lines changed

test/integration/plugin/test_credentials_providers.py

+3-4
Original file line numberDiff line numberDiff line change
@@ -113,16 +113,15 @@ def testSslAndIam(idp_arg):
113113
idp_arg["iam"] = True
114114
with pytest.raises(
115115
redshift_connector.InterfaceError,
116-
match="Invalid connection property setting. SSL must be enabled when using IAM",
116+
match="Invalid connection property setting",
117117
):
118118
redshift_connector.connect(**idp_arg)
119119

120120
idp_arg["iam"] = False
121121
idp_arg["credentials_provider"] = "OktacredentialSProvider"
122122
with pytest.raises(
123123
redshift_connector.InterfaceError,
124-
match="Invalid connection property setting. IAM must be enabled when using credentials "
125-
"via identity provider",
124+
match="Invalid connection property setting",
126125
):
127126
redshift_connector.connect(**idp_arg)
128127

@@ -131,7 +130,7 @@ def testSslAndIam(idp_arg):
131130
idp_arg["credentials_provider"] = None
132131
with pytest.raises(
133132
redshift_connector.InterfaceError,
134-
match="Invalid connection property setting. " "Credentials provider cannot be None when IAM is enabled",
133+
match="Invalid connection property setting",
135134
):
136135
redshift_connector.connect(**idp_arg)
137136

test/unit/auth/__init__.py

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import typing
2+
from unittest.mock import MagicMock, patch
3+
4+
import boto3 # type: ignore
5+
import pytest # type: ignore
6+
from pytest_mock import mocker
7+
8+
from redshift_connector import InterfaceError
9+
from redshift_connector.auth.aws_credentials_provider import (
10+
AWSCredentialsProvider,
11+
AWSDirectCredentialsHolder,
12+
)
13+
from redshift_connector.credentials_holder import AWSProfileCredentialsHolder
14+
from redshift_connector.redshift_property import RedshiftProperty
15+
16+
17+
def _make_aws_credentials_obj_with_profile() -> AWSCredentialsProvider:
18+
cred_provider: AWSCredentialsProvider = AWSCredentialsProvider()
19+
rp: RedshiftProperty = RedshiftProperty()
20+
profile_name: str = "myProfile"
21+
22+
rp.profile = profile_name
23+
24+
cred_provider.add_parameter(rp)
25+
return cred_provider
26+
27+
28+
def _make_aws_credentials_obj_with_credentials() -> AWSCredentialsProvider:
29+
cred_provider: AWSCredentialsProvider = AWSCredentialsProvider()
30+
rp: RedshiftProperty = RedshiftProperty()
31+
access_key_id: str = "my_access"
32+
secret_key: str = "my_secret"
33+
session_token: str = "my_session"
34+
35+
rp.access_key_id = access_key_id
36+
rp.secret_access_key = secret_key
37+
rp.session_token = session_token
38+
39+
cred_provider.add_parameter(rp)
40+
return cred_provider
41+
42+
43+
def test_create_aws_credentials_provider_with_profile():
44+
cred_provider: AWSCredentialsProvider = _make_aws_credentials_obj_with_profile()
45+
assert cred_provider.profile == "myProfile"
46+
assert cred_provider.access_key_id is None
47+
assert cred_provider.secret_access_key is None
48+
assert cred_provider.session_token is None
49+
50+
51+
def test_create_aws_credentials_provider_with_credentials():
52+
cred_provider: AWSCredentialsProvider = _make_aws_credentials_obj_with_credentials()
53+
assert cred_provider.profile is None
54+
assert cred_provider.access_key_id == "my_access"
55+
assert cred_provider.secret_access_key == "my_secret"
56+
assert cred_provider.session_token == "my_session"
57+
58+
59+
def test_get_cache_key_with_profile():
60+
cred_provider: AWSCredentialsProvider = _make_aws_credentials_obj_with_profile()
61+
assert cred_provider.get_cache_key() == hash(cred_provider.profile)
62+
63+
64+
def test_get_cache_key_with_credentials():
65+
cred_provider: AWSCredentialsProvider = _make_aws_credentials_obj_with_credentials()
66+
assert cred_provider.get_cache_key() == hash("my_access")
67+
68+
69+
def test_get_credentials_checks_cache_first(mocker):
70+
mocked_credential_holder = MagicMock()
71+
72+
def mock_set_cache(cp: AWSCredentialsProvider, key: str = "tomato") -> None:
73+
cp.cache[key] = mocked_credential_holder # type: ignore
74+
75+
cred_provider: AWSCredentialsProvider = AWSCredentialsProvider()
76+
mocker.patch("redshift_connector.auth.AWSCredentialsProvider.get_cache_key", return_value="tomato")
77+
78+
with patch("redshift_connector.auth.AWSCredentialsProvider.refresh") as mocked_refresh:
79+
mocked_refresh.side_effect = mock_set_cache(cred_provider)
80+
get_cache_key_spy = mocker.spy(cred_provider, "get_cache_key")
81+
82+
assert cred_provider.get_credentials() == mocked_credential_holder
83+
84+
assert get_cache_key_spy.called is True
85+
assert get_cache_key_spy.call_count == 1
86+
87+
88+
def test_get_credentials_refresh_error_is_raised(mocker):
89+
cred_provider: AWSCredentialsProvider = AWSCredentialsProvider()
90+
mocker.patch("redshift_connector.auth.AWSCredentialsProvider.get_cache_key", return_value="tomato")
91+
expected_exception = "something went wrong"
92+
93+
with patch("redshift_connector.auth.AWSCredentialsProvider.refresh") as mocked_refresh:
94+
mocked_refresh.side_effect = Exception(expected_exception)
95+
96+
with pytest.raises(InterfaceError, match=expected_exception):
97+
cred_provider.get_credentials()
98+
99+
100+
def test_refresh_uses_profile_if_present(mocker):
101+
cred_provider: AWSCredentialsProvider = _make_aws_credentials_obj_with_profile()
102+
mocked_boto_session: MagicMock = MagicMock()
103+
104+
with patch("boto3.Session", return_value=mocked_boto_session):
105+
cred_provider.refresh()
106+
107+
assert hash("myProfile") in cred_provider.cache
108+
assert isinstance(cred_provider.cache[hash("myProfile")], AWSProfileCredentialsHolder)
109+
assert cred_provider.cache[hash("myProfile")].profile == "myProfile"
110+
111+
112+
def test_refresh_uses_credentials_if_present(mocker):
113+
cred_provider: AWSCredentialsProvider = _make_aws_credentials_obj_with_credentials()
114+
mocked_boto_session: MagicMock = MagicMock()
115+
116+
with patch("boto3.Session", return_value=mocked_boto_session):
117+
cred_provider.refresh()
118+
119+
assert hash("my_access") in cred_provider.cache
120+
assert isinstance(cred_provider.cache[hash("my_access")], AWSDirectCredentialsHolder)
121+
assert cred_provider.cache[hash("my_access")].access_key_id == "my_access"
122+
assert cred_provider.cache[hash("my_access")].secret_access_key == "my_secret"
123+
assert cred_provider.cache[hash("my_access")].session_token == "my_session"

test/unit/plugin/test_credentials_providers.py

+2-3
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,7 @@ def test_ssl_and_iam_invalid_should_fail(idp_arg):
3838
idp_arg["credentials_provider"] = "OktacredentialSProvider"
3939
with pytest.raises(
4040
redshift_connector.InterfaceError,
41-
match="Invalid connection property setting. IAM must be enabled when using credentials "
42-
"via identity provider",
41+
match="Invalid connection property setting",
4342
):
4443
redshift_connector.connect(**idp_arg)
4544

@@ -48,6 +47,6 @@ def test_ssl_and_iam_invalid_should_fail(idp_arg):
4847
idp_arg["credentials_provider"] = None
4948
with pytest.raises(
5049
redshift_connector.InterfaceError,
51-
match="Invalid connection property setting. " "Credentials provider cannot be None when IAM is enabled",
50+
match="Invalid connection property setting",
5251
):
5352
redshift_connector.connect(**idp_arg)

test/unit/test_credentials_holder.py

+65
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import typing
2+
from unittest.mock import MagicMock
3+
4+
import pytest # type: ignore
5+
from pytest_mock import mocker
6+
7+
from redshift_connector.credentials_holder import (
8+
ABCAWSCredentialsHolder,
9+
AWSDirectCredentialsHolder,
10+
AWSProfileCredentialsHolder,
11+
)
12+
13+
14+
def test_aws_direct_credentials_holder_should_have_session():
15+
mocked_session: MagicMock = MagicMock()
16+
obj: AWSDirectCredentialsHolder = AWSDirectCredentialsHolder(
17+
access_key_id="something", secret_access_key="secret", session_token="fornow", session=mocked_session
18+
)
19+
20+
assert isinstance(obj, ABCAWSCredentialsHolder)
21+
assert hasattr(obj, "get_boto_session")
22+
assert obj.has_associated_session == True
23+
assert obj.get_boto_session() == mocked_session
24+
25+
26+
valid_aws_direct_credential_params: typing.List[typing.Dict[str, typing.Optional[str]]] = [
27+
{"access_key_id": "something", "secret_access_key": "secret", "session_token": "fornow"},
28+
{"access_key_id": "something", "secret_access_key": "secret", "session_token": None},
29+
]
30+
31+
32+
@pytest.mark.parametrize("input", valid_aws_direct_credential_params)
33+
def test_aws_direct_credentials_holder_get_session_credentials(input):
34+
input["session"] = MagicMock()
35+
obj: AWSDirectCredentialsHolder = AWSDirectCredentialsHolder(**input)
36+
37+
ret_value: typing.Dict[str, str] = obj.get_session_credentials()
38+
39+
assert len(ret_value) == 3 if input["session_token"] is not None else 2
40+
41+
assert ret_value["aws_access_key_id"] == input["access_key_id"]
42+
assert ret_value["aws_secret_access_key"] == input["secret_access_key"]
43+
44+
if input["session_token"] is not None:
45+
assert ret_value["aws_session_token"] == input["session_token"]
46+
47+
48+
def test_aws_profile_credentials_holder_should_have_session():
49+
mocked_session: MagicMock = MagicMock()
50+
obj: AWSProfileCredentialsHolder = AWSProfileCredentialsHolder(profile="myprofile", session=mocked_session)
51+
52+
assert isinstance(obj, ABCAWSCredentialsHolder)
53+
assert hasattr(obj, "get_boto_session")
54+
assert obj.has_associated_session == True
55+
assert obj.get_boto_session() == mocked_session
56+
57+
58+
def test_aws_profile_credentials_holder_get_session_credentials():
59+
profile_val: str = "myprofile"
60+
obj: AWSProfileCredentialsHolder = AWSProfileCredentialsHolder(profile=profile_val, session=MagicMock())
61+
62+
ret_value = obj.get_session_credentials()
63+
assert len(ret_value) == 1
64+
65+
assert ret_value["profile"] == profile_val

test/unit/test_iam_helper.py

+113-5
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from pytest_mock import mocker
55

66
from redshift_connector import InterfaceError, RedshiftProperty, set_iam_properties
7+
from redshift_connector.auth import AWSCredentialsProvider
78
from redshift_connector.config import ClientProtocolVersion
89
from redshift_connector.iam_helper import set_iam_credentials
910
from redshift_connector.plugin import (
@@ -40,7 +41,7 @@ def mock_all_provider_get_credentials(mocker):
4041
mocker.patch("redshift_connector.plugin.{}.get_credentials".format(provider), return_value=None)
4142

4243

43-
def get_set_iam_properties_args(**kwargs):
44+
def get_set_iam_properties_args(**kwargs) -> typing.Dict[str, typing.Any]:
4445
return {
4546
"info": RedshiftProperty(),
4647
"user": "awsuser",
@@ -80,6 +81,10 @@ def get_set_iam_properties_args(**kwargs):
8081
"allow_db_user_override": True,
8182
"client_protocol_version": ClientProtocolVersion.BASE_SERVER,
8283
"database_metadata_current_db_only": True,
84+
"access_key_id": None,
85+
"secret_access_key": None,
86+
"session_token": None,
87+
"profile": None,
8388
"ssl_insecure": None,
8489
**kwargs,
8590
}
@@ -138,19 +143,75 @@ def test_set_iam_properties_enforce_client_protocol_version(_input):
138143
({"ssl": False, "iam": True}, "Invalid connection property setting. SSL must be enabled when using IAM"),
139144
(
140145
{"iam": False, "credentials_provider": "anything"},
141-
"Invalid connection property setting. IAM must be enabled when using credentials via identity provider",
146+
"Invalid connection property setting",
147+
),
148+
(
149+
{"iam": False, "profile": "default"},
150+
"Invalid connection property setting",
151+
),
152+
(
153+
{"iam": False, "access_key_id": "my_key"},
154+
"Invalid connection property setting",
155+
),
156+
(
157+
{"iam": False, "secret_access_key": "shh it's a secret"},
158+
"Invalid connection property setting",
159+
),
160+
(
161+
{"iam": False, "session_token": "my_session"},
162+
"Invalid connection property setting",
142163
),
143164
(
144165
{"iam": True, "ssl": True},
145-
"Invalid connection property setting. Credentials provider cannot be None when IAM is enabled",
166+
"Invalid connection property setting",
167+
),
168+
(
169+
{"iam": True, "ssl": True, "access_key_id": "my_key", "credentials_provider": "OktaCredentialsProvider"},
170+
"Invalid connection property setting",
171+
),
172+
(
173+
{"iam": True, "ssl": True, "secret_access_key": "my_secret", "credentials_provider": "OktaCredentialsProvider"},
174+
"Invalid connection property setting",
175+
),
176+
(
177+
{"iam": True, "ssl": True, "session_token": "token", "credentials_provider": "OktaCredentialsProvider"},
178+
"Invalid connection property setting",
179+
),
180+
(
181+
{"iam": True, "ssl": True, "profile": "default", "credentials_provider": "OktaCredentialsProvider"},
182+
"Invalid connection property setting",
183+
),
184+
(
185+
{"iam": True, "ssl": True, "profile": "default", "access_key_id": "my_key"},
186+
"Invalid connection property setting",
187+
),
188+
(
189+
{"iam": True, "ssl": True, "profile": "default", "secret_access_key": "my_secret"},
190+
"Invalid connection property setting",
191+
),
192+
(
193+
{"iam": True, "ssl": True, "profile": "default", "session_token": "token"},
194+
"Invalid connection property setting",
195+
),
196+
(
197+
{"iam": True, "ssl": True, "secret_access_key": "my_secret"},
198+
"Invalid connection property setting",
199+
),
200+
(
201+
{"iam": True, "ssl": True, "session_token": "token"},
202+
"Invalid connection property setting",
203+
),
204+
(
205+
{"iam": True, "ssl": True, "access_key_id": "my_key", "password": ""},
206+
"Invalid connection property setting",
146207
),
147208
(
148209
{"iam": False, "ssl_insecure": False},
149-
"Invalid connection property setting. IAM must be enabled when using ssl_insecure",
210+
"Invalid connection property setting",
150211
),
151212
(
152213
{"iam": False, "ssl_insecure": True},
153-
"Invalid connection property setting. IAM must be enabled when using ssl_insecure",
214+
"Invalid connection property setting",
154215
),
155216
]
156217

@@ -213,3 +274,50 @@ def test_set_iam_properties_provider_assigned(mocker, provider):
213274
assert spy.call_count == 1
214275
# ensure call to add_Parameter was made on the expected Provider class
215276
assert isinstance(spy.call_args[0][0], expectedProvider) is True
277+
278+
279+
valid_aws_credential_args: typing.List[typing.Dict[str, str]] = [
280+
{"profile": "default"},
281+
{"access_key_id": "myAccessKey", "secret_access_key": "mySecret"},
282+
{"access_key_id": "myAccessKey", "password": "myHiddenSecret"},
283+
{"access_key_id": "myAccessKey", "secret_access_key": "mySecret", "session_token": "mySession"},
284+
]
285+
286+
287+
@pytest.mark.parametrize("test_input", valid_aws_credential_args)
288+
def test_set_iam_properties_via_aws_credentials(mocker, test_input):
289+
# spy = mocker.spy("redshift_connector", "set_iam_credentials")
290+
info_obj: typing.Dict[str, typing.Any] = get_set_iam_properties_args(**test_input)
291+
info_obj["ssl"] = True
292+
info_obj["iam"] = True
293+
294+
mocker.patch("redshift_connector.iam_helper.set_iam_credentials", return_value=None)
295+
set_iam_properties(**info_obj)
296+
297+
for aws_cred_key, aws_cred_val in enumerate(test_input):
298+
if aws_cred_key == "profile":
299+
assert info_obj["info"].profile == aws_cred_val
300+
if aws_cred_key == "access_key_id":
301+
assert info_obj["info"].access_key_id == aws_cred_val
302+
if aws_cred_key == "secret_access_key":
303+
assert info_obj["info"].secret_access_key == aws_cred_val
304+
if aws_cred_key == "password":
305+
assert info_obj["info"].password == aws_cred_val
306+
if aws_cred_key == "session_token":
307+
assert info_obj["info"].session_token == aws_cred_val
308+
309+
310+
def test_set_iam_credentials_via_aws_credentials(mocker):
311+
redshift_property: RedshiftProperty = RedshiftProperty()
312+
redshift_property.profile = "profile_val"
313+
redshift_property.access_key_id = "access_val"
314+
redshift_property.secret_access_key = "secret_val"
315+
redshift_property.session_token = "session_val"
316+
317+
mocker.patch("redshift_connector.iam_helper.set_cluster_credentials", return_value=None)
318+
spy = mocker.spy(AWSCredentialsProvider, "add_parameter")
319+
320+
set_iam_credentials(redshift_property)
321+
assert spy.called is True
322+
assert spy.call_count == 1
323+
assert spy.call_args[0][1] == redshift_property

0 commit comments

Comments
 (0)