Commit Graph
12 Commits
Author SHA1 Message Date
Marian 77c9bdd694 fix(oidc): reject an unrecognized boolean instead of guessing
_env_bool returned the default for anything outside {true,1,yes}, so
BAMBUDDY_OIDC_REQUIRE_EMAIL_VERIFIED=on silently read as OFF and
BAMBUDDY_OIDC_ENABLED=on silently disabled the provider -- the exact
opposite of what .env.example claimed. Unrecognized values now raise
EnvOIDCConfigError, caught in _apply_env_oidc_provider the same way a
bad DEFAULT_GROUP or a ValidationError already is: logged and left
running, never released on a typo.

Also promotes _env_bool to env_bool now that it has a call site in
auth.py, and corrects the boolean-parsing sentence in .env.example.
2026-08-01 09:17:33 +00:00
Marian c547505c64 fix(oidc): treat a blank optional env var as unset, not a refusal
BAMBUDDY_OIDC_SCOPES, _EMAIL_CLAIM and _ICON_URL fell back to their
default only when the key was absent, so `BAMBUDDY_OIDC_ICON_URL=` in
a compose file (as .env.example ships it, commented) reached the
schema validator as an empty string and got the whole provider
refused. default_group already treated blank as unset; these three
now follow the same rule.
2026-08-01 09:17:33 +00:00
MarianandClaude Opus 4.8 6eea61dc78 fix(oidc): survive a failing rollback in the never-raise handler too
The recovery rollback after a failed commit was itself unguarded, so a
rollback that raises on a wedged connection would still take the boot
down -- the exact failure the never-raise contract exists to prevent.
Suppressed; the caller's `async with` discards the session regardless.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016q8EAf9Rj7ZHL92sPnXYxy
2026-07-31 13:18:21 +00:00
Marian c4b5d42f48 fix(oidc): log distinctly when env config adopts a UI-created provider
A name collision with a provider that was NOT already env-managed
overwrites its issuer, client id and secret in place and locks it
behind the env-managed 409 -- but it logged the same routine "applied"
line as an ordinary re-apply, giving no signal a UI provider was just
taken over. Adoption is now a WARNING with its own wording; a routine
re-apply of an already env-managed provider keeps the INFO line.
2026-07-31 13:11:15 +00:00
Marian 6dff1e9644 fix(oidc): make apply_env_oidc_provider never raise on DB errors
The db.execute/db.commit calls in the upsert and release paths sat
outside the try/except that only wrapped OIDCProviderCreate, so a
commit failure at startup (connection blip, WAL lock) propagated out
of the lifespan and took the instance down -- the exact outcome this
module exists to avoid. The body now runs inside a private
_apply_env_oidc_provider(), with the public entry point catching,
logging and rolling back on any exception.
2026-07-31 13:10:14 +00:00
Marian ed29319cf9 docs(oidc): drop the upgrade-path claim from the release-path rationale
The two-flagged-row state was never released, so no installation can carry it
in -- the reason to release every flagged row is not repair, it is that the
sweep's invariant is not enforced anywhere and scalar_one_or_none() turns a
broken one into a failed boot. Comment, test docstring and test name say that
instead.
2026-07-30 14:44:49 +00:00
Marian 3c679459e1 feat(oidc): set the default group from the environment, by name
Without it every account auto-created through the env provider fell back to
Viewers (routes/mfa.py), and because the provider is locked the UI could not
correct it either -- a real limitation for a declarative deployment running
BAMBUDDY_OIDC_AUTO_CREATE_USERS=true.

BAMBUDDY_OIDC_DEFAULT_GROUP names a group rather than an id: ids are handed out
per installation, so the same compose file would point at a different group on
the next deployment. The name is matched exactly, resolved against the database
before anything is written, and default_group_id joins _APPLIED_FIELDS so
dropping the variable clears the group again -- the environment is the whole
truth for this row.

A name that matches no group is refused rather than defaulted: silently landing
users in Viewers is the failure this variable exists to remove, and the API
already answers 422 for a default_group_id that does not exist. The refusal is
logged and survivable, and it says which of the two cases happened, because
they differ sharply -- an existing provider keeps running on its last good
config, while on a first boot nothing is created and no SSO button appears.

Raised by maziggy in review of #2625 as a scope decision; documented in
.env.example and in the companion wiki PR.
2026-07-30 14:43:23 +00:00
Marian d64f5d9651 fix(oidc): release the row the env config managed before a rename
apply_env_oidc_provider matches the provider by BAMBUDDY_OIDC_NAME but never
cleared is_env_managed from the row it managed previously. Renaming the
variable therefore left two flagged rows, and both consequences are reachable
by ordinary config edits: the old row stayed enabled with a stale issuer and
secret on the login page while _refuse_if_env_managed answered 409 to every
attempt to edit, disable or delete it -- the dead end reachable only through
the database that the release path exists to prevent -- and unsetting the
variables later hit scalar_one_or_none() on two rows, so MultipleResultsFound
propagated out of the lifespan and the app stopped booting.

The upsert now sweeps the flag off every other row, the same shape the
autologin sweep one block down already uses: disable and release rather than
delete, for the same cascade reason as everywhere else in this branch. The
release path releases every flagged row it finds instead of exactly one -- the
sweep should keep that at one, but a release path that dies with
MultipleResultsFound the moment that invariant breaks is a second way to lose
the boot, and the query costs the same either way.

Releasing now clears is_autologin as well. Without it a released row keeps a
latent autologin claim: update_oidc_provider only re-runs the exclusivity sweep
when a request sets is_autologin=True, so merely re-enabling the row in the UI
would silently make it the autologin target again.

Reported by maziggy in review of #2625, with the rename reproduction.
2026-07-30 14:43:23 +00:00
MarianandClaude Opus 4.8 1116b43fbd fix(oidc): never log the client_secret when env config is rejected
apply_env_oidc_provider logged the raw Pydantic exception on rejection.
client_secret has max_length=512, so a longer value raises string_too_long
and str(exc) embeds input_value=..., leaking BAMBUDDY_OIDC_CLIENT_SECRET into
the logs (maziggy review, PR #2625).

Split the catch: ValidationError logs errors(include_input=False), which
strips submitted values; any other exception logs only its class name, never
str(exc). Rejection stays survivable — a bad config is still skipped and the
app still boots.

Adds two regression tests: an over-long secret is rejected without the value
reaching the log, and a non-ValidationError is survived without leaking its
message.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-28 21:05:29 +00:00
Marian c163b3524a fix(oidc): identify the env provider by name, and release it when unconfigured
Two problems, both from using is_env_managed as the provider's identity.

An operator who names the env provider after one that already exists hit the
unique constraint on `name` during the insert. That happens inside the
lifespan, so the app did not boot -- from a function whose docstring promises
it never raises. The lookup now matches on the name, which is unique, so an
existing provider is adopted and updated instead of duplicated.

And removing the config left the row disabled but still flagged, so the API
went on refusing every edit and delete while nothing managed it any more: a
dead end reachable only through the database. The flag is now cleared as well,
handing the provider back to the UI. Re-adding the config finds the same row
by name, so the account links it carries survive the round trip.

Falls out of the same change: the issuer URL and client id can be rotated
under an unchanged name without orphaning those links.

Found by Marian asking what happens when you want to change the provider --
the answer was "you cannot, ever again".

Refs #2593
2026-07-28 21:05:29 +00:00
Marian 58602a3f1b feat(oidc): upsert the env-managed provider
The row is updated in place, never delete-recreated: user_oidc_links
references it with ON DELETE CASCADE, so recreating the provider would
silently unlink every account bound to it. For the same reason, removing the
variables disables the provider rather than deleting it -- the links would not
come back when the config does.

Config goes through OIDCProviderCreate, the schema the API already uses, so
the environment cannot reach a state the UI would have refused. That covers
the SEC-1 auto-link check: auto-link plus unverified email is an account
takeover, and it is rejected here exactly as it is in the UI.

Nothing raises. This runs during startup, so a typo in one variable must not
stop the app from booting -- a rejected config is logged and skipped, leaving
the previous provider untouched.

Refs #2593
2026-07-28 21:05:29 +00:00
Marian d6ecd92480 feat(oidc): read BAMBUDDY_OIDC_* env config
A declarative deployment has no way to click through the settings UI, so one
provider can be configured entirely from the environment. This reads and
defaults only -- validity is decided later by the same OIDCProviderCreate
schema the API uses, so env config cannot bypass a check the UI enforces.

All four required vars or nothing, and an empty one counts as unset: a
provider missing its secret would otherwise be written to the database and
fail at authorize time, far from the typo in the compose file that caused it.
Booleans follow the project's existing spelling convention (true/1/yes), so an
unrecognised value leaves the documented default rather than guessing.

Refs #2593
2026-07-28 21:05:29 +00:00