Compare commits

...

7 Commits

Author SHA1 Message Date
elasticspoon b0792ea751
Merge 341feddf1b into 13dd526f11 2024-05-03 22:12:10 +00:00
Colin Adler 13dd526f11
fix: prevent stdlib logging from messing up ssh (#13161)
Fixes https://github.com/coder/coder/issues/13144
2024-05-03 22:12:06 +00:00
recanman b20c63c185
fix: install openrc service on alpine (#12294) (#12870)
* fix: install openrc service on alpine (#12294)

* fmt

---------

Co-authored-by: Kyle Carberry <kyle@coder.com>
2024-05-03 21:09:23 +00:00
Michael Brewer 060f023174
feat: mask coder login token to enhance security (#12948)
* feat(login): treat coder token as a secret

* Update login.go
2024-05-03 17:03:13 -04:00
elasticspoon 341feddf1b fixup!: remove check for ENV token validity in login
I made an incorrect assumption, we can check if
a flag/env token is valid on login and create a new
token if the provided one is invalid.

However, on subsequent requests the invalid ENV token
will still take priority over the valid token stored
on disk. Therefore, this behavior does not work.
2024-04-26 17:23:14 -04:00
elasticspoon f0616ea5e5 fixup!: handle cliui.Canceled error
cliui should return nil when returning cliui.Canceled (CTRL-C) and
return error others

fix nits
2024-04-24 20:23:18 -04:00
elasticspoon f45143bfc7 feat(cli): prompt when relogging as authed user (#11004, #9329)
Prompt the user if they want to re-authenticate if they run
`coder login` when already authenticated.

Does not apply if they provided a token via flag or ENV
variable. A new token should be generated an stored in that situation
unless the `--use-token-as-session` flag was also used.

fix(cli): prompt user on login attempt with invalid ENV token

Fixes issue where if invalid token was set as ENV
variable a user would be unable to login until the ENV
variable was cleared.

User will now be informed that the token is invalid and
prompted to login normally.
2024-04-06 20:14:50 -04:00
10 changed files with 285 additions and 26 deletions

View File

@ -274,7 +274,52 @@ func (r *RootCmd) login() *serpent.Command {
return nil
}
// Check for session token from flags or environment.
sessionToken, _ := inv.ParsedFlags().GetString(varToken)
if sessionToken != "" && !useTokenForSession {
// If a session token is provided on the cli, use it to generate
// a new one. This is because the cli `--token` flag provides
// a token for the command being invoked. We should not store
// this token, and `/logout` should not delete it.
// /login should generate a new token and store that.
client.SetSessionToken(sessionToken)
// Use CreateAPIKey over CreateToken because this is a session
// key that should not show on the `tokens` page. This should
// match the same behavior of the `/cli-auth` page for generating
// a session token.
key, err := client.CreateAPIKey(ctx, codersdk.Me)
if err != nil {
return xerrors.Errorf("create api key: %w", err)
}
sessionToken = key.Key
}
// Check for existing session token on disk, and validate user data.
// If the token exists but is invalid, then it is probably expired.
// Skip this check if the user has provided a valid token; a new token
// should be generated.
var userResp codersdk.User
if configToken, _ := r.createConfig().Session().Read(); sessionToken == "" && configToken != "" {
client.SetSessionToken(configToken)
userResp, err = client.User(ctx, codersdk.Me)
if err == nil {
_, err = cliui.Prompt(inv, cliui.PromptOptions{
Text: fmt.Sprintf("You are already authenticated %s. Are you sure you want to log in again?",
pretty.Sprint(cliui.DefaultStyles.Keyword, userResp.Username)),
IsConfirm: true,
Default: cliui.ConfirmYes,
})
if errors.Is(err, cliui.Canceled) {
return nil
}
if err != nil {
return err
}
}
sessionToken = ""
}
// If we still don't have a session token, prompt the user for one.
if sessionToken == "" {
authURL := *serverURL
// Don't use filepath.Join, we don't want to use the os separator
@ -287,7 +332,8 @@ func (r *RootCmd) login() *serpent.Command {
}
sessionToken, err = cliui.Prompt(inv, cliui.PromptOptions{
Text: "Paste your token here:",
Text: "Paste your token here:",
Secret: true,
Validate: func(token string) error {
client.SetSessionToken(token)
_, err := client.User(ctx, codersdk.Me)
@ -300,29 +346,16 @@ func (r *RootCmd) login() *serpent.Command {
if err != nil {
return xerrors.Errorf("paste token prompt: %w", err)
}
} else if !useTokenForSession {
// If a session token is provided on the cli, use it to generate
// a new one. This is because the cli `--token` flag provides
// a token for the command being invoked. We should not store
// this token, and `/logout` should not delete it.
// /login should generate a new token and store that.
client.SetSessionToken(sessionToken)
// Use CreateAPIKey over CreateToken because this is a session
// key that should not show on the `tokens` page. This should
// match the same behavior of the `/cli-auth` page for generating
// a session token.
key, err := client.CreateAPIKey(ctx, "me")
if err != nil {
return xerrors.Errorf("create api key: %w", err)
}
sessionToken = key.Key
}
// If we didn't login via session token on disk
// Login to get user data - verify it is OK before persisting
client.SetSessionToken(sessionToken)
resp, err := client.User(ctx, codersdk.Me)
if err != nil {
return xerrors.Errorf("get user: %w", err)
if userResp.Email == "" {
client.SetSessionToken(sessionToken)
userResp, err = client.User(ctx, codersdk.Me)
if err != nil {
return xerrors.Errorf("get user: %w", err)
}
}
config := r.createConfig()
@ -335,7 +368,8 @@ func (r *RootCmd) login() *serpent.Command {
return xerrors.Errorf("write server url: %w", err)
}
_, _ = fmt.Fprintf(inv.Stdout, Caret+"Welcome to Coder, %s! You're authenticated.\n", pretty.Sprint(cliui.DefaultStyles.Keyword, resp.Username))
_, _ = fmt.Fprintf(inv.Stdout, Caret+"Welcome to Coder, %s! You're authenticated.\n",
pretty.Sprint(cliui.DefaultStyles.Keyword, userResp.Username))
return nil
},
}
@ -366,7 +400,7 @@ func (r *RootCmd) login() *serpent.Command {
},
{
Flag: "use-token-as-session",
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.",
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. See `coder --help` for more information on how to set a token.",
Value: serpent.BoolOf(&useTokenForSession),
},
}

View File

@ -225,6 +225,8 @@ func TestLogin(t *testing.T) {
inv, root := clitest.New(t, "login", "--no-open")
clitest.SetupConfig(t, client, root)
err := root.Session().Delete()
assert.NoError(t, err)
doneChan := make(chan struct{})
pty := ptytest.New(t).Attach(inv)
@ -291,6 +293,110 @@ func TestLogin(t *testing.T) {
<-doneChan
})
t.Run("ExistingUserExpiredSessionToken", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
coderdtest.CreateFirstUser(t, client)
inv, root := clitest.New(t, "login", "--no-open", client.URL.String())
pty := ptytest.New(t).Attach(inv)
err := root.Session().Write("an-expired-token")
assert.NoError(t, err)
doneChan := make(chan struct{})
go func() {
defer close(doneChan)
err := inv.Run()
assert.NoError(t, err)
}()
pty.ExpectMatch("Paste your token here:")
pty.WriteLine(client.SessionToken())
pty.ExpectMatch("Welcome to Coder")
<-doneChan
})
t.Run("ExistingUserRelogin", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
coderdtest.CreateFirstUser(t, client)
inv, root := clitest.New(t, "login", "--no-open")
clitest.SetupConfig(t, client, root)
pty := ptytest.New(t).Attach(inv)
doneChan := make(chan struct{})
go func() {
defer close(doneChan)
err := inv.Run()
assert.NoError(t, err)
}()
pty.ExpectMatch("You are already authenticated")
pty.ExpectMatch("Are you sure you want to log in again?")
pty.WriteLine("yes")
pty.ExpectMatch("Paste your token here:")
pty.WriteLine(client.SessionToken())
pty.ExpectMatch("Welcome to Coder")
<-doneChan
})
t.Run("ExistingUserReloginRejection", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
coderdtest.CreateFirstUser(t, client)
inv, root := clitest.New(t, "login", "--no-open")
clitest.SetupConfig(t, client, root)
doneChan := make(chan struct{})
pty := ptytest.New(t).Attach(inv)
go func() {
defer close(doneChan)
err := inv.Run()
assert.NoError(t, err)
}()
pty.ExpectMatch("You are already authenticated")
pty.ExpectMatch("Are you sure you want to log in again?")
pty.WriteLine("no")
<-doneChan
})
t.Run("AuthenticatedUserTokenFlagValid", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
coderdtest.CreateFirstUser(t, client)
inv, root := clitest.New(t, "login", client.URL.String(), "--no-open", "--token", client.SessionToken())
clitest.SetupConfig(t, client, root)
err := inv.Run()
assert.NoError(t, err)
sessionFile, err := root.Session().Read()
require.NoError(t, err)
// This **should not be equal** to the token we passed in.
require.NotEqual(t, client.SessionToken(), sessionFile)
})
t.Run("AuthenticatedUserTokenFlagValidUseTokenAsSession", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
coderdtest.CreateFirstUser(t, client)
inv, root := clitest.New(t, "login", client.URL.String(), "--no-open", "--token", client.SessionToken(), "--use-token-as-session")
clitest.SetupConfig(t, client, root)
err := inv.Run()
assert.NoError(t, err)
sessionFile, err := root.Session().Read()
require.NoError(t, err)
require.Equal(t, client.SessionToken(), sessionFile)
})
// TokenFlag should generate a new session token and store it in the session file.
t.Run("TokenFlag", func(t *testing.T) {
t.Parallel()

View File

@ -1441,7 +1441,7 @@ func newProvisionerDaemon(
connector[string(database.ProvisionerTypeTerraform)] = sdkproto.NewDRPCProvisionerClient(terraformClient)
default:
return nil, fmt.Errorf("unknown provisioner type %q", provisionerType)
return nil, xerrors.Errorf("unknown provisioner type %q", provisionerType)
}
}

View File

@ -6,6 +6,7 @@ import (
"errors"
"fmt"
"io"
"log"
"net/http"
"net/url"
"os"
@ -79,6 +80,10 @@ func (r *RootCmd) ssh() *serpent.Command {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
// Prevent unnecessary logs from the stdlib from messing up the TTY.
// See: https://github.com/coder/coder/issues/13144
log.SetOutput(io.Discard)
logger := inv.Logger
defer func() {
if retErr != nil {

View File

@ -25,6 +25,7 @@ OPTIONS:
--use-token-as-session bool
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.
See `coder --help` for more information on how to set a token.
———
Run `coder --help` for a list of global options.

2
docs/cli/login.md generated
View File

@ -54,4 +54,4 @@ Specifies whether a trial license should be provisioned for the Coder deployment
| ---- | ----------------- |
| Type | <code>bool</code> |
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.
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. See `coder --help` for more information on how to set a token.

38
scripts/linux-pkg/coder-openrc Executable file
View File

@ -0,0 +1,38 @@
#!/sbin/openrc-run
name=coder
description="Coder - Self-hosted developer workspaces on your infra"
document="https://coder.com/docs/coder-oss"
depend() {
need net
after net-online
use dns logger
}
checkpath --directory --owner coder:coder --mode 0700 /var/cache/coder
start_pre() {
if [ ! -f /etc/coder.d/coder.env ]; then
eerror "/etc/coder.d/coder.env file does not exist"
return 1
fi
# Read and export environment variables ignoring comment lines and blank lines
while IFS= read -r line; do
# Skip blank or comment lines
if [ -z "$line" ] || [[ "$line" =~ ^# ]]; then
continue
fi
export "$line"
done < /etc/coder.d/coder.env
}
command="/usr/bin/coder"
command_args="server"
command_user="coder:coder"
command_background="yes"
pidfile="/run/coder.pid"
restart="always"
restart_delay="5"
stop_timeout="90"

View File

@ -0,0 +1,39 @@
#!/sbin/openrc-run
name=coder-workspace-proxy
description="Coder - external workspace proxy server"
document="https://coder.com/docs/coder-oss"
depend() {
need net
after net-online
use dns logger
}
checkpath --directory --owner coder:coder --mode 0700 /var/cache/coder
start_pre() {
if [ ! -f /etc/coder.d/coder-workspace-proxy.env ]; then
eerror "/etc/coder.d/coder-workspace-proxy.env file does not exist"
return 1
fi
# Read and export environment variables ignoring comment lines and blank lines
while IFS= read -r line; do
# Skip blank or comment lines
if [ -z "$line" ] || [[ "$line" =~ ^# ]]; then
continue
fi
export "$line"
done < /etc/coder.d/coder-workspace-proxy.env
}
command="/usr/bin/coder"
command_args="workspace-proxy server"
command_user="coder:coder"
command_background="yes"
pidfile="/run/coder-workspace-proxy.pid"
restart="always"
restart_delay="5"
stop_timeout="90"

View File

@ -0,0 +1,29 @@
name: coder
platform: linux
arch: "${GOARCH}"
version: "${CODER_VERSION}"
version_schema: semver
release: 1
vendor: Coder
homepage: https://coder.com
maintainer: Coder <support@coder.com>
description: |
Provision development environments with infrastructure with code
license: AGPL-3.0
suggests:
- postgresql
scripts:
preinstall: preinstall.sh
contents:
- src: coder
dst: /usr/bin/coder
- src: coder.env
dst: /etc/coder.d/coder.env
type: "config|noreplace"
- src: coder-workspace-proxy-openrc
dst: /etc/init.d/coder-workspace-proxy
- src: coder-openrc
dst: /etc/init.d/coder

View File

@ -89,9 +89,16 @@ ln "$(realpath scripts/linux-pkg/coder.service)" "$temp_dir/"
ln "$(realpath scripts/linux-pkg/nfpm.yaml)" "$temp_dir/"
ln "$(realpath scripts/linux-pkg/preinstall.sh)" "$temp_dir/"
nfpm_config_file="nfpm.yaml"
# Use nfpm-alpine.yaml when building for Alpine (OpenRC).
if [[ "$format" == "apk" ]]; then
nfpm_config_file="nfpm-alpine.yaml"
fi
pushd "$temp_dir"
GOARCH="$arch" CODER_VERSION="$version" nfpm package \
-f nfpm.yaml \
-f "$nfpm_config_file" \
-p "$format" \
-t "$output_path" \
1>&2