373 lines
18 KiB
Go
373 lines
18 KiB
Go
//go:build linux
|
|
|
|
package authconfig
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/aritmolab/thothii/tools/tht/internal/authprojection"
|
|
"github.com/aritmolab/thothii/tools/tht/internal/compose"
|
|
"github.com/aritmolab/thothii/tools/tht/internal/config"
|
|
"github.com/aritmolab/thothii/tools/tht/internal/safeio"
|
|
)
|
|
|
|
func TestProjectedAuthMutatorsBlockBeforeCanonicalWriteAndPublishOnlyEqualSnapshots(t *testing.T) {
|
|
installation, spec := projectedAuthInstallation(t)
|
|
adminPassword := writePasswordFile(t, "initial projected administrator password\n")
|
|
userPassword := writePasswordFile(t, "projected ordinary user password\n")
|
|
previous := runProjectedAuthMutation
|
|
blockedObservations := 0
|
|
runProjectedAuthMutation = func(ctx context.Context, canonicalRoot string, projection ProjectionSpec, mutate func() error) error {
|
|
return previous(ctx, canonicalRoot, projection, func() error {
|
|
status, err := authprojection.Inspect(authprojection.Spec{RuntimeRoot: projection.RuntimeRoot, UID: projection.UID, GID: projection.GID})
|
|
if !errors.Is(err, authprojection.ErrBlocked) || status.Selector.State != "blocked" {
|
|
t.Fatalf("projection before canonical mutation = %#v, %v; want blocked", status, err)
|
|
}
|
|
blockedObservations++
|
|
return mutate()
|
|
})
|
|
}
|
|
t.Cleanup(func() { runProjectedAuthMutation = previous })
|
|
|
|
for _, args := range [][]string{
|
|
{"configure", "--mode", "local", "--public-url", "http://127.0.0.1:8080", "--admin-user", "admin", "--password-file", adminPassword},
|
|
{"user", "add", "operator", "--role", "user", "--password-file", userPassword},
|
|
{"user", "set-password", "operator", "--password-file", adminPassword},
|
|
{"user", "disable", "operator"},
|
|
{"user", "enable", "operator"},
|
|
{"user", "grant", "operator", "--role", "admin"},
|
|
{"user", "revoke", "operator", "--role", "admin"},
|
|
{"user", "logout-all", "operator", "--yes"},
|
|
} {
|
|
var stdout, stderr bytes.Buffer
|
|
if code := Run(context.Background(), installation, args, strings.NewReader(""), &stdout, &stderr); code != 0 {
|
|
t.Fatalf("%v = %d, stdout=%q stderr=%q", args, code, stdout.String(), stderr.String())
|
|
}
|
|
assertProjectedCanonicalReadyAndEqual(t, installation.AuthenticationDirectory(), spec)
|
|
if strings.Contains(stdout.String()+stderr.String(), "projected administrator password") || strings.Contains(stdout.String()+stderr.String(), "$argon2id$") {
|
|
t.Fatalf("%v leaked secret material", args)
|
|
}
|
|
}
|
|
if blockedObservations != 8 {
|
|
t.Fatalf("blocked observations = %d, want 8", blockedObservations)
|
|
}
|
|
}
|
|
|
|
func TestProjectedAuthMutationLeavesBlockedAfterChangedCanonicalFailure(t *testing.T) {
|
|
installation, spec := projectedAuthInstallation(t)
|
|
adminPassword := writePasswordFile(t, "initial projected administrator password\n")
|
|
if code := Run(context.Background(), installation, []string{"configure", "--mode", "local", "--public-url", "http://127.0.0.1:8080", "--admin-user", "admin", "--password-file", adminPassword}, strings.NewReader(""), ioDiscard{}, ioDiscard{}); code != 0 {
|
|
t.Fatalf("configure = %d", code)
|
|
}
|
|
previous := runProjectedAuthMutation
|
|
runProjectedAuthMutation = func(ctx context.Context, canonicalRoot string, projection ProjectionSpec, mutate func() error) error {
|
|
return previous(ctx, canonicalRoot, projection, func() error {
|
|
if err := mutate(); err != nil {
|
|
return err
|
|
}
|
|
return errors.New("synthetic-password-sentinel")
|
|
})
|
|
}
|
|
t.Cleanup(func() { runProjectedAuthMutation = previous })
|
|
|
|
var stdout, stderr bytes.Buffer
|
|
code := Run(context.Background(), installation, []string{"user", "logout-all", "admin", "--yes"}, strings.NewReader(""), &stdout, &stderr)
|
|
if code == 0 || strings.Contains(stdout.String()+stderr.String(), "synthetic-password-sentinel") {
|
|
t.Fatalf("changed mutation failure was accepted or leaked: code=%d stdout=%q stderr=%q", code, stdout.String(), stderr.String())
|
|
}
|
|
_, err := authprojection.Inspect(authprojection.Spec{RuntimeRoot: spec.RuntimeRoot, UID: spec.UID, GID: spec.GID})
|
|
if !errors.Is(err, authprojection.ErrBlocked) {
|
|
t.Fatalf("Inspect() error = %v, want blocked runtime projection", err)
|
|
}
|
|
}
|
|
|
|
func TestProjectedAuthMutatorFailuresRestoreOnlyUnchangedCanonicalState(t *testing.T) {
|
|
mutators := []string{"configure", "user add", "user set-password", "user enable", "user disable", "user grant", "user revoke", "user logout-all"}
|
|
for _, mutator := range mutators {
|
|
t.Run(mutator+" restores ready before callback", func(t *testing.T) {
|
|
installation, spec, args := preparedProjectedMutation(t, mutator)
|
|
previous := runProjectedAuthMutation
|
|
runProjectedAuthMutation = func(ctx context.Context, canonicalRoot string, projection ProjectionSpec, mutate func() error) error {
|
|
return previous(ctx, canonicalRoot, projection, func() error {
|
|
return errors.New("synthetic-password-sentinel")
|
|
})
|
|
}
|
|
t.Cleanup(func() { runProjectedAuthMutation = previous })
|
|
|
|
var stdout, stderr bytes.Buffer
|
|
if code := Run(context.Background(), installation, args, strings.NewReader(""), &stdout, &stderr); code == 0 {
|
|
t.Fatalf("%s accepted injected pre-mutation failure", mutator)
|
|
}
|
|
if strings.Contains(stdout.String()+stderr.String(), "synthetic-password-sentinel") {
|
|
t.Fatalf("%s leaked injected failure: stdout=%q stderr=%q", mutator, stdout.String(), stderr.String())
|
|
}
|
|
assertProjectedCanonicalReadyAndEqual(t, installation.AuthenticationDirectory(), spec)
|
|
})
|
|
|
|
t.Run(mutator+" leaves blocked after changed canonical error", func(t *testing.T) {
|
|
var installation config.Installation
|
|
var spec ProjectionSpec
|
|
var args []string
|
|
if mutator == "configure" {
|
|
installation, spec = projectedAuthInstallation(t)
|
|
passwordFile := writePasswordFile(t, "fresh projected bootstrap password\n")
|
|
args = []string{"configure", "--mode", "local", "--public-url", "http://127.0.0.1:8080", "--admin-user", "admin", "--password-file", passwordFile}
|
|
} else {
|
|
installation, spec, args = preparedProjectedMutation(t, mutator)
|
|
}
|
|
previous := runProjectedAuthMutation
|
|
runProjectedAuthMutation = func(ctx context.Context, canonicalRoot string, projection ProjectionSpec, mutate func() error) error {
|
|
return previous(ctx, canonicalRoot, projection, func() error {
|
|
if err := mutate(); err != nil {
|
|
return err
|
|
}
|
|
return errors.New("synthetic-password-sentinel")
|
|
})
|
|
}
|
|
t.Cleanup(func() { runProjectedAuthMutation = previous })
|
|
|
|
var stdout, stderr bytes.Buffer
|
|
if code := Run(context.Background(), installation, args, strings.NewReader(""), &stdout, &stderr); code == 0 {
|
|
t.Fatalf("%s accepted changed-canonical injected failure", mutator)
|
|
}
|
|
if strings.Contains(stdout.String()+stderr.String(), "synthetic-password-sentinel") {
|
|
t.Fatalf("%s leaked injected failure: stdout=%q stderr=%q", mutator, stdout.String(), stderr.String())
|
|
}
|
|
_, err := authprojection.Inspect(authprojection.Spec{RuntimeRoot: spec.RuntimeRoot, UID: spec.UID, GID: spec.GID})
|
|
if !errors.Is(err, authprojection.ErrBlocked) {
|
|
t.Fatalf("%s Inspect() error = %v, want blocked projection", mutator, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestProjectedAuthPublishStatusAndCheckFailClosed(t *testing.T) {
|
|
installation, spec := projectedAuthInstallation(t)
|
|
passwordFile := writePasswordFile(t, "projected authentication password\n")
|
|
if code := Run(context.Background(), installation, []string{"configure", "--mode", "local", "--public-url", "http://127.0.0.1:8080", "--admin-user", "admin", "--password-file", passwordFile}, strings.NewReader(""), ioDiscard{}, ioDiscard{}); code != 0 {
|
|
t.Fatalf("configure = %d", code)
|
|
}
|
|
var stdout, stderr bytes.Buffer
|
|
if code := Run(context.Background(), installation, []string{"publish"}, strings.NewReader(""), &stdout, &stderr); code != 0 {
|
|
t.Fatalf("publish = %d, stdout=%q stderr=%q", code, stdout.String(), stderr.String())
|
|
}
|
|
if code := Run(context.Background(), installation, []string{"publish", "secret"}, strings.NewReader(""), ioDiscard{}, ioDiscard{}); code == 0 {
|
|
t.Fatal("publish accepted content arguments")
|
|
}
|
|
stdout.Reset()
|
|
if code := Run(context.Background(), installation, []string{"status", "--json"}, strings.NewReader(""), &stdout, &stderr); code != 0 {
|
|
t.Fatalf("status = %d, stderr=%q", code, stderr.String())
|
|
}
|
|
var raw map[string]json.RawMessage
|
|
if err := json.Unmarshal(stdout.Bytes(), &raw); err != nil {
|
|
t.Fatalf("status JSON = %q, %v", stdout.String(), err)
|
|
}
|
|
if len(raw) != 4 {
|
|
t.Fatalf("status keys = %#v, want exactly projection public keys", raw)
|
|
}
|
|
for _, key := range []string{"state", "generation", "canonicalRevision", "equal"} {
|
|
if _, ok := raw[key]; !ok {
|
|
t.Fatalf("status keys = %#v, missing %q", raw, key)
|
|
}
|
|
}
|
|
var status struct {
|
|
State string `json:"state"`
|
|
Generation string `json:"generation"`
|
|
CanonicalRevision string `json:"canonicalRevision"`
|
|
Equal bool `json:"equal"`
|
|
}
|
|
if err := json.Unmarshal(stdout.Bytes(), &status); err != nil || status.State != "ready" || !status.Equal || status.Generation == "" || status.CanonicalRevision == "" {
|
|
t.Fatalf("status = %q, %#v, %v", stdout.String(), status, err)
|
|
}
|
|
if strings.Contains(stdout.String(), "password") || strings.Contains(stdout.String(), "$argon2id$") {
|
|
t.Fatalf("status exposed secret material: %q", stdout.String())
|
|
}
|
|
|
|
transaction, err := BeginExternalProjectionTransaction(context.Background(), installation.AuthenticationDirectory(), spec)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer transaction.Close()
|
|
canonical, err := loadSnapshotBytes(installation.AuthenticationDirectory())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
stdout.Reset()
|
|
stderr.Reset()
|
|
if code := Run(context.Background(), installation, []string{"status", "--json"}, strings.NewReader(""), &stdout, &stderr); code != 0 {
|
|
t.Fatalf("blocked status JSON = %d, stderr=%q", code, stderr.String())
|
|
}
|
|
raw = nil
|
|
if err := json.Unmarshal(stdout.Bytes(), &raw); err != nil {
|
|
t.Fatalf("blocked status JSON = %q, %v", stdout.String(), err)
|
|
}
|
|
if len(raw) != 4 {
|
|
t.Fatalf("blocked status keys = %#v, want exactly projection public keys", raw)
|
|
}
|
|
for _, key := range []string{"state", "generation", "canonicalRevision", "equal"} {
|
|
if _, ok := raw[key]; !ok {
|
|
t.Fatalf("blocked status keys = %#v, missing %q", raw, key)
|
|
}
|
|
}
|
|
status = struct {
|
|
State string `json:"state"`
|
|
Generation string `json:"generation"`
|
|
CanonicalRevision string `json:"canonicalRevision"`
|
|
Equal bool `json:"equal"`
|
|
}{}
|
|
if err := json.Unmarshal(stdout.Bytes(), &status); err != nil || status.State != "blocked" || status.Generation != "" || status.CanonicalRevision != canonical.CanonicalRevision || status.Equal {
|
|
t.Fatalf("blocked status = %q, %#v, %v", stdout.String(), status, err)
|
|
}
|
|
stdout.Reset()
|
|
stderr.Reset()
|
|
if code := Run(context.Background(), installation, []string{"status"}, strings.NewReader(""), &stdout, &stderr); code != 0 {
|
|
t.Fatalf("blocked status text = %d, stderr=%q", code, stderr.String())
|
|
}
|
|
wantText := "State: blocked\nGeneration: \nCanonical revision: " + canonical.CanonicalRevision + "\nEqual: false\n"
|
|
if stdout.String() != wantText {
|
|
t.Fatalf("blocked status text = %q, want %q", stdout.String(), wantText)
|
|
}
|
|
|
|
calls := 0
|
|
runner := runnerFunc(func(_ context.Context, _ []string, _ io.Reader) (compose.Result, error) {
|
|
calls++
|
|
return compose.Result{}, errors.New("backend diagnostic must not run")
|
|
})
|
|
stdout.Reset()
|
|
stderr.Reset()
|
|
if code := RunWithRunner(context.Background(), installation, []string{"check", "--json"}, strings.NewReader(""), &stdout, &stderr, runner); code == 0 || calls != 0 {
|
|
t.Fatalf("check admitted blocked projection: code=%d calls=%d stdout=%q stderr=%q", code, calls, stdout.String(), stderr.String())
|
|
}
|
|
}
|
|
|
|
func TestRequireRuntimeAuthProjectionReadyRejectsMissingBlockedAndDivergentStates(t *testing.T) {
|
|
for _, state := range []string{"missing", "blocked", "divergent"} {
|
|
t.Run(state, func(t *testing.T) {
|
|
installation, spec := projectedAuthInstallation(t)
|
|
passwordFile := writePasswordFile(t, "projected readiness password\n")
|
|
if code := Run(context.Background(), installation, []string{"configure", "--mode", "local", "--public-url", "http://127.0.0.1:8080", "--admin-user", "admin", "--password-file", passwordFile}, strings.NewReader(""), ioDiscard{}, ioDiscard{}); code != 0 {
|
|
t.Fatalf("configure = %d", code)
|
|
}
|
|
if err := RequireRuntimeAuthProjectionReady(installation); err != nil {
|
|
t.Fatalf("ready projection rejected: %v", err)
|
|
}
|
|
switch state {
|
|
case "missing":
|
|
if err := os.RemoveAll(spec.RuntimeRoot); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
case "blocked":
|
|
transaction, err := BeginExternalProjectionTransaction(context.Background(), installation.AuthenticationDirectory(), spec)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() { _ = transaction.Close() })
|
|
case "divergent":
|
|
if err := MutateUsers(installation.AuthenticationDirectory(), func(registry *Registry) error {
|
|
registry.Users[0].AuthRevision++
|
|
return nil
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
if err := RequireRuntimeAuthProjectionReady(installation); err == nil {
|
|
t.Fatalf("RequireRuntimeAuthProjectionReady accepted %s projection", state)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestProjectedAuthCommandsRefuseBeforeMutationWhenNotRoot(t *testing.T) {
|
|
installation, _ := projectedAuthInstallation(t)
|
|
passwordFile := writePasswordFile(t, "projected authentication password\n")
|
|
previous := authProjectionEffectiveUID
|
|
authProjectionEffectiveUID = func() int { return 1000 }
|
|
t.Cleanup(func() { authProjectionEffectiveUID = previous })
|
|
if code := Run(context.Background(), installation, []string{"configure", "--mode", "local", "--public-url", "http://127.0.0.1:8080", "--admin-user", "admin", "--password-file", passwordFile}, strings.NewReader(""), ioDiscard{}, ioDiscard{}); code == 0 {
|
|
t.Fatal("non-root projected configure succeeded")
|
|
}
|
|
if _, err := os.Lstat(filepath.Join(installation.AuthenticationDirectory(), authFileName)); !errors.Is(err, os.ErrNotExist) {
|
|
t.Fatalf("canonical auth was written after non-root refusal: %v", err)
|
|
}
|
|
}
|
|
|
|
func projectedAuthInstallation(t *testing.T) (config.Installation, ProjectionSpec) {
|
|
t.Helper()
|
|
root := t.TempDir()
|
|
canonicalRoot, runtimeRoot := filepath.Join(root, "canonical-auth"), filepath.Join(root, "runtime-auth")
|
|
for _, directory := range []string{canonicalRoot, runtimeRoot} {
|
|
if err := safeio.EnsurePrivateDirectory(directory); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
if err := os.WriteFile(filepath.Join(root, "operator.env"), []byte("SAFE_VALUE=1\n"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
uid, gid := uint32(os.Geteuid()), uint32(os.Getegid())
|
|
installation := config.Installation{Profile: "server", EnvFile: filepath.Join(root, "operator.env"), Authentication: config.Authentication{
|
|
ConfigDirectory: canonicalRoot,
|
|
RuntimeProjection: &config.RuntimeProjection{Directory: runtimeRoot, UID: uid, GID: gid},
|
|
}}
|
|
return installation, ProjectionSpec{RuntimeRoot: runtimeRoot, UID: uid, GID: gid}
|
|
}
|
|
|
|
func assertProjectedCanonicalReadyAndEqual(t *testing.T, canonicalRoot string, spec ProjectionSpec) {
|
|
t.Helper()
|
|
status, err := authprojection.Inspect(authprojection.Spec{RuntimeRoot: spec.RuntimeRoot, UID: spec.UID, GID: spec.GID})
|
|
if err != nil || status.Selector.State != "ready" {
|
|
t.Fatalf("Inspect() = %#v, %v; want ready", status, err)
|
|
}
|
|
snapshot, err := loadSnapshotBytes(canonicalRoot)
|
|
if err != nil || status.Snapshot.Generation != snapshot.Generation || status.Snapshot.CanonicalRevision != snapshot.CanonicalRevision {
|
|
t.Fatalf("projection = %#v, canonical = %#v, %v; want equality", status.Snapshot, snapshot, err)
|
|
}
|
|
}
|
|
|
|
func preparedProjectedMutation(t *testing.T, mutator string) (config.Installation, ProjectionSpec, []string) {
|
|
t.Helper()
|
|
installation, spec := projectedAuthInstallation(t)
|
|
adminPassword := writePasswordFile(t, "prepared projected administrator password\n")
|
|
userPassword := writePasswordFile(t, "prepared projected user password\n")
|
|
configure := []string{"configure", "--mode", "local", "--public-url", "http://127.0.0.1:8080", "--admin-user", "admin", "--password-file", adminPassword}
|
|
if code := Run(context.Background(), installation, configure, strings.NewReader(""), ioDiscard{}, ioDiscard{}); code != 0 {
|
|
t.Fatalf("prepare %s configure = %d", mutator, code)
|
|
}
|
|
if mutator == "configure" {
|
|
return installation, spec, configure
|
|
}
|
|
if code := Run(context.Background(), installation, []string{"user", "add", "operator", "--role", "user", "--password-file", userPassword}, strings.NewReader(""), ioDiscard{}, ioDiscard{}); code != 0 {
|
|
t.Fatalf("prepare %s add = %d", mutator, code)
|
|
}
|
|
if mutator == "user enable" {
|
|
if code := Run(context.Background(), installation, []string{"user", "disable", "operator"}, strings.NewReader(""), ioDiscard{}, ioDiscard{}); code != 0 {
|
|
t.Fatalf("prepare %s disable = %d", mutator, code)
|
|
}
|
|
}
|
|
if mutator == "user revoke" {
|
|
if code := Run(context.Background(), installation, []string{"user", "grant", "operator", "--role", "admin"}, strings.NewReader(""), ioDiscard{}, ioDiscard{}); code != 0 {
|
|
t.Fatalf("prepare %s grant = %d", mutator, code)
|
|
}
|
|
}
|
|
args := map[string][]string{
|
|
"user add": {"user", "add", "second", "--role", "user", "--password-file", userPassword},
|
|
"user set-password": {"user", "set-password", "operator", "--password-file", adminPassword},
|
|
"user enable": {"user", "enable", "operator"},
|
|
"user disable": {"user", "disable", "operator"},
|
|
"user grant": {"user", "grant", "operator", "--role", "admin"},
|
|
"user revoke": {"user", "revoke", "operator", "--role", "admin"},
|
|
"user logout-all": {"user", "logout-all", "operator", "--yes"},
|
|
}[mutator]
|
|
if args == nil {
|
|
t.Fatalf("unknown projected mutator %q", mutator)
|
|
}
|
|
return installation, spec, args
|
|
}
|