fix Task1 publication and secret preflight
This commit is contained in:
@@ -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.")
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user