127 lines
4.6 KiB
Python
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
|