mirror of
https://github.com/coder/coder.git
synced 2025-07-13 21:36:50 +00:00
feat: add flag to disable password auth (#5991)
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.
This commit is contained in:
269
cli/server_createadminuser_test.go
Normal file
269
cli/server_createadminuser_test.go
Normal file
@ -0,0 +1,269 @@
|
||||
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")
|
||||
})
|
||||
}
|
Reference in New Issue
Block a user