Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

feat: Login via CLI#298

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 ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Merged
bryphe-coder merged 19 commits intomainfrombryphe/feat/210/cli-login
Feb 18, 2022
Merged
Show file tree
Hide file tree
Changes fromall commits
Commits
Show all changes
19 commits
Select commitHold shift + click to select a range
6c50b85
Create initial route for generating an API key
bryphe-coderFeb 16, 2022
fe55c73
Add cli-auth page
bryphe-coderFeb 16, 2022
3bb3164
Fix lint warnings
bryphe-coderFeb 16, 2022
9ec938e
Add login prompt for non-first-time-user case
bryphe-coderFeb 16, 2022
87fc9e2
Add test case for login
bryphe-coderFeb 17, 2022
6607a5a
Factor out CliAuth token component
bryphe-coderFeb 17, 2022
0e323ce
Add storybook for CliAuthToken
bryphe-coderFeb 17, 2022
a33d2c7
Merge main
bryphe-coderFeb 17, 2022
9cc7e94
Add test case for CliAuthToken
bryphe-coderFeb 17, 2022
7774e29
Add codersdk function + test
bryphe-coderFeb 17, 2022
8ea2920
Fix lint issues
bryphe-coderFeb 17, 2022
669f2ca
Discard output of browser pkg
bryphe-coderFeb 17, 2022
0a43941
Merge main
bryphe-coderFeb 17, 2022
5ecba6d
Add mask for session token prompt
bryphe-coderFeb 17, 2022
13696a7
Inline saveSessionToken into both code paths
bryphe-coderFeb 17, 2022
dccb009
Move setting browser.Stderr/Stdout to init
bryphe-coderFeb 17, 2022
ca1a458
Change route /api-keys -> /users/{user}/keys
bryphe-coderFeb 17, 2022
b232d0c
Remove leftover saveSessionToken func
bryphe-coderFeb 17, 2022
0d829b3
Update route in UI for getting new token
bryphe-coderFeb 18, 2022
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
98 changes: 97 additions & 1 deletioncli/login.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,20 +2,39 @@ package cli

import (
"fmt"
"io/ioutil"
"net/url"
"os/exec"
"os/user"
"runtime"
"strings"

"github.com/fatih/color"
"github.com/go-playground/validator/v10"
"github.com/manifoldco/promptui"
"github.com/pkg/browser"
"github.com/spf13/cobra"
"golang.org/x/xerrors"

"github.com/coder/coder/coderd"
"github.com/coder/coder/codersdk"
)

const (
goosWindows = "windows"
goosDarwin = "darwin"
)

func init() {
// Hide output from the browser library,
// otherwise we can get really verbose and non-actionable messages
// when in SSH or another type of headless session
// NOTE: This needs to be in `init` to prevent data races
// (multiple threads trying to set the global browser.Std* variables)
browser.Stderr = ioutil.Discard
browser.Stdout = ioutil.Discard
}

func login() *cobra.Command {
return &cobra.Command{
Use: "login <url>",
Expand DownExpand Up@@ -116,8 +135,10 @@ func login() *cobra.Command {
if err != nil {
return xerrors.Errorf("login with password: %w", err)
}

sessionToken := resp.SessionToken
config := createConfig(cmd)
err = config.Session().Write(resp.SessionToken)
err = config.Session().Write(sessionToken)
if err != nil {
return xerrors.Errorf("write session token: %w", err)
}
Expand All@@ -130,7 +151,82 @@ func login() *cobra.Command {
return nil
}

authURL := *serverURL
authURL.Path = serverURL.Path + "/cli-auth"
if err := openURL(authURL.String()); err != nil {
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "Open the following in your browser:\n\n\t%s\n\n", authURL.String())
} else {
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "Your browser has been opened to visit:\n\n\t%s\n\n", authURL.String())
}

sessionToken, err := prompt(cmd, &promptui.Prompt{
Label: "Paste your token here:",
Mask: '*',
Validate: func(token string) error {
client.SessionToken = token
_, err := client.User(cmd.Context(), "me")
if err != nil {
return xerrors.New("That's not a valid token!")
}
return err
},
})
if err != nil {
return xerrors.Errorf("paste token prompt: %w", err)
}

// Login to get user data - verify it is OK before persisting
client.SessionToken = sessionToken
resp, err := client.User(cmd.Context(), "me")
if err != nil {
return xerrors.Errorf("get user: %w", err)
}

config := createConfig(cmd)
err = config.Session().Write(sessionToken)
if err != nil {
return xerrors.Errorf("write session token: %w", err)
}
err = config.URL().Write(serverURL.String())
if err != nil {
return xerrors.Errorf("write server url: %w", err)
}

_, _ = fmt.Fprintf(cmd.OutOrStdout(), "%s Welcome to Coder, %s! You're authenticated.\n", color.HiBlackString(">"), color.HiCyanString(resp.Username))
return nil
},
}
}

// isWSL determines if coder-cli is running within Windows Subsystem for Linux
func isWSL() (bool, error) {
if runtime.GOOS == goosDarwin || runtime.GOOS == goosWindows {
return false, nil
}
data, err := ioutil.ReadFile("/proc/version")
if err != nil {
return false, xerrors.Errorf("read /proc/version: %w", err)
}
return strings.Contains(strings.ToLower(string(data)), "microsoft"), nil
}

// openURL opens the provided URL via user's default browser
func openURL(urlToOpen string) error {
var cmd string
var args []string

wsl, err := isWSL()
if err != nil {
return xerrors.Errorf("test running Windows Subsystem for Linux: %w", err)
}

if wsl {
cmd = "cmd.exe"
args = []string{"/c", "start"}
urlToOpen = strings.ReplaceAll(urlToOpen, "&", "^&")
args = append(args, urlToOpen)
return exec.Command(cmd, args...).Start()
}

return browser.OpenURL(urlToOpen)
}
58 changes: 58 additions & 0 deletionscli/login_test.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
package cli_test

import (
"context"
"testing"

"github.com/stretchr/testify/require"

"github.com/coder/coder/cli/clitest"
"github.com/coder/coder/coderd"
"github.com/coder/coder/coderd/coderdtest"
"github.com/coder/coder/pty/ptytest"
)
Expand DownExpand Up@@ -50,4 +52,60 @@ func TestLogin(t *testing.T) {
}
pty.ExpectMatch("Welcome to Coder")
})

t.Run("ExistingUserValidTokenTTY", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t)
_, err := client.CreateInitialUser(context.Background(), coderd.CreateInitialUserRequest{
Username: "test-user",
Email: "test-user@coder.com",
Organization: "acme-corp",
Password: "password",
})
require.NoError(t, err)
token, err := client.LoginWithPassword(context.Background(), coderd.LoginWithPasswordRequest{
Email: "test-user@coder.com",
Password: "password",
})
require.NoError(t, err)

root, _ := clitest.New(t, "login", client.URL.String(), "--force-tty")
pty := ptytest.New(t)
root.SetIn(pty.Input())
root.SetOut(pty.Output())
go func() {
err := root.Execute()
require.NoError(t, err)
}()

pty.ExpectMatch("Paste your token here:")
pty.WriteLine(token.SessionToken)
pty.ExpectMatch("Welcome to Coder")
})

t.Run("ExistingUserInvalidTokenTTY", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t)
_, err := client.CreateInitialUser(context.Background(), coderd.CreateInitialUserRequest{
Username: "test-user",
Email: "test-user@coder.com",
Organization: "acme-corp",
Password: "password",
})
require.NoError(t, err)

root, _ := clitest.New(t, "login", client.URL.String(), "--force-tty")
pty := ptytest.New(t)
root.SetIn(pty.Input())
root.SetOut(pty.Output())
go func() {
err := root.Execute()
// An error is expected in this case, since the login wasn't successful:
require.Error(t, err)
}()

pty.ExpectMatch("Paste your token here:")
pty.WriteLine("an-invalid-token")
pty.ExpectMatch("That's not a valid token!")
})
}
9 changes: 6 additions & 3 deletionscoderd/coderd.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -36,6 +36,7 @@ func New(options *Options) http.Handler {
})
r.Post("/login", api.postLogin)
r.Post("/logout", api.postLogout)

// Used for setup.
r.Get("/user", api.user)
r.Post("/user", api.postUser)
Expand All@@ -44,10 +45,12 @@ func New(options *Options) http.Handler {
httpmw.ExtractAPIKey(options.Database, nil),
)
r.Post("/", api.postUsers)
r.Group(func(r chi.Router) {

r.Route("/{user}", func(r chi.Router) {
r.Use(httpmw.ExtractUserParam(options.Database))
r.Get("/{user}", api.userByName)
r.Get("/{user}/organizations", api.organizationsByUser)
r.Get("/", api.userByName)
r.Get("/organizations", api.organizationsByUser)
r.Post("/keys", api.postKeyForUser)
})
})
r.Route("/projects", func(r chi.Router) {
Expand Down
49 changes: 49 additions & 0 deletionscoderd/users.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -55,6 +55,11 @@ type LoginWithPasswordResponse struct {
SessionToken string `json:"session_token" validate:"required"`
}

// GenerateAPIKeyResponse contains an API key for a user.
type GenerateAPIKeyResponse struct {
Key string `json:"key"`
}

// Returns whether the initial user has been created or not.
func (api *api) user(rw http.ResponseWriter, r *http.Request) {
userCount, err := api.Database.GetUserCount(r.Context())
Expand DownExpand Up@@ -312,6 +317,50 @@ func (api *api) postLogin(rw http.ResponseWriter, r *http.Request) {
})
}

// Creates a new session key, used for logging in via the CLI
func (api *api) postKeyForUser(rw http.ResponseWriter, r *http.Request) {
user := httpmw.UserParam(r)
apiKey := httpmw.APIKey(r)

if user.ID != apiKey.UserID {
httpapi.Write(rw, http.StatusUnauthorized, httpapi.Response{
Message: "Keys can only be generated for the authenticated user",
})
return
}

keyID, keySecret, err := generateAPIKeyIDSecret()
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("generate api key parts: %s", err.Error()),
})
return
}
hashed := sha256.Sum256([]byte(keySecret))

_, err = api.Database.InsertAPIKey(r.Context(), database.InsertAPIKeyParams{
ID: keyID,
UserID: apiKey.UserID,
ExpiresAt: database.Now().AddDate(1, 0, 0), // Expire after 1 year (same as v1)
CreatedAt: database.Now(),
UpdatedAt: database.Now(),
HashedSecret: hashed[:],
LoginType: database.LoginTypeBuiltIn,
})
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("insert api key: %s", err.Error()),
})
return
}

// This format is consumed by the APIKey middleware.
generatedAPIKey := fmt.Sprintf("%s-%s", keyID, keySecret)

render.Status(r, http.StatusCreated)
render.JSON(rw, r, GenerateAPIKeyResponse{Key: generatedAPIKey})
}

// Clear the user's session cookie
func (*api) postLogout(rw http.ResponseWriter, r *http.Request) {
// Get a blank token cookie
Expand Down
27 changes: 27 additions & 0 deletionscoderd/users_test.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -119,6 +119,33 @@ func TestOrganizationsByUser(t *testing.T) {
require.Len(t, orgs, 1)
}

func TestPostKey(t *testing.T) {
t.Parallel()
t.Run("InvalidUser", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t)
_ = coderdtest.CreateInitialUser(t, client)

// Clear session token
client.SessionToken = ""
// ...and request an API key
_, err := client.CreateAPIKey(context.Background())
var apiErr *codersdk.Error
require.ErrorAs(t, err, &apiErr)
require.Equal(t, http.StatusUnauthorized, apiErr.StatusCode())
})

t.Run("Success", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t)
_ = coderdtest.CreateInitialUser(t, client)
apiKey, err := client.CreateAPIKey(context.Background())
require.NotNil(t, apiKey)
require.GreaterOrEqual(t, len(apiKey.Key), 2)
require.NoError(t, err)
})
}

func TestPostLogin(t *testing.T) {
t.Parallel()
t.Run("InvalidUser", func(t *testing.T) {
Expand Down
14 changes: 14 additions & 0 deletionscodersdk/users.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -56,6 +56,20 @@ func (c *Client) CreateUser(ctx context.Context, req coderd.CreateUserRequest) (
return user, json.NewDecoder(res.Body).Decode(&user)
}

// CreateAPIKey calls the /api-key API
func (c *Client) CreateAPIKey(ctx context.Context) (*coderd.GenerateAPIKeyResponse, error) {
res, err := c.request(ctx, http.MethodPost, "/api/v2/users/me/keys", nil)
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode > http.StatusCreated {
return nil, readBodyAsError(res)
}
apiKey := &coderd.GenerateAPIKeyResponse{}
return apiKey, json.NewDecoder(res.Body).Decode(apiKey)
}

// LoginWithPassword creates a session token authenticating with an email and password.
// Call `SetSessionToken()` to apply the newly acquired token to the client.
func (c *Client) LoginWithPassword(ctx context.Context, req coderd.LoginWithPasswordRequest) (coderd.LoginWithPasswordResponse, error) {
Expand Down
1 change: 1 addition & 0 deletionsgo.mod
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -115,6 +115,7 @@ require (
github.com/pion/stun v0.3.5 // indirect
github.com/pion/turn/v2 v2.0.6 // indirect
github.com/pion/udp v0.1.1 // indirect
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/sirupsen/logrus v1.8.1 // indirect
Expand Down
1 change: 1 addition & 0 deletionsgo.sum
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -1062,6 +1062,7 @@ github.com/pion/udp v0.1.1/go.mod h1:6AFo+CMdKQm7UiA0eUPA8/eVCTx8jBIITLZHc9DWX5M
github.com/pion/webrtc/v3 v3.1.23 h1:suyNiF9o2/6SBsyWA1UweraUWYkaHCNJdt/16b61I5w=
github.com/pion/webrtc/v3 v3.1.23/go.mod h1:L5S/oAhL0Fzt/rnftVQRrP80/j5jygY7XRZzWwFx6P4=
github.com/pkg/browser v0.0.0-20210706143420-7d21f8c997e2/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU=
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
Expand Down
13 changes: 13 additions & 0 deletionssite/api.ts
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -139,3 +139,16 @@ export const logout = async (): Promise<void> => {

return
}

export const getApiKey = async (): Promise<{ key: string }> => {
const response = await fetch("/api/v2/users/me/keys", {
method: "POST",
})

if (!response.ok) {
const body = await response.json()
throw new Error(body.message)
}

return await response.json()
}
Loading

[8]ページ先頭

©2009-2025 Movatter.jp