Unverified Commit 5062d495 by Matt Drayer Committed by GitHub

Merge pull request #16475 from edx/mattdrayer/ENT-686-3

mattdrayer/ENT-686: Enhanced logging for SuccessFactors exceptions
parents c02fd79f 6027e5fc
...@@ -231,10 +231,10 @@ class SapSuccessFactorsIdentityProvider(EdXSAMLIdentityProvider): ...@@ -231,10 +231,10 @@ class SapSuccessFactorsIdentityProvider(EdXSAMLIdentityProvider):
def odata_client_id(self): def odata_client_id(self):
return self.conf['odata_client_id'] return self.conf['odata_client_id']
def missing_variables(self): def invalid_configuration(self):
""" """
Check that we have all the details we need to properly retrieve rich data from the Check that we have all the details we need to properly retrieve rich data from the
SAP SuccessFactors OData API. If we don't, then we should log a warning indicating SAP SuccessFactors BizX OData API. If we don't, then we should log a warning indicating
the specific variables that are missing. the specific variables that are missing.
""" """
if not all(var in self.conf for var in self.required_variables): if not all(var in self.conf for var in self.required_variables):
...@@ -246,85 +246,136 @@ class SapSuccessFactorsIdentityProvider(EdXSAMLIdentityProvider): ...@@ -246,85 +246,136 @@ class SapSuccessFactorsIdentityProvider(EdXSAMLIdentityProvider):
) )
return missing return missing
def get_odata_api_client(self, user_id): def log_bizx_api_exception(self, transaction_data, err):
sys_msg = err.response.json() if err.response else 'Not available'
headers = err.response.headers if err.response else 'Not available'
token_data = transaction_data.get('token_data')
token_data = token_data if token_data else 'Not available'
log_msg_template = (
'SAPSuccessFactors exception received for {operation_name} request. ' +
'URL: {url} ' +
'Company ID: {company_id}. ' +
'User ID: {user_id}. ' +
'Error message: {err_msg}. ' +
'System message: {sys_msg}. ' +
'Headers: {headers}. ' +
'Token Data: {token_data}.'
)
log_msg = log_msg_template.format(
operation_name=transaction_data['operation_name'],
url=transaction_data['endpoint_url'],
company_id=transaction_data['company_id'],
user_id=transaction_data['user_id'],
err_msg=err.message,
sys_msg=sys_msg,
headers=headers,
token_data=token_data,
)
log.warning(log_msg, exc_info=True)
def generate_bizx_oauth_api_saml_assertion(self, user_id):
""" """
Get a Requests session with the headers needed to properly authenticate it with Obtain a SAML assertion from the SAP SuccessFactors BizX OAuth2 identity provider service using
the SAP SuccessFactors OData API. information specified in the third party authentication configuration "Advanced Settings" section.
""" """
session = requests.Session() session = requests.Session()
assertion = session.post( transaction_data = {
self.sapsf_idp_url, 'operation_name': 'generate_bizx_oauth_api_saml_assertion',
data={ 'endpoint_url': self.sapsf_idp_url,
'client_id': self.odata_client_id, 'token_url': self.sapsf_token_url,
'user_id': user_id, 'company_id': self.odata_company_id,
'token_url': self.sapsf_token_url, 'client_id': self.odata_client_id,
'private_key': self.sapsf_private_key, 'user_id': user_id,
}, 'private_key': self.sapsf_private_key,
timeout=self.timeout, }
) try:
assertion.raise_for_status() assertion = session.post(
assertion = assertion.text transaction_data['endpoint_url'],
token = session.post( data=transaction_data,
self.sapsf_token_url, timeout=self.timeout,
data={ )
'client_id': self.odata_client_id, assertion.raise_for_status()
'company_id': self.odata_company_id, except requests.RequestException as err:
'grant_type': 'urn:ietf:params:oauth:grant-type:saml2-bearer', self.log_bizx_api_exception(transaction_data, err)
'assertion': assertion, return None
}, return assertion.text
timeout=self.timeout,
) def generate_bizx_oauth_api_access_token(self, user_id):
token.raise_for_status() """
token = token.json()['access_token'] Request a new access token from the SuccessFactors BizX OAuth2 identity provider service
session.headers.update({'Authorization': 'Bearer {}'.format(token), 'Accept': 'application/json'}) using a valid SAML assertion (see generate_bizx_api_saml_assertion) and the infomration specified
in the third party authentication configuration "Advanced Settings" section.
"""
session = requests.Session()
transaction_data = {
'operation_name': 'generate_bizx_oauth_api_access_token',
'endpoint_url': self.sapsf_token_url,
'client_id': self.odata_client_id,
'company_id': self.odata_company_id,
'grant_type': 'urn:ietf:params:oauth:grant-type:saml2-bearer',
}
assertion = self.generate_bizx_oauth_api_saml_assertion(user_id)
if not assertion:
return None
try:
transaction_data['assertion'] = assertion
token_response = session.post(
transaction_data['endpoint_url'],
data=transaction_data,
timeout=self.timeout,
)
token_response.raise_for_status()
except requests.RequestException as err:
self.log_bizx_api_exception(transaction_data, err)
return None
return token_response.json()
def get_bizx_odata_api_client(self, user_id):
session = requests.Session()
access_token_data = self.generate_bizx_oauth_api_access_token(user_id)
if not access_token_data:
return None
token_string = access_token_data['access_token']
session.headers.update({'Authorization': 'Bearer {}'.format(token_string), 'Accept': 'application/json'})
session.token_data = access_token_data
return session return session
def get_user_details(self, attributes): def get_user_details(self, attributes):
""" """
Attempt to get rich user details from the SAP SuccessFactors OData API. If we're missing any Attempt to get rich user details from the SAP SuccessFactors OData API. If we're missing any
of the details we need to do that, fail nicely by returning the details we're able to extract of the info we need to do that, or if the request triggers an exception, then fail nicely by
from just the SAML response and log a warning. returning the basic user details we're able to extract from just the SAML response.
""" """
details = super(SapSuccessFactorsIdentityProvider, self).get_user_details(attributes) basic_details = super(SapSuccessFactorsIdentityProvider, self).get_user_details(attributes)
if self.missing_variables(): if self.invalid_configuration():
# If there aren't enough details to make the request, log a warning and return the details return basic_details
# from the SAML assertion. user_id = basic_details['username']
return details
username = details['username']
fields = ','.join(self.field_mappings) fields = ','.join(self.field_mappings)
odata_api_url = '{root_url}User(userId=\'{user_id}\')?$select={fields}'.format( transaction_data = {
root_url=self.odata_api_root_url, 'operation_name': 'get_user_details',
user_id=username, 'user_id': user_id,
fields=fields, 'company_id': self.odata_company_id,
) 'fields': fields,
'endpoint_url': '{root_url}User(userId=\'{user_id}\')?$select={fields}'.format(
root_url=self.odata_api_root_url,
user_id=user_id,
fields=fields,
),
}
client = self.get_bizx_odata_api_client(user_id=transaction_data['user_id'])
if not client:
return basic_details
try: try:
client = self.get_odata_api_client(user_id=username) transaction_data['token_data'] = client.token_data
response = client.get( response = client.get(
odata_api_url, transaction_data['endpoint_url'],
timeout=self.timeout, timeout=self.timeout,
) )
response.raise_for_status() response.raise_for_status()
response = response.json() response = response.json()
except requests.RequestException as err: except requests.RequestException as err:
# If there was an HTTP level error, log the error and return the details from the SAML assertion. self.log_bizx_api_exception(transaction_data, err)
sys_msg = err.response.json() if err.response else "Not available" return basic_details
headers = err.response.headers if err.response else "Not available"
log_msg_template = (
'Unable to retrieve user details with username {username} from SAPSuccessFactors for company ' +
'ID {company} with url "{url}". Error message: {err_msg}. System message: {sys_msg}. ' +
'Headers: {headers}'
)
log_msg = log_msg_template.format(
username=username,
company=self.odata_company_id,
url=odata_api_url,
err_msg=err.message,
sys_msg=sys_msg,
headers=headers
)
log.warning(log_msg, exc_info=True)
return details
return self.get_registration_fields(response) return self.get_registration_fields(response)
......
...@@ -504,7 +504,7 @@ class SuccessFactorsIntegrationTest(SamlIntegrationTestUtilities, IntegrationTes ...@@ -504,7 +504,7 @@ class SuccessFactorsIntegrationTest(SamlIntegrationTestUtilities, IntegrationTes
odata_company_id = 'NCC1701D' odata_company_id = 'NCC1701D'
odata_api_root_url = 'http://api.successfactors.com/odata/v2/' odata_api_root_url = 'http://api.successfactors.com/odata/v2/'
mocked_odata_ai_url = self._mock_odata_api_for_error(odata_api_root_url, self.USER_USERNAME) mocked_odata_api_url = self._mock_odata_api_for_error(odata_api_root_url, self.USER_USERNAME)
self._configure_testshib_provider( self._configure_testshib_provider(
identity_provider_type='sap_success_factors', identity_provider_type='sap_success_factors',
metadata_source=TESTSHIB_METADATA_URL, metadata_source=TESTSHIB_METADATA_URL,
...@@ -520,7 +520,7 @@ class SuccessFactorsIntegrationTest(SamlIntegrationTestUtilities, IntegrationTes ...@@ -520,7 +520,7 @@ class SuccessFactorsIntegrationTest(SamlIntegrationTestUtilities, IntegrationTes
super(SuccessFactorsIntegrationTest, self).test_register() super(SuccessFactorsIntegrationTest, self).test_register()
logging_messages = str([log_msg.getMessage() for log_msg in log_capture.records]).replace('\\', '') logging_messages = str([log_msg.getMessage() for log_msg in log_capture.records]).replace('\\', '')
self.assertIn(odata_company_id, logging_messages) self.assertIn(odata_company_id, logging_messages)
self.assertIn(mocked_odata_ai_url, logging_messages) self.assertIn(mocked_odata_api_url, logging_messages)
self.assertIn(self.USER_USERNAME, logging_messages) self.assertIn(self.USER_USERNAME, logging_messages)
self.assertIn("SAPSuccessFactors", logging_messages) self.assertIn("SAPSuccessFactors", logging_messages)
self.assertIn("Error message", logging_messages) self.assertIn("Error message", logging_messages)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment