From 0b1768190783a5b01866e7b8435be475039c4d24 Mon Sep 17 00:00:00 2001 From: Leo Vasanko Date: Sat, 6 Dec 2025 00:51:18 +0000 Subject: [PATCH] Changed origin config to take multiple origins and if any are configured, restrict access to these. Removed bootstrap name options of created org and user (both can be easily renamed from web ui). Cleanup. --- paskia/bootstrap.py | 42 ++++------------------- paskia/db/sql.py | 8 +++-- paskia/fastapi/__main__.py | 8 ----- paskia/fastapi/admin.py | 4 +-- paskia/fastapi/api.py | 3 +- paskia/fastapi/mainapp.py | 2 -- paskia/fastapi/session.py | 2 +- paskia/fastapi/user.py | 4 +-- paskia/globals.py | 4 +-- paskia/util/hostutil.py | 69 +++++++++++++------------------------- 10 files changed, 40 insertions(+), 106 deletions(-) diff --git a/paskia/bootstrap.py b/paskia/bootstrap.py index ec177f0..9043e9a 100644 --- a/paskia/bootstrap.py +++ b/paskia/bootstrap.py @@ -53,16 +53,10 @@ async def _create_and_log_admin_reset_link(user_uuid, message, session_type) -> return reset_link -async def bootstrap_system( - user_name: str | None = None, org_name: str | None = None -) -> dict: +async def bootstrap_system() -> dict: """ Bootstrap the entire system with default data. - Args: - user_name: Display name for the admin user (default: "Admin") - org_name: Display name for the organization (default: "Organization") - Returns: dict: Contains information about created entities and reset link """ @@ -70,7 +64,7 @@ async def bootstrap_system( perm0 = Permission(id="auth:admin", display_name="Master Admin") await globals.db.instance.create_permission(perm0) - org = Org(uuid7.create(), org_name or "Organization") + org = Org(uuid7.create(), "Organization") await globals.db.instance.create_organization(org) # After creation, org.permissions now includes the auto-created org admin permission @@ -89,7 +83,7 @@ async def bootstrap_system( user = User( uuid=uuid7.create(), - display_name=user_name or "Admin", + display_name="Admin", role_uuid=role.uuid, created_at=datetime.now(timezone.utc), visits=0, @@ -159,16 +153,10 @@ async def check_admin_credentials() -> bool: return False -async def bootstrap_if_needed( - default_admin: str | None = None, default_org: str | None = None -) -> bool: +async def bootstrap_if_needed() -> bool: """ Check if system needs bootstrapping and perform it if necessary. - Args: - default_admin: Display name for the admin user - default_org: Display name for the organization - Returns: bool: True if bootstrapping was performed, False if system was already set up """ @@ -185,35 +173,17 @@ async def bootstrap_if_needed( # No admin permission found, need to bootstrap # Bootstrap creates the admin user AND the reset link, so no need to check credentials after - await bootstrap_system(default_admin, default_org) + await bootstrap_system() return True # CLI interface async def main(): """Main CLI entry point for bootstrapping.""" - import argparse - # Configure logging for CLI usage logging.basicConfig(level=logging.INFO, format="%(message)s", force=True) - parser = argparse.ArgumentParser( - description="Bootstrap passkey authentication system" - ) - parser.add_argument( - "--user-name", - default=None, - help="Name for the admin user (default: Admin)", - ) - parser.add_argument( - "--org-name", - default=None, - help="Name for the organization (default: Organization)", - ) - - args = parser.parse_args() - - await globals.init(default_admin=args.user_name, default_org=args.org_name) + await globals.init() if __name__ == "__main__": diff --git a/paskia/db/sql.py b/paskia/db/sql.py index aac4993..49b7d5b 100644 --- a/paskia/db/sql.py +++ b/paskia/db/sql.py @@ -5,6 +5,7 @@ This module provides an async database layer using SQLAlchemy async mode for managing users and credentials in a WebAuthn authentication system. """ +import os from contextlib import asynccontextmanager from datetime import datetime, timezone from uuid import UUID @@ -40,7 +41,7 @@ from paskia.db import ( ) from paskia.globals import db -DB_PATH = "sqlite+aiosqlite:///paskia.sqlite" +DB_PATH_DEFAULT = "sqlite+aiosqlite:///paskia.sqlite" def _normalize_dt(value: datetime | None) -> datetime | None: @@ -52,7 +53,8 @@ def _normalize_dt(value: datetime | None) -> datetime | None: async def init(*args, **kwargs): - db.instance = DB() + db_path = os.environ.get("PASKIA_DB", DB_PATH_DEFAULT) + db.instance = DB(db_path) await db.instance.init_db() @@ -289,7 +291,7 @@ class RolePermission(Base): class DB(DatabaseInterface): """Database class that handles its own connections.""" - def __init__(self, db_path: str = DB_PATH): + def __init__(self, db_path: str = DB_PATH_DEFAULT): """Initialize with database path.""" self.engine = create_async_engine(db_path, echo=False) # Ensure SQLite foreign key enforcement is ON for every new connection diff --git a/paskia/fastapi/__main__.py b/paskia/fastapi/__main__.py index ae96943..5976582 100644 --- a/paskia/fastapi/__main__.py +++ b/paskia/fastapi/__main__.py @@ -174,10 +174,6 @@ def main(): # Collect origins and handle auth_host origins = getattr(args, "origins", None) or [] - if not getattr(args, "auth_host", None): - # Preserve pre-set env variable if CLI option omitted - args.auth_host = os.environ.get("PASKIA_AUTH_HOST") - if args.auth_host: # Normalize auth_host with scheme if "://" not in args.auth_host: @@ -203,8 +199,6 @@ def main(): "rp_name": args.rp_name or None, "origins": origins or None, "auth_host": args.auth_host or None, - "default_admin": os.getenv("PASKIA_DEFAULT_ADMIN") or None, - "default_org": os.getenv("PASKIA_DEFAULT_ORG") or None, } os.environ["PASKIA_CONFIG"] = json.dumps(config) @@ -217,8 +211,6 @@ def main(): rp_id=config["rp_id"], rp_name=config["rp_name"], origins=config["origins"], - default_admin=config["default_admin"], - default_org=config["default_org"], bootstrap=True, ) ) diff --git a/paskia/fastapi/admin.py b/paskia/fastapi/admin.py index c74e066..07ecba4 100644 --- a/paskia/fastapi/admin.py +++ b/paskia/fastapi/admin.py @@ -452,9 +452,7 @@ async def admin_create_user_registration_link( expiry=expiry, token_type=token_type, ) - url = hostutil.reset_link_url( - token, request.url.scheme, request.headers.get("host") - ) + url = hostutil.reset_link_url(token) return { "url": url, "expires": ( diff --git a/paskia/fastapi/api.py b/paskia/fastapi/api.py index 0585a8d..c395de3 100644 --- a/paskia/fastapi/api.py +++ b/paskia/fastapi/api.py @@ -21,7 +21,7 @@ from paskia.authsession import ( session_expiry, ) from paskia.fastapi import authz, session, user -from paskia.fastapi.session import AUTH_COOKIE +from paskia.fastapi.session import AUTH_COOKIE, AUTH_COOKIE_NAME from paskia.globals import db from paskia.globals import passkey as global_passkey from paskia.util import frontend, hostutil, htmlutil, passphrase, userinfo @@ -199,6 +199,7 @@ async def get_settings(): "rp_name": pk.rp_name, "ui_base_path": base_path, "auth_host": hostutil.configured_auth_host(), + "session_cookie": AUTH_COOKIE_NAME, } diff --git a/paskia/fastapi/mainapp.py b/paskia/fastapi/mainapp.py index adc7491..74707e7 100644 --- a/paskia/fastapi/mainapp.py +++ b/paskia/fastapi/mainapp.py @@ -31,8 +31,6 @@ async def lifespan(app: FastAPI): # pragma: no cover - startup path rp_id=config["rp_id"], rp_name=config["rp_name"], origins=config["origins"], - default_admin=config["default_admin"], - default_org=config["default_org"], bootstrap=False, ) except ValueError as e: diff --git a/paskia/fastapi/session.py b/paskia/fastapi/session.py index f2b2665..778b72f 100644 --- a/paskia/fastapi/session.py +++ b/paskia/fastapi/session.py @@ -12,7 +12,7 @@ from fastapi import Cookie, Request, Response, WebSocket from paskia.authsession import EXPIRES -AUTH_COOKIE_NAME = "__Host-auth" +AUTH_COOKIE_NAME = "__Host-paskia" AUTH_COOKIE = Cookie(None, alias=AUTH_COOKIE_NAME) diff --git a/paskia/fastapi/user.py b/paskia/fastapi/user.py index df9ea60..9dd128e 100644 --- a/paskia/fastapi/user.py +++ b/paskia/fastapi/user.py @@ -150,9 +150,7 @@ async def api_create_link( expiry=expiry, token_type="device addition", ) - url = hostutil.reset_link_url( - token, request.url.scheme, request.headers.get("host") - ) + url = hostutil.reset_link_url(token) return { "message": "Registration link generated successfully", "url": url, diff --git a/paskia/globals.py b/paskia/globals.py index 5d8508f..ba918ba 100644 --- a/paskia/globals.py +++ b/paskia/globals.py @@ -30,8 +30,6 @@ async def init( rp_id: str = "localhost", rp_name: str | None = None, origins: list[str] | None = None, - default_admin: str | None = None, - default_org: str | None = None, *, bootstrap: bool = True, ) -> None: @@ -60,7 +58,7 @@ async def init( # Bootstrap system if needed from .bootstrap import bootstrap_if_needed - await bootstrap_if_needed(default_admin, default_org) + await bootstrap_if_needed() # Global instances diff --git a/paskia/util/hostutil.py b/paskia/util/hostutil.py index 12d4f4f..cfc2cd2 100644 --- a/paskia/util/hostutil.py +++ b/paskia/util/hostutil.py @@ -8,35 +8,24 @@ from urllib.parse import urlparse, urlsplit from paskia.globals import passkey as global_passkey -def _default_origin_scheme() -> str: - """Get the default scheme from configured origins, or fallback to https.""" - allowed = global_passkey.instance.allowed_origins - if allowed: - # Pick any origin from the set - origin_url = urlparse(next(iter(allowed))) - return origin_url.scheme or "https" - return "https" - - @lru_cache(maxsize=1) -def _load_config() -> tuple[str | None, str] | None: - """Load auth_host from PASKIA_CONFIG JSON, falling back to PASKIA_AUTH_HOST.""" - # Try PASKIA_CONFIG first (set by CLI) - config_json = os.getenv("PASKIA_CONFIG") - if config_json: - config = json.loads(config_json) - raw = config["auth_host"] # Always present, may be None - else: - # Fallback for external usage (e.g., PASKIA_AUTH_HOST set directly) - raw = os.getenv("PASKIA_AUTH_HOST") +def _load_config() -> tuple[str, str] | None: + """Load auth_host from PASKIA_CONFIG JSON. + Returns (scheme, netloc) tuple if configured, None otherwise. + """ + config_json = os.getenv("PASKIA_CONFIG") + if not config_json: + return None + config = json.loads(config_json) + raw = config["auth_host"] # Always present, may be None if not raw: return None parsed = urlparse(raw if "://" in raw else f"//{raw}") netloc = parsed.netloc or parsed.path if not netloc: return None - return (parsed.scheme or None, netloc.strip("/")) + return (parsed.scheme or "https", netloc.strip("/")) def configured_auth_host() -> str | None: @@ -52,36 +41,24 @@ def ui_base_path() -> str: return "/" if is_root_mode() else "/auth/" -def auth_site_base_url(scheme: str | None = None, host: str | None = None) -> str: +def auth_site_base_url() -> str: + """Return the base URL for the auth site UI. + + If auth_host is configured (root mode), returns its URL. + Otherwise, constructs URL from rp_id with /auth/ path. + """ cfg = _load_config() if cfg: - cfg_scheme, cfg_host = cfg - scheme_to_use = cfg_scheme or scheme or _default_origin_scheme() - netloc = cfg_host - else: - if host: - scheme_to_use = scheme or _default_origin_scheme() - netloc = host.strip("/") - else: - # Use the first allowed origin, or fallback to rp_id - allowed = global_passkey.instance.allowed_origins - if allowed: - origin = allowed[0].rstrip("/") - return f"{origin}{ui_base_path()}" - # Fallback: construct from rp_id - rp_id = global_passkey.instance.rp_id - return f"https://{rp_id}{ui_base_path()}" + scheme, netloc = cfg + return f"{scheme}://{netloc}/" - base = f"{scheme_to_use}://{netloc}".rstrip("/") - path = ui_base_path().lstrip("/") - return f"{base}/{path}" if path else f"{base}/" + # Not in root mode: use rp_id with /auth/ path + rp_id = global_passkey.instance.rp_id + return f"https://{rp_id}/auth/" -def reset_link_url( - token: str, scheme: str | None = None, host: str | None = None -) -> str: - base = auth_site_base_url(scheme, host) - return f"{base}{token}" +def reset_link_url(token: str) -> str: + return f"{auth_site_base_url()}{token}" def reload_config() -> None: