- Same-secret backend token is still accepted by the frontend guard - Permission denial must not invoke the schema or database callback - Admin error bodies must not echo secrets or raw tokens
106 lines
3.3 KiB
Go
106 lines
3.3 KiB
Go
package cabana
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"git.golem15.com/golem15/summercms/bouncer"
|
|
"git.golem15.com/golem15/summercms/pact"
|
|
)
|
|
|
|
type orderController struct {
|
|
perms []string
|
|
}
|
|
|
|
func (orderController) ID() string { return "acme.demo.widgets" }
|
|
func (orderController) ModelName() string { return "Widget" }
|
|
func (orderController) ConfigDir() string { return "controllers/widgets" }
|
|
func (c orderController) RequiredPermissions() []string {
|
|
return c.perms
|
|
}
|
|
|
|
func TestAuthorizationOrder(t *testing.T) {
|
|
svc := &service{reg: &Registry{byID: map[string]*CompiledController{
|
|
"acme.demo.widgets": {
|
|
Controller: orderController{perms: []string{"acme.demo.access"}},
|
|
List: &ListSchema{RecordsPerPage: 20, Columns: []ListColumn{}},
|
|
},
|
|
}}}
|
|
t.Run("permission before schema", func(t *testing.T) {
|
|
called := 0
|
|
rec := httptest.NewRecorder()
|
|
req := controllerRequest(&bouncer.Principal{ID: 4})
|
|
svc.protect(rec, req, func(*CompiledController) { called++ })
|
|
if rec.Code != http.StatusForbidden {
|
|
t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
if called != 0 {
|
|
t.Fatal("schema or database callback ran before permission denial")
|
|
}
|
|
assertErrorCode(t, rec.Body.Bytes(), "forbidden")
|
|
})
|
|
t.Run("superuser reaches handler", func(t *testing.T) {
|
|
called := 0
|
|
rec := httptest.NewRecorder()
|
|
req := controllerRequest(&bouncer.Principal{ID: 1, IsSuperuser: true})
|
|
svc.protect(rec, req, func(*CompiledController) { called++ })
|
|
if called != 1 {
|
|
t.Fatalf("superuser callback count=%d, want 1", called)
|
|
}
|
|
})
|
|
t.Run("unknown controller is not queried", func(t *testing.T) {
|
|
called := 0
|
|
rec := httptest.NewRecorder()
|
|
req := controllerRequest(&bouncer.Principal{ID: 1, IsSuperuser: true})
|
|
req.SetPathValue("controller", "missing")
|
|
svc.protect(rec, req, func(*CompiledController) { called++ })
|
|
if rec.Code != http.StatusNotFound || called != 0 {
|
|
t.Fatalf("status=%d called=%d", rec.Code, called)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestSecretRedaction(t *testing.T) {
|
|
const secret = "summercms-test-only-admin-hs256-secret"
|
|
const token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxIn0.signature"
|
|
rec := httptest.NewRecorder()
|
|
writeUnauthenticated(rec, errors.New(secret+" "+token))
|
|
body := rec.Body.String()
|
|
if strings.Contains(body, secret) || strings.Contains(body, token) || strings.Contains(body, "eyJ") {
|
|
t.Fatalf("unauthorized body leaked credential material: %s", body)
|
|
}
|
|
assertErrorCode(t, rec.Body.Bytes(), "unauthenticated")
|
|
}
|
|
|
|
func controllerRequest(principal *bouncer.Principal) *http.Request {
|
|
req := httptest.NewRequest(http.MethodGet, "/_admin/api/v1/acme/demo/widgets", nil)
|
|
req.SetPathValue("vendor", "acme")
|
|
req.SetPathValue("plugin", "demo")
|
|
req.SetPathValue("controller", "widgets")
|
|
if principal != nil {
|
|
req = req.WithContext(bouncer.WithUser(req.Context(), principal))
|
|
}
|
|
return req
|
|
}
|
|
|
|
func assertErrorCode(t *testing.T, raw []byte, code string) {
|
|
t.Helper()
|
|
var body struct {
|
|
Error struct {
|
|
Code string `json:"code"`
|
|
} `json:"error"`
|
|
}
|
|
if err := json.Unmarshal(raw, &body); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if body.Error.Code != code {
|
|
t.Fatalf("error code=%q, want %s; body %s", body.Error.Code, code, raw)
|
|
}
|
|
}
|
|
|
|
var _ pact.AdminPermissioned = orderController{}
|