Skip to main content

liminal_server/auth_lys/
routes.rs

1//! Exhaustive route classifications for the existing wire surfaces.
2
3use super::HotAction;
4use liminal::protocol::FrameType;
5use liminal_protocol::wire::{ClientDiscriminant, PARTICIPANT_FRAME_TYPE};
6use serde::{Deserialize, Serialize};
7
8/// Where the exact placed subject must be resolved before deciding.
9#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
10#[serde(rename_all = "snake_case")]
11pub enum SubjectSource {
12    /// Subject carried by this frame and resolved through admitted mapping.
13    FrameSubject,
14    /// Subject retained by this connection's subscription.
15    SubscriptionBinding,
16    /// Subject retained by this connection's conversation.
17    ConversationBinding,
18    /// Every explicitly served subject in the admitted worker registration.
19    WorkerBinding,
20    /// Subject retained by the recipient-bound push correlation.
21    PushBinding,
22    /// Conversation-to-subject mapping, never a guessed numeric subject.
23    ParticipantBinding,
24    /// The link the frame offers, under the network, with the end it names.
25    FrameLink,
26    /// The link open on this connection's stream, and the frame's subject.
27    LinkBinding,
28}
29
30/// Permission policy; control and ignored inbound frames create no data effect.
31#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
32#[serde(tag = "class", rename_all = "snake_case")]
33pub enum RouteClassification {
34    /// One offline decision before the operation's effect.
35    Hot {
36        /// Required subject permission.
37        action: HotAction,
38        /// Existing authoritative binding used to resolve the subject.
39        source: SubjectSource,
40    },
41    /// Release an already-owned resource without creating new reach.
42    Cleanup {
43        /// Permission associated with the released resource.
44        action: HotAction,
45        /// Binding whose ownership must still be checked.
46        source: SubjectSource,
47    },
48    /// One live decision before the operation's effect, never the hot seam:
49    /// the frame's offered end names `link_export` or `link_import` on the
50    /// link (ACCESS-002 R1).
51    Deliberate {
52        /// Binding used to resolve the link and the end.
53        source: SubjectSource,
54    },
55    /// A message crossing a link: `link_export` held by the exporting
56    /// machine and `link_import` by the importing one on the opened link
57    /// resource, both decided offline from the two passes (ACCESS-002 R1).
58    Crossing {
59        /// Binding used to resolve the link and the subject.
60        source: SubjectSource,
61    },
62    /// Credential admission; no subject operation is authorized by this result.
63    Admission,
64    /// Liveness or connection teardown with no new subject effect.
65    Control,
66    /// Inbound server-originated frame, ignored without dispatch.
67    ServerOnly,
68    /// Decode the participant subrecord and classify it independently.
69    ParticipantEnvelope,
70    /// Unknown inbound frame; no data operation can dispatch.
71    Unknown,
72}
73
74const fn hot(action: HotAction, source: SubjectSource) -> RouteClassification {
75    RouteClassification::Hot { action, source }
76}
77const fn cleanup(action: HotAction, source: SubjectSource) -> RouteClassification {
78    RouteClassification::Cleanup { action, source }
79}
80
81/// Classifies every core frame category, including the participant extension.
82///
83/// This match has no catch-all over known variants; a new wire route requires
84/// an explicit policy before the classifier can compile.
85#[must_use]
86pub const fn classify_wire_route(frame: FrameType) -> RouteClassification {
87    use SubjectSource as Source;
88    match frame {
89        FrameType::Connect => RouteClassification::Admission,
90        FrameType::Disconnect | FrameType::Ping => RouteClassification::Control,
91        FrameType::Publish => hot(HotAction::Publish, Source::FrameSubject),
92        FrameType::Subscribe => hot(HotAction::Subscribe, Source::FrameSubject),
93        FrameType::Unsubscribe => cleanup(HotAction::Subscribe, Source::SubscriptionBinding),
94        FrameType::ConversationOpen => hot(HotAction::Call, Source::FrameSubject),
95        FrameType::ConversationMessage => hot(HotAction::Call, Source::ConversationBinding),
96        FrameType::ConversationClose => cleanup(HotAction::Call, Source::ConversationBinding),
97        FrameType::Accept | FrameType::Defer | FrameType::Reject => {
98            hot(HotAction::Subscribe, Source::SubscriptionBinding)
99        }
100        FrameType::PushReply => hot(HotAction::Serve, Source::PushBinding),
101        FrameType::WorkerRegister => hot(HotAction::Serve, Source::WorkerBinding),
102        FrameType::LinkOpen => RouteClassification::Deliberate {
103            source: Source::FrameLink,
104        },
105        FrameType::LinkMessage => RouteClassification::Crossing {
106            source: Source::LinkBinding,
107        },
108        FrameType::ConnectAck
109        | FrameType::ConnectError
110        | FrameType::SubscribeAck
111        | FrameType::SubscribeError
112        | FrameType::PublishAck
113        | FrameType::PublishError
114        | FrameType::ConversationError
115        | FrameType::Pong
116        | FrameType::Push
117        | FrameType::WorkerRegisterAck
118        | FrameType::LinkOpenAck
119        | FrameType::DisconnectReason
120        | FrameType::Deliver => RouteClassification::ServerOnly,
121        FrameType::Unknown(tag) => {
122            if tag == PARTICIPANT_FRAME_TYPE {
123                RouteClassification::ParticipantEnvelope
124            } else {
125                RouteClassification::Unknown
126            }
127        }
128    }
129}
130
131/// Classifies every decoded client participant subrecord.
132///
133/// A recovery batch must decide every mapped conversation before any effect.
134/// Cleanup still requires the existing participant/connection ownership proof.
135#[must_use]
136pub const fn classify_participant_route(route: ClientDiscriminant) -> RouteClassification {
137    use SubjectSource::ParticipantBinding as Source;
138    match route {
139        ClientDiscriminant::EnrollmentRequest
140        | ClientDiscriminant::CredentialAttachRequest
141        | ClientDiscriminant::ParticipantAck
142        | ClientDiscriminant::MarkerAck
143        | ClientDiscriminant::ObserverRecoveryHandshake
144        | ClientDiscriminant::SettlementQuery => hot(HotAction::Subscribe, Source),
145        ClientDiscriminant::RecordAdmission => hot(HotAction::Publish, Source),
146        ClientDiscriminant::DetachRequest | ClientDiscriminant::LeaveRequest => {
147            cleanup(HotAction::Subscribe, Source)
148        }
149    }
150}