feat(server): activate projected authentication safely

This commit is contained in:
User
2026-08-22 01:01:36 +02:00
parent 903c0b4de5
commit 3d9a9f0675
32 changed files with 1837 additions and 38 deletions
+143 -2
View File
@@ -22,6 +22,7 @@ import (
"unicode/utf16"
"unicode/utf8"
"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/output"
@@ -46,6 +47,8 @@ var errCommandRefused = errors.New("authentication command refused")
var writeNewAuthFile = safeio.WriteCanonicalNewFile
var removeAuthFile = safeio.RemoveCanonicalPrivateRegular
var runProjectedAuthMutation = RunProjectedMutation
var publishProjectedAuthCanonical = PublishProjectedCanonical
// Run implements the host-only authentication operator surface. It accepts password bytes only
// from an echo-free terminal or a bounded private file, and never writes them to either stream.
@@ -62,27 +65,165 @@ func RunWithRunner(ctx context.Context, installation config.Installation, args [
directory := installation.AuthenticationDirectory()
switch args[0] {
case "configure":
if err := configure(directory, args[1:], stdin, stderr); err != nil {
if err := runInstallationAuthMutation(ctx, installation, func() error {
return configure(directory, args[1:], stdin, stderr)
}); err != nil {
return authFailure(stderr, authMessage(err))
}
return 0
case "publish":
if err := publishInstallationAuthentication(ctx, installation, args[1:]); err != nil {
return authFailure(stderr, authMessage(err))
}
return 0
case "status":
if installation.HasRuntimeAuthProjection() {
if err := projectedStatus(installation, args[1:], stdout); err != nil {
return authFailure(stderr, authMessage(err))
}
return 0
}
if err := status(directory, args[1:], stdout); err != nil {
return authFailure(stderr, authMessage(err))
}
return 0
case "user":
if err := user(directory, args[1:], stdin, stdout, stderr); err != nil {
if err := runInstallationUserMutation(ctx, installation, args[1:], func() error {
return user(directory, args[1:], stdin, stdout, stderr)
}); err != nil {
return authFailure(stderr, authMessage(err))
}
return 0
case "check":
if err := RequireRuntimeAuthProjectionReady(installation); err != nil {
return authFailure(stderr, authMessage(err))
}
return checkCommand(ctx, installation, args[1:], stdout, stderr, runner)
default:
return authFailure(stderr, "unknown auth subcommand")
}
}
// RuntimeAuthProjectionStatus reads only public runtime-projection metadata and compares it with
// a detached, validated snapshot of the canonical authentication store. It never publishes or
// repairs the projection.
func RuntimeAuthProjectionStatus(installation config.Installation) (ProjectionStatus, error) {
projection := installation.RuntimeAuthProjection()
if projection == nil {
return ProjectionStatus{}, errCommandRefused
}
canonical, err := loadSnapshotBytes(installation.AuthenticationDirectory())
if err != nil {
return ProjectionStatus{}, errCommandRefused
}
published, err := authprojection.Inspect(authprojection.Spec{
RuntimeRoot: projection.Directory,
UID: projection.UID,
GID: projection.GID,
})
if errors.Is(err, authprojection.ErrBlocked) {
return ProjectionStatus{
State: "blocked",
CanonicalRevision: canonical.CanonicalRevision,
Equal: false,
}, nil
}
if err != nil {
return ProjectionStatus{}, errCommandRefused
}
return ProjectionStatus{
State: published.Selector.State,
Generation: published.Snapshot.Generation,
CanonicalRevision: canonical.CanonicalRevision,
Equal: equalProjection(published, canonical),
}, nil
}
// RequireRuntimeAuthProjectionReady is the shared fail-closed pre-admission check. It is a no-op
// for an installation that does not declare a runtime projection.
func RequireRuntimeAuthProjectionReady(installation config.Installation) error {
if !installation.HasRuntimeAuthProjection() {
return nil
}
status, err := RuntimeAuthProjectionStatus(installation)
if err != nil || status.State != "ready" || !status.Equal {
return errCommandRefused
}
return nil
}
func runInstallationAuthMutation(ctx context.Context, installation config.Installation, mutate func() error) error {
projection := installation.RuntimeAuthProjection()
if projection == nil {
return mutate()
}
if err := requireProjectedAuthMutationPrivilege(); err != nil {
return errCommandRefused
}
return runProjectedAuthMutation(ctx, installation.AuthenticationDirectory(), ProjectionSpec{
RuntimeRoot: projection.Directory,
UID: projection.UID,
GID: projection.GID,
}, mutate)
}
func runInstallationUserMutation(ctx context.Context, installation config.Installation, args []string, mutate func() error) error {
if len(args) == 0 || args[0] == "list" || !installation.HasRuntimeAuthProjection() {
return mutate()
}
switch args[0] {
case "add", "set-password", "enable", "disable", "grant", "revoke", "logout-all":
return runInstallationAuthMutation(ctx, installation, mutate)
default:
return mutate()
}
}
func publishInstallationAuthentication(ctx context.Context, installation config.Installation, args []string) error {
if len(args) != 0 || !installation.HasRuntimeAuthProjection() {
return errCommandRefused
}
if err := requireProjectedAuthMutationPrivilege(); err != nil {
return errCommandRefused
}
projection := installation.RuntimeAuthProjection()
status, err := publishProjectedAuthCanonical(ctx, installation.AuthenticationDirectory(), ProjectionSpec{
RuntimeRoot: projection.Directory,
UID: projection.UID,
GID: projection.GID,
})
if err != nil || status.State != "ready" || !status.Equal {
return errCommandRefused
}
return nil
}
func projectedStatus(installation config.Installation, args []string, stdout io.Writer) error {
jsonMode := len(args) == 1 && args[0] == "--json"
if len(args) != 0 && !jsonMode {
return errCommandRefused
}
status, err := RuntimeAuthProjectionStatus(installation)
if err != nil {
return errCommandRefused
}
if jsonMode {
return json.NewEncoder(stdout).Encode(struct {
State string `json:"state"`
Generation string `json:"generation"`
CanonicalRevision string `json:"canonicalRevision"`
Equal bool `json:"equal"`
}{
State: status.State,
Generation: status.Generation,
CanonicalRevision: status.CanonicalRevision,
Equal: status.Equal,
})
}
_, err = fmt.Fprintf(stdout, "State: %s\nGeneration: %s\nCanonical revision: %s\nEqual: %t\n", status.State, status.Generation, status.CanonicalRevision, status.Equal)
return err
}
// AuthDiagnostic is the closed JSON contract emitted by the backend diagnostic command.
type AuthDiagnostic struct {
Level string `json:"level"`
@@ -0,0 +1,372 @@
//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
}
@@ -0,0 +1,14 @@
//go:build linux
package authconfig
import "os"
var authProjectionEffectiveUID = os.Geteuid
func requireProjectedAuthMutationPrivilege() error {
if authProjectionEffectiveUID() != 0 {
return errCommandRefused
}
return nil
}
@@ -0,0 +1,5 @@
//go:build !linux
package authconfig
func requireProjectedAuthMutationPrivilege() error { return errCommandRefused }