carlos / platform Public

console: pin fail-closed handling of an unknown stored role (#229)

A console that does not know a stored role (v0.21.0 does not know deploy)
answers its token exactly as it answers no token, at every registered CLI
door. The test stores role 'futurerole' on a deploy token and compares the
with-token and no-token answers per door.

v0.21.0 carries both refusals:

$ git show v0.21.0:internal/console/svcprincipal.go | grep -n -A3 'r, ok := svcrole.Parse(role)'
126:		r, ok := svcrole.Parse(role)
127-		if !ok {
128-			return Principal{}, false
129-		}

$ git show v0.21.0:internal/console/authcli.go | grep -n 'allow.Permits(p.Role)'
425:		if p.Kind == PrincipalService && !allow.Permits(p.Role) {
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 062cf37a294d578111853af28bcb64109e6a20c3 parent 56fd784
1 file changed, +80 −0
  • internal/console/deploymixed_test.go +80 −0
diff --git a/internal/console/deploymixed_test.go b/internal/console/deploymixed_test.go
new file mode 100644
index 000000000..e2d03136c
--- /dev/null
+++ b/internal/console/deploymixed_test.go
@@ -0,0 +1,80 @@
+package console
+
+import (
+ "io"
+ "net/http"
+ "regexp"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/carlosframework/platform/internal/svcrole"
+)
+
+type rawReply struct {
+ status int
+ body string
+}
+
+// doRaw sends one request with an optional bearer token. Non-GET methods
+// carry the body {}.
+func doRaw(t *testing.T, method, url, tok string) rawReply {
+ t.Helper()
+ var body io.Reader
+ if method != http.MethodGet && method != http.MethodHead {
+ body = strings.NewReader("{}")
+ }
+ req, err := http.NewRequest(method, url, body)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if body != nil {
+ req.Header.Set("Content-Type", "application/json")
+ }
+ if tok != "" {
+ req.Header.Set("Authorization", "Bearer "+tok)
+ }
+ resp, err := http.DefaultClient.Do(req)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer resp.Body.Close()
+ b, err := io.ReadAll(resp.Body)
+ if err != nil {
+ t.Fatal(err)
+ }
+ return rawReply{status: resp.StatusCode, body: string(b)}
+}
+
+// TestUnknownStoredRoleIsNotSignedInAtEveryDoor is spec §9's first check: a
+// console that does not know a stored role (as v0.21.0 does not know
+// deploy) answers its token exactly as it answers no token at all.
+func TestUnknownStoredRoleIsNotSignedInAtEveryDoor(t *testing.T) {
+ fx := newConsoleFixture(t)
+ if _, err := fx.st.CreateAppIn(OpsAccountID, "hello", "paul@keymail.dev", time.Now()); err != nil {
+ t.Fatal(err)
+ }
+ raw, err := fx.st.NewServicePrincipalScoped(OpsAccountID, "ci", "hello", svcrole.Deploy, []string{"edge"}, "paul@keymail.dev", time.Now())
+ if err != nil {
+ t.Fatal(err)
+ }
+ if _, err := fx.st.db.Exec(`UPDATE cli_tokens SET role = 'futurerole' WHERE token_hash = ?`, hashToken(raw)); err != nil {
+ t.Fatal(err)
+ }
+ if len(fx.srv.cliRoles) == 0 {
+ t.Fatal("no registered CLI doors to check")
+ }
+ fill := regexp.MustCompile(`\{[a-z]+\}`)
+ for pattern := range fx.srv.cliRoles {
+ method, path, _ := strings.Cut(pattern, " ")
+ path = strings.Replace(path, "{account}", "bab", 1)
+ path = strings.Replace(path, "{app}", "hello", 1)
+ path = fill.ReplaceAllString(path, "x")
+ with := doRaw(t, method, fx.ts.URL+path, raw)
+ without := doRaw(t, method, fx.ts.URL+path, "")
+ if with.status != without.status || with.body != without.body {
+ t.Errorf("%s: unknown role answered %d %q; no token answered %d %q",
+ pattern, with.status, with.body, without.status, without.body)
+ }
+ }
+}