mirror of
https://github.com/hanzoai/framework.git
synced 2026-10-30 15:04:47 +00:00
Access held both in one bool, so an org's own System Manager carried the platform's cross-tenant override and a DocType could withhold nothing from its own administrator. Manager stays the meta-permission — who defines a DocType and installs a module in this org — and Super is the platform identity that overrides a permission table. Can now asks three things in order: does the DocType admit the right at all (an immutable record has no writer and no remover, sudo included), is the caller sudo, else the calculus. And a read-only field compares by what the value would BECOME rather than how it was spelled, so a console that PUTs a whole document back as RFC3339 is not refused an instant the store already holds. Co-authored-by: Hanzo Dev <[email protected]>
187 lines
7.1 KiB
Go
187 lines
7.1 KiB
Go
package framework
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// held_test.go covers the three rules that decide what a body may say about a
|
|
// stored document: a read-only field is held at creation as well as at update,
|
|
// an update keeps what the body does not name, and an immutable DocType has no
|
|
// writer and no remover at all — not even the org owner, whose System Manager
|
|
// role is now read through the same permission table as every other role.
|
|
|
|
// sudo is the platform identity acting inside one org — cross-tenant scope, not
|
|
// a second tenant.
|
|
func sudo(org string) Caller { return Caller{Org: org, User: "root", IsAdmin: true} }
|
|
|
|
// ledgerDT is a record a machine writes and people read: a state it is born in,
|
|
// a stamp its own code lands, a note nobody maintains, and a body field.
|
|
func ledgerDT() DocType {
|
|
return DocType{
|
|
Name: "Entry", Module: "ledger", TitleField: "memo",
|
|
Fields: []DocField{
|
|
{Fieldname: "memo", Fieldtype: FieldData},
|
|
{Fieldname: "account", Fieldtype: FieldData},
|
|
{Fieldname: "state", Fieldtype: FieldSelect, Options: "open\nclosed", Default: "open", ReadOnly: true},
|
|
{Fieldname: "closed", Fieldtype: FieldData, ReadOnly: true},
|
|
},
|
|
Perms: []DocPerm{
|
|
{Role: RoleSystemManager, Read: true, Write: true, Create: true, Delete: true},
|
|
},
|
|
}
|
|
}
|
|
|
|
// TestCreateHoldsAReadOnlyDefault: a read-only field the DocType gives a default
|
|
// is that value at birth, so a create naming another one is refused.
|
|
func TestCreateHoldsAReadOnlyDefault(t *testing.T) {
|
|
e := testEngine(t)
|
|
seed(t, e, "acme", ledgerDT())
|
|
id := at("ledger", "Entry")
|
|
|
|
if _, err := e.CreateDocument(context.Background(), owner("acme"), id, map[string]any{
|
|
"memo": "forged", "state": "closed", "closed": "2026-09-16",
|
|
}); err == nil {
|
|
t.Fatal("a create landed a read-only field on a state the DocType does not declare at birth")
|
|
} else if !strings.Contains(err.Error(), "state") {
|
|
t.Fatalf("the refusal must name the field, got %v", err)
|
|
}
|
|
|
|
// A read-only field the DocType declares nothing for is the create's to
|
|
// establish, and the default one may be named at the value it already is.
|
|
doc, err := e.CreateDocument(context.Background(), owner("acme"), id, map[string]any{
|
|
"memo": "real", "state": "open", "closed": "2026-09-16",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create: %v", err)
|
|
}
|
|
if doc.Data["closed"] != "2026-09-16" {
|
|
t.Fatalf("the create did not establish the undeclared read-only field: %v", doc.Data)
|
|
}
|
|
}
|
|
|
|
// TestUpdateKeepsWhatTheBodyDoesNotName: a partial body changes what it names
|
|
// and nothing else, and an empty value is how a field is cleared.
|
|
func TestUpdateKeepsWhatTheBodyDoesNotName(t *testing.T) {
|
|
e := testEngine(t)
|
|
seed(t, e, "acme", ledgerDT())
|
|
id := at("ledger", "Entry")
|
|
made, err := e.CreateDocument(context.Background(), owner("acme"), id, map[string]any{
|
|
"memo": "opening", "account": "1001",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create: %v", err)
|
|
}
|
|
|
|
doc, err := e.UpdateDocument(context.Background(), owner("acme"), id, made.Name, map[string]any{
|
|
"memo": "corrected",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("update: %v", err)
|
|
}
|
|
if doc.Data["account"] != "1001" {
|
|
t.Errorf("a body naming only the memo erased the account: %v", doc.Data)
|
|
}
|
|
if doc.Data["memo"] != "corrected" {
|
|
t.Errorf("the body's own field did not change: %v", doc.Data)
|
|
}
|
|
|
|
doc, err = e.UpdateDocument(context.Background(), owner("acme"), id, made.Name, map[string]any{
|
|
"memo": "corrected", "account": "",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("update: %v", err)
|
|
}
|
|
if doc.Data["account"] != nil && doc.Data["account"] != "" {
|
|
t.Errorf("naming a field empty did not clear it: %v", doc.Data)
|
|
}
|
|
}
|
|
|
|
// TestUpdateHoldsAReadOnlyFieldTheDocumentNeverCarried: a read-only field left
|
|
// unwritten is still not the caller's to establish once a document exists.
|
|
func TestUpdateHoldsAReadOnlyFieldTheDocumentNeverCarried(t *testing.T) {
|
|
e := testEngine(t)
|
|
seed(t, e, "acme", ledgerDT())
|
|
id := at("ledger", "Entry")
|
|
made, err := e.CreateDocument(context.Background(), owner("acme"), id, map[string]any{"memo": "opening"})
|
|
if err != nil {
|
|
t.Fatalf("create: %v", err)
|
|
}
|
|
if _, err := e.UpdateDocument(context.Background(), owner("acme"), id, made.Name, map[string]any{
|
|
"closed": "2026-09-16",
|
|
}); err == nil {
|
|
t.Fatal("an update stamped a read-only field the document never carried")
|
|
}
|
|
}
|
|
|
|
// TestImmutableHasNoWriterAndNoRemover: the record a machine writes is closed to
|
|
// every role on the generic surface, and the in-process API — which the owning
|
|
// subsystem writes through — still reaches it.
|
|
func TestImmutableHasNoWriterAndNoRemover(t *testing.T) {
|
|
e := testEngine(t)
|
|
dt := ledgerDT()
|
|
dt.Immutable = true
|
|
seed(t, e, "acme", dt)
|
|
id := at("ledger", "Entry")
|
|
|
|
made, err := e.Ingest(context.Background(), "acme", id, map[string]any{"memo": "act"}, "")
|
|
if err != nil {
|
|
t.Fatalf("the owning subsystem could not write its own record: %v", err)
|
|
}
|
|
if _, err := e.UpdateDocument(context.Background(), owner("acme"), id, made.Name, map[string]any{
|
|
"memo": "rewritten",
|
|
}); err == nil {
|
|
t.Error("the org owner wrote over a document of an immutable DocType")
|
|
}
|
|
if err := e.DeleteDocument(context.Background(), owner("acme"), id, made.Name); err == nil {
|
|
t.Error("the org owner removed a document of an immutable DocType")
|
|
}
|
|
if err := e.DeleteDocument(context.Background(), sudo("acme"), id, made.Name); err == nil {
|
|
t.Error("a platform superuser removed a document of an immutable DocType")
|
|
}
|
|
if _, err := e.GetDocument(context.Background(), owner("acme"), id, made.Name); err != nil {
|
|
t.Errorf("an immutable document must still read: %v", err)
|
|
}
|
|
}
|
|
|
|
// TestAPermissionTableWithholdsFromTheOrgOwner: a DocType naming System Manager
|
|
// with read alone grants read alone. Manager is the meta-permission that defines
|
|
// DocTypes; it is not a document right.
|
|
func TestAPermissionTableWithholdsFromTheOrgOwner(t *testing.T) {
|
|
e := testEngine(t)
|
|
dt := ledgerDT()
|
|
dt.Perms = []DocPerm{{Role: RoleSystemManager, Read: true}}
|
|
seed(t, e, "acme", dt)
|
|
id := at("ledger", "Entry")
|
|
|
|
if _, err := e.CreateDocument(context.Background(), owner("acme"), id, map[string]any{"memo": "by hand"}); err == nil {
|
|
t.Error("the org owner created a document its DocType grants them no create on")
|
|
}
|
|
made, err := e.Ingest(context.Background(), "acme", id, map[string]any{"memo": "act"}, "")
|
|
if err != nil {
|
|
t.Fatalf("ingest: %v", err)
|
|
}
|
|
if _, err := e.GetDocument(context.Background(), owner("acme"), id, made.Name); err != nil {
|
|
t.Errorf("the read the table does grant was refused: %v", err)
|
|
}
|
|
}
|
|
|
|
// TestSudoOverridesTheTableAndNotTheRecord: platform sudo is the one
|
|
// cross-tenant identity and overrides a DocType's permission table; it is not an
|
|
// exception to what a record IS, so an immutable one refuses it too.
|
|
func TestSudoOverridesTheTableAndNotTheRecord(t *testing.T) {
|
|
e := testEngine(t)
|
|
dt := ledgerDT()
|
|
dt.Perms = []DocPerm{{Role: RoleSystemManager, Read: true}}
|
|
seed(t, e, "acme", dt)
|
|
id := at("ledger", "Entry")
|
|
made, err := e.Ingest(context.Background(), "acme", id, map[string]any{"memo": "act"}, "")
|
|
if err != nil {
|
|
t.Fatalf("ingest: %v", err)
|
|
}
|
|
if err := e.DeleteDocument(context.Background(), sudo("acme"), id, made.Name); err != nil {
|
|
t.Fatalf("platform sudo was held to a per-DocType table: %v", err)
|
|
}
|
|
}
|