From 54733017bd5615e6e374d1c2662141d133ec6796 Mon Sep 17 00:00:00 2001 From: Alexander J Sheehan Date: Fri, 13 Oct 2023 14:00:02 +0000 Subject: [PATCH] chore: updating sso orchestrator self service api endpoints --- CHANGELOG.rst | 3 + enterprise/__init__.py | 2 +- .../enterprise_customer_sso_configuration.py | 23 ++++--- .../migrations/0194_auto_20231013_1359.py | 23 +++++++ enterprise/models.py | 10 ++++ tests/test_enterprise/api/test_views.py | 60 ++++++++++++++++++- 6 files changed, 110 insertions(+), 11 deletions(-) create mode 100644 enterprise/migrations/0194_auto_20231013_1359.py diff --git a/CHANGELOG.rst b/CHANGELOG.rst index b554a2738b..eafe471096 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -15,6 +15,9 @@ Change Log Unreleased ---------- +[4.6.4] +------- +chore: updating sso orchestrator self service api endpoints [4.6.3] ------- diff --git a/enterprise/__init__.py b/enterprise/__init__.py index cce4ecb055..89ac717eca 100644 --- a/enterprise/__init__.py +++ b/enterprise/__init__.py @@ -2,4 +2,4 @@ Your project description goes here. """ -__version__ = "4.6.3" +__version__ = "4.6.4" diff --git a/enterprise/api/v1/views/enterprise_customer_sso_configuration.py b/enterprise/api/v1/views/enterprise_customer_sso_configuration.py index 9a9a5e8e7a..a93156a57c 100644 --- a/enterprise/api/v1/views/enterprise_customer_sso_configuration.py +++ b/enterprise/api/v1/views/enterprise_customer_sso_configuration.py @@ -24,6 +24,7 @@ from enterprise import models from enterprise.api.utils import get_enterprise_customer_from_user_id from enterprise.api.v1 import serializers +from enterprise.api_client.sso_orchestrator import SsoOrchestratorClientError from enterprise.logging import getEnterpriseLogger from enterprise.models import EnterpriseCustomer, EnterpriseCustomerSsoConfiguration, EnterpriseCustomerUser from enterprise.tasks import send_sso_configured_email @@ -146,6 +147,14 @@ def oauth_orchestration_complete(self, request, configuration_uuid, *args, **kwa ' not been marked as submitted.' ) + if error_msg := request.POST.get('error'): + LOGGER.error( + f'SSO configuration record {sso_configuration_record.pk} has failed to configure due to {error_msg}.' + ) + sso_configuration_record.errored_at = localized_utcnow() + sso_configuration_record.save() + return Response(status=HTTP_200_OK) + # Mark the configuration record as active IFF this the record has never been configured. if not sso_configuration_record.configured_at: sso_configuration_record.active = True @@ -248,14 +257,12 @@ def create(self, request, *args, **kwargs): request_data['entity_id'] = entity_id try: - new_record = EnterpriseCustomerSsoConfiguration.objects.create(**request_data) - except TypeError as e: - LOGGER.error(f'{CONFIG_CREATE_ERROR}{e}') - return Response({'error': f'{CONFIG_CREATE_ERROR}{e}'}, status=HTTP_400_BAD_REQUEST) - - # Wondering what to do here with error handling - # If we fail to submit for configuration (ie get a network error) should we rollback the created record? - new_record.submit_for_configuration() + with transaction.atomic(): + new_record = EnterpriseCustomerSsoConfiguration.objects.create(**request_data) + new_record.submit_for_configuration() + except (TypeError, SsoOrchestratorClientError) as e: + LOGGER.error(f'{CONFIG_CREATE_ERROR} {e}') + return Response({'error': f'{CONFIG_CREATE_ERROR} {e}'}, status=HTTP_400_BAD_REQUEST) return Response({'data': new_record.pk}, status=HTTP_201_CREATED) diff --git a/enterprise/migrations/0194_auto_20231013_1359.py b/enterprise/migrations/0194_auto_20231013_1359.py new file mode 100644 index 0000000000..c46f657dda --- /dev/null +++ b/enterprise/migrations/0194_auto_20231013_1359.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2.22 on 2023-10-13 13:59 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('enterprise', '0193_auto_20231005_1708'), + ] + + operations = [ + migrations.AddField( + model_name='enterprisecustomerssoconfiguration', + name='errored_at', + field=models.DateTimeField(blank=True, help_text='The date and time when the orchestrator encountered an error during configuration.', null=True), + ), + migrations.AddField( + model_name='historicalenterprisecustomerssoconfiguration', + name='errored_at', + field=models.DateTimeField(blank=True, help_text='The date and time when the orchestrator encountered an error during configuration.', null=True), + ), + ] diff --git a/enterprise/models.py b/enterprise/models.py index 0d5d60a7ec..c883dac36a 100644 --- a/enterprise/models.py +++ b/enterprise/models.py @@ -3947,6 +3947,14 @@ class Meta: ) ) + errored_at = models.DateTimeField( + blank=True, + null=True, + help_text=_( + "The date and time when the orchestrator encountered an error during configuration." + ) + ) + # ---------------------------- SAP Success Factors attribute mappings ---------------------------- # odata_api_timeout_interval = models.PositiveIntegerField( @@ -4051,6 +4059,8 @@ def is_pending_configuration(self): if self.submitted_at: if not self.configured_at: return True + if self.errored_at and self.errored_at > self.submitted_at: + return False if self.submitted_at > self.configured_at: return True return False diff --git a/tests/test_enterprise/api/test_views.py b/tests/test_enterprise/api/test_views.py index a3b27adf9a..fcde9ab424 100644 --- a/tests/test_enterprise/api/test_views.py +++ b/tests/test_enterprise/api/test_views.py @@ -7228,13 +7228,13 @@ def post_new_sso_configuration(self, data): ) return self.client.post(url, data=data) - def post_sso_configuration_complete(self, config_pk): + def post_sso_configuration_complete(self, config_pk, data=None): """Helper method to hit the configuration complete endpoint for sso configurations.""" url = settings.TEST_SERVER + reverse( self.SSO_CONFIGURATION_COMPLETE_ENDPOINT, kwargs={'configuration_uuid': config_pk} ) - return self.client.post(url) + return self.client.post(url, data=data) def _get_existing_sso_record_url(self, config_pk): """Helper method to get the url for an existing sso configuration endpoint.""" @@ -7291,6 +7291,27 @@ def test_sso_configuration_oauth_orchestration_complete_not_found(self): response = self.post_sso_configuration_complete(config_pk) assert response.status_code == 404 + @mock.patch("enterprise.api_client.braze.BrazeAPIClient.get_braze_client") + def test_sso_configuration_oauth_orchestration_complete_error(self, mock_braze_client): + """ + Verify that the endpoint is able to mark an sso config as errored. + """ + mock_braze_client.return_value.get_braze_client.return_value = mock.MagicMock() + self.set_jwt_cookie(ENTERPRISE_OPERATOR_ROLE, "*") + config_pk = uuid.uuid4() + enterprise_sso_orchestration_config = EnterpriseCustomerSsoConfigurationFactory( + uuid=config_pk, + enterprise_customer=self.enterprise_customer, + configured_at=None, + submitted_at=localized_utcnow(), + ) + assert enterprise_sso_orchestration_config.is_pending_configuration() + response = self.post_sso_configuration_complete(config_pk, data={'error': 'test error'}) + enterprise_sso_orchestration_config.refresh_from_db() + assert enterprise_sso_orchestration_config.configured_at is None + assert enterprise_sso_orchestration_config.errored_at is not None + assert response.status_code == status.HTTP_200_OK + @mock.patch("enterprise.api_client.braze.BrazeAPIClient.get_braze_client") def test_sso_configuration_oauth_orchestration_complete(self, mock_braze_client): """ @@ -7561,6 +7582,41 @@ def test_sso_configuration_create_bad_data_format(self): response = self.post_new_sso_configuration(data) assert "somewhackyvalue" in response.json()['error'] + @responses.activate + def test_sso_configuration_create_error_from_orchestrator(self): + """ + Test that the sso orchestration create endpoint will rollback a created object if the submission for + configuration fails. + """ + xml_metadata = """ + + + """ + responses.add( + responses.GET, + "https://examples.com/metadata.xml", + body=xml_metadata, + ) + responses.add( + responses.POST, + urljoin(get_sso_orchestrator_api_base_url(), get_sso_orchestrator_configure_path()), + json={'error': 'some error'}, + status=400, + ) + data = { + "metadata_url": "https://examples.com/metadata.xml", + "active": False, + "enterprise_customer": str(self.enterprise_customer.uuid), + "identity_provider": "cornerstone" + } + self.set_jwt_cookie(ENTERPRISE_ADMIN_ROLE, self.enterprise_customer.uuid) + + response = self.post_new_sso_configuration(data) + + assert response.status_code == 400 + assert EnterpriseCustomerSsoConfiguration.objects.all().count() == 0 + + def test_sso_configuration_create_bad_xml_url(self): """ Test expected response when creating a new sso configuration with a bad xml url.