mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
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
This commit is contained in:
@@ -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
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
})
|
||||
}
|
||||
+8
-2
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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 {
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
})
|
||||
@@ -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)
|
||||
@@ -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",
|
||||
})
|
||||
|
||||
@@ -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(),
|
||||
|
||||
Vendored
+1
@@ -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
|
||||
|
||||
+14
@@ -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.
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user