Skip to content

Commit f15d298

Browse files
authored
Merge branch 'main' into matifali/devcontainer
2 parents 31b29fe + 9a7705c commit f15d298

File tree

7 files changed

+125
-38
lines changed

7 files changed

+125
-38
lines changed

cli/login.go

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -41,16 +41,18 @@ func (r *RootCmd) login() *clibase.Cmd {
4141
const firstUserTrialEnv = "CODER_FIRST_USER_TRIAL"
4242

4343
var (
44-
email string
45-
username string
46-
password string
47-
trial bool
44+
email string
45+
username string
46+
password string
47+
trial bool
48+
useTokenForSession bool
4849
)
4950
cmd := &clibase.Cmd{
5051
Use: "login <url>",
5152
Short: "Authenticate with Coder deployment",
5253
Middleware: clibase.RequireRangeArgs(0, 1),
5354
Handler: func(inv *clibase.Invocation) error {
55+
ctx := inv.Context()
5456
rawURL := ""
5557
if len(inv.Args) == 0 {
5658
rawURL = r.clientURL.String()
@@ -89,7 +91,7 @@ func (r *RootCmd) login() *clibase.Cmd {
8991
_, _ = fmt.Fprintln(inv.Stderr, cliui.DefaultStyles.Warn.Render(err.Error()))
9092
}
9193

92-
hasInitialUser, err := client.HasFirstUser(inv.Context())
94+
hasInitialUser, err := client.HasFirstUser(ctx)
9395
if err != nil {
9496
return xerrors.Errorf("Failed to check server %q for first user, is the URL correct and is coder accessible from your browser? Error - has initial user: %w", serverURL.String(), err)
9597
}
@@ -182,7 +184,7 @@ func (r *RootCmd) login() *clibase.Cmd {
182184
trial = v == "yes" || v == "y"
183185
}
184186

185-
_, err = client.CreateFirstUser(inv.Context(), codersdk.CreateFirstUserRequest{
187+
_, err = client.CreateFirstUser(ctx, codersdk.CreateFirstUserRequest{
186188
Email: email,
187189
Username: username,
188190
Password: password,
@@ -191,7 +193,7 @@ func (r *RootCmd) login() *clibase.Cmd {
191193
if err != nil {
192194
return xerrors.Errorf("create initial user: %w", err)
193195
}
194-
resp, err := client.LoginWithPassword(inv.Context(), codersdk.LoginWithPasswordRequest{
196+
resp, err := client.LoginWithPassword(ctx, codersdk.LoginWithPasswordRequest{
195197
Email: email,
196198
Password: password,
197199
})
@@ -235,7 +237,7 @@ func (r *RootCmd) login() *clibase.Cmd {
235237
Secret: true,
236238
Validate: func(token string) error {
237239
client.SetSessionToken(token)
238-
_, err := client.User(inv.Context(), codersdk.Me)
240+
_, err := client.User(ctx, codersdk.Me)
239241
if err != nil {
240242
return xerrors.New("That's not a valid token!")
241243
}
@@ -245,11 +247,27 @@ func (r *RootCmd) login() *clibase.Cmd {
245247
if err != nil {
246248
return xerrors.Errorf("paste token prompt: %w", err)
247249
}
250+
} else if !useTokenForSession {
251+
// If a session token is provided on the cli, use it to generate
252+
// a new one. This is because the cli `--token` flag provides
253+
// a token for the command being invoked. We should not store
254+
// this token, and `/logout` should not delete it.
255+
// /login should generate a new token and store that.
256+
client.SetSessionToken(sessionToken)
257+
// Use CreateAPIKey over CreateToken because this is a session
258+
// key that should not show on the `tokens` page. This should
259+
// match the same behavior of the `/cli-auth` page for generating
260+
// a session token.
261+
key, err := client.CreateAPIKey(ctx, "me")
262+
if err != nil {
263+
return xerrors.Errorf("create api key: %w", err)
264+
}
265+
sessionToken = key.Key
248266
}
249267

250268
// Login to get user data - verify it is OK before persisting
251269
client.SetSessionToken(sessionToken)
252-
resp, err := client.User(inv.Context(), codersdk.Me)
270+
resp, err := client.User(ctx, codersdk.Me)
253271
if err != nil {
254272
return xerrors.Errorf("get user: %w", err)
255273
}
@@ -293,6 +311,11 @@ func (r *RootCmd) login() *clibase.Cmd {
293311
Description: "Specifies whether a trial license should be provisioned for the Coder deployment or not.",
294312
Value: clibase.BoolOf(&trial),
295313
},
314+
{
315+
Flag: "use-token-as-session",
316+
Description: "By default, the CLI will generate a new session token when logging in. This flag will instead use the provided token as the session token.",
317+
Value: clibase.BoolOf(&useTokenForSession),
318+
},
296319
}
297320
return cmd
298321
}

cli/login_test.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,7 @@ func TestLogin(t *testing.T) {
197197
<-doneChan
198198
})
199199

200+
// TokenFlag should generate a new session token and store it in the session file.
200201
t.Run("TokenFlag", func(t *testing.T) {
201202
t.Parallel()
202203
client := coderdtest.New(t, nil)
@@ -206,6 +207,7 @@ func TestLogin(t *testing.T) {
206207
require.NoError(t, err)
207208
sessionFile, err := cfg.Session().Read()
208209
require.NoError(t, err)
209-
require.Equal(t, client.SessionToken(), sessionFile)
210+
// This **should not be equal** to the token we passed in.
211+
require.NotEqual(t, client.SessionToken(), sessionFile)
210212
})
211213
}

cli/testdata/coder_login_--help.golden

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,5 +19,9 @@ Authenticate with Coder deployment
1919
Specifies a username to use if creating the first user for the
2020
deployment.
2121

22+
--use-token-as-session bool
23+
By default, the CLI will generate a new session token when logging in.
24+
This flag will instead use the provided token as the session token.
25+
2226
---
2327
Run `coder --help` for a list of global options.

codersdk/apikey.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,10 @@ func (c *Client) CreateToken(ctx context.Context, userID string, req CreateToken
7878
}
7979

8080
// CreateAPIKey generates an API key for the user ID provided.
81-
// DEPRECATED: use CreateToken instead.
81+
// CreateToken should be used over CreateAPIKey. CreateToken allows better
82+
// tracking of the token's usage and allows for custom expiration.
83+
// Only use CreateAPIKey if you want to emulate the session created for
84+
// a browser like login.
8285
func (c *Client) CreateAPIKey(ctx context.Context, user string) (GenerateAPIKeyResponse, error) {
8386
res, err := c.Request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/users/%s/keys", user), nil)
8487
if err != nil {

docs/cli/login.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,11 @@ Specifies whether a trial license should be provisioned for the Coder deployment
4747
| Environment | <code>$CODER_FIRST_USER_USERNAME</code> |
4848

4949
Specifies a username to use if creating the first user for the deployment.
50+
51+
### --use-token-as-session
52+
53+
| | |
54+
| ---- | ----------------- |
55+
| Type | <code>bool</code> |
56+
57+
By default, the CLI will generate a new session token when logging in. This flag will instead use the provided token as the session token.

pty/pty_windows.go

Lines changed: 60 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -65,10 +65,13 @@ func newPty(opt ...Option) (*ptyWindows, error) {
6565
0,
6666
uintptr(unsafe.Pointer(&pty.console)),
6767
)
68-
if int32(ret) < 0 {
68+
// CreatePseudoConsole returns S_OK on success, as per:
69+
// https://learn.microsoft.com/en-us/windows/console/createpseudoconsole
70+
if windows.Handle(ret) != windows.S_OK {
6971
_ = pty.Close()
7072
return nil, xerrors.Errorf("create pseudo console (%d): %w", int32(ret), err)
7173
}
74+
7275
return pty, nil
7376
}
7477

@@ -134,39 +137,65 @@ func (p *ptyWindows) Resize(height uint16, width uint16) error {
134137
Y: int16(height),
135138
X: int16(width),
136139
})))))
137-
if ret != 0 {
140+
if windows.Handle(ret) != windows.S_OK {
138141
return err
139142
}
140143
return nil
141144
}
142145

143-
func (p *ptyWindows) Close() error {
144-
p.closeMutex.Lock()
145-
defer p.closeMutex.Unlock()
146-
if p.closed {
147-
return nil
148-
}
149-
p.closed = true
150-
146+
// closeConsoleNoLock closes the console handle, and sets it to
147+
// windows.InvalidHandle. It must be called with p.closeMutex held.
148+
func (p *ptyWindows) closeConsoleNoLock() error {
151149
// if we are running a command in the PTY, the corresponding *windowsProcess
152150
// may have already closed the PseudoConsole when the command exited, so that
153151
// output reads can get to EOF. In that case, we don't need to close it
154152
// again here.
155153
if p.console != windows.InvalidHandle {
154+
// ClosePseudoConsole has no return value and typically the syscall
155+
// returns S_FALSE (a success value). We could ignore the return value
156+
// and error here but we handle anyway, it just in case.
157+
//
158+
// Note that ClosePseudoConsole is a blocking system call and may write
159+
// a final frame to the output buffer (p.outputWrite), so there must be
160+
// a consumer (p.outputRead) to ensure we don't block here indefinitely.
161+
//
162+
// https://docs.microsoft.com/en-us/windows/console/closepseudoconsole
156163
ret, _, err := procClosePseudoConsole.Call(uintptr(p.console))
157-
if ret < 0 {
158-
return xerrors.Errorf("close pseudo console: %w", err)
164+
if winerrorFailed(ret) {
165+
return xerrors.Errorf("close pseudo console (%d): %w", ret, err)
159166
}
160167
p.console = windows.InvalidHandle
161168
}
162169

163-
// We always have these files
164-
_ = p.outputRead.Close()
165-
_ = p.inputWrite.Close()
166-
// These get closed & unset if we Start() a new process.
170+
return nil
171+
}
172+
173+
func (p *ptyWindows) Close() error {
174+
p.closeMutex.Lock()
175+
defer p.closeMutex.Unlock()
176+
if p.closed {
177+
return nil
178+
}
179+
180+
// Close the pseudo console, this will also terminate the process attached
181+
// to this pty. If it was created via Start(), this also unblocks close of
182+
// the readers below.
183+
err := p.closeConsoleNoLock()
184+
if err != nil {
185+
return err
186+
}
187+
188+
// Only set closed after the console has been successfully closed.
189+
p.closed = true
190+
191+
// Close the pipes ensuring that the writer is closed before the respective
192+
// reader, otherwise closing the reader may block indefinitely. Note that
193+
// outputWrite and inputRead are unset when we Start() a new process.
167194
if p.outputWrite != nil {
168195
_ = p.outputWrite.Close()
169196
}
197+
_ = p.outputRead.Close()
198+
_ = p.inputWrite.Close()
170199
if p.inputRead != nil {
171200
_ = p.inputRead.Close()
172201
}
@@ -184,15 +213,13 @@ func (p *windowsProcess) waitInternal() {
184213
// c.f. https://devblogs.microsoft.com/commandline/windows-command-line-introducing-the-windows-pseudo-console-conpty/
185214
p.pw.closeMutex.Lock()
186215
defer p.pw.closeMutex.Unlock()
187-
if p.pw.console != windows.InvalidHandle {
188-
ret, _, err := procClosePseudoConsole.Call(uintptr(p.pw.console))
189-
if ret < 0 && p.cmdErr == nil {
190-
// if we already have an error from the command, prefer that error
191-
// but if the command succeeded and closing the PseudoConsole fails
192-
// then record that error so that we have a chance to see it
193-
p.cmdErr = err
194-
}
195-
p.pw.console = windows.InvalidHandle
216+
217+
err := p.pw.closeConsoleNoLock()
218+
// if we already have an error from the command, prefer that error
219+
// but if the command succeeded and closing the PseudoConsole fails
220+
// then record that error so that we have a chance to see it
221+
if err != nil && p.cmdErr == nil {
222+
p.cmdErr = err
196223
}
197224
}()
198225

@@ -225,3 +252,11 @@ func (p *windowsProcess) killOnContext(ctx context.Context) {
225252
p.Kill()
226253
}
227254
}
255+
256+
// winerrorFailed returns true if the syscall failed, this function
257+
// assumes the return value is a 32-bit integer, like HRESULT.
258+
//
259+
// https://learn.microsoft.com/en-us/windows/win32/api/winerror/nf-winerror-failed
260+
func winerrorFailed(r1 uintptr) bool {
261+
return int32(r1) < 0
262+
}

pty/ptytest/ptytest.go

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -354,7 +354,13 @@ type PTY struct {
354354
func (p *PTY) Close() error {
355355
p.t.Helper()
356356
pErr := p.PTY.Close()
357-
eErr := p.outExpecter.close("close")
357+
if pErr != nil {
358+
p.logf("PTY: Close failed: %v", pErr)
359+
}
360+
eErr := p.outExpecter.close("PTY close")
361+
if eErr != nil {
362+
p.logf("PTY: close expecter failed: %v", eErr)
363+
}
358364
if pErr != nil {
359365
return pErr
360366
}
@@ -398,7 +404,13 @@ type PTYCmd struct {
398404
func (p *PTYCmd) Close() error {
399405
p.t.Helper()
400406
pErr := p.PTYCmd.Close()
401-
eErr := p.outExpecter.close("close")
407+
if pErr != nil {
408+
p.logf("PTYCmd: Close failed: %v", pErr)
409+
}
410+
eErr := p.outExpecter.close("PTYCmd close")
411+
if eErr != nil {
412+
p.logf("PTYCmd: close expecter failed: %v", eErr)
413+
}
402414
if pErr != nil {
403415
return pErr
404416
}

0 commit comments

Comments
 (0)