Move imports to top of file.

This commit is contained in:
2026-01-27 20:16:32 +00:00
parent e8247a2c7f
commit 7504aaf7e0
17 changed files with 51 additions and 75 deletions
+9 -6
View File
@@ -9,13 +9,16 @@ independent of any web framework:
""" """
from datetime import datetime, timezone from datetime import datetime, timezone
from typing import TYPE_CHECKING
from uuid import UUID from uuid import UUID
from paskia import db from paskia import db
from paskia.config import SESSION_LIFETIME from paskia.config import RESET_LIFETIME, SESSION_LIFETIME
from paskia.db import ResetToken, Session
from paskia.util import hostutil from paskia.util import hostutil
if TYPE_CHECKING:
from paskia.db import ResetToken, Session
EXPIRES = SESSION_LIFETIME EXPIRES = SESSION_LIFETIME
@@ -24,21 +27,21 @@ def expires() -> datetime:
def reset_expires() -> datetime: def reset_expires() -> datetime:
from .config import RESET_LIFETIME
return datetime.now(timezone.utc) + RESET_LIFETIME return datetime.now(timezone.utc) + RESET_LIFETIME
async def get_reset(token: str) -> ResetToken: async def get_reset(token: str) -> "ResetToken":
"""Validate a credential reset token.""" """Validate a credential reset token."""
record = db.get_reset_token(token) record = db.get_reset_token(token)
if record: if record:
return record return record
raise ValueError("This authentication link is no longer valid.") raise ValueError("This authentication link is no longer valid.")
async def get_session(token: str, host: str | None = None) -> Session: async def get_session(token: str, host: str | None = None) -> "Session":
"""Validate a session token and return session data if valid.""" """Validate a session token and return session data if valid."""
host = hostutil.normalize_host(host) host = hostutil.normalize_host(host)
if not host: if not host:
raise ValueError("Invalid host") raise ValueError("Invalid host")
+2 -6
View File
@@ -9,8 +9,8 @@ generating a reset link for initial admin setup.
import asyncio import asyncio
import logging import logging
from paskia import db from paskia import authsession, db, globals
from paskia.util import hostutil from paskia.util import hostutil, passphrase
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -72,8 +72,6 @@ async def check_admin_credentials() -> bool:
if not db.get_user_credential_ids(admin_user.uuid): if not db.get_user_credential_ids(admin_user.uuid):
# Admin exists but has no credentials, create reset link # Admin exists but has no credentials, create reset link
from paskia import authsession
from paskia.util import passphrase
token = passphrase.generate() token = passphrase.generate()
expiry = authsession.reset_expires() expiry = authsession.reset_expires()
@@ -118,8 +116,6 @@ async def main():
# Configure logging for CLI usage # Configure logging for CLI usage
logging.basicConfig(level=logging.INFO, format="%(message)s", force=True) logging.basicConfig(level=logging.INFO, format="%(message)s", force=True)
from paskia import globals
await globals.init() await globals.init()
+2 -3
View File
@@ -19,6 +19,7 @@ Usage:
db.create_user(user) db.create_user(user)
""" """
import paskia.db.operations as operations
from paskia.db.background import ( from paskia.db.background import (
start_background, start_background,
start_cleanup, start_cleanup,
@@ -81,9 +82,7 @@ from paskia.db.structs import (
def data() -> DB: def data() -> DB:
"""Get the database instance for direct read access.""" """Get the database instance for direct read access."""
from paskia.db.operations import _db return operations._db
return _db
__all__ = [ __all__ = [
+2 -2
View File
@@ -8,6 +8,8 @@ import asyncio
import logging import logging
from datetime import datetime, timezone from datetime import datetime, timezone
from paskia.db.operations import _db, _store
# Flush changes to disk every N seconds # Flush changes to disk every N seconds
FLUSH_INTERVAL = 1 FLUSH_INTERVAL = 1
# Cleanup expired items every N seconds (cheap when nothing to remove) # Cleanup expired items every N seconds (cheap when nothing to remove)
@@ -20,7 +22,6 @@ _background_task: asyncio.Task | None = None
def cleanup() -> None: def cleanup() -> None:
"""Remove expired sessions and reset tokens from the database.""" """Remove expired sessions and reset tokens from the database."""
from paskia.db.operations import _db
if _db is None: if _db is None:
return return
@@ -45,7 +46,6 @@ def cleanup() -> None:
async def flush() -> None: async def flush() -> None:
"""Write all pending database changes to disk.""" """Write all pending database changes to disk."""
from paskia.db.operations import _store
if _store is None: if _store is None:
_logger.warning("flush() called but _store is None") _logger.warning("flush() called but _store is None")
+6 -7
View File
@@ -13,6 +13,9 @@ import secrets
from datetime import datetime, timezone from datetime import datetime, timezone
from uuid import UUID from uuid import UUID
import uuid7
from paskia.config import SESSION_LIFETIME
from paskia.db.jsonl import ( from paskia.db.jsonl import (
DB_PATH_DEFAULT, DB_PATH_DEFAULT,
JsonlStore, JsonlStore,
@@ -28,6 +31,8 @@ from paskia.db.structs import (
SessionContext, SessionContext,
User, User,
) )
from paskia.util.hostutil import normalize_host
from paskia.util.passphrase import generate as generate_passphrase
from paskia.util.passphrase import is_well_formed as _is_passphrase from paskia.util.passphrase import is_well_formed as _is_passphrase
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
@@ -137,7 +142,6 @@ def get_session_context(
- Example usage in docstring (db/__init__.py:16) - Example usage in docstring (db/__init__.py:16)
- Get session context from auth token (util/permutil.py:43) - Get session context from auth token (util/permutil.py:43)
""" """
from paskia.util.hostutil import normalize_host
if session_key not in _db.sessions: if session_key not in _db.sessions:
return None return None
@@ -279,7 +283,6 @@ def create_organization(org: Org, *, ctx: SessionContext | None = None) -> None:
_db.orgs[org.uuid] = new_org _db.orgs[org.uuid] = new_org
new_org.uuid = org.uuid new_org.uuid = org.uuid
# Create Administration role with org admin permission # Create Administration role with org admin permission
import uuid7
admin_role_uuid = uuid7.create() admin_role_uuid = uuid7.create()
# Find the auth:org:admin permission UUID # Find the auth:org:admin permission UUID
@@ -786,7 +789,6 @@ def create_credential_session(
Returns the generated session token. Returns the generated session token.
""" """
from paskia.config import SESSION_LIFETIME
now = datetime.now(timezone.utc) now = datetime.now(timezone.utc)
expiry = now + SESSION_LIFETIME expiry = now + SESSION_LIFETIME
@@ -853,10 +855,6 @@ def bootstrap(
Returns: Returns:
The reset passphrase for admin registration. The reset passphrase for admin registration.
""" """
import uuid7
from paskia.authsession import reset_expires
from paskia.util.passphrase import generate as generate_passphrase
# Check if system is already bootstrapped # Check if system is already bootstrapped
for p in _db.permissions.values(): for p in _db.permissions.values():
@@ -876,6 +874,7 @@ def bootstrap(
if reset_passphrase is None: if reset_passphrase is None:
reset_passphrase = generate_passphrase() reset_passphrase = generate_passphrase()
if reset_expiry is None: if reset_expiry is None:
from paskia.util.timeutil import reset_expires # noqa: PLC0415
reset_expiry = reset_expires() reset_expiry = reset_expires()
reset_key = _reset_key(reset_passphrase) reset_key = _reset_key(reset_passphrase)
+7 -10
View File
@@ -5,10 +5,16 @@ from uuid import UUID
from fastapi import Body, FastAPI, HTTPException, Query, Request, Response from fastapi import Body, FastAPI, HTTPException, Query, Request, Response
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
from paskia import aaguid as aaguid_mod
from paskia import db from paskia import db
from paskia.authsession import EXPIRES, reset_expires from paskia.authsession import EXPIRES, reset_expires
from paskia.db import Org as OrgDC
from paskia.db import Permission as PermDC
from paskia.db import Role as RoleDC
from paskia.db import User as UserDC
from paskia.fastapi import authz from paskia.fastapi import authz
from paskia.fastapi.session import AUTH_COOKIE from paskia.fastapi.session import AUTH_COOKIE
from paskia.globals import passkey
from paskia.util import ( from paskia.util import (
hostutil, hostutil,
passphrase, passphrase,
@@ -17,6 +23,7 @@ from paskia.util import (
useragent, useragent,
vitedev, vitedev,
) )
from paskia.util.hostutil import normalize_host
app = FastAPI() app = FastAPI()
@@ -137,7 +144,6 @@ async def admin_create_org(
ctx = await authz.verify( ctx = await authz.verify(
auth, ["auth:admin"], host=request.headers.get("host"), match=permutil.has_all auth, ["auth:admin"], host=request.headers.get("host"), match=permutil.has_all
) )
from ..db import Org as OrgDC # local import to avoid cycles
display_name = payload.get("display_name") or "New Organization" display_name = payload.get("display_name") or "New Organization"
permissions = payload.get("permissions") or [] permissions = payload.get("permissions") or []
@@ -296,7 +302,6 @@ async def admin_create_role(
raise authz.AuthException( raise authz.AuthException(
status_code=403, detail="Insufficient permissions", mode="forbidden" status_code=403, detail="Insufficient permissions", mode="forbidden"
) )
from ..db import Role as RoleDC
display_name = payload.get("display_name") or "New Role" display_name = payload.get("display_name") or "New Role"
perms = payload.get("permissions") or [] perms = payload.get("permissions") or []
@@ -486,7 +491,6 @@ async def admin_create_user(
role_name = payload.get("role") role_name = payload.get("role")
if not display_name or not role_name: if not display_name or not role_name:
raise ValueError("display_name and role are required") raise ValueError("display_name and role are required")
from ..db import User as UserDC
roles = [r for r in db.data().roles.values() if r.org == org_uuid] roles = [r for r in db.data().roles.values() if r.org == org_uuid]
role_obj = next((r for r in roles if r.display_name == role_name), None) role_obj = next((r for r in roles if r.display_name == role_name), None)
@@ -674,7 +678,6 @@ async def admin_get_user_detail(
"sign_count": c.sign_count, "sign_count": c.sign_count,
} }
) )
from .. import aaguid as aaguid_mod
aaguid_info = aaguid_mod.filter(aaguids) aaguid_info = aaguid_mod.filter(aaguids)
@@ -854,7 +857,6 @@ def _validate_permission_domain(domain: str | None) -> None:
"""Validate that domain is rp_id or a subdomain of it.""" """Validate that domain is rp_id or a subdomain of it."""
if domain is None: if domain is None:
return return
from paskia.globals import passkey
rp_id = passkey.instance.rp_id rp_id = passkey.instance.rp_id
if domain == rp_id or domain.endswith(f".{rp_id}"): if domain == rp_id or domain.endswith(f".{rp_id}"):
@@ -870,7 +872,6 @@ def _check_admin_lockout(
Raises ValueError if this change would result in no auth:admin permissions Raises ValueError if this change would result in no auth:admin permissions
being accessible from the current host. being accessible from the current host.
""" """
from paskia.util.hostutil import normalize_host
normalized_host = normalize_host(current_host) normalized_host = normalize_host(current_host)
host_without_port = normalized_host.rsplit(":", 1)[0] if normalized_host else None host_without_port = normalized_host.rsplit(":", 1)[0] if normalized_host else None
@@ -905,7 +906,6 @@ def _check_admin_lockout_on_delete(perm_uuid: str, current_host: str | None) ->
Raises ValueError if this deletion would result in no auth:admin permissions Raises ValueError if this deletion would result in no auth:admin permissions
being accessible from the current host. being accessible from the current host.
""" """
from paskia.util.hostutil import normalize_host
normalized_host = normalize_host(current_host) normalized_host = normalize_host(current_host)
host_without_port = normalized_host.rsplit(":", 1)[0] if normalized_host else None host_without_port = normalized_host.rsplit(":", 1)[0] if normalized_host else None
@@ -970,7 +970,6 @@ async def admin_create_permission(
match=permutil.has_all, match=permutil.has_all,
max_age="5m", max_age="5m",
) )
from ..db import Permission as PermDC
scope = payload.get("scope") or payload.get( scope = payload.get("scope") or payload.get(
"id" "id"
@@ -1018,8 +1017,6 @@ async def admin_update_permission(
if perm.scope == "auth:admin" or new_scope == "auth:admin": if perm.scope == "auth:admin" or new_scope == "auth:admin":
_check_admin_lockout(str(perm.uuid), domain_value, request.headers.get("host")) _check_admin_lockout(str(perm.uuid), domain_value, request.headers.get("host"))
from ..db import Permission as PermDC
updated_perm = PermDC( updated_perm = PermDC(
scope=new_scope, scope=new_scope,
display_name=new_display_name, display_name=new_display_name,
+2 -3
View File
@@ -1,3 +1,4 @@
import json
import logging import logging
import os import os
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
@@ -7,6 +8,7 @@ from fastapi import FastAPI, HTTPException, Request, Response
from fastapi.responses import FileResponse, RedirectResponse from fastapi.responses import FileResponse, RedirectResponse
from fastapi_vue import Frontend from fastapi_vue import Frontend
from paskia import globals
from paskia.fastapi import admin, api, auth_host, ws from paskia.fastapi import admin, api, auth_host, ws
from paskia.fastapi.session import AUTH_COOKIE from paskia.fastapi.session import AUTH_COOKIE
from paskia.util import hostutil, passphrase, vitedev from paskia.util import hostutil, passphrase, vitedev
@@ -30,9 +32,6 @@ async def lifespan(app: FastAPI): # pragma: no cover - startup path
so that uvicorn reload / multiprocess workers inherit the settings. so that uvicorn reload / multiprocess workers inherit the settings.
All keys are guaranteed to exist; values are already normalized by __main__.py. All keys are guaranteed to exist; values are already normalized by __main__.py.
""" """
import json
from paskia import globals
config = json.loads(os.environ["PASKIA_CONFIG"]) config = json.loads(os.environ["PASKIA_CONFIG"])
+2 -7
View File
@@ -16,10 +16,11 @@ import base64url
from fastapi import FastAPI, WebSocket, WebSocketDisconnect from fastapi import FastAPI, WebSocket, WebSocketDisconnect
from paskia import db, remoteauth from paskia import db, remoteauth
from paskia.authsession import expires
from paskia.fastapi.session import infodict from paskia.fastapi.session import infodict
from paskia.fastapi.wschat import authenticate_chat from paskia.fastapi.wschat import authenticate_chat
from paskia.fastapi.wsutil import validate_origin, websocket_error_handler from paskia.fastapi.wsutil import validate_origin, websocket_error_handler
from paskia.util import passphrase, pow from paskia.util import hostutil, passphrase, pow, useragent
# Create a FastAPI subapp for remote auth WebSocket endpoints # Create a FastAPI subapp for remote auth WebSocket endpoints
app = FastAPI() app = FastAPI()
@@ -268,7 +269,6 @@ async def websocket_remote_auth_permit(ws: WebSocket):
6. Client sends WebAuthn response 6. Client sends WebAuthn response
7. Server sends {status: "success", message: "..."} 7. Server sends {status: "success", message: "..."}
""" """
from paskia.util import useragent
origin = validate_origin(ws) origin = validate_origin(ws)
@@ -289,7 +289,6 @@ async def websocket_remote_auth_permit(ws: WebSocket):
) )
request = None request = None
webauthn_challenge = None
explicitly_denied = False explicitly_denied = False
try: try:
@@ -321,8 +320,6 @@ async def websocket_remote_auth_permit(ws: WebSocket):
if request.action == "register": if request.action == "register":
# For registration, create a reset token for device addition # For registration, create a reset token for device addition
from paskia.authsession import expires
from paskia.util import hostutil
token_str = passphrase.generate() token_str = passphrase.generate()
expiry = expires() expiry = expires()
@@ -345,8 +342,6 @@ async def websocket_remote_auth_permit(ws: WebSocket):
) )
else: else:
# Default login action # Default login action
from paskia.authsession import expires
from paskia.util import hostutil
normalized_host = hostutil.normalize_host(request.host) normalized_host = hostutil.normalize_host(request.host)
session_token = db.login( session_token = db.login(
+3 -3
View File
@@ -1,5 +1,8 @@
from typing import Generic, TypeVar from typing import Generic, TypeVar
from paskia import db, remoteauth
from paskia.bootstrap import bootstrap_if_needed
from paskia.db import start_background
from paskia.sansio import Passkey from paskia.sansio import Passkey
T = TypeVar("T") T = TypeVar("T")
@@ -42,7 +45,6 @@ async def init(
Set PASKIA_DB environment variable to specify the JSONL database file path. Set PASKIA_DB environment variable to specify the JSONL database file path.
Default: paskia.jsonl Default: paskia.jsonl
""" """
from . import db, remoteauth
# Initialize passkey instance with provided parameters # Initialize passkey instance with provided parameters
passkey.instance = Passkey( passkey.instance = Passkey(
@@ -59,12 +61,10 @@ async def init(
if bootstrap: if bootstrap:
# Bootstrap system if needed # Bootstrap system if needed
from .bootstrap import bootstrap_if_needed
await bootstrap_if_needed() await bootstrap_if_needed()
# Start background flush/cleanup task after bootstrap # Start background flush/cleanup task after bootstrap
from .db import start_background
await start_background() await start_background()
+1 -1
View File
@@ -11,6 +11,7 @@ Or via the CLI entry point (if installed):
paskia-migrate --sql sqlite+aiosqlite:///paskia.sqlite --json paskia.jsonl paskia-migrate --sql sqlite+aiosqlite:///paskia.sqlite --json paskia.jsonl
""" """
import argparse
import asyncio import asyncio
import re import re
from datetime import datetime, timezone from datetime import datetime, timezone
@@ -257,7 +258,6 @@ async def migrate_from_sql(
def main(): def main():
"""CLI entry point for migration.""" """CLI entry point for migration."""
import argparse
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
description="Migrate Paskia database from SQL to JSON" description="Migrate Paskia database from SQL to JSON"
+1 -2
View File
@@ -24,7 +24,7 @@ from datetime import datetime, timedelta, timezone
from typing import Callable from typing import Callable
from uuid import UUID from uuid import UUID
from paskia.util import passphrase from paskia.util import passphrase, pow
# Remote auth requests expire after this duration # Remote auth requests expire after this duration
REMOTE_AUTH_LIFETIME = timedelta(minutes=5) REMOTE_AUTH_LIFETIME = timedelta(minutes=5)
@@ -319,7 +319,6 @@ class RemoteAuthManager:
Returns: Returns:
PoW work units (pow.NORMAL or pow.HARD) PoW work units (pow.NORMAL or pow.HARD)
""" """
from paskia.util import pow
count = self.get_connection_count() count = self.get_connection_count()
return pow.HARD if count >= 10 else pow.NORMAL return pow.HARD if count >= 10 else pow.NORMAL
+1 -2
View File
@@ -3,7 +3,7 @@
import json import json
import os import os
from functools import lru_cache from functools import lru_cache
from urllib.parse import urlsplit from urllib.parse import urlparse, urlsplit
@lru_cache(maxsize=1) @lru_cache(maxsize=1)
@@ -24,7 +24,6 @@ def dedicated_auth_host() -> str | None:
auth_host = _load_config().get("auth_host") auth_host = _load_config().get("auth_host")
if not auth_host: if not auth_host:
return None return None
from urllib.parse import urlparse
parsed = urlparse(auth_host if "://" in auth_host else f"//{auth_host}") parsed = urlparse(auth_host if "://" in auth_host else f"//{auth_host}")
return parsed.netloc or parsed.path or None return parsed.netloc or parsed.path or None
+1 -1
View File
@@ -10,6 +10,7 @@ but doesn't provide server-side fetching of HTML content.
import asyncio import asyncio
import mimetypes import mimetypes
import os import os
from importlib import resources
from pathlib import Path from pathlib import Path
import httpx import httpx
@@ -24,7 +25,6 @@ def _get_dev_server() -> str | None:
def _resolve_static_dir() -> Path: def _resolve_static_dir() -> Path:
"""Resolve the static files directory.""" """Resolve the static files directory."""
from importlib import resources
# Try packaged path via importlib.resources (works for wheel/installed). # Try packaged path via importlib.resources (works for wheel/installed).
try: # pragma: no cover - trivial path resolution try: # pragma: no cover - trivial path resolution
+1 -1
View File
@@ -79,7 +79,7 @@ target-version = "py39"
line-length = 88 line-length = 88
[tool.ruff.lint] [tool.ruff.lint]
select = ["E", "F", "I", "N", "W", "UP"] select = ["E", "F", "I", "N", "W", "UP", "PLC0415"]
ignore = ["E501"] # Line too long ignore = ["E501"] # Line too long
isort.known-first-party = ["paskia"] isort.known-first-party = ["paskia"]
+5 -6
View File
@@ -19,8 +19,9 @@ import httpx
import pytest import pytest
import pytest_asyncio import pytest_asyncio
import paskia.db.operations as ops_db
from paskia import globals as paskia_globals from paskia import globals as paskia_globals
from paskia.authsession import expires from paskia.authsession import expires, reset_expires
from paskia.db import ( from paskia.db import (
Credential, Credential,
Org, Org,
@@ -36,9 +37,12 @@ from paskia.db import (
create_session, create_session,
create_user, create_user,
) )
from paskia.db.jsonl import JsonlStore
from paskia.db.operations import DB, _create_token from paskia.db.operations import DB, _create_token
from paskia.fastapi.mainapp import app
from paskia.fastapi.session import AUTH_COOKIE_NAME from paskia.fastapi.session import AUTH_COOKIE_NAME
from paskia.sansio import Passkey from paskia.sansio import Passkey
from paskia.util.passphrase import generate
@pytest.fixture(scope="session") @pytest.fixture(scope="session")
@@ -52,8 +56,6 @@ def event_loop():
@pytest_asyncio.fixture(scope="function") @pytest_asyncio.fixture(scope="function")
async def test_db() -> AsyncGenerator[DB, None]: async def test_db() -> AsyncGenerator[DB, None]:
"""Create an in-memory JSON database for testing.""" """Create an in-memory JSON database for testing."""
import paskia.db.operations as ops_db
from paskia.db.jsonl import JsonlStore
with tempfile.NamedTemporaryFile(suffix=".jsonl", delete=True) as f: with tempfile.NamedTemporaryFile(suffix=".jsonl", delete=True) as f:
db = DB() db = DB()
@@ -225,8 +227,6 @@ async def regular_session_token(
@pytest_asyncio.fixture(scope="function") @pytest_asyncio.fixture(scope="function")
async def reset_token(test_db: DB, test_user: User, test_credential: Credential) -> str: async def reset_token(test_db: DB, test_user: User, test_credential: Credential) -> str:
"""Create a reset token for the test user.""" """Create a reset token for the test user."""
from paskia.authsession import reset_expires
from paskia.util.passphrase import generate
token = generate() token = generate()
create_reset_token( create_reset_token(
@@ -248,7 +248,6 @@ async def client(
initialized first. initialized first.
""" """
# Import app after globals are set # Import app after globals are set
from paskia.fastapi.mainapp import app
transport = httpx.ASGITransport(app=app) transport = httpx.ASGITransport(app=app)
async with httpx.AsyncClient( async with httpx.AsyncClient(
+2 -8
View File
@@ -11,6 +11,7 @@ These tests cover:
- Credential management - Credential management
""" """
import os
from datetime import datetime, timezone from datetime import datetime, timezone
from uuid import UUID from uuid import UUID
@@ -19,6 +20,7 @@ import pytest
import pytest_asyncio import pytest_asyncio
import uuid7 import uuid7
from paskia import db
from paskia.authsession import expires from paskia.authsession import expires
from paskia.db import ( from paskia.db import (
Credential, Credential,
@@ -78,7 +80,6 @@ async def second_org_user(test_db: DB, second_org_role: Role) -> User:
@pytest_asyncio.fixture(scope="function") @pytest_asyncio.fixture(scope="function")
async def second_org_credential(test_db: DB, second_org_user: User) -> Credential: async def second_org_credential(test_db: DB, second_org_user: User) -> Credential:
"""Create a credential for the second org user.""" """Create a credential for the second org user."""
import os
credential = Credential.create( credential = Credential.create(
credential_id=os.urandom(32), credential_id=os.urandom(32),
@@ -139,7 +140,6 @@ async def org_admin_user(test_db: DB, org_admin_role: Role) -> User:
@pytest_asyncio.fixture(scope="function") @pytest_asyncio.fixture(scope="function")
async def org_admin_credential(test_db: DB, org_admin_user: User) -> Credential: async def org_admin_credential(test_db: DB, org_admin_user: User) -> Credential:
"""Create a credential for the org admin user.""" """Create a credential for the org admin user."""
import os
credential = Credential.create( credential = Credential.create(
credential_id=os.urandom(32), credential_id=os.urandom(32),
@@ -1423,7 +1423,6 @@ class TestAdminPermissions:
): ):
"""Cannot rename the auth:admin permission.""" """Cannot rename the auth:admin permission."""
# Get the auth:admin permission # Get the auth:admin permission
from paskia import db
perms = list(db.data().permissions.values()) perms = list(db.data().permissions.values())
admin_perm = next(p for p in perms if p.scope == "auth:admin") admin_perm = next(p for p in perms if p.scope == "auth:admin")
@@ -1478,7 +1477,6 @@ class TestAdminPermissions:
): ):
"""Cannot delete the only auth:admin permission (would lock out admin).""" """Cannot delete the only auth:admin permission (would lock out admin)."""
# Get the auth:admin permission # Get the auth:admin permission
from paskia import db
perms = list(db.data().permissions.values()) perms = list(db.data().permissions.values())
admin_perm = next(p for p in perms if p.scope == "auth:admin") admin_perm = next(p for p in perms if p.scope == "auth:admin")
@@ -1496,14 +1494,12 @@ class TestAdminPermissions:
self, client: httpx.AsyncClient, session_token: str, test_db: DB self, client: httpx.AsyncClient, session_token: str, test_db: DB
): ):
"""Can delete an auth:admin permission if another accessible one exists.""" """Can delete an auth:admin permission if another accessible one exists."""
from paskia.db import Permission
# Create a second auth:admin permission (no domain restriction) # Create a second auth:admin permission (no domain restriction)
perm2 = Permission.create(scope="auth:admin", display_name="Secondary Admin") perm2 = Permission.create(scope="auth:admin", display_name="Secondary Admin")
create_permission(perm2) create_permission(perm2)
# Get the original auth:admin permission (the one created in setup) # Get the original auth:admin permission (the one created in setup)
from paskia import db
perms = list(db.data().permissions.values()) perms = list(db.data().permissions.values())
admin_perms = [p for p in perms if p.scope == "auth:admin"] admin_perms = [p for p in perms if p.scope == "auth:admin"]
@@ -1524,7 +1520,6 @@ class TestAdminPermissions:
self, client: httpx.AsyncClient, session_token: str, test_db: DB self, client: httpx.AsyncClient, session_token: str, test_db: DB
): ):
"""Cannot delete auth:admin if remaining one has mismatched domain.""" """Cannot delete auth:admin if remaining one has mismatched domain."""
from paskia.db import Permission
# Create a second auth:admin permission with a different domain # Create a second auth:admin permission with a different domain
perm2 = Permission.create( perm2 = Permission.create(
@@ -1536,7 +1531,6 @@ class TestAdminPermissions:
# Cannot delete the original one because the remaining one is not accessible # Cannot delete the original one because the remaining one is not accessible
# Get the original auth:admin permission # Get the original auth:admin permission
from paskia import db
perms = list(db.data().permissions.values()) perms = list(db.data().permissions.values())
admin_perms = [p for p in perms if p.scope == "auth:admin" and p.domain is None] admin_perms = [p for p in perms if p.scope == "auth:admin" and p.domain is None]
+4 -7
View File
@@ -10,12 +10,15 @@ These tests cover:
- /auth/api/set-session - Set session from bearer token - /auth/api/set-session - Set session from bearer token
""" """
from datetime import datetime, timezone from datetime import datetime, timedelta, timezone
import httpx import httpx
import pytest import pytest
from paskia.authsession import EXPIRES
from paskia.db import create_session, delete_session from paskia.db import create_session, delete_session
from paskia.db.operations import _create_token
from paskia.util.passphrase import generate
from tests.conftest import auth_headers from tests.conftest import auth_headers
@@ -410,7 +413,6 @@ class TestTokenInfoEndpoint:
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_token_info_with_nonexistent_token(self, client: httpx.AsyncClient): async def test_token_info_with_nonexistent_token(self, client: httpx.AsyncClient):
"""Token info with well-formed but non-existent token should return 401.""" """Token info with well-formed but non-existent token should return 401."""
from paskia.util.passphrase import generate
fake_token = generate() fake_token = generate()
response = await client.get( response = await client.get(
@@ -499,7 +501,6 @@ class TestValidateSessionRefresh:
self, client: httpx.AsyncClient, test_db self, client: httpx.AsyncClient, test_db
): ):
"""Validate should handle session expiry during refresh attempt.""" """Validate should handle session expiry during refresh attempt."""
from paskia.db.operations import _create_token
# Create a token but don't create a session for it # Create a token but don't create a session for it
token = _create_token() token = _create_token()
@@ -519,10 +520,6 @@ class TestValidateSessionRefresh:
test_credential, test_credential,
): ):
"""Validate should return 401 if session disappears during refresh.""" """Validate should return 401 if session disappears during refresh."""
from datetime import timedelta
from paskia.authsession import EXPIRES
from paskia.db.operations import _create_token
# Create a session with an old expiry time to trigger refresh # Create a session with an old expiry time to trigger refresh
token = _create_token() token = _create_token()