fix Task1 publication and secret preflight

This commit is contained in:
2026-08-11 04:06:23 +02:00
parent fcc45520ad
commit c7f7a6e1b0
8 changed files with 324 additions and 215 deletions
+64 -23
View File
@@ -107,16 +107,6 @@ func run(ctx context.Context, args []string, stdout, stderr io.Writer) int {
if parseErr != nil {
return commandUsageError(stderr, parseErr.Error())
}
result, operationErr := workspaceops.RunWithProjector(ctx, installation, runner, workspaceCommand, nil, func(result workspaceops.Result) (workspaceops.Result, error) {
return projectWorkspaceResult(result, secretValues)
})
if operationErr != nil {
if workspaceUsageError(operationErr) {
return commandUsageError(stderr, operationErr.Error())
}
fmt.Fprintf(stderr, "thothctl: %s\n", output.Sanitize(operationErr.Error(), secretValues))
return 1
}
jsonMode := true
switch c := workspaceCommand.(type) {
case workspaceops.InspectCommand:
@@ -134,17 +124,29 @@ func run(ctx context.Context, args []string, stdout, stderr io.Writer) int {
case workspaceops.RunRequest:
jsonMode = c.JSON
}
publicResult, projectionErr := projectWorkspaceResult(result, secretValues)
if projectionErr != nil {
fmt.Fprintln(stderr, "thothctl: invalid workspace result")
var finalOutput []byte
result, operationErr := workspaceops.RunWithProjectorAndSecrets(ctx, installation, runner, workspaceCommand, nil, func(result workspaceops.Result) (workspaceops.Result, error) {
return projectWorkspaceResult(result, secretValues)
}, secretValues, func(result workspaceops.Result) error {
var err error
if jsonMode {
finalOutput, err = encodeWorkspaceJSON(result)
} else {
finalOutput, err = encodeWorkspaceHuman(result)
}
if err != nil || len(finalOutput) > maxPublicStdoutBytes {
return errors.New("workspace result exceeds output limit")
}
return nil
})
if operationErr != nil {
if workspaceUsageError(operationErr) {
return commandUsageError(stderr, operationErr.Error())
}
fmt.Fprintf(stderr, "thothctl: %s\n", output.Sanitize(operationErr.Error(), secretValues))
return 1
}
if jsonMode {
if err := writeWorkspaceJSON(stdout, publicResult); err != nil {
fmt.Fprintln(stderr, "thothctl: workspace result exceeds output limit")
return 1
}
} else if err := renderWorkspaceHuman(stdout, publicResult); err != nil {
if _, err := stdout.Write(finalOutput); err != nil {
fmt.Fprintln(stderr, "thothctl: workspace result exceeds output limit")
return 1
}
@@ -268,21 +270,34 @@ func (w *boundedWriter) Write(p []byte) (int, error) {
return n, err
}
func writeWorkspaceJSON(w io.Writer, result workspaceops.Result) error {
func encodeWorkspaceJSON(result workspaceops.Result) ([]byte, error) {
var encoded bytes.Buffer
encoder := json.NewEncoder(&encoded)
// Keep public output compact and avoid HTML-escape amplification of warnings.
encoder.SetEscapeHTML(false)
if err := encoder.Encode(result); err != nil {
return err
return nil, err
}
if encoded.Len() > maxPublicStdoutBytes {
return encoded.Bytes(), nil
}
func writeWorkspaceJSON(w io.Writer, result workspaceops.Result) error {
encoded, err := encodeWorkspaceJSON(result)
if err != nil || len(encoded) > maxPublicStdoutBytes {
return io.ErrShortWrite
}
_, err := w.Write(encoded.Bytes())
_, err = w.Write(encoded)
return err
}
func encodeWorkspaceHuman(result workspaceops.Result) ([]byte, error) {
var encoded bytes.Buffer
if err := renderWorkspaceHuman(&encoded, result); err != nil {
return nil, err
}
return encoded.Bytes(), nil
}
func projectWorkspaceResult(result workspaceops.Result, secretValues []string) (workspaceops.Result, error) {
// Public fields are either closed identities (which must never be rewritten)
// or human-rendered strings (which are redacted and checked for spoofing).
@@ -361,9 +376,35 @@ func projectWorkspaceResult(result workspaceops.Result, secretValues []string) (
return workspaceops.Result{}, err
}
}
if publicResultContainsSecret(result, secretValues) {
return workspaceops.Result{}, errors.New("workspace result contains a declared secret")
}
return result, nil
}
func publicResultContainsSecret(result workspaceops.Result, secrets []string) bool {
values := []string{result.Status, result.Code, result.WorkspaceID, result.WorkspaceRevision, result.DescriptorBlob, result.Operation, result.RunID}
values = append(values, result.CompletedStages...)
values = append(values, result.Warnings...)
for key, value := range result.ChildRuns {
values = append(values, key, value)
}
for key := range result.Counts {
values = append(values, key)
}
for _, artifact := range result.ArtifactIdentities {
values = append(values, artifact.Kind, artifact.Digest)
}
for _, value := range values {
for _, secret := range secrets {
if secret != "" && strings.Contains(value, secret) {
return true
}
}
}
return false
}
func renderWorkspaceHuman(w io.Writer, result workspaceops.Result) error {
if result.Code == workspaceops.CodeRegistryBootstrapRecoveryConflict {
_, err := fmt.Fprintln(w, "Bootstrap recovery is ambiguous or corrupt; inspect the installation registry jobs.")
+13 -14
View File
@@ -134,20 +134,19 @@ func TestRunWorkspacePublicDispatchExitMatrix(t *testing.T) {
}
func TestRunWorkspaceBoundsFinalJSONEncoding(t *testing.T) {
fixture := newCLIFixture(t, "")
fixture.setEnvironment(t)
payload, encodedLength := boundedWorkspaceResultPayload(t, (1<<20)-1)
if len(payload) >= 1<<20 {
t.Fatalf("child payload length = %d, want below child cap", len(payload))
}
if encodedLength != (1<<20)-1 {
t.Fatalf("final encoded length = %d, want %d", encodedLength, (1<<20)-1)
}
writeWorkspaceResultFile(t, fixture, payload)
var stdout, stderr bytes.Buffer
code := run(context.Background(), []string{"--installation", fixture.installationPath, "workspace", "inspect", "--workspace", "psd", "--json"}, &stdout, &stderr)
if code != 0 || stdout.Len() != (1<<20)-1 || stderr.Len() != 0 {
t.Fatalf("bounded output = exit %d stdout %d stderr %q", code, stdout.Len(), stderr.String())
for _, tc := range []struct{ name string; final int; wantCode int }{{"exact", 1 << 20, 0}, {"one-over", (1 << 20) + 1, 1}} {
t.Run(tc.name, func(t *testing.T) {
fixture := newCLIFixture(t, "")
fixture.setEnvironment(t)
payload, encodedLength := boundedWorkspaceResultPayload(t, tc.final)
if encodedLength != tc.final { t.Fatalf("final encoded length = %d, want %d", encodedLength, tc.final) }
writeWorkspaceResultFile(t, fixture, payload)
var stdout, stderr bytes.Buffer
code := run(context.Background(), []string{"--installation", fixture.installationPath, "workspace", "inspect", "--workspace", "psd", "--json"}, &stdout, &stderr)
if code != tc.wantCode || (tc.wantCode == 0 && stdout.Len() != tc.final) || (tc.wantCode != 0 && stdout.Len() != 0) {
t.Fatalf("bounded output = exit %d stdout %d stderr %q", code, stdout.Len(), stderr.String())
}
})
}
}