Two evaluators, and DENY exists only where one can see it (L4 auth / ACL)¶
The wire layout is the full NFSv4-style model; the required-modules MCU profile enforces a subset of it (ADR-0020). ACE evaluation is therefore a pure per-target policy (ADR-0050) with two adapters, both always compiled:
policy |
rule |
|
|---|---|---|
|
any applicable ACE grants; order is irrelevant |
|
|
ordered first-match-per-bit; the first applicable ACE decides by its type |
|
What to notice¶
The same two ACEs mean the opposite thing under the two evaluators. Hand both a DENY followed by an ALLOW, same subject and same bit: the full policy answers DENY, the ALLOW-only policy answers ALLOW — because it never looks at the type at all.
Which is precisely why
parse_aclrefuses to store a DENY under the ALLOW-only profile. Not a limitation to work around: a stored restriction the running evaluator cannot see would be read as a grant, and a security document must never be interpreted more broadly than it was written.kAcceptsDenyis the constant that decides it, and the parser is its only reader.Stored order is semantic under the full policy, and meaningless under the other. Reversing the two ACEs reverses the full policy’s verdict. An ALLOW-only list has nothing to order, which is exactly what lets the MCU subset skip the whole concept.
effective_acl_tis where the policy is chosen, and it adds the open-by-default rule on top: no effective ACE ⇒ allowed; any present ACE ⇒NO_MATCHdenies.Both arms run in every build. The policies are named explicitly as template arguments rather than left to the target’s binding, so this example is not a run-time skip: the ALLOW-only and the full policy are both exercised whatever the target selected. The bound choice —
acl_policy_t, plain C++ inconfig.hppand rebindable by aconfig_override.hppfragment (ADR-0068) — is only printed, on the last line.Nothing here is conditional — the target builds and runs under every CI leg, and it touches no graph at all: the policies are pure functions over ACE lists.
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 — two evaluators, and DENY exists only where one of them can see it.
9 *
10 * The wire layout is the full NFSv4-style model; the required-modules MCU profile enforces a
11 * SUBSET of it (ADR-0020). ACE evaluation is therefore a pure per-target policy (ADR-0050) with
12 * two adapters, both always compiled:
13 *
14 * - `allow_only_policy_t` — the default. Any applicable ACE grants. Order is irrelevant,
15 * because DENY does not exist in this profile.
16 * - `full_acl_policy_t` — ordered first-match-per-bit: the FIRST applicable ACE decides, by its
17 * own type. This is the host profile, selected per target.
18 *
19 * Hand both the SAME two-ACE list — a DENY followed by an ALLOW for the same subject and the
20 * same bit — and they answer the opposite thing. That is not a quirk to work around; it is the
21 * reason `parse_acl` REFUSES to store a DENY ACE under the ALLOW-only profile. A stored DENY
22 * that the running evaluator cannot see would be read as a grant, and a security document must
23 * never be interpreted more broadly than it was written.
24 *
25 * Both policies are named EXPLICITLY below, as template arguments, so this example demonstrates
26 * both arms in every build regardless of which one the target binds. The bound choice is
27 * `tr::graph::acl_policy_t` (ADR-0068 — plain C++ in `config.hpp`, rebindable by a
28 * `config_override.hpp` fragment); it is printed for information and nothing here is skipped
29 * because of it.
30 *
31 * Runs under ctest as `example_acl_policy_profiles`; returns non-zero on any failed check.
32 */
33
34#include <cstdint>
35#include <cstdio>
36#include <cstring>
37#include <span>
38#include <string_view>
39#include <vector>
40
41#include "libtracer/security_acl.hpp"
42
43namespace {
44
45using tr::graph::ace_t;
46using tr::graph::ace_type_t;
47using tr::graph::acl_right_t;
48using tr::graph::acl_verdict_t;
49using tr::graph::allow_only_policy_t;
50using tr::graph::effective_acl_t;
51using tr::graph::full_acl_policy_t;
52
53/** @brief Report expectation @p what and record a failure on @p ok. */
54void check(bool& ok, bool cond, const char* what) {
55 std::printf(" [%s] %s\n", cond ? "ok" : "FAIL", what);
56 ok = ok && cond;
57}
58
59/** @brief @p s as opaque subject-token bytes. */
60std::vector<std::byte> as_bytes(std::string_view s) {
61 std::vector<std::byte> out(s.size());
62 std::memcpy(out.data(), s.data(), s.size());
63 return out;
64}
65
66/** @brief @p right as the single `access_mask` bit it is. */
67constexpr std::uint32_t bit(acl_right_t right) { return static_cast<std::uint32_t>(right); }
68
69/** @brief Check time — every ACE below is permanent, so the clock is not a variable here. */
70constexpr std::uint64_t kNow = 1'800'000'000'000'000'000ULL;
71
72} // namespace
73
74int main() {
75 bool ok = true;
76 const std::vector<std::byte> alice = as_bytes("alice");
77 const std::uint32_t read = bit(acl_right_t::READ);
78
79 // The discriminating list: refuse alice, then grant her. Same subject, same bit.
80 const ace_t deny_then_allow[] = {
81 {.type = ace_type_t::DENY, .subject = as_bytes("alice"), .access_mask = read},
82 {.type = ace_type_t::ALLOW, .subject = as_bytes("alice"), .access_mask = read},
83 };
84 check(ok, full_acl_policy_t::allows(alice, read, deny_then_allow, kNow) == acl_verdict_t::DENY,
85 "full policy: the FIRST applicable ACE decides, and it is a DENY");
86 check(ok,
87 allow_only_policy_t::allows(alice, read, deny_then_allow, kNow) == acl_verdict_t::ALLOW,
88 "ALLOW-only policy: it never looks at the type, so the same bytes GRANT");
89
90 // Which is exactly why that list may not be stored under the ALLOW-only profile. `parse_acl`
91 // is where the refusal happens — the write door, not the evaluator (see acl_parse_strict).
92 const auto encoded = tr::graph::encode_acl(deny_then_allow);
93 const auto decoded = tr::wire::decode(encoded);
94 check(ok, decoded.has_value(), "the ACL TLV itself is well-formed either way");
95 check(ok, tr::graph::parse_acl<full_acl_policy_t>(*decoded).has_value(),
96 "the full profile parses it: it can evaluate what it is about to store");
97 const auto refused = tr::graph::parse_acl<allow_only_policy_t>(*decoded);
98 check(ok, !refused && refused.error() == tr::graph::status_t::TYPE_MISMATCH,
99 "the ALLOW-only profile refuses it with TYPE_MISMATCH, rather than storing a "
100 "restriction it would go on to read as a grant");
101 check(ok, !allow_only_policy_t::kAcceptsDeny && full_acl_policy_t::kAcceptsDeny,
102 "kAcceptsDeny is the constant that decides that, and parse_acl is its only reader");
103
104 // Order is a property of the full policy alone. Flip the two ACEs and it flips its answer;
105 // an ALLOW-only list has nothing to order, which is why the MCU subset can skip the concept.
106 const ace_t allow_then_deny[] = {deny_then_allow[1], deny_then_allow[0]};
107 check(ok, full_acl_policy_t::allows(alice, read, allow_then_deny, kNow) == acl_verdict_t::ALLOW,
108 "full policy: reversing the two ACEs reverses the verdict — stored order is semantic");
109
110 // Above the policies, `effective_acl_t` adds the open-by-default rule and is where the
111 // policy is chosen. Same merged list, both arms, in one build.
112 effective_acl_t merged;
113 merged.append_own(deny_then_allow);
114 check(ok, !effective_acl_t::allows<full_acl_policy_t>(merged.merged(), alice, read, kNow),
115 "through effective_acl_t, the full profile still denies alice");
116 check(ok, effective_acl_t::allows<allow_only_policy_t>(merged.merged(), alice, read, kNow),
117 "…and the ALLOW-only profile still grants her");
118 check(ok,
119 !effective_acl_t::allows<full_acl_policy_t>(merged.merged(), as_bytes("bob"), read, kNow),
120 "and neither profile lets bob in: no ACE names him, and the list is not empty");
121
122 std::printf("this build binds acl_policy_t with kAcceptsDeny=%s; both arms ran anyway\n",
123 tr::graph::acl_policy_t::kAcceptsDeny ? "true (full)" : "false (ALLOW-only)");
124 return ok ? 0 : 1;
125}
See also: security-acl module · config module · deployment profiles · strict ACL parsing · inheritance.