Skip to content

Commit abf7633

Browse files
committed
Merge branch 'main' into webterm
2 parents 229c7e4 + 82364d1 commit abf7633

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

53 files changed

+2216
-547
lines changed

.goreleaser.yaml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ builds:
2929
- id: coder-slim
3030
dir: cmd/coder
3131
ldflags:
32-
["-s -w -X github.com/coder/coder/cli/buildinfo.tag={{ .Version }}"]
32+
["-s -w -X github.com/coder/coder/buildinfo.tag={{ .Version }}"]
3333
env: [CGO_ENABLED=0]
3434
goos: [darwin, linux, windows]
3535
goarch: [amd64]
@@ -42,7 +42,7 @@ builds:
4242
dir: cmd/coder
4343
flags: [-tags=embed]
4444
ldflags:
45-
["-s -w -X github.com/coder/coder/cli/buildinfo.tag={{ .Version }}"]
45+
["-s -w -X github.com/coder/coder/buildinfo.tag={{ .Version }}"]
4646
env: [CGO_ENABLED=0]
4747
goos: [linux]
4848
goarch: [amd64, arm64]
@@ -51,7 +51,7 @@ builds:
5151
dir: cmd/coder
5252
flags: [-tags=embed]
5353
ldflags:
54-
["-s -w -X github.com/coder/coder/cli/buildinfo.tag={{ .Version }}"]
54+
["-s -w -X github.com/coder/coder/buildinfo.tag={{ .Version }}"]
5555
env: [CGO_ENABLED=0]
5656
goos: [windows]
5757
goarch: [amd64, arm64]
@@ -60,7 +60,7 @@ builds:
6060
dir: cmd/coder
6161
flags: [-tags=embed]
6262
ldflags:
63-
["-s -w -X github.com/coder/coder/cli/buildinfo.tag={{ .Version }}"]
63+
["-s -w -X github.com/coder/coder/buildinfo.tag={{ .Version }}"]
6464
env: [CGO_ENABLED=0]
6565
goos: [darwin]
6666
goarch: [amd64, arm64]

cli/server.go

Lines changed: 31 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import (
4141
"github.com/coder/coder/coderd/gitsshkey"
4242
"github.com/coder/coder/coderd/turnconn"
4343
"github.com/coder/coder/codersdk"
44+
"github.com/coder/coder/cryptorand"
4445
"github.com/coder/coder/provisioner/terraform"
4546
"github.com/coder/coder/provisionerd"
4647
"github.com/coder/coder/provisionersdk"
@@ -50,11 +51,13 @@ import (
5051
// nolint:gocyclo
5152
func server() *cobra.Command {
5253
var (
53-
accessURL string
54-
address string
55-
cacheDir string
56-
dev bool
57-
postgresURL string
54+
accessURL string
55+
address string
56+
cacheDir string
57+
dev bool
58+
devUserEmail string
59+
devUserPassword string
60+
postgresURL string
5861
// provisionerDaemonCount is a uint8 to ensure a number > 0.
5962
provisionerDaemonCount uint8
6063
oauth2GithubClientID string
@@ -271,10 +274,19 @@ func server() *cobra.Command {
271274
config := createConfig(cmd)
272275

273276
if dev {
274-
err = createFirstUser(cmd, client, config)
277+
if devUserPassword == "" {
278+
devUserPassword, err = cryptorand.String(10)
279+
if err != nil {
280+
return xerrors.Errorf("generate random admin password for dev: %w", err)
281+
}
282+
}
283+
err = createFirstUser(cmd, client, config, devUserEmail, devUserPassword)
275284
if err != nil {
276285
return xerrors.Errorf("create first user: %w", err)
277286
}
287+
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "email: %s\n", devUserEmail)
288+
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "password: %s\n", devUserPassword)
289+
_, _ = fmt.Fprintln(cmd.ErrOrStderr())
278290

279291
_, _ = 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 `+
280292
cliui.Styles.Field.Render("ctrl+c")+` to clean up provisioned infrastructure.`)+"\n\n")
@@ -399,6 +411,8 @@ func server() *cobra.Command {
399411
// systemd uses the CACHE_DIRECTORY environment variable!
400412
cliflag.StringVarP(root.Flags(), &cacheDir, "cache-dir", "", "CACHE_DIRECTORY", filepath.Join(os.TempDir(), "coder-cache"), "Specifies a directory to cache binaries for provision operations.")
401413
cliflag.BoolVarP(root.Flags(), &dev, "dev", "", "CODER_DEV_MODE", false, "Serve Coder in dev mode for tinkering")
414+
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)")
415+
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")
402416
cliflag.StringVarP(root.Flags(), &postgresURL, "postgres-url", "", "CODER_PG_CONNECTION_URL", "", "URL of a PostgreSQL database to connect to")
403417
cliflag.Uint8VarP(root.Flags(), &provisionerDaemonCount, "provisioner-daemons", "", "CODER_PROVISIONER_DAEMONS", 3, "The amount of provisioner daemons to create on start.")
404418
cliflag.StringVarP(root.Flags(), &oauth2GithubClientID, "oauth2-github-client-id", "", "CODER_OAUTH2_GITHUB_CLIENT_ID", "",
@@ -440,19 +454,25 @@ func server() *cobra.Command {
440454
return root
441455
}
442456

443-
func createFirstUser(cmd *cobra.Command, client *codersdk.Client, cfg config.Root) error {
457+
func createFirstUser(cmd *cobra.Command, client *codersdk.Client, cfg config.Root, email, password string) error {
458+
if email == "" {
459+
return xerrors.New("email is empty")
460+
}
461+
if password == "" {
462+
return xerrors.New("password is empty")
463+
}
444464
_, err := client.CreateFirstUser(cmd.Context(), codersdk.CreateFirstUserRequest{
445-
Email: "admin@coder.com",
465+
Email: email,
446466
Username: "developer",
447-
Password: "password",
467+
Password: password,
448468
OrganizationName: "acme-corp",
449469
})
450470
if err != nil {
451471
return xerrors.Errorf("create first user: %w", err)
452472
}
453473
token, err := client.LoginWithPassword(cmd.Context(), codersdk.LoginWithPasswordRequest{
454-
Email: "admin@coder.com",
455-
Password: "password",
474+
Email: email,
475+
Password: password,
456476
})
457477
if err != nil {
458478
return xerrors.Errorf("login with first user: %w", err)

cli/server_test.go

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,19 @@ import (
99
"crypto/x509"
1010
"crypto/x509/pkix"
1111
"encoding/pem"
12+
"fmt"
1213
"math/big"
1314
"net"
1415
"net/http"
1516
"net/url"
1617
"os"
1718
"runtime"
19+
"strings"
20+
"sync"
1821
"testing"
1922
"time"
2023

24+
"github.com/stretchr/testify/assert"
2125
"github.com/stretchr/testify/require"
2226
"go.uber.org/goleak"
2327

@@ -72,10 +76,30 @@ func TestServer(t *testing.T) {
7276
t.Parallel()
7377
ctx, cancelFunc := context.WithCancel(context.Background())
7478
defer cancelFunc()
79+
80+
wantEmail := "admin@coder.com"
81+
7582
root, cfg := clitest.New(t, "server", "--dev", "--skip-tunnel", "--address", ":0")
83+
var buf strings.Builder
84+
root.SetOutput(&buf)
85+
var wg sync.WaitGroup
86+
wg.Add(1)
7687
go func() {
88+
defer wg.Done()
89+
7790
err := root.ExecuteContext(ctx)
7891
require.ErrorIs(t, err, context.Canceled)
92+
93+
// Verify that credentials were output to the terminal.
94+
assert.Contains(t, buf.String(), fmt.Sprintf("email: %s", wantEmail), "expected output %q; got no match", wantEmail)
95+
// Check that the password line is output and that it's non-empty.
96+
if _, after, found := strings.Cut(buf.String(), "password: "); found {
97+
before, _, _ := strings.Cut(after, "\n")
98+
before = strings.Trim(before, "\r") // Ensure no control character is left.
99+
assert.NotEmpty(t, before, "expected non-empty password; got empty")
100+
} else {
101+
t.Error("expected password line output; got no match")
102+
}
79103
}()
80104
var token string
81105
require.Eventually(t, func() bool {
@@ -92,6 +116,55 @@ func TestServer(t *testing.T) {
92116
client.SessionToken = token
93117
_, err = client.User(ctx, codersdk.Me)
94118
require.NoError(t, err)
119+
120+
cancelFunc()
121+
wg.Wait()
122+
})
123+
// Duplicated test from "Development" above to test setting email/password via env.
124+
// Cannot run parallel due to os.Setenv.
125+
//nolint:paralleltest
126+
t.Run("Development with email and password from env", func(t *testing.T) {
127+
ctx, cancelFunc := context.WithCancel(context.Background())
128+
defer cancelFunc()
129+
130+
wantEmail := "myadmin@coder.com"
131+
wantPassword := "testpass42"
132+
t.Setenv("CODER_DEV_ADMIN_EMAIL", wantEmail)
133+
t.Setenv("CODER_DEV_ADMIN_PASSWORD", wantPassword)
134+
135+
root, cfg := clitest.New(t, "server", "--dev", "--skip-tunnel", "--address", ":0")
136+
var buf strings.Builder
137+
root.SetOutput(&buf)
138+
var wg sync.WaitGroup
139+
wg.Add(1)
140+
go func() {
141+
defer wg.Done()
142+
143+
err := root.ExecuteContext(ctx)
144+
require.ErrorIs(t, err, context.Canceled)
145+
146+
// Verify that credentials were output to the terminal.
147+
assert.Contains(t, buf.String(), fmt.Sprintf("email: %s", wantEmail), "expected output %q; got no match", wantEmail)
148+
assert.Contains(t, buf.String(), fmt.Sprintf("password: %s", wantPassword), "expected output %q; got no match", wantPassword)
149+
}()
150+
var token string
151+
require.Eventually(t, func() bool {
152+
var err error
153+
token, err = cfg.Session().Read()
154+
return err == nil
155+
}, 15*time.Second, 25*time.Millisecond)
156+
// Verify that authentication was properly set in dev-mode.
157+
accessURL, err := cfg.URL().Read()
158+
require.NoError(t, err)
159+
parsed, err := url.Parse(accessURL)
160+
require.NoError(t, err)
161+
client := codersdk.New(parsed)
162+
client.SessionToken = token
163+
_, err = client.User(ctx, codersdk.Me)
164+
require.NoError(t, err)
165+
166+
cancelFunc()
167+
wg.Wait()
95168
})
96169
t.Run("TLSBadVersion", func(t *testing.T) {
97170
t.Parallel()

coderd/database/databasefake/databasefake.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -709,6 +709,29 @@ func (q *fakeQuerier) GetOrganizationMemberByUserID(_ context.Context, arg datab
709709
return database.OrganizationMember{}, sql.ErrNoRows
710710
}
711711

712+
func (q *fakeQuerier) GetOrganizationIDsByMemberIDs(_ context.Context, ids []uuid.UUID) ([]database.GetOrganizationIDsByMemberIDsRow, error) {
713+
q.mutex.RLock()
714+
defer q.mutex.RUnlock()
715+
716+
getOrganizationIDsByMemberIDRows := make([]database.GetOrganizationIDsByMemberIDsRow, 0, len(ids))
717+
for _, userID := range ids {
718+
userOrganizationIDs := make([]uuid.UUID, 0)
719+
for _, membership := range q.organizationMembers {
720+
if membership.UserID == userID {
721+
userOrganizationIDs = append(userOrganizationIDs, membership.OrganizationID)
722+
}
723+
}
724+
getOrganizationIDsByMemberIDRows = append(getOrganizationIDsByMemberIDRows, database.GetOrganizationIDsByMemberIDsRow{
725+
UserID: userID,
726+
OrganizationIDs: userOrganizationIDs,
727+
})
728+
}
729+
if len(getOrganizationIDsByMemberIDRows) == 0 {
730+
return nil, sql.ErrNoRows
731+
}
732+
return getOrganizationIDsByMemberIDRows, nil
733+
}
734+
712735
func (q *fakeQuerier) GetProvisionerDaemons(_ context.Context) ([]database.ProvisionerDaemon, error) {
713736
q.mutex.RLock()
714737
defer q.mutex.RUnlock()

coderd/database/querier.go

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/queries.sql.go

Lines changed: 39 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/queries/organizationmembers.sql

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,13 @@ INSERT INTO
2020
)
2121
VALUES
2222
($1, $2, $3, $4, $5) RETURNING *;
23+
24+
-- name: GetOrganizationIDsByMemberIDs :many
25+
SELECT
26+
user_id, array_agg(organization_id) :: uuid [ ] AS "organization_IDs"
27+
FROM
28+
organization_members
29+
WHERE
30+
user_id = ANY(@ids :: uuid [ ])
31+
GROUP BY
32+
user_id;
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package userpassword_test
2+
3+
import (
4+
"crypto/sha256"
5+
"testing"
6+
7+
"github.com/coder/coder/cryptorand"
8+
"golang.org/x/crypto/bcrypt"
9+
"golang.org/x/crypto/pbkdf2"
10+
)
11+
12+
var (
13+
salt = []byte(must(cryptorand.String(16)))
14+
secret = []byte(must(cryptorand.String(24)))
15+
16+
resBcrypt []byte
17+
resPbkdf2 []byte
18+
)
19+
20+
func BenchmarkBcryptMinCost(b *testing.B) {
21+
var r []byte
22+
b.ReportAllocs()
23+
24+
for i := 0; i < b.N; i++ {
25+
r, _ = bcrypt.GenerateFromPassword(secret, bcrypt.MinCost)
26+
}
27+
28+
resBcrypt = r
29+
}
30+
31+
func BenchmarkPbkdf2MinCost(b *testing.B) {
32+
var r []byte
33+
b.ReportAllocs()
34+
35+
for i := 0; i < b.N; i++ {
36+
r = pbkdf2.Key(secret, salt, 1024, 64, sha256.New)
37+
}
38+
39+
resPbkdf2 = r
40+
}
41+
42+
func BenchmarkBcryptDefaultCost(b *testing.B) {
43+
var r []byte
44+
b.ReportAllocs()
45+
46+
for i := 0; i < b.N; i++ {
47+
r, _ = bcrypt.GenerateFromPassword(secret, bcrypt.DefaultCost)
48+
}
49+
50+
resBcrypt = r
51+
}
52+
53+
func BenchmarkPbkdf2(b *testing.B) {
54+
var r []byte
55+
b.ReportAllocs()
56+
57+
for i := 0; i < b.N; i++ {
58+
r = pbkdf2.Key(secret, salt, 65536, 64, sha256.New)
59+
}
60+
61+
resPbkdf2 = r
62+
}
63+
64+
func must(s string, err error) string {
65+
if err != nil {
66+
panic(err)
67+
}
68+
69+
return s
70+
}

0 commit comments

Comments
 (0)