Files
2026-07-13 13:12:00 +08:00

127 lines
4.6 KiB
Python

"""Tests for the read-only OIDC admin user list (``GET /auth/users``).
The OIDC analog of the accounts provider's ``GET /auth/users`` — same
response shape, admin-gated, but read-only (OIDC identities are owned by
the IdP, so there are no password-based management actions). This is the
#1489 fix: under OIDC the SPA otherwise has no admin user surface at all.
Mirrors the harness in ``test_oidc_invites.py`` (real DB + a mounted
OIDC router on a ``TestClient``, presenting the admin's session JWT as a
Bearer token).
"""
from __future__ import annotations
from collections.abc import Iterator
from pathlib import Path
import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
from omnigent.server.accounts_store import SqlAlchemyAccountStore
from omnigent.server.admin_list import AdminList
from omnigent.server.auth import UnifiedAuthProvider
from omnigent.server.oidc import OIDCConfig, mint_session_cookie
from omnigent.server.routes.auth import create_auth_router
from omnigent.stores.permission_store.sqlalchemy_store import SqlAlchemyPermissionStore
_TEST_SECRET = bytes.fromhex("aa" * 32)
def _oidc_config() -> OIDCConfig:
"""Build an OIDCConfig over plain HTTP (so TestClient cookies work)."""
return OIDCConfig(
issuer="https://accounts.google.com",
client_id="cid",
client_secret="secret",
redirect_uri="http://localhost:8000/auth/callback",
cookie_secret=_TEST_SECRET,
scopes="openid email profile",
session_ttl_hours=8,
logout_redirect_uri=None,
allowed_domains=None,
provider_type="oidc",
authorization_endpoint="https://accounts.google.com/o/oauth2/v2/auth",
token_endpoint="https://oauth2.googleapis.com/token",
jwks_uri="https://www.googleapis.com/oauth2/v3/certs",
userinfo_endpoint=None,
allow_invites=False,
)
@pytest.fixture
def oidc_users_client(tmp_path: Path, db_uri: str) -> Iterator[tuple[TestClient, str]]:
"""OIDC router mounted on a TestClient, with two users pre-seeded.
Yields the client and the admin's session JWT. ``admin@example.com``
is an admin; ``member@example.com`` is a regular user. The reserved
``local`` sentinel is also seeded (must be hidden from the list).
"""
perm_store = SqlAlchemyPermissionStore(db_uri)
account_store = SqlAlchemyAccountStore(db_uri)
admins = tmp_path / "admins"
admins.write_text("")
admin_list = AdminList(admins)
perm_store.ensure_user("admin@example.com", is_admin=True)
perm_store.ensure_user("member@example.com")
perm_store.ensure_user("local", is_admin=True) # reserved sentinel
config = _oidc_config()
provider = UnifiedAuthProvider(source="oidc", oidc_config=config)
app = FastAPI()
app.include_router(
create_auth_router(provider, perm_store, admin_list, account_store),
prefix="/auth",
)
admin_jwt = mint_session_cookie(
user_id="admin@example.com",
cookie_secret=config.cookie_secret,
ttl_hours=8,
provider="oidc",
)
with TestClient(app) as client:
yield client, admin_jwt
def test_users_route_admin_lists_real_users(
oidc_users_client: tuple[TestClient, str],
) -> None:
"""An admin gets every real user with the admin flag; sentinels hidden."""
client, admin_jwt = oidc_users_client
resp = client.get("/auth/users", headers={"Authorization": f"Bearer {admin_jwt}"})
assert resp.status_code == 200, resp.text
users = {u["id"]: u for u in resp.json()["users"]}
# Reserved `local` sentinel is excluded; both real users are present.
assert set(users) == {"admin@example.com", "member@example.com"}
assert users["admin@example.com"]["is_admin"] is True
assert users["member@example.com"]["is_admin"] is False
# OIDC identities have no password — the shape carries has_password=False.
assert users["member@example.com"]["has_password"] is False
def test_users_route_requires_auth(
oidc_users_client: tuple[TestClient, str],
) -> None:
"""An unauthenticated caller gets 401."""
client, _admin_jwt = oidc_users_client
resp = client.get("/auth/users")
assert resp.status_code == 401
def test_users_route_rejects_non_admin(
oidc_users_client: tuple[TestClient, str],
) -> None:
"""A non-admin authenticated user gets 403 (can't list users)."""
client, _admin_jwt = oidc_users_client
member_jwt = mint_session_cookie(
user_id="member@example.com",
cookie_secret=_TEST_SECRET,
ttl_hours=8,
provider="oidc",
)
resp = client.get("/auth/users", headers={"Authorization": f"Bearer {member_jwt}"})
assert resp.status_code == 403