Skip to content

Let a credential's owner reconnect it themselves - #2129

Merged
ppXD merged 1 commit into
mainfrom
fix/let-an-owner-reconnect-their-own-credential
Oct 19, 2026
Merged

ppXD merged 1 commit into
mainfrom
fix/let-an-owner-reconnect-their-own-credential

Conversation

@ppXD

@ppXD ppXD commented Oct 19, 2026

Copy link
Copy Markdown
Owner

Summary

  • A credential's owner can reconnect it themselves. POST /api/credentials/oauth/init naming a replaceCredentialId passes the credential's own person on membership alone (a Member or Viewer whose sign-in died brings it back in place, in the starting browser and across sites through the SPA's completion page); anyone else still needs credentials.manage, and a connect that names no credential keeps its bar. The same-account rule, retired and departed refusals and the named-owner check are unchanged.
  • One definition of who may repair a credential, so a button is shown exactly when the request would be accepted. A reconnect signs in as the person, so it also keeps the reach over them: an Admin cannot reconnect another Admin's or an Owner's sign-in, and nobody can for someone who has left the team (a pasted token has no rank reach, but also cannot be replaced for someone who left). The pipeline, the OAuth callback's re-check (starter removed, demoted, or the person promoted past them before the code is exchanged), the run-detail remedy, and the new callerMayRepair on GET /api/credentials, which the repository rows read, all ask the same rule over the person's role as TeamStanding.RolesActedOnAsync reads it (TeamMemberService uses it too). The remedy's reasons and "who to ask" now name only people who could do what the caller cannot.
  • Compatible: callerMayRepair is an additive nullable field (an older SPA ignores it, a newer SPA falls back to the role when a server sends none); no migration, setting or change to any endpoint's request or status.

Test plan

  • Unit: PermitsReconnect table, CredentialPerson, CallerStanding and the remedy's wording and ordering per role, person and departed owner
  • Integration: 48-case agreement matrix (asker role x own/other person's role/left x sign-in/pasted token) where the remedy button, callerMayRepair and the real command (accepted only if it returns) must agree
  • Integration: callback refuses a starter removed from the team, demoted, or outranked by a promotion, through both completion doors, never exchanging the code; RoleHeldAsync agrees with the resolver in a deleted team; reach agreement for every kind of person
  • HTTP E2E: reach matrix with the list flag, departed owner 404, plus the existing owner-reconnect and cross-site SPA completion cases via GitLabDoorkeeperStub
  • Frontend: vitest for the repository row (server flag beats the role, fallback without it) and the remedy card; pnpm lint, tsc --noEmit, full vitest, build
  • Regression: full unit suite (14763), credential/OAuth/teams/tenancy/authorization/binding integration lanes (701), credentials/auth/teams/repositories/webhooks HTTP E2E lanes (272), dotnet build CodeSpace.sln

Starting an OAuth connect took credentials.manage of everyone, so a Member
or Viewer whose own sign-in had died could not bring it back: only an
Admin could start it, and an Admin can only finish it by signing in as
that person. Replacing a pasted token over the same credential already
passed its owner on membership alone, so putting a sign-in right was
harder than putting a token right.

A connect that names a credential to re-authorize in place
(ReplaceCredentialId) is now held to that credential's bar: its own
person on membership, anyone else credentials.manage. A connect that
names none mints a credential and keeps credentials.manage. Everything
else a re-authorization checks is unchanged: the same provider account,
a retired credential, an owner without standing, the rank reach over the
person acted on, and the named owner being the credential's.

A reconnect signs in AS the person, so it also keeps the reach over them
(CredentialOwnerOrPermission.PermitsReconnect): an Admin cannot reconnect
another Admin's or an Owner's sign-in, and nobody can for someone who has
left the team. That was only the service's refusal, unknown to a button,
so an Admin was offered a Reconnect on an Owner's sign-in that always
answered 403, and a Reconnect or Replace token on the credential of
someone who had left that no one could use. CallerStanding now models it,
and the remedy names only the people who could do what the caller cannot.

The bar is one definition, used by the pipeline (a marker and behavior),
by CallerStanding for the remedy's buttons, by the credential list's new
callerMayRepair, which the repository rows read, and by the OAuth
callback, which asks it again of whoever started the connect before the
code is exchanged: a starter demoted, removed from the team, or the
person promoted past them in the ten minutes between start and callback
never has a token written. The person's role is read one way, by
TeamStanding.RolesActedOnAsync, which TeamMemberService now uses as well.

callerMayRepair is additive: an older SPA ignores it and a newer one
falls back to the role when a server sends none. No migration and no new
setting. A pod from before this refuses the owner as it always did; one
from after completes a connect an older pod started.
@ppXD
ppXD merged commit 8f6cedb into main Oct 19, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant