def create_light_response(self, auth_class_map: Dict[str, LevelOfAssurance] = None) -> LightResponse: """Convert SAML response to light response.""" response = LightResponse(attributes=OrderedDict()) root = self.document.getroot() if root.tag != Q_NAMES['saml2p:Response']: raise ValidationError({ get_element_path(root): 'Wrong root element: {!r}'.format(root.tag)}) response.id = root.get('ID') response.in_response_to_id = root.get('InResponseTo') response.issuer = self.issuer response.status = status = Status() status_code_elm = root.find('./{}/{}'.format(Q_NAMES['saml2p:Status'], Q_NAMES['saml2p:StatusCode'])) if status_code_elm is not None: sub_status_code_elm = status_code_elm.find('./{}'.format(Q_NAMES['saml2p:StatusCode'])) status_message_elm = root.find('./{}/{}'.format(Q_NAMES['saml2p:Status'], Q_NAMES['saml2p:StatusMessage'])) status_code = status_code_elm.get('Value') sub_status_code = sub_status_code_elm.get('Value') if sub_status_code_elm is not None else None if status_code == SubStatusCode.VERSION_MISMATCH.value: # VERSION_MISMATCH is a status code in SAML 2 but a sub status code in Light response! status.status_code = StatusCode.REQUESTER status.sub_status_code = SubStatusCode.VERSION_MISMATCH else: status.status_code = StatusCode(status_code) try: status.sub_status_code = SubStatusCode(sub_status_code) except ValueError: # None or a sub status codes not recognized by eIDAS status.sub_status_code = None status.failure = status.status_code != StatusCode.SUCCESS if status_message_elm is not None: status.status_message = status_message_elm.text assertion_elm = self.assertion if assertion_elm is not None: try: self._parse_assertion(response, assertion_elm, auth_class_map) except ValidationError as e: return LightResponse( id=root.get('ID'), in_response_to_id=root.get('InResponseTo'), issuer=self.issuer, status=Status( failure=True, status_code=StatusCode.RESPONDER, status_message='Invalid data of {!r}: {!r}'.format(*e.errors.popitem()))) response.relay_state = self.relay_state return response
def set_failure(self, failure: bool) -> None: data = self.__class__.VALID_DATA.copy() if failure: self.OPTIONAL = self.__class__.OPTIONAL_FAILURE data.update({ 'status': Status(failure=failure, status_code=StatusCode.REQUESTER, sub_status_code=SubStatusCode.REQUEST_DENIED, status_message='Oops.'), 'attributes': OrderedDict(), 'subject': None, 'subject_name_id_format': None, 'level_of_assurance': None, }) else: self.OPTIONAL = self.__class__.OPTIONAL data['status'] = Status(failure=False) self.VALID_DATA = data
def test_rewrite_name_id_persistent(self): light_response_data = LIGHT_RESPONSE_DICT.copy() light_response_data['status'] = Status(**light_response_data['status']) light_response_data['subject_name_id_format'] = NameIdFormat.PERSISTENT view = IdentityProviderResponseView() view.light_response = LightResponse(**light_response_data) view.auxiliary_data = {'name_id_format': NameIdFormat.PERSISTENT.value} view.rewrite_name_id() self.assertEqual(view.light_response.subject_name_id_format, NameIdFormat.PERSISTENT) self.assertEqual(view.light_response.subject, 'CZ/CZ/ff70c9dd-6a05-4068-aaa2-b57be4f328e9')
def test_rewrite_name_id_unspecified_to_transient(self, uuid_mock): light_response_data = LIGHT_RESPONSE_DICT.copy() light_response_data['status'] = Status(**light_response_data['status']) light_response_data['subject_name_id_format'] = NameIdFormat.UNSPECIFIED view = IdentityProviderResponseView() view.light_response = LightResponse(**light_response_data) view.auxiliary_data = {'name_id_format': NameIdFormat.TRANSIENT.value} view.rewrite_name_id() self.assertEqual(view.light_response.subject_name_id_format, NameIdFormat.TRANSIENT) self.assertEqual(view.light_response.subject, '0uuid4')
def test_rewrite_name_id_failure(self): light_response_data = FAILED_LIGHT_RESPONSE_DICT.copy() light_response_data['status'] = Status(**light_response_data['status']) light_response_data['subject_name_id_format'] = NameIdFormat.PERSISTENT view = IdentityProviderResponseView() view.light_response = LightResponse(**light_response_data) view.auxiliary_data = {'name_id_format': NameIdFormat.TRANSIENT.value} view.rewrite_name_id() self.assertEqual(view.light_response.subject_name_id_format, NameIdFormat.PERSISTENT) self.assertIsNone(view.light_response.subject)
def test_create_light_token(self, uuid_mock: MagicMock): view = IdentityProviderResponseView() view.request = self.factory.post(self.url) light_response_data = LIGHT_RESPONSE_DICT.copy() light_response_data['status'] = Status(**light_response_data['status']) view.light_response = LightResponse(**light_response_data) token, encoded_token = view.create_light_token('test-token-issuer', 'sha256', 'test-secret') self.assertEqual(token.id, 'T0uuid4') self.assertEqual(token.issuer, 'test-token-issuer') self.assertEqual(token.created, datetime(2017, 12, 11, 16, 12, 5)) self.assertEqual(token.encode('sha256', 'test-secret').decode('ascii'), encoded_token) self.assertEqual(uuid_mock.mock_calls, [call()])
def test_from_light_response_minimal(self): self.maxDiff = None status = Status(failure=False) response = self.create_light_response(True, ip_address=None, status=status, attributes={}) saml_response = SAMLResponse.from_light_response( response, None, None, datetime(2017, 12, 11, 14, 12, 5, 148000), timedelta(minutes=5)) with cast( TextIO, (DATA_DIR / 'saml_response_from_light_response_minimal.xml').open('r')) as f2: data = f2.read() self.assertXMLEqual( dump_xml(saml_response.document).decode('utf-8'), data)
def test_from_light_response_version_mismatch(self): self.maxDiff = None status = Status(failure=True, sub_status_code=SubStatusCode.VERSION_MISMATCH, status_message='Oops.') response = self.create_light_response(False, issuer=None, ip_address=None, status=status) saml_response = SAMLResponse.from_light_response( response, None, None, datetime(2017, 12, 11, 14, 12, 5, 148000), timedelta(minutes=5)) with cast(TextIO, (DATA_DIR / 'saml_response_from_light_response_version_mismatch.xml' ).open('r')) as f2: data = f2.read() self.assertXMLEqual( dump_xml(saml_response.document).decode('utf-8'), data)
def test_post_success(self, uuid_mock: MagicMock): with cast(BinaryIO, (DATA_DIR / 'saml_response.xml').open('rb')) as f: saml_request_xml = f.read() response = self.client.post(self.url, {'SAMLResponse': b64encode(saml_request_xml).decode('ascii'), 'RelayState': 'relay123'}) # Context self.assertIn('token', response.context) self.assertEqual(response.context['token_parameter'], 'test_token') self.assertEqual(response.context['eidas_url'], 'https://test.example.net/SpecificProxyServiceResponse') self.assertEqual(response.context['error'], None) # Token encoded_token = response.context['token'] token = LightToken.decode(encoded_token, 'sha256', 'response-token-secret') self.assertEqual(token.id, 'T0uuid4') self.assertEqual(token.issuer, 'response-token-issuer') self.assertEqual(token.created, datetime(2017, 12, 11, 14, 12, 5)) # Storing light response light_response_data = LIGHT_RESPONSE_DICT.copy() light_response_data.update({ 'status': Status(**light_response_data['status']), 'id': 'test-saml-response-id', # Preserved 'in_response_to_id': 'test-saml-request-id', # Preserved 'issuer': 'https://test.example.net/node-proxy-service-response', # Replaced }) light_response = LightResponse(**light_response_data) self.assertEqual(self.client_class_mock.mock_calls, [call(timeout=66)]) self.assertEqual(self.client_mock.mock_calls, [call.connect('test.example.net', 1234), call.get_cache('test-proxy-service-response-cache'), call.get_cache().put('T0uuid4', dump_xml(light_response.export_xml()).decode('utf-8'))]) # Rendering self.assertContains(response, 'Redirect to eIDAS Node is in progress') self.assertContains(response, '<form class="auto-submit" action="https://test.example.net/SpecificProxyServiceResponse"') self.assertContains(response, '<input type="hidden" name="test_token" value="{}"'.format(encoded_token)) self.assertNotIn(b'An error occurred', response.content)
def create_response(self, success: bool) -> LightResponse: data = (LIGHT_RESPONSE_DICT if success else FAILED_LIGHT_RESPONSE_DICT).copy() data['status'] = Status(**data['status']) return LightResponse(**data)
class TestLightResponse(ValidationMixin, SimpleTestCase): MODEL = LightResponse OPTIONAL = {'issuer', 'ip_address', 'relay_state'} OPTIONAL_FAILURE = {'issuer', 'ip_address', 'relay_state', 'subject', 'subject_name_id_format', 'level_of_assurance'} VALID_DATA = { 'id': 'uuid', 'in_response_to_id': 'uuid2', 'issuer': 'MyIssuer', 'ip_address': '127.0.0.1', 'relay_state': 'state 123', 'subject': 'my subject', 'subject_name_id_format': NameIdFormat.PERSISTENT, 'level_of_assurance': LevelOfAssurance.LOW, 'status': Status(failure=False), 'attributes': OrderedDict( [('http://eidas.europa.eu/attributes/naturalperson/CurrentFamilyName', []), ('http://eidas.europa.eu/attributes/naturalperson/CurrentGivenName', ['Antonio', 'Lucio'])]), } INVALID_DATA = { 'id': 1, 'in_response_to_id': 2, 'issuer': 3, 'ip_address': 4, 'relay_state': 5, 'subject': 6, 'subject_name_id_format': str(NameIdFormat.PERSISTENT), 'level_of_assurance': str(LevelOfAssurance.LOW), 'status': Status(), 'attributes': ['attr1'], } def tearDown(self) -> None: if self.VALID_DATA is not self.__class__.VALID_DATA: del self.VALID_DATA if self.OPTIONAL is not self.__class__.OPTIONAL: del self.OPTIONAL def create_response(self, success: bool) -> LightResponse: data = (LIGHT_RESPONSE_DICT if success else FAILED_LIGHT_RESPONSE_DICT).copy() data['status'] = Status(**data['status']) return LightResponse(**data) def set_failure(self, failure: bool) -> None: data = self.__class__.VALID_DATA.copy() if failure: self.OPTIONAL = self.__class__.OPTIONAL_FAILURE data.update({ 'status': Status(failure=failure, status_code=StatusCode.REQUESTER, sub_status_code=SubStatusCode.REQUEST_DENIED, status_message='Oops.'), 'attributes': OrderedDict(), 'subject': None, 'subject_name_id_format': None, 'level_of_assurance': None, }) else: self.OPTIONAL = self.__class__.OPTIONAL data['status'] = Status(failure=False) self.VALID_DATA = data def test_required_for_failure(self): self.set_failure(True) self.test_required() def test_attributes_with_response_status_ok(self): self.set_failure(False) self.assert_attributes_valid('attributes') def test_attributes_with_response_status_failure(self): self.set_failure(True) self.assert_attributes_valid('attributes') def test_export_xml_with_response_status_ok(self): self.maxDiff = None response = self.create_response(True) with cast(BinaryIO, (DATA_DIR / 'light_response.xml').open('r')) as f: data = f.read() self.assertEqual(dump_xml(response.export_xml()).decode('utf-8'), data) def test_export_xml_with_response_status_failure(self): self.maxDiff = None response = self.create_response(False) with cast(BinaryIO, (DATA_DIR / 'light_response_failure.xml').open('r')) as f: data = f.read() self.assertEqual(dump_xml(response.export_xml()).decode('utf-8'), data) def test_load_xml_with_response_status_ok(self): self.maxDiff = None self.set_failure(False) response = self.create_response(True) with cast(BinaryIO, (DATA_DIR / 'light_response.xml').open('r')) as f: data = f.read() self.assertEqual(LightResponse.load_xml(parse_xml(data)), response) def test_load_xml_with_response_status_failure(self): self.maxDiff = None response = self.create_response(False) with cast(BinaryIO, (DATA_DIR / 'light_response_failure.xml').open('r')) as f: data = f.read() self.assertEqual(LightResponse.load_xml(parse_xml(data)), response)
def test_deserialize_sub_status_code_invalid(self): status = Status() elm = Element('subStatusCode') for invalid in '##', 'test ## test': elm.text = invalid self.assertIsNone(status.deserialize_sub_status_code(elm))
def get_light_response(self, **kwargs) -> LightResponse: light_response_data = LIGHT_RESPONSE_DICT.copy() light_response_data['status'] = Status(**light_response_data['status']) light_response_data.update(**kwargs) return LightResponse(**light_response_data)
def get_light_response_data(self, subject_id: str) -> dict: data = deepcopy(LIGHT_RESPONSE_DICT) data['status'] = Status(**data['status']) data['subject'] = subject_id data['attributes'][PERSON_ID][0] = subject_id return data