Skip to content

Commit 91a2025

Browse files
kylecarbsEmyrk
authored andcommitted
feat: add external-auth cli (#10052)
* feat: add `external-auth` cli * Add subcommands * Improve descriptions * Add external-auth subcommand * Fix docs * Fix gen * Fix comment * Fix golden file
1 parent 02bcd20 commit 91a2025

22 files changed

+611
-103
lines changed

cli/externalauth.go

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
package cli
2+
3+
import (
4+
"os/signal"
5+
6+
"golang.org/x/xerrors"
7+
8+
"github.com/coder/coder/v2/cli/clibase"
9+
"github.com/coder/coder/v2/cli/cliui"
10+
"github.com/coder/coder/v2/codersdk/agentsdk"
11+
)
12+
13+
func (r *RootCmd) externalAuth() *clibase.Cmd {
14+
return &clibase.Cmd{
15+
Use: "external-auth",
16+
Short: "Manage external authentication",
17+
Long: "Authenticate with external services inside of a workspace.",
18+
Handler: func(i *clibase.Invocation) error {
19+
return i.Command.HelpHandler(i)
20+
},
21+
Children: []*clibase.Cmd{
22+
r.externalAuthAccessToken(),
23+
},
24+
}
25+
}
26+
27+
func (r *RootCmd) externalAuthAccessToken() *clibase.Cmd {
28+
var silent bool
29+
return &clibase.Cmd{
30+
Use: "access-token <provider>",
31+
Short: "Print auth for an external provider",
32+
Long: "Print an access-token for an external auth provider. " +
33+
"The access-token will be validated and sent to stdout with exit code 0. " +
34+
"If a valid access-token cannot be obtained, the URL to authenticate will be sent to stdout with exit code 1\n" + formatExamples(
35+
example{
36+
Description: "Ensure that the user is authenticated with GitHub before cloning.",
37+
Command: `#!/usr/bin/env sh
38+
39+
OUTPUT=$(coder external-auth access-token github)
40+
if [ $? -eq 0 ]; then
41+
echo "Authenticated with GitHub"
42+
else
43+
echo "Please authenticate with GitHub:"
44+
echo $OUTPUT
45+
fi
46+
`,
47+
},
48+
),
49+
Options: clibase.OptionSet{{
50+
Name: "Silent",
51+
Flag: "s",
52+
Description: "Do not print the URL or access token.",
53+
Value: clibase.BoolOf(&silent),
54+
}},
55+
56+
Handler: func(inv *clibase.Invocation) error {
57+
ctx := inv.Context()
58+
59+
ctx, stop := signal.NotifyContext(ctx, InterruptSignals...)
60+
defer stop()
61+
62+
client, err := r.createAgentClient()
63+
if err != nil {
64+
return xerrors.Errorf("create agent client: %w", err)
65+
}
66+
67+
token, err := client.ExternalAuth(ctx, agentsdk.ExternalAuthRequest{
68+
ID: inv.Args[0],
69+
})
70+
if err != nil {
71+
return xerrors.Errorf("get external auth token: %w", err)
72+
}
73+
74+
if !silent {
75+
if token.URL != "" {
76+
_, err = inv.Stdout.Write([]byte(token.URL))
77+
} else {
78+
_, err = inv.Stdout.Write([]byte(token.AccessToken))
79+
}
80+
if err != nil {
81+
return err
82+
}
83+
}
84+
85+
if token.URL != "" {
86+
return cliui.Canceled
87+
}
88+
return nil
89+
},
90+
}
91+
}

cli/externalauth_test.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package cli_test
2+
3+
import (
4+
"context"
5+
"net/http"
6+
"net/http/httptest"
7+
"testing"
8+
9+
"github.com/coder/coder/v2/cli/clitest"
10+
"github.com/coder/coder/v2/cli/cliui"
11+
"github.com/coder/coder/v2/coderd/httpapi"
12+
"github.com/coder/coder/v2/codersdk/agentsdk"
13+
"github.com/coder/coder/v2/pty/ptytest"
14+
)
15+
16+
func TestExternalAuth(t *testing.T) {
17+
t.Parallel()
18+
t.Run("CanceledWithURL", func(t *testing.T) {
19+
t.Parallel()
20+
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
21+
httpapi.Write(context.Background(), w, http.StatusOK, agentsdk.ExternalAuthResponse{
22+
URL: "https://github.com",
23+
})
24+
}))
25+
t.Cleanup(srv.Close)
26+
url := srv.URL
27+
inv, _ := clitest.New(t, "--agent-url", url, "external-auth", "access-token", "github")
28+
pty := ptytest.New(t)
29+
inv.Stdout = pty.Output()
30+
waiter := clitest.StartWithWaiter(t, inv)
31+
pty.ExpectMatch("https://github.com")
32+
waiter.RequireIs(cliui.Canceled)
33+
})
34+
t.Run("SuccessWithToken", func(t *testing.T) {
35+
t.Parallel()
36+
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
37+
httpapi.Write(context.Background(), w, http.StatusOK, agentsdk.ExternalAuthResponse{
38+
AccessToken: "bananas",
39+
})
40+
}))
41+
t.Cleanup(srv.Close)
42+
url := srv.URL
43+
inv, _ := clitest.New(t, "--agent-url", url, "external-auth", "access-token", "github")
44+
pty := ptytest.New(t)
45+
inv.Stdout = pty.Output()
46+
clitest.Start(t, inv)
47+
pty.ExpectMatch("bananas")
48+
})
49+
}

cli/gitaskpass.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"github.com/coder/coder/v2/cli/cliui"
1414
"github.com/coder/coder/v2/cli/gitauth"
1515
"github.com/coder/coder/v2/codersdk"
16+
"github.com/coder/coder/v2/codersdk/agentsdk"
1617
"github.com/coder/retry"
1718
)
1819

@@ -38,7 +39,9 @@ func (r *RootCmd) gitAskpass() *clibase.Cmd {
3839
return xerrors.Errorf("create agent client: %w", err)
3940
}
4041

41-
token, err := client.GitAuth(ctx, host, false)
42+
token, err := client.ExternalAuth(ctx, agentsdk.ExternalAuthRequest{
43+
Match: host,
44+
})
4245
if err != nil {
4346
var apiError *codersdk.Error
4447
if errors.As(err, &apiError) && apiError.StatusCode() == http.StatusNotFound {
@@ -63,7 +66,10 @@ func (r *RootCmd) gitAskpass() *clibase.Cmd {
6366
}
6467

6568
for r := retry.New(250*time.Millisecond, 10*time.Second); r.Wait(ctx); {
66-
token, err = client.GitAuth(ctx, host, true)
69+
token, err = client.ExternalAuth(ctx, agentsdk.ExternalAuthRequest{
70+
Match: host,
71+
Listen: true,
72+
})
6773
if err != nil {
6874
continue
6975
}

cli/gitaskpass_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ func TestGitAskpass(t *testing.T) {
2323
t.Run("UsernameAndPassword", func(t *testing.T) {
2424
t.Parallel()
2525
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
26-
httpapi.Write(context.Background(), w, http.StatusOK, agentsdk.GitAuthResponse{
26+
httpapi.Write(context.Background(), w, http.StatusOK, agentsdk.ExternalAuthResponse{
2727
Username: "something",
2828
Password: "bananas",
2929
})
@@ -65,8 +65,8 @@ func TestGitAskpass(t *testing.T) {
6565

6666
t.Run("Poll", func(t *testing.T) {
6767
t.Parallel()
68-
resp := atomic.Pointer[agentsdk.GitAuthResponse]{}
69-
resp.Store(&agentsdk.GitAuthResponse{
68+
resp := atomic.Pointer[agentsdk.ExternalAuthResponse]{}
69+
resp.Store(&agentsdk.ExternalAuthResponse{
7070
URL: "https://something.org",
7171
})
7272
poll := make(chan struct{}, 10)
@@ -96,7 +96,7 @@ func TestGitAskpass(t *testing.T) {
9696
}()
9797
<-poll
9898
stderr.ExpectMatch("Open the following URL to authenticate")
99-
resp.Store(&agentsdk.GitAuthResponse{
99+
resp.Store(&agentsdk.ExternalAuthResponse{
100100
Username: "username",
101101
Password: "password",
102102
})

cli/root.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ func (r *RootCmd) Core() []*clibase.Cmd {
8383
// Please re-sort this list alphabetically if you change it!
8484
return []*clibase.Cmd{
8585
r.dotfiles(),
86+
r.externalAuth(),
8687
r.login(),
8788
r.logout(),
8889
r.netcheck(),

cli/testdata/coder_--help.golden

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ SUBCOMMANDS:
2020
delete Delete a workspace
2121
dotfiles Personalize your workspace by applying a canonical
2222
dotfiles repository
23+
external-auth Manage external authentication
2324
list List workspaces
2425
login Authenticate with Coder deployment
2526
logout Unauthenticate your local session
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
coder v0.0.0-devel
2+
3+
USAGE:
4+
coder external-auth
5+
6+
Manage external authentication
7+
8+
Authenticate with external services inside of a workspace.
9+
10+
SUBCOMMANDS:
11+
access-token Print auth for an external provider
12+
13+
———
14+
Run `coder --help` for a list of global options.
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
coder v0.0.0-devel
2+
3+
USAGE:
4+
coder external-auth access-token [flags] <provider>
5+
6+
Print auth for an external provider
7+
8+
Print an access-token for an external auth provider. The access-token will be
9+
validated and sent to stdout with exit code 0. If a valid access-token cannot
10+
be obtained, the URL to authenticate will be sent to stdout with exit code 1
11+
- Ensure that the user is authenticated with GitHub before cloning.:
12+
13+
$ #!/usr/bin/env sh
14+
15+
OUTPUT=$(coder external-auth access-token github)
16+
if [ $? -eq 0 ]; then
17+
echo "Authenticated with GitHub"
18+
else
19+
echo "Please authenticate with GitHub:"
20+
echo $OUTPUT
21+
fi
22+
23+
OPTIONS:
24+
--s bool
25+
Do not print the URL or access token.
26+
27+
———
28+
Run `coder --help` for a list of global options.

coderd/apidoc/docs.go

Lines changed: 67 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)