From 84350e4e7c46078ae80c8435026abea4f061ccf8 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Thu, 25 Jun 2026 08:54:37 -0500 Subject: [PATCH] feat: report SCIM configuration on Deployment (#26628) Adds two nullable booleans to `telemetry.Deployment`: - `SCIMEnabled`: `true` when `CODER_SCIM_AUTH_HEADER` is set. - `SCIMUseLegacy`: `true` when `CODER_SCIM_USE_LEGACY` is set. Both mirror `Deployment.IDPOrgSync`: nullable for backward compatibility, and report configuration state rather than license entitlement (#16323). Lives on `Deployment` rather than `Snapshot` so the existing `bqDeployment` table on `coder/coder-telemetry-server` gets two columns instead of a new table. `SCIMAPIKey` is annotated as a secret and is scrubbed by `WithoutSecrets` before the config reaches telemetry, so `DeploymentConfig.SCIMAPIKey` is always empty in production. The booleans are pre-computed from the pre-scrub `DeploymentValues` in `cli/server.go` and passed in via `telemetry.Options.SCIMEnabled` / `SCIMUseLegacy`. Pairs with [coder/coder-telemetry-server#43](https://github.com/coder/coder-telemetry-server/pull/43), which adds the matching `bqDeployment` columns and the manual BigQuery `ALTER TABLE` step. --- Generated by Coder Agents on behalf of @Emyrk. --- cli/server.go | 4 +++ coderd/telemetry/telemetry.go | 24 ++++++++++++++++++ coderd/telemetry/telemetry_test.go | 40 ++++++++++++++++++++++++++++++ 3 files changed, 68 insertions(+) diff --git a/cli/server.go b/cli/server.go index ed892159ce..3697cb5fe4 100644 --- a/cli/server.go +++ b/cli/server.go @@ -988,6 +988,10 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd. URL: vals.Telemetry.URL.Value(), Tunnel: tunnel != nil, DeploymentConfig: deploymentConfigWithoutSecrets, + // SCIMAPIKey is a secret and is scrubbed by WithoutSecrets above, + // so we derive SCIMEnabled from vals (pre-scrub) instead. + SCIMEnabled: vals.SCIMAPIKey != "", + SCIMUseLegacy: vals.UseLegacySCIM.Value(), ParseLicenseJWT: func(lic *telemetry.License) error { // This will be nil when running in AGPL-only mode. if options.ParseLicenseClaims == nil { diff --git a/coderd/telemetry/telemetry.go b/coderd/telemetry/telemetry.go index 7feeda1531..2f6945ba3f 100644 --- a/coderd/telemetry/telemetry.go +++ b/coderd/telemetry/telemetry.go @@ -61,6 +61,16 @@ type Options struct { BuiltinPostgres bool Tunnel bool + // SCIMEnabled is true when CODER_SCIM_AUTH_HEADER is set on the server. + // Must be derived from the pre-WithoutSecrets DeploymentValues because the + // SCIM API key is annotated as a secret and is cleared before the config + // is handed to the telemetry reporter. + SCIMEnabled bool + // SCIMUseLegacy is true when the legacy SCIM handler is selected via + // CODER_SCIM_USE_LEGACY. Not secret-scrubbed, but accepted alongside + // SCIMEnabled so both come from the same source. + SCIMUseLegacy bool + SnapshotFrequency time.Duration ParseLicenseJWT func(lic *License) error } @@ -332,6 +342,9 @@ func (r *remoteReporter) deployment() error { r.options.Logger.Debug(r.ctx, "check IDP org sync", slog.Error(err)) } + scimEnabled := r.options.SCIMEnabled + scimUseLegacy := r.options.SCIMUseLegacy + data, err := json.Marshal(&Deployment{ ID: r.options.DeploymentID, Architecture: sysInfo.Architecture, @@ -352,6 +365,8 @@ func (r *remoteReporter) deployment() error { StartedAt: r.startedAt, ShutdownAt: r.shutdownAt, IDPOrgSync: &idpOrgSync, + SCIMEnabled: &scimEnabled, + SCIMUseLegacy: &scimUseLegacy, }) if err != nil { return xerrors.Errorf("marshal deployment: %w", err) @@ -1636,6 +1651,15 @@ type Deployment struct { // While IDPOrgSync will always be set, it's nullable to make // the struct backwards compatible with older coder versions. IDPOrgSync *bool `json:"idp_org_sync"` + // SCIMEnabled is true when CODER_SCIM_AUTH_HEADER is set on the deployment. + // Reports configuration state, not license entitlement. Nullable so older + // Coder versions that do not emit the field decode as nil. + SCIMEnabled *bool `json:"scim_enabled"` + // SCIMUseLegacy is true when the legacy SCIM handler is selected via + // CODER_SCIM_USE_LEGACY instead of the SCIM 2.0 handler in + // enterprise/coderd/scim. Nullable for the same backward compatibility + // reason as SCIMEnabled. + SCIMUseLegacy *bool `json:"scim_use_legacy"` } type APIKey struct { diff --git a/coderd/telemetry/telemetry_test.go b/coderd/telemetry/telemetry_test.go index b3de13bff7..85773f326a 100644 --- a/coderd/telemetry/telemetry_test.go +++ b/coderd/telemetry/telemetry_test.go @@ -585,6 +585,46 @@ func TestTelemetry(t *testing.T) { deployment, _ = collectSnapshot(ctx, t, db, nil) require.True(t, *deployment.IDPOrgSync) }) + t.Run("SCIM", func(t *testing.T) { + t.Parallel() + ctx := testutil.Context(t, testutil.WaitMedium) + db, _ := dbtestutil.NewDB(t) + + // 1. Default Options: both flags false (and reported as such). + deployment, _ := collectSnapshot(ctx, t, db, nil) + require.NotNil(t, deployment.SCIMEnabled) + require.False(t, *deployment.SCIMEnabled) + require.NotNil(t, deployment.SCIMUseLegacy) + require.False(t, *deployment.SCIMUseLegacy) + + // 2. Both Options flags true: both reported true. + deployment, _ = collectSnapshot(ctx, t, db, func(opts telemetry.Options) telemetry.Options { + opts.SCIMEnabled = true + opts.SCIMUseLegacy = true + return opts + }) + require.True(t, *deployment.SCIMEnabled) + require.True(t, *deployment.SCIMUseLegacy) + + // 3. Enabled only: enabled true, legacy false. + deployment, _ = collectSnapshot(ctx, t, db, func(opts telemetry.Options) telemetry.Options { + opts.SCIMEnabled = true + return opts + }) + require.True(t, *deployment.SCIMEnabled) + require.False(t, *deployment.SCIMUseLegacy) + + // 4. The reporter never reads DeploymentConfig.SCIMAPIKey directly: + // even if a non-empty key sneaks through (it would not in production + // because of WithoutSecrets), SCIMEnabled reflects only Options. + deployment, _ = collectSnapshot(ctx, t, db, func(opts telemetry.Options) telemetry.Options { + opts.DeploymentConfig = &codersdk.DeploymentValues{ + SCIMAPIKey: "a-secret-bearer-token", + } + return opts + }) + require.False(t, *deployment.SCIMEnabled) + }) } // nolint:paralleltest