mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-10-26 22:37:03 +00:00
b901ec629c
* feat(web): choose an API key's access when creating it, and see every key's access (TASK-3866)
Console settings now offers Read and write (["*"], the default, the same
value the store writes when scopes are omitted) or Read only (["read"])
when making a key, and the token list says what each key can do. The
label mirrors tokenScopeAllows, so legacy ""/[] keys read as
unrestricted and null, unparseable or all-unknown values read as No
access, never as full access. One fixture,
internal/server/testdata/token_scope_levels.json, is checked by both the
Go test and the vitest test, so the label cannot drift from the server.
Both create doors (POST /auth/tokens and POST /workspaces/{ws}/tokens)
now refuse scopes the server would not honour, with 400 validation_error
naming the value: non-arrays, the string "null", the legacy [] form,
and unknown values. They used to be stored verbatim, minting dead keys.
Omitted still means ["*"]. Documented in docs/api.md.
Claude-Session: https://claude.ai/code/session_01HsAbPwmN6oyEuQAp6z6NhQ
* test(e2e): mint through the admin session; the suite PAT is refused session_required (TASK-3866)
Claude-Session: https://claude.ai/code/session_01HsAbPwmN6oyEuQAp6z6NhQ
* test: fixture rows where JS trim and Go TrimSpace disagree, and read after write; scopes is always a string (TASK-3866)
Mutants T2, T6 and T8 survived the first set: NBSP is trimmed by both
runtimes, no row put read after write, and the null branch was
unreachable because the API's scopes is a Go string.
Claude-Session: https://claude.ai/code/session_01HsAbPwmN6oyEuQAp6z6NhQ
* fix(server): an explicit "scopes": null is refused, not read as omitted (TASK-3866)
Codex r1 (High): decoded into APITokenCreate, a JSON null scopes field
arrived as "" and minted a full-access key. Both create doors now keep
the field raw (apiTokenCreateRequest), so a null, an array instead of a
string, or a number is refused 400 validation_error naming the value.
Omitted or "" still keeps the ["*"] default.
describeTokenScopes treats any non-string as No access instead of
throwing: the existing settings tests build tokens without scopes, and
the string-only signature from the previous commit crashed them.
Claude-Session: https://claude.ai/code/session_01HsAbPwmN6oyEuQAp6z6NhQ
* fix(server): only an omitted scopes field gets the default; "" is refused (TASK-3866)
Codex r2 M1: an explicit "scopes": "" minted a full-access key. "" is
the stored legacy unrestricted form, refused for the reason [] is. No
caller sends it: APITokenCreate.Scopes has carried omitempty since the
model was added, and the web sends no field or a non-empty one.
The raw field now decodes into a *string, so a null is nil however it
is spaced. Codex r2 H1 (a spaced null bypassing the check) did not
reproduce: encoding/json hands RawMessage the bare literal.
Claude-Session: https://claude.ai/code/session_01HsAbPwmN6oyEuQAp6z6NhQ
49 lines
1.2 KiB
Go
49 lines
1.2 KiB
Go
package server
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"os"
|
|
"testing"
|
|
)
|
|
|
|
// TestTokenScopeLevelsFixture holds tokenScopeAllows to the level each row
|
|
// of testdata/token_scope_levels.json claims (TASK-3866). The web's token
|
|
// list labels keys from the same file (web/src/lib/settings/tokenScopes.test.ts),
|
|
// so a change to the server's check that the label does not follow turns
|
|
// one of the two red.
|
|
func TestTokenScopeLevelsFixture(t *testing.T) {
|
|
raw, err := os.ReadFile("testdata/token_scope_levels.json")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var fixture struct {
|
|
Rows []struct {
|
|
Scopes string `json:"scopes"`
|
|
Level string `json:"level"`
|
|
} `json:"rows"`
|
|
}
|
|
if err := json.Unmarshal(raw, &fixture); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(fixture.Rows) == 0 {
|
|
t.Fatal("fixture has no rows")
|
|
}
|
|
for _, row := range fixture.Rows {
|
|
read := tokenScopeAllows(row.Scopes, http.MethodGet, "/api/v1/workspaces")
|
|
write := tokenScopeAllows(row.Scopes, http.MethodPatch, "/api/v1/workspaces")
|
|
got := "none"
|
|
switch {
|
|
case read && write:
|
|
got = "write"
|
|
case read:
|
|
got = "read"
|
|
case write:
|
|
got = "write-without-read"
|
|
}
|
|
if got != row.Level {
|
|
t.Errorf("scopes %q: server grants %s, fixture says %s", row.Scopes, got, row.Level)
|
|
}
|
|
}
|
|
}
|