feat!: add admin-controlled dynamic client registration toggle - #27316
Conversation
Adds GetOAuth2DCREnabled/UpsertOAuth2DCREnabled queries backed by a new site_configs key, with dbauthz RBAC checks against rbac.ResourceDeploymentConfig. This will back an admin-toggleable, persistent replacement for the ExperimentOAuth2 flag currently gating dynamic client registration.
Adds a DCR-specific, persistent enabled/disabled setting so admin control over dynamic client registration survives the eventual removal of the ExperimentOAuth2 flag. POST /oauth2/register now checks the flag and the discovery endpoint conditionally omits registration_endpoint, both defaulting to enabled to preserve current behavior on upgrade. Adds an audited GET/PUT /api/v2/oauth2-provider/settings admin endpoint backed by a new oauth2_provider_settings audit resource type. Disabling only stops new self-registrations; clients registered before the toggle continue to authorize and exchange tokens normally.
Aligns with the canonical design proposal, which specifies DCR disabled by default, correcting this repos local proposal doc which argued for default-enabled. Safe to change now since OAuth2 as a whole is still gated behind ExperimentOAuth2 and has not reached GA. Adds oauth2providertest.EnableDCR and updates every pre-existing OAuth2 test that registers a client to call it explicitly, since those tests previously relied on the implicit default-enabled behavior.
Docs previewCheck off each page once it's been reviewed. If a page changes in a later push, its checkbox clears automatically so it gets a fresh look. Pages not yet wired into the docs navigation aren't listed here.
|
paralleltestctx flags reusing a parent-scope testutil.Context after t.Parallel(), since the timeout clock starts before the parallel subtest actually runs. Give each of the three affected subtests in TestOAuth2DynamicClientRegistrationDisabled its own context instead.
TestCreateDynamicClientRegistration_DCREnabled calls CreateDynamicClientRegistration directly via httptest.NewRecorder, bypassing the full coderdtest HTTP server, exercising all three states: explicitly enabled, explicitly disabled, and never configured (defaults to disabled). Complements the existing HTTP-level integration coverage in TestOAuth2DynamicClientRegistrationDisabled.
"Never configured means DCR defaults to disabled" was awkward, elliptical phrasing. Reword to a plain if/then statement in all three call sites.
Document the deliberate no-cache decision at both per-request read sites: registration and discovery aren't hot paths, so a DB read per request is an acceptable cost for a setting that can be flipped live. A flood of requests from a misconfigured or misbehaving client is a rate-limiting/firewalling problem, not a reason to add a cache here.
TestGetAuthorizationServerMetadata_DCREnabled calls GetAuthorizationServerMetadata directly via httptest.NewRecorder, bypassing the full coderdtest HTTP server, exercising all three states: explicitly enabled, explicitly disabled, and never configured (defaults to omitting registration_endpoint).
GetPermissionDenied and PutPermissionDenied were identical except for which SDK method they called against a forbidden client. Collapse them into a single table-driven PermissionDenied subtest.
Adds coder oauth2-provider dcr enable/disable, mirroring the existing coder notifications pause/resume and coder prebuilds pause/resume CLI pattern for toggling a boolean deployment setting. Wraps PutOAuth2ProviderSettings. Nested under a dcr subgroup (rather than flat like its two precedents) since OAuth2ProviderSettings is expected to gain a second field (Initial Access Token requirement) later.
main gained 000546_drop_chat_history_api_key_fks and 000547_mcp_server_oauth2_revocation_url since this branch was created, colliding with this branch's own 000546_audit_oauth2_provider_settings. Renumbered to 000548 via fix_migration_numbers.sh.
putOAuth2ProviderSettings only ever set aReq.New, leaving aReq.Old at its zero value. Diffing against the zero value happened to look correct when enabling DCR (false -> true) but produced an empty diff when disabling it (true -> false), silently hiding that the setting had changed. Fetches the real previous value via GetOAuth2DCREnabled before the upsert, same never-configured and RBAC-error handling as the GET handler. Adds TestOAuth2ProviderSettingsAuditDiff (enterprise/coderd), since the mock auditor used elsewhere always stubs diffs to empty and can't catch this, and a migration test reproducing the production upgrade path for the audit_logs resource_type enum addition.
main independently added 000548_drop_chat_gateway_key_columns since this branch's last sync, colliding with this branch's own 000548_audit_oauth2_provider_settings. Renumbered to 000549 via fix_migration_numbers.sh and updated the matching test name/comments.
Manual verification: OAuth2 Dynamic Client Registration admin toggleRan the flows below against a local dev Coderd ( Summary
0. PrerequisitesSetup commands./scripts/develop.sh # Terminal 1, leave running
./scripts/coder-dev.sh login # Terminal 2
export SESSION_TOKEN=$(cat ./.coderv2/session)
export BASE_URL=http://localhost:3000
export AUTH_HEADER="Coder-Session-Token: $SESSION_TOKEN"
export ORG_ID=$(curl -s "$BASE_URL/api/v2/users/me" -H "$AUTH_HEADER" | jq -r '.organization_ids[0]')
curl -s -X POST "$BASE_URL/api/v2/users" -H "$AUTH_HEADER" \
-H "Content-Type: application/json" \
-d '{"email": "dcr-member@coder.com", "username": "dcr-member", "password": "SomeSecurePassword123!", "login_type": "password", "organization_ids": ["'"$ORG_ID"'"]}'
export MEMBER_SESSION_TOKEN=$(curl -s -X POST "$BASE_URL/api/v2/users/login" \
-H "Content-Type: application/json" \
-d '{"email": "dcr-member@coder.com", "password": "SomeSecurePassword123!"}' | jq -r '.session_token')
export MEMBER_AUTH_HEADER="Coder-Session-Token: $MEMBER_SESSION_TOKEN"1. Default state: DCR is disabled out of the boxVerifies sequenceDiagram
participant A as Owner
participant C as Anonymous OAuth2 Client
participant S as coderd
A->>S: GET /api/v2/oauth2-provider/settings
S-->>A: 200 OK - dynamic_client_registration_enabled: false
Note over S: Never configured (sql.ErrNoRows) -> treated as disabled
C->>S: GET /.well-known/oauth-authorization-server
S-->>C: 200 OK - registration_endpoint absent from response
C->>S: POST /oauth2/register
S-->>C: 403 invalid_request,<br/>"Dynamic client registration is disabled on this deployment"
Commands and outputcurl -s "$BASE_URL/api/v2/oauth2-provider/settings" -H "$AUTH_HEADER" | jq .{
"dynamic_client_registration_enabled": false
}curl -s "$BASE_URL/.well-known/oauth-authorization-server" | jq 'has("registration_endpoint")'curl -s -w "\nHTTP %{http_code}\n" -X POST "$BASE_URL/oauth2/register" \
-H "Content-Type: application/json" \
-d '{"client_name": "manual-test-dcr-disabled", "redirect_uris": ["http://localhost:9876/callback"]}'{"error":"invalid_request","error_description":"Dynamic client registration is disabled on this deployment"}✅ Pass — settings default to 2. A non-owner member can't view or change the settingVerifies the RBAC check in sequenceDiagram
participant M as Member (non-owner)
participant S as coderd
M->>S: GET /api/v2/oauth2-provider/settings
S-->>M: 403 Forbidden
M->>S: PUT /api/v2/oauth2-provider/settings
S-->>M: 403 Forbidden
Commands and outputcurl -s -o /dev/null -w "%{http_code}\n" "$BASE_URL/api/v2/oauth2-provider/settings" -H "$MEMBER_AUTH_HEADER"curl -s -o /dev/null -w "%{http_code}\n" -X PUT "$BASE_URL/api/v2/oauth2-provider/settings" \
-H "$MEMBER_AUTH_HEADER" -H "Content-Type: application/json" \
-d '{"dynamic_client_registration_enabled": true}'✅ Pass — non-owner member correctly denied both 3. Owner enables DCR live (no restart required)Verifies sequenceDiagram
participant A as Owner
participant S as coderd
participant DB as site_configs
A->>S: PUT /api/v2/oauth2-provider/settings<br/>{dynamic_client_registration_enabled: true}
S->>DB: UPSERT oauth2_dcr_enabled = true
S-->>A: 200 OK
Note over S,DB: Audited: a new audit_logs row is written
A->>S: GET /api/v2/oauth2-provider/settings
S-->>A: 200 OK - dynamic_client_registration_enabled: true
Commands and outputcurl -s -X PUT "$BASE_URL/api/v2/oauth2-provider/settings" \
-H "$AUTH_HEADER" -H "Content-Type: application/json" \
-d '{"dynamic_client_registration_enabled": true}' | jq .{
"dynamic_client_registration_enabled": true
}curl -s "$BASE_URL/api/v2/oauth2-provider/settings" -H "$AUTH_HEADER" | jq .{
"dynamic_client_registration_enabled": true
}✅ Pass — write succeeded and persisted; a fresh 3a. Same thing via the CLIVerifies sequenceDiagram
participant A as Owner (CLI)
participant S as coderd
A->>S: coder oauth2-provider dcr disable
S-->>A: "Dynamic client registration is now disabled."
A->>S: coder oauth2-provider dcr enable
S-->>A: "Dynamic client registration is now enabled."
Commands and output./scripts/coder-dev.sh oauth2-provider dcr disable
curl -s "$BASE_URL/api/v2/oauth2-provider/settings" -H "$AUTH_HEADER" | jq .{
"dynamic_client_registration_enabled": false
}./scripts/coder-dev.sh oauth2-provider dcr enable
curl -s "$BASE_URL/api/v2/oauth2-provider/settings" -H "$AUTH_HEADER" | jq .{
"dynamic_client_registration_enabled": true
}✅ Pass — both CLI subcommands round-trip correctly, each verified via a fresh API 3b. Non-owner member gets the same
|
| Manual step | Automated test |
|---|---|
| 1 | TestOAuth2ProviderSettings/DefaultDisabled; TestGetAuthorizationServerMetadata_DCREnabled/NeverConfiguredDefaultsToOmitted, TestCreateDynamicClientRegistration_DCREnabled/NeverConfiguredDefaultsToDisabled |
| 2 | TestOAuth2ProviderSettings/PermissionDenied/{Get,Put} |
| 3 | TestOAuth2ProviderSettings/RoundTrip |
| 3a, 3b | TestOAuth2ProviderDCR (Enable/Disable), TestOAuth2ProviderDCR_RegularUser |
| 4 | TestGetAuthorizationServerMetadata_DCREnabled/EnabledAdvertisesRegistrationEndpoint, TestCreateDynamicClientRegistration_DCREnabled/EnabledAllowsRegistration |
| 5 | TestOAuth2DynamicClientRegistrationDisabled (all four subtests) |
| 7b | TestOAuth2ProviderSettingsAuditDiff — caught the empty-diff-on-disable bug found manually in this step |
| 8 | TestMigration000549AuditOAuth2ProviderSettingsEnumInSingleTxn |
make test RUN='TestOAuth2ProviderSettings|TestOAuth2DynamicClientRegistrationDisabled|TestCreateDynamicClientRegistration_DCREnabled|TestGetAuthorizationServerMetadata_DCREnabled|TestOAuth2ProviderDCR|TestOAuth2ProviderSettingsAuditDiff|TestMigration000549AuditOAuth2ProviderSettingsEnumInSingleTxn'
Documentation CheckThis PR adds an admin-controlled toggle for OAuth2 Dynamic Client Registration (DCR): DCR is now disabled by default, and owners enable/disable it live via Updates Needed
Automated review via Coder Agents |
There was a problem hiding this comment.
Approving. Detailed comments are inline; one PR-level note:
Flag the breaking change. Since deployments using DCR under ExperimentOAuth2 will have registration stop working on upgrade (intentional per the design), this PR should carry the release/breaking label (I added the label).
Coder Agents on behalf of @Emyrk.
|
For future PRs of this shape, consider stacking instead of one 54-file PR:
Your "suggested review order" section is already this list — stacked, it becomes merge structure instead of reading instructions. Coder Agents on behalf of @Emyrk. |
# Conflicts: # docs/admin/security/audit-logs.md
|
Filed follow-up issues for exposing the new setting beyond the API/CLI, both assigned to @BobbyHo:
Coder Agents on behalf of @Emyrk. |
|
Reviewing from the downstream side — I'm building the
1. The PUT is a whole-object replace, and IAT arrives laterI gather Today type OAuth2ProviderSettings struct {
DynamicClientRegistrationEnabled bool `json:"dynamic_client_registration_enabled"`
}and var req codersdk.OAuth2ProviderSettings
if !httpapi.Read(ctx, rw, r, &req) { return }
...
tx.UpsertOAuth2DCREnabled(ctx, req.DynamicClientRegistrationEnabled)Once IAT exists as a second plain
No race, no concurrency — one user, one request. And Terraform users pin their provider version independently of their Coder version, so client/server skew is the routine state of affairs rather than an edge case. The fix is pointer fields, so absent means "leave alone" rather than "set false", with the handler upserting only what was actually sent: DynamicClientRegistrationEnabled *bool `json:"dynamic_client_registration_enabled,omitempty"`To be clear, this does not have to happen in this PR. Switching One thing worth ruling out: an 2. Follow-up on the audit
|
DynamicClientRegistrationEnabled is now *bool with omitempty so a PUT that omits the field leaves the current value unchanged, instead of decoding to false and silently clearing it. This matters once a second field lands in this struct: without this, an older client built against today's single-field version would always encode the newer field's zero value on every unrelated update, silently disabling it. GET always returns a concrete, non-nil value. Also regenerates docs/admin/security/audit-logs.md against a truly fresh _gen/bin/auditdocgen build; the version committed by the earlier merge-conflict fix was generated against a stale binary and didn't reproduce identically in CI's fresh `make gen`. Addresses: #27316 (comment)
TestMigration000551AuditOAuth2ProviderSettingsEnumInSingleTxn still referenced migration 551 after an earlier merge-conflict fix renumbered the audit_oauth2_provider_settings migration to 552. Renamed the test and updated both migration ranges to reference 552.
`codersdk.OAuth2ProviderSettings` and the `OAuth2ProviderSettings` / `PutOAuth2ProviderSettings` client methods are introduced by coder/coder#27316, which is required by the `coderd_oauth2_provider_settings` resource. TEMPORARY PIN: #27316 has not merged, so no released coder/coder version exposes these symbols. This pins the PR's head commit (569a0eb3411212ff080b836f514d1dfe4386ccc7, branch coder-eng-3056-dcr-flag) so the resource compiles and its tests run. Re-pin to a released version before merging. Transitively bumps the `go` directive to 1.26.5 and ~12 indirect dependencies. Refs #395
coder/coder#27316 adds a deployment-level toggle for OAuth2 Dynamic Client Registration (RFC 7591), reachable via `coder oauth2-provider dcr enable|disable` or a raw `PUT /api/v2/oauth2-provider/settings`. Neither is declarative, so a deployment managed by this provider had no way to express "DCR is enabled here" as Terraform state, short of a `local-exec` provisioner that participates in neither plan/diff, drift detection, nor destroy. Add a `coderd_oauth2_provider_settings` resource and a data source of the same name, modelled on `coderd_organization_sync_settings`: a singleton wrapping a GET-for-read / PUT-for-write API with no delete endpoint. Resource: - `dynamic_client_registration_enabled` is Required. Opting out means not declaring the resource; a plain Optional attribute would refresh to a concrete bool against a null plan and produce a perpetual diff. - Create and Update share one idempotent PUT; the API has no separate create. - Delete resets to the documented default (false), since no DELETE verb exists. A failed reset keeps the resource in state so destroy can retry. - ImportState allows adopting an already-configured deployment without overwriting it. The import ID is an unused placeholder: the resource is a deployment-wide singleton and Read() takes no parameters. - ModifyPlan warns at plan time when a first apply would disable DCR on a deployment where it is currently enabled. Deliberately asymmetric: a live `false` is indistinguishable from never-configured, so warning in that direction would fire on every greenfield apply. - A 404 is reported as an unsupported Coder version rather than treated as a deleted resource. This singleton always exists on a supported deployment, so 404 can only mean the endpoint is missing; removing state would hide a version problem behind a phantom diff. Data source: read-only, GET only, never PUT. The setting is a deployment singleton, so only one configuration can own the resource; any other that needs the value reads it here. It also works with tokens holding read but not write access on the deployment config, a broader set of roles than the resource requires. `DynamicClientRegistrationEnabled` is a `*bool`, so that a PUT can omit it to leave the value unchanged. Both write paths therefore send an explicit non-nil pointer: this resource owns the value outright, and omitting it would make Create/Update no-ops and silently turn Delete's reset-to-default into a no-op. Reads go through `dcrEnabledOrDefault`, which falls back to the deployment default if the field is ever nil -- a GET is documented to always return non-nil, so that branch is defensive against a contract violation. Tests run against a recording httptest fake rather than `integration.StartCoder`, which pulls a published coder image that does not yet contain #27316. The fake also allows asserting requests that must *not* happen -- import never PUTs, the data source never PUTs, an undeclared resource issues no calls at all -- and injecting 403/404/5xx responses that a real deployment will not produce on demand. It records whether the DCR field was present on each PUT, not just its value, so a regression that stopped sending it would fail rather than pass silently. Closes #395
…nt settings Surfaces the admin-controlled DCR setting from GET/PUT /api/v2/oauth2-provider/settings (added in #27316) on the OAuth2 Applications deployment settings page. Enabling the switch requires confirming a warning dialog, since it lets any client self-register against the deployment per RFC 7591; disabling is immediate.
main independently added 000552_ai_budget_notifications since the last sync, colliding with this branch's 000552_audit_oauth2_provider_settings. Renumbered to 000553 and updated the corresponding single-txn enum test (name and both migration ranges) to match.
…nt settings Surfaces the admin-controlled DCR setting from GET/PUT /api/v2/oauth2-provider/settings (added in #27316) on the OAuth2 Applications deployment settings page. Enabling the switch requires confirming a warning dialog, since it lets any client self-register against the deployment per RFC 7591; disabling is immediate.
Resolve two conflicts: - coderd/database/migrations/migrate_test.go: both branches appended a migration test at the same location. Keep both, and renumber this branch's migration from 000553 to 000556 since main now ends at 000555 (000553_ai_budget_admin_notifications collided). None of main's new migrations alter a type, so the final schema and the resource_type enum value ordering in dump.sql are unchanged. - docs/reference/api/enterprise.md: generated output where both branches added an endpoint section at the same position. Regenerated from the merged Go swagger annotations rather than hand-merged.
…n with main main landed 000556_user_secrets_enabled (#27537), which collides with this branch's 000556_audit_oauth2_provider_settings. golang-migrate's iofs driver rejects the whole migration set on a duplicate ordinal, so every table was missing and sqlc vet failed with a flood of 'relation does not exist' errors. Renumber to 000557 and update the test's migration range. The migration adds an enum value and 000556_user_secrets_enabled alters no types, so the resulting schema is unchanged: dump.sql is byte-identical after regeneration.
…nt settings Surfaces the admin-controlled DCR setting from GET/PUT /api/v2/oauth2-provider/settings (added in #27316) on the OAuth2 Applications deployment settings page. Enabling the switch requires confirming a warning dialog, since it lets any client self-register against the deployment per RFC 7591; disabling is immediate.
…n with main The rebase brought in 000557_connection_type_tunnel, which collides with this branch's 000557_audit_oauth2_provider_settings. golang-migrate's iofs driver rejects the entire migration set on a duplicate ordinal, so dump.sql generation, sqlc vet, and the e2e server all failed to start. That single collision accounts for the gen, lint, offlinedocs, sqlc-vet, and test-e2e failures. main's 000557 adds a value to connection_type while this migration adds one to resource_type, so ordering between them does not matter and dump.sql is byte-identical after regeneration.

POST /oauth2/register(RFC 7591 Dynamic Client Registration) has exactly one gate today:ExperimentOAuth2, a static, process-lifetime flag that wraps the entire/oauth2/*route tree as an all-or-nothing switch. That flag is scheduled for removal at GA, which would leave DCR with zero admin control at all once it is gone.Add a persistent, DCR-specific
oauth2_dcr_enableddeployment setting, independent of the experiment system, so admin control over DCR survives GA.POST /oauth2/registerchecks the flag and rejects new registrations with an RFC 7591-shaped403when disabled; discovery metadata (GET /.well-known/oauth-authorization-server) conditionally omitsregistration_endpoint. A new auditedGET/PUT /api/v2/oauth2-provider/settingsendpoint lets an owner toggle it live, no restart required. The setting defaults to disabled, matching the canonical design proposal; disabling only stops new self-registrations, clients that already registered continue to authorize and exchange tokens normally.Address issue described in ENG-3056.
Where this sits in the request path
sequenceDiagram autonumber participant A as Admin participant S as coderd participant DB as site_configs<br/>(oauth2_dcr_enabled) participant C as OAuth2/MCP Client Note over A,S: Admin toggles DCR (new) A->>S: PUT /api/v2/oauth2-provider/settings<br/>{dynamic_client_registration_enabled: false} S->>S: authorizeContext(ActionUpdate, ResourceDeploymentConfig) S->>DB: UPSERT oauth2_dcr_enabled = false S-->>A: 200 OK (audited) Note over C,S: Client discovery + registration afterward C->>S: GET /.well-known/oauth-authorization-server S->>DB: GetOAuth2DCREnabled (system ctx, every request, no cache) DB-->>S: false S-->>C: 200 metadata, registration_endpoint omitted C->>S: POST /oauth2/register S->>DB: GetOAuth2DCREnabled (system ctx, every request, no cache) DB-->>S: false S-->>C: 403 invalid_request,<br/>"Dynamic client registration is disabled" Note over C,S: A client that registered before the change is unaffected C->>S: GET /oauth2/authorize?client_id=... Note over S: no DCR-enabled check on this path S-->>C: 200 (proceeds normally) C->>S: PUT/DELETE /oauth2/clients/{client_id} (RFC 7592 self-management) Note over S: no DCR-enabled check on this path either S-->>C: 200 (proceeds normally)Files changed: manual vs. generated
Reviewers should focus on the manual files. The generated ones are
make genoutput that follows mechanically from the manual changes and don't need direct review.Manual files (26) — click to expand, grouped the same way as "Suggested review order" below
1. Database
coderd/database/queries/siteconfig.sqlGetOAuth2DCREnabled/UpsertOAuth2DCREnabledquery pair on the existing genericsite_configstable. No schema change.coderd/database/dbauthz/dbauthz.gorbac.ResourceDeploymentConfig) on the two new query methods; extends thesubjectSystemOAuth2system-actor role with read-onlyResourceDeploymentConfigaccess, needed so the public discovery/registration endpoints can read the flag viadbauthz.AsSystemOAuth2.coderd/database/dbauthz/dbauthz_test.goGetOAuth2DCREnabled/UpsertOAuth2DCREnabledin the method-coverage test suite.2. Request gating (the actual feature)
coderd/oauth2provider/registration.goCreateDynamicClientRegistrationreads the flag first and returns an RFC 7591-shaped403when disabled (defaults disabled if never configured).coderd/oauth2provider/registration_test.goTestCreateDynamicClientRegistration_DCREnabled: calls the handler directly (no HTTP server), covering enabled / explicitly disabled / never-configured.coderd/oauth2provider/metadata.goGetAuthorizationServerMetadataconditionally omitsregistration_endpointfrom discovery metadata when DCR is disabled.coderd/oauth2provider/metadata_test.goTestGetAuthorizationServerMetadata_DCREnabled: same three states, for the discovery handler.3. Admin settings endpoint
codersdk/oauth2.goOAuth2ProviderSettingsSDK type plusClient.OAuth2ProviderSettings/PutOAuth2ProviderSettingsmethods.coderd/oauth2.gooauth2ProviderSettings/putOAuth2ProviderSettingsadmin handlers (audited viaaudit.InitRequest); updates theGetAuthorizationServerMetadatacall site to passapi.Database.coderd/coderd.goGET/PUT /api/v2/oauth2-provider/settings.coderd/oauth2_provider_settings_test.goGET/PUTround-trip, default-disabled-before-any-PUT, and403for a non-owner on bothGETandPUT.4. Audit wiring
coderd/database/types.godatabase.OAuth2ProviderSettingsaudit-only struct (mirrorsNotificationsSettings).coderd/audit/diff.goAuditabletype union.coderd/audit/request.goResourceTarget,ResourceID,ResourceType,ResourceRequiresOrgID).codersdk/audit.goResourceTypeOAuth2ProviderSettingsconstant and itsFriendlyStringcase.enterprise/audit/table.goActionTrack/ActionIgnore) for the new struct.coderd/database/migrations/000546_audit_oauth2_provider_settings.up.sqloauth2_provider_settingsto theresource_typePostgres enum, required for the audit wiring above (resource_typeis a real enum, not a Go-only value).coderd/database/migrations/000546_audit_oauth2_provider_settings.down.sqlALTER TYPE ... ADD VALUEcan't be reverted).5. Test-suite ripple from the disabled-by-default flip
coderd/oauth2provider/oauth2providertest/helpers.goEnableDCR, since DCR now defaults to disabled and many pre-existing tests need it turned on to register a client.coderd/oauth2_test.goTestOAuth2DynamicClientRegistrationDisabled(registers a client, disables DCR, verifies new registration is rejected while the existing client's self-management, authorize, and token exchange all keep working); callsEnableDCRin every pre-existing test that registers a client.coderd/oauth2_error_compliance_test.goEnableDCRin every test that registers a client, so RFC-error-format assertions aren't masked by the new disabled-by-default gate.coderd/oauth2_metadata_validation_test.goEnableDCRadded to every registration-dependent test.coderd/oauth2_security_test.gocoderd/oauth2provider/validation_test.gooauth2_metadata_validation_test.goin a different package).coderd/oauth2provider/provider_test.gocoderd/mcp/mcp_e2e_test.goGenerated files (12) — from
make gen, no need to review directlycoderd/apidoc/docs.go,coderd/apidoc/swagger.json,coderd/database/dbmetrics/querymetrics.go,coderd/database/dbmock/dbmock.go,coderd/database/dump.sql,coderd/database/models.go,coderd/database/querier.go,coderd/database/queries.sql.go,docs/admin/security/audit-logs.md,docs/reference/api/enterprise.md,docs/reference/api/schemas.md,site/src/api/typesGenerated.ts.Suggested review order
1. Database
Establishes the persisted setting and its RBAC rule; everything else builds on
GetOAuth2DCREnabled/UpsertOAuth2DCREnabled.coderd/database/queries/siteconfig.sql— the two new queries. Same boolean-encoding pattern as the existingoauth2_github_default_eligiblekey right above them in the same file.coderd/database/dbauthz/dbauthz.go— the RBAC wrapper for those two queries, plus thesubjectSystemOAuth2role extension (search this file forResourceDeploymentConfig, it appears in both spots).coderd/database/dbauthz/dbauthz_test.go— asserts the RBAC checks from (2) actually fire.2. Request gating (the actual feature)
Where
POST /oauth2/registerand discovery metadata change behavior.coderd/oauth2provider/registration.go— the primary gate. Read this first; it's the feature.coderd/oauth2provider/registration_test.go— its new unit test, exercising the gate's three states directly against the handler.coderd/oauth2provider/metadata.go— the same gating pattern applied to the discoveryGETendpoint.coderd/oauth2provider/metadata_test.go— its new unit test.3. Admin settings endpoint
How an owner flips the setting live.
codersdk/oauth2.go— theOAuth2ProviderSettingsSDK type andClientmethods first; this is the public contract everything below implements against.coderd/oauth2.go— theGET/PUThandlers themselves.coderd/coderd.go— route registration, to see where those handlers get wired in.coderd/oauth2_provider_settings_test.go— round-trip and permission tests.4. Audit wiring
Plumbing required so step 3's
PUTis auditable; mechanical except for (3).coderd/database/types.go— the audit-only struct; everything else in this layer exists to plumb it through.coderd/audit/diff.go— adds it to theAuditabletype union (the compiler enforces this one).coderd/audit/request.go— the four dispatch switches; the one part of this layer worth reading closely.codersdk/audit.go— the API-facing resource type constant.enterprise/audit/table.go— the field-action map.coderd/database/migrations/000546_audit_oauth2_provider_settings.{up,down}.sql— read last; a consequence of needing a newresource_typeenum value for (1)-(5), not a design decision of its own.5. Test-suite ripple from the disabled-by-default flip
coderd/oauth2provider/oauth2providertest/helpers.go— the newEnableDCRhelper. Read first to understand the fix pattern before seeing it applied repeatedly.coderd/oauth2_test.go— next, since it also contains the newTestOAuth2DynamicClientRegistrationDisabled, not justEnableDCRcall sites.coderd/oauth2_error_compliance_test.go,coderd/oauth2_metadata_validation_test.go,coderd/oauth2_security_test.go,coderd/oauth2provider/validation_test.go,coderd/oauth2provider/provider_test.go,coderd/mcp/mcp_e2e_test.go.Explicitly out of scope
Per the design proposal: rate limiting on
POST /oauth2/register(tracked separately), retroactively affecting already-registered clients when DCR is disabled (this only gates new self-registration), and an Initial Access Token requirement (a separate, follow-up ticket).