Effective ACL = own ACEs + INHERIT-flagged ancestor ACEs (L4 auth / ACL)¶
:acl is not written per leaf, the way :subscribers[] is not. An ACE on a composite carries
the kAceInherit flag and applies to that composite’s whole subtree, so an owner grants an
orchestrator admin over /dev once instead of over every endpoint below it — NFSv4 inheritance
riding the address composition
(ADR-0020).
What to notice¶
Inheritance is per ACE, not per list. One
:aclholds both kinds side by side, and the example’s composite does: a flaggedREADthat covers the subtree, and an unflaggedWRITEthat applies to/devand nowhere else.It covers the subtree, not the child. The flagged ACE reaches
/dev/temp/rawas readily as/dev/temp. The walk is over strict ancestors, nearest first.An inherited grant is still one bit. An inherited
READis not aWRITE; nothing widens on the way down.Own ACEs come first, and combine rather than replace. Writing an
:aclon the child does not shadow what it inherits — the example’sappgainsWRITEwhilefleetkeeps its inheritedREAD. Ordering (own before ancestors) only matters under the full policy, but the merge is built that way regardless (ADR-0050).The result worth internalising: a closed parent can sit over an open child. A descendant whose only candidate ACE is an ancestor’s unflagged one has an empty effective ACL, and an empty effective ACL is open. The example writes an unflagged ACE on
/site, watches a stranger be refused there — and watches the same stranger write/site/leaf. If you meant to protect the subtree, the flag is not optional.Only the merge is cached, never a verdict. An
:aclwrite marks the subtree dirty and the next check rebuilds, so a revoked grant takes effect on the very next operation — and expiry, which is evaluated against the caller’s clock, needs no invalidation at all (expiry).Nothing here is conditional — the target builds and runs under every CI leg.
Source¶
1/*
2 * SPDX-License-Identifier: Apache-2.0
3 * SPDX-FileCopyrightText: Copyright 2026 avatarsd LLC
4 */
5
6/**
7 * @file
8 * @brief ONE CONCEPT — a vertex's EFFECTIVE ACL is its own ACEs plus the INHERIT-flagged
9 * ancestor ones.
10 *
11 * `:acl` is not written per leaf, the way `:subscribers[]` is not: an ACE on a composite carries
12 * the `kAceInherit` flag and applies to that composite's whole subtree, so an owner grants an
13 * orchestrator admin over `/dev` once instead of over every endpoint below it (ADR-0020, NFSv4
14 * inheritance riding the address composition).
15 *
16 * Inheritance is PER ACE, not per list. One `:acl` can hold both kinds side by side, and the
17 * unflagged ones apply to the vertex they were written to and nowhere else. That asymmetry is
18 * the whole example, and it produces the one result worth internalising: a descendant whose only
19 * candidate ACE is an ancestor's UNFLAGGED one has an EMPTY effective ACL, and an empty effective
20 * ACL is open — so the parent is closed while the child beneath it is not.
21 *
22 * Nothing here is cached as a verdict. An `:acl` write marks the subtree dirty and the next
23 * check rebuilds the merge, so a revoked grant takes effect on the very next operation.
24 *
25 * Runs under ctest as `example_acl_inherit`; returns non-zero on any failed check.
26 */
27
28#include <cstdio>
29#include <cstring>
30#include <span>
31#include <string_view>
32#include <vector>
33
34#include "libtracer/graph.hpp"
35#include "libtracer/mem_heap.hpp"
36#include "libtracer/security_acl.hpp"
37
38namespace {
39
40using tr::graph::ace_t;
41using tr::graph::acl_right_t;
42using tr::graph::graph_t;
43using tr::graph::kAceInherit;
44using tr::graph::path_t;
45using tr::graph::role_t;
46using tr::graph::status_t;
47using tr::graph::subject_token_t;
48using tr::graph::vertex_handle_t;
49
50/** @brief Report expectation @p what and record a failure on @p ok. */
51void check(bool& ok, bool cond, const char* what) {
52 std::printf(" [%s] %s\n", cond ? "ok" : "FAIL", what);
53 ok = ok && cond;
54}
55
56/** @brief @p s as opaque subject-token bytes. */
57std::vector<std::byte> as_bytes(std::string_view s) {
58 std::vector<std::byte> out(s.size());
59 std::memcpy(out.data(), s.data(), s.size());
60 return out;
61}
62
63/** @brief The caller context IS the subject token. */
64std::expected<subject_token_t, tr::wire::err_t> caller_is_subject(void*, std::string_view caller) {
65 return as_bytes(caller);
66}
67
68/** @brief @p right as the single `access_mask` bit it is. */
69constexpr std::uint32_t bit(acl_right_t right) { return static_cast<std::uint32_t>(right); }
70
71/** @brief A one-byte VALUE. */
72tr::view::view_t some_value() {
73 const std::byte one[1] = {std::byte{0x01}};
74 return *tr::view::over_bytes(one);
75}
76
77/** @brief True iff @p r was refused by an ACL gate. */
78template <class T>
79bool denied(const tr::graph::result_t<T>& r) {
80 return !r.has_value() && r.error() == status_t::PERMISSION_DENIED;
81}
82
83} // namespace
84
85int main() {
86 bool ok = true;
87 graph_t g;
88 g.configure_subject_resolver(caller_is_subject, nullptr);
89
90 const vertex_handle_t dev = g.register_vertex(path_t("/dev"), role_t::STORED_VALUE);
91 const vertex_handle_t temp = g.register_vertex(path_t("/dev/temp"), role_t::STORED_VALUE);
92 const vertex_handle_t raw = g.register_vertex(path_t("/dev/temp/raw"), role_t::STORED_VALUE);
93 for (vertex_handle_t v : {dev, temp, raw}) (void)g.write(v, some_value()); // trusted seeds
94
95 // One :acl on the composite, carrying both kinds of ACE.
96 const ace_t composite[] = {
97 {.flags = kAceInherit, .subject = as_bytes("fleet"), .access_mask = bit(acl_right_t::READ)},
98 {.subject = as_bytes("local"), .access_mask = bit(acl_right_t::WRITE)},
99 };
100 (void)g.write(path_t("/dev:acl"), *tr::view::over_bytes(tr::graph::encode_acl(composite)));
101
102 // The flagged ACE reaches the whole subtree, at any depth.
103 check(ok, g.read(temp, "fleet").has_value(), "the INHERIT ACE grants READ one level down");
104 check(ok, g.read(raw, "fleet").has_value(), "…and two: it covers the subtree, not the child");
105 check(ok, denied(g.write(temp, some_value(), "fleet")),
106 "…and it inherits ONE bit — an inherited READ is not a WRITE");
107
108 // The unflagged one applies where it was written, and stops there.
109 check(ok, g.write(dev, some_value(), "local").has_value(),
110 "the unflagged ACE grants WRITE on /dev itself");
111 check(ok, denied(g.write(temp, some_value(), "local")),
112 "…and does not travel: /dev/temp is closed by the inherited ACE, which does not name it");
113
114 // Own ACEs come first, and combine with what is inherited rather than replacing it.
115 const ace_t own[] = {{.subject = as_bytes("app"), .access_mask = bit(acl_right_t::WRITE)}};
116 (void)g.write(path_t("/dev/temp:acl"), *tr::view::over_bytes(tr::graph::encode_acl(own)));
117 check(ok, g.write(temp, some_value(), "app").has_value(), "the child's own ACE grants WRITE");
118 check(ok, g.read(temp, "fleet").has_value(),
119 "…and the inherited READ still applies alongside it");
120
121 // The consequence: an unflagged ancestor ACE leaves its descendants with NOTHING to
122 // evaluate, and nothing to evaluate is open. A closed parent over an open child.
123 const vertex_handle_t site = g.register_vertex(path_t("/site"), role_t::STORED_VALUE);
124 const vertex_handle_t leaf = g.register_vertex(path_t("/site/leaf"), role_t::STORED_VALUE);
125 (void)g.write(site, some_value());
126 (void)g.write(leaf, some_value());
127 const ace_t unflagged[] = {
128 {.subject = as_bytes("someone"), .access_mask = bit(acl_right_t::READ)}};
129 (void)g.write(path_t("/site:acl"), *tr::view::over_bytes(tr::graph::encode_acl(unflagged)));
130 check(ok, denied(g.write(site, some_value(), "stranger")),
131 "the ACE closes /site to a stranger");
132 check(ok, g.write(leaf, some_value(), "stranger").has_value(),
133 "…and /site/leaf stays OPEN: an unflagged ancestor ACE is not in its effective ACL");
134
135 std::printf("effective ACL = own ACEs + ancestor ACEs carrying flag 0x%02x\n", kAceInherit);
136 return ok ? 0 : 1;
137}
See also: security-acl module ·
graph model ·
network formation ·
access_mask is a bitfield ·
the two policy profiles.