Add per-role HTTP method scoping to minted tokens (#2)
ci/woodpecker/tag/release Pipeline was successful

## Why

Every token this engine mints is as powerful as the apps it can reach, so a read-only integration can still write to the *arr. arrproxy now accepts a method scope at mint time, and the engine has no way to ask for one.

## How

- Add an optional `methods` role field, uppercase-normalized and de-duplicated.
- Reject a method outside GET/HEAD/POST/PUT/PATCH/DELETE/OPTIONS at role write.
- Forward the role's scope on the arrproxy mint request and echo it in the creds response.
- Omit the field when a role has no scope, so unscoped roles behave exactly as before.
- Cover normalization, rejection, pass-through and the unscoped case.

Requires arrproxy >= v0.5.0 deployed.

Reviewed-on: #2
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
This commit was merged in pull request #2.
This commit is contained in:
2026-08-30 14:45:04 +10:00
committed by BenVincent
parent c3205dde45
commit b1b11330a6
8 changed files with 329 additions and 4 deletions
+33
View File
@@ -2,6 +2,7 @@ package arrstack
import (
"context"
"strings"
"testing"
)
@@ -35,6 +36,38 @@ func TestClient_MintToken(t *testing.T) {
}
}
func TestClient_MintToken_ForwardsMethods(t *testing.T) {
m := newMockArrproxy(t)
client, _ := newClient(&arrstackConfig{BaseURL: m.server.URL, AdminToken: m.adminToken})
if _, err := client.MintToken(context.Background(), mintTokenRequest{
Subject: "vault:arrstack:readonly",
Apps: []string{"sonarr"},
Methods: []string{"GET", "HEAD"},
TTLSeconds: 60,
}); err != nil {
t.Fatalf("MintToken: %v", err)
}
if strings.Join(m.lastRequest.Methods, ",") != "GET,HEAD" {
t.Fatalf("expected methods forwarded, got %v", m.lastRequest.Methods)
}
}
func TestClient_MintToken_RejectsUnknownMethod(t *testing.T) {
m := newMockArrproxy(t)
client, _ := newClient(&arrstackConfig{BaseURL: m.server.URL, AdminToken: m.adminToken})
_, err := client.MintToken(context.Background(), mintTokenRequest{
Subject: "vault:arrstack:readonly",
Apps: []string{"sonarr"},
Methods: []string{"FETCH"},
TTLSeconds: 60,
})
if err == nil {
t.Fatal("expected arrproxy to reject an unknown HTTP method")
}
}
func TestClient_MintToken_RejectsBadSubject(t *testing.T) {
m := newMockArrproxy(t)
client, _ := newClient(&arrstackConfig{BaseURL: m.server.URL, AdminToken: m.adminToken})