3.6 KiB
3.6 KiB
Audit: Community Policy Engine (m10-community-policy-engine)
Security
- Write enforcement is fail-closed.
CommunityPolicyEvaluator::check_writereturnsErron any violation; there is no silent pass-through. An unknown role returns an error (not a default allow). - Policy is schema-sealed.
CommunityPolicyrules are validated atSchemaBuilder::build()time — signal names are cross-referenced against the schema. A policy referencing a non-existent signal is rejected before the schema is usable, preventing misconfiguration at runtime. - Allow/deny conflicts are caught at build time. A signal in both
allowed_write_signalsanddenied_write_signalsfails schema construction. No ambiguous runtime behavior. - Read suppression is additive and conservative. The suppressed signal set is a union of
denied_read_signalsplus any signal not in the (non-empty)allowed_read_signalslist. A signal not explicitly allowed when the allow list is non-empty is suppressed — this is the correct conservative interpretation. - No privilege escalation path. Community context is caller-supplied but role lookup is schema-bound. A caller cannot invent a role that grants extra access — an unrecognized role is an error, not a fallback-to-admin.
Correctness
- Spec coverage: all 10 test matrix scenarios (I1–I10) are implemented and passing.
- Empty allow/deny lists correctly mean "no restriction" (admin role). A role with no lists set passes all writes and suppresses no reads.
- Signal suppression does not affect candidate generation. The suppressed set only influences
ProfileExecutorscoring — items with suppressed signals remain in the candidate set. I5 verifies result count is unchanged. - Decay-rate index is used correctly.
read_decay_score(entity, signal, 0)in tests uses index 0 (the only configured lambda) — not a timestamp. Prior bug (usingts.as_nanos()as the index) was fixed. - Universe bitmap registration required for retrieve. Tests I5 and I6 correctly call
write_item_with_metadatabefore retrieve queries to register items in the universe bitmap — the scan strategy requires this.
Performance
is_suppressedis O(1) with fast-path early exit. The check!self.suppressed_signals.is_empty()short-circuits before theHashSet::containslookup when no community context is active. No cost on unconstrained queries.- Suppressed set construction is O(n) in schema signals. Built once per query in
stage3_score; schema signal count is bounded and small. Not a hot path concern. - No lock contention.
ProfileExecutoris constructed per-query and owns itssuppressed_signalsset. No shared mutable state.
Observability
- Error messages reference the specific signal name or role name that caused the violation (e.g.
"policy violation: signal 'pin' is denied for role 'member'"). Callers can act on the error without guessing. - Schema validation errors name the duplicate, unknown, or conflicting signal/role for immediate diagnostics.
Tech Debt
- Module duplication resolved. The stale
entities/revocation.rsflat file (left over from the previous session's linter pass) was removed. The canonical implementation isentities/revocation/mod.rs+entities/revocation/tests.rs. No functional impact, but the conflict would have prevented compilation until fixed. ProfileExecutor::newchanged fromconst fntofnto accommodateHashSet::new(). This is the correct approach;const fnwas not required by any caller.
Verdict
Approved. No security issues, no correctness gaps, no performance concerns. Implementation is complete and production-ready.