test(09-01): add failing audience crossover and authorization-order tests
- 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
This commit is contained in:
42
bouncer/audience_test.go
Normal file
42
bouncer/audience_test.go
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
package bouncer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAudienceCrossover(t *testing.T) {
|
||||||
|
const secret = "same-secret-for-both-guards"
|
||||||
|
users := memUsers{byID: map[uint]*Principal{1: {ID: 1}}}
|
||||||
|
userTok, _, err := MintAudience(secret, "1", "https://app.test/login", time.Hour, AudienceUser)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
backendTok, _, err := MintAudience(secret, "1", "https://app.test/_admin/api/v1/auth/login", time.Hour, AudienceBackend)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
frontend := NewJWTGuard(secret, users, nil)
|
||||||
|
backend := NewBackendJWTGuard(secret, users, nil, nil)
|
||||||
|
|
||||||
|
if principal, err := backend.Authenticate(withBearer(backendTok)); err != nil || principal == nil || principal.ID != 1 {
|
||||||
|
t.Fatalf("backend guard rejected its own audience: %v", err)
|
||||||
|
}
|
||||||
|
if principal, err := frontend.Authenticate(withBearer(userTok)); err != nil || principal == nil || principal.ID != 1 {
|
||||||
|
t.Fatalf("frontend guard rejected its own audience: %v", err)
|
||||||
|
}
|
||||||
|
if principal, err := backend.Authenticate(withBearer(userTok)); err == nil || principal != nil {
|
||||||
|
t.Fatal("backend guard accepted a frontend-audience token signed with the same secret")
|
||||||
|
}
|
||||||
|
if principal, err := frontend.Authenticate(withBearer(backendTok)); err == nil || principal != nil {
|
||||||
|
t.Fatal("frontend guard accepted a backend-audience token signed with the same secret")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func withBearer(token string) *http.Request {
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||||
|
req.Header.Set("Authorization", "Bearer "+token)
|
||||||
|
return req
|
||||||
|
}
|
||||||
105
cabana/security_test.go
Normal file
105
cabana/security_test.go
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
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{}
|
||||||
Reference in New Issue
Block a user