Skip to content

Commit

Permalink
Merge pull request #12 from open-craft/0x29a/bb8083/per-user-algolia-…
Browse files Browse the repository at this point in the history
…key-nutmeg

feat: per-user secured Algolia API keys [BB-8083][BACKPORT]
  • Loading branch information
Agrendalath authored Nov 21, 2023
2 parents 4e6e348 + b0664c2 commit ef788e4
Show file tree
Hide file tree
Showing 11 changed files with 122 additions and 4 deletions.
4 changes: 4 additions & 0 deletions .github/workflows/mysql8-migrations.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ jobs:
key: ${{ runner.os }}-pip-${{ hashFiles('requirements/pip_tools.txt') }}
restore-keys: ${{ runner.os }}-pip-

- name: Downgrade pip to work around https://github.com/mitsuhiko/rye/issues/368
run: |
pip install --upgrade pip==23.1
- name: Ubuntu and sql Versions
run: |
lsb_release -a
Expand Down
7 changes: 5 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -74,11 +74,14 @@ docs: ## generate Sphinx HTML documentation, including API docs
# Define PIP_COMPILE_OPTS=-v to get more information during make upgrade.
PIP_COMPILE = pip-compile --upgrade --rebuild $(PIP_COMPILE_OPTS)
LOCAL_EDX_PINS = requirements/edx-platform-constraints.txt
PLATFORM_BASE_REQS = https://raw.githubusercontent.com/edx/edx-platform/master/requirements/edx/base.txt
# edx-enterprise can't work alone and thus doesn't have it's own requirements. It pulls base requirements
# file from edx-platform and constrains it's own requirements (mostly for the test environment) using it.
PLATFORM_BASE_REQS = https://raw.githubusercontent.com/open-craft/edx-platform/opencraft-release/nutmeg.2/requirements/edx/base.txt
COMMON_CONSTRAINTS_TXT=requirements/common_constraints.txt
.PHONY: $(COMMON_CONSTRAINTS_TXT)
$(COMMON_CONSTRAINTS_TXT):
wget -O "$(@)" https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt || touch "$(@)"
# Requirements were upgraded 15 February 2022. This commit was added to `edx-lint` on 14 February 2022.
wget -O "$(@)" https://raw.githubusercontent.com/openedx/edx-lint/7dce0b15b1e8ef402bc8ad0d160a4a2516d079fe/edx_lint/files/common_constraints.txt || touch "$(@)"
echo "$(COMMON_CONSTRAINTS_TEMP_COMMENT)" | cat - $(@) > temp && mv temp $(@)

check_pins: $(COMMON_CONSTRAINTS_TXT) ## check that our local copy of edx-platform pins is accurate
Expand Down
39 changes: 39 additions & 0 deletions enterprise/api/v1/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from urllib.parse import quote_plus, unquote

import requests
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
Expand Down Expand Up @@ -451,6 +452,44 @@ def toggle_universal_link(self, request, pk=None):
headers = self.get_success_headers(response_body)
return Response(response_body, status=HTTP_200_OK, headers=headers)

@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.
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:
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):
"""
Expand Down
2 changes: 2 additions & 0 deletions enterprise/settings/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,8 @@ def root(*args):
'status': 'published'
}

ENTERPRISE_ALGOLIA_SEARCH_API_KEY = 'test'

TABLEAU_URL = 'http://localhost'
TABLEAU_ADMIN_USER = 'edx'
TABLEAU_ADMIN_USER_PASSWORD = 'edx'
Expand Down
1 change: 1 addition & 0 deletions requirements/base.in
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
# This file contains the dependencies explicitly needed for this library.
#
# Packages directly used by this library that we do not need pinned to a specific version.
algoliasearch<3.0.0
bleach
celery
code-annotations
Expand Down
6 changes: 6 additions & 0 deletions requirements/dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ alabaster==0.7.12
# via
# -r requirements/doc.txt
# sphinx
algoliasearch==2.6.3
# via
# -r requirements/doc.txt
# -r requirements/test-master.txt
# -r requirements/test.txt
amqp==5.0.9
# via
# -r requirements/doc.txt
Expand Down Expand Up @@ -561,6 +566,7 @@ requests==2.27.1
# -r requirements/doc.txt
# -r requirements/test-master.txt
# -r requirements/test.txt
# algoliasearch
# edx-drf-extensions
# edx-rest-api-client
# pyjwkest
Expand Down
3 changes: 3 additions & 0 deletions requirements/doc.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
#
alabaster==0.7.12
# via sphinx
algoliasearch==2.6.3
# via -r requirements/test-master.txt
amqp==5.0.9
# via
# -r requirements/test-master.txt
Expand Down Expand Up @@ -279,6 +281,7 @@ readme-renderer==32.0
requests==2.27.1
# via
# -r requirements/test-master.txt
# algoliasearch
# edx-drf-extensions
# edx-rest-api-client
# pyjwkest
Expand Down
8 changes: 6 additions & 2 deletions requirements/edx-platform-constraints.txt
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ aiohttp==3.8.1
# via geoip2
aiosignal==1.2.0
# via aiohttp
algoliasearch==2.6.3
# via -r requirements/edx/base.in
# via kombu
analytics-python==1.4.0
# via -r requirements/edx/base.in
Expand Down Expand Up @@ -685,7 +687,8 @@ openedx-events==0.7.1
# via -r requirements/edx/base.in
openedx-filters==0.4.3
# via -r requirements/edx/base.in
ora2==3.8.2
# TODO: Backport https://github.com/openedx/edx-ora2/pull/1869 to Olive if it uses a lower version than 4.5.0.
ora2 @ git+https://github.com/open-craft/edx-ora2@agrendalath/bb-6151-nutmeg_backport
# via -r requirements/edx/base.in
packaging==21.3
# via
Expand Down Expand Up @@ -725,7 +728,7 @@ psutil==5.9.0
# via
# -r requirements/edx/paver.txt
# edx-django-utils
py2neo==2021.2.3
py2neo @ https://github.com/overhangio/py2neo/releases/download/2021.2.3/py2neo-2021.2.3.tar.gz
# via
# -c requirements/edx/../constraints.txt
# -r requirements/edx/base.in
Expand Down Expand Up @@ -855,6 +858,7 @@ regex==2022.1.18
requests==2.27.1
# via
# -r requirements/edx/paver.txt
# algoliasearch
# analytics-python
# coreapi
# django-oauth-toolkit
Expand Down
5 changes: 5 additions & 0 deletions requirements/test-master.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@
#
# make upgrade
#
algoliasearch==2.6.3
# via
# -c requirements/edx-platform-constraints.txt
# -r requirements/base.in
amqp==5.0.9
# via kombu
aniso8601==9.0.1
Expand Down Expand Up @@ -281,6 +285,7 @@ requests==2.27.1
# via
# -c requirements/edx-platform-constraints.txt
# -r requirements/base.in
# algoliasearch
# edx-drf-extensions
# edx-rest-api-client
# pyjwkest
Expand Down
3 changes: 3 additions & 0 deletions requirements/test.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
#
# make upgrade
#
algoliasearch==2.6.3
# via -r requirements/test-master.txt
# via
# -r requirements/test-master.txt
# kombu
Expand Down Expand Up @@ -287,6 +289,7 @@ pyyaml==6.0
requests==2.27.1
# via
# -r requirements/test-master.txt
# algoliasearch
# edx-drf-extensions
# edx-rest-api-client
# pyjwkest
Expand Down
48 changes: 48 additions & 0 deletions tests/test_enterprise/api/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
Tests for the `edx-enterprise` api module.
"""

import base64
import json
import uuid
from datetime import datetime, timedelta
Expand Down Expand Up @@ -114,6 +115,7 @@
ENTERPRISE_CUSTOMER_REPORTING_ENDPOINT = reverse('enterprise-customer-reporting-list')
ENTERPRISE_LEARNER_LIST_ENDPOINT = reverse('enterprise-learner-list')
ENTERPRISE_CUSTOMER_WITH_ACCESS_TO_ENDPOINT = reverse('enterprise-customer-with-access-to')
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'
Expand Down Expand Up @@ -1608,6 +1610,52 @@ def test_partial_update(self, enterprise_role, enterprise_uuid_for_role, expecte
enterprise_customer.refresh_from_db()
assert enterprise_customer.slug == 'new-slug'

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)

# 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
Expand Down

0 comments on commit ef788e4

Please sign in to comment.