Skip to content

feat: constrain the OAuth2 client type column - #27931

Merged
BobbyHo merged 12 commits into
mainfrom
oauth2-client-type-constraint
Aug 10, 2026
Merged

feat: constrain the OAuth2 client type column#27931
BobbyHo merged 12 commits into
mainfrom
oauth2-client-type-constraint

Conversation

@BobbyHo

@BobbyHo BobbyHo commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Extracted from #27873 so the schema change can be reviewed for migration safety on its own. #27873 will rebase onto this.

client_type decides whether the token endpoint validates a client secret at all, and the column accepts any text: nullable, no CHECK, no enum. No Go path can write a bad value today, and IsPublic fails closed on anything unrecognized, so the read side is safe. What the schema still permits is the problem: a future migration writing 'public' onto a row that holds a secret turns off client authentication for that app with nothing to catch it, no constraint, no log, no audit entry, no test.

000565 adds CHECK (client_type IN ('confidential', 'public')) and NOT NULL. The UPDATE ahead of it should touch zero rows, since migration 000344 added the column with a default of 'confidential' and backfilled with COALESCE; it is there so SET NOT NULL cannot fail on an unexpected row. Both ALTERs take ACCESS EXCLUSIVE and scan a table holding one row per registered OAuth2 client, so the lock is brief.

The second migration, and why it aligns the way it does

Two columns describe the same fact and can currently contradict each other.

token_endpoint_auth_method is the client's own declaration: registered client metadata under RFC 7591 §2, where "none" is defined to mean the client is public and has no secret. client_type is Coder's derived copy, and it is what the token endpoint enforces on. RFC 7591 defines no client_type metadata field; the column exists only as a denormalization.

Registration used to persist the declaration verbatim while hardcoding client_type to 'confidential', so rows exist declaring "none" on a client stored confidential that was issued, and still requires, a real secret. A client that reads its own metadata and believes it is public will drop that secret and stop being able to exchange codes.

000566 aligns the declaration to what is enforced, not the reverse. Deriving enforcement from the declaration would reclassify every such client as public and stop requiring the secret it holds, which is a silent authentication downgrade. The down migration is deliberately empty: the previous values are not recorded, and restoring them would only reinstate metadata that tells a client to authenticate in a way the server rejects.

Application changes

SET NOT NULL changes the generated field from sql.NullString to string, so the three write sites are updated to match. That is the entire application diff and no behavior depends on it.

Refs https://linear.app/codercom/issue/ENG-3029/oauth2-support-public-client

client_type decides whether the token endpoint validates a client secret at
all, and the column accepted any text: nullable, no CHECK, no enum. No Go
path can write a bad value, and IsPublic fails closed on anything
unrecognized, so the read side is safe today. The point is what the schema
still permits: a future migration writing 'public' onto a row that holds a
secret turns off client authentication for that app with nothing to catch it,
no constraint, no log, no audit entry, no test.

Two columns also describe the same fact and could contradict each other.
token_endpoint_auth_method is the client's own declaration, registered
metadata under RFC 7591 §2 where "none" means the client is public and has no
secret; client_type is the derived value enforced on. Registration used to
persist the declaration verbatim while hardcoding client_type, so rows exist
declaring "none" on a client stored confidential that holds a real secret. A
client reading its own metadata concludes it is public, drops the secret, and
stops being able to exchange codes. The backfill aligns the declaration to
what is enforced; deriving enforcement from the declaration instead would
reclassify those clients as public and stop requiring the secret they were
issued.

SET NOT NULL changes the generated field from sql.NullString to string, so
the three write sites are updated to match. That is the whole application
change; no behavior depends on it.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
BobbyHo added a commit that referenced this pull request Aug 6, 2026
An RFC 7591 registration requesting `token_endpoint_auth_method: "none"`
now produces a public client: no secret is minted, `client_type` is
persisted as `public`, and the token endpoint accepts that client's
authorization_code exchange with PKCE alone. Discovery advertises `"none"`
as a supported auth method.

PKCE was already mandatory for every authorization_code flow, so public
clients inherit it unchanged. That makes the code ownership check
(`dbCode.AppID != app.ID`) the sole binding between the exchange and the app
named by `client_id` for a public client, where it was defense in depth for
confidential ones. It is retained and covered with a public client, along
with the refresh and revocation paths, which are the first to see a NULL
`app_secret_id`. Verifier length is now checked against RFC 7636 §4.1, since
for a public client PKCE is the only client authentication and a
one-character verifier hashes to a well-formed challenge.

Registration writes the app and its secret in one transaction. Two
independently committed inserts could leave a permanently committed app that
can never authenticate while still holding a registration access token.

An RFC 7592 update can no longer move a client between public and
confidential, and secret creation is rejected for a public app: the token
endpoint would never validate that secret, and deleting it revokes nothing
because a public client's tokens carry a NULL `app_secret_id` instead of
cascading from the secret.

Clients registered with `"none"` before it was honored are stored
confidential and still require their secret, so an update that resends their
own metadata is accepted rather than rejected, and the auth method reported
back is the one the server actually enforces.

Depends on #27931 for the client_type constraint and the schema-level
alignment of the two columns.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
@BobbyHo

BobbyHo commented Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

/coder-agents-review

@coder-agents-review

coder-agents-review Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Chat: Review posted | View chat
Requested: 2026-08-06 22:27 UTC by @BobbyHo

Review history
  • R1 (2026-08-06), 1 Note, 1 P2, 1 P3, COMMENT. Review
  • R2 (2026-08-06): 20 reviewers, 7 Nit, 3 Note, 3 P2, 4 P3, COMMENT. Review

deep-review v0.9.0 | Round 2 | 5064006..c258668

Last posted: Round 2, 17 findings (3 P2, 4 P3, 7 Nit, 3 Note), COMMENT. Review

Finding inventory

Finding inventory: PR 27931

Findings

# Sev Status Location Summary Round Reviewer Posted
CRF-1 P2 Author fixed (c258668); panel verified R2 000566_oauth2_auth_method_backfill.up.sql:20 Backfill repair logic has zero test coverage; no fixture or migration test exercises a row the UPDATEs touch R1 Netero Yes
CRF-2 P3 Author accepted R2; panel evaluated and accepted R2 (residual tracked as CRF-8) 000566_oauth2_auth_method_backfill.up.sql:24 Repaired contradiction is recreated by first dynamic registration with auth method "none" until #27873 lands R1 Netero Yes
CRF-3 Note Deferred (#27873) coderd/oauth2provider/apps.go:95 client_type values remain bare string literals in three packages with no shared constant R1 Netero Yes
CRF-4 Note Open coderd/database/migrations/migrate_test.go:2786 Local strPtr closure duplicates coderd/util/ptr.Ref R2 Netero Yes
CRF-5 P3 Open migrate_test.go:2793 No public+NULL seed and NULL-blind invariant query; losing the second UPDATE's IS NULL branch survives the test (mutation-verified) R2 Bisky P3, Meruem P3, Hisoka Note, Knuckle Note Yes
CRF-6 P2 Open 000565_oauth2_client_type_constraint.up.sql:1 Migration and test comments state present-tense enforcement that does not exist at this commit; IsPublic named but absent from the tree R2 Gon P2, Razor P3, Ryosuke P3, Leorio P3 Yes
CRF-7 P2 Open migrate_test.go:2937 Six parallel subtests query through the parent's 60s ctx; deadline runs during -parallel queue wait (32s of 60 measured at -parallel=1) R2 Komugi Yes
CRF-8 P3 Open 000566_oauth2_auth_method_backfill.up.sql:24 Permanent fix CRF-2 leans on (cross-column CHECK, SET NOT NULL, window-row backfill) exists only as a PR comment; no ticket R2 Mafu-san P3, Kite, Pariston, Meruem, Komugi Yes
CRF-9 P3 Open 000566_oauth2_auth_method_backfill.up.sql:23 Confidential-side repair predicate narrower than public-side; garbage non-'none' declarations never repaired, future CHECK will not catch '' either (row existence unverified) R2 Knov Yes
CRF-10 Nit Open (downgraded from Gon P2: skill taxonomy maps working-code rule violations to Nit; pattern, not ten accidents) 000566_oauth2_auth_method_backfill.up.sql:1 Comment duplication pattern: rationale repeated across migration headers, test docs, and require messages at 10 sites R2 Gon Yes
CRF-11 Nit Open migrate_test.go:2849 CHECK-violation assertion uses raw constraint-name string instead of database.IsCheckViolation with the generated constant this PR adds R2 Robin, Zoro Yes
CRF-12 Nit Open migrate_test.go:2744 Helper doc claims "every combination the two migrations care about" but 000565's NULL client_type row is seeded in its own test R2 Leorio, Zoro Yes
CRF-13 Nit Open migrate_test.go:2757 Stepper loop silently accepts exhaustion instead of t.Fatalf like every sibling loop in the file R2 Komugi Yes
CRF-14 Nit Open migrate_test.go:2859 NULL-insert probe asserts bare require.Error while siblings pin the constraint name R2 Chopper Yes
CRF-15 Nit Open 000566_oauth2_auth_method_backfill.down.sql:1 "Deliberately empty" precedes an executed SELECT 1; comment-only down migrations have precedent R2 Leorio Yes
CRF-16 Nit Open migrate_test.go:2766 "RFC 7591 §2" here vs "section 2" in both migration files of the same PR R2 Gon Yes
CRF-17 Note Open (no change requested) 000565_oauth2_client_type_constraint.up.sql:10 Non-NULL stranger value makes ADD CONSTRAINT fail and blocks the whole single-transaction upgrade; right direction, undocumented consequence R2 Meruem Yes

Contested and acknowledged

CRF-2 (P3, 000566_oauth2_auth_method_backfill.up.sql:24) - contradiction recreated by registration until #27873

  • Finding: After 000566 repairs existing rows, the first dynamic registration with token_endpoint_auth_method: "none" recreates the contradiction (none passes validation, registration persists the declaration verbatim, DetermineClientType() hardcodes "confidential"), and the 000565 CHECK guards only client_type's domain, not the cross-column relation. Proposed noting the window in the migration comment or rejecting "none" at registration until feat: support public (secretless, PKCE-only) OAuth2 clients at the handler layer #27873.
  • Author defense (R2, PRRC_kwDOGkVX1s7edJsT): Accepted the window deliberately, no code change. Verified the mechanism against a real database: post-migration, INSERT (client_type=confidential, token_endpoint_auth_method=none) is admitted and breaks the invariant. Exposure is bounded because feat: support public (secretless, PKCE-only) OAuth2 clients at the handler layer #27873 lands before the next release cut. Rejecting "none" at registration was declined (turns a working registration into a 400, breaks oauth2_security_test.go:258, reverted one PR later). Moving 000566 into feat: support public (secretless, PKCE-only) OAuth2 clients at the handler layer #27873 declined on the maintainer's call. The permanent fix, a cross-column CHECK plus SET NOT NULL on token_endpoint_auth_method, cannot land before feat: support public (secretless, PKCE-only) OAuth2 clients at the handler layer #27873 (every none registration would 500) and is sequenced after it. The migration comment's "holds for the whole table" is left as-is at the maintainer's request; author states the correction on the record: the invariant holds at commit time and is maintained from feat: support public (secretless, PKCE-only) OAuth2 clients at the handler layer #27873 onward. The test added for CRF-1 asserts the cross-column invariant the schema lacks.
  • Not yet panel-evaluated; the R2 panel judges the defense.
  • Panel closure (R2, unanimous among 13 evaluators): All evaluating reviewers accepted the defense. Common grounds: the failure direction is closed (token endpoint requires the secret unconditionally at this commit, verified at tokens.go:94-241 by Razor, Pariston, Mafuuu); the window strictly shrinks the contradictory-row population vs base (Kite, Ryosuke); rejecting "none" was verified to break oauth2_security_test.go:258 and be reverted one PR later (Kite read the test); and the forgotten-backfill failure mode is structurally loud, since Postgres validates existing rows at ADD CONSTRAINT time, so the future cross-column CHECK cannot ship over contradictory rows without its own backfill (Chopper, Knuckle, Ryosuke, Razor; only a deliberate NOT VALID escapes, flagged for the follow-up reviewer). Residual, the untracked permanent-fix work itself, spun out as CRF-8.

CRF-3 (Note, coderd/oauth2provider/apps.go:95) - client_type bare string literals

Round log

Round 1

Netero-only (P2 present, panel gated). 1 P2, 1 P3, 1 Note. Reviewed against 5064006..8b0eb45.

Round 2

Churn guard: PROCEED. CRF-1 author fixed (c258668, +227 test lines), CRF-2 acknowledged, CRF-3 deferred to #27873. 0 silent.

Round 2 panel

Netero (1 new Note, CRF-4) then full panel: 18 trigger-matched + 2 wildcards (Zoro, Killua). CRF-1 fix verified by 12 reviewers running the tests; CRF-2 closed by unanimous panel acceptance with residual spun to CRF-8. New: 2 P2, 3 P3, 7 Nit, 1 Note (CRF-5..17). Gon's 10 comment-bloat P2s consolidated and downgraded to one Nit (CRF-10). Reviewed against 5064006..c258668.

About deep-review

CRF = Coder Review Finding (P0-P4, Nit, Note)

Reviewer Focus
Bisky tests
Chopper ops/errors
Churn-guard change verification
Ging language modernization
Gon naming
Hisoka edge cases
Killua perf
Kite change integrity
Knov contracts
Knuckle SQL
Komugi flake/determinism
Kurapika security
Law decomposition
Leorio docs
Luffy product
Mafu-san process
Mafuuu contracts
Melody dispatch/pairing
Meruem structural
Nami frontend
Netero mechanical checks
Pariston premise testing
Pen-botter product gaps
Razor verification
Robin duplication
Ryosuke Go arch
Takumi concurrency
Zoro shape

🤖 Managed by Coder Agents.

@coder-agents-review coder-agents-review Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a first-pass review only: the findings below are mechanical checks from the automated first-pass reviewer. The full review panel has not yet reviewed this PR and will review after these findings are addressed.

The change itself is well shaped: the migration ordering in 000565 (UPDATE, ADD CONSTRAINT, SET NOT NULL) fails loudly on an unexpected value instead of rewriting it, the PR description's factual claims about migration 000344 check out, all three sql.NullString write sites were updated, and build, vet, migration round-trip, and audit tests all pass. As the first-pass reviewer put it: "the 000565 ordering (UPDATE, ADD CONSTRAINT, SET NOT NULL) fails the migration loudly on an unexpected value rather than rewriting it, which is correct."

Severity count: 1 P2, 1 P3, 1 Note.

🤖 This review was automatically generated with Coder Agents.

Comment thread coderd/oauth2provider/apps.go
…d backfill

Addresses CRF-1 on #27931. Neither migration had a dedicated test, and the
shared fixture cannot reach the case that matters: testdata/fixtures/
000182_oauth2_provider.up.sql inserts one app with no
token_endpoint_auth_method, so the backfill's IS NULL branch matches it while
the '= none' branch, the one repairing the actual bug, matches zero rows in
CI.

Follows the existing Stepper-to-prior-version, seed, migrate, assert pattern
of TestMigration000542ChatReasoningEffortBackfill,
TestMigration000562OAuth2PublicClientTokensBackfill and
TestMigration000563TemplateAgentsAllowedBackfill.

The backfill test seeds all six combinations that matter: a confidential
client declaring "none" (the known bug), one with no declaration at all, a
public client declaring a secret-based method, and the three already
consistent shapes. It asserts client_secret_post is not flattened to basic,
that client_type itself is never rewritten, and states the resulting
invariant directly by counting rows where the declaration contradicts the
enforced type.

The constraint test asserts a NULL client_type is backfilled to confidential
rather than blocking SET NOT NULL, that the column becomes NOT NULL, and that
the CHECK actually rejects "Public", "PUBLIC", "public ", "bogus", "" and
NULL while both canonical values remain insertable.

Both are mutation-checked. Removing the backfill's '= none' branch fails the
legacy case and the invariant count; removing the CHECK fails the rejection
assertions.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
@BobbyHo

BobbyHo commented Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

/coder-agents-review

@coder-agents-review coder-agents-review Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Round 2: full panel review (Netero first pass, then 20 reviewers). The round-1 findings resolved well: CRF-1's fix was verified by twelve reviewers independently running both migration tests against Postgres, with the mutation checks the author claimed reproduced by one of them; CRF-2's accepted window was evaluated by thirteen reviewers and the defense holds unanimously, since the failure direction is fail-closed, the window strictly shrinks the contradictory-row population versus base, and a forgotten backfill in the future cross-column CHECK migration fails loudly at ADD CONSTRAINT rather than shipping silently. As Knuckle put it: "The interest rate on this loan is low and the balloon payment is enforced by the database itself."

The change itself drew repeated praise: correct alignment direction (declaration follows enforcement, never the reverse), honest irreversible down migration, a constraint test that proves rejection instead of trusting existence, and per-case reason strings that make failures self-diagnosing.

New findings this round: 2 P2, 3 P3, 8 Nit, 2 Note.

One item needs a human decision rather than agent acceptance: the permanent fix that the CRF-2 closure leans on (cross-column CHECK, SET NOT NULL on token_endpoint_auth_method, and the backfill for window-created rows) is tracked nowhere except a PR comment (CRF-8). Either confirm ENG-3029 itemizes that third migration or file a ticket.

Process observation (Leorio): the subject of commit c258668 is 79 characters and truncates in log and GitHub views; "test(coderd/database/migrations): cover client_type constraint and backfill" fits within 72. The commit bodies, by contrast, are exemplary: alternatives considered, mutation checks documented.

🤖 This review was automatically generated with Coder Agents.

Comment thread coderd/database/migrations/migrate_test.go Outdated
Comment thread coderd/database/migrations/migrate_test.go
Comment thread coderd/database/migrations/000566_oauth2_auth_method_backfill.up.sql Outdated
Comment thread coderd/database/migrations/000566_oauth2_auth_method_backfill.up.sql Outdated
Comment thread coderd/database/migrations/migrate_test.go Outdated
Comment thread coderd/database/migrations/000566_oauth2_auth_method_backfill.down.sql Outdated
Comment thread coderd/database/migrations/migrate_test.go Outdated
Comment thread coderd/database/migrations/migrate_test.go Outdated
BobbyHo and others added 3 commits August 6, 2026 17:29
… this tree

Addresses part of CRF-6 on #27931. The comment named IsPublic, which is
added in #27873 and greps to nothing here, so a reader during the window
between the two PRs goes looking for a function that does not exist. Ten
panel reviewers flagged it independently.

States the property directly instead, which is true at this commit and stays
true afterwards.

The finding's other four instances describe enforcement that arrives with
#27873 and are deliberately left: unlike a dangling symbol they are accurate
statements about where the column is headed, and each becomes true when that
PR lands.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
… method

Addresses CRF-9 on #27931. The two UPDATEs were asymmetric: the public branch
excluded the one valid value and repaired everything else, while the
confidential branch enumerated the two bad shapes known today, so a
confidential row holding '' or an unrecognized method passed through
untouched.

That row would then be invisible permanently, not just unrepaired. The
cross-column constraint this is heading towards, (method = 'none') =
(client_type = 'public'), evaluates false = false for it and passes, so
nothing looks at it again while RFC 7592 GET keeps handing the client a
declaration it cannot use.

Both branches now name the values valid for their client type and repair
everything else, which makes the migration idempotent against history it
cannot inspect rather than against the one bug we know about. Reachability is
unverified and stated as such in the comment: ApplyDefaults maps "" to
client_secret_basic and Valid() rejects unknown methods, so such a row needs a
write path predating those guards.

The IS NULL arm is kept deliberately. NULL NOT IN (...) evaluates to NULL and
WHERE admits only true, so the widening alone would have stopped repairing
NULL rows, which the narrow predicate did handle.

Both regressions are mutation-checked: restoring the narrow predicate fails
the empty and unrecognized cases, and dropping the IS NULL arm fails the NULL
case.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
… assertions

Addresses CRF-7 and CRF-5 on #27931.

CRF-7: the parallel subtests queried through the parent's context. Its
deadline starts when it is created, but a parallel subtest does not run until
a -parallel slot frees, so most of the budget can be spent queued behind other
tests in the package before the first query runs. The reported failure would
be "context deadline exceeded" on a sub-20ms single-row read, naming no code
and worsening as the package grows. paralleltestctx does not catch it because
the context arrives through a helper's return value rather than a direct call
in the subtest. Each subtest now creates its own.

CRF-5, first half: no public + NULL row was seeded, so the second UPDATE's
IS NULL arm matched nothing and removing it left the test green. That shape is
now seeded and asserted.

CRF-5, second half: the closing invariant used <>, which is NULL-blind. With a
NULL declaration the comparison is NULL, WHERE drops the row, and an
unrepaired NULL counts as consistent. Now IS DISTINCT FROM. This matters past
this test: the predicate is the obvious candidate for the permanent
cross-column CHECK after #27873, and a CHECK is more forgiving still, since
NULL reads as not-violated.

Mutation-checked. Removing the second UPDATE's IS NULL arm now fails both the
publicNull case and the invariant count; with the old <> form only the former
fires, which is what made the blind spot invisible. Full package passes at
-parallel=1, the worst case for queue wait.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
@BobbyHo BobbyHo changed the title feat(coderd/database): constrain the OAuth2 client type column feat: constrain the OAuth2 client type column Aug 7, 2026
BobbyHo and others added 4 commits August 6, 2026 20:44
…nt type migrations

Addresses CRF-4 and CRF-10 through CRF-16 on #27931.

CRF-11: the CHECK-violation assertion matched the constraint name as a raw
string, while this migration generates a typed constant for it. Now uses
database.IsCheckViolation with CheckOauth2ProviderAppsClientTypeCheck, the
established idiom, which matches the pq error code and constraint name rather
than error text, and which make gen keeps correct through a rename.

CRF-13: the stepper loop treated exhaustion as success, so a renumbered or
squashed 564 would silently apply 000565 and 000566 during setup and fail
later on a confusing "constraint already exists". Now t.Fatalf, matching every
other stepper loop in the file.

CRF-14: the NULL-insert probe asserted only that some error occurred, while
its siblings pin the constraint. Now pins the not-null violation.

CRF-15: the down migration opened with "Deliberately empty." and then ran
SELECT 1. Comment-only down migrations run fine (000506 is the precedent), so
the statement is gone and the first line is true.

CRF-12: the helper doc claimed it seeds every combination the two migrations
care about, but 000565's NULL client_type row is seeded in that test.
Narrowed to what it actually covers.

CRF-10: the declaration-versus-enforcement rationale was written four times
and 000565's twice. Kept the migration headers as the owning location and
removed the copies that restated an adjacent require message, map literal, or
header. Also folded in CRF-17's deploy-blocking consequence, which was the one
thing the 000565 header did not say.

CRF-16: "section 2" throughout, the majority form in the diff. CRF-4: the
local strPtr closure replaced with the shared ptr.Ref.

Re-verified after the assertion changes: dropping the CHECK still fails the
constraint test, and the full package passes at -parallel=1.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Three of the ten cited sites survived 1a54ff7.

migrate_test.go:2874: "Both canonical values must still be insertable."
restated the loop and its require message, which is the canonical copy
because it prints on failure. Deleted.

TestMigration000566's doc: paragraph two repeated the 000566 header
verbatim, so it is gone. Paragraph one said "the shared fixture", which
read as setupMigration000565Apps, called on the next line and seeding
token_endpoint_auth_method on every row, making the sentence false. The
intended referent is the testdata/fixtures run, whose only
oauth2_provider_apps row (000182) sets no auth method. Named directly.

The invariant assertion's comment opened by restating its own require
message. Removed those two lines; the IS DISTINCT FROM rationale is the
part that is not derivable from the code and it stands alone.

TestMigration000565 and TestMigration000566 pass; go vet clean.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
… change

CRF-13 asked for the stepper loop in setupMigration000565Apps to treat
exhaustion as fatal. An earlier pass applied the same fix to
TestMigration000546ChatHistoryAPIKeyConstraints, which predates this branch
and has nothing to do with the OAuth2 client type work. The convention gap
there is real, but it belongs in its own change rather than widening this
diff into an unrelated test.

migrate_test.go now differs from main only by the ptr import and the new
000565/000566 coverage.

TestMigration000546, TestMigration000565 and TestMigration000566 pass.
@linear-code

linear-code Bot commented Aug 9, 2026

Copy link
Copy Markdown

PLAT-473

BobbyHo and others added 2 commits August 9, 2026 10:12
…lient_type

The subtest passed a zero-value InsertOAuth2ProviderAppParams. That was fine
while client_type was sql.NullString, where the zero value inserts NULL into
a nullable column. This branch makes the column NOT NULL with a CHECK for
'confidential' and 'public', and the generated insert always names the
column, so the DEFAULT never applies and the zero value now inserts '' and
fails the constraint:

  pq: new row for relation "oauth2_provider_apps" violates check constraint
      "oauth2_provider_apps_client_type_check"

Seeds 'confidential', the value every production call site already passes.
The subtest asserts RBAC, not column contents, so this does not change what
it verifies.

Fixes test-go-pg, test-go-pg-17 and test-go-race-pg on b254294, which all
failed on this one subtest.
@BobbyHo
BobbyHo marked this pull request as ready for review August 10, 2026 14:54
@BobbyHo
BobbyHo requested a review from Emyrk August 10, 2026 15:06
@Emyrk

Emyrk commented Aug 10, 2026

Copy link
Copy Markdown
Member

The eventual cross-column CHECK, (token_endpoint_auth_method = 'none') = (client_type = 'public'), is optional hardening, not a gap in this PR: today's write path would 500 on the first "none" registration, so it can only land after #27873. Track it as a ticket so it doesn't live only in this thread.

Posted by Coder Agents on behalf of @Emyrk.

Comment thread coderd/database/migrations/000566_oauth2_auth_method_backfill.up.sql Outdated

@Emyrk Emyrk left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coder Agents found no blocking comments.

Review details

Walked the full diff at 0f40876:

  • 000565: correct shape — NULL backfill fails closed to confidential, lock is brief on a tiny table, and blocking the upgrade on a stranger value is the right failure mode for an auth-enforcement column. On Coder-written data it cannot fail.
  • 000566: declaration→enforcement is the correct alignment direction; the reverse would waive secrets that legacy clients hold and must present. Predicates repair every invalid shape per branch, IS NULL arms are present, and client_secret_post is preserved.
  • Down migrations: 565 is a clean structural inverse; 566's documented no-op is right — the old values are unrecoverable and restoring them would only reinstate metadata the server rejects.
  • Re-contradiction window: acceptable — metadata-only (the token endpoint requires the secret unconditionally at this commit and never reads client_type), bounded by #27873. See my comment about tracking the follow-up cross-column CHECK as a ticket.
  • App diff: mechanical sql.NullStringstring; no read site depended on NULL-ness.
  • Tests: verified all deep-review R2 findings (CRF-4, 5, 7, 9, 11–16) are fixed at head — per-subtest contexts, publicNull seed, IS DISTINCT FROM invariant, IsCheckViolation with the generated constant, widened predicates.

One accuracy note for the record: 000565's opening comment and the PR body describe client_type deciding token-endpoint secret validation and reference IsPublic — both describe the post-#27873 world, not this commit. Not blocking per discussion.

Review conducted with Coder Agents on behalf of @Emyrk.

@BobbyHo

BobbyHo commented Aug 10, 2026

Copy link
Copy Markdown
Contributor Author

Tracked as PLAT-473.

@BobbyHo
BobbyHo merged commit 16c5877 into main Aug 10, 2026
28 checks passed
@BobbyHo
BobbyHo deleted the oauth2-client-type-constraint branch August 10, 2026 17:49
@github-actions github-actions Bot locked and limited conversation to collaborators Aug 10, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants