Add per-role HTTP method scoping to minted tokens (#2)
ci/woodpecker/tag/release Pipeline was successful
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:
+31
-2
@@ -3,6 +3,7 @@ package arrstack
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strconv"
|
||||
@@ -33,6 +34,7 @@ func getTestBackend(t *testing.T) (*arrstackBackend, logical.Storage) {
|
||||
type mockToken struct {
|
||||
Subject string
|
||||
Apps []string
|
||||
Methods []string
|
||||
Label string
|
||||
ExpiresAt *time.Time
|
||||
Disabled bool
|
||||
@@ -49,9 +51,13 @@ type mockArrproxy struct {
|
||||
counter int
|
||||
adminToken string
|
||||
apps map[string]bool
|
||||
methods map[string]bool
|
||||
|
||||
mintErr bool
|
||||
mintErr bool
|
||||
// lastRequest is the decoded mint payload; lastBody is the raw JSON, so a
|
||||
// test can assert a field was omitted rather than sent empty.
|
||||
lastRequest mintTokenRequest
|
||||
lastBody []byte
|
||||
}
|
||||
|
||||
func newMockArrproxy(t *testing.T) *mockArrproxy {
|
||||
@@ -60,6 +66,10 @@ func newMockArrproxy(t *testing.T) *mockArrproxy {
|
||||
tokens: make(map[string]*mockToken),
|
||||
adminToken: "arrproxy-admin-secret",
|
||||
apps: map[string]bool{"sonarr": true, "radarr": true, "prowlarr": true},
|
||||
methods: map[string]bool{
|
||||
"GET": true, "HEAD": true, "POST": true, "PUT": true,
|
||||
"PATCH": true, "DELETE": true, "OPTIONS": true,
|
||||
},
|
||||
}
|
||||
|
||||
mux := http.NewServeMux()
|
||||
@@ -108,12 +118,18 @@ func (m *mockArrproxy) handleMint(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
http.Error(w, "bad request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
var req mintTokenRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
if err := json.Unmarshal(body, &req); err != nil {
|
||||
http.Error(w, "bad request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
m.lastRequest = req
|
||||
m.lastBody = body
|
||||
|
||||
if !strings.HasPrefix(req.Subject, "vault:arrstack:") || strings.TrimSpace(strings.TrimPrefix(req.Subject, "vault:arrstack:")) == "" {
|
||||
http.Error(w, "subject must be namespaced with vault:arrstack:", http.StatusBadRequest)
|
||||
@@ -129,6 +145,13 @@ func (m *mockArrproxy) handleMint(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
}
|
||||
// An absent or empty methods list is unrestricted.
|
||||
for _, meth := range req.Methods {
|
||||
if !m.methods[meth] {
|
||||
http.Error(w, "methods must name known HTTP methods", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
}
|
||||
if req.TTLSeconds < 0 {
|
||||
http.Error(w, "ttl_seconds must not be negative", http.StatusBadRequest)
|
||||
return
|
||||
@@ -141,9 +164,14 @@ func (m *mockArrproxy) handleMint(w http.ResponseWriter, r *http.Request) {
|
||||
exp := time.Now().UTC().Add(time.Duration(req.TTLSeconds) * time.Second)
|
||||
expiresAt = &exp
|
||||
}
|
||||
methods := req.Methods
|
||||
if methods == nil {
|
||||
methods = []string{}
|
||||
}
|
||||
m.tokens[id] = &mockToken{
|
||||
Subject: req.Subject,
|
||||
Apps: req.Apps,
|
||||
Methods: methods,
|
||||
Label: req.Label,
|
||||
ExpiresAt: expiresAt,
|
||||
}
|
||||
@@ -151,6 +179,7 @@ func (m *mockArrproxy) handleMint(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusCreated, map[string]interface{}{
|
||||
"id": id,
|
||||
"token": "arr_" + id + "_plaintext",
|
||||
"methods": methods,
|
||||
"expires_at": expiresAt,
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user