feat(epitrope): implement grant narrowing, validity, budgets and lifecycle - #86
Merged
Merged
Conversation
rkyv 0.8 with bytecheck is the wire format fixed by topology.md; the format features are named so a conflicting layout fails the build. snafu is the fleet error library. toml is a dev-dependency for the contract fixture test. deny.toml allows MIT, Apache-2.0, and Unicode-3.0 for these crates and skips the syn 2 copy that snafu-derive and munge_macro still build with.
Identifiers with ULID text form, the capability, mode, scope, and budget vocabulary, request and reply payloads for every capability, the outcome taxonomy, the frame header codec, handshake frames, the authentication transcript, and a decoder that bounds length before copying and validates the archive before any field is read.
Every message type round-trips through a full frame; corrupted and truncated bodies are rejected. The fixture test maps each file under docs/contract/fixtures onto syntheke types and fails when the directory or a declared fixture is missing.
Request gains a grant field naming the one grant a call acts under, in execute and dry-run mode. GrantIssueRequest drops parent_grant: the designated grant is the parent, so the two can no longer disagree. Fault frames now refuse any failure other than ProtocolError and AuthFailed on encode and decode, and VersionChoice is non-exhaustive. Fixtures carry grant as a wire field on every capability request; the cast gives tnt0f grant grn0f, and neg_foreign_grant covers a request naming another tenant's grant. The fixture conformance test splits into load, mapping, and test modules.
Every capability request names the grant it acts under; the daemon authorizes against that grant's chain only. A missing or foreign designated grant returns the same NotFoundOrDenied, and the idempotency digest covers the grant. The wire section now lists the eight frame kinds with their bytes, restricts Fault to connection-level failures, fixes the auth transcript layout, and states time units. Fixture keys move grant to the wire fields. The contract stays at version 1.
Names the crates on each side of the syn 2/syn 3 split, the one alternative (holding rkyv at 0.8.17) and why it is rejected, and the retirement condition. The license list takes the same entries and order as the storage branch so the later merge touches only comments.
Add the pure authorization crate over the syntheke contract types: - narrowing of a child grant against the designated parent on every axis, reporting the first failing NarrowingAxis; - chain validity at an injected clock against revocation records, so an ancestor's revocation invalidates descendants without fan-out; - designated-grant authorization where a grant the connection's tenant does not hold reads exactly as a missing one; - reservation and settlement arithmetic with checked ledgers and a typed overrun; - the invocation transition table and restart recovery actions; - a dry-run planner over a read-only snapshot trait; - D17.7 audit scope defaults and a rule view with no audit accessor; - a minimal origin parser for target scopes that refuses every construct where parsers could disagree about the host. proptest (dev only, no default features) checks that a child passing the narrowing check is never broader than its parent on any axis.
# Conflicts: # Cargo.lock # Cargo.toml
Add DenyCode::BudgetUnavailable for exhaustion on a ledger the caller does not own; it names no dimension. Add NarrowingAxis::AuditScope. Failure::Denied carries axis: Option<NarrowingAxis>, set exactly for NarrowingViolation; Response::check refuses any other combination in a reply or a plan's refusal (DeniedAxisMismatch). Move the outcome unit tests to outcome/tests.rs.
Upstream budget exhaustion is Denied{BudgetUnavailable}. A narrowing
refusal carries its axis on the wire, and the audit scope is its own
axis. The chain walk and designated-grant lookups refuse a view answer
filed under another id, and the walk re-checks each link's depth
against its parent's maximum. The narrowing property now checks both
directions.
State Own-scope lineage, check order, session and target refusals, mode-independent decisions, empty chains on refused plans, every attenuation axis with its order, revocation regardless of effect time, the legal transitions, BudgetUnavailable, and the axis on Denied.
# Conflicts: # Cargo.toml
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What changed
crates/epitropeis the pure authorization core. It does no I/O, uses no tokio, and reads the clock only through an injectedClock.check_issuevalidates a proposed child against the designated grant on every axis, in contract order: capabilities, audit scope, session scope, target scope, each ceiling against the parent's remaining budget, validity window, depth, and issuer/parent link. A violation reports the failing axis back to the issuer asDenied{NarrowingViolation, axis}.check_chainwalks leaf to root atclock.now().now == expires_atcounts as expired.authorize. It authorizes against the designated grant's chain only.NotFoundOrDenied.check_revoke. A request may revoke only the designated grant or a grant descending from it, and the designated grant's chain must conferGrantRevoke. Any target outside that subtree, including another tenant's grant, gets the sameNotFoundOrDeniedas a missing one. Revoking an already-revoked grant is idempotent.plan_reservationchecks every ledger along the chain, plus the session and tenant ledgers.BudgetExceeded{dimension}.Denied{BudgetUnavailable}and names no dimension.settlecharges the actual use, never more than was reserved, and releases the rest. All arithmetic is checked.recovery_actiongives the restart rule for each state.All; agents default toOwnAndOwnedSessions.RuleViewhas no audit accessor, and a compile-fail doctest proves it.Contract amendments (the contract stays at v1; nothing has shipped): new
synthekecodesDenyCode::BudgetUnavailableandDenyCode::SessionRequired,Denied{code, axis}, andNarrowingAxis::AuditScope.capability-contract.mdadds:Ownsession scope, which covers descendant tenants;A new fixture,
neg_session_required, is included.Why
Phase 01 S2 requires every invocation to be authorized before any effect. This crate holds that logic with no I/O, so it can be tested exhaustively and cannot write.
Dependencies
Runtime:
synthekeandsnafuonly. Dev:proptest1.11 (alloc/no_std, a fixed ChaCha seed, no filesystem or OS entropy).Stage and acceptance
Phase 01 S2. This PR proves the acceptance clauses for grant attenuation, expiry and revocation, and for cross-tenant read and metadata denial, at the authorization boundary. Dry-run can't write because the view traits have no write methods.
Proof (toolchain 1.97.1)
clippy -D warnings,cargo deny check,cargo tree -d(only the documented syn skip) and the doc checks are clean.