test(12.1-05): threat test for the Phase 12.1 framework contracts and the first gate stages
- TestPhase121Threats: one subtest per mitigated threat T-12.1-01 to T-12.1-15 - roster fixture: sentinel names and knobs for failing hooks and providers - scripts/check-phase12.1.sh: fail-closed go test detector, --self-test and --security
This commit is contained in:
@@ -11,6 +11,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"testing/fstest"
|
||||
"time"
|
||||
@@ -190,11 +191,50 @@ func (s *rosterSpy) takeBulk() []pact.AdminBulkActionInput {
|
||||
return out
|
||||
}
|
||||
|
||||
// rosterKnobs switch on failures of the controller's providers, which get no
|
||||
// record to carry a sentinel name. A nil pointer switches nothing on.
|
||||
type rosterKnobs struct {
|
||||
// permissionOptions makes AdminPermissionOptions fail.
|
||||
permissionOptions atomic.Bool
|
||||
// permissionValues makes AdminPermissionValues fail.
|
||||
permissionValues atomic.Bool
|
||||
// relationLocks makes AdminRelationLocks fail.
|
||||
relationLocks atomic.Bool
|
||||
// slowArchive, when set, runs inside the archive bulk action after the
|
||||
// rows were locked (concurrency tests).
|
||||
slowArchive atomic.Pointer[func()]
|
||||
}
|
||||
|
||||
// The sentinel names below make one hook of the roster controller misbehave
|
||||
// for the person who carries the name.
|
||||
const (
|
||||
// rosterKeep: FormBeforeDelete refuses with a ForbiddenError.
|
||||
rosterKeep = "Keep"
|
||||
// rosterKeepAfter: FormAfterUpdate and FormAfterDelete refuse after the
|
||||
// row was written or removed.
|
||||
rosterKeepAfter = "KeepAfter"
|
||||
// rosterShort: ListRowStates answers one entry too few.
|
||||
rosterShort = "Short"
|
||||
// rosterStateErr: ListRowStates fails with a plain error.
|
||||
rosterStateErr = "StateErr"
|
||||
// rosterAppliesErr: the activate record action's Applies fails.
|
||||
rosterAppliesErr = "AppliesErr"
|
||||
// rosterCrash: the archive bulk action fails with a plain error.
|
||||
rosterCrash = "Crash"
|
||||
// rosterRunErr: the reinstate record action fails with a plain error
|
||||
// after its write.
|
||||
rosterRunErr = "RunErr"
|
||||
// rosterDenyCreate and rosterDenyAfterCreate: the create hooks refuse.
|
||||
rosterDenyCreate = "DenyCreate"
|
||||
rosterDenyAfterCreate = "DenyAfterCreate"
|
||||
)
|
||||
|
||||
// rosterPlugin is the acme.roster fixture plugin. fsys, when set, replaces
|
||||
// the fixture tree (boot-error tests).
|
||||
type rosterPlugin struct {
|
||||
spy *rosterSpy
|
||||
fsys fs.FS
|
||||
spy *rosterSpy
|
||||
knobs *rosterKnobs
|
||||
fsys fs.FS
|
||||
// db is the handle the controller reads filter choices and locked tags
|
||||
// with outside a transaction.
|
||||
db *gorm.DB
|
||||
@@ -208,7 +248,7 @@ func (rosterPlugin) Requires() []string { return nil }
|
||||
func (rosterPlugin) Register(*backpack.App) error { return nil }
|
||||
func (rosterPlugin) Boot(*backpack.App) error { return nil }
|
||||
func (p rosterPlugin) AdminControllers() []pact.AdminController {
|
||||
return []pact.AdminController{rosterController{spy: p.spy, db: p.db, relations: p.relations}}
|
||||
return []pact.AdminController{rosterController{spy: p.spy, knobs: p.knobs, db: p.db, relations: p.relations}}
|
||||
}
|
||||
func (rosterPlugin) Permissions() []pact.Permission {
|
||||
return []pact.Permission{{Code: "acme.roster.access", Roles: []string{"developer"}}, {Code: "acme.roster.manage", Roles: []string{"developer"}}}
|
||||
@@ -235,6 +275,7 @@ func (rosterPlugin) LangFS() fs.FS {
|
||||
|
||||
type rosterController struct {
|
||||
spy *rosterSpy
|
||||
knobs *rosterKnobs
|
||||
db *gorm.DB
|
||||
relations func([]cabana.FieldRelationContract) []cabana.FieldRelationContract
|
||||
}
|
||||
@@ -275,6 +316,9 @@ func (c rosterController) handle(ctx context.Context) *gorm.DB {
|
||||
// AdminRelationLocks locks the staff tag for an administrator without
|
||||
// acme.roster.manage.
|
||||
func (c rosterController) AdminRelationLocks(ctx context.Context, field string) (cabana.RelationLock, error) {
|
||||
if c.knobs != nil && c.knobs.relationLocks.Load() {
|
||||
return cabana.RelationLock{}, fmt.Errorf("the lock table said hunter2")
|
||||
}
|
||||
principal, _ := bouncer.User(ctx)
|
||||
if field != "tags" || cabana.Allows(principal, []string{"acme.roster.manage"}) {
|
||||
return cabana.RelationLock{}, nil
|
||||
@@ -339,8 +383,13 @@ func (c rosterController) ListRowStates(ctx context.Context, db *gorm.DB, record
|
||||
if person.DeletedAt.Valid {
|
||||
out[i] = append(out[i], pact.RowStateDeleted)
|
||||
}
|
||||
if person.Name == "Odd" {
|
||||
switch person.Name {
|
||||
case "Odd":
|
||||
out[i] = append(out[i], pact.RowState("starred"))
|
||||
case rosterShort:
|
||||
return out[:len(out)-1], nil
|
||||
case rosterStateErr:
|
||||
return nil, fmt.Errorf("the state table said hunter2")
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
@@ -387,7 +436,10 @@ var rosterPermissionCodes = []cabana.PermissionOption{
|
||||
|
||||
// AdminPermissionOptions serves the permission editor's options per
|
||||
// administrator.
|
||||
func (rosterController) AdminPermissionOptions(ctx context.Context, field string) ([]cabana.PermissionOption, error) {
|
||||
func (c rosterController) AdminPermissionOptions(ctx context.Context, field string) ([]cabana.PermissionOption, error) {
|
||||
if c.knobs != nil && c.knobs.permissionOptions.Load() {
|
||||
return nil, fmt.Errorf("the permission table said hunter2")
|
||||
}
|
||||
if field != "permissions" {
|
||||
return nil, fmt.Errorf("unknown permission field %s", field)
|
||||
}
|
||||
@@ -402,7 +454,10 @@ func (rosterController) AdminPermissionOptions(ctx context.Context, field string
|
||||
}
|
||||
|
||||
// AdminPermissionValues reads the stored JSON object.
|
||||
func (rosterController) AdminPermissionValues(_ context.Context, _ string, record any) (map[string]int, error) {
|
||||
func (c rosterController) AdminPermissionValues(_ context.Context, _ string, record any) (map[string]int, error) {
|
||||
if c.knobs != nil && c.knobs.permissionValues.Load() {
|
||||
return nil, fmt.Errorf("the permission column said hunter2")
|
||||
}
|
||||
person := record.(*rosterPerson)
|
||||
out := map[string]int{}
|
||||
if person.Permissions == nil || *person.Permissions == "" {
|
||||
@@ -466,11 +521,33 @@ func (c rosterController) FormBeforeCreate(ctx context.Context, model any) error
|
||||
model.(*rosterPerson).Tenant = "acme"
|
||||
storePassword(model.(*rosterPerson), values)
|
||||
delete(values, "notify")
|
||||
if model.(*rosterPerson).Name == rosterDenyCreate {
|
||||
return rosterRefused
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c rosterController) FormAfterCreate(ctx context.Context, _ any) error {
|
||||
func (c rosterController) FormAfterCreate(ctx context.Context, model any) error {
|
||||
c.spy.recordVirtual("after-create", ctx)
|
||||
if model.(*rosterPerson).Name == rosterDenyAfterCreate {
|
||||
return rosterRefused
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// FormAfterUpdate refuses the name KeepAfter after the row was written.
|
||||
func (rosterController) FormAfterUpdate(_ context.Context, model any) error {
|
||||
if model.(*rosterPerson).Name == rosterKeepAfter {
|
||||
return rosterRefused
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// FormBeforeDelete refuses the person named Keep.
|
||||
func (rosterController) FormBeforeDelete(_ context.Context, model any) error {
|
||||
if model.(*rosterPerson).Name == rosterKeep {
|
||||
return rosterRefused
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -501,7 +578,14 @@ func (rosterController) FormAfterDelete(ctx context.Context, model any) error {
|
||||
if !ok {
|
||||
return fmt.Errorf("no transaction on the context")
|
||||
}
|
||||
return tx.Unscoped().Delete(model).Error
|
||||
if err := tx.Unscoped().Delete(model).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
// Refused after the row was removed: the transaction must bring it back.
|
||||
if model.(*rosterPerson).Name == rosterKeepAfter {
|
||||
return rosterRefused
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AdminBulkActions: activate needs acme.roster.manage and sets active on the
|
||||
@@ -540,12 +624,20 @@ func (c rosterController) AdminBulkActions() []pact.AdminBulkAction {
|
||||
if !ok {
|
||||
return pact.AdminBulkActionResult{}, fmt.Errorf("no transaction on the context")
|
||||
}
|
||||
if c.knobs != nil {
|
||||
if wait := c.knobs.slowArchive.Load(); wait != nil {
|
||||
(*wait)()
|
||||
}
|
||||
}
|
||||
for _, record := range in.Records {
|
||||
// A refusal after earlier rows were written: the whole
|
||||
// selection must roll back.
|
||||
if record.(*rosterPerson).Name == rosterLocked {
|
||||
return pact.AdminBulkActionResult{}, rosterRefused
|
||||
}
|
||||
if record.(*rosterPerson).Name == rosterCrash {
|
||||
return pact.AdminBulkActionResult{}, fmt.Errorf("the archive said hunter2")
|
||||
}
|
||||
if err := tx.Delete(record).Error; err != nil {
|
||||
return pact.AdminBulkActionResult{}, err
|
||||
}
|
||||
@@ -563,6 +655,9 @@ func (c rosterController) AdminRecordActions() []pact.AdminRecordAction {
|
||||
Name: "activate", Label: "acme.roster::lang.people.activate",
|
||||
Permissions: []string{"acme.roster.manage"},
|
||||
Applies: func(_ context.Context, record any) (bool, error) {
|
||||
if record.(*rosterPerson).Name == rosterAppliesErr {
|
||||
return false, fmt.Errorf("the applies check said hunter2")
|
||||
}
|
||||
return !record.(*rosterPerson).Active, nil
|
||||
},
|
||||
Run: func(ctx context.Context, in pact.AdminRecordActionInput) (pact.AdminRecordActionResult, error) {
|
||||
@@ -594,6 +689,9 @@ func (c rosterController) AdminRecordActions() []pact.AdminRecordAction {
|
||||
if in.Record.(*rosterPerson).Name == rosterLocked {
|
||||
return pact.AdminRecordActionResult{}, rosterRefused
|
||||
}
|
||||
if in.Record.(*rosterPerson).Name == rosterRunErr {
|
||||
return pact.AdminRecordActionResult{}, fmt.Errorf("the reinstate said hunter2")
|
||||
}
|
||||
return pact.AdminRecordActionResult{}, nil
|
||||
},
|
||||
}}
|
||||
@@ -604,7 +702,8 @@ func (c rosterController) AdminRecordActions() []pact.AdminRecordAction {
|
||||
// token), limited (acme.roster.access only), cookie and cookie-only.
|
||||
type rosterEnv struct {
|
||||
*actEnv
|
||||
spy *rosterSpy
|
||||
spy *rosterSpy
|
||||
knobs *rosterKnobs
|
||||
}
|
||||
|
||||
func newRosterEnv(t *testing.T) (*rosterEnv, *gorm.DB) {
|
||||
@@ -656,7 +755,8 @@ func newRosterEnvWith(t *testing.T, configure func(*rosterPlugin)) (*rosterEnv,
|
||||
t.Fatal(err)
|
||||
}
|
||||
spy := &rosterSpy{}
|
||||
plugin := rosterPlugin{spy: spy, db: gdb}
|
||||
knobs := &rosterKnobs{}
|
||||
plugin := rosterPlugin{spy: spy, knobs: knobs, db: gdb}
|
||||
if configure != nil {
|
||||
configure(&plugin)
|
||||
}
|
||||
@@ -668,7 +768,7 @@ func newRosterEnvWith(t *testing.T, configure func(*rosterPlugin)) (*rosterEnv,
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
env := &rosterEnv{actEnv: &actEnv{h: h}, spy: spy}
|
||||
env := &rosterEnv{actEnv: &actEnv{h: h}, spy: spy, knobs: knobs}
|
||||
rec := postJSON(t, h, adminAPI("/auth/login"), map[string]string{"login": login, "password": adminTestPassword})
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("login status=%d body=%s", rec.Code, rec.Body.String())
|
||||
|
||||
Reference in New Issue
Block a user