Skip to content

feat: add external-auth cli #10052

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Oct 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 91 additions & 0 deletions cli/externalauth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package cli

import (
"os/signal"

"golang.org/x/xerrors"

"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/codersdk/agentsdk"
)

func (r *RootCmd) externalAuth() *clibase.Cmd {
return &clibase.Cmd{
Use: "external-auth",
Short: "Manage external authentication",
Long: "Authenticate with external services inside of a workspace.",
Handler: func(i *clibase.Invocation) error {
return i.Command.HelpHandler(i)
},
Children: []*clibase.Cmd{
r.externalAuthAccessToken(),
},
}
}

func (r *RootCmd) externalAuthAccessToken() *clibase.Cmd {
var silent bool
return &clibase.Cmd{
Use: "access-token <provider>",
Short: "Print auth for an external provider",
Long: "Print an access-token for an external auth provider. " +
"The access-token will be validated and sent to stdout with exit code 0. " +
"If a valid access-token cannot be obtained, the URL to authenticate will be sent to stdout with exit code 1\n" + formatExamples(
example{
Description: "Ensure that the user is authenticated with GitHub before cloning.",
Command: `#!/usr/bin/env sh

OUTPUT=$(coder external-auth access-token github)
if [ $? -eq 0 ]; then
echo "Authenticated with GitHub"
else
echo "Please authenticate with GitHub:"
echo $OUTPUT
fi
`,
},
),
Options: clibase.OptionSet{{
Name: "Silent",
Flag: "s",
Description: "Do not print the URL or access token.",
Value: clibase.BoolOf(&silent),
}},

Handler: func(inv *clibase.Invocation) error {
ctx := inv.Context()

ctx, stop := signal.NotifyContext(ctx, InterruptSignals...)
defer stop()

client, err := r.createAgentClient()
if err != nil {
return xerrors.Errorf("create agent client: %w", err)
}

token, err := client.ExternalAuth(ctx, agentsdk.ExternalAuthRequest{
ID: inv.Args[0],
})
if err != nil {
return xerrors.Errorf("get external auth token: %w", err)
}

if !silent {
if token.URL != "" {
_, err = inv.Stdout.Write([]byte(token.URL))
} else {
_, err = inv.Stdout.Write([]byte(token.AccessToken))
}
if err != nil {
return err
}
}

if token.URL != "" {
return cliui.Canceled
}
return nil
},
}
}
49 changes: 49 additions & 0 deletions cli/externalauth_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package cli_test

import (
"context"
"net/http"
"net/http/httptest"
"testing"

"github.com/coder/coder/v2/cli/clitest"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/coderd/httpapi"
"github.com/coder/coder/v2/codersdk/agentsdk"
"github.com/coder/coder/v2/pty/ptytest"
)

func TestExternalAuth(t *testing.T) {
t.Parallel()
t.Run("CanceledWithURL", func(t *testing.T) {
t.Parallel()
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
httpapi.Write(context.Background(), w, http.StatusOK, agentsdk.ExternalAuthResponse{
URL: "https://github.com",
})
}))
t.Cleanup(srv.Close)
url := srv.URL
inv, _ := clitest.New(t, "--agent-url", url, "external-auth", "access-token", "github")
pty := ptytest.New(t)
inv.Stdout = pty.Output()
waiter := clitest.StartWithWaiter(t, inv)
pty.ExpectMatch("https://github.com")
waiter.RequireIs(cliui.Canceled)
})
t.Run("SuccessWithToken", func(t *testing.T) {
t.Parallel()
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
httpapi.Write(context.Background(), w, http.StatusOK, agentsdk.ExternalAuthResponse{
AccessToken: "bananas",
})
}))
t.Cleanup(srv.Close)
url := srv.URL
inv, _ := clitest.New(t, "--agent-url", url, "external-auth", "access-token", "github")
pty := ptytest.New(t)
inv.Stdout = pty.Output()
clitest.Start(t, inv)
pty.ExpectMatch("bananas")
})
}
10 changes: 8 additions & 2 deletions cli/gitaskpass.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/cli/gitauth"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/codersdk/agentsdk"
"github.com/coder/retry"
)

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

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

for r := retry.New(250*time.Millisecond, 10*time.Second); r.Wait(ctx); {
token, err = client.GitAuth(ctx, host, true)
token, err = client.ExternalAuth(ctx, agentsdk.ExternalAuthRequest{
Match: host,
Listen: true,
})
if err != nil {
continue
}
Expand Down
8 changes: 4 additions & 4 deletions cli/gitaskpass_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ func TestGitAskpass(t *testing.T) {
t.Run("UsernameAndPassword", func(t *testing.T) {
t.Parallel()
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
httpapi.Write(context.Background(), w, http.StatusOK, agentsdk.GitAuthResponse{
httpapi.Write(context.Background(), w, http.StatusOK, agentsdk.ExternalAuthResponse{
Username: "something",
Password: "bananas",
})
Expand Down Expand Up @@ -65,8 +65,8 @@ func TestGitAskpass(t *testing.T) {

t.Run("Poll", func(t *testing.T) {
t.Parallel()
resp := atomic.Pointer[agentsdk.GitAuthResponse]{}
resp.Store(&agentsdk.GitAuthResponse{
resp := atomic.Pointer[agentsdk.ExternalAuthResponse]{}
resp.Store(&agentsdk.ExternalAuthResponse{
URL: "https://something.org",
})
poll := make(chan struct{}, 10)
Expand Down Expand Up @@ -96,7 +96,7 @@ func TestGitAskpass(t *testing.T) {
}()
<-poll
stderr.ExpectMatch("Open the following URL to authenticate")
resp.Store(&agentsdk.GitAuthResponse{
resp.Store(&agentsdk.ExternalAuthResponse{
Username: "username",
Password: "password",
})
Expand Down
1 change: 1 addition & 0 deletions cli/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ func (r *RootCmd) Core() []*clibase.Cmd {
// Please re-sort this list alphabetically if you change it!
return []*clibase.Cmd{
r.dotfiles(),
r.externalAuth(),
r.login(),
r.logout(),
r.netcheck(),
Expand Down
1 change: 1 addition & 0 deletions cli/testdata/coder_--help.golden
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ SUBCOMMANDS:
delete Delete a workspace
dotfiles Personalize your workspace by applying a canonical
dotfiles repository
external-auth Manage external authentication
list List workspaces
login Authenticate with Coder deployment
logout Unauthenticate your local session
Expand Down
14 changes: 14 additions & 0 deletions cli/testdata/coder_external-auth_--help.golden
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
coder v0.0.0-devel

USAGE:
coder external-auth

Manage external authentication

Authenticate with external services inside of a workspace.

SUBCOMMANDS:
access-token Print auth for an external provider

———
Run `coder --help` for a list of global options.
28 changes: 28 additions & 0 deletions cli/testdata/coder_external-auth_access-token_--help.golden
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
coder v0.0.0-devel

USAGE:
coder external-auth access-token [flags] <provider>

Print auth for an external provider

Print an access-token for an external auth provider. The access-token will be
validated and sent to stdout with exit code 0. If a valid access-token cannot
be obtained, the URL to authenticate will be sent to stdout with exit code 1
- Ensure that the user is authenticated with GitHub before cloning.:

$ #!/usr/bin/env sh

OUTPUT=$(coder external-auth access-token github)
if [ $? -eq 0 ]; then
echo "Authenticated with GitHub"
else
echo "Please authenticate with GitHub:"
echo $OUTPUT
fi

OPTIONS:
--s bool
Do not print the URL or access token.

———
Run `coder --help` for a list of global options.
74 changes: 67 additions & 7 deletions coderd/apidoc/docs.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading