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 | 83 | if !ok { |
| 84 | 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 | 97 | // A runner-scoped key reaches the runner protocol and nothing else, so |
| 87 | 98 | // the key a CI host holds cannot administer the instance. |
| 88 | 99 | if c.Scope != "full" && !(c.Scope == "runner" && cmd.Path[0] == "runner") { |
| @@ -108,15 +119,6 @@ func Dispatch(c *Ctx, argv []string) int { | ||
| 108 | 119 | return c.fail(protocol.ExitDenied, |
| 109 | 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 | 122 | if !cmd.ReadsStdin { |
| 121 | 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 | } | |