Skip to content

Commit

Permalink
feat: add "Setup Auth org id" action for Enterprise Customers
Browse files Browse the repository at this point in the history
ENT-8169
  • Loading branch information
pwnage101 committed Jan 5, 2024
1 parent 11e5af7 commit 75946f3
Show file tree
Hide file tree
Showing 13 changed files with 353 additions and 12 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -90,5 +90,8 @@ enterprise/static/enterprise/bundles/*.js
# Virtual environments
venv/

# pyenv
.python-version

# TODO: When we move to be a service, ignore this too.
#enterprise/static/enterprise/bundles/
1 change: 0 additions & 1 deletion .python-version

This file was deleted.

5 changes: 5 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ Change Log
Unreleased
----------

[4.8.19]
--------

feat: add "Setup Auth org id" action for Enterprise Customers (ENT-8169)

[4.8.18]
--------

Expand Down
2 changes: 1 addition & 1 deletion enterprise/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
Your project description goes here.
"""

__version__ = "4.8.18"
__version__ = "4.8.19"
52 changes: 47 additions & 5 deletions enterprise/admin/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
CatalogQueryPreviewView,
EnterpriseCustomerManageLearnerDataSharingConsentView,
EnterpriseCustomerManageLearnersView,
EnterpriseCustomerSetupAuthOrgIDView,
EnterpriseCustomerTransmitCoursesView,
TemplatePreviewView,
)
Expand All @@ -45,6 +46,7 @@
discovery_query_url,
get_all_field_names,
get_default_catalog_content_filter,
get_sso_orchestrator_configure_edx_oauth_path,
localized_utcnow,
)

Expand Down Expand Up @@ -233,7 +235,29 @@ class EnterpriseCustomerAdmin(DjangoObjectActions, SimpleHistoryAdmin):
export_as_csv_action('CSV Export', fields=EXPORT_AS_CSV_FIELDS),
]

change_actions = ('manage_learners', 'manage_learners_data_sharing_consent', 'transmit_courses_metadata')
change_actions = (
'setup_auth_org_id',
'manage_learners',
'manage_learners_data_sharing_consent',
'transmit_courses_metadata',
)

def get_change_actions(self, *args, **kwargs):
"""
Buttons that appear at the top of the "Change Enterprise Customer" page.
Due to a known deficiency in the upstream django_object_actions library, we must STILL define change_actions
above with all possible values.
"""
change_actions = (
'manage_learners',
'manage_learners_data_sharing_consent',
'transmit_courses_metadata',
)
# Add the "Setup Auth org id" button only if it is configured.
if get_sso_orchestrator_configure_edx_oauth_path():
change_actions = ('setup_auth_org_id',) + change_actions
return change_actions

form = EnterpriseCustomerAdminForm

Expand Down Expand Up @@ -357,6 +381,19 @@ def transmit_courses_metadata(self, request, obj):

transmit_courses_metadata.label = 'Transmit Courses Metadata'

@admin.action(
description='Setup auth_org_id for this Enterprise Customer'
)
def setup_auth_org_id(self, request, obj):
"""
Object tool handler method - redirects to `Setup Auth org id` view.
"""
# url names coming from get_urls are prefixed with 'admin' namespace
setup_auth_org_id_url = reverse('admin:' + UrlNames.SETUP_AUTH_ORG_ID, args=(obj.uuid,))
return HttpResponseRedirect(setup_auth_org_id_url)

setup_auth_org_id.label = 'Setup Auth org id'

def get_urls(self):
"""
Returns the additional urls used by the custom object tools.
Expand All @@ -365,18 +402,23 @@ def get_urls(self):
re_path(
r"^([^/]+)/manage_learners$",
self.admin_site.admin_view(EnterpriseCustomerManageLearnersView.as_view()),
name=UrlNames.MANAGE_LEARNERS
name=UrlNames.MANAGE_LEARNERS,
),
re_path(
r"^([^/]+)/clear_learners_data_sharing_consent",
self.admin_site.admin_view(EnterpriseCustomerManageLearnerDataSharingConsentView.as_view()),
name=UrlNames.MANAGE_LEARNERS_DSC
name=UrlNames.MANAGE_LEARNERS_DSC,
),
re_path(
r"^([^/]+)/transmit_courses_metadata",
self.admin_site.admin_view(EnterpriseCustomerTransmitCoursesView.as_view()),
name=UrlNames.TRANSMIT_COURSES_METADATA
)
name=UrlNames.TRANSMIT_COURSES_METADATA,
),
re_path(
r"^([^/]+)/setup_auth_org_id",
self.admin_site.admin_view(EnterpriseCustomerSetupAuthOrgIDView.as_view()),
name=UrlNames.SETUP_AUTH_ORG_ID,
),
]
return customer_urls + super().get_urls()

Expand Down
1 change: 1 addition & 0 deletions enterprise/admin/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ class UrlNames:
MANAGE_LEARNERS = URL_PREFIX + "manage_learners"
MANAGE_LEARNERS_DSC = URL_PREFIX + "manage_learners_data_sharing_consent"
TRANSMIT_COURSES_METADATA = URL_PREFIX + "transmit_courses_metadata"
SETUP_AUTH_ORG_ID = URL_PREFIX + "setup_auth_org_id"
PREVIEW_EMAIL_TEMPLATE = URL_PREFIX + "preview_email_template"
PREVIEW_QUERY_RESULT = URL_PREFIX + "preview_query_result"

Expand Down
75 changes: 73 additions & 2 deletions enterprise/admin/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from django.core.management import call_command
from django.db import transaction
from django.db.models import Q
from django.http import HttpResponse, HttpResponseRedirect, JsonResponse
from django.http import HttpResponse, HttpResponseForbidden, HttpResponseRedirect, HttpResponseServerError, JsonResponse
from django.shortcuts import get_object_or_404, render
from django.urls import reverse
from django.utils.translation import gettext as _
Expand All @@ -36,6 +36,7 @@
)
from enterprise.api_client.discovery import get_course_catalog_api_service_client
from enterprise.api_client.ecommerce import EcommerceApiClient
from enterprise.api_client.sso_orchestrator import EnterpriseSSOOrchestratorApiClient, SsoOrchestratorClientError
from enterprise.constants import PAGE_SIZE
from enterprise.errors import LinkUserToEnterpriseError
from enterprise.models import (
Expand All @@ -50,6 +51,7 @@
delete_data_sharing_consent,
enroll_users_in_course,
get_ecommerce_worker_user,
get_sso_orchestrator_configure_edx_oauth_path,
validate_course_exists_for_enterprise,
validate_email_to_link,
)
Expand Down Expand Up @@ -174,7 +176,8 @@ def get_form_view(self, request, customer_uuid, additional_context=None):
render the form with appropriate context.
"""
context = self._build_context(request, customer_uuid)
context.update(additional_context)
if additional_context:
context.update(additional_context)
return render(request, self.template, context)


Expand Down Expand Up @@ -895,3 +898,71 @@ def delete(self, request, customer_uuid):
return HttpResponse(message, content_type="application/json", status=404)

return JsonResponse({})


class EnterpriseCustomerSetupAuthOrgIDView(BaseEnterpriseCustomerView):
"""
Setup Auth org id View.
This action will configure SSO to GetSmarter using edX credentials via Auth0.
"""
template = 'enterprise/admin/setup_auth_org_id.html'

def get(self, request, customer_uuid):
"""
Handle GET request - render "Setup Auth org id" form.
Arguments:
request (django.http.request.HttpRequest): Request instance
customer_uuid (str): Enterprise Customer UUID
Returns:
django.http.response.HttpResponse: HttpResponse
"""
if not get_sso_orchestrator_configure_edx_oauth_path():
return HttpResponseForbidden(
"The ENTERPRISE_SSO_ORCHESTRATOR_CONFIGURE_EDX_OAUTH_PATH setting was not configured."
)
return self.get_form_view(request, customer_uuid)

def post(self, request, customer_uuid):
"""
Handle POST request - handle form submissions.
Arguments:
request (django.http.request.HttpRequest): Request instance
customer_uuid (str): Enterprise Customer UUID
"""
if not get_sso_orchestrator_configure_edx_oauth_path():
return HttpResponseForbidden(
"The ENTERPRISE_SSO_ORCHESTRATOR_CONFIGURE_EDX_OAUTH_PATH setting was not configured."
)

enterprise_customer = EnterpriseCustomer.objects.get(uuid=customer_uuid)

# Call the configure-edx-oauth endpoint on the enterprise-sso-orchestrator service to obtain an orgId.
# This will raise SsoOrchestratorClientError if the API request fails.
try:
auth_org_id = EnterpriseSSOOrchestratorApiClient().configure_edx_oauth(enterprise_customer)
except SsoOrchestratorClientError as exc:
error_msg = (
f"Error configuring edx oauth for enterprise customer {enterprise_customer.name}"
f"<{enterprise_customer.uuid}>: {exc}"
)
LOG.exception(error_msg)
return HttpResponseServerError(error_msg)

if auth_org_id:
enterprise_customer.auth_org_id = auth_org_id
enterprise_customer.save()
messages.success(request, _('Successfully written the "Auth org id" field for this enterprise customer.'))
return HttpResponseRedirect(reverse("admin:" + UrlNames.SETUP_AUTH_ORG_ID, args=(customer_uuid,)))
else:
# Annoyingly, there's still the remote possibility that the request succeeded but we failed to retrieve the
# auth_org_id. This might be due to a regression in the API response schema.
error_msg = (
f"Error configuring edx oauth for enterprise customer {enterprise_customer.name}"
f"<{enterprise_customer.uuid}>: Missing orgId."
)
LOG.exception(error_msg)
return HttpResponseServerError(error_msg)
32 changes: 31 additions & 1 deletion enterprise/api_client/sso_orchestrator.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
get_sso_orchestrator_api_base_url,
get_sso_orchestrator_basic_auth_password,
get_sso_orchestrator_basic_auth_username,
get_sso_orchestrator_configure_edx_oauth_path,
get_sso_orchestrator_configure_path,
)

Expand Down Expand Up @@ -67,6 +68,14 @@ def _get_orchestrator_configure_url(self):
# probably want config value validated for this
return urljoin(self.base_url, get_sso_orchestrator_configure_path())

def _get_orchestrator_configure_edx_oauth_url(self):
"""
get the configure-edx-oautht url for the SSO Orchestrator API
"""
if path := get_sso_orchestrator_configure_edx_oauth_path():
return urljoin(self.base_url, path)
return None

def _create_auth_header(self):
"""
create the basic auth header for requests to the SSO Orchestrator API
Expand All @@ -93,7 +102,7 @@ def _create_session(self):

def _post(self, url, data=None):
"""
make a GET request to the SSO Orchestrator API
make a POST request to the SSO Orchestrator API
"""
self._create_session()
response = self.session.post(url, json=data, auth=self._create_auth_header())
Expand Down Expand Up @@ -133,3 +142,24 @@ def configure_sso_orchestration_record(

response = self._post(self._get_orchestrator_configure_url(), data=request_data)
return response.get('samlServiceProviderInformation', {}).get('spMetadataUrl', {})

def configure_edx_oauth(self, enterprise_customer):
"""
Configure SSO to GetSmarter using edX credentials via Auth0.
Args:
enterprise_customer (EnterpriseCustomer): The enterprise customer for which to configure edX OAuth.
Returns:
str: Auth0 Organization ID.
Raises:
SsoOrchestratorClientError: If the request to the SSO Orchestrator API failed.
"""
request_data = {
'enterpriseName': enterprise_customer.name,
'enterpriseSlug': enterprise_customer.slug,
'enterpriseUuid': str(enterprise_customer.uuid),
}
response = self._post(self._get_orchestrator_configure_edx_oauth_url(), data=request_data)
return response.get('orgId', None)
1 change: 1 addition & 0 deletions enterprise/settings/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -363,3 +363,4 @@ def root(*args):
ENTERPRISE_SSO_ORCHESTRATOR_WORKER_PASSWORD = 'password'
ENTERPRISE_SSO_ORCHESTRATOR_BASE_URL = 'https://foobar.com'
ENTERPRISE_SSO_ORCHESTRATOR_CONFIGURE_PATH = 'configure'
ENTERPRISE_SSO_ORCHESTRATOR_CONFIGURE_EDX_OAUTH_PATH = 'configure-edx-oauth'
54 changes: 54 additions & 0 deletions enterprise/templates/enterprise/admin/setup_auth_org_id.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
{% extends "admin/base_site.html" %}
{% load i18n static admin_urls %}

{% block breadcrumbs %}
<div class="breadcrumbs">
<a href="{% url 'admin:index' %}">{% trans "Home" %}</a>
&rsaquo; <a href="{% url 'admin:app_list' app_label=opts.app_label %}">{{ opts.app_config.verbose_name }}</a>
&rsaquo; {% if has_change_permission %}
<a href="{% url opts|admin_urlname:'changelist' %}">{{ opts.verbose_name_plural|capfirst }}</a>
{% else %}
{{ opts.verbose_name_plural|capfirst }}
{% endif %}
&rsaquo; {% if has_change_permission %}
<a href="{% url opts|admin_urlname:'change' enterprise_customer.uuid %}">
{{ enterprise_customer|truncatewords:"18" }}
</a>
{% else %}
{{ enterprise_customer|capfirst }}
{% endif %}
&rsaquo;
{% trans "Setup Auth org id" %}
</div>
{% endblock %}

{% block content %}
<div id="content-main">
<div class="forms-panel">
<h1>{% trans "Setup Auth org id" %}</h1>
<p>
This action is required for customers who will have learners in executive education courses. Setting up the
Auth org id will enable the enterprise's learners to take Exec ed or OCM courses using the same set of login
credentials. Clicking the button below will facilitate the necessary steps with our identity vendor, Auth0, and
will overwrite any value that may already be in the "Auth org id" field.
</p>
<form action="" method="post" id="setup-auth-org-id-form">
{% csrf_token %}
<div style="display:flex";>
<div style="vertical-align:top">
<input type="hidden" name="post" value="submit" />
<input type="hidden" name="action" value="submit" />
<input type="submit" value="{% trans "Setup Auth org id" %}" />
</div>
<div style="margin-left:0.5rem">
<input type="button" value="Cancel" onclick="history.go(-1)"/>
</div>
</div>
</form>
</div>
</div>
{% endblock %}

{% block footer %}
{{ block.super }}
{% endblock %}
7 changes: 7 additions & 0 deletions enterprise/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -1565,6 +1565,13 @@ def get_sso_orchestrator_configure_path():
return settings.ENTERPRISE_SSO_ORCHESTRATOR_CONFIGURE_PATH


def get_sso_orchestrator_configure_edx_oauth_path():
"""
Return the SSO orchestrator configure-edx-oauth endpoint path, or None if it is not defined.
"""
return getattr(settings, "ENTERPRISE_SSO_ORCHESTRATOR_CONFIGURE_EDX_OAUTH_PATH", None)


def get_enterprise_worker_user():
"""
Return the user object of enterprise worker user.
Expand Down
Loading

0 comments on commit 75946f3

Please sign in to comment.