Skip to content

feat: remove SAML provider admin views from openedx-platform#38104

Merged
pwnage101 merged 1 commit intomasterfrom
pwnage101/ENT-11567
Apr 14, 2026
Merged

feat: remove SAML provider admin views from openedx-platform#38104
pwnage101 merged 1 commit intomasterfrom
pwnage101/ENT-11567

Conversation

@pwnage101
Copy link
Copy Markdown
Contributor

@pwnage101 pwnage101 commented Mar 5, 2026

This change to remove URLs is safe because bundled in this same commit is an upgrade of edx-enterprise to a version which re-adds the URLs.

ENT-11567


Related:


Integration testing in local devstack:

Steps to seed data for integration testing in devstack

# Create a barebones test EnterpriseCustomer named "Test Enterprise" and with slug "test-enterprise".
python manage.py lms seed_enterprise_devstack_data

# Create a SAMLConfiguration (required before SAMLProviderConfig)
python manage.py lms shell -c "
from django.contrib.sites.models import Site
from common.djangoapps.third_party_auth.models import SAMLConfiguration
site = Site.objects.get_current()
SAMLConfiguration(site=site, slug='default', enabled=True, entity_id='http://saml.example.com').save()
print('')
print('')
print('SAMLConfiguration created')
"

# Create a SAMLProviderConfig
python manage.py lms shell -c "
from django.contrib.sites.models import Site
from common.djangoapps.third_party_auth.models import SAMLProviderConfig
site = Site.objects.get_current()
SAMLProviderConfig(
    site=site,
    slug='test-saml-idp',
    name='Test SAML IdP',
    entity_id='https://idp.example.com/shibboleth',
    metadata_source='https://idp.example.com/metadata.xml',
    enabled=True,
).save()
print('')
print('')
print('SAMLProviderConfig created')
"

# Create a EnterpriseCustomerIdentityProvider
python manage.py lms shell -c "
from enterprise.models import EnterpriseCustomer, EnterpriseCustomerIdentityProvider
ec = EnterpriseCustomer.objects.get(slug='test-enterprise')
EnterpriseCustomerIdentityProvider.objects.get_or_create(
    provider_id='saml-test-saml-idp',
    enterprise_customer=ec,
)
print('')
print('')
print('EnterpriseCustomerIdentityProvider created')
"

Claude-generated script to create a Postman collection

python manage.py lms shell <<'EOF'
import json
import uuid as uuid_mod
from django.contrib.sites.models import Site
from oauth2_provider.models import Application
from enterprise.models import EnterpriseCustomer, EnterpriseCustomerIdentityProvider
from common.djangoapps.third_party_auth.models import SAMLProviderConfig, SAMLConfiguration

# Read DB records
ec = EnterpriseCustomer.objects.get(slug='test-enterprise')
ecidp = EnterpriseCustomerIdentityProvider.objects.get(enterprise_customer=ec)
site = Site.objects.get_current()
slug = ecidp.provider_id.replace('saml-', '', 1)
spc = SAMLProviderConfig.objects.filter(slug=slug).order_by('-change_date').first()

enterprise_uuid = str(ec.uuid)
provider_config_id = str(spc.pk) if spc else ''

# Find the enterprise admin user created by seed_enterprise_devstack_data
from django.contrib.auth import get_user_model
User = get_user_model()
admin_user = User.objects.get(username='enterprise_admin_test-enterprise')

# Create or get a DOT application with password grant for the admin user
dot_app, created = Application.objects.get_or_create(
    name='enterprise-admin-postman',
    defaults={
        'user': admin_user,
        'client_id': 'enterprise-admin-postman-key',
        'client_secret': 'enterprise-admin-postman-secret',
        'client_type': Application.CLIENT_CONFIDENTIAL,
        'authorization_grant_type': Application.GRANT_PASSWORD,
        'redirect_uris': '',
    },
)

collection = {
    "info": {
        "name": "Ticket 08 - SAML Admin Enterprise Views",
        "_postman_id": str(uuid_mod.uuid4()),
        "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
    },
    "variable": [
        {"key": "base_url", "value": "http://localhost:18000"},
        {"key": "enterprise_uuid", "value": enterprise_uuid},
        {"key": "provider_config_id", "value": provider_config_id},
        {"key": "jwt_token", "value": ""},
        {"key": "client_id", "value": dot_app.client_id},
        {"key": "client_secret", "value": dot_app.client_secret},
        {"key": "admin_username", "value": admin_user.username},
        {"key": "admin_password", "value": "edx"},
    ],
    "item": [
        {
            "name": "Get JWT Token (enterprise admin)",
            "event": [
                {
                    "listen": "test",
                    "script": {
                        "type": "text/javascript",
                        "exec": [
                            "var jsonData = pm.response.json();",
                            "pm.collectionVariables.set('jwt_token', jsonData.access_token);",
                            "pm.test('Token retrieved', function() {",
                            "    pm.expect(jsonData.access_token).to.be.a('string');",
                            "});"
                        ]
                    }
                }
            ],
            "request": {
                "auth": {"type": "noauth"},
                "method": "POST",
                "url": {
                    "raw": "{{base_url}}/oauth2/access_token",
                    "host": ["{{base_url}}"],
                    "path": ["oauth2", "access_token"]
                },
                "header": [{"key": "Content-Type", "value": "application/x-www-form-urlencoded"}],
                "body": {
                    "mode": "urlencoded",
                    "urlencoded": [
                        {"key": "grant_type", "value": "password"},
                        {"key": "client_id", "value": "{{client_id}}"},
                        {"key": "client_secret", "value": "{{client_secret}}"},
                        {"key": "username", "value": "{{admin_username}}"},
                        {"key": "password", "value": "{{admin_password}}"},
                        {"key": "token_type", "value": "jwt"},
                    ]
                }
            }
        },
        {
            "name": "List SAML Provider Configs",
            "request": {
                "method": "GET",
                "header": [{"key": "Authorization", "value": "JWT {{jwt_token}}"}],
                "url": {
                    "raw": "{{base_url}}/auth/saml/v0/provider_config/?enterprise-id={{enterprise_uuid}}",
                    "host": ["{{base_url}}"],
                    "path": ["auth", "saml", "v0", "provider_config", ""],
                    "query": [{"key": "enterprise-id", "value": "{{enterprise_uuid}}"}]
                }
            }
        },
        {
            "name": "Create SAML Provider Config",
            "request": {
                "method": "POST",
                "url": {
                    "raw": "{{base_url}}/auth/saml/v0/provider_config/",
                    "host": ["{{base_url}}"],
                    "path": ["auth", "saml", "v0", "provider_config", ""]
                },
                "header": [
                    {"key": "Authorization", "value": "JWT {{jwt_token}}"},
                    {"key": "Content-Type", "value": "application/json"},
                ],
                "body": {
                    "mode": "raw",
                    "raw": json.dumps({
                        "enterprise_customer_uuid": "{{enterprise_uuid}}",
                        "entity_id": "https://new-idp.example.com/shibboleth",
                        "metadata_source": "https://new-idp.example.com/metadata.xml",
                        "name": "New Test SAML IdP",
                        "slug": "new-test-saml-idp",
                        "enabled": True,
                    }, indent=2)
                }
            }
        },
        {
            "name": "Get SAML Provider Config",
            "request": {
                "method": "GET",
                "header": [{"key": "Authorization", "value": "JWT {{jwt_token}}"}],
                "url": {
                    "raw": "{{base_url}}/auth/saml/v0/provider_config/{{provider_config_id}}/?enterprise-id={{enterprise_uuid}}",
                    "host": ["{{base_url}}"],
                    "path": ["auth", "saml", "v0", "provider_config", "{{provider_config_id}}", ""],
                    "query": [{"key": "enterprise-id", "value": "{{enterprise_uuid}}"}]
                }
            }
        },
        {
            "name": "Update SAML Provider Config",
            "request": {
                "method": "PATCH",
                "url": {
                    "raw": "{{base_url}}/auth/saml/v0/provider_config/{{provider_config_id}}/?enterprise-id={{enterprise_uuid}}",
                    "host": ["{{base_url}}"],
                    "path": ["auth", "saml", "v0", "provider_config", "{{provider_config_id}}", ""],
                    "query": [{"key": "enterprise-id", "value": "{{enterprise_uuid}}"}]
                },
                "header": [
                    {"key": "Authorization", "value": "JWT {{jwt_token}}"},
                    {"key": "Content-Type", "value": "application/json"},
                ],
                "body": {
                    "mode": "raw",
                    "raw": json.dumps({
                        "enterprise_customer_uuid": "{{enterprise_uuid}}",
                        "name": "Updated SAML IdP Name",
                    }, indent=2)
                }
            }
        },
        {
            "name": "Delete SAML Provider Config",
            "request": {
                "method": "DELETE",
                "header": [{"key": "Authorization", "value": "JWT {{jwt_token}}"}],
                "url": {
                    "raw": "{{base_url}}/auth/saml/v0/provider_config/{{provider_config_id}}/?enterprise-id={{enterprise_uuid}}",
                    "host": ["{{base_url}}"],
                    "path": ["auth", "saml", "v0", "provider_config", "{{provider_config_id}}", ""],
                    "query": [{"key": "enterprise-id", "value": "{{enterprise_uuid}}"}]
                }
            }
        },
        {
            "name": "List SAML Provider Data",
            "request": {
                "method": "GET",
                "header": [{"key": "Authorization", "value": "JWT {{jwt_token}}"}],
                "url": {
                    "raw": "{{base_url}}/auth/saml/v0/provider_data/?enterprise-id={{enterprise_uuid}}",
                    "host": ["{{base_url}}"],
                    "path": ["auth", "saml", "v0", "provider_data", ""],
                    "query": [{"key": "enterprise-id", "value": "{{enterprise_uuid}}"}]
                }
            }
        },
        {
            "name": "Sync Provider Data",
            "request": {
                "method": "POST",
                "url": {
                    "raw": "{{base_url}}/auth/saml/v0/provider_data/sync_provider_data",
                    "host": ["{{base_url}}"],
                    "path": ["auth", "saml", "v0", "provider_data", "sync_provider_data"]
                },
                "header": [
                    {"key": "Authorization", "value": "JWT {{jwt_token}}"},
                    {"key": "Content-Type", "value": "application/json"},
                ],
                "body": {
                    "mode": "raw",
                    "raw": json.dumps({
                        "enterprise_customer_uuid": "{{enterprise_uuid}}",
                    }, indent=2)
                }
            }
        },
    ]
}

print(json.dumps(collection, indent=2))
EOF

Manually testing the postman collection against my devstack running the new code and with seeded test data was successful.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR removes enterprise-admin-only SAML provider management REST endpoints from openedx-platform and documents the architectural decision to host those endpoints in the edx-enterprise plugin, preserving the existing API contract under the same URL prefix.

Changes:

  • Added an ADR documenting the move of provider_config / provider_data SAML admin viewsets to edx-enterprise.
  • Unregistered (and removed) the SAML provider admin viewsets, URLs, and their API tests from common.djangoapps.third_party_auth.
  • Cleaned up third_party_auth.utils by removing now-unused SAML admin helper functions and adding focused unit tests for create_or_update_bulk_saml_provider_data.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated no comments.

Show a summary per file
File Description
docs/decisions/0025-saml-admin-views-in-enterprise-plugin.rst Documents the decision and consequences of moving the SAML admin endpoints to the enterprise plugin.
common/djangoapps/third_party_auth/utils.py Removes SAML-admin-only helpers and associated unused imports; keeps core SAML metadata parsing + provider data upsert logic.
common/djangoapps/third_party_auth/urls.py Stops registering the removed admin endpoints; adds a note about routes now provided by edx-enterprise.
common/djangoapps/third_party_auth/tests/test_utils.py Removes tests for deleted helpers; adds tests for create_or_update_bulk_saml_provider_data.
common/djangoapps/third_party_auth/samlproviderdata/views.py Deleted: removes enterprise-admin-only Provider Data viewset from platform.
common/djangoapps/third_party_auth/samlproviderdata/urls.py Deleted: removes router registration for Provider Data endpoints.
common/djangoapps/third_party_auth/samlproviderdata/tests/test_samlproviderdata.py Deleted: removes platform-level API tests for Provider Data endpoints.
common/djangoapps/third_party_auth/samlproviderconfig/views.py Deleted: removes enterprise-admin-only Provider Config viewset from platform.
common/djangoapps/third_party_auth/samlproviderconfig/urls.py Deleted: removes router registration for Provider Config endpoints.
common/djangoapps/third_party_auth/samlproviderconfig/tests/test_samlproviderconfig.py Deleted: removes platform-level API tests for Provider Config endpoints.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@pwnage101 pwnage101 force-pushed the pwnage101/ENT-11567 branch 2 times, most recently from e7bf0b1 to 73299f8 Compare April 14, 2026 00:46
@pwnage101 pwnage101 marked this pull request as ready for review April 14, 2026 00:46
@pwnage101 pwnage101 requested a review from Copilot April 14, 2026 00:47
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 15 out of 15 changed files in this pull request and generated 1 comment.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread common/djangoapps/third_party_auth/tests/test_utils.py
@pwnage101 pwnage101 force-pushed the pwnage101/ENT-11567 branch from 73299f8 to 1860f66 Compare April 14, 2026 01:54
Other steps taken too:

- Add ADR and URL routes comment for migrated SAML admin views to
  help ensure people don't inadvertantly register conflicting URL routes.
- Remove validate_uuid4_string, convert_saml_slug_provider_id, and
  fetch_metadata_xml from third_party_auth/utils.py (migrated to
  edx-enterprise)
- Remove test_convert_saml_slug_provider_id test and import
- Add TestCreateOrUpdateBulkSAMLProviderData test class with 4 tests
  covering create, update, bulk_update, and multiple keys paths
- Remove unused imports: logging, requests, uuid.UUID

ENT-11567

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@pwnage101 pwnage101 force-pushed the pwnage101/ENT-11567 branch from 1860f66 to 1d23dff Compare April 14, 2026 19:02
@pwnage101 pwnage101 merged commit ff0ffa1 into master Apr 14, 2026
45 of 46 checks passed
@pwnage101 pwnage101 deleted the pwnage101/ENT-11567 branch April 14, 2026 19:30
@pwnage101
Copy link
Copy Markdown
Contributor Author

Created edx#231 to deploy to the 2U fork.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enterprise An enterprise-related change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants