diff --git a/enterprise_access/apps/api/v1/views/provisioning.py b/enterprise_access/apps/api/v1/views/provisioning.py index df0f182e..daf37c64 100644 --- a/enterprise_access/apps/api/v1/views/provisioning.py +++ b/enterprise_access/apps/api/v1/views/provisioning.py @@ -3,20 +3,31 @@ """ import logging +import requests from drf_spectacular.utils import extend_schema from edx_rbac.mixins import PermissionRequiredMixin from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication -from rest_framework import generics, permissions, status +from rest_framework import exceptions, generics, permissions, status from rest_framework.response import Response from enterprise_access.apps.api import serializers from enterprise_access.apps.core import constants +from enterprise_access.apps.provisioning import api as provisioning_api logger = logging.getLogger(__name__) PROVISIONING_API_TAG = 'Provisioning' +class ProvisioningException(exceptions.APIException): + """ + General provisioning-related API exception. + """ + status_code = 422 + default_detail = 'Could not execute this provisioning request' + default_code = 'provisioning_error' + + @extend_schema( tags=[PROVISIONING_API_TAG], summary='Create a new provisioning request.', @@ -37,4 +48,41 @@ class ProvisioningCreateView(PermissionRequiredMixin, generics.CreateAPIView): def create(self, request, *args, **kwargs): request_serializer = serializers.ProvisioningRequestSerializer(data=request.data) request_serializer.is_valid(raise_exception=True) - return Response('ack', status=status.HTTP_201_CREATED) + + customer_request_data = request_serializer.validated_data['enterprise_customer'] + try: + created_customer = provisioning_api.get_or_create_enterprise_customer( + name=customer_request_data['name'], + country=customer_request_data['country'], + slug=customer_request_data['slug'], + ) + except requests.exceptions.HTTPError as exc: + raise ProvisioningException( + detail=f'Error get/creating customer record: {exc}', + code='customer_provisioning_error', + ) from exc + + admin_emails = [ + record.get('user_email') + for record in request_serializer.validated_data['pending_admins'] + ] + + try: + customer_admins = provisioning_api.get_or_create_enterprise_admin_users( + enterprise_customer_uuid=created_customer['uuid'], + user_emails=admin_emails, + ) + except requests.exceptions.HTTPError as exc: + raise ProvisioningException( + detail=f'Error get/creating admin records: {exc}', + code='admin_provisioning_error', + ) from exc + + response_serializer = serializers.ProvisioningResponseSerializer({ + 'enterprise_customer': created_customer, + 'pending_admins': customer_admins, + }) + return Response( + response_serializer.data, + status=status.HTTP_201_CREATED, + ) diff --git a/enterprise_access/apps/api_client/lms_client.py b/enterprise_access/apps/api_client/lms_client.py index cfbf5e22..03eb976c 100755 --- a/enterprise_access/apps/api_client/lms_client.py +++ b/enterprise_access/apps/api_client/lms_client.py @@ -128,6 +128,7 @@ def create_enterprise_customer(self, *, name, slug, country, **kwargs): }, **kwargs, } + raise HTTPError('foo') response = self.client.post( self.enterprise_customer_endpoint, json=payload, @@ -196,6 +197,33 @@ def get_enterprise_admin_users(self, enterprise_customer_uuid): return results + def get_enterprise_pending_admin_users(self, enterprise_customer_uuid): + """ + Gets all pending enterprise admin records for the given customer uuid. + + Arguments: + enterprise_customer_uuid (UUID): UUID of the enterprise customer. + Returns: + List of dictionaries of pending admin users. + """ + response = self.client.get( + self.pending_enterprise_admin_endpoint + f'?enterprise_customer={enterprise_customer_uuid}', + timeout=settings.LMS_CLIENT_TIMEOUT, + ) + try: + response.raise_for_status() + logger.info( + 'Fetched pending admin records for customer %s', enterprise_customer_uuid, + ) + payload = response.json() + return payload.get('results', []) + except requests.exceptions.HTTPError: + logger.exception( + 'Failed to fetch pending admin record for customer %s: %s', + enterprise_customer_uuid, response.content.decode() + ) + raise + def create_enterprise_admin_user(self, enterprise_customer_uuid, user_email): """ Creates a new enterprise pending admin record. @@ -210,12 +238,12 @@ def create_enterprise_admin_user(self, enterprise_customer_uuid, user_email): 'enterprise_customer': enterprise_customer_uuid, 'user_email': user_email, } - try: - response = self.client.post( + response = self.client.post( self.pending_enterprise_admin_endpoint, json=payload, timeout=settings.LMS_CLIENT_TIMEOUT, ) + try: response.raise_for_status() logger.info( 'Successfully created pending admin record for customer %s, email %s', @@ -225,8 +253,8 @@ def create_enterprise_admin_user(self, enterprise_customer_uuid, user_email): return payload except requests.exceptions.HTTPError: logger.exception( - 'Failed to create pending admin record for customer %s, email %s', - enterprise_customer_uuid, user_email, + 'Failed to create pending admin record for customer %s, email %s: %s', + enterprise_customer_uuid, user_email, response.content.decode() ) raise diff --git a/enterprise_access/apps/api_client/tests/test_lms_client.py b/enterprise_access/apps/api_client/tests/test_lms_client.py index d5584d63..7f990a0e 100644 --- a/enterprise_access/apps/api_client/tests/test_lms_client.py +++ b/enterprise_access/apps/api_client/tests/test_lms_client.py @@ -216,6 +216,43 @@ def test_create_enterprise_customer_data(self, mock_oauth_client, mock_json): timeout=settings.LMS_CLIENT_TIMEOUT, ) + @mock.patch('requests.Response.json') + @mock.patch('enterprise_access.apps.api_client.base_oauth.OAuthAPIClient') + def test_get_enterprise_pending_admin_users(self, mock_oauth_client, mock_json): + """ + Test that we can use the LmsApiClient to fetch existing pending admin records. + """ + customer_uuid = str(uuid4()) + + mock_response_payload_results = [{ + 'id': 1, + 'enterprise_customer': customer_uuid, + 'user_email': 'test-existing-admin@example.com', + }] + mock_response_payload = { + 'count': 1, + 'results': mock_response_payload_results, + } + mock_json.return_value = mock_response_payload + + mock_get = mock_oauth_client.return_value.get + + mock_get.return_value = requests.Response() + mock_get.return_value.status_code = 200 + + client = LmsApiClient() + response_payload = client.get_enterprise_pending_admin_users(customer_uuid) + + self.assertEqual(response_payload, mock_response_payload_results) + expected_url = ( + 'http://edx-platform.example.com/enterprise/api/v1/pending-enterprise-admin/' + f'?enterprise_customer={customer_uuid}' + ) + mock_get.assert_called_once_with( + expected_url, + timeout=settings.LMS_CLIENT_TIMEOUT, + ) + @mock.patch('requests.Response.json') @mock.patch('enterprise_access.apps.api_client.base_oauth.OAuthAPIClient') def test_create_enterprise_admin_user(self, mock_oauth_client, mock_json): @@ -316,6 +353,31 @@ def test_create_enterprise_admin_error(self, mock_oauth_client): timeout=settings.LMS_CLIENT_TIMEOUT, ) + @mock.patch('enterprise_access.apps.api_client.base_oauth.OAuthAPIClient') + def test_get_enterprise_pending_admin_error(self, mock_oauth_client): + """ + Tests that we raise an exception appropriately when listing pending + admin records with the LmsApiClient(). + """ + customer_uuid = str(uuid4()) + mock_get = mock_oauth_client.return_value.get + + mock_get.side_effect = requests.exceptions.HTTPError('whoopsie') + mock_get.return_value.status_code = 400 + + client = LmsApiClient() + with self.assertRaises(requests.exceptions.HTTPError): + client.get_enterprise_pending_admin_users(customer_uuid) + + expected_url = ( + 'http://edx-platform.example.com/enterprise/api/v1/pending-enterprise-admin/' + f'?enterprise_customer={customer_uuid}' + ) + mock_get.assert_called_once_with( + expected_url, + timeout=settings.LMS_CLIENT_TIMEOUT, + ) + @mock.patch('enterprise_access.apps.api_client.base_oauth.OAuthAPIClient') def test_unlink_users_from_enterprise(self, mock_oauth_client): """ diff --git a/enterprise_access/apps/provisioning/__init__.py b/enterprise_access/apps/provisioning/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/enterprise_access/apps/provisioning/api.py b/enterprise_access/apps/provisioning/api.py new file mode 100644 index 00000000..14e72db0 --- /dev/null +++ b/enterprise_access/apps/provisioning/api.py @@ -0,0 +1,64 @@ +""" +Python API for provisioning operations. +""" +import logging + +from ..api_client.lms_client import LmsApiClient + +logger = logging.getLogger(__name__) + + +def get_or_create_enterprise_customer(*, name, slug, country, **kwargs): + """ + Get or creates an enterprise customer with the provided arguments. + """ + client = LmsApiClient() + existing_customer = client.get_enterprise_customer_data(enterprise_customer_slug=slug) + if existing_customer: + logger.info('Provisioning: enterprise_customer slug %s already exists', slug) + return existing_customer + + created_customer = client.create_enterprise_customer( + name=name, slug=slug, country=country, **kwargs, + ) + logger.info('Provisioning: created enterprise customer with slug %s', slug) + + +def get_or_create_enterprise_admin_users(enterprise_customer_uuid, user_emails): + """ + Creates pending admin records from the given ``user_email`` for the customer + identified by ``enterprise_customer_uuid``. + """ + client = LmsApiClient() + existing_admins = client.get_enterprise_admin_users(enterprise_customer_uuid) + existing_admin_emails = {record['email'] for record in existing_admins} + logger.info( + 'Provisioning: customer %s has existing admin emails %s', + enterprise_customer_uuid, + existing_admin_emails, + ) + + existing_pending_admins = client.get_enterprise_pending_admin_users(enterprise_customer_uuid) + existing_pending_admin_emails = {record['user_email'] for record in existing_pending_admins} + logger.info( + 'Provisioning: customer %s has existing pending admin emails %s', + enterprise_customer_uuid, + existing_pending_admin_emails, + ) + + user_emails_to_create = list( + (set(user_emails) - existing_admin_emails) - existing_pending_admin_emails + ) + + created_admins = [] + for user_email in user_emails_to_create: + created_admins.append( + client.create_enterprise_admin_user(enterprise_customer_uuid, user_email) + ) + logger.info( + 'Provisioning: created admin %s for customer %s', + user_email, + enterprise_customer_uuid, + ) + + return created_admins + existing_pending_admins