Repository navigation
feat: let an Owner turn connected apps off for the farm - #1147
Conversation
Allow connected apps is a Farm settings field, on by default. Off, consent refuses before any password, and every OAuth token of the farm is refused on its next request in the statement that already reads Account.IsActive. Registration is unaffected. Back on restores every connection nobody disconnected. Closes #1146
|
Screenshots for #1146, captured at 1:1 at head The switch now lives at the top of Setup › Connected apps. Farm settings is unchanged from The sim stack serves plain http, and OpenIddict refuses OAuth requests over http. So both stacks ran behind a local Caddy https edge ( Setup › Connected apps, before, and after with the switch on (1280). The same at 390. The switch turned off (1280 and 390). The control saved on its own, with no settings form involved. Consent, before and after with the switch off. Before shows the normal request. After shows the refusal, which asks for no password. Cancel returns the user to the app. |
…e column Round 1 of the #1147 review (Codex GPT-6 Astra): - The Owner's Connected apps page reads AllowConnectedApps with its rows, so a switch flipped elsewhere shows in both directions. - The consent refusal focuses its heading and is announced in a polite live region, on the first load and after Allow. - The Farm settings section moves to ConnectedAppsSetting.tsx. CI: the #283 base-reference guard now compares AllowConnectedApps. The migration's default turns the base row on, and Account.Create starts on.
|
Round 1 from Codex GPT-6 Astra is addressed in F1 (P2), Owner page notice from the session's farm. Fixed. F2 (P2), consent refusal neither focused nor announced. Fixed. The off view's heading takes the existing heading ref and F3 (P3), SettingsPage over 1,000 lines. The section moved to CI, The precache now measures 1,947.13 of 1,950 KiB. The screenshot comment is updated in place to this head. |
|
I checked the round-1 fixes (0031a14..891d5ae) directly, without another review round:
CI is green at 891d5ae, and the review loop for this PR is complete. Toggling the switch stays audited as the existing farm-settings update, with the before and after flag and the Owner as actor; there is no separate audit action. |
The maintainer moved the switch out of Farm settings. The Owner now flips it at the top of Setup > Connected apps, a labelled MUI Switch, saved through its own Owner-only PUT /account/connected-apps under the settings Version token. Account.SetConnectedApps bumps Version only on a real change, and each change writes Account.UpdateSettings with the switch before and after. The Farm settings PUT no longer carries the field. The maintainer approved raising the precache ceiling to 1,960 KiB for the toggle; #1149 is the planned way to lower it again.
The explicit restore duplicated the reload that follows every save, which already brings the control back to what the server holds.
|
The switch has moved, as the maintainer asked. It now sits at the top of Setup › Connected apps, not in Farm settings. The commits are Farm settings. Connected apps page. A labelled MUI Endpoint.
Tests. The Owner-only, version race, audit, consent refusal, token refusal, restore and registration tests now drive the new endpoint. A new test races two Owners holding the same version, which kills the Precache. The page measures 1,952.69 KiB, of which MUI Docs. Help, the in-app glossary and The screenshot comment is updated in place to |
|
I checked the move to Setup › Connected apps (891d5ae..4b828f2) directly:
CI failed four times on Docker Hub's anonymous pull rate limit, not on the code. Attempt 4 passed. The review loop for this PR is complete. |










An Owner can now turn connected apps off for the whole farm. The switch is Allow connected apps, at the top of Setup › Connected apps, the page that lists every app on the farm. It is not in Farm settings. It is on by default, so every existing farm keeps working.
Closes #1146
What off does
access_denied. The refusal takes focus and is announced in a polite live region.Auth.ConnectedAppsOffon its next request.CredentialEpochVerifierreads the switch in the statement that already readsAccount.IsActive, so the check adds no database round trip.client_id, which a session JWT never does (OAuth slice 6: audit provenance — record which application acted, alongside the human #800).Turning it back on restores every connection nobody disconnected. Off refuses, and only Disconnect revokes. I found nothing that argues for revoking instead. OpenIddict's purge prunes only dead tokens and authorizations, so a farm that stays off for weeks loses nothing.
The control
The switch is a labelled MUI
Switchwith one line under it: "Off: new connections are refused, and connected apps stop on their next request." The page reads the switch and its version fromGET /accountwith its rows, not from the session's farm, so a switch another Owner flipped shows correctly. A save announces "Connected apps are on." or "Connected apps are off." in a polite live region. A failed save restores the control and shows an error. After every save the page reloads, so the next flip uses the current version.The endpoint
PUT /api/v1/account/connected-appstakes{ allow, version }.Idempotency-Key, like every write.Account.SetConnectedAppsbumpsVersiononly on a real change, the wayRenamedoes.Versiontoken with the Farm settings save. A racing settings save, or a second Owner holding an older version, gets 409 rather than a silent overwrite.Account.UpdateSettingswithAllowConnectedAppsbefore and after, and the Owner as actor. There is no new audit action; the maintainer accepted that.allowis a 400, never a silentfalse.AccountEndpoints.SetConnectedAppshas its ownRealModuleLedger.Adaptersrow, and the coupling matrix is regenerated.The Farm settings
PUTdoes not carry the field. An earlier version of this PR put the switch there, and the maintainer moved it to the Connected apps page.The migration adds the column with a database default of
true. The EF model sets no default, because a model default on aboolmakes EF skip an insertedfalse.The rationale is in
docs/decisions/1146-connected-apps-switch.md, with a one-paragraph rule insrc/AGENTS.md.Precache
The precache measures 1,952.69 KiB.
MUI Switchcosts 5.60 KiB of that, and nothing else in the app uses it. The maintainer approved raising the ceiling from 1,950 to 1,960 KiB for this toggle, recorded inweb/scripts/verify-sw.mjsand in thedocs/designs/822-mui-revamp.mdamendment. #1149, precaching only the active language, is the planned way to win the space back and lower the ceiling. Any further raise needs a new maintainer decision.Accepted costs
OAuthLastUsedStampruns during token validation, before the refusal. So an app that retries while the switch is off still updates Last used. A suspended farm and a disabled user already behave this way.Where to look
SetConnectedAppsHandler.cs,Account.SetConnectedAppsandAccountEndpoints.SetConnectedAppshold the save.CredentialEpochVerifier.csandCredentialEpochMiddleware.cshold the per-request check.OAuthEndpoints.Authorizeholds the consent refusal. Its adapter row now reaches Farm.ConnectedAppsPage.tsxholds the control.OAuthFailClosedTests.ConnectedAppsSwitch.cshas one test per rule.BaseReferenceDataMigrationTestscomparesAllowConnectedAppson the Base provisioning: static ref-data into schema + secure first-run admin (generated secret + MustChangePassword) — retire Seed:* config #283 base account row.tools/oauth/mutation-check.shgains 11 mutants and aMUTANT_FILTERvariable to run a subset.Verification
MUTANT_FILTER='^switch-' tools/oauth/mutation-check.shkilled all 11 mutants at539fef75. Baseline and restore were both green at 125 of 125.switch-new-farm-offConnectedApps_AreOnByDefault_ForNewAndExistingFarmsswitch-existing-farms-offConnectedApps_AreOnByDefault_ForNewAndExistingFarmsswitch-not-owner-onlyPUT /account/connected-appsdropsOwnerOnlyConnectedAppsSwitch_IsOwnerOnlyswitch-token-uncheckedConnectedAppsOff_RefusesAnExistingToken_OnTheNextRequestswitch-token-unrecognisedConnectedAppsOff_RefusesAnExistingToken_OnTheNextRequestswitch-refuses-sessionsConnectedAppsOff_LeavesSessionsAloneswitch-off-sticksConnectedAppsBackOn_RestoresTheConnectionswitch-consent-uncheckedConnectedAppsOff_RefusesConsent_BeforeThePassword_AndIssuesNoCodeswitch-registration-checkedConnectedAppsOff_LeavesRegistrationAloneswitch-unauditedConnectedAppsSwitch_IsAudited_WithTheOwnerAsActorswitch-race-unversionedSetConnectedAppsskipsVersion++ConnectedAppsSwitch_TwoOwnersAtOneVersion_ExactlyOneWinsThe race between the switch and a Farm settings save at one version is
ConnectedAppsSwitch_RacingASettingsSave_ExactlyOneWins. It passes, but it is not the race mutant's kill, because its outcome depends on which request lands first. Two Owners at one version is deterministic.Measured locally at
539fef75. The later4b828f21changes only the page's failure path, and its specs pass 13 of 13 there:false,BaseReferenceDataMigrationTests' virgin-database test fails withExpected: True,Actual: False.verify-sw.test.mjspassed 9 of 9.The SPA mutants were applied by hand, run, and restored:
Screenshots are in the comment below, captured at the head named there.