Skip to content

Commit 6984349

Browse files
authored
expose credentials expiration (#219)
1 parent f46d7e5 commit 6984349

File tree

5 files changed

+74
-12
lines changed

5 files changed

+74
-12
lines changed

awscrt/auth.py

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,22 +24,47 @@ class AwsCredentials(NativeResource):
2424
Args:
2525
access_key_id (str): Access key ID
2626
secret_access_key (str): Secret access key
27-
session_token (Optional[str]): Session token
27+
session_token (Optional[str]): Optional security token associated with
28+
the credentials.
29+
expiration (Optional[datetime.datetime]): Optional expiration datetime,
30+
that the credentials will no longer be valid past.
31+
Converted to UTC timezone and rounded down to nearest second.
32+
If not set, then credentials do not expire.
2833
2934
Attributes:
3035
access_key_id (str): Access key ID
3136
secret_access_key (str): Secret access key
32-
session_token (Optional[str]): Session token
37+
session_token (Optional[str]): Security token associated with
38+
the credentials. None if not set.
39+
expiration (Optional[datetime.datetime]): Expiration datetime,
40+
that the credentials will no longer be valid past.
41+
None if credentials do not expire.
42+
Timezone is always UTC.
3343
"""
3444
__slots__ = ()
3545

36-
def __init__(self, access_key_id, secret_access_key, session_token=None):
46+
# C layer uses UINT64_MAX as timestamp for non-expiring credentials
47+
_NONEXPIRING_TIMESTAMP = 0xFFFFFFFFFFFFFFFF
48+
49+
def __init__(self, access_key_id, secret_access_key, session_token=None, expiration=None):
3750
assert isinstance(access_key_id, str)
3851
assert isinstance(secret_access_key, str)
3952
assert isinstance(session_token, str) or session_token is None
4053

54+
# C layer uses large int as timestamp for non-expiring credentials
55+
if expiration is None:
56+
expiration_timestamp = self._NONEXPIRING_TIMESTAMP
57+
else:
58+
expiration_timestamp = int(expiration.timestamp())
59+
if expiration_timestamp < 0 or expiration_timestamp >= self._NONEXPIRING_TIMESTAMP:
60+
raise OverflowError("expiration datetime out of range")
61+
4162
super().__init__()
42-
self._binding = _awscrt.credentials_new(access_key_id, secret_access_key, session_token)
63+
self._binding = _awscrt.credentials_new(
64+
access_key_id,
65+
secret_access_key,
66+
session_token,
67+
expiration_timestamp)
4368

4469
@classmethod
4570
def _from_binding(cls, binding):
@@ -61,6 +86,15 @@ def secret_access_key(self):
6186
def session_token(self):
6287
return _awscrt.credentials_session_token(self._binding)
6388

89+
@property
90+
def expiration(self):
91+
timestamp = _awscrt.credentials_expiration_timestamp_seconds(self._binding)
92+
# C layer uses large int as timestamp for non-expiring credentials
93+
if timestamp == self._NONEXPIRING_TIMESTAMP:
94+
return None
95+
else:
96+
return datetime.datetime.fromtimestamp(timestamp, tz=datetime.timezone.utc)
97+
6498
def __deepcopy__(self, memo):
6599
# AwsCredentials is immutable, so just return self.
66100
return self

source/auth.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ PyObject *aws_py_credentials_new(PyObject *self, PyObject *args);
1212
PyObject *aws_py_credentials_access_key_id(PyObject *self, PyObject *args);
1313
PyObject *aws_py_credentials_secret_access_key(PyObject *self, PyObject *args);
1414
PyObject *aws_py_credentials_session_token(PyObject *self, PyObject *args);
15+
PyObject *aws_py_credentials_expiration_timestamp_seconds(PyObject *self, PyObject *args);
1516

1617
PyObject *aws_py_credentials_provider_get_credentials(PyObject *self, PyObject *args);
1718
PyObject *aws_py_credentials_provider_new_chain_default(PyObject *self, PyObject *args);

source/auth_credentials.c

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,24 +26,22 @@ PyObject *aws_py_credentials_new(PyObject *self, PyObject *args) {
2626
struct aws_byte_cursor access_key_id;
2727
struct aws_byte_cursor secret_access_key;
2828
struct aws_byte_cursor session_token; /* session_token is optional */
29+
uint64_t expiration_timestamp_sec;
2930
if (!PyArg_ParseTuple(
3031
args,
31-
"s#s#z#",
32+
"s#s#z#K",
3233
&access_key_id.ptr,
3334
&access_key_id.len,
3435
&secret_access_key.ptr,
3536
&secret_access_key.len,
3637
&session_token.ptr,
37-
&session_token.len)) {
38+
&session_token.len,
39+
&expiration_timestamp_sec)) {
3840
return NULL;
3941
}
4042

4143
struct aws_credentials *credentials = aws_credentials_new(
42-
aws_py_get_allocator(),
43-
access_key_id,
44-
secret_access_key,
45-
session_token,
46-
UINT64_MAX /*expiration_timepoint_seconds*/);
44+
aws_py_get_allocator(), access_key_id, secret_access_key, session_token, expiration_timestamp_sec);
4745
if (!credentials) {
4846
return PyErr_AwsLastError();
4947
}
@@ -115,6 +113,23 @@ PyObject *aws_py_credentials_session_token(PyObject *self, PyObject *args) {
115113
return s_credentials_get_member_str(args, CREDENTIALS_MEMBER_SESSION_TOKEN);
116114
}
117115

116+
PyObject *aws_py_credentials_expiration_timestamp_seconds(PyObject *self, PyObject *args) {
117+
(void)self;
118+
119+
PyObject *capsule;
120+
if (!PyArg_ParseTuple(args, "O", &capsule)) {
121+
return NULL;
122+
}
123+
124+
const struct aws_credentials *credentials = PyCapsule_GetPointer(capsule, s_capsule_name_credentials);
125+
if (!credentials) {
126+
return NULL;
127+
}
128+
129+
uint64_t timestamp = aws_credentials_get_expiration_timepoint_seconds(credentials);
130+
return PyLong_FromUnsignedLongLong(timestamp);
131+
}
132+
118133
/**
119134
* Binds a Python CredentialsProvider to a native aws_credentials_provider.
120135
*/

source/module.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -543,6 +543,7 @@ static PyMethodDef s_module_methods[] = {
543543
AWS_PY_METHOD_DEF(credentials_access_key_id, METH_VARARGS),
544544
AWS_PY_METHOD_DEF(credentials_secret_access_key, METH_VARARGS),
545545
AWS_PY_METHOD_DEF(credentials_session_token, METH_VARARGS),
546+
AWS_PY_METHOD_DEF(credentials_expiration_timestamp_seconds, METH_VARARGS),
546547
AWS_PY_METHOD_DEF(credentials_provider_get_credentials, METH_VARARGS),
547548
AWS_PY_METHOD_DEF(credentials_provider_new_chain_default, METH_VARARGS),
548549
AWS_PY_METHOD_DEF(credentials_provider_new_static, METH_VARARGS),

test/test_auth.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
EXAMPLE_ACCESS_KEY_ID = 'example_access_key_id'
1414
EXAMPLE_SECRET_ACCESS_KEY = 'example_secret_access_key'
1515
EXAMPLE_SESSION_TOKEN = 'example_session_token'
16+
EXAMPLE_SESSION_EXPIRATION = datetime.datetime.fromtimestamp(1609911816, tz=datetime.timezone.utc)
1617

1718

1819
class ScopedEnvironmentVariable:
@@ -40,12 +41,14 @@ def test_create(self):
4041
credentials = awscrt.auth.AwsCredentials(
4142
EXAMPLE_ACCESS_KEY_ID,
4243
EXAMPLE_SECRET_ACCESS_KEY,
43-
EXAMPLE_SESSION_TOKEN)
44+
EXAMPLE_SESSION_TOKEN,
45+
EXAMPLE_SESSION_EXPIRATION)
4446

4547
# Don't use assertEqual(), which could log actual credentials if test fails.
4648
self.assertTrue(EXAMPLE_ACCESS_KEY_ID == credentials.access_key_id)
4749
self.assertTrue(EXAMPLE_SECRET_ACCESS_KEY == credentials.secret_access_key)
4850
self.assertTrue(EXAMPLE_SESSION_TOKEN == credentials.session_token)
51+
self.assertTrue(EXAMPLE_SESSION_EXPIRATION == credentials.expiration)
4952

5053
def test_create_no_session_token(self):
5154
credentials = awscrt.auth.AwsCredentials(EXAMPLE_ACCESS_KEY_ID, EXAMPLE_SECRET_ACCESS_KEY)
@@ -55,6 +58,14 @@ def test_create_no_session_token(self):
5558
self.assertTrue(EXAMPLE_SECRET_ACCESS_KEY == credentials.secret_access_key)
5659
self.assertTrue(credentials.session_token is None)
5760

61+
def test_create_no_expiration(self):
62+
credentials = awscrt.auth.AwsCredentials(EXAMPLE_ACCESS_KEY_ID, EXAMPLE_SECRET_ACCESS_KEY)
63+
64+
# Don't use assertEqual(), which could log actual credentials if test fails.
65+
self.assertTrue(EXAMPLE_ACCESS_KEY_ID == credentials.access_key_id)
66+
self.assertTrue(EXAMPLE_SECRET_ACCESS_KEY == credentials.secret_access_key)
67+
self.assertTrue(credentials.expiration is None)
68+
5869

5970
class TestProvider(NativeResourceTest):
6071
def test_static_provider(self):

0 commit comments

Comments
 (0)