Extract auth into enlace_auth (BREAKING; 0.1.0)#13
Conversation
8bd9162 to
ce1801d
Compare
There was a problem hiding this comment.
LGTM with three minor follow-ups (none blocking — happy to land as-is). Posting as a comment-review before approval
Overall assessment
The split is exactly what was discussed in the handoff: `build_backend(config, *, plugins=())` is a clean keyword-only entry, the `Plugin` callable signature matches what `enlace_auth.plugin` advertises, and the `ENLACE_PLUGINS=mod:attr,...` env-var resolver in `create_app()` is well-bounded (raises `EnlaceConfigError` on parse / import / lookup / non-callable failures, distinct from any auth-specific error).
Removed surface in `enlace.auth` and `enlace.stores` is clean — all the `removed` files in the diff confirm both subpackages are gone, not just emptied. CI green on 3.10 and 3.12.
Things I checked closely
- Plugin invocation order in `build_backend`: plugins run after the parent app is created and CORS middleware is added, but before `/_apps` is registered. That's the right order — `/_apps` reads `request.state.user_id` which the auth plugin's middleware sets, so the route definition has to come after the plugin mounts the middleware. ✓
- ENLACE_PLUGINS resolver: handles each failure mode distinctly (no colon → format error; ImportError → import error; AttributeError → name error; non-callable → type error). All raise `EnlaceConfigError` so callers can catch one class. ✓
- `AppConfig` round-trip fields (`access`, `allowed_users`, `shared_password_env`) all kept; `access` correctly relaxed from `Literal[...]` to plain `str`. Existing `app.toml` files in tw_platform and the in-progress papp PR (per-app policies) round-trip without edits. ✓
- `PlatformConfig.auth` and `.stores` as untyped dicts — exactly what we agreed. tw_platform's `platform.toml` parses unchanged (verified: `coerce_auth_config(config.auth).enabled` is the new path; will reference in tw_platform docs). ✓
- `run_doctor(extra_static_checks=, extra_http_checks=)` plugin hooks present and wired through. ✓
- `doctor` in CLI keeps app-source-pattern scans (those are app-side anti-patterns, not auth-state checks — right call to keep them in enlace). ✓
Three minor follow-ups (not blocking)
-
`enlace/init.py` says `version = "0.0.1"` but `pyproject.toml` says `0.1.0`. They'll desync after publish. Worth either bumping the `version` literal to `"0.1.0"` or, better, deriving it from package metadata (`importlib.metadata.version("enlace")`) so future bumps stay in one place.
-
`compose._can_access` interprets the access strings (`"public"`, `"local"`, `"protected:user"`, `"protected:shared"`) for the `/_apps` listing filter. That's defensible — `/_apps` is part of enlace's discovery surface, and a UX filter isn't an auth gate — but the module docstring at the top of `base.py` says enlace is "auth-agnostic" and "does not interpret" `access`. There's a small contradiction between those two statements. Consider tightening the wording, e.g., "enlace doesn't enforce access — that's enlace_auth's job — but it does filter `/_apps` listings by the same string vocabulary, so the access strings are part of enlace's contract too."
-
`Plugin` is a top-level type alias in `compose.py` but not re-exported from `enlace/init.py`. Plugin authors writing `from enlace import Plugin` will hit ImportError. Cheap to add to the re-export list.
None of these block. They can land in a follow-up PR after the user merges.
What I'm doing locally
Already opened companion PRs that depend on this landing:
- thorwhalen/papp#1 — per-app `access` policies in `app.toml` files, recovered from the discarded server-side history.
- thorwhalen/tw_platform#2 — `.env.example` + SYNC.md + twp-sync skill updated for `ENLACE_PLUGINS=enlace_auth:plugin`, `ENLACE_ADMIN_EMAILS`, and the `enlace-auth` CLI rename.
Both are safe to land after `enlace 0.1.0` and `enlace_auth 0.1.0` are published to PyPI but before the actual prod deploy. Coordinated deploy comes after.
The auth subsystem (auth router, sessions, CSRF, OAuth, per-app access rules, the per-user store, and all related diagnostics) moves to a new sibling package, i2mint/enlace_auth. Hosts opt in by passing the auth plugin to build_backend or by setting ENLACE_PLUGINS=enlace_auth:plugin. Why split: - enlace's mission is composition + discovery + routing. Auth has grown to ~1300 LOC across two subpackages plus diagnostics, env-var checks, and CLI commands. Hosts that don't want auth shouldn't carry it. - The admin dashboard work coming next would have piled another ~150 LOC of router + UI plumbing into the same tree. Better to give auth+admin its own package now. Public API breaks: - AuthConfig, OAuthProviderConfig, StoreBackendConfig, AccessLevel removed from enlace.base. Now in enlace_auth.config. - PlatformConfig.auth: dict[str, Any] (was AuthConfig). - PlatformConfig.stores: dict[str, dict] (was dict[str, StoreBackendConfig]). - AppConfig.access: free-form str (was AccessLevel Literal). Values parse identically; enlace just stops interpreting them. - build_backend(config) no longer wires auth. Use plugins=[enlace_auth.plugin] or set ENLACE_PLUGINS=enlace_auth:plugin. - enlace.compose.EnlaceConfigError no longer raised for bad signing keys — enlace_auth raises EnlaceAuthConfigError now. - CLI auth-* commands removed from `enlace`. Re-added in enlace_auth as `enlace-auth init|generate-signing-key|hash-password|list-sessions| revoke-session`. - enlace.doctor checks for signing-key / shared-passwords / oauth / csrf moved to enlace_auth.diagnostics. run_doctor gained extra_static_checks and extra_http_checks for plugins. What stays in enlace: - AppConfig fields access, allowed_users, shared_password_env (no-op fields enlace_auth reads). - enlace.diagnose's app-source-pattern checks (sub-app auth middleware, unsafe store keys, etc.) — those are app-side, not platform-side. Tests: enlace 78/78, enlace_auth 98/98 green. Version: 0.1.0 (breaking).
ce1801d to
5206416
Compare
* ci: adopt wads uv-based workflow + bump to 0.1.0 - Add .github/workflows/ci.yml from wads/data/github_ci_uv.yml. Reads [tool.wads.ci] from pyproject (uv installer, 3.10/3.12 matrix, no coverage, no Windows). Publishes to PyPI on main pushes via uv publish + the existing PYPI_PASSWORD token secret. - Replace .gitignore with the wads template (only adds the standard wads_configs.json prefix; rest is identical to what was here). - Bump version 0.0.1 -> 0.1.0 to match the public API contract documented in i2mint/enlace#13. The PR description says 0.1.0; the file said 0.0.1. Aligning before first publish so the wheel metadata matches what callers were told to pin against. Pushing to a feature branch first to verify CI passes; merge to main triggers publish. * fix(lint): clear ruff failures (autofix + 2 line-length splits) - ruff --fix: removed unused imports (Optional, AuthConfig) and reordered import blocks in __main__.py and diagnostics.py. - admin/routes.py: split a long line by lifting a set comprehension into a named local (existing_lower). - plugin.py: hoist getattr(app, 'shared_password_env', None) into a local so the if-test fits in 88 chars. 98/98 tests pass locally; ruff check clean.
Summary
enlace.auth,enlace.stores, auth diagnostics, the auth env-var doctor checks, and theauth-*CLI commands out ofenlaceinto a new sibling package,i2mint/enlace_auth(separate PR / repo).enlacekeeps a genericplugins=[]parameter onbuild_backend(config, *, plugins=()). Hosts opt in to auth by passingenlace_auth.plugin(or by settingENLACE_PLUGINS=enlace_auth:pluginfor the bundledcreate_appfactory).enlaceto 0.1.0 (breaking).Why split —
enlace's mission is composition + discovery + routing. Auth had grown to ~1300 LOC across two subpackages plus diagnostics, env-var checks, and CLI commands; the planned admin dashboard would have piled another ~150 LOC on top. Hosts that don't want auth shouldn't carry it.Public API breaks
from enlace import AuthConfig, OAuthProviderConfig, StoreBackendConfigfrom enlace_auth.config import …PlatformConfig.auth: AuthConfigPlatformConfig.auth: dict[str, Any]PlatformConfig.stores: dict[str, StoreBackendConfig]PlatformConfig.stores: dict[str, dict]AppConfig.access: AccessLevel(Literal[…])AppConfig.access: str(free-form; enlace ignores)build_backend(config)auto-wires auth from[auth]configbuild_backend(config, plugins=[enlace_auth.plugin])enlace.EnlaceConfigErrorfor bad signing keyenlace_auth.EnlaceAuthConfigErrorenlace auth-init/auth-generate-signing-key/auth-hash-password/auth-list-sessions/auth-revoke-sessionenlace-auth init/generate-signing-key/hash-password/list-sessions/revoke-sessionenlace.doctor._check_signing_keyetc.enlace_auth.diagnostics.check_signing_keyetc.; pass viarun_doctor(extra_static_checks=…, extra_http_checks=…)AppConfig.access,AppConfig.allowed_users, andAppConfig.shared_password_envstay onAppConfig—enlacedoesn't interpret them, but they round-trip throughplatform.tomlandapp.tomlsoenlace_authcan read them. No changes required to existing TOML files.What stays in enlace
enlace.diagnosekeeps the app-source-pattern scans (sub-app auth middleware, unsafe store keys, hardcoded user IDs). Those are app-side anti-patterns — they're useful regardless of which auth provider you use.EnlaceConfigErrorclass itself (still raised for non-auth misconfig).Migration for hosts
Or, if you serve via the bundled
enlace.compose:create_appfactory:export ENLACE_PLUGINS=enlace_auth:pluginTest plan
pytest enlace/tests tests— 78/78 pass onauth-extractionpytest testsinenlace_auth— 98/98 pass (covers all moved tests + newtest_admin.py)tw_platformintegration: bumpenlaceand addenlace_authto requirements, setENLACE_PLUGINS, redeploy throughpython deploy.py deploy(separate change-window; coordinated with the operator-side agent)