Skip to content

Commit 45b53c2

Browse files
authored
feat: allow external services to be authable (#9996)
* feat: allow external services to be authable * Refactor external auth config structure for defaults * Add support for new config properties * Change the name of external auth * Move externalauth -> external-auth * Run gen * Fix tests * Fix MW tests * Fix git auth redirect * Fix lint * Fix name * Allow any ID * Fix invalid type test * Fix e2e tests * Fix comments * Fix colors * Allow accepting any type as string * Run gen * Fix href
1 parent f62f45a commit 45b53c2

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

58 files changed

+1139
-1026
lines changed

cli/cliui/gitauth.go renamed to cli/cliui/externalauth.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,12 @@ import (
1111
"github.com/coder/coder/v2/codersdk"
1212
)
1313

14-
type GitAuthOptions struct {
14+
type ExternalAuthOptions struct {
1515
Fetch func(context.Context) ([]codersdk.TemplateVersionExternalAuth, error)
1616
FetchInterval time.Duration
1717
}
1818

19-
func GitAuth(ctx context.Context, writer io.Writer, opts GitAuthOptions) error {
19+
func ExternalAuth(ctx context.Context, writer io.Writer, opts ExternalAuthOptions) error {
2020
if opts.FetchInterval == 0 {
2121
opts.FetchInterval = 500 * time.Millisecond
2222
}
@@ -38,7 +38,7 @@ func GitAuth(ctx context.Context, writer io.Writer, opts GitAuthOptions) error {
3838
return nil
3939
}
4040

41-
_, _ = fmt.Fprintf(writer, "You must authenticate with %s to create a workspace with this template. Visit:\n\n\t%s\n\n", auth.Type.Pretty(), auth.AuthenticateURL)
41+
_, _ = fmt.Fprintf(writer, "You must authenticate with %s to create a workspace with this template. Visit:\n\n\t%s\n\n", auth.DisplayName, auth.AuthenticateURL)
4242

4343
ticker.Reset(opts.FetchInterval)
4444
spin.Start()
@@ -66,7 +66,7 @@ func GitAuth(ctx context.Context, writer io.Writer, opts GitAuthOptions) error {
6666
}
6767
}
6868
spin.Stop()
69-
_, _ = fmt.Fprintf(writer, "Successfully authenticated with %s!\n\n", auth.Type.Pretty())
69+
_, _ = fmt.Fprintf(writer, "Successfully authenticated with %s!\n\n", auth.DisplayName)
7070
}
7171
return nil
7272
}

cli/cliui/gitauth_test.go renamed to cli/cliui/externalauth_test.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import (
1515
"github.com/coder/coder/v2/testutil"
1616
)
1717

18-
func TestGitAuth(t *testing.T) {
18+
func TestExternalAuth(t *testing.T) {
1919
t.Parallel()
2020

2121
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
@@ -25,12 +25,13 @@ func TestGitAuth(t *testing.T) {
2525
cmd := &clibase.Cmd{
2626
Handler: func(inv *clibase.Invocation) error {
2727
var fetched atomic.Bool
28-
return cliui.GitAuth(inv.Context(), inv.Stdout, cliui.GitAuthOptions{
28+
return cliui.ExternalAuth(inv.Context(), inv.Stdout, cliui.ExternalAuthOptions{
2929
Fetch: func(ctx context.Context) ([]codersdk.TemplateVersionExternalAuth, error) {
3030
defer fetched.Store(true)
3131
return []codersdk.TemplateVersionExternalAuth{{
3232
ID: "github",
33-
Type: codersdk.ExternalAuthProviderGitHub,
33+
DisplayName: "GitHub",
34+
Type: codersdk.EnhancedExternalAuthProviderGitHub.String(),
3435
Authenticated: fetched.Load(),
3536
AuthenticateURL: "https://example.com/gitauth/github",
3637
}}, nil

cli/create.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,7 @@ func prepWorkspaceBuild(inv *clibase.Invocation, client *codersdk.Client, args p
265265
return nil, err
266266
}
267267

268-
err = cliui.GitAuth(ctx, inv.Stdout, cliui.GitAuthOptions{
268+
err = cliui.ExternalAuth(ctx, inv.Stdout, cliui.ExternalAuthOptions{
269269
Fetch: func(ctx context.Context) ([]codersdk.TemplateVersionExternalAuth, error) {
270270
return client.TemplateVersionExternalAuth(ctx, templateVersion.ID)
271271
},

cli/create_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -613,7 +613,8 @@ func TestCreateWithGitAuth(t *testing.T) {
613613
OAuth2Config: &testutil.OAuth2Config{},
614614
ID: "github",
615615
Regex: regexp.MustCompile(`github\.com`),
616-
Type: codersdk.ExternalAuthProviderGitHub,
616+
Type: codersdk.EnhancedExternalAuthProviderGitHub.String(),
617+
DisplayName: "GitHub",
617618
}},
618619
IncludeProvisionerDaemon: true,
619620
})

cli/server.go

Lines changed: 103 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -98,85 +98,6 @@ import (
9898
"github.com/coder/wgtunnel/tunnelsdk"
9999
)
100100

101-
// ReadGitAuthProvidersFromEnv is provided for compatibility purposes with the
102-
// viper CLI.
103-
// DEPRECATED
104-
func ReadGitAuthProvidersFromEnv(environ []string) ([]codersdk.GitAuthConfig, error) {
105-
// The index numbers must be in-order.
106-
sort.Strings(environ)
107-
108-
var providers []codersdk.GitAuthConfig
109-
for _, v := range clibase.ParseEnviron(environ, "CODER_GITAUTH_") {
110-
tokens := strings.SplitN(v.Name, "_", 2)
111-
if len(tokens) != 2 {
112-
return nil, xerrors.Errorf("invalid env var: %s", v.Name)
113-
}
114-
115-
providerNum, err := strconv.Atoi(tokens[0])
116-
if err != nil {
117-
return nil, xerrors.Errorf("parse number: %s", v.Name)
118-
}
119-
120-
var provider codersdk.GitAuthConfig
121-
switch {
122-
case len(providers) < providerNum:
123-
return nil, xerrors.Errorf(
124-
"provider num %v skipped: %s",
125-
len(providers),
126-
v.Name,
127-
)
128-
case len(providers) == providerNum:
129-
// At the next next provider.
130-
providers = append(providers, provider)
131-
case len(providers) == providerNum+1:
132-
// At the current provider.
133-
provider = providers[providerNum]
134-
}
135-
136-
key := tokens[1]
137-
switch key {
138-
case "ID":
139-
provider.ID = v.Value
140-
case "TYPE":
141-
provider.Type = v.Value
142-
case "CLIENT_ID":
143-
provider.ClientID = v.Value
144-
case "CLIENT_SECRET":
145-
provider.ClientSecret = v.Value
146-
case "AUTH_URL":
147-
provider.AuthURL = v.Value
148-
case "TOKEN_URL":
149-
provider.TokenURL = v.Value
150-
case "VALIDATE_URL":
151-
provider.ValidateURL = v.Value
152-
case "REGEX":
153-
provider.Regex = v.Value
154-
case "DEVICE_FLOW":
155-
b, err := strconv.ParseBool(v.Value)
156-
if err != nil {
157-
return nil, xerrors.Errorf("parse bool: %s", v.Value)
158-
}
159-
provider.DeviceFlow = b
160-
case "DEVICE_CODE_URL":
161-
provider.DeviceCodeURL = v.Value
162-
case "NO_REFRESH":
163-
b, err := strconv.ParseBool(v.Value)
164-
if err != nil {
165-
return nil, xerrors.Errorf("parse bool: %s", v.Value)
166-
}
167-
provider.NoRefresh = b
168-
case "SCOPES":
169-
provider.Scopes = strings.Split(v.Value, " ")
170-
case "APP_INSTALL_URL":
171-
provider.AppInstallURL = v.Value
172-
case "APP_INSTALLATIONS_URL":
173-
provider.AppInstallationsURL = v.Value
174-
}
175-
providers[providerNum] = provider
176-
}
177-
return providers, nil
178-
}
179-
180101
func createOIDCConfig(ctx context.Context, vals *codersdk.DeploymentValues) (*coderd.OIDCConfig, error) {
181102
if vals.OIDC.ClientID == "" {
182103
return nil, xerrors.Errorf("OIDC client ID must be set!")
@@ -568,14 +489,14 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
568489
}
569490
}
570491

571-
gitAuthEnv, err := ReadGitAuthProvidersFromEnv(os.Environ())
492+
extAuthEnv, err := ReadExternalAuthProvidersFromEnv(os.Environ())
572493
if err != nil {
573-
return xerrors.Errorf("read git auth providers from env: %w", err)
494+
return xerrors.Errorf("read external auth providers from env: %w", err)
574495
}
575496

576-
vals.GitAuthProviders.Value = append(vals.GitAuthProviders.Value, gitAuthEnv...)
497+
vals.ExternalAuthConfigs.Value = append(vals.ExternalAuthConfigs.Value, extAuthEnv...)
577498
externalAuthConfigs, err := externalauth.ConvertConfig(
578-
vals.GitAuthProviders.Value,
499+
vals.ExternalAuthConfigs.Value,
579500
vals.AccessURL.Value(),
580501
)
581502
if err != nil {
@@ -816,7 +737,7 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
816737
if vals.Telemetry.Enable {
817738
gitAuth := make([]telemetry.GitAuth, 0)
818739
// TODO:
819-
var gitAuthConfigs []codersdk.GitAuthConfig
740+
var gitAuthConfigs []codersdk.ExternalAuthConfig
820741
for _, cfg := range gitAuthConfigs {
821742
gitAuth = append(gitAuth, telemetry.GitAuth{
822743
Type: cfg.Type,
@@ -2242,3 +2163,101 @@ func ConfigureHTTPServers(inv *clibase.Invocation, cfg *codersdk.DeploymentValue
22422163

22432164
return httpServers, nil
22442165
}
2166+
2167+
// ReadExternalAuthProvidersFromEnv is provided for compatibility purposes with
2168+
// the viper CLI.
2169+
func ReadExternalAuthProvidersFromEnv(environ []string) ([]codersdk.ExternalAuthConfig, error) {
2170+
providers, err := parseExternalAuthProvidersFromEnv("CODER_EXTERNAL_AUTH_", environ)
2171+
if err != nil {
2172+
return nil, err
2173+
}
2174+
// Deprecated: To support legacy git auth!
2175+
gitProviders, err := parseExternalAuthProvidersFromEnv("CODER_GITAUTH_", environ)
2176+
if err != nil {
2177+
return nil, err
2178+
}
2179+
return append(providers, gitProviders...), nil
2180+
}
2181+
2182+
// parseExternalAuthProvidersFromEnv consumes environment variables to parse
2183+
// external auth providers. A prefix is provided to support the legacy
2184+
// parsing of `GITAUTH` environment variables.
2185+
func parseExternalAuthProvidersFromEnv(prefix string, environ []string) ([]codersdk.ExternalAuthConfig, error) {
2186+
// The index numbers must be in-order.
2187+
sort.Strings(environ)
2188+
2189+
var providers []codersdk.ExternalAuthConfig
2190+
for _, v := range clibase.ParseEnviron(environ, prefix) {
2191+
tokens := strings.SplitN(v.Name, "_", 2)
2192+
if len(tokens) != 2 {
2193+
return nil, xerrors.Errorf("invalid env var: %s", v.Name)
2194+
}
2195+
2196+
providerNum, err := strconv.Atoi(tokens[0])
2197+
if err != nil {
2198+
return nil, xerrors.Errorf("parse number: %s", v.Name)
2199+
}
2200+
2201+
var provider codersdk.ExternalAuthConfig
2202+
switch {
2203+
case len(providers) < providerNum:
2204+
return nil, xerrors.Errorf(
2205+
"provider num %v skipped: %s",
2206+
len(providers),
2207+
v.Name,
2208+
)
2209+
case len(providers) == providerNum:
2210+
// At the next next provider.
2211+
providers = append(providers, provider)
2212+
case len(providers) == providerNum+1:
2213+
// At the current provider.
2214+
provider = providers[providerNum]
2215+
}
2216+
2217+
key := tokens[1]
2218+
switch key {
2219+
case "ID":
2220+
provider.ID = v.Value
2221+
case "TYPE":
2222+
provider.Type = v.Value
2223+
case "CLIENT_ID":
2224+
provider.ClientID = v.Value
2225+
case "CLIENT_SECRET":
2226+
provider.ClientSecret = v.Value
2227+
case "AUTH_URL":
2228+
provider.AuthURL = v.Value
2229+
case "TOKEN_URL":
2230+
provider.TokenURL = v.Value
2231+
case "VALIDATE_URL":
2232+
provider.ValidateURL = v.Value
2233+
case "REGEX":
2234+
provider.Regex = v.Value
2235+
case "DEVICE_FLOW":
2236+
b, err := strconv.ParseBool(v.Value)
2237+
if err != nil {
2238+
return nil, xerrors.Errorf("parse bool: %s", v.Value)
2239+
}
2240+
provider.DeviceFlow = b
2241+
case "DEVICE_CODE_URL":
2242+
provider.DeviceCodeURL = v.Value
2243+
case "NO_REFRESH":
2244+
b, err := strconv.ParseBool(v.Value)
2245+
if err != nil {
2246+
return nil, xerrors.Errorf("parse bool: %s", v.Value)
2247+
}
2248+
provider.NoRefresh = b
2249+
case "SCOPES":
2250+
provider.Scopes = strings.Split(v.Value, " ")
2251+
case "APP_INSTALL_URL":
2252+
provider.AppInstallURL = v.Value
2253+
case "APP_INSTALLATIONS_URL":
2254+
provider.AppInstallationsURL = v.Value
2255+
case "DISPLAY_NAME":
2256+
provider.DisplayName = v.Value
2257+
case "DISPLAY_ICON":
2258+
provider.DisplayIcon = v.Value
2259+
}
2260+
providers[providerNum] = provider
2261+
}
2262+
return providers, nil
2263+
}

cli/server_test.go

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,27 +49,66 @@ import (
4949
"github.com/coder/coder/v2/testutil"
5050
)
5151

52+
func TestReadExternalAuthProvidersFromEnv(t *testing.T) {
53+
t.Parallel()
54+
t.Run("Valid", func(t *testing.T) {
55+
t.Parallel()
56+
providers, err := cli.ReadExternalAuthProvidersFromEnv([]string{
57+
"CODER_EXTERNAL_AUTH_0_ID=1",
58+
"CODER_EXTERNAL_AUTH_0_TYPE=gitlab",
59+
"CODER_EXTERNAL_AUTH_1_ID=2",
60+
"CODER_EXTERNAL_AUTH_1_CLIENT_ID=sid",
61+
"CODER_EXTERNAL_AUTH_1_CLIENT_SECRET=hunter12",
62+
"CODER_EXTERNAL_AUTH_1_TOKEN_URL=google.com",
63+
"CODER_EXTERNAL_AUTH_1_VALIDATE_URL=bing.com",
64+
"CODER_EXTERNAL_AUTH_1_SCOPES=repo:read repo:write",
65+
"CODER_EXTERNAL_AUTH_1_NO_REFRESH=true",
66+
"CODER_EXTERNAL_AUTH_1_DISPLAY_NAME=Google",
67+
"CODER_EXTERNAL_AUTH_1_DISPLAY_ICON=/icon/google.svg",
68+
})
69+
require.NoError(t, err)
70+
require.Len(t, providers, 2)
71+
72+
// Validate the first provider.
73+
assert.Equal(t, "1", providers[0].ID)
74+
assert.Equal(t, "gitlab", providers[0].Type)
75+
76+
// Validate the second provider.
77+
assert.Equal(t, "2", providers[1].ID)
78+
assert.Equal(t, "sid", providers[1].ClientID)
79+
assert.Equal(t, "hunter12", providers[1].ClientSecret)
80+
assert.Equal(t, "google.com", providers[1].TokenURL)
81+
assert.Equal(t, "bing.com", providers[1].ValidateURL)
82+
assert.Equal(t, []string{"repo:read", "repo:write"}, providers[1].Scopes)
83+
assert.Equal(t, true, providers[1].NoRefresh)
84+
assert.Equal(t, "Google", providers[1].DisplayName)
85+
assert.Equal(t, "/icon/google.svg", providers[1].DisplayIcon)
86+
})
87+
}
88+
89+
// TestReadGitAuthProvidersFromEnv ensures that the deprecated `CODER_GITAUTH_`
90+
// environment variables are still supported.
5291
func TestReadGitAuthProvidersFromEnv(t *testing.T) {
5392
t.Parallel()
5493
t.Run("Empty", func(t *testing.T) {
5594
t.Parallel()
56-
providers, err := cli.ReadGitAuthProvidersFromEnv([]string{
95+
providers, err := cli.ReadExternalAuthProvidersFromEnv([]string{
5796
"HOME=/home/frodo",
5897
})
5998
require.NoError(t, err)
6099
require.Empty(t, providers)
61100
})
62101
t.Run("InvalidKey", func(t *testing.T) {
63102
t.Parallel()
64-
providers, err := cli.ReadGitAuthProvidersFromEnv([]string{
103+
providers, err := cli.ReadExternalAuthProvidersFromEnv([]string{
65104
"CODER_GITAUTH_XXX=invalid",
66105
})
67106
require.Error(t, err, "providers: %+v", providers)
68107
require.Empty(t, providers)
69108
})
70109
t.Run("SkipKey", func(t *testing.T) {
71110
t.Parallel()
72-
providers, err := cli.ReadGitAuthProvidersFromEnv([]string{
111+
providers, err := cli.ReadExternalAuthProvidersFromEnv([]string{
73112
"CODER_GITAUTH_0_ID=invalid",
74113
"CODER_GITAUTH_2_ID=invalid",
75114
})
@@ -78,7 +117,7 @@ func TestReadGitAuthProvidersFromEnv(t *testing.T) {
78117
})
79118
t.Run("Valid", func(t *testing.T) {
80119
t.Parallel()
81-
providers, err := cli.ReadGitAuthProvidersFromEnv([]string{
120+
providers, err := cli.ReadExternalAuthProvidersFromEnv([]string{
82121
"CODER_GITAUTH_0_ID=1",
83122
"CODER_GITAUTH_0_TYPE=gitlab",
84123
"CODER_GITAUTH_1_ID=2",

cmd/cliui/main.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -331,17 +331,17 @@ func main() {
331331
// Complete the auth!
332332
gitlabAuthed.Store(true)
333333
}()
334-
return cliui.GitAuth(inv.Context(), inv.Stdout, cliui.GitAuthOptions{
334+
return cliui.ExternalAuth(inv.Context(), inv.Stdout, cliui.ExternalAuthOptions{
335335
Fetch: func(ctx context.Context) ([]codersdk.TemplateVersionExternalAuth, error) {
336336
count.Add(1)
337337
return []codersdk.TemplateVersionExternalAuth{{
338338
ID: "github",
339-
Type: codersdk.ExternalAuthProviderGitHub,
339+
Type: codersdk.EnhancedExternalAuthProviderGitHub.String(),
340340
Authenticated: githubAuthed.Load(),
341341
AuthenticateURL: "https://example.com/gitauth/github?redirect=" + url.QueryEscape("/gitauth?notify"),
342342
}, {
343343
ID: "gitlab",
344-
Type: codersdk.ExternalAuthProviderGitLab,
344+
Type: codersdk.EnhancedExternalAuthProviderGitLab.String(),
345345
Authenticated: gitlabAuthed.Load(),
346346
AuthenticateURL: "https://example.com/gitauth/gitlab?redirect=" + url.QueryEscape("/gitauth?notify"),
347347
}}, nil

0 commit comments

Comments
 (0)