Expand Up @@ -688,24 +688,6 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd. } } if vals.OAuth2.Github.ClientSecret != "" || vals.OAuth2.Github.DeviceFlow.Value() { options.GithubOAuth2Config, err = configureGithubOAuth2( oauthInstrument, vals.AccessURL.Value(), vals.OAuth2.Github.ClientID.String(), vals.OAuth2.Github.ClientSecret.String(), vals.OAuth2.Github.DeviceFlow.Value(), vals.OAuth2.Github.AllowSignups.Value(), vals.OAuth2.Github.AllowEveryone.Value(), vals.OAuth2.Github.AllowedOrgs, vals.OAuth2.Github.AllowedTeams, vals.OAuth2.Github.EnterpriseBaseURL.String(), ) if err != nil { return xerrors.Errorf("configure github oauth2: %w", err) } } // As OIDC clients can be confidential or public, // we should only check for a client id being set. // The underlying library handles the case of no Expand Down Expand Up @@ -793,6 +775,20 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd. return xerrors.Errorf("set deployment id: %w", err) } githubOAuth2ConfigParams, err := getGithubOAuth2ConfigParams(ctx, options.Database, vals) if err != nil { return xerrors.Errorf("get github oauth2 config params: %w", err) } if githubOAuth2ConfigParams != nil { options.GithubOAuth2Config, err = configureGithubOAuth2( oauthInstrument, githubOAuth2ConfigParams, ) if err != nil { return xerrors.Errorf("configure github oauth2: %w", err) } } options.RuntimeConfig = runtimeconfig.NewManager() // This should be output before the logs start streaming. Expand Down Expand Up @@ -1843,25 +1839,101 @@ func configureCAPool(tlsClientCAFile string, tlsConfig *tls.Config) error { return nil } // TODO: convert the argument list to a struct, it's easy to mix up the order of the arguments // const ( // Client ID for https://github.com/apps/coder GithubOAuth2DefaultProviderClientID = "Iv1.6a2b4b4aec4f4fe7" GithubOAuth2DefaultProviderAllowEveryone = true GithubOAuth2DefaultProviderDeviceFlow = true ) type githubOAuth2ConfigParams struct { accessURL *url.URL clientID string clientSecret string deviceFlow bool allowSignups bool allowEveryone bool allowOrgs []string rawTeams []string enterpriseBaseURL string } func getGithubOAuth2ConfigParams(ctx context.Context, db database.Store, vals *codersdk.DeploymentValues) (*githubOAuth2ConfigParams, error) { params := githubOAuth2ConfigParams{ accessURL: vals.AccessURL.Value(), clientID: vals.OAuth2.Github.ClientID.String(), clientSecret: vals.OAuth2.Github.ClientSecret.String(), deviceFlow: vals.OAuth2.Github.DeviceFlow.Value(), allowSignups: vals.OAuth2.Github.AllowSignups.Value(), allowEveryone: vals.OAuth2.Github.AllowEveryone.Value(), allowOrgs: vals.OAuth2.Github.AllowedOrgs.Value(), rawTeams: vals.OAuth2.Github.AllowedTeams.Value(), enterpriseBaseURL: vals.OAuth2.Github.EnterpriseBaseURL.String(), } // If the user manually configured the GitHub OAuth2 provider, // we won't add the default configuration. if params.clientID != "" || params.clientSecret != "" || params.enterpriseBaseURL != "" { return ¶ms, nil } // Check if the user manually disabled the default GitHub OAuth2 provider. if !vals.OAuth2.Github.DefaultProviderEnable.Value() { return nil, nil //nolint:nilnil } // Check if the deployment is eligible for the default GitHub OAuth2 provider. // We want to enable it only for new deployments, and avoid enabling it // if a deployment was upgraded from an older version. // nolint:gocritic // Requires system privileges defaultEligible, err := db.GetOAuth2GithubDefaultEligible(dbauthz.AsSystemRestricted(ctx)) if err != nil && !errors.Is(err, sql.ErrNoRows) { return nil, xerrors.Errorf("get github default eligible: %w", err) } defaultEligibleNotSet := errors.Is(err, sql.ErrNoRows) if defaultEligibleNotSet { // nolint:gocritic // User count requires system privileges userCount, err := db.GetUserCount(dbauthz.AsSystemRestricted(ctx)) if err != nil { return nil, xerrors.Errorf("get user count: %w", err) } // We check if a deployment is new by checking if it has any users. defaultEligible = userCount == 0 // nolint:gocritic // Requires system privileges if err := db.UpsertOAuth2GithubDefaultEligible(dbauthz.AsSystemRestricted(ctx), defaultEligible); err != nil { return nil, xerrors.Errorf("upsert github default eligible: %w", err) } } if !defaultEligible { return nil, nil //nolint:nilnil } params.clientID = GithubOAuth2DefaultProviderClientID params.allowEveryone = GithubOAuth2DefaultProviderAllowEveryone params.deviceFlow = GithubOAuth2DefaultProviderDeviceFlow return ¶ms, nil } //nolint:revive // Ignore flag-parameter: parameter 'allowEveryone' seems to be a control flag, avoid control coupling (revive) func configureGithubOAuth2(instrument *promoauth.Factory,accessURL *url.URL, clientID, clientSecret string, deviceFlow, allowSignups, allowEveryone bool, allowOrgs []string, rawTeams []string, enterpriseBaseURL string ) (*coderd.GithubOAuth2Config, error) { redirectURL, err := accessURL.Parse("/api/v2/users/oauth2/github/callback") func configureGithubOAuth2(instrument *promoauth.Factory,params *githubOAuth2ConfigParams ) (*coderd.GithubOAuth2Config, error) { redirectURL, err :=params. accessURL.Parse("/api/v2/users/oauth2/github/callback") if err != nil { return nil, xerrors.Errorf("parse github oauth callback url: %w", err) } if allowEveryone && len(allowOrgs) > 0 { ifparams. allowEveryone && len(params. allowOrgs) > 0 { return nil, xerrors.New("allow everyone and allowed orgs cannot be used together") } if allowEveryone && len(rawTeams) > 0 { ifparams. allowEveryone && len(params. rawTeams) > 0 { return nil, xerrors.New("allow everyone and allowed teams cannot be used together") } if !allowEveryone && len(allowOrgs) == 0 { if !params. allowEveryone && len(params. allowOrgs) == 0 { return nil, xerrors.New("allowed orgs is empty: must specify at least one org or allow everyone") } allowTeams := make([]coderd.GithubOAuth2Team, 0, len(rawTeams)) for _, rawTeam := range rawTeams { allowTeams := make([]coderd.GithubOAuth2Team, 0, len(params. rawTeams)) for _, rawTeam := rangeparams. rawTeams { parts := strings.SplitN(rawTeam, "/", 2) if len(parts) != 2 { return nil, xerrors.Errorf("github team allowlist is formatted incorrectly. got %s; wanted <organization>/<team>", rawTeam) Expand All @@ -1873,8 +1945,8 @@ func configureGithubOAuth2(instrument *promoauth.Factory, accessURL *url.URL, cl } endpoint := xgithub.Endpoint if enterpriseBaseURL != "" { enterpriseURL, err := url.Parse(enterpriseBaseURL) ifparams. enterpriseBaseURL != "" { enterpriseURL, err := url.Parse(params. enterpriseBaseURL) if err != nil { return nil, xerrors.Errorf("parse enterprise base url: %w", err) } Expand All @@ -1893,8 +1965,8 @@ func configureGithubOAuth2(instrument *promoauth.Factory, accessURL *url.URL, cl } instrumentedOauth := instrument.NewGithub("github-login", &oauth2.Config{ ClientID: clientID, ClientSecret: clientSecret, ClientID:params. clientID, ClientSecret:params. clientSecret, Endpoint: endpoint, RedirectURL: redirectURL.String(), Scopes: []string{ Expand All @@ -1906,17 +1978,17 @@ func configureGithubOAuth2(instrument *promoauth.Factory, accessURL *url.URL, cl createClient := func(client *http.Client, source promoauth.Oauth2Source) (*github.Client, error) { client = instrumentedOauth.InstrumentHTTPClient(client, source) if enterpriseBaseURL != "" { return github.NewEnterpriseClient(enterpriseBaseURL, "", client) ifparams. enterpriseBaseURL != "" { return github.NewEnterpriseClient(params. enterpriseBaseURL, "", client) } return github.NewClient(client), nil } var deviceAuth *externalauth.DeviceAuth if deviceFlow { ifparams. deviceFlow { deviceAuth = &externalauth.DeviceAuth{ Config: instrumentedOauth, ClientID: clientID, ClientID:params. clientID, TokenURL: endpoint.TokenURL, Scopes: []string{"read:user", "read:org", "user:email"}, CodeURL: endpoint.DeviceAuthURL, Expand All @@ -1925,9 +1997,9 @@ func configureGithubOAuth2(instrument *promoauth.Factory, accessURL *url.URL, cl return &coderd.GithubOAuth2Config{ OAuth2Config: instrumentedOauth, AllowSignups: allowSignups, AllowEveryone: allowEveryone, AllowOrganizations: allowOrgs, AllowSignups:params. allowSignups, AllowEveryone:params. allowEveryone, AllowOrganizations:params. allowOrgs, AllowTeams: allowTeams, AuthenticatedUser: func(ctx context.Context, client *http.Client) (*github.User, error) { api, err := createClient(client, promoauth.SourceGitAPIAuthUser) Expand Down Expand Up @@ -1966,19 +2038,20 @@ func configureGithubOAuth2(instrument *promoauth.Factory, accessURL *url.URL, cl team, _, err := api.Teams.GetTeamMembershipBySlug(ctx, org, teamSlug, username) return team, err }, DeviceFlowEnabled: deviceFlow, DeviceFlowEnabled:params. deviceFlow, ExchangeDeviceCode: func(ctx context.Context, deviceCode string) (*oauth2.Token, error) { if !deviceFlow { if !params. deviceFlow { return nil, xerrors.New("device flow is not enabled") } return deviceAuth.ExchangeDeviceCode(ctx, deviceCode) }, AuthorizeDevice: func(ctx context.Context) (*codersdk.ExternalAuthDevice, error) { if !deviceFlow { if !params. deviceFlow { return nil, xerrors.New("device flow is not enabled") } return deviceAuth.AuthorizeDevice(ctx) }, DefaultProviderConfigured: params.clientID == GithubOAuth2DefaultProviderClientID, }, nil } Expand Down