diff --git a/zddc/internal/fs/tree.go b/zddc/internal/fs/tree.go index 341f120..a3ee4b0 100644 --- a/zddc/internal/fs/tree.go +++ b/zddc/internal/fs/tree.go @@ -291,11 +291,26 @@ func ListDirectory(ctx context.Context, decider policy.Decider, fsRoot, dirPath, // Row rollups carry synthetic spec entries so the tables tool // can walkServer them. Folder-nav virtuals don't need spec // files — they're just party listings rendered by browse. + // Verbs reflect actual cascade authority at each synthetic + // spec's URL so elevated admins see them as writable (they + // CAN materialise an override .zddc / spec by PUTting to + // the virtual path). Non-admins fall through to the default + // 'r' that the embedded baseline grants on the rollup view. if zddc.IsRowSlot(vv.Slot) { - result = append(result, - listing.FileInfo{Name: "table.yaml", URL: baseURL + "table.yaml", IsDir: false, Virtual: true, Verbs: zddc.VerbR.String()}, - listing.FileInfo{Name: "form.yaml", URL: baseURL + "form.yaml", IsDir: false, Virtual: true, Verbs: zddc.VerbR.String()}, - ) + for _, spec := range []string{"table.yaml", "form.yaml"} { + specURL := baseURL + spec + verbs := policy.EffectiveVerbsFromChainP(ctx, decider, parentChain, principal, specURL) + if !verbs.Has(zddc.VerbR) { + continue + } + result = append(result, listing.FileInfo{ + Name: spec, + URL: specURL, + IsDir: false, + Virtual: true, + Verbs: verbs.String(), + }) + } } } diff --git a/zddc/internal/handler/tables.html b/zddc/internal/handler/tables.html index e5e9e3e..20398a7 100644 --- a/zddc/internal/handler/tables.html +++ b/zddc/internal/handler/tables.html @@ -1534,7 +1534,7 @@ body.is-elevated::after {