feat: add SAML provider admin viewsets to enterprise API#2550
feat: add SAML provider admin viewsets to enterprise API#2550
Conversation
a9d2a21 to
8cbe8bf
Compare
There was a problem hiding this comment.
Pull request overview
Adds enterprise API endpoints to administer SAML provider configuration and provider data (including a “sync metadata” action), migrating supporting utilities from openedx-platform into edx-enterprise.
Changes:
- Introduces
SAMLProviderConfigViewSetandSAMLProviderDataViewSetunder/enterprise/api/v1/auth/saml/v0/.... - Adds shared SAML helper utilities (
validate_uuid4_string, metadata fetch/parsing helpers). - Adds unit/API tests covering the new utilities and viewsets.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 13 comments.
Show a summary per file
| File | Description |
|---|---|
enterprise/api/v1/views/saml_utils.py |
New SAML helper functions (UUID validation, slug/provider_id conversion, metadata XML fetch/parse). |
enterprise/api/v1/views/saml_provider_config.py |
New admin CRUD viewset for SAML provider config + enterprise IDP association management. |
enterprise/api/v1/views/saml_provider_data.py |
New admin CRUD viewset for SAML provider data + metadata sync action. |
enterprise/api/v1/urls.py |
Registers new SAML admin routes via a router. |
tests/test_enterprise/api/test_saml_utils.py |
Unit tests for the migrated SAML utilities. |
tests/test_enterprise/api/test_saml_provider_config.py |
API tests for the SAML provider config viewset behaviors. |
tests/test_enterprise/api/test_saml_provider_data.py |
API tests for the SAML provider data viewset and sync action. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| raise Http404('No matching SAML provider found.') # pylint: disable=raise-missing-from | ||
| provider_data_id = self.request.parser_context.get('kwargs', {}).get('pk') | ||
| if provider_data_id: | ||
| return SAMLProviderData.objects.filter(id=provider_data_id) |
There was a problem hiding this comment.
In the detail case (when pk is present), the queryset is filtered only by id, not by the enterprise’s SAML provider/entity_id. That means an enterprise admin could potentially access/update/delete a SAMLProviderData row belonging to a different enterprise by guessing its ID. Filter by both id and entity_id (derived from the enterprise’s SAML provider), or otherwise enforce enterprise scoping for the detail route as well.
| return SAMLProviderData.objects.filter(id=provider_data_id) | |
| return SAMLProviderData.objects.filter( | |
| id=provider_data_id, | |
| entity_id=saml_provider.entity_id, | |
| ) |
There was a problem hiding this comment.
this is probably worth doing as a general safeguard
| try: | ||
| log.info("Fetching %s", url) | ||
| if not url.lower().startswith('https'): | ||
| log.warning("This SAML metadata URL is not secure! It should use HTTPS. (%s)", url) | ||
| response = requests.get(url, verify=True) # May raise HTTPError or SSLError or ConnectionError | ||
| response.raise_for_status() # May raise an HTTPError | ||
|
|
||
| try: | ||
| parser = etree.XMLParser(remove_comments=True) | ||
| xml = etree.fromstring(response.content, parser) | ||
| except etree.XMLSyntaxError: # lint-amnesty, pylint: disable=try-except-raise | ||
| raise | ||
| return xml | ||
| except (requests.exceptions.SSLError, requests.exceptions.HTTPError, requests.exceptions.RequestException) as error: | ||
| log.exception(str(error), exc_info=error) | ||
| raise error | ||
| except etree.XMLSyntaxError as error: | ||
| log.exception(str(error), exc_info=error) | ||
| raise error |
There was a problem hiding this comment.
In fetch_metadata_xml, using log.exception(..., exc_info=error) is incorrect (it should typically be exc_info=True inside an except block), and raise error will replace the original traceback context. Prefer log.exception("...") (or log.exception("...", exc_info=True)) and a bare raise to preserve the original traceback. Also consider adding a reasonable timeout to requests.get to avoid hanging requests.
b5db25d to
96d8730
Compare
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #2550 +/- ##
==========================================
+ Coverage 85.79% 85.91% +0.12%
==========================================
Files 247 250 +3
Lines 16434 16604 +170
Branches 1629 1639 +10
==========================================
+ Hits 14100 14266 +166
- Misses 1999 2001 +2
- Partials 335 337 +2
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 17 out of 18 changed files in this pull request and generated 5 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| log = logging.getLogger(__name__) | ||
|
|
There was a problem hiding this comment.
log = logging.getLogger(__name__) is currently unused in this module; if no logging is needed, remove it to avoid lint failures and reduce noise.
251d5ea to
f7f3e47
Compare
9c60030 to
4b2d4bf
Compare
| #enterprise/static/enterprise/bundles/ | ||
|
|
||
| # claude code personalization | ||
| CLAUDE.md |
There was a problem hiding this comment.
@iloveagent57 how can we stop adding CLAUDE.md to .gitignore?
- Add SAMLProviderConfigViewSet and SAMLProviderDataViewSet to the enterprise API with permission checks and comprehensive tests. - Add get_saml_identity_provider and get_identity_providers utility views for enterprise SAML operations. - Migrate SAML utility functions (get_saml_provider_data_by_enterprise, get_saml_provider_configuration) from third_party_auth to enterprise, with comprehensive tests using django-mock-queries MockSet. ENT-11567 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
4b2d4bf to
2fd6aa8
Compare
iloveagent57
left a comment
There was a problem hiding this comment.
approving to unblock, but I agree with these two copilot suggestions
| saml_config_ids = [ | ||
| config.id for config in SAMLProviderConfig.objects.current_set() | ||
| if config.provider_id in slug_list | ||
| ] | ||
| return SAMLProviderConfig.objects.filter(id__in=saml_config_ids) |
| raise Http404('No matching SAML provider found.') # pylint: disable=raise-missing-from | ||
| provider_data_id = self.request.parser_context.get('kwargs', {}).get('pk') | ||
| if provider_data_id: | ||
| return SAMLProviderData.objects.filter(id=provider_data_id) |
There was a problem hiding this comment.
this is probably worth doing as a general safeguard
- Push provider_id filtering into database via current_set().filter() instead of iterating all configs in Python. - Scope SAMLProviderData detail lookups by entity_id to prevent cross-enterprise data access by guessing IDs. ENT-11567 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
3236df4 to
f079944
Compare
Migrate two enterprise-specific viewsets from openedx-platform into edx-enterprise.
ENT-11567
Related: