Skip to content

GET /scopes/{scope_id}/policy returns 500 (uncaught ScopeNotFoundError) for an unknown or not-yet-synced scope #919

Description

@Kyzgor

Describe the bug

GET /scopes/{scope_id}/policy returns HTTP 500 {"error": "Uncaught server exception"} for a scope that is not in the repository: an unknown scope, or an existing one during the create/sync window. It should be a 404.

get_scope_policy catches ScopeNotFoundError and falls back to _generate_default_scope_bundle, which does scopes.get("default") (scopes/api.py#L288). When no default scope exists, that re-raises ScopeNotFoundError uncaught, so the response is a 500. Sibling routes get_scope and refresh_scope already return 404 for an absent scope, and the OPAL client's policy fetcher already handles a 404.

To Reproduce

Against the route alone (no Docker/Redis):

from fastapi import FastAPI
from fastapi.testclient import TestClient
from fastapi_websocket_pubsub import PubSubEndpoint
from opal_common.authentication.deps import JWTAuthenticator
from opal_common.authentication.types import JWTAlgorithm
from opal_common.authentication.verifier import JWTVerifier
from opal_common.middleware import configure_middleware
from opal_server.scopes.api import init_scope_router
from opal_server.scopes.scope_repository import ScopeNotFoundError

class InMemoryScopeRepository:
    def __init__(self): self._scopes = {}
    async def get(self, sid):
        if sid in self._scopes: return self._scopes[sid]
        raise ScopeNotFoundError(sid)
    async def put(self, s): self._scopes[s.scope_id] = s
    async def all(self): return list(self._scopes.values())
    async def delete(self, sid): self._scopes.pop(sid, None)

def _auth():
    return JWTAuthenticator(JWTVerifier(public_key=None, algorithm=JWTAlgorithm.RS256,
        audience="https://api.opal.ac/v1/", issuer="https://opal.ac/"))

def test_absent_scope_policy_returns_500():
    app = FastAPI(); configure_middleware(app)
    app.include_router(init_scope_router(InMemoryScopeRepository(), _auth(), PubSubEndpoint()), prefix="/scopes")
    resp = TestClient(app, raise_server_exceptions=False).get("/scopes/documents/policy")
    assert resp.status_code == 500  # bug; should be 404

Or against docker/docker-compose-scopes-example.yml (it provisions myscope/herscope, so documents is absent), requesting a scope that was never created:

$ curl -s -o /dev/null -w '%{http_code}\n' 'http://localhost:7002/scopes/documents/policy?path=.'
500
$ curl -s 'http://localhost:7002/scopes/documents/policy?path=.'
{"error":"Uncaught server exception"}

Server log: the requested scope, then the default fallback, then the re-raise, so the response is a 500:

ScopeNotFoundError: Scope documents not found
ScopeNotFoundError: Scope default not found
ScopeNotFoundError: Scope documents not found

This also hits an existing scope during the normal create/sync window, since the OPAL client fetches /scopes/{scope_id}/policy?path=. on connect. The #779 reporter's posted log (v0.8.0) shows it returning 500 repeatedly until the scope syncs:

"GET /scopes/documents/policy?path=. HTTP/1.1" 500
"GET /scopes/documents/policy?path=. HTTP/1.1" 500
"GET /scopes/documents/policy?path=. HTTP/1.1" 200   # after sync
full server traceback
opal_server.scopes.scope_repository.ScopeNotFoundError: Scope documents not found
    raise ScopeNotFoundError(scope_id)
opal_server.scopes.scope_repository.ScopeNotFoundError: Scope default not found
    raise ScopeNotFoundError(scope_id)
opal_server.scopes.scope_repository.ScopeNotFoundError: Scope documents not found

Expected behavior

A 404 for an unknown or not-yet-synced scope, matching get_scope/refresh_scope and the client's 404 handling; _generate_default_scope_bundle should handle an absent default scope gracefully (or the route could degrade to an empty bundle). Happy to open a PR.

OPAL version

master (76f898e), via the test above and a scopes-mode Docker run; also in v0.8.0 (the #779 reporter's posted log).

Related: #779.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions