-
Notifications
You must be signed in to change notification settings - Fork 0
feat: per-user secured Algolia API keys [BB-8083] #11
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,6 +8,7 @@ | |
| from urllib.parse import quote_plus, unquote | ||
|
|
||
| import jwt | ||
| from algoliasearch.search_client import SearchClient | ||
| from django_filters.rest_framework import DjangoFilterBackend | ||
| from edx_rbac.decorators import permission_required | ||
| from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication | ||
|
|
@@ -525,6 +526,44 @@ def unlink_users(self, request, pk=None): # pylint: disable=unused-argument | |
|
|
||
| return Response(status=HTTP_200_OK) | ||
|
|
||
| @action(detail=False) | ||
| def algolia_key(self, request, *args, **kwargs): | ||
| """ | ||
| Returns an Algolia API key that is secured to only allow searching for | ||
| objects associated with enterprise customers that the user is linked to. | ||
|
Comment on lines
+532
to
+533
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would it make sense to mention that we use this in the
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done 👍 |
||
|
|
||
| This endpoint is used with `frontend-app-learner-portal-enterprise` MFE | ||
| currently. | ||
| """ | ||
|
|
||
| if not (api_key := getattr(settings, "ENTERPRISE_ALGOLIA_SEARCH_API_KEY", "")): | ||
| LOGGER.warning("Algolia search API key is not configured. To enable this view, " | ||
| "set `ENTERPRISE_ALGOLIA_SEARCH_API_KEY` in settings.") | ||
| raise Http404 | ||
|
|
||
| queryset = self.queryset.filter( | ||
| **{ | ||
| self.USER_ID_FILTER: request.user.id, | ||
| "enterprise_customer_users__linked": True | ||
| } | ||
| ).values_list("uuid", flat=True) | ||
|
|
||
| if len(queryset) == 0: | ||
|
Agrendalath marked this conversation as resolved.
|
||
| raise NotFound(_("User is not linked to any enterprise customers.")) | ||
|
|
||
| secured_key = SearchClient.generate_secured_api_key( | ||
| api_key, | ||
| { | ||
| "filters": " OR ".join( | ||
| f"enterprise_customer_uuids:{enterprise_customer_uuid}" | ||
| for enterprise_customer_uuid | ||
| in queryset | ||
| ), | ||
| } | ||
| ) | ||
|
|
||
| return Response({"key": secured_key}, status=HTTP_200_OK) | ||
|
|
||
|
|
||
| class EnterpriseCourseEnrollmentViewSet(EnterpriseReadWriteModelViewSet): | ||
| """ | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,6 +2,7 @@ | |
| Tests for the `edx-enterprise` api module. | ||
| """ | ||
|
|
||
| import base64 | ||
| import json | ||
| import uuid | ||
| from datetime import datetime, timedelta | ||
|
|
@@ -113,6 +114,7 @@ | |
| ENTERPRISE_LEARNER_LIST_ENDPOINT = reverse('enterprise-learner-list') | ||
| ENTERPRISE_CUSTOMER_WITH_ACCESS_TO_ENDPOINT = reverse('enterprise-customer-with-access-to') | ||
| ENTERPRISE_CUSTOMER_UNLINK_USERS_ENDPOINT = reverse('enterprise-customer-unlink-users', kwargs={'pk': FAKE_UUIDS[0]}) | ||
| ENTERPRISE_CUSTOMER_ALGOLIA_KEY_ENDPOINT = reverse('enterprise-customer-algolia-key') | ||
| PENDING_ENTERPRISE_LEARNER_LIST_ENDPOINT = reverse('pending-enterprise-learner-list') | ||
| LICENSED_ENTERPISE_COURSE_ENROLLMENTS_REVOKE_ENDPOINT = reverse( | ||
| 'licensed-enterprise-course-enrollment-license-revoke' | ||
|
|
@@ -1670,6 +1672,52 @@ def test_unlink_users(self, enterprise_role, enterprise_uuid_for_role, is_relink | |
| assert enterprise_customer_user_2.is_relinkable == is_relinkable | ||
| assert enterprise_customer_user_2.is_relinkable == is_relinkable | ||
|
|
||
| def test_algolia_key(self): | ||
| """ | ||
| Tests that the endpoint algolia_key endpoint returns the correct secured key. | ||
| """ | ||
|
|
||
| # Test that the endpoint returns 401 if the user is not logged in. | ||
| self.client.logout() | ||
| response = self.client.get(ENTERPRISE_CUSTOMER_ALGOLIA_KEY_ENDPOINT) | ||
| assert response.status_code == status.HTTP_401_UNAUTHORIZED | ||
|
|
||
| username = 'test_learner_portal_user' | ||
| self.create_user(username=username, is_staff=False) | ||
| self.client.login(username=username, password=TEST_PASSWORD) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: could you please add an assert for the AnonymousUser (i.e., before we log in)?
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done 👍 |
||
|
|
||
| # Test that the endpoint returns 404 if the Algolia Search API key is not set. | ||
| with override_settings(ENTERPRISE_ALGOLIA_SEARCH_API_KEY=None): | ||
| response = self.client.get(ENTERPRISE_CUSTOMER_ALGOLIA_KEY_ENDPOINT) | ||
| assert response.status_code == status.HTTP_404_NOT_FOUND | ||
|
|
||
| # Test that the endpoint returns 404 if the user is not linked to any enterprise. | ||
| response = self.client.get(ENTERPRISE_CUSTOMER_ALGOLIA_KEY_ENDPOINT) | ||
| assert response.status_code == status.HTTP_404_NOT_FOUND | ||
|
|
||
| # Test that the endpoint returns 200 if the user is linked to at least one enterprise. | ||
| enterprise_customer_1 = factories.EnterpriseCustomerFactory(uuid=FAKE_UUIDS[0]) | ||
| enterprise_customer_2 = factories.EnterpriseCustomerFactory(uuid=FAKE_UUIDS[1]) | ||
| factories.EnterpriseCustomerFactory(uuid=FAKE_UUIDS[2]) # extra unlinked enterprise | ||
|
|
||
| factories.EnterpriseCustomerUserFactory( | ||
| user_id=self.user.id, | ||
| enterprise_customer=enterprise_customer_1 | ||
| ) | ||
| factories.EnterpriseCustomerUserFactory( | ||
| user_id=self.user.id, | ||
| enterprise_customer=enterprise_customer_2 | ||
| ) | ||
|
|
||
| response = self.client.get(ENTERPRISE_CUSTOMER_ALGOLIA_KEY_ENDPOINT) | ||
| assert response.status_code == status.HTTP_200_OK | ||
|
|
||
| # Test that the endpoint returns the key encoding correct filters. | ||
| decoded_key = base64.b64decode(response.json()["key"]).decode("utf-8") | ||
| assert decoded_key.endswith( | ||
| f"filters=enterprise_customer_uuids%3A{FAKE_UUIDS[0]}+OR+enterprise_customer_uuids%3A{FAKE_UUIDS[1]}" | ||
| ) | ||
|
|
||
|
|
||
| @ddt.ddt | ||
| @mark.django_db | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.