mirror of
https://github.com/coder/coder.git
synced 2025-07-09 11:45:56 +00:00
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:
committed by
GitHub
parent
eea9729704
commit
afc43fe95f
@ -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)
|
||||||
|
@ -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()
|
||||||
|
@ -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,
|
||||||
|
Reference in New Issue
Block a user