🚨(backend) fix search without refresh token
i remove the decorator on the search function and refresh the token in the function so i can try catch it. Signed-off-by: charles <[email protected]>
This commit is contained in:
@@ -8,6 +8,9 @@ from django.core.cache import cache
|
||||
from django.core.files.storage import default_storage
|
||||
|
||||
import botocore
|
||||
import requests
|
||||
from lasuite.oidc_login.backends import get_oidc_refresh_token, store_tokens
|
||||
from rest_framework.exceptions import AuthenticationFailed
|
||||
from rest_framework.throttling import BaseThrottle
|
||||
|
||||
|
||||
@@ -179,3 +182,31 @@ class AIUserRateThrottle(AIBaseRateThrottle):
|
||||
if x_forwarded_for
|
||||
else request.META.get("REMOTE_ADDR")
|
||||
)
|
||||
|
||||
|
||||
def refresh_access_token(session):
|
||||
"""Refresh the OIDC access token using the refresh token."""
|
||||
refresh_token = get_oidc_refresh_token(session)
|
||||
if not refresh_token:
|
||||
raise AuthenticationFailed({"error": "Refresh token is missing from session"})
|
||||
|
||||
response = requests.post(
|
||||
settings.OIDC_OP_TOKEN_ENDPOINT,
|
||||
data={
|
||||
"grant_type": "refresh_token",
|
||||
"client_id": settings.OIDC_RP_CLIENT_ID,
|
||||
"client_secret": settings.OIDC_RP_CLIENT_SECRET,
|
||||
"refresh_token": refresh_token,
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
response.raise_for_status()
|
||||
token_info = response.json()
|
||||
|
||||
store_tokens(
|
||||
session,
|
||||
access_token=token_info.get("access_token"),
|
||||
id_token=None,
|
||||
refresh_token=token_info.get("refresh_token"),
|
||||
)
|
||||
return session
|
||||
|
||||
@@ -25,7 +25,6 @@ from django.db.models.functions import Greatest, Left, Length
|
||||
from django.http import Http404, StreamingHttpResponse
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.http import content_disposition_header
|
||||
from django.utils.text import capfirst, slugify
|
||||
@@ -38,11 +37,11 @@ from botocore.exceptions import ClientError
|
||||
from csp.constants import NONE
|
||||
from csp.decorators import csp_update
|
||||
from lasuite.malware_detection import malware_detection
|
||||
from lasuite.oidc_login.decorators import refresh_oidc_access_token
|
||||
from lasuite.tools.email import get_domain_from_email
|
||||
from pydantic import ValidationError as PydanticValidationError
|
||||
from rest_framework import filters, status, viewsets
|
||||
from rest_framework import response as drf_response
|
||||
from rest_framework.exceptions import AuthenticationFailed
|
||||
from rest_framework.permissions import AllowAny
|
||||
from rest_framework.views import APIView
|
||||
|
||||
@@ -84,6 +83,7 @@ from .throttling import (
|
||||
UserListThrottleBurst,
|
||||
UserListThrottleSustained,
|
||||
)
|
||||
from .utils import refresh_access_token
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -1415,7 +1415,6 @@ class DocumentViewSet(
|
||||
return duplicated_document
|
||||
|
||||
@drf.decorators.action(detail=False, methods=["get"], url_path="search")
|
||||
@method_decorator(refresh_oidc_access_token)
|
||||
def search(self, request, *args, **kwargs):
|
||||
"""
|
||||
Returns an ordered list of documents best matching the search query parameter 'q'.
|
||||
@@ -1430,6 +1429,15 @@ class DocumentViewSet(
|
||||
if search_type == SearchType.TITLE:
|
||||
return self._title_search(request, params.validated_data, *args, **kwargs)
|
||||
|
||||
try:
|
||||
request.session = refresh_access_token(request.session)
|
||||
except AuthenticationFailed:
|
||||
logging.warning(
|
||||
"User unauthenticated or error while refreshing token, "
|
||||
"falling back to title search."
|
||||
)
|
||||
return self._title_search(request, params.validated_data, *args, **kwargs)
|
||||
|
||||
indexer = get_document_indexer()
|
||||
if indexer is None:
|
||||
# fallback on title search if the indexer is not configured
|
||||
@@ -1440,7 +1448,10 @@ class DocumentViewSet(
|
||||
indexer, request, params=params, search_type=search_type
|
||||
)
|
||||
except requests.exceptions.RequestException as e:
|
||||
logger.error("Error while searching documents with indexer: %s", e)
|
||||
logger.error(
|
||||
"Error while searching documents with indexer \n%s \nfall back on title search",
|
||||
e,
|
||||
)
|
||||
# fallback on title search if the indexer is not reached
|
||||
return self._title_search(request, params.validated_data, *args, **kwargs)
|
||||
|
||||
|
||||
@@ -7,6 +7,8 @@ from django.core.cache import cache
|
||||
|
||||
import pytest
|
||||
import responses
|
||||
from cryptography.fernet import Fernet
|
||||
from lasuite.oidc_login.backends import get_cipher_suite
|
||||
|
||||
from core import factories
|
||||
from core.tests.utils.urls import reload_urls
|
||||
@@ -143,3 +145,28 @@ def user_token():
|
||||
A fixture to create a user token for testing.
|
||||
"""
|
||||
return build_authorization_bearer("some_token")
|
||||
|
||||
|
||||
@pytest.fixture(name="oidc_settings")
|
||||
def fixture_oidc_settings(settings):
|
||||
"""Fixture to configure OIDC settings for the tests."""
|
||||
settings.OIDC_OP_TOKEN_ENDPOINT = "https://auth.example.com/token"
|
||||
settings.OIDC_OP_AUTHORIZATION_ENDPOINT = "https://auth.example.com/authorize"
|
||||
settings.OIDC_RP_CLIENT_ID = "client_id"
|
||||
settings.OIDC_RP_CLIENT_SECRET = "client_secret"
|
||||
settings.OIDC_AUTHENTICATION_CALLBACK_URL = "oidc_authentication_callback"
|
||||
settings.OIDC_RP_SCOPES = "openid email"
|
||||
settings.OIDC_USE_NONCE = True
|
||||
settings.OIDC_STATE_SIZE = 32
|
||||
settings.OIDC_NONCE_SIZE = 32
|
||||
settings.OIDC_VERIFY_SSL = True
|
||||
settings.OIDC_TOKEN_USE_BASIC_AUTH = False
|
||||
settings.OIDC_STORE_ACCESS_TOKEN = True
|
||||
settings.OIDC_STORE_REFRESH_TOKEN = True
|
||||
settings.OIDC_STORE_REFRESH_TOKEN_KEY = Fernet.generate_key()
|
||||
|
||||
get_cipher_suite.cache_clear()
|
||||
|
||||
yield settings
|
||||
|
||||
get_cipher_suite.cache_clear()
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
Tests for Documents API endpoint in impress's core app: search
|
||||
"""
|
||||
|
||||
import re
|
||||
from unittest import mock
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
import responses
|
||||
@@ -12,7 +14,7 @@ from rest_framework.test import APIClient
|
||||
from waffle.testutils import override_flag
|
||||
|
||||
from core import factories
|
||||
from core.enums import FeatureFlag, SearchType
|
||||
from core.enums import FeatureFlag
|
||||
from core.services.search_indexers import get_document_indexer
|
||||
|
||||
fake = Faker()
|
||||
@@ -26,47 +28,29 @@ def enable_flag_find_hybrid_search():
|
||||
yield
|
||||
|
||||
|
||||
@mock.patch("core.services.search_indexers.FindDocumentIndexer.search_query")
|
||||
@mock.patch("core.api.viewsets.DocumentViewSet.list")
|
||||
@responses.activate
|
||||
def test_api_documents_search_anonymous(search_query, indexer_settings):
|
||||
def test_api_documents_search_anonymous(mock_list, indexer_settings):
|
||||
"""
|
||||
Anonymous users should be allowed to search documents with Find.
|
||||
Anonymous users should not be allowed to search documents with Find.
|
||||
they should fall back on title search.
|
||||
"""
|
||||
indexer_settings.SEARCH_URL = "http://find/api/v1.0/search"
|
||||
|
||||
# mock Find response
|
||||
responses.add(
|
||||
responses.POST,
|
||||
"http://find/api/v1.0/search",
|
||||
json=[],
|
||||
status=200,
|
||||
)
|
||||
mocked_response = {
|
||||
"count": 0,
|
||||
"next": None,
|
||||
"previous": None,
|
||||
"results": [{"title": "mocked list result"}],
|
||||
}
|
||||
mock_list.return_value = drf_response.Response(mocked_response)
|
||||
|
||||
q = "alpha"
|
||||
response = APIClient().get("/api/v1.0/documents/search/", data={"q": q})
|
||||
|
||||
assert search_query.call_count == 1
|
||||
assert search_query.call_args[1] == {
|
||||
"data": {
|
||||
"q": q,
|
||||
"visited": [],
|
||||
"services": ["docs"],
|
||||
"nb_results": 50,
|
||||
"order_by": "updated_at",
|
||||
"order_direction": "desc",
|
||||
"path": None,
|
||||
"search_type": SearchType.HYBRID,
|
||||
},
|
||||
"token": None,
|
||||
}
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"count": 0,
|
||||
"next": None,
|
||||
"previous": None,
|
||||
"results": [],
|
||||
}
|
||||
assert mock_list.call_count == 1
|
||||
assert mock_list.call_args[0][0].GET.get("q") == q
|
||||
assert response.json() == mocked_response
|
||||
|
||||
|
||||
@mock.patch("core.api.viewsets.DocumentViewSet.list")
|
||||
@@ -194,8 +178,13 @@ def test_api_documents_search_invalid_params(indexer_settings):
|
||||
|
||||
|
||||
@responses.activate
|
||||
def test_api_documents_search_success(indexer_settings):
|
||||
@patch("core.api.viewsets.refresh_access_token")
|
||||
def test_api_documents_search_success(
|
||||
mocked_refresh_access_token, indexer_settings, oidc_settings, settings
|
||||
): # pylint: disable=unused-argument
|
||||
"""Validate the format of documents as returned by the search view."""
|
||||
mocked_refresh_access_token.side_effect = lambda session: session
|
||||
|
||||
indexer_settings.SEARCH_URL = "http://find/api/v1.0/search"
|
||||
assert get_document_indexer() is not None
|
||||
|
||||
@@ -204,7 +193,7 @@ def test_api_documents_search_success(indexer_settings):
|
||||
# Find response
|
||||
responses.add(
|
||||
responses.POST,
|
||||
"http://find/api/v1.0/search",
|
||||
indexer_settings.SEARCH_URL,
|
||||
json=[
|
||||
{
|
||||
"_id": str(document["id"]),
|
||||
@@ -213,7 +202,11 @@ def test_api_documents_search_success(indexer_settings):
|
||||
],
|
||||
status=200,
|
||||
)
|
||||
response = APIClient().get("/api/v1.0/documents/search/", data={"q": "alpha"})
|
||||
|
||||
user = factories.UserFactory()
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
response = client.get("/api/v1.0/documents/search/", data={"q": "alpha"})
|
||||
|
||||
assert response.status_code == 200
|
||||
content = response.json()
|
||||
|
||||
@@ -4,6 +4,7 @@ Tests for Find search feature flags
|
||||
|
||||
from unittest import mock
|
||||
|
||||
from django.contrib.sessions.backends.cache import SessionStore
|
||||
from django.http import HttpResponse
|
||||
|
||||
import pytest
|
||||
@@ -11,6 +12,7 @@ import responses
|
||||
from rest_framework.test import APIClient
|
||||
from waffle.testutils import override_flag
|
||||
|
||||
from core import factories
|
||||
from core.enums import FeatureFlag, SearchType
|
||||
from core.services.search_indexers import get_document_indexer
|
||||
|
||||
@@ -18,6 +20,7 @@ pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
@responses.activate
|
||||
@mock.patch("core.api.viewsets.refresh_access_token")
|
||||
@mock.patch("core.api.viewsets.DocumentViewSet._title_search")
|
||||
@mock.patch("core.api.viewsets.DocumentViewSet._search_with_indexer")
|
||||
@pytest.mark.parametrize(
|
||||
@@ -44,12 +47,14 @@ pytestmark = pytest.mark.django_db
|
||||
def test_api_documents_search_success( # noqa : PLR0913
|
||||
mock_search_with_indexer,
|
||||
mock_title_search,
|
||||
mocked_refresh_access_token,
|
||||
activated_flags,
|
||||
expected_search_type,
|
||||
expected_search_with_indexer_called,
|
||||
expected_title_search_called,
|
||||
indexer_settings,
|
||||
):
|
||||
oidc_settings,
|
||||
): # pylint: disable=unused-argument
|
||||
"""
|
||||
Test that the API endpoint for searching documents returns a successful response
|
||||
with the expected search type according to the activated feature flags,
|
||||
@@ -59,7 +64,11 @@ def test_api_documents_search_success( # noqa : PLR0913
|
||||
|
||||
mock_search_with_indexer.return_value = HttpResponse()
|
||||
mock_title_search.return_value = HttpResponse()
|
||||
mocked_refresh_access_token.side_effect = lambda session: session
|
||||
|
||||
user = factories.UserFactory()
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
with override_flag(
|
||||
FeatureFlag.FLAG_FIND_HYBRID_SEARCH,
|
||||
active=FeatureFlag.FLAG_FIND_HYBRID_SEARCH in activated_flags,
|
||||
@@ -68,9 +77,7 @@ def test_api_documents_search_success( # noqa : PLR0913
|
||||
FeatureFlag.FLAG_FIND_FULL_TEXT_SEARCH,
|
||||
active=FeatureFlag.FLAG_FIND_FULL_TEXT_SEARCH in activated_flags,
|
||||
):
|
||||
response = APIClient().get(
|
||||
"/api/v1.0/documents/search/", data={"q": "alpha"}
|
||||
)
|
||||
response = client.get("/api/v1.0/documents/search/", data={"q": "alpha"})
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
Reference in New Issue
Block a user