feat: enable GitHub OAuth2 login by default on new deployments (#16662)

Third and final PR to address
https://github.com/coder/coder/issues/16230.

This PR enables GitHub OAuth2 login by default on new deployments.
Combined with https://github.com/coder/coder/pull/16629, this will allow
the first admin user to sign up with GitHub rather than email and
password.

We take care not to enable the default on deployments that would upgrade
to a Coder version with this change.

To disable the default provider an admin can set the
`CODER_OAUTH2_GITHUB_DEFAULT_PROVIDER` env variable to false.
This commit is contained in:
Hugo Dutka
2025-02-25 16:31:33 +01:00
committed by GitHub
parent 67d89bb102
commit d3a56ae3ef
25 changed files with 544 additions and 83 deletions

16
coderd/apidoc/docs.go generated
View File

@ -10331,7 +10331,7 @@ const docTemplate = `{
"type": "object",
"properties": {
"github": {
"$ref": "#/definitions/codersdk.AuthMethod"
"$ref": "#/definitions/codersdk.GithubAuthMethod"
},
"oidc": {
"$ref": "#/definitions/codersdk.OIDCAuthMethod"
@ -11857,6 +11857,17 @@ const docTemplate = `{
}
}
},
"codersdk.GithubAuthMethod": {
"type": "object",
"properties": {
"default_provider_configured": {
"type": "boolean"
},
"enabled": {
"type": "boolean"
}
}
},
"codersdk.Group": {
"type": "object",
"properties": {
@ -12519,6 +12530,9 @@ const docTemplate = `{
"client_secret": {
"type": "string"
},
"default_provider_enable": {
"type": "boolean"
},
"device_flow": {
"type": "boolean"
},

View File

@ -9189,7 +9189,7 @@
"type": "object",
"properties": {
"github": {
"$ref": "#/definitions/codersdk.AuthMethod"
"$ref": "#/definitions/codersdk.GithubAuthMethod"
},
"oidc": {
"$ref": "#/definitions/codersdk.OIDCAuthMethod"
@ -10642,6 +10642,17 @@
}
}
},
"codersdk.GithubAuthMethod": {
"type": "object",
"properties": {
"default_provider_configured": {
"type": "boolean"
},
"enabled": {
"type": "boolean"
}
}
},
"codersdk.Group": {
"type": "object",
"properties": {
@ -11255,6 +11266,9 @@
"client_secret": {
"type": "string"
},
"default_provider_enable": {
"type": "boolean"
},
"device_flow": {
"type": "boolean"
},

View File

@ -1845,6 +1845,13 @@ func (q *querier) GetNotificationsSettings(ctx context.Context) (string, error)
return q.db.GetNotificationsSettings(ctx)
}
func (q *querier) GetOAuth2GithubDefaultEligible(ctx context.Context) (bool, error) {
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceDeploymentConfig); err != nil {
return false, err
}
return q.db.GetOAuth2GithubDefaultEligible(ctx)
}
func (q *querier) GetOAuth2ProviderAppByID(ctx context.Context, id uuid.UUID) (database.OAuth2ProviderApp, error) {
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceOauth2App); err != nil {
return database.OAuth2ProviderApp{}, err
@ -4435,6 +4442,13 @@ func (q *querier) UpsertNotificationsSettings(ctx context.Context, value string)
return q.db.UpsertNotificationsSettings(ctx, value)
}
func (q *querier) UpsertOAuth2GithubDefaultEligible(ctx context.Context, eligible bool) error {
if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceDeploymentConfig); err != nil {
return err
}
return q.db.UpsertOAuth2GithubDefaultEligible(ctx, eligible)
}
func (q *querier) UpsertOAuthSigningKey(ctx context.Context, value string) error {
if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceSystem); err != nil {
return err

View File

@ -4405,6 +4405,12 @@ func (s *MethodTestSuite) TestSystemFunctions() {
Value: "value",
}).Asserts(rbac.ResourceSystem, policy.ActionUpdate)
}))
s.Run("GetOAuth2GithubDefaultEligible", s.Subtest(func(db database.Store, check *expects) {
check.Args().Asserts(rbac.ResourceDeploymentConfig, policy.ActionRead).Errors(sql.ErrNoRows)
}))
s.Run("UpsertOAuth2GithubDefaultEligible", s.Subtest(func(db database.Store, check *expects) {
check.Args(true).Asserts(rbac.ResourceDeploymentConfig, policy.ActionUpdate)
}))
}
func (s *MethodTestSuite) TestNotifications() {

View File

@ -254,6 +254,7 @@ type data struct {
announcementBanners []byte
healthSettings []byte
notificationsSettings []byte
oauth2GithubDefaultEligible *bool
applicationName string
logoURL string
appSecurityKey string
@ -3515,6 +3516,16 @@ func (q *FakeQuerier) GetNotificationsSettings(_ context.Context) (string, error
return string(q.notificationsSettings), nil
}
func (q *FakeQuerier) GetOAuth2GithubDefaultEligible(_ context.Context) (bool, error) {
q.mutex.RLock()
defer q.mutex.RUnlock()
if q.oauth2GithubDefaultEligible == nil {
return false, sql.ErrNoRows
}
return *q.oauth2GithubDefaultEligible, nil
}
func (q *FakeQuerier) GetOAuth2ProviderAppByID(_ context.Context, id uuid.UUID) (database.OAuth2ProviderApp, error) {
q.mutex.Lock()
defer q.mutex.Unlock()
@ -11154,6 +11165,14 @@ func (q *FakeQuerier) UpsertNotificationsSettings(_ context.Context, data string
return nil
}
func (q *FakeQuerier) UpsertOAuth2GithubDefaultEligible(_ context.Context, eligible bool) error {
q.mutex.Lock()
defer q.mutex.Unlock()
q.oauth2GithubDefaultEligible = &eligible
return nil
}
func (q *FakeQuerier) UpsertOAuthSigningKey(_ context.Context, value string) error {
q.mutex.Lock()
defer q.mutex.Unlock()

View File

@ -871,6 +871,13 @@ func (m queryMetricsStore) GetNotificationsSettings(ctx context.Context) (string
return r0, r1
}
func (m queryMetricsStore) GetOAuth2GithubDefaultEligible(ctx context.Context) (bool, error) {
start := time.Now()
r0, r1 := m.s.GetOAuth2GithubDefaultEligible(ctx)
m.queryLatencies.WithLabelValues("GetOAuth2GithubDefaultEligible").Observe(time.Since(start).Seconds())
return r0, r1
}
func (m queryMetricsStore) GetOAuth2ProviderAppByID(ctx context.Context, id uuid.UUID) (database.OAuth2ProviderApp, error) {
start := time.Now()
r0, r1 := m.s.GetOAuth2ProviderAppByID(ctx, id)
@ -2817,6 +2824,13 @@ func (m queryMetricsStore) UpsertNotificationsSettings(ctx context.Context, valu
return r0
}
func (m queryMetricsStore) UpsertOAuth2GithubDefaultEligible(ctx context.Context, eligible bool) error {
start := time.Now()
r0 := m.s.UpsertOAuth2GithubDefaultEligible(ctx, eligible)
m.queryLatencies.WithLabelValues("UpsertOAuth2GithubDefaultEligible").Observe(time.Since(start).Seconds())
return r0
}
func (m queryMetricsStore) UpsertOAuthSigningKey(ctx context.Context, value string) error {
start := time.Now()
r0 := m.s.UpsertOAuthSigningKey(ctx, value)

View File

@ -1762,6 +1762,21 @@ func (mr *MockStoreMockRecorder) GetNotificationsSettings(ctx any) *gomock.Call
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNotificationsSettings", reflect.TypeOf((*MockStore)(nil).GetNotificationsSettings), ctx)
}
// GetOAuth2GithubDefaultEligible mocks base method.
func (m *MockStore) GetOAuth2GithubDefaultEligible(ctx context.Context) (bool, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetOAuth2GithubDefaultEligible", ctx)
ret0, _ := ret[0].(bool)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetOAuth2GithubDefaultEligible indicates an expected call of GetOAuth2GithubDefaultEligible.
func (mr *MockStoreMockRecorder) GetOAuth2GithubDefaultEligible(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetOAuth2GithubDefaultEligible", reflect.TypeOf((*MockStore)(nil).GetOAuth2GithubDefaultEligible), ctx)
}
// GetOAuth2ProviderAppByID mocks base method.
func (m *MockStore) GetOAuth2ProviderAppByID(ctx context.Context, id uuid.UUID) (database.OAuth2ProviderApp, error) {
m.ctrl.T.Helper()
@ -5936,6 +5951,20 @@ func (mr *MockStoreMockRecorder) UpsertNotificationsSettings(ctx, value any) *go
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertNotificationsSettings", reflect.TypeOf((*MockStore)(nil).UpsertNotificationsSettings), ctx, value)
}
// UpsertOAuth2GithubDefaultEligible mocks base method.
func (m *MockStore) UpsertOAuth2GithubDefaultEligible(ctx context.Context, eligible bool) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpsertOAuth2GithubDefaultEligible", ctx, eligible)
ret0, _ := ret[0].(error)
return ret0
}
// UpsertOAuth2GithubDefaultEligible indicates an expected call of UpsertOAuth2GithubDefaultEligible.
func (mr *MockStoreMockRecorder) UpsertOAuth2GithubDefaultEligible(ctx, eligible any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertOAuth2GithubDefaultEligible", reflect.TypeOf((*MockStore)(nil).UpsertOAuth2GithubDefaultEligible), ctx, eligible)
}
// UpsertOAuthSigningKey mocks base method.
func (m *MockStore) UpsertOAuthSigningKey(ctx context.Context, value string) error {
m.ctrl.T.Helper()

View File

@ -185,6 +185,7 @@ type sqlcQuerier interface {
GetNotificationTemplateByID(ctx context.Context, id uuid.UUID) (NotificationTemplate, error)
GetNotificationTemplatesByKind(ctx context.Context, kind NotificationTemplateKind) ([]NotificationTemplate, error)
GetNotificationsSettings(ctx context.Context) (string, error)
GetOAuth2GithubDefaultEligible(ctx context.Context) (bool, error)
GetOAuth2ProviderAppByID(ctx context.Context, id uuid.UUID) (OAuth2ProviderApp, error)
GetOAuth2ProviderAppCodeByID(ctx context.Context, id uuid.UUID) (OAuth2ProviderAppCode, error)
GetOAuth2ProviderAppCodeByPrefix(ctx context.Context, secretPrefix []byte) (OAuth2ProviderAppCode, error)
@ -553,6 +554,7 @@ type sqlcQuerier interface {
// Insert or update notification report generator logs with recent activity.
UpsertNotificationReportGeneratorLog(ctx context.Context, arg UpsertNotificationReportGeneratorLogParams) error
UpsertNotificationsSettings(ctx context.Context, value string) error
UpsertOAuth2GithubDefaultEligible(ctx context.Context, eligible bool) error
UpsertOAuthSigningKey(ctx context.Context, value string) error
UpsertProvisionerDaemon(ctx context.Context, arg UpsertProvisionerDaemonParams) (ProvisionerDaemon, error)
UpsertRuntimeConfig(ctx context.Context, arg UpsertRuntimeConfigParams) error

View File

@ -8100,6 +8100,23 @@ func (q *sqlQuerier) GetNotificationsSettings(ctx context.Context) (string, erro
return notifications_settings, err
}
const getOAuth2GithubDefaultEligible = `-- name: GetOAuth2GithubDefaultEligible :one
SELECT
CASE
WHEN value = 'true' THEN TRUE
ELSE FALSE
END
FROM site_configs
WHERE key = 'oauth2_github_default_eligible'
`
func (q *sqlQuerier) GetOAuth2GithubDefaultEligible(ctx context.Context) (bool, error) {
row := q.db.QueryRowContext(ctx, getOAuth2GithubDefaultEligible)
var column_1 bool
err := row.Scan(&column_1)
return column_1, err
}
const getOAuthSigningKey = `-- name: GetOAuthSigningKey :one
SELECT value FROM site_configs WHERE key = 'oauth_signing_key'
`
@ -8243,6 +8260,28 @@ func (q *sqlQuerier) UpsertNotificationsSettings(ctx context.Context, value stri
return err
}
const upsertOAuth2GithubDefaultEligible = `-- name: UpsertOAuth2GithubDefaultEligible :exec
INSERT INTO site_configs (key, value)
VALUES (
'oauth2_github_default_eligible',
CASE
WHEN $1::bool THEN 'true'
ELSE 'false'
END
)
ON CONFLICT (key) DO UPDATE
SET value = CASE
WHEN $1::bool THEN 'true'
ELSE 'false'
END
WHERE site_configs.key = 'oauth2_github_default_eligible'
`
func (q *sqlQuerier) UpsertOAuth2GithubDefaultEligible(ctx context.Context, eligible bool) error {
_, err := q.db.ExecContext(ctx, upsertOAuth2GithubDefaultEligible, eligible)
return err
}
const upsertOAuthSigningKey = `-- name: UpsertOAuthSigningKey :exec
INSERT INTO site_configs (key, value) VALUES ('oauth_signing_key', $1)
ON CONFLICT (key) DO UPDATE set value = $1 WHERE site_configs.key = 'oauth_signing_key'

View File

@ -107,3 +107,27 @@ ON CONFLICT (key) DO UPDATE SET value = $2 WHERE site_configs.key = $1;
DELETE FROM site_configs
WHERE site_configs.key = $1;
-- name: GetOAuth2GithubDefaultEligible :one
SELECT
CASE
WHEN value = 'true' THEN TRUE
ELSE FALSE
END
FROM site_configs
WHERE key = 'oauth2_github_default_eligible';
-- name: UpsertOAuth2GithubDefaultEligible :exec
INSERT INTO site_configs (key, value)
VALUES (
'oauth2_github_default_eligible',
CASE
WHEN sqlc.arg(eligible)::bool THEN 'true'
ELSE 'false'
END
)
ON CONFLICT (key) DO UPDATE
SET value = CASE
WHEN sqlc.arg(eligible)::bool THEN 'true'
ELSE 'false'
END
WHERE site_configs.key = 'oauth2_github_default_eligible';

View File

@ -765,6 +765,8 @@ type GithubOAuth2Config struct {
AllowEveryone bool
AllowOrganizations []string
AllowTeams []GithubOAuth2Team
DefaultProviderConfigured bool
}
func (c *GithubOAuth2Config) Exchange(ctx context.Context, code string, opts ...oauth2.AuthCodeOption) (*oauth2.Token, error) {
@ -806,7 +808,10 @@ func (api *API) userAuthMethods(rw http.ResponseWriter, r *http.Request) {
Password: codersdk.AuthMethod{
Enabled: !api.DeploymentValues.DisablePasswordAuth.Value(),
},
Github: codersdk.AuthMethod{Enabled: api.GithubOAuth2Config != nil},
Github: codersdk.GithubAuthMethod{
Enabled: api.GithubOAuth2Config != nil,
DefaultProviderConfigured: api.GithubOAuth2Config != nil && api.GithubOAuth2Config.DefaultProviderConfigured,
},
OIDC: codersdk.OIDCAuthMethod{
AuthMethod: codersdk.AuthMethod{Enabled: api.OIDCConfig != nil},
SignInText: signInText,