fix(auth): harden unified diagnostic execution

This commit is contained in:
2026-08-17 17:25:26 +02:00
parent 30ee9433dc
commit ef244ab56d
18 changed files with 670 additions and 68 deletions
+20 -10
View File
@@ -101,7 +101,7 @@ type authDiagnosticWire struct {
}
type authDiagnosticsWire struct {
Ready bool `json:"ready"`
Ready *bool `json:"ready"`
Mode string `json:"mode"`
Checks []authDiagnosticWire `json:"checks"`
}
@@ -246,7 +246,10 @@ func decodeAuthDiagnostics(value string) (AuthDiagnostics, error) {
if err := decoder.Decode(&wire); err != nil || decoder.Decode(&struct{}{}) != io.EOF {
return AuthDiagnostics{}, errors.New("authentication diagnostic report is invalid")
}
report := AuthDiagnostics{Ready: wire.Ready, Mode: wire.Mode, Checks: make([]AuthDiagnostic, 0, len(wire.Checks))}
if wire.Ready == nil {
return AuthDiagnostics{}, errors.New("authentication diagnostic report is invalid")
}
report := AuthDiagnostics{Ready: *wire.Ready, Mode: wire.Mode, Checks: make([]AuthDiagnostic, 0, len(wire.Checks))}
for _, item := range wire.Checks {
var field *string
if item.Field != nil {
@@ -266,16 +269,16 @@ func decodeAuthDiagnostics(value string) (AuthDiagnostics, error) {
return report, nil
}
func authenticationSecretValues(installation config.Installation) []string {
func authenticationSecretValues(installation config.Installation) ([]string, error) {
files, err := installation.SecretFiles()
if err != nil {
return nil
return nil, errors.New("authentication diagnostic secret corpus is unavailable")
}
values, err := output.SecretValuesFromFiles(files)
if err != nil {
return nil
return nil, errors.New("authentication diagnostic secret corpus is unavailable")
}
return values
return values, nil
}
func sanitizeAuthDiagnostics(report AuthDiagnostics, secrets []string) AuthDiagnostics {
@@ -339,15 +342,23 @@ func runCheck(ctx context.Context, installation config.Installation, runner comp
if runner == nil {
return AuthDiagnostics{}, "", errors.New("authentication diagnostic runner is unavailable")
}
secrets, err := authenticationSecretValues(installation)
if err != nil {
return AuthDiagnostics{}, "", err
}
timeout := authCheckTimeout
if interactive {
timeout = interactiveAuthCheckTimeout
}
bounded, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
command := []string{"run", "--rm", "--no-deps", "--no-TTY", "core", "node", "dist/auth/diagnostic-command.js", "--json"}
if useRunningCore {
command = []string{"exec", "-T", "core", "node", "dist/auth/diagnostic-command.js", "--json"}
command := []string{"exec", "-T", "core", "node", "dist/auth/diagnostic-command.js", "--json"}
if !useRunningCore {
containerName, err := compose.NewOneShotContainerName("thothii-auth-check")
if err != nil {
return AuthDiagnostics{}, "", errors.New("authentication diagnostic command failed")
}
command = []string{"run", "--rm", "--no-deps", "--no-TTY", "--name", containerName, "core", "node", "dist/auth/diagnostic-command.js", "--json"}
}
if interactive {
command = append(command, "--interactive")
@@ -356,7 +367,6 @@ func runCheck(ctx context.Context, installation config.Installation, runner comp
StdoutBytes: maxAuthDiagnosticOutputBytes,
StderrBytes: maxAuthDiagnosticOutputBytes,
})
secrets := authenticationSecretValues(installation)
var exitError *exec.ExitError
validProcessOutcome := result.ExitCode == 0 && err == nil ||
result.ExitCode == 1 && (err == nil || errors.As(err, &exitError))
+51 -8
View File
@@ -43,10 +43,7 @@ func TestAuthCheckRunsOneShotCoreDiagnosticWithPristineJSON(t *testing.T) {
if len(calls) != 1 {
t.Fatalf("Docker calls = %#v, want one", calls)
}
want := installation.ComposeArgs("run", "--rm", "--no-deps", "--no-TTY", "core", "node", "dist/auth/diagnostic-command.js", "--json")
if strings.Join(calls[0], "\x00") != strings.Join(want, "\x00") {
t.Fatalf("auth check Docker call = %#v, want %#v", calls[0], want)
}
assertAuthOneShotCommand(t, installation, calls[0], false)
}
func TestAuthCheckEmitsValidFailedReportFromRealExitError(t *testing.T) {
@@ -142,6 +139,8 @@ func TestAuthDiagnosticsContractRejectsContradictionsDuplicatesAndAttackerFields
func TestAuthCheckRejectsNullAndUnexpectedDiagnosticFields(t *testing.T) {
for _, report := range []string{
`{"mode":"oidc","checks":[{"level":"error","code":"oidc_secret_missing","message":"failure"}]}`,
`{"ready":null,"mode":"oidc","checks":[{"level":"error","code":"oidc_secret_missing","message":"failure"}]}`,
`{"ready":false,"mode":"oidc","checks":[{"level":"error","code":"oidc_mapped_group_missing","message":"failure","field":null}]}`,
`{"ready":false,"mode":"oidc","checks":[{"level":"error","code":"oidc_secret_missing","message":"failure","unexpected":"attacker"}]}`,
} {
@@ -185,9 +184,27 @@ func TestAuthCheckInteractiveForwardsOnlyTheValidatedDevicePrompt(t *testing.T)
if len(calls) != 1 {
t.Fatalf("Docker calls = %#v, want one", calls)
}
want := installation.ComposeArgs("run", "--rm", "--no-deps", "--no-TTY", "core", "node", "dist/auth/diagnostic-command.js", "--json", "--interactive")
if strings.Join(calls[0], "\x00") != strings.Join(want, "\x00") {
t.Fatalf("interactive auth command = %#v, want %#v", calls[0], want)
assertAuthOneShotCommand(t, installation, calls[0], true)
}
func TestAuthCheckFailsBeforeExecutionWhenDeclaredSecretCorpusIsIncomplete(t *testing.T) {
installation := authInstallation(newAuthDirectory(t))
installation.EnvFile = filepath.Join(t.TempDir(), "operator.env")
missing := filepath.Join(t.TempDir(), "missing-pi-auth.json")
if err := os.WriteFile(installation.EnvFile, []byte("PI_AUTH_FILE="+missing+"\n"), 0o600); err != nil {
t.Fatal(err)
}
calls := 0
runner := runnerFunc(func(_ context.Context, _ []string, _ io.Reader) (compose.Result, error) {
calls++
return compose.Result{Stdout: `{"ready":true,"mode":"oidc","checks":[{"level":"info","code":"auth_ready","message":"Authentication is ready."}]}`}, nil
})
var stdout, stderr bytes.Buffer
code := RunWithRunner(context.Background(), installation, []string{"check", "--json"}, strings.NewReader(""), &stdout, &stderr, runner)
if code != 1 || calls != 0 || stdout.Len() != 0 || stderr.String() != "tht: authentication diagnostics could not be completed\n" {
t.Fatalf("incomplete corpus was not refused before execution: code=%d calls=%d stdout=%q stderr=%q", code, calls, stdout.String(), stderr.String())
}
}
@@ -232,6 +249,29 @@ func (run runnerFunc) Run(ctx context.Context, args []string, input io.Reader) (
return run(ctx, args, input)
}
func assertAuthOneShotCommand(t *testing.T, installation config.Installation, got []string, interactive bool) {
t.Helper()
prefix := installation.ComposeArgs("run", "--rm", "--no-deps", "--no-TTY", "--name")
suffix := []string{"core", "node", "dist/auth/diagnostic-command.js", "--json"}
if interactive {
suffix = append(suffix, "--interactive")
}
if len(got) != len(prefix)+1+len(suffix) ||
strings.Join(got[:len(prefix)], "\x00") != strings.Join(prefix, "\x00") ||
strings.Join(got[len(prefix)+1:], "\x00") != strings.Join(suffix, "\x00") {
t.Fatalf("auth check Docker call = %#v, want named one-shot command", got)
}
name := got[len(prefix)]
if !strings.HasPrefix(name, "thothii-auth-check-") || len(name) != len("thothii-auth-check-")+24 {
t.Fatalf("auth check container name = %q, want unique bounded name", name)
}
for _, character := range name {
if !(character >= 'a' && character <= 'z' || character >= '0' && character <= '9' || character == '-') {
t.Fatalf("auth check container name = %q, want safe characters", name)
}
}
}
func TestRunConfiguresLocalRegistryAndRedactsStatusJSON(t *testing.T) {
directory := newAuthDirectory(t)
passwordFile := writePasswordFile(t, "this is a local test password\n")
@@ -538,7 +578,7 @@ func writeAuthExecutable(t *testing.T, contents string) string {
func stringPointer(value string) *string { return &value }
func authInstallation(directory string) config.Installation {
installation := config.Installation{}
installation := config.Installation{EnvFile: filepath.Join(filepath.Dir(directory), "operator.env")}
installation.Authentication.ConfigDirectory = directory
return installation
}
@@ -549,6 +589,9 @@ func newAuthDirectory(t *testing.T) string {
if 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)
}
return filepath.Join(root, "auth")
}