feat(listing): per-entry verbs string for client-side capability gating
Add a `verbs` field (canonical "rwcda" subset) to every directory
listing entry, computed via a new
`policy.EffectiveVerbsFromChainP(ctx, d, chain, p, path)` helper that
routes each of the five actions through the decider and unions the
allowed bits — so an external OPA's overrides surface in the wire
field, and active-admin elevation produces the full grant.
Semantics:
- file entry: verbs from the parent dir's chain (files inherit;
they have no .zddc of their own). Same chain Writable uses.
- directory entry: verbs from the subdir's OWN chain, so a fenced
or extended .zddc inside it shows through.
- virtual entries (auto-own homes, canonical-folder placeholders,
workflow received/ window, table.yaml/form.yaml spec rows):
verbs computed against the would-be path's chain so client
affordances render correctly before any write materialises a
real folder.
Writable stays in lockstep with verbs for the transition window so
existing clients (markdown/yaml editor save buttons) keep working
unchanged. Clients should migrate to checking 'w' in verbs and let
Writable wither.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
fb50bb5ef6
commit
53a10ab119
4 changed files with 228 additions and 55 deletions
|
|
@ -133,8 +133,8 @@ func ListDirectory(ctx context.Context, decider policy.Decider, fsRoot, dirPath,
|
|||
continue
|
||||
}
|
||||
subURLPath := baseURL + name + "/"
|
||||
allowed, _ := policy.AllowFromChainP(ctx, decider, chain, principal, subURLPath)
|
||||
if !allowed {
|
||||
subVerbs := policy.EffectiveVerbsFromChainP(ctx, decider, chain, principal, subURLPath)
|
||||
if !subVerbs.Has(zddc.VerbR) {
|
||||
continue // omit denied directories silently
|
||||
}
|
||||
// Pull the title from this subdir's own .zddc, if it has
|
||||
|
|
@ -156,6 +156,7 @@ func ListDirectory(ctx context.Context, decider policy.Decider, fsRoot, dirPath,
|
|||
DisplayName: displayName,
|
||||
Declared: declared,
|
||||
Title: title,
|
||||
Verbs: subVerbs.String(),
|
||||
}
|
||||
result = append(result, fi)
|
||||
continue
|
||||
|
|
@ -172,26 +173,22 @@ func ListDirectory(ctx context.Context, decider policy.Decider, fsRoot, dirPath,
|
|||
DisplayName: displayName,
|
||||
Declared: declared,
|
||||
}
|
||||
// Writable surfaces whether THIS principal could PUT this file
|
||||
// — same decision as the file API's authorizeAction would
|
||||
// reach. Uses the parent-dir chain (computed once above);
|
||||
// active-admin status short-circuits the per-file decider
|
||||
// query when the principal already holds admin authority.
|
||||
// .zddc requires ActionAdmin (not ActionWrite) so the verb
|
||||
// matches the file API's gate at fileapi.go:362-364.
|
||||
action := policy.ActionWrite
|
||||
if name == ".zddc" {
|
||||
action = policy.ActionAdmin
|
||||
}
|
||||
// Verbs surfaces what the principal can do at this file's URL,
|
||||
// computed against the parent-dir chain (files inherit from
|
||||
// parent; they have no .zddc of their own). Writable is the
|
||||
// legacy single-bit projection — it stays in lockstep with
|
||||
// the verbs string for the transition window. For .zddc files
|
||||
// the legacy gate maps Writable to the admin verb (a) instead
|
||||
// of write (w), matching fileapi.go's ActionAdmin gate at
|
||||
// the .zddc URL.
|
||||
fileURL := baseURL + name
|
||||
if parentActiveAdmin {
|
||||
fi.Writable = true
|
||||
} else {
|
||||
allowed, _ := policy.AllowActionFromChainP(ctx, decider, parentChain, principal, fileURL, action)
|
||||
if allowed {
|
||||
fi.Writable = true
|
||||
}
|
||||
fileVerbs := policy.EffectiveVerbsFromChainP(ctx, decider, parentChain, principal, fileURL)
|
||||
fi.Verbs = fileVerbs.String()
|
||||
writableBit := zddc.VerbW
|
||||
if name == ".zddc" {
|
||||
writableBit = zddc.VerbA
|
||||
}
|
||||
fi.Writable = fileVerbs.Has(writableBit) || parentActiveAdmin
|
||||
result = append(result, fi)
|
||||
}
|
||||
|
||||
|
|
@ -201,7 +198,7 @@ func ListDirectory(ctx context.Context, decider policy.Decider, fsRoot, dirPath,
|
|||
// any case variant already exists for them. A first write to that
|
||||
// path materialises a real folder with auto-own .zddc; subsequent
|
||||
// listings drop the synthetic entry naturally.
|
||||
if syn, ok := virtualUserHomeEntry(fsRoot, dirPath, userEmail, baseURL, result); ok {
|
||||
if syn, ok := virtualUserHomeEntry(ctx, decider, fsRoot, dirPath, principal, baseURL, result); ok {
|
||||
result = append(result, syn)
|
||||
}
|
||||
|
||||
|
|
@ -211,7 +208,7 @@ func ListDirectory(ctx context.Context, decider policy.Decider, fsRoot, dirPath,
|
|||
// previously did this client-side; moving it server-side lets the
|
||||
// directory's `display:` map apply to virtual entries the same
|
||||
// way it applies to real ones.
|
||||
result = append(result, virtualCanonicalFolders(fsRoot, absDir, baseURL, result, displayMap)...)
|
||||
result = append(result, virtualCanonicalFolders(ctx, decider, fsRoot, absDir, principal, baseURL, result, displayMap)...)
|
||||
|
||||
// Project-level virtual views:
|
||||
//
|
||||
|
|
@ -241,28 +238,24 @@ func ListDirectory(ctx context.Context, decider policy.Decider, fsRoot, dirPath,
|
|||
appendVirtualRow := func(syntheticName, partyAbs string) {
|
||||
rowURL := baseURL + url.PathEscape(syntheticName)
|
||||
chain := chainFor(partyAbs)
|
||||
if allowed, _ := policy.AllowFromChainP(ctx, decider, chain, principal, rowURL); !allowed {
|
||||
verbs := policy.EffectiveVerbsFromChainP(ctx, decider, chain, principal, rowURL)
|
||||
if !verbs.Has(zddc.VerbR) {
|
||||
return
|
||||
}
|
||||
partyActiveAdmin := elevated && userEmail != "" &&
|
||||
zddc.IsAdminForChain(chain, userEmail)
|
||||
writable := partyActiveAdmin
|
||||
if !writable {
|
||||
allowed, _ := policy.AllowActionFromChainP(ctx, decider, chain, principal, rowURL, policy.ActionWrite)
|
||||
writable = allowed
|
||||
}
|
||||
result = append(result, listing.FileInfo{
|
||||
Name: syntheticName,
|
||||
URL: rowURL,
|
||||
IsDir: false,
|
||||
Virtual: true,
|
||||
Writable: writable,
|
||||
Writable: verbs.Has(zddc.VerbW),
|
||||
Verbs: verbs.String(),
|
||||
})
|
||||
}
|
||||
appendVirtualPartyDir := func(party, partyAbs string) {
|
||||
dirURL := baseURL + url.PathEscape(party) + "/"
|
||||
chain := chainFor(partyAbs)
|
||||
if allowed, _ := policy.AllowFromChainP(ctx, decider, chain, principal, dirURL); !allowed {
|
||||
verbs := policy.EffectiveVerbsFromChainP(ctx, decider, chain, principal, dirURL)
|
||||
if !verbs.Has(zddc.VerbR) {
|
||||
return
|
||||
}
|
||||
result = append(result, listing.FileInfo{
|
||||
|
|
@ -270,6 +263,7 @@ func ListDirectory(ctx context.Context, decider policy.Decider, fsRoot, dirPath,
|
|||
URL: dirURL,
|
||||
IsDir: true,
|
||||
Virtual: true,
|
||||
Verbs: verbs.String(),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -299,8 +293,8 @@ func ListDirectory(ctx context.Context, decider policy.Decider, fsRoot, dirPath,
|
|||
// files — they're just party listings rendered by browse.
|
||||
if zddc.IsRowSlot(vv.Slot) {
|
||||
result = append(result,
|
||||
listing.FileInfo{Name: "table.yaml", URL: baseURL + "table.yaml", IsDir: false, Virtual: true},
|
||||
listing.FileInfo{Name: "form.yaml", URL: baseURL + "form.yaml", IsDir: false, Virtual: true},
|
||||
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()},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -321,12 +315,22 @@ func ListDirectory(ctx context.Context, decider policy.Decider, fsRoot, dirPath,
|
|||
}
|
||||
}
|
||||
if !hasReal {
|
||||
result = append(result, listing.FileInfo{
|
||||
Name: "received/",
|
||||
URL: baseURL + "received/",
|
||||
IsDir: true,
|
||||
Virtual: true,
|
||||
})
|
||||
receivedURL := baseURL + "received/"
|
||||
// Verbs against the canonical workflow's chain — the
|
||||
// virtual `received/` resolves to a read-through window
|
||||
// onto received/<tracking>/; writes go through serveFilePut
|
||||
// which rewrites to a +Cn revision. Read is the only verb
|
||||
// surfaced here.
|
||||
vrVerbs := policy.EffectiveVerbsFromChainP(ctx, decider, parentChain, principal, receivedURL)
|
||||
if vrVerbs.Has(zddc.VerbR) {
|
||||
result = append(result, listing.FileInfo{
|
||||
Name: "received/",
|
||||
URL: receivedURL,
|
||||
IsDir: true,
|
||||
Virtual: true,
|
||||
Verbs: (vrVerbs & zddc.VerbR).String(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -358,7 +362,8 @@ func ListDirectory(ctx context.Context, decider policy.Decider, fsRoot, dirPath,
|
|||
// chain, short-circuited when the principal already holds admin
|
||||
// authority. An elevated admin sees writable=true and the editor lets
|
||||
// them save; a non-admin sees writable=false and the editor mounts
|
||||
// read-only.
|
||||
// read-only. Verbs carries the full verb set so a client can also gate
|
||||
// other affordances (e.g. delete on the editor's toolbar).
|
||||
func virtualZddcEntry(ctx context.Context, decider policy.Decider, parentChain zddc.PolicyChain, principal zddc.Principal, parentActiveAdmin bool, absDir, baseURL string) (listing.FileInfo, bool) {
|
||||
zddcPath := filepath.Join(absDir, ".zddc")
|
||||
if _, err := os.Stat(zddcPath); err == nil {
|
||||
|
|
@ -366,17 +371,14 @@ func virtualZddcEntry(ctx context.Context, decider policy.Decider, parentChain z
|
|||
} else if !os.IsNotExist(err) {
|
||||
return listing.FileInfo{}, false
|
||||
}
|
||||
writable := parentActiveAdmin
|
||||
if !writable {
|
||||
allowed, _ := policy.AllowActionFromChainP(ctx, decider, parentChain, principal, baseURL+".zddc", policy.ActionAdmin)
|
||||
writable = allowed
|
||||
}
|
||||
verbs := policy.EffectiveVerbsFromChainP(ctx, decider, parentChain, principal, baseURL+".zddc")
|
||||
return listing.FileInfo{
|
||||
Name: ".zddc",
|
||||
URL: baseURL + ".zddc",
|
||||
IsDir: false,
|
||||
Virtual: true,
|
||||
Writable: writable,
|
||||
Writable: verbs.Has(zddc.VerbA) || parentActiveAdmin,
|
||||
Verbs: verbs.String(),
|
||||
}, true
|
||||
}
|
||||
|
||||
|
|
@ -388,8 +390,10 @@ func virtualZddcEntry(ctx context.Context, decider policy.Decider, parentChain z
|
|||
// incoming, received, issued under archive/<party>/; whatever an
|
||||
// operator added via on-disk .zddc paths:). Case-insensitive
|
||||
// presence check suppresses a virtual entry when the on-disk
|
||||
// directory exists in any case.
|
||||
func virtualCanonicalFolders(fsRoot, absDir, baseURL string,
|
||||
// directory exists in any case. Verbs are computed against each
|
||||
// synthetic child's would-be chain so client-side gating matches
|
||||
// what a real on-disk folder would carry.
|
||||
func virtualCanonicalFolders(ctx context.Context, decider policy.Decider, fsRoot, absDir string, principal zddc.Principal, baseURL string,
|
||||
real []listing.FileInfo, displayMap map[string]string) []listing.FileInfo {
|
||||
|
||||
declared := zddc.ChildrenDeclaredAt(fsRoot, absDir)
|
||||
|
|
@ -411,13 +415,24 @@ func virtualCanonicalFolders(fsRoot, absDir, baseURL string,
|
|||
if present[strings.ToLower(name)] {
|
||||
continue
|
||||
}
|
||||
childAbs := filepath.Join(absDir, name)
|
||||
chain, err := zddc.EffectivePolicy(fsRoot, childAbs)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
childURL := baseURL + url.PathEscape(name) + "/"
|
||||
verbs := policy.EffectiveVerbsFromChainP(ctx, decider, chain, principal, childURL)
|
||||
if !verbs.Has(zddc.VerbR) {
|
||||
continue
|
||||
}
|
||||
synth = append(synth, listing.FileInfo{
|
||||
Name: name + "/",
|
||||
URL: baseURL + url.PathEscape(name) + "/",
|
||||
URL: childURL,
|
||||
IsDir: true,
|
||||
Virtual: true,
|
||||
DisplayName: lookupDisplay(displayMap, name),
|
||||
Declared: true, // synthesized entries are by definition cascade-declared
|
||||
Verbs: verbs.String(),
|
||||
})
|
||||
}
|
||||
return synth
|
||||
|
|
@ -439,8 +454,8 @@ func virtualCanonicalFolders(fsRoot, absDir, baseURL string,
|
|||
// - viewerEmail is non-empty
|
||||
// - real does not already contain a directory entry that case-folds
|
||||
// to viewerEmail (so a materialised home doesn't get duplicated)
|
||||
func virtualUserHomeEntry(fsRoot, dirPath, viewerEmail, baseURL string, real []listing.FileInfo) (listing.FileInfo, bool) {
|
||||
if viewerEmail == "" {
|
||||
func virtualUserHomeEntry(ctx context.Context, decider policy.Decider, fsRoot, dirPath string, principal zddc.Principal, baseURL string, real []listing.FileInfo) (listing.FileInfo, bool) {
|
||||
if principal.Email == "" {
|
||||
return listing.FileInfo{}, false
|
||||
}
|
||||
rel := strings.Trim(filepath.ToSlash(dirPath), "/")
|
||||
|
|
@ -456,15 +471,31 @@ func virtualUserHomeEntry(fsRoot, dirPath, viewerEmail, baseURL string, real []l
|
|||
}
|
||||
// fi.Name carries a trailing slash for dirs.
|
||||
bare := strings.TrimSuffix(fi.Name, "/")
|
||||
if strings.EqualFold(bare, viewerEmail) {
|
||||
if strings.EqualFold(bare, principal.Email) {
|
||||
return listing.FileInfo{}, false
|
||||
}
|
||||
}
|
||||
// Compute verbs against the would-be home's own chain — the
|
||||
// auto_own_fenced declaration in defaults.zddc.yaml means a real
|
||||
// home grants the creator rwcda; the synthetic entry reports the
|
||||
// same so client-side gating renders the "+ New" affordances
|
||||
// immediately, before the first write materialises the folder.
|
||||
homeAbs := filepath.Join(fsRoot, filepath.FromSlash(dirPath), principal.Email)
|
||||
chain, err := zddc.EffectivePolicy(fsRoot, homeAbs)
|
||||
if err != nil {
|
||||
return listing.FileInfo{}, false
|
||||
}
|
||||
homeURL := baseURL + url.PathEscape(principal.Email) + "/"
|
||||
verbs := policy.EffectiveVerbsFromChainP(ctx, decider, chain, principal, homeURL)
|
||||
if !verbs.Has(zddc.VerbR) {
|
||||
return listing.FileInfo{}, false
|
||||
}
|
||||
return listing.FileInfo{
|
||||
Name: viewerEmail + "/",
|
||||
URL: baseURL + url.PathEscape(viewerEmail) + "/",
|
||||
Name: principal.Email + "/",
|
||||
URL: homeURL,
|
||||
IsDir: true,
|
||||
Virtual: true,
|
||||
Verbs: verbs.String(),
|
||||
}, true
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -257,3 +257,101 @@ func TestListDirectory_VirtualFolderNav_FiltersInFlight(t *testing.T) {
|
|||
t.Errorf("project-level folder-nav listing = %v, want %v", partyDirs, want)
|
||||
}
|
||||
}
|
||||
|
||||
// TestListDirectory_VerbsPerEntry — every entry in a directory listing
|
||||
// carries `verbs`, the canonical "rwcda" subset granted to the caller
|
||||
// at that entry's URL. Files and dirs are gated against different
|
||||
// chains (files use parent's, dirs use their own), so a fenced subdir
|
||||
// surfaces a different verb set than its file siblings.
|
||||
func TestListDirectory_VerbsPerEntry(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
// Root grants alice read across the project; bob nothing.
|
||||
if err := os.WriteFile(filepath.Join(root, ".zddc"),
|
||||
[]byte("acl:\n permissions:\n \"alice@example.com\": rw\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Join(root, "Proj", "sub"), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(root, "Proj", "doc.md"), []byte("x"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Subdir extends alice's grant to include create — confirms the
|
||||
// dir entry's verbs come from its OWN chain, not parent's.
|
||||
if err := os.WriteFile(filepath.Join(root, "Proj", "sub", ".zddc"),
|
||||
[]byte("acl:\n permissions:\n \"alice@example.com\": rwc\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
zddc.InvalidateCache(root)
|
||||
|
||||
got, err := ListDirectory(context.Background(), nil, root,
|
||||
"Proj", "alice@example.com", "/Proj/", false, false)
|
||||
if err != nil {
|
||||
t.Fatalf("list: %v", err)
|
||||
}
|
||||
wantVerbs := map[string]string{
|
||||
"doc.md": "rw", // file: parent chain (project root → rw)
|
||||
"sub/": "rwc", // dir: own chain (extends to rwc)
|
||||
}
|
||||
for _, fi := range got {
|
||||
want, ok := wantVerbs[fi.Name]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if fi.Verbs != want {
|
||||
t.Errorf("entry %s verbs = %q, want %q", fi.Name, fi.Verbs, want)
|
||||
}
|
||||
// Writable stays in lockstep with verbs for the transition
|
||||
// window — w bit for files, r/c semantics for dirs (no
|
||||
// Writable on dirs today; we don't assert it).
|
||||
if !fi.IsDir {
|
||||
wantWritable := want == "rw"
|
||||
if fi.Writable != wantWritable {
|
||||
t.Errorf("entry %s Writable = %v, want %v", fi.Name, fi.Writable, wantWritable)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestListDirectory_VerbsActiveAdminBypass — an elevated admin sees the
|
||||
// full "rwcda" verb set on every entry regardless of explicit ACL
|
||||
// grants. Mirrors the InternalDecider's single bypass branch.
|
||||
func TestListDirectory_VerbsActiveAdminBypass(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
if err := os.WriteFile(filepath.Join(root, ".zddc"),
|
||||
[]byte("admins:\n - admin@example.com\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Join(root, "Proj"), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(root, "Proj", "doc.md"), []byte("x"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
zddc.InvalidateCache(root)
|
||||
|
||||
// Elevated admin sees rwcda everywhere.
|
||||
got, err := ListDirectory(context.Background(), nil, root,
|
||||
"Proj", "admin@example.com", "/Proj/", false, true /* elevated */)
|
||||
if err != nil {
|
||||
t.Fatalf("list: %v", err)
|
||||
}
|
||||
for _, fi := range got {
|
||||
if fi.Verbs != "rwcda" {
|
||||
t.Errorf("elevated admin %s verbs = %q, want rwcda", fi.Name, fi.Verbs)
|
||||
}
|
||||
}
|
||||
|
||||
// Same admin un-elevated sees nothing (no explicit ACL grant,
|
||||
// admin bypass disabled).
|
||||
got, err = ListDirectory(context.Background(), nil, root,
|
||||
"Proj", "admin@example.com", "/Proj/", false, false)
|
||||
if err != nil {
|
||||
t.Fatalf("list un-elevated: %v", err)
|
||||
}
|
||||
for _, fi := range got {
|
||||
if fi.Verbs == "rwcda" {
|
||||
t.Errorf("un-elevated admin %s verbs = %q, should not be full grant", fi.Name, fi.Verbs)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -60,5 +60,30 @@ type FileInfo struct {
|
|||
// false-or-unknown and gate writes accordingly. Read-only-by-
|
||||
// default is the safer client-side fallback if the server forgets
|
||||
// to populate it.
|
||||
//
|
||||
// Superseded by Verbs (which carries the full verb set); kept
|
||||
// alongside it for the transition window so existing clients
|
||||
// reading writable: don't break. New clients should read Verbs and
|
||||
// check for 'w'.
|
||||
Writable bool `json:"writable,omitempty"`
|
||||
|
||||
// Verbs is the canonical "rwcda" subset granted to the calling
|
||||
// principal at this entry's URL. Computed by running the policy
|
||||
// decider for each of the five actions and unioning the allowed
|
||||
// bits, with the same active-admin bypass that Writable uses.
|
||||
//
|
||||
// Semantics per entry kind:
|
||||
// - file: verbs from the parent directory's chain (files have no
|
||||
// .zddc of their own; they inherit). Same chain Writable uses.
|
||||
// - directory: verbs from the subdirectory's OWN chain, so a
|
||||
// fenced/extended .zddc inside it shows through. The client
|
||||
// interprets per-kind: `w` on a dir = "I can rename this
|
||||
// folder", `c` on a dir = N/A at this URL (use path-scoped
|
||||
// /.profile/access?path= for "can I create inside this dir").
|
||||
//
|
||||
// omitempty: empty set ("") is the explicit-deny case; absence
|
||||
// means the server didn't populate it. Clients should treat both
|
||||
// as "no permissions known" and fall back to a server round-trip
|
||||
// (or just disable affordances) rather than assuming any grant.
|
||||
Verbs string `json:"verbs,omitempty"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -395,6 +395,25 @@ func AllowActionFromChainP(ctx context.Context, d Decider, chain zddc.PolicyChai
|
|||
return d.Allow(ctx, in)
|
||||
}
|
||||
|
||||
// EffectiveVerbsFromChainP returns the verb set the principal effectively
|
||||
// holds at path under chain. Routes each verb through the decider so an
|
||||
// external OPA's overrides surface in the result; with the InternalDecider
|
||||
// the answer is the cascade's effective grant plus WORM composition plus
|
||||
// the active-admin bypass. Output is the same wire shape the client
|
||||
// receives in listing entry verbs / /.profile/access?path= responses.
|
||||
func EffectiveVerbsFromChainP(ctx context.Context, d Decider, chain zddc.PolicyChain, p zddc.Principal, path string) zddc.VerbSet {
|
||||
if p.Elevated && p.Email != "" && zddc.IsAdminForChain(chain, p.Email) {
|
||||
return zddc.VerbAll
|
||||
}
|
||||
var verbs zddc.VerbSet
|
||||
for _, action := range []string{ActionRead, ActionWrite, ActionCreate, ActionDelete, ActionAdmin} {
|
||||
if allowed, _ := AllowActionFromChainP(ctx, d, chain, p, path, action); allowed {
|
||||
verbs |= actionVerb(action)
|
||||
}
|
||||
}
|
||||
return verbs
|
||||
}
|
||||
|
||||
// cachingDecider wraps another Decider with a small per-decision cache.
|
||||
// Designed for the external-OPA hot path: a single .archive listing or
|
||||
// directory enumeration can hit the same (email, dir-policy) tuple
|
||||
|
|
|
|||
Loading…
Reference in a new issue