fix(auth): harden unified diagnostic execution
This commit is contained in:
@@ -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))
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user