access_mask is a bitfield, and every gate tests one bit (L4 auth / ACL)

There is no access level and no ordering among the rights. acl_right_t values are single bits, a stored ACE may carry any OR of them, and each door in the graph asks for its own bit and no other (ADR-0020, reference/05 §0x0A).

The example gives six subjects one single-bit grant each, on one vertex, and then runs every gate twice — once for the subject that holds its bit and once for a subject that holds a different one. So every allow is the bit under test, and every deny is a caller who is authorized for something else rather than for nothing.

What to notice

  • WRITE_ACL is precisely the admin right. Not a role, not a flag beside the mask — one bit in the same bitfield, and the one that lets a subject rewrite the policy and so delegate to others. That is what makes “the owner grants an orchestrator admin over a subtree, and the orchestrator then leaves” an ordinary ACE write rather than a privileged mode (reference/13).

  • Reading the policy is READ_ACL, not READ. A subject that may read a vertex’s value learns nothing about who else may. The two bits are independent in both directions.

  • SUBSCRIBE is the producer’s fan-out gate. Appending to :subscribers[] asks the source vertex “who may subscribe to me?”, and a WRITE grant does not buy it. The dual question — “who may write into me?” — is the target’s WRITE bit, evaluated at delivery (reference/13 §Delivery and the two ACLs).

  • CREATE is its own bit because creation is a write. In-band vertex creation goes through the same door as everything else (ADR-0017, superseded by ADR-0059), so it needs a bit of its own or every writer would be a creator.

  • DELETE and WRITE_OWNER are reserved and have no core surface yet. They are in the enum because the wire layout is the full NFSv4 model; nothing in core/ gates on them.

  • 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 — `access_mask` is a bitfield, and every gate tests exactly ONE bit.
  9 *
 10 * There is no "access level" and no ordering among the rights: `acl_right_t` values are single
 11 * bits, a stored ACE may carry any OR of them, and each door in the graph asks for its own bit
 12 * and no other (docs/reference/05 §0x0A, ADR-0020). A subject granted `READ` cannot write; a
 13 * subject granted `READ` cannot even read the `:acl`, because reading the policy is
 14 * `READ_ACL` — its own right.
 15 *
 16 * The bit worth naming is `WRITE_ACL`: it is PRECISELY the admin right, the one that lets a
 17 * subject rewrite the policy and so delegate to others. Not a role, not a flag beside the mask —
 18 * one bit in the same bitfield, which is what makes "the owner grants an orchestrator admin over
 19 * a subtree, and the orchestrator leaves" an ordinary ACE write (reference/13).
 20 *
 21 * Six subjects, six single-bit grants, one vertex — so every allow below is the bit under test
 22 * and every deny is a subject that holds a DIFFERENT bit rather than nothing at all.
 23 *
 24 * Runs under ctest as `example_acl_right_bits`; returns non-zero on any failed check.
 25 */
 26
 27#include <cstdio>
 28#include <cstring>
 29#include <span>
 30#include <string_view>
 31#include <vector>
 32
 33#include "libtracer/graph.hpp"
 34#include "libtracer/mem_heap.hpp"
 35#include "libtracer/security_acl.hpp"
 36#include "libtracer/tlv_emit.hpp"
 37
 38namespace {
 39
 40using tr::graph::ace_t;
 41using tr::graph::acl_right_t;
 42using tr::graph::graph_t;
 43using tr::graph::path_t;
 44using tr::graph::role_t;
 45using tr::graph::status_t;
 46using tr::graph::subject_token_t;
 47using tr::graph::vertex_handle_t;
 48using tr::wire::opt_t;
 49using tr::wire::type_t;
 50
 51/** @brief Report expectation @p what and record a failure on @p ok. */
 52void check(bool& ok, bool cond, const char* what) {
 53    std::printf("  [%s] %s\n", cond ? "ok" : "FAIL", what);
 54    ok = ok && cond;
 55}
 56
 57/** @brief @p s as opaque subject-token bytes. */
 58std::vector<std::byte> as_bytes(std::string_view s) {
 59    std::vector<std::byte> out(s.size());
 60    std::memcpy(out.data(), s.data(), s.size());
 61    return out;
 62}
 63
 64/** @brief The caller context IS the subject token — the resolver is not what this example is
 65 *         about (see acl_subject_resolver for the seam itself). */
 66std::expected<subject_token_t, tr::wire::err_t> caller_is_subject(void*, std::string_view caller) {
 67    return as_bytes(caller);
 68}
 69
 70/** @brief @p right as the single `access_mask` bit it is. */
 71constexpr std::uint32_t bit(acl_right_t right) { return static_cast<std::uint32_t>(right); }
 72
 73/** @brief A one-byte VALUE — the payload the WRITE gate carries. */
 74tr::view::view_t some_value() {
 75    const std::byte one[1] = {std::byte{0x01}};
 76    return *tr::view::over_bytes(one);
 77}
 78
 79/** @brief A `SUBSCRIBER{PATH}` naming @p target — the value a `:subscribers[]` append carries. */
 80tr::view::view_t subscriber_to(std::string_view target) {
 81    std::vector<std::byte> path;
 82    (void)tr::wire::emit_path_segment(path, target);
 83    std::vector<std::byte> path_tlv;
 84    tr::wire::emit_tlv(path_tlv, type_t::PATH, opt_t{}, path);
 85    std::vector<std::byte> out;
 86    tr::wire::emit_tlv(out, type_t::SUBSCRIBER, opt_t{.pl = true}, path_tlv);
 87    return *tr::view::over_bytes(out);
 88}
 89
 90/** @brief A `SPEC` creating a STORED_VALUE child named @p name — the `:children[]` value. */
 91tr::view::view_t child_named(std::string_view name) {
 92    std::vector<std::byte> body;
 93    tr::wire::emit_name(body, "type");
 94    tr::wire::emit_name(body, "stored_value");
 95    tr::wire::emit_name(body, "name");
 96    tr::wire::emit_name(body, name);
 97    std::vector<std::byte> out;
 98    tr::wire::emit_tlv(out, type_t::SPEC, opt_t{.pl = true}, body);
 99    return *tr::view::over_bytes(out);
100}
101
102/** @brief True iff @p r was refused by an ACL gate. */
103template <class T>
104bool denied(const tr::graph::result_t<T>& r) {
105    return !r.has_value() && r.error() == status_t::PERMISSION_DENIED;
106}
107
108}  // namespace
109
110int main() {
111    bool ok = true;
112    graph_t g;
113    g.configure_subject_resolver(caller_is_subject, nullptr);
114    const vertex_handle_t v = g.register_vertex(path_t("/x"), role_t::STORED_VALUE);
115    (void)g.write(v, some_value());  // trusted local seed, so a refused read is the only failure
116
117    // One ACE per right, one subject per ACE. No subject holds two bits.
118    const ace_t aces[] = {
119        {.subject = as_bytes("reader"), .access_mask = bit(acl_right_t::READ)},
120        {.subject = as_bytes("writer"), .access_mask = bit(acl_right_t::WRITE)},
121        {.subject = as_bytes("watcher"), .access_mask = bit(acl_right_t::SUBSCRIBE)},
122        {.subject = as_bytes("builder"), .access_mask = bit(acl_right_t::CREATE)},
123        {.subject = as_bytes("auditor"), .access_mask = bit(acl_right_t::READ_ACL)},
124        {.subject = as_bytes("admin"), .access_mask = bit(acl_right_t::WRITE_ACL)},
125    };
126    (void)g.write(path_t("/x:acl"), *tr::view::over_bytes(tr::graph::encode_acl(aces)));
127
128    const auto acl_field = path_t::parse("/x:acl");
129    const auto subscribers = path_t::parse("/x:subscribers[]");
130    const auto children = path_t::parse("/x:children[]");
131
132    // READ (0x01) — the data plane's read door.
133    check(ok, g.read(v, "reader").has_value(), "READ: the reader reads");
134    check(ok, denied(g.read(v, "writer")), "READ: the writer may not — WRITE is a different bit");
135
136    // WRITE (0x02) — the data plane's write door, and the fan-in gate for deliveries.
137    check(ok, g.write(v, some_value(), "writer").has_value(), "WRITE: the writer writes");
138    check(ok, denied(g.write(v, some_value(), "reader")), "WRITE: the reader may not");
139
140    // SUBSCRIBE (0x04) — the PRODUCER's fan-out gate: who may append to my :subscribers[].
141    check(ok, g.write(v, subscribers->field(), subscriber_to("sink"), "watcher").has_value(),
142          "SUBSCRIBE: the watcher appends a :subscribers[] edge");
143    check(ok, denied(g.write(v, subscribers->field(), subscriber_to("sink"), "writer")),
144          "SUBSCRIBE: a WRITE grant does not buy a subscription");
145
146    // CREATE (0x08) — in-band vertex creation is a write, and it has its own bit (ADR-0017).
147    check(ok, g.write(v, children->field(), child_named("kid"), "builder").has_value(),
148          "CREATE: the builder creates /x/kid");
149    check(ok, denied(g.write(v, children->field(), child_named("other"), "writer")),
150          "CREATE: the writer may not create a child");
151
152    // READ_ACL (0x20) — reading the policy is not reading the value.
153    check(ok, g.read(v, acl_field->field(), "auditor").has_value(),
154          "READ_ACL: the auditor reads the :acl");
155    check(ok, denied(g.read(v, acl_field->field(), "reader")),
156          "READ_ACL: READ alone does not disclose the policy");
157
158    // WRITE_ACL (0x40) — precisely the admin right: rewrite the policy, delegate to others.
159    const ace_t delegated[] = {
160        {.subject = as_bytes("admin"), .access_mask = bit(acl_right_t::WRITE_ACL)}};
161    check(ok,
162          g.write(v, acl_field->field(), *tr::view::over_bytes(tr::graph::encode_acl(delegated)),
163                  "admin")
164              .has_value(),
165          "WRITE_ACL: the admin rewrites the policy — this bit IS 'admin'");
166    check(ok,
167          denied(g.write(v, acl_field->field(),
168                         *tr::view::over_bytes(tr::graph::encode_acl(delegated)), "writer")),
169          "WRITE_ACL: nobody else can, however much of the data plane they hold");
170
171    std::printf("six rights, six single-bit grants: 0x%02x 0x%02x 0x%02x 0x%02x 0x%02x 0x%02x\n",
172                bit(acl_right_t::READ), bit(acl_right_t::WRITE), bit(acl_right_t::SUBSCRIBE),
173                bit(acl_right_t::CREATE), bit(acl_right_t::READ_ACL), bit(acl_right_t::WRITE_ACL));
174    return ok ? 0 : 1;
175}

See also: security-acl module · protocol TLVs · network formation · open by default · inheritance.