From 49f6a0dc517bbfa92c32c87474c532e473830f06 Mon Sep 17 00:00:00 2001 From: Bobby Ho Date: Sat, 12 Sep 2026 10:29:57 -0700 Subject: [PATCH] feat: log the OAuth2 provider state and registered apps at startup Every coder server start logs one info line stating whether the OAuth2 provider is enabled, naming CODER_OAUTH2_PROVIDER_ENABLE. When the provider is disabled and applications are still registered, one warning gives the count and the flag to set. A failed query is logged and does not abort startup. The SpammyLogs test allows two more rows for the new line. --- cli/server.go | 2 + cli/server_test.go | 4 +- coderd/oauth2_provider_state.go | 40 +++++++++++ coderd/oauth2_provider_state_internal_test.go | 72 +++++++++++++++++++ 4 files changed, 117 insertions(+), 1 deletion(-) create mode 100644 coderd/oauth2_provider_state.go create mode 100644 coderd/oauth2_provider_state_internal_test.go diff --git a/cli/server.go b/cli/server.go index b64b337964b..bb34265a2c0 100644 --- a/cli/server.go +++ b/cli/server.go @@ -1026,6 +1026,8 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd. } } + coderd.LogOAuth2ProviderState(ctx, logger, options.Database, vals.OAuth2.Provider) + options.RuntimeConfig = runtimeconfig.NewManager() // This should be output before the logs start streaming. diff --git a/cli/server_test.go b/cli/server_test.go index 956ab3ef56a..5f3e93baa34 100644 --- a/cli/server_test.go +++ b/cli/server_test.go @@ -381,7 +381,9 @@ func TestServer(t *testing.T) { out := pty.ReadAll() numLines := countLines(string(out)) t.Logf("numLines: %d", numLines) - require.Less(t, numLines, 20, "expected less than 20 lines of output (terminal width 80), got %d", numLines) + // The OAuth2 provider state line is logged on every start and wraps + // to two rows at this width. + require.Less(t, numLines, 22, "expected less than 22 lines of output (terminal width 80), got %d", numLines) }) t.Run("OAuth2GitHubDefaultProvider", func(t *testing.T) { diff --git a/coderd/oauth2_provider_state.go b/coderd/oauth2_provider_state.go new file mode 100644 index 00000000000..80f64807f1a --- /dev/null +++ b/coderd/oauth2_provider_state.go @@ -0,0 +1,40 @@ +package coderd + +import ( + "context" + "fmt" + + "cdr.dev/slog/v3" + "github.com/coder/coder/v2/coderd/database" + "github.com/coder/coder/v2/coderd/database/dbauthz" + "github.com/coder/coder/v2/codersdk" +) + +// oauth2ProviderDisabledWithAppsMessage is logged at startup when the +// provider is off but applications are still registered. +const oauth2ProviderDisabledWithAppsMessage = "The OAuth2 provider is disabled but %d OAuth2 application(s) are registered. Existing applications, secrets and user authorizations are preserved but cannot be used until the provider is enabled. Set CODER_OAUTH2_PROVIDER_ENABLE=true to enable it." + +// LogOAuth2ProviderState logs whether the OAuth2 provider is enabled so +// every start leaves one line recording the value of +// CODER_OAUTH2_PROVIDER_ENABLE. When the provider is disabled it also warns +// if applications are still registered. Errors are logged and startup +// continues. +func LogOAuth2ProviderState(ctx context.Context, logger slog.Logger, db database.Store, cfg codersdk.OAuth2ProviderConfig) { + flag := slog.F("flag", "CODER_OAUTH2_PROVIDER_ENABLE") + if cfg.Enable.Value() { + logger.Info(ctx, "oauth2 provider enabled", flag) + return + } + logger.Info(ctx, "oauth2 provider disabled", flag) + + //nolint:gocritic // Startup-only read; no user actor is present. + apps, err := db.GetOAuth2ProviderApps(dbauthz.AsSystemRestricted(ctx)) + if err != nil { + logger.Warn(ctx, "oauth2 provider: list registered applications", slog.Error(err)) + return + } + if len(apps) == 0 { + return + } + logger.Warn(ctx, fmt.Sprintf(oauth2ProviderDisabledWithAppsMessage, len(apps))) +} diff --git a/coderd/oauth2_provider_state_internal_test.go b/coderd/oauth2_provider_state_internal_test.go new file mode 100644 index 00000000000..8aaad890045 --- /dev/null +++ b/coderd/oauth2_provider_state_internal_test.go @@ -0,0 +1,72 @@ +package coderd + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" + "golang.org/x/xerrors" + + "cdr.dev/slog/v3" + "github.com/coder/coder/v2/coderd/database" + "github.com/coder/coder/v2/coderd/database/dbmock" + "github.com/coder/coder/v2/codersdk" + "github.com/coder/coder/v2/testutil" + "github.com/coder/serpent" +) + +func TestLogOAuth2ProviderState(t *testing.T) { + t.Parallel() + + for _, tc := range []struct { + name string + enabled bool + apps []database.OAuth2ProviderApp + queryErr error + wantInfo string + wantWarn []string + }{ + { + name: "Enabled", + enabled: true, + wantInfo: "oauth2 provider enabled", + }, + { + name: "DisabledNoApps", + wantInfo: "oauth2 provider disabled", + }, + { + name: "DisabledWithApps", + apps: []database.OAuth2ProviderApp{{}, {}}, + wantInfo: "oauth2 provider disabled", + wantWarn: []string{fmt.Sprintf(oauth2ProviderDisabledWithAppsMessage, 2)}, + }, + { + name: "DisabledQueryError", + queryErr: xerrors.New("boom"), + wantInfo: "oauth2 provider disabled", + wantWarn: []string{"oauth2 provider: list registered applications"}, + }, + } { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + ctx := testutil.Context(t, testutil.WaitShort) + rec := &logRecorder{} + db := dbmock.NewMockStore(gomock.NewController(t)) + // The query runs only while the provider is disabled. An + // unexpected call fails the test. + if !tc.enabled { + db.EXPECT().GetOAuth2ProviderApps(gomock.Any()).Return(tc.apps, tc.queryErr) + } + + cfg := codersdk.OAuth2ProviderConfig{Enable: serpent.Bool(tc.enabled)} + LogOAuth2ProviderState(ctx, slog.Make(rec), db, cfg) + + require.Equal(t, []string{tc.wantInfo}, rec.messages(slog.LevelInfo), + "exactly one info line per start") + require.Equal(t, tc.wantWarn, rec.messages(slog.LevelWarn)) + }) + } +}