Files
summercms/modules/cabana/token_postgres_smoke_test.go
Jakub Zych b9273c1da7 feat(16.3-01): tracer — g15_ token scopes through composite backend guard
- Add pact.TokenScope/HasTokenScopes and bouncer.ScopeLimit on Principal
- Append backend_api_tokens migration; Allows evaluates scope before superuser
- Composite guard routes Bearer g15_… to token lookup; smoke tests cover path

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-11 01:46:32 +02:00

157 lines
5.3 KiB
Go

package cabana_test
import (
"encoding/json"
"io/fs"
"net/http"
"testing"
"testing/fstest"
"git.golem15.com/golem15/summercms/modules/backpack"
"git.golem15.com/golem15/summercms/modules/cabana"
"git.golem15.com/golem15/summercms/modules/pact"
"git.golem15.com/golem15/summercms/modules/party"
"git.golem15.com/golem15/summercms/modules/surf"
"git.golem15.com/golem15/summercms/modules/compass"
"git.golem15.com/golem15/summercms/modules/lagoon"
"os"
"path/filepath"
)
type tokenDemoWidget struct {
ID uint `gorm:"column:id;primaryKey"`
Name string `gorm:"column:name"`
}
func (tokenDemoWidget) TableName() string { return "cabana_token_demo_widgets" }
func (tokenDemoWidget) Fillable() []string { return []string{"name"} }
func (tokenDemoWidget) Rules() map[string]string { return map[string]string{} }
type tokenDemoPlugin struct{ fsys fs.FS }
func (p tokenDemoPlugin) ID() string { return "acme.demo" }
func (p tokenDemoPlugin) Requires() []string { return nil }
func (p tokenDemoPlugin) Register(*backpack.App) error { return nil }
func (p tokenDemoPlugin) Boot(*backpack.App) error { return nil }
func (p tokenDemoPlugin) AdminControllers() []pact.AdminController {
return []pact.AdminController{tokenDemoController{}}
}
func (p tokenDemoPlugin) Models() []any { return []any{&tokenDemoWidget{}} }
func (p tokenDemoPlugin) Permissions() []pact.Permission {
return []pact.Permission{
{Code: "acme.demo.access", Roles: []string{"developer"}},
{Code: "acme.demo.reports", Roles: []string{"developer"}},
}
}
func (p tokenDemoPlugin) TokenScopes() []pact.TokenScope {
all := []string{"GET", "HEAD", "POST", "PUT", "PATCH", "DELETE"}
return []pact.TokenScope{
{
Name: "demo", Label: "Demo", Description: "Full demo access",
Permissions: []string{"acme.demo.*"}, Methods: all,
},
{
Name: "demo:reports", Label: "Demo reports", Description: "Reports only",
Permissions: []string{"acme.demo.reports"}, Methods: []string{"GET", "HEAD"},
},
}
}
func (p tokenDemoPlugin) AdminFS() fs.FS { return p.fsys }
type tokenDemoController struct{}
func (tokenDemoController) ID() string { return "acme.demo.widgets" }
func (tokenDemoController) ModelName() string { return "Widget" }
func (tokenDemoController) ConfigDir() string { return "controllers/widgets" }
func (tokenDemoController) NewRecord() any { return &tokenDemoWidget{} }
func (tokenDemoController) RequiredPermissions() []string { return []string{"acme.demo.access"} }
func tokenDemoFS() fs.FS {
return fstest.MapFS{
"controllers/widgets/config_list.yaml": &fstest.MapFile{Data: []byte("list: ~/plugins/acme/demo/models/widget/columns.yaml\nmodelClass: Widget\nrecordsPerPage: 20\nshowSearch: true\n")},
"models/widget/columns.yaml": &fstest.MapFile{Data: []byte("columns:\n name:\n label: Name\n searchable: true\n")},
}
}
func tokenAdminHandler(t *testing.T) http.Handler {
t.Helper()
gdb := adminGorm(t)
dir := t.TempDir()
if err := os.WriteFile(filepath.Join(dir, "app.yaml"), []byte("name: cabana-token\n"), 0o644); err != nil {
t.Fatal(err)
}
cfg, err := compass.Open(compass.Options{
Dir: dir,
Environ: []string{
"SUMMER_ENV=development",
"SUMMER_ADMIN__JWT__SECRET=" + adminTestSecret,
},
})
if err != nil {
t.Fatal(err)
}
if err := cfg.Set("http.body_limits.default_bytes", 1048576); err != nil {
t.Fatal(err)
}
if err := cfg.Set("http.body_limits.upload_bytes", 1048576); err != nil {
t.Fatal(err)
}
app := backpack.New(cfg)
if err := lagoon.Publish(app, adminSQL, gdb); err != nil {
t.Fatal(err)
}
h, err := surf.Assemble(app, []party.Plugin{tokenDemoPlugin{fsys: tokenDemoFS()}})
if err != nil {
t.Fatal(err)
}
return h
}
func TestTokenTracerPostgres(t *testing.T) {
gdb := adminGorm(t)
if err := gdb.AutoMigrate(&tokenDemoWidget{}); err != nil {
t.Fatal(err)
}
h := tokenAdminHandler(t)
user := insertAdmin(t, gdb, "tok-dev", "tok-dev@example.test", "secret-pass", true, false)
demoPlain, err := cabana.IssueTestAPIToken(gdb, user.ID, []string{"demo"}, nil)
if err != nil {
t.Fatal(err)
}
reportsPlain, err := cabana.IssueTestAPIToken(gdb, user.ID, []string{"demo:reports"}, nil)
if err != nil {
t.Fatal(err)
}
listPath := adminAPI("/acme/demo/widgets")
if rec := getAuth(t, h, listPath, demoPlain); rec.Code != http.StatusOK {
t.Fatalf("demo list: %d %s", rec.Code, rec.Body.String())
}
if rec := getAuth(t, h, listPath, reportsPlain); rec.Code != http.StatusForbidden {
t.Fatalf("reports list: want 403, got %d %s", rec.Code, rec.Body.String())
}
if rec := postAuth(t, h, http.MethodPost, listPath, reportsPlain, []byte(`{}`)); rec.Code != http.StatusForbidden {
t.Fatalf("reports POST: want 403, got %d %s", rec.Code, rec.Body.String())
} else {
var body struct {
Error struct {
Details map[string]any `json:"details"`
} `json:"error"`
}
if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
t.Fatal(err)
}
if body.Error.Details["reason"] != "token_scope" {
t.Fatalf("reason = %v body=%s", body.Error.Details["reason"], rec.Body.String())
}
}
if err := gdb.Delete(&user).Error; err != nil {
t.Fatal(err)
}
if rec := getAuth(t, h, listPath, demoPlain); rec.Code != http.StatusUnauthorized {
t.Fatalf("soft-deleted owner: want 401, got %d %s", rec.Code, rec.Body.String())
}
}