Commit ffcb3af61a
Verified · cmc ci/build: success ci/test: success ci/vuln: success
Layout: unified · split
internal/control/control.go +11 −9
| @@ -83,6 +83,17 @@ func Dispatch(c *Ctx, argv []string) int { | |||
| 83 | if !ok { | 83 | if !ok { |
| 84 | return c.fail(protocol.ExitUsage, "unknown command %q", argv[0]) | 84 | return c.fail(protocol.ExitUsage, "unknown command %q", argv[0]) |
| 85 | } | 85 | } |
| 86 | // Strip the global --json flag wherever it appears, before any | ||
| 87 | // refusal below: a scripted caller needs the envelope most when it is | ||
| 88 | // being told no (#109). | ||
| 89 | args := rest[:0:0] | ||
| 90 | for _, a := range rest { | ||
| 91 | if a == "--json" { | ||
| 92 | c.JSON = true | ||
| 93 | continue | ||
| 94 | } | ||
| 95 | args = append(args, a) | ||
| 96 | } | ||
| 86 | // A runner-scoped key reaches the runner protocol and nothing else, so | 97 | // A runner-scoped key reaches the runner protocol and nothing else, so |
| 87 | // the key a CI host holds cannot administer the instance. | 98 | // the key a CI host holds cannot administer the instance. |
| 88 | if c.Scope != "full" && !(c.Scope == "runner" && cmd.Path[0] == "runner") { | 99 | if c.Scope != "full" && !(c.Scope == "runner" && cmd.Path[0] == "runner") { |
| @@ -108,15 +119,6 @@ func Dispatch(c *Ctx, argv []string) int { | |||
| 108 | return c.fail(protocol.ExitDenied, | 119 | return c.fail(protocol.ExitDenied, |
| 109 | "your account is not active yet: verify your email first (email verify <code>, or ask for the mail again with email add)") | 120 | "your account is not active yet: verify your email first (email verify <code>, or ask for the mail again with email add)") |
| 110 | } | 121 | } |
| 111 | // Strip the global --json flag wherever it appears. | ||
| 112 | args := rest[:0:0] | ||
| 113 | for _, a := range rest { | ||
| 114 | if a == "--json" { | ||
| 115 | c.JSON = true | ||
| 116 | continue | ||
| 117 | } | ||
| 118 | args = append(args, a) | ||
| 119 | } | ||
| 120 | if !cmd.ReadsStdin { | 122 | if !cmd.ReadsStdin { |
| 121 | c.Stdin = emptyReader{} | 123 | c.Stdin = emptyReader{} |
| 122 | } | 124 | } |
internal/control/control_test.go +30
| @@ -193,3 +193,33 @@ func TestAdminNounGatedInDispatch(t *testing.T) { | |||
| 193 | } | 193 | } |
| 194 | } | 194 | } |
| 195 | } | 195 | } |
| 196 | |||
| 197 | // TestRefusalsHonourJSON: a refusal from the dispatcher's own checks is a | ||
| 198 | // JSON envelope when --json was given, like any other failure. The flag | ||
| 199 | // used to be stripped after those checks, so a read-only token or a | ||
| 200 | // pending account got plain text on stderr exactly when a script needed | ||
| 201 | // to parse the error (#109). | ||
| 202 | func TestRefusalsHonourJSON(t *testing.T) { | ||
| 203 | cases := []struct { | ||
| 204 | name string | ||
| 205 | ctx Ctx | ||
| 206 | argv []string | ||
| 207 | }{ | ||
| 208 | {"read-only token", Ctx{ReadOnly: true, Scope: "full", ViaAPI: true}, []string{"repo", "create", "a/b", "--json"}}, | ||
| 209 | {"pending account", Ctx{User: store.User{Pending: true}, Scope: "full"}, []string{"repo", "list", "--json"}}, | ||
| 210 | {"git-scoped key", Ctx{Scope: "git"}, []string{"whoami", "--json"}}, | ||
| 211 | {"non-admin", Ctx{Scope: "full"}, []string{"admin", "stats", "--json"}}, | ||
| 212 | } | ||
| 213 | for _, tc := range cases { | ||
| 214 | var out, errOut bytes.Buffer | ||
| 215 | c := tc.ctx | ||
| 216 | c.Stdout, c.Stderr = &out, &errOut | ||
| 217 | if code := Dispatch(&c, tc.argv); code != protocol.ExitDenied { | ||
| 218 | t.Errorf("%s: exit %d, want %d", tc.name, code, protocol.ExitDenied) | ||
| 219 | } | ||
| 220 | var env protocol.Envelope | ||
| 221 | if err := json.Unmarshal(out.Bytes(), &env); err != nil || env.Error == "" { | ||
| 222 | t.Errorf("%s: no JSON envelope on stdout: %q (stderr %q)", tc.name, out.String(), errOut.String()) | ||
| 223 | } | ||
| 224 | } | ||
| 225 | } | ||