pythonsolrapache-zookeeperpysolr

Pysolr JWT Token can't be passed as authentication


Structure

The SOLR and Zookeeper Nodes run in a docker environment.

The Solr Authentication is a MultiAuthPlugin with the BasicAuthPlugin used to log in to the SOLR Admin UI and the JWTAuthPlugin used for the requests.

What I want

I need to send a request from my RestAPI to SOLR so I can search for content. For this I use pysolr.

The Problem

When I send the request with the token given as auth parameter I always get a "TypeError: 'str' object is not callable" error.

What I tried

I first tried my function without the authentication which works and returns my search results as expected. Then I tried with a static token which returns the error. And finally I tried the same with a always newly generated token which as well returns the error. Since I wasn't sure if the generated token works I printed the token in the console and tried to send a request with Postman with the given token which did work. When I use the "Basic Authentication" using a username and password it works as well.

What I expected

I expect, that the request is sent to SOLR. Then SOLR should check if the JWT token is valid with the settings set in the security.json and finally should return the search results.

Code

from typing import Any

import jwt
import pysolr
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi_cache.decorator import cache

from app.caching import TTL
from app.core.config import settings
from app.models import SearchResults
from app.queryparams import SearchQueryParams
from app.utils import check_search_input

router = APIRouter()

@router.get("/", response_model=SearchResults)
# @cache(expire=TTL.WEEK, namespace="public:search")
async def search(
    skip: int = 0,
    limit: int = 100,
    params: SearchQueryParams = Depends(),
) -> Any:
    """
    Search query.
    """
    if not check_search_input(params=params):
        raise HTTPException(
            status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
            detail="Mindestens ein Feld muss ausgefüllt werden",
        )

    zookeeper = pysolr.ZooKeeper("search-zoo1,search-zoo2,search-zoo3")

    jwt_token = jwt.encode({
        "sub": "test-backend",
        "name": "test-backend",
        "iat": 1516239022,
        "admin": False,
        "iss": "test-solr-auth",
        "solr": "test-backend",
        "aud": "test-solr-api",
        "exp": 33292598400,
        "scope": "test-backend",
        "realm": "test-solr-jwt"
    }, settings.KEY, algorithm="RS256")

    solr = pysolr.SolrCloud(zookeeper, "tag", auth=jwt_token)

    res = solr.search(q=params.query, start=skip, rows=limit)

    return SearchResults(data=res, count=res.hits)

Error traceback

INFO: "GET /v1/search/?skip=0&limit=100&query=title%3A%22Yellow%22&descending=false HTTP/1.1" 500 Internal Server Error
ERROR:    Exception in ASGI application
Traceback (most recent call last):
   File "/app/.venv/lib/python3.13/site-packages/uvicorn/protocols/http/httptools_impl.py", line 401, in run_asgi
     result = await app(  # type: ignore[func-returns-value]
              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
         self.scope, self.receive, self.send
         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
     )
     ^
   File "/app/.venv/lib/python3.13/site-packages/uvicorn/middleware/proxy_headers.py", line 60, in __call__
     return await self.app(scope, receive, send)
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   File "/app/.venv/lib/python3.13/site-packages/fastapi/applications.py", line 1054, in __call__
     await super().__call__(scope, receive, send)
   File "/app/.venv/lib/python3.13/site-packages/starlette/applications.py", line 113, in __call__
     await self.middleware_stack(scope, receive, send)
   File "/app/.venv/lib/python3.13/site-packages/starlette/middleware/errors.py", line 187, in __call__
     raise exc
   File "/app/.venv/lib/python3.13/site-packages/starlette/middleware/errors.py", line 165, in __call__
     await self.app(scope, receive, _send)
   File "/app/.venv/lib/python3.13/site-packages/starlette/middleware/cors.py", line 93, in __call__
     await self.simple_response(scope, receive, send, request_headers=headers)
   File "/app/.venv/lib/python3.13/site-packages/starlette/middleware/cors.py", line 144, in simple_response
     await self.app(scope, receive, send)
   File "/app/.venv/lib/python3.13/site-packages/starlette/middleware/exceptions.py", line 62, in __call__
     await wrap_app_handling_exceptions(self.app, conn)(scope, receive, send)
   File "/app/.venv/lib/python3.13/site-packages/starlette/_exception_handler.py", line 53, in wrapped_app
     raise exc
   File "/app/.venv/lib/python3.13/site-packages/starlette/_exception_handler.py", line 42, in wrapped_app
     await app(scope, receive, sender)
   File "/app/.venv/lib/python3.13/site-packages/starlette/routing.py", line 715, in __call__
     await self.middleware_stack(scope, receive, send)
   File "/app/.venv/lib/python3.13/site-packages/starlette/routing.py", line 735, in app
     await route.handle(scope, receive, send)
   File "/app/.venv/lib/python3.13/site-packages/starlette/routing.py", line 288, in handle
     await self.app(scope, receive, send)
   File "/app/.venv/lib/python3.13/site-packages/starlette/routing.py", line 76, in app
     await wrap_app_handling_exceptions(app, request)(scope, receive, send)
   File "/app/.venv/lib/python3.13/site-packages/starlette/_exception_handler.py", line 53, in wrapped_app
     raise exc
   File "/app/.venv/lib/python3.13/site-packages/starlette/_exception_handler.py", line 42, in wrapped_app
     await app(scope, receive, sender)
   File "/app/.venv/lib/python3.13/site-packages/starlette/routing.py", line 73, in app
     response = await f(request)
                ^^^^^^^^^^^^^^^^
   File "/app/.venv/lib/python3.13/site-packages/fastapi/routing.py", line 301, in app
     raw_response = await run_endpoint_function(
                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
     ...<3 lines>...
     )
     ^
   File "/app/.venv/lib/python3.13/site-packages/fastapi/routing.py", line 212, in run_endpoint_function
     return await dependant.call(**values)
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   File "/app/app/api/routes/public/search.py", line 56, in search
     res = solr.search(q=params.query, start=skip, rows=limit)
   File "/app/.venv/lib/python3.13/site-packages/pysolr.py", line 834, in search
     response = self._select(params, handler=search_handler)
   File "/app/.venv/lib/python3.13/site-packages/pysolr.py", line 486, in _select
     return self._send_request("get", path)
            ~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^
   File "/app/.venv/lib/python3.13/site-packages/pysolr.py", line 1512, in _send_request
     return Solr._send_request(self, method, path, body, headers, files)
            ~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   File "/app/.venv/lib/python3.13/site-packages/pysolr.py", line 414, in _send_request
     resp = requests_method(
         url,
     ...<4 lines>...
         auth=self.auth,
     )
   File "/app/.venv/lib/python3.13/site-packages/requests/sessions.py", line 602, in get
     return self.request("GET", url, **kwargs)
            ~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^
   File "/app/.venv/lib/python3.13/site-packages/requests/sessions.py", line 575, in request
     prep = self.prepare_request(req)
   File "/app/.venv/lib/python3.13/site-packages/requests/sessions.py", line 484, in prepare_request
     p.prepare(
     ~~~~~~~~~^
         method=request.method.upper(),
         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
     ...<10 lines>...
         hooks=merge_hooks(request.hooks, self.hooks),
         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
     )
     ^
   File "/app/.venv/lib/python3.13/site-packages/requests/models.py", line 371, in prepare
     self.prepare_auth(auth, url)
     ~~~~~~~~~~~~~~~~~^^^^^^^^^^^
   File "/app/.venv/lib/python3.13/site-packages/requests/models.py", line 602, in prepare_auth
     r = auth(self)
 TypeError: 'str' object is not callable

Solution

  • So I found a solution to this problem. It might not be pretty but it works.

    What I did is, I created a class named "JWTAuth" which uses the AuthBase class. When I call the class I pass the token. This way the auth parameter of pysolr will receive a object and not a string, thus it is happy.

    class JWTAuth(AuthBase):
        def __init__(self, jwt_token):
            self.jwt_token = jwt_token
    
        def __call__(self, r):
            r.headers['Authorization'] = f'Bearer {self.jwt_token}'
            return r
    
    async def search(
        skip: int = 0,
        limit: int = 100,
        params: SearchQueryParams = Depends(),
    ) -> Any:
        """
        Search query.
        """
    
        zookeeper = pysolr.ZooKeeper("search-zoo1,search-zoo2,search-zoo3")
    
        solr = pysolr.SolrCloud(zookeeper, "tag", auth=JWTAuth(add-token-here))
    
        res = solr.search(q=params.query, start=skip, rows=limit)
    
        return SearchResults(data=res, count=res.hits)