carlos / platform Public

cli: the policy commands resolve an account name to its sqid (#328, #329)

The policy door matches its account segment as a sqid only. apps set
--egress-allow/--size, instances set --size and apps show now match the
account reference against the caller's memberships with pickAccount,
--account's one rule, and send the sqid. A reference no membership names
(an operator acting on another account by sqid) travels as it is.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 97085bd6112b2dac9f226c48a0e1ecd296773359 parent 58361a9
2 files changed, +93 −1
  • apps_policy.go +35 −0
  • apps_policy_test.go +58 −1
diff --git a/apps_policy.go b/apps_policy.go
index cddd536a4..7aca0c5b7 100644
--- a/apps_policy.go
+++ b/apps_policy.go
@@ -25,6 +25,7 @@ import (
"github.com/carlosframework/platform/internal/cgroup"
"github.com/carlosframework/platform/internal/egressaddr"
+ "github.com/carlosframework/platform/internal/registry"
)
// Copy (collected for review in plan Task 15).
@@ -160,8 +161,38 @@ func policyReplyFrom(app string, status int, body []byte) (apiAppPolicy, error)
return out, nil
}
+// policyAccount turns the account reference into the sqid the policy door
+// takes: unlike the member doors, it resolves no names (its path segment is
+// matched as a sqid). --account-id is a sqid already. Otherwise the
+// reference is matched against the caller's memberships by pickAccount,
+// --account's one rule (a sqid or a name, case-insensitively, an ambiguity
+// refused). A reference no membership names travels as it is: an operator
+// is often not a member of the account, and a sqid needs no lookup.
+func (s *apiSession) policyAccount(ctx context.Context, a apiAcct) (apiAcct, error) {
+ if a.Exact {
+ return a, nil
+ }
+ accts, supported, err := s.listAccounts(ctx)
+ if err != nil || !supported {
+ return a, err
+ }
+ reg := make([]registry.Account, len(accts))
+ for i, e := range accts {
+ reg[i] = registry.Account{Sqid: e.Sqid, Name: e.Name}
+ }
+ sqid, err := pickAccount(reg, a.Seg)
+ if err != nil {
+ return a, err
+ }
+ return apiAcct{Seg: sqid, Exact: true}, nil
+}
+
// getAppPolicy is the door's GET: the policy a write would build on now.
func (s *apiSession) getAppPolicy(ctx context.Context, a apiAcct, app string) (apiAppPolicy, error) {
+ a, err := s.policyAccount(ctx, a)
+ if err != nil {
+ return apiAppPolicy{}, err
+ }
status, body, err := s.getJSON(ctx, apiPath(a, app, "/policy"))
if err != nil {
return apiAppPolicy{}, err
@@ -172,6 +203,10 @@ func (s *apiSession) getAppPolicy(ctx context.Context, a apiAcct, app string) (a
// changeAppPolicy reads the policy, then writes c on the ETag it read. A
// 412 comes back as appsPolicyChanged and is not retried.
func (s *apiSession) changeAppPolicy(ctx context.Context, a apiAcct, app string, c policyChange) (apiAppPolicy, error) {
+ a, err := s.policyAccount(ctx, a)
+ if err != nil {
+ return apiAppPolicy{}, err
+ }
cur, err := s.getAppPolicy(ctx, a, app)
if err != nil {
return apiAppPolicy{}, err
diff --git a/apps_policy_test.go b/apps_policy_test.go
index 9a7bf0062..8218e3e58 100644
--- a/apps_policy_test.go
+++ b/apps_policy_test.go
@@ -27,8 +27,11 @@ func policyChangeFor(t *testing.T, args ...string) (*policyChange, error) {
}
// policyConsole is a fake console's policy door. getBody is the GET's
-// answer (getStatus when set); putStatus/putBody the PUT's.
+// answer (getStatus when set); putStatus/putBody the PUT's. accounts is the
+// memberships listing (GET /api/cli/apps); "" answers it as an older
+// console does, with a bare 404. calls leaves the listing out.
type policyConsole struct {
+ accounts string
getStatus int
getBody string
putStatus int
@@ -45,6 +48,15 @@ func (c *policyConsole) session(t *testing.T) *apiSession {
t.Helper()
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
b, _ := io.ReadAll(r.Body)
+ if r.Method == http.MethodGet && r.URL.Path == "/api/cli/apps" {
+ if c.accounts == "" {
+ http.NotFound(w, r)
+ return
+ }
+ w.Header().Set("Content-Type", "application/json")
+ io.WriteString(w, c.accounts)
+ return
+ }
c.mu.Lock()
c.calls = append(c.calls, r.Method+" "+r.URL.Path)
c.mu.Unlock()
@@ -320,3 +332,48 @@ func TestAppsSetRunPolicyChangePrintsWhatTheConsoleStored(t *testing.T) {
t.Fatalf("err = %v", err)
}
}
+
+// The policy door takes the account's sqid and resolves no names, so a name
+// given with --account (or as the configured default) is resolved to the
+// sqid among the caller's memberships before the request; one no membership
+// names, such as another tenant's sqid, travels as it is.
+func TestAppPolicyResolvesAnAccountNameToItsSqid(t *testing.T) {
+ ctx := context.Background()
+ accounts := `{"accounts":[{"name":"shop-team","sqid":"bab","role":"owner","apps":["shop"]},{"name":"other","sqid":"zzz","role":"member","apps":[]}]}`
+ captureStdout(t)
+
+ c := &policyConsole{accounts: accounts, getBody: `{"policy":{"v":1},"etag":""}`}
+ if err := appsShowRun(ctx, c.session(t), apiAcct{Seg: "Shop-Team"}, "shop"); err != nil {
+ t.Fatal(err)
+ }
+ if want := []string{"GET /api/cli/apps/bab/shop/policy"}; !reflect.DeepEqual(c.calls, want) {
+ t.Fatalf("apps show: requests = %v, want %v", c.calls, want)
+ }
+
+ c = &policyConsole{accounts: accounts, getBody: `{"policy":{"v":1},"etag":""}`, putBody: `{"policy":{"v":1,"default_size":"large"},"etag":"e1"}`}
+ large := "large"
+ if err := appsSetRun(ctx, c.session(t), apiAcct{Seg: "shop-team"}, "shop", nil, nil, &policyChange{DefaultSize: &large}); err != nil {
+ t.Fatal(err)
+ }
+ if want := []string{"GET /api/cli/apps/bab/shop/policy", "PUT /api/cli/apps/bab/shop/policy"}; !reflect.DeepEqual(c.calls, want) {
+ t.Fatalf("apps set: requests = %v, want %v", c.calls, want)
+ }
+
+ c = &policyConsole{accounts: accounts, getBody: `{"policy":{"v":1},"etag":""}`, putBody: `{"policy":{"v":1},"etag":"e1"}`}
+ def := "default"
+ if err := instancesSetRun(ctx, c.session(t), apiAcct{Seg: "shop-team"}, "shop", "shop-a.example.com", nil, nil, &def); err != nil {
+ t.Fatal(err)
+ }
+ if want := []string{"GET /api/cli/apps/bab/shop/policy", "PUT /api/cli/apps/bab/shop/policy"}; !reflect.DeepEqual(c.calls, want) {
+ t.Fatalf("instances set: requests = %v, want %v", c.calls, want)
+ }
+
+ // A sqid no membership names (an operator acting on another account).
+ c = &policyConsole{accounts: accounts, getBody: `{"policy":{"v":1},"etag":""}`}
+ if err := appsShowRun(ctx, c.session(t), apiAcct{Seg: "bcd"}, "shop"); err != nil {
+ t.Fatal(err)
+ }
+ if want := []string{"GET /api/cli/apps/bcd/shop/policy"}; !reflect.DeepEqual(c.calls, want) {
+ t.Fatalf("foreign sqid: requests = %v, want %v", c.calls, want)
+ }
+}