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 aaec175
Show file tree
Hide file tree
Showing 12 changed files with 258 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
73 changes: 71 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, HttpResponseRedirect, HttpResponseForbidden, 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 @@ -52,6 +53,7 @@
get_ecommerce_worker_user,
validate_course_exists_for_enterprise,
validate_email_to_link,
get_sso_orchestrator_configure_edx_oauth_path,
)

# Only create manual enrollments if running in edx-platform
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,69 @@ 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 auth org ID.
# 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:
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 @@ -17,6 +17,7 @@
get_sso_orchestrator_basic_auth_password,
get_sso_orchestrator_basic_auth_username,
get_sso_orchestrator_configure_path,
get_sso_orchestrator_configure_edx_oauth_path,
)

USER_AGENT = user_agent()
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'
52 changes: 52 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,52 @@
{% 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 will configure SSO to GetSmarter using edX credentials via Auth0, and populate this enterprise
customer's auth_org_id field. This overwrites whatever value is currently in that 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 aaec175

Please sign in to comment.