33Licensed under the Apache License, Version 2.0 (the "License");
44you may not use this file except in compliance with the License.
55"""
6- import base64
76import re
8- import sys
9- import platform
10- import xml .etree .ElementTree as ET
117from collections import namedtuple
128from urllib .parse import quote
139
1410import 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
1714try :
1815 from alibabacloud_sts20150401 import client as _sts_client
2320except ImportError :
2421 ALIBABA_CLOUD_SDK_AVAILABLE = False
2522
26- from . import errors , version
23+ from . import errors
2724
2825ALIBABA_CLOUD_SAML_ROLE_ATTRIBUTE = 'https://www.aliyun.com/SAML-Role/Attributes/Role'
2926
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-
4835def _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
0 commit comments