mirror of
https://github.com/coder/coder.git
synced 2025-07-06 15:41:45 +00:00
Adds a flag --disable-password-auth that prevents the password login endpoint from working unless the user has the "owner" (aka. site admin) role. Adds a subcommand `coder server create-admin-user` which creates a user directly in the database with the "owner" role, the "admin" role in every organization, and password auth. This is to avoid lock-out situations where all accounts have the login type set to an identity provider and nobody can login.
270 lines
7.5 KiB
Go
270 lines
7.5 KiB
Go
package cli_test
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"fmt"
|
|
"runtime"
|
|
"testing"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/coder/coder/cli/clitest"
|
|
"github.com/coder/coder/coderd/database"
|
|
"github.com/coder/coder/coderd/database/postgres"
|
|
"github.com/coder/coder/coderd/rbac"
|
|
"github.com/coder/coder/coderd/userpassword"
|
|
"github.com/coder/coder/pty/ptytest"
|
|
"github.com/coder/coder/testutil"
|
|
)
|
|
|
|
//nolint:paralleltest, tparallel
|
|
func TestServerCreateAdminUser(t *testing.T) {
|
|
const (
|
|
username = "dean"
|
|
email = "dean@example.com"
|
|
password = "SecurePa$$word123"
|
|
)
|
|
|
|
verifyUser := func(t *testing.T, dbURL, username, email, password string) {
|
|
t.Helper()
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
defer cancel()
|
|
|
|
sqlDB, err := sql.Open("postgres", dbURL)
|
|
require.NoError(t, err)
|
|
defer sqlDB.Close()
|
|
db := database.New(sqlDB)
|
|
|
|
pingCtx, pingCancel := context.WithTimeout(ctx, testutil.WaitShort)
|
|
defer pingCancel()
|
|
_, err = db.Ping(pingCtx)
|
|
require.NoError(t, err, "ping db")
|
|
|
|
user, err := db.GetUserByEmailOrUsername(ctx, database.GetUserByEmailOrUsernameParams{
|
|
Email: email,
|
|
})
|
|
require.NoError(t, err)
|
|
require.Equal(t, username, user.Username, "username does not match")
|
|
require.Equal(t, email, user.Email, "email does not match")
|
|
|
|
ok, err := userpassword.Compare(string(user.HashedPassword), password)
|
|
require.NoError(t, err)
|
|
require.True(t, ok, "password does not match")
|
|
|
|
require.EqualValues(t, []string{rbac.RoleOwner()}, user.RBACRoles, "user does not have owner role")
|
|
|
|
// Check that user is admin in every org.
|
|
orgs, err := db.GetOrganizations(ctx)
|
|
require.NoError(t, err)
|
|
orgIDs := make(map[uuid.UUID]struct{}, len(orgs))
|
|
for _, org := range orgs {
|
|
orgIDs[org.ID] = struct{}{}
|
|
}
|
|
|
|
orgMemberships, err := db.GetOrganizationMembershipsByUserID(ctx, user.ID)
|
|
require.NoError(t, err)
|
|
orgIDs2 := make(map[uuid.UUID]struct{}, len(orgMemberships))
|
|
for _, membership := range orgMemberships {
|
|
orgIDs2[membership.OrganizationID] = struct{}{}
|
|
assert.Equal(t, []string{rbac.RoleOrgAdmin(membership.OrganizationID)}, membership.Roles, "user is not org admin")
|
|
}
|
|
|
|
require.Equal(t, orgIDs, orgIDs2, "user is not in all orgs")
|
|
}
|
|
|
|
t.Run("OK", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
if runtime.GOOS != "linux" || testing.Short() {
|
|
// Skip on non-Linux because it spawns a PostgreSQL instance.
|
|
t.SkipNow()
|
|
}
|
|
connectionURL, closeFunc, err := postgres.Open()
|
|
require.NoError(t, err)
|
|
defer closeFunc()
|
|
ctx, cancelFunc := context.WithCancel(context.Background())
|
|
defer cancelFunc()
|
|
|
|
sqlDB, err := sql.Open("postgres", connectionURL)
|
|
require.NoError(t, err)
|
|
defer sqlDB.Close()
|
|
db := database.New(sqlDB)
|
|
|
|
pingCtx, pingCancel := context.WithTimeout(ctx, testutil.WaitShort)
|
|
defer pingCancel()
|
|
_, err = db.Ping(pingCtx)
|
|
require.NoError(t, err, "ping db")
|
|
|
|
// Insert a few orgs.
|
|
org1Name, org1ID := "org1", uuid.New()
|
|
org2Name, org2ID := "org2", uuid.New()
|
|
_, err = db.InsertOrganization(ctx, database.InsertOrganizationParams{
|
|
ID: org1ID,
|
|
Name: org1Name,
|
|
CreatedAt: database.Now(),
|
|
UpdatedAt: database.Now(),
|
|
})
|
|
require.NoError(t, err)
|
|
_, err = db.InsertOrganization(ctx, database.InsertOrganizationParams{
|
|
ID: org2ID,
|
|
Name: org2Name,
|
|
CreatedAt: database.Now(),
|
|
UpdatedAt: database.Now(),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
root, _ := clitest.New(t,
|
|
"server", "create-admin-user",
|
|
"--postgres-url", connectionURL,
|
|
"--ssh-keygen-algorithm", "ed25519",
|
|
"--username", username,
|
|
"--email", email,
|
|
"--password", password,
|
|
)
|
|
pty := ptytest.New(t)
|
|
root.SetOutput(pty.Output())
|
|
root.SetErr(pty.Output())
|
|
errC := make(chan error, 1)
|
|
go func() {
|
|
err := root.ExecuteContext(ctx)
|
|
t.Log("root.ExecuteContext() returned:", err)
|
|
errC <- err
|
|
}()
|
|
|
|
pty.ExpectMatch("Creating user...")
|
|
pty.ExpectMatch("Generating user SSH key...")
|
|
pty.ExpectMatch(fmt.Sprintf("Adding user to organization %q (%s) as admin...", org1Name, org1ID.String()))
|
|
pty.ExpectMatch(fmt.Sprintf("Adding user to organization %q (%s) as admin...", org2Name, org2ID.String()))
|
|
pty.ExpectMatch("User created successfully.")
|
|
pty.ExpectMatch(username)
|
|
pty.ExpectMatch(email)
|
|
pty.ExpectMatch("****")
|
|
|
|
require.NoError(t, <-errC)
|
|
|
|
verifyUser(t, connectionURL, username, email, password)
|
|
})
|
|
|
|
//nolint:paralleltest
|
|
t.Run("Env", func(t *testing.T) {
|
|
if runtime.GOOS != "linux" || testing.Short() {
|
|
// Skip on non-Linux because it spawns a PostgreSQL instance.
|
|
t.SkipNow()
|
|
}
|
|
connectionURL, closeFunc, err := postgres.Open()
|
|
require.NoError(t, err)
|
|
defer closeFunc()
|
|
ctx, cancelFunc := context.WithCancel(context.Background())
|
|
defer cancelFunc()
|
|
|
|
t.Setenv("CODER_POSTGRES_URL", connectionURL)
|
|
t.Setenv("CODER_SSH_KEYGEN_ALGORITHM", "ecdsa")
|
|
t.Setenv("CODER_USERNAME", username)
|
|
t.Setenv("CODER_EMAIL", email)
|
|
t.Setenv("CODER_PASSWORD", password)
|
|
|
|
root, _ := clitest.New(t, "server", "create-admin-user")
|
|
pty := ptytest.New(t)
|
|
root.SetOutput(pty.Output())
|
|
root.SetErr(pty.Output())
|
|
errC := make(chan error, 1)
|
|
go func() {
|
|
err := root.ExecuteContext(ctx)
|
|
t.Log("root.ExecuteContext() returned:", err)
|
|
errC <- err
|
|
}()
|
|
|
|
pty.ExpectMatch("User created successfully.")
|
|
pty.ExpectMatch(username)
|
|
pty.ExpectMatch(email)
|
|
pty.ExpectMatch("****")
|
|
|
|
require.NoError(t, <-errC)
|
|
|
|
verifyUser(t, connectionURL, username, email, password)
|
|
})
|
|
|
|
t.Run("Stdin", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
if runtime.GOOS != "linux" || testing.Short() {
|
|
// Skip on non-Linux because it spawns a PostgreSQL instance.
|
|
t.SkipNow()
|
|
}
|
|
connectionURL, closeFunc, err := postgres.Open()
|
|
require.NoError(t, err)
|
|
defer closeFunc()
|
|
ctx, cancelFunc := context.WithCancel(context.Background())
|
|
defer cancelFunc()
|
|
|
|
root, _ := clitest.New(t,
|
|
"server", "create-admin-user",
|
|
"--postgres-url", connectionURL,
|
|
"--ssh-keygen-algorithm", "rsa4096",
|
|
)
|
|
pty := ptytest.New(t)
|
|
root.SetIn(pty.Input())
|
|
root.SetOutput(pty.Output())
|
|
root.SetErr(pty.Output())
|
|
errC := make(chan error, 1)
|
|
go func() {
|
|
err := root.ExecuteContext(ctx)
|
|
t.Log("root.ExecuteContext() returned:", err)
|
|
errC <- err
|
|
}()
|
|
|
|
pty.ExpectMatch("> Username")
|
|
pty.WriteLine(username)
|
|
pty.ExpectMatch("> Email")
|
|
pty.WriteLine(email)
|
|
pty.ExpectMatch("> Password")
|
|
pty.WriteLine(password)
|
|
pty.ExpectMatch("> Confirm password")
|
|
pty.WriteLine(password)
|
|
|
|
pty.ExpectMatch("User created successfully.")
|
|
pty.ExpectMatch(username)
|
|
pty.ExpectMatch(email)
|
|
pty.ExpectMatch("****")
|
|
|
|
require.NoError(t, <-errC)
|
|
|
|
verifyUser(t, connectionURL, username, email, password)
|
|
})
|
|
|
|
t.Run("Validates", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
if runtime.GOOS != "linux" || testing.Short() {
|
|
// Skip on non-Linux because it spawns a PostgreSQL instance.
|
|
t.SkipNow()
|
|
}
|
|
connectionURL, closeFunc, err := postgres.Open()
|
|
require.NoError(t, err)
|
|
defer closeFunc()
|
|
ctx, cancelFunc := context.WithCancel(context.Background())
|
|
defer cancelFunc()
|
|
|
|
root, _ := clitest.New(t,
|
|
"server", "create-admin-user",
|
|
"--postgres-url", connectionURL,
|
|
"--ssh-keygen-algorithm", "rsa4096",
|
|
"--username", "$",
|
|
"--email", "not-an-email",
|
|
"--password", "x",
|
|
)
|
|
pty := ptytest.New(t)
|
|
root.SetOutput(pty.Output())
|
|
root.SetErr(pty.Output())
|
|
|
|
err = root.ExecuteContext(ctx)
|
|
require.Error(t, err)
|
|
require.ErrorContains(t, err, "'email' failed on the 'email' tag")
|
|
require.ErrorContains(t, err, "'username' failed on the 'username' tag")
|
|
})
|
|
}
|