Skip to content

Commit 09b2f96

Browse files
committed
Update Python version requirement and refactor common functions
- Updated the Python version requirement in README.md from 3.7+ to 3.10+. - Refactored common functions in common.py to improve code organization and reusability, including user agent and request header generation. - Enhanced the handling of SAML responses and role attributes across multiple files, improving code clarity and maintainability. - Updated various classes to utilize the new common functions for consistency in HTTP requests and SAML processing.
1 parent 17cb4c4 commit 09b2f96

23 files changed

Lines changed: 583 additions & 1222 deletions

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ Okta is a registered trademark of Okta, Inc. and this tool has no affiliation wi
1616

1717
[Okta SAML integration to AWS using the AWS App](https://help.okta.com/en/prod/Content/Topics/Miscellaneous/References/OktaAWSMulti-AccountConfigurationGuide.pdf)
1818

19-
Python 3.7+
19+
Python 3.10+
2020

2121
#### A Note on Python 3.10+ Compatibility on Windows
2222

docs/index.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
**Type:** CLI (Python command-line tool)
88
**Language:** Python 3.7+
99
**Architecture:** Modular authentication system with pluggable MFA providers
10-
**Version:** 2.8.2
10+
**Released Version:** 2.8.2
11+
**Latest Version:** 2.9.0-pre
1112

1213
### Quick Reference
1314

docs/project-overview.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99
| Property | Value |
1010
|----------|-------|
1111
| **Name** | gimme-aws-creds |
12-
| **Version** | 2.8.2 |
12+
| **Released Version** | 2.8.2 |
13+
| **Latest Version** | 2.9.0-pre |
1314
| **License** | Apache License 2.0 |
1415
| **Python** | 3.7+ |
1516
| **Maintainer** | Eric Pierce |

gimme_aws_creds/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
__all__ = ['alibaba_cloud', 'config', 'aws', 'main', 'ui', 'common', 'default', 'duo', 'errors', 'okta_classic', 'okta_identity_engine', 'registered_authenticators', 'u2f', 'webauthn']
2-
version = '2.8.2-pre'
2+
version = '2.9.0-pre'

gimme_aws_creds/alibaba_cloud.py

Lines changed: 26 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,13 @@
33
Licensed under the Apache License, Version 2.0 (the "License");
44
you may not use this file except in compliance with the License.
55
"""
6-
import base64
76
import re
8-
import sys
9-
import platform
10-
import xml.etree.ElementTree as ET
117
from collections import namedtuple
128
from urllib.parse import quote
139

1410
import requests
15-
from bs4 import BeautifulSoup
11+
12+
from .common import user_agent, request_headers_json, parse_saml_form, parse_saml_role_attributes, okta_token_exchange
1613

1714
try:
1815
from alibabacloud_sts20150401 import client as _sts_client
@@ -23,7 +20,7 @@
2320
except ImportError:
2421
ALIBABA_CLOUD_SDK_AVAILABLE = False
2522

26-
from . import errors, version
23+
from . import errors
2724

2825
ALIBABA_CLOUD_SAML_ROLE_ATTRIBUTE = 'https://www.aliyun.com/SAML-Role/Attributes/Role'
2926

@@ -35,23 +32,13 @@
3532
)
3633

3734

38-
def _user_agent():
39-
return "gimme-aws-creds {};{};{}".format(version, sys.platform, platform.python_version())
40-
41-
42-
def _request_headers_json():
43-
return {
44-
'User-Agent': _user_agent(),
45-
'Accept': 'application/json',
46-
}
47-
4835
def _account_id_from_role_arn(role_arn):
4936
m = re.match(r'acs:ram::(\d+):', role_arn)
5037
if not m:
5138
return ''
5239
return m.group(1)
5340

54-
class AlibabaCloudClient(object):
41+
class AlibabaCloudClient:
5542
"""Alibaba Cloud RAM credentials via Okta Native-to-Web SSO (interclient token) and STS AssumeRoleWithSAML."""
5643

5744
HTTP_TIMEOUT = 30
@@ -71,36 +58,12 @@ def __init__(self, http_client, okta_org_url, client_id, verify_ssl_certs=True):
7158
self._verify_ssl_certs = verify_ssl_certs
7259

7360
def _interclient_token_exchange(self, app_id, access_token, id_token):
74-
response = self._http_client.post(
75-
self._okta_org_url + '/oauth2/v1/token',
76-
headers=_request_headers_json(),
77-
data={
78-
'actor_token': access_token,
79-
'actor_token_type': 'urn:ietf:params:oauth:token-type:access_token',
80-
'client_id': self._client_id,
81-
'audience': 'urn:okta:apps:{}'.format(app_id),
82-
'grant_type': 'urn:ietf:params:oauth:grant-type:token-exchange',
83-
'requested_token_type': 'urn:okta:params:oauth:token-type:interclient_token',
84-
'subject_token': id_token,
85-
'subject_token_type': 'urn:ietf:params:oauth:token-type:id_token',
86-
},
87-
verify=self._verify_ssl_certs,
88-
timeout=self.HTTP_TIMEOUT,
61+
return okta_token_exchange(
62+
self._http_client, self._okta_org_url, self._client_id,
63+
app_id, access_token, id_token,
64+
requested_token_type='urn:okta:params:oauth:token-type:interclient_token',
65+
verify_ssl=self._verify_ssl_certs, timeout=self.HTTP_TIMEOUT,
8966
)
90-
try:
91-
response_data = response.json()
92-
except ValueError as e:
93-
raise errors.GimmeAWSCredsError(
94-
'Invalid JSON response from token exchange endpoint: {}'.format(str(e)), 2)
95-
96-
if response.status_code == 200:
97-
return response_data
98-
if response.status_code == 400:
99-
raise errors.GimmeAWSCredsError(
100-
'LOGIN ERROR: Interclient token exchange failed: {}'.format(
101-
response_data.get('error_description', 'Unknown error')), 2)
102-
response.raise_for_status()
103-
return None
10467

10568
@staticmethod
10669
def _saml_app_fetch_url(saml_app_url, interclient_token):
@@ -117,31 +80,22 @@ def get_saml_response(self, saml_sso_url, saml_app_url, auth_session):
11780
fetch_url = self._saml_app_fetch_url(saml_sso_url, interclient_token)
11881
response = self._http_client.get(
11982
fetch_url,
120-
headers=_request_headers_json(),
83+
headers=request_headers_json(),
12184
verify=self._verify_ssl_certs,
12285
timeout=self.HTTP_TIMEOUT,
12386
)
12487

12588
if response.status_code != 200:
12689
response.raise_for_status()
12790

128-
saml_response = None
129-
relay_state = None
130-
form_action = None
131-
132-
saml_soup = BeautifulSoup(response.text, 'html.parser')
133-
if saml_soup.find('form') is not None:
134-
form_action = saml_soup.find('form').get('action')
135-
for input_tag in saml_soup.find_all('input'):
136-
if input_tag.get('name') == 'SAMLResponse':
137-
saml_response = input_tag.get('value')
138-
elif input_tag.get('name') == 'RelayState':
139-
relay_state = input_tag.get('value')
91+
saml_response, relay_state, form_action = parse_saml_form(response.text)
14092

14193
if saml_response is None:
14294
saml_error = 'Did not receive SAML Response after successful authentication [{}]'.format(saml_app_url)
143-
if saml_soup.find(class_='error-content') is not None:
144-
saml_error += '\n' + saml_soup.find(class_='error-content').get_text()
95+
from bs4 import BeautifulSoup
96+
error_soup = BeautifulSoup(response.text, 'html.parser')
97+
if error_soup.find(class_='error-content') is not None:
98+
saml_error += '\n' + error_soup.find(class_='error-content').get_text()
14599
raise errors.GimmeAWSCredsError(saml_error, 2)
146100

147101
return {'SAMLResponse': saml_response, 'RelayState': relay_state, 'TargetUrl': form_action}
@@ -153,25 +107,18 @@ def enumerate_saml_roles(assertion_b64):
153107

154108
@staticmethod
155109
def _enumerate_saml_roles_impl(assertion_b64):
156-
root = ET.fromstring(base64.b64decode(assertion_b64))
157110
roles = []
158-
for attr in root.iter('{urn:oasis:names:tc:SAML:2.0:assertion}Attribute'):
159-
if attr.get('Name') != ALIBABA_CLOUD_SAML_ROLE_ATTRIBUTE:
160-
continue
161-
for val in attr.iter('{urn:oasis:names:tc:SAML:2.0:assertion}AttributeValue'):
162-
text = (val.text or '').strip()
163-
if not text:
164-
continue
165-
parts = [p.strip() for p in text.split(',')]
166-
if len(parts) != 2:
167-
raise errors.GimmeAWSCredsError(
168-
'Invalid Alibaba Cloud role pair (expected role_arn,saml_provider_arn): {}'.format(text), 2)
169-
role_arn, saml_provider_arn = parts[0], parts[1]
170-
roles.append(AlibabaCloudRoleSet(
171-
role_arn=role_arn,
172-
saml_provider_arn=saml_provider_arn,
173-
account_id=_account_id_from_role_arn(role_arn),
174-
))
111+
for text in parse_saml_role_attributes(assertion_b64, ALIBABA_CLOUD_SAML_ROLE_ATTRIBUTE):
112+
parts = [p.strip() for p in text.split(',')]
113+
if len(parts) != 2:
114+
raise errors.GimmeAWSCredsError(
115+
'Invalid Alibaba Cloud role pair (expected role_arn,saml_provider_arn): {}'.format(text), 2)
116+
role_arn, saml_provider_arn = parts[0], parts[1]
117+
roles.append(AlibabaCloudRoleSet(
118+
role_arn=role_arn,
119+
saml_provider_arn=saml_provider_arn,
120+
account_id=_account_id_from_role_arn(role_arn),
121+
))
175122
return roles
176123

177124
ASSUME_ROLE_MAX_DURATION = 3600

gimme_aws_creds/aws.py

Lines changed: 4 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -11,19 +11,15 @@
1111
"""
1212
import base64
1313
import json
14-
import xml.etree.ElementTree as ET
1514

16-
import urllib3
17-
import requests
1815
from bs4 import BeautifulSoup
19-
from requests.adapters import HTTPAdapter
20-
from urllib3.util.retry import Retry
2116

2217
import gimme_aws_creds.common as commondef
18+
from .common import parse_saml_role_attributes, create_http_session
2319
from . import errors
2420

2521

26-
class AwsResolver(object):
22+
class AwsResolver:
2723
"""
2824
The Aws Client Class performes post request on AWS sign-in page
2925
to fetch friendly names/alias for account and IAM roles
@@ -34,15 +30,7 @@ def __init__(self, verify_ssl_certs=True):
3430
:param verify_ssl_certs: Enable/disable SSL verification
3531
"""
3632
self._verify_ssl_certs = verify_ssl_certs
37-
38-
if verify_ssl_certs is False:
39-
urllib3.disable_warnings()
40-
41-
# Allow up to 5 retries on requests to AWS in case we have network issues
42-
self._http_client = requests.Session()
43-
retries = Retry(total=5, backoff_factor=1,
44-
allowed_methods=['POST'])
45-
self._http_client.mount('https://', HTTPAdapter(max_retries=retries))
33+
self._http_client = create_http_session(verify_ssl=verify_ssl_certs, allowed_methods=['POST'])
4634

4735
def get_signinpage(self, saml_token, saml_target_url):
4836
""" Post SAML token to aws sign in page and get back html result"""
@@ -62,12 +50,7 @@ def _enumerate_saml_roles(self, assertion, saml_target_url):
6250
signin_page = self.get_signinpage(assertion, saml_target_url)
6351

6452
""" using the assertion to fetch aws sign-in page, parse it and return aws sts creds """
65-
role_pairs = []
66-
root = ET.fromstring(base64.b64decode(assertion))
67-
for saml2_attribute in root.iter('{urn:oasis:names:tc:SAML:2.0:assertion}Attribute'):
68-
if saml2_attribute.get('Name') == 'https://aws.amazon.com/SAML/Attributes/Role':
69-
for saml2_attribute_value in saml2_attribute.iter('{urn:oasis:names:tc:SAML:2.0:assertion}AttributeValue'):
70-
role_pairs.append(saml2_attribute_value.text)
53+
role_pairs = list(parse_saml_role_attributes(assertion, 'https://aws.amazon.com/SAML/Attributes/Role'))
7154

7255
# build a temp hash table
7356
table = {}

0 commit comments

Comments
 (0)