feat: Generate random admin user password in dev mode (#1207)

* feat: Generate random admin user password in dev mode

* Add dev mode test with email/pass from env

* Set email/pass for playwright e2e test via cli flags
This commit is contained in:
Mathias Fredriksson
2022-04-28 19:13:44 +03:00
committed by GitHub
parent eea9729704
commit afc43fe95f
3 changed files with 109 additions and 27 deletions

View File

@ -41,27 +41,23 @@ import (
"github.com/coder/coder/coderd/gitsshkey" "github.com/coder/coder/coderd/gitsshkey"
"github.com/coder/coder/coderd/turnconn" "github.com/coder/coder/coderd/turnconn"
"github.com/coder/coder/codersdk" "github.com/coder/coder/codersdk"
"github.com/coder/coder/cryptorand"
"github.com/coder/coder/provisioner/terraform" "github.com/coder/coder/provisioner/terraform"
"github.com/coder/coder/provisionerd" "github.com/coder/coder/provisionerd"
"github.com/coder/coder/provisionersdk" "github.com/coder/coder/provisionersdk"
"github.com/coder/coder/provisionersdk/proto" "github.com/coder/coder/provisionersdk/proto"
) )
var defaultDevUser = codersdk.CreateFirstUserRequest{
Email: "admin@coder.com",
Username: "developer",
Password: "password",
OrganizationName: "acme-corp",
}
// nolint:gocyclo // nolint:gocyclo
func server() *cobra.Command { func server() *cobra.Command {
var ( var (
accessURL string accessURL string
address string address string
cacheDir string cacheDir string
dev bool dev bool
postgresURL string devUserEmail string
devUserPassword string
postgresURL string
// provisionerDaemonCount is a uint8 to ensure a number > 0. // provisionerDaemonCount is a uint8 to ensure a number > 0.
provisionerDaemonCount uint8 provisionerDaemonCount uint8
oauth2GithubClientID string oauth2GithubClientID string
@ -278,12 +274,18 @@ func server() *cobra.Command {
config := createConfig(cmd) config := createConfig(cmd)
if dev { if dev {
err = createFirstUser(cmd, client, config) if devUserPassword == "" {
devUserPassword, err = cryptorand.String(10)
if err != nil {
return xerrors.Errorf("generate random admin password for dev: %w", err)
}
}
err = createFirstUser(cmd, client, config, devUserEmail, devUserPassword)
if err != nil { if err != nil {
return xerrors.Errorf("create first user: %w", err) return xerrors.Errorf("create first user: %w", err)
} }
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "email: %s\n", defaultDevUser.Email) _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "email: %s\n", devUserEmail)
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "password: %s\n", defaultDevUser.Password) _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "password: %s\n", devUserPassword)
_, _ = fmt.Fprintln(cmd.ErrOrStderr()) _, _ = fmt.Fprintln(cmd.ErrOrStderr())
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), cliui.Styles.Wrap.Render(`Started in dev mode. All data is in-memory! `+cliui.Styles.Bold.Render("Do not use in production")+`. Press `+ _, _ = fmt.Fprintf(cmd.ErrOrStderr(), cliui.Styles.Wrap.Render(`Started in dev mode. All data is in-memory! `+cliui.Styles.Bold.Render("Do not use in production")+`. Press `+
@ -409,6 +411,8 @@ func server() *cobra.Command {
// systemd uses the CACHE_DIRECTORY environment variable! // systemd uses the CACHE_DIRECTORY environment variable!
cliflag.StringVarP(root.Flags(), &cacheDir, "cache-dir", "", "CACHE_DIRECTORY", filepath.Join(os.TempDir(), "coder-cache"), "Specifies a directory to cache binaries for provision operations.") cliflag.StringVarP(root.Flags(), &cacheDir, "cache-dir", "", "CACHE_DIRECTORY", filepath.Join(os.TempDir(), "coder-cache"), "Specifies a directory to cache binaries for provision operations.")
cliflag.BoolVarP(root.Flags(), &dev, "dev", "", "CODER_DEV_MODE", false, "Serve Coder in dev mode for tinkering") cliflag.BoolVarP(root.Flags(), &dev, "dev", "", "CODER_DEV_MODE", false, "Serve Coder in dev mode for tinkering")
cliflag.StringVarP(root.Flags(), &devUserEmail, "dev-admin-email", "", "CODER_DEV_ADMIN_EMAIL", "admin@coder.com", "Specifies the admin email to be used in dev mode (--dev)")
cliflag.StringVarP(root.Flags(), &devUserPassword, "dev-admin-password", "", "CODER_DEV_ADMIN_PASSWORD", "", "Specifies the admin password to be used in dev mode (--dev) instead of a randomly generated one")
cliflag.StringVarP(root.Flags(), &postgresURL, "postgres-url", "", "CODER_PG_CONNECTION_URL", "", "URL of a PostgreSQL database to connect to") cliflag.StringVarP(root.Flags(), &postgresURL, "postgres-url", "", "CODER_PG_CONNECTION_URL", "", "URL of a PostgreSQL database to connect to")
cliflag.Uint8VarP(root.Flags(), &provisionerDaemonCount, "provisioner-daemons", "", "CODER_PROVISIONER_DAEMONS", 3, "The amount of provisioner daemons to create on start.") cliflag.Uint8VarP(root.Flags(), &provisionerDaemonCount, "provisioner-daemons", "", "CODER_PROVISIONER_DAEMONS", 3, "The amount of provisioner daemons to create on start.")
cliflag.StringVarP(root.Flags(), &oauth2GithubClientID, "oauth2-github-client-id", "", "CODER_OAUTH2_GITHUB_CLIENT_ID", "", cliflag.StringVarP(root.Flags(), &oauth2GithubClientID, "oauth2-github-client-id", "", "CODER_OAUTH2_GITHUB_CLIENT_ID", "",
@ -450,14 +454,25 @@ func server() *cobra.Command {
return root return root
} }
func createFirstUser(cmd *cobra.Command, client *codersdk.Client, cfg config.Root) error { func createFirstUser(cmd *cobra.Command, client *codersdk.Client, cfg config.Root, email, password string) error {
_, err := client.CreateFirstUser(cmd.Context(), defaultDevUser) if email == "" {
return xerrors.New("email is empty")
}
if password == "" {
return xerrors.New("password is empty")
}
_, err := client.CreateFirstUser(cmd.Context(), codersdk.CreateFirstUserRequest{
Email: email,
Username: "developer",
Password: password,
OrganizationName: "acme-corp",
})
if err != nil { if err != nil {
return xerrors.Errorf("create first user: %w", err) return xerrors.Errorf("create first user: %w", err)
} }
token, err := client.LoginWithPassword(cmd.Context(), codersdk.LoginWithPasswordRequest{ token, err := client.LoginWithPassword(cmd.Context(), codersdk.LoginWithPasswordRequest{
Email: defaultDevUser.Email, Email: email,
Password: defaultDevUser.Password, Password: password,
}) })
if err != nil { if err != nil {
return xerrors.Errorf("login with first user: %w", err) return xerrors.Errorf("login with first user: %w", err)

View File

@ -1,7 +1,6 @@
package cli_test package cli_test
import ( import (
"bytes"
"context" "context"
"crypto/ecdsa" "crypto/ecdsa"
"crypto/elliptic" "crypto/elliptic"
@ -10,12 +9,15 @@ import (
"crypto/x509" "crypto/x509"
"crypto/x509/pkix" "crypto/x509/pkix"
"encoding/pem" "encoding/pem"
"fmt"
"math/big" "math/big"
"net" "net"
"net/http" "net/http"
"net/url" "net/url"
"os" "os"
"runtime" "runtime"
"strings"
"sync"
"testing" "testing"
"time" "time"
@ -74,18 +76,30 @@ func TestServer(t *testing.T) {
t.Parallel() t.Parallel()
ctx, cancelFunc := context.WithCancel(context.Background()) ctx, cancelFunc := context.WithCancel(context.Background())
defer cancelFunc() defer cancelFunc()
wantEmail := "admin@coder.com"
root, cfg := clitest.New(t, "server", "--dev", "--skip-tunnel", "--address", ":0") root, cfg := clitest.New(t, "server", "--dev", "--skip-tunnel", "--address", ":0")
var stdoutBuf bytes.Buffer var buf strings.Builder
root.SetOutput(&stdoutBuf) root.SetOutput(&buf)
var wg sync.WaitGroup
wg.Add(1)
go func() { go func() {
defer wg.Done()
err := root.ExecuteContext(ctx) err := root.ExecuteContext(ctx)
require.ErrorIs(t, err, context.Canceled) require.ErrorIs(t, err, context.Canceled)
// Verify that credentials were output to the terminal. // Verify that credentials were output to the terminal.
wantEmail := "email: admin@coder.com" assert.Contains(t, buf.String(), fmt.Sprintf("email: %s", wantEmail), "expected output %q; got no match", wantEmail)
wantPassword := "password: password" // Check that the password line is output and that it's non-empty.
assert.Contains(t, stdoutBuf.String(), wantEmail, "expected output %q; got no match", wantEmail) if _, after, found := strings.Cut(buf.String(), "password: "); found {
assert.Contains(t, stdoutBuf.String(), wantPassword, "expected output %q; got no match", wantPassword) before, _, _ := strings.Cut(after, "\n")
before = strings.Trim(before, "\r") // Ensure no control character is left.
assert.NotEmpty(t, before, "expected non-empty password; got empty")
} else {
t.Error("expected password line output; got no match")
}
}() }()
var token string var token string
require.Eventually(t, func() bool { require.Eventually(t, func() bool {
@ -102,6 +116,55 @@ func TestServer(t *testing.T) {
client.SessionToken = token client.SessionToken = token
_, err = client.User(ctx, codersdk.Me) _, err = client.User(ctx, codersdk.Me)
require.NoError(t, err) require.NoError(t, err)
cancelFunc()
wg.Wait()
})
// Duplicated test from "Development" above to test setting email/password via env.
// Cannot run parallel due to os.Setenv.
//nolint:paralleltest
t.Run("Development with email and password from env", func(t *testing.T) {
ctx, cancelFunc := context.WithCancel(context.Background())
defer cancelFunc()
wantEmail := "myadmin@coder.com"
wantPassword := "testpass42"
t.Setenv("CODER_DEV_ADMIN_EMAIL", wantEmail)
t.Setenv("CODER_DEV_ADMIN_PASSWORD", wantPassword)
root, cfg := clitest.New(t, "server", "--dev", "--skip-tunnel", "--address", ":0")
var buf strings.Builder
root.SetOutput(&buf)
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
err := root.ExecuteContext(ctx)
require.ErrorIs(t, err, context.Canceled)
// Verify that credentials were output to the terminal.
assert.Contains(t, buf.String(), fmt.Sprintf("email: %s", wantEmail), "expected output %q; got no match", wantEmail)
assert.Contains(t, buf.String(), fmt.Sprintf("password: %s", wantPassword), "expected output %q; got no match", wantPassword)
}()
var token string
require.Eventually(t, func() bool {
var err error
token, err = cfg.Session().Read()
return err == nil
}, 15*time.Second, 25*time.Millisecond)
// Verify that authentication was properly set in dev-mode.
accessURL, err := cfg.URL().Read()
require.NoError(t, err)
parsed, err := url.Parse(accessURL)
require.NoError(t, err)
client := codersdk.New(parsed)
client.SessionToken = token
_, err = client.User(ctx, codersdk.Me)
require.NoError(t, err)
cancelFunc()
wg.Wait()
}) })
t.Run("TLSBadVersion", func(t *testing.T) { t.Run("TLSBadVersion", func(t *testing.T) {
t.Parallel() t.Parallel()

View File

@ -1,5 +1,6 @@
import { PlaywrightTestConfig } from "@playwright/test" import { PlaywrightTestConfig } from "@playwright/test"
import * as path from "path" import * as path from "path"
import * as constants from "./constants"
const config: PlaywrightTestConfig = { const config: PlaywrightTestConfig = {
testDir: "tests", testDir: "tests",
@ -17,7 +18,10 @@ const config: PlaywrightTestConfig = {
// https://playwright.dev/docs/test-advanced#launching-a-development-web-server-during-the-tests // https://playwright.dev/docs/test-advanced#launching-a-development-web-server-during-the-tests
webServer: { webServer: {
// Run the coder daemon directly. // Run the coder daemon directly.
command: `go run -tags embed ${path.join(__dirname, "../../cmd/coder/main.go")} server --dev --skip-tunnel`, command: `go run -tags embed ${path.join(
__dirname,
"../../cmd/coder/main.go",
)} server --dev --skip-tunnel --dev-admin-email ${constants.email} --dev-admin-password ${constants.password}`,
port: 3000, port: 3000,
timeout: 120 * 10000, timeout: 120 * 10000,
reuseExistingServer: false, reuseExistingServer: false,