1use crate::auth_lys::{
13 AccessAction, AccessDecisionError, AccessResource, AdmittedLysPass, DeliberateDecision,
14 GrantReceipt, LysDeliberateConsumer, ResourceKind, SubjectBinding, SubjectBindingError,
15 decide_deliberate,
16};
17use crate::link_handshake::{LocalMachine, MachineIdentity};
18use std::collections::BTreeMap;
19
20mod life;
21pub use life::LinkLifeCounters;
22pub(crate) use life::{CloseLinks, LinkLife, LinkRegistry};
23
24#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
26pub enum LinkEnd {
27 #[error("peer_gone")]
29 PeerGone,
30 #[error("revoked")]
32 Revoked,
33 #[error("refused: {0}")]
35 Refused(String),
36 #[error("replaced")]
38 Replaced,
39 #[error("shutdown")]
41 Shutdown,
42}
43
44#[derive(Clone, Copy, Debug, PartialEq, Eq)]
46pub enum LinkAction {
47 Export,
49 Import,
51}
52
53impl LinkAction {
54 #[must_use]
56 pub const fn access_action(self) -> AccessAction {
57 match self {
58 Self::Export => AccessAction::LinkExport,
59 Self::Import => AccessAction::LinkImport,
60 }
61 }
62
63 #[must_use]
65 pub const fn other(self) -> Self {
66 match self {
67 Self::Export => Self::Import,
68 Self::Import => Self::Export,
69 }
70 }
71}
72
73impl From<liminal::protocol::LinkEnd> for LinkAction {
74 fn from(end: liminal::protocol::LinkEnd) -> Self {
75 match end {
76 liminal::protocol::LinkEnd::Export => Self::Export,
77 liminal::protocol::LinkEnd::Import => Self::Import,
78 }
79 }
80}
81
82#[derive(Debug)]
91pub struct LinkHost {
92 local: LocalMachine,
93 rights: Box<dyn LinkRights>,
94 recorded: BTreeMap<String, MachineIdentity>,
95}
96
97impl LinkHost {
98 #[must_use]
100 pub const fn new(
101 local: LocalMachine,
102 rights: Box<dyn LinkRights>,
103 recorded: BTreeMap<String, MachineIdentity>,
104 ) -> Self {
105 Self {
106 local,
107 rights,
108 recorded,
109 }
110 }
111
112 #[must_use]
114 pub const fn local(&self) -> &LocalMachine {
115 &self.local
116 }
117
118 #[must_use]
120 pub fn rights(&self) -> &dyn LinkRights {
121 self.rights.as_ref()
122 }
123
124 #[must_use]
126 pub fn recorded_machine(&self, holder: &str) -> Option<MachineIdentity> {
127 self.recorded.get(holder).copied()
128 }
129}
130
131pub trait LinkRights: Send + Sync + std::fmt::Debug {
135 fn holds(
140 &self,
141 action: LinkAction,
142 subject: &SubjectBinding,
143 now: u64,
144 ) -> Result<GrantReceipt, AccessDecisionError>;
145}
146
147#[derive(Clone, Debug, PartialEq, Eq)]
149pub struct Link {
150 network_id: String,
151 id: String,
152 exporter: MachineIdentity,
153 importer: MachineIdentity,
154}
155
156#[derive(Debug, PartialEq, Eq)]
158pub enum LinkError {
159 Identifier(&'static str),
161 SameMachine,
163}
164
165impl std::fmt::Display for LinkError {
166 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
167 match self {
168 Self::Identifier(member) => write!(formatter, "link {member} is not an identifier"),
169 Self::SameMachine => formatter.write_str("a link joins two different machines"),
170 }
171 }
172}
173
174impl std::error::Error for LinkError {}
175
176#[derive(Debug, PartialEq, Eq)]
178pub enum LinkRefusal {
179 Binding(SubjectBindingError),
181 Export(AccessDecisionError),
183 Import(AccessDecisionError),
185}
186
187impl std::fmt::Display for LinkRefusal {
188 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
189 match self {
190 Self::Binding(SubjectBindingError(member)) => {
191 write!(formatter, "crossing refused: subject {member}")
192 }
193 Self::Export(error) => write!(formatter, "crossing refused at export: {error}"),
194 Self::Import(error) => write!(formatter, "crossing refused at import: {error}"),
195 }
196 }
197}
198
199impl std::error::Error for LinkRefusal {}
200
201#[derive(Clone, Debug, PartialEq, Eq)]
203pub struct Crossing {
204 pub export: GrantReceipt,
206 pub import: GrantReceipt,
208}
209
210impl Link {
211 pub fn new(
217 network_id: String,
218 id: String,
219 exporter: MachineIdentity,
220 importer: MachineIdentity,
221 ) -> Result<Self, LinkError> {
222 for (member, value) in [("network", &network_id), ("id", &id)] {
223 if value.is_empty() || value.chars().any(char::is_whitespace) {
224 return Err(LinkError::Identifier(member));
225 }
226 }
227 if exporter == importer {
228 return Err(LinkError::SameMachine);
229 }
230 Ok(Self {
231 network_id,
232 id,
233 exporter,
234 importer,
235 })
236 }
237
238 #[must_use]
240 pub fn network_id(&self) -> &str {
241 &self.network_id
242 }
243
244 #[must_use]
246 pub fn resource(&self) -> AccessResource {
247 AccessResource {
248 kind: ResourceKind::Link,
249 id: self.id.clone(),
250 }
251 }
252
253 #[must_use]
255 pub const fn exporter(&self) -> &MachineIdentity {
256 &self.exporter
257 }
258
259 #[must_use]
261 pub const fn importer(&self) -> &MachineIdentity {
262 &self.importer
263 }
264
265 pub fn open<C: LysDeliberateConsumer>(
273 &self,
274 end: LinkAction,
275 consumer: &C,
276 pass: &AdmittedLysPass<C::Pass>,
277 now: u64,
278 ) -> Result<DeliberateDecision, AccessDecisionError> {
279 decide_deliberate(consumer, pass, &self.resource(), end.access_action(), now)
280 }
281
282 pub fn crossing(
288 &self,
289 exporter: &dyn LinkRights,
290 importer: &dyn LinkRights,
291 subject: &str,
292 now: u64,
293 ) -> Result<Crossing, LinkRefusal> {
294 let bound =
295 SubjectBinding::link(self.network_id.clone(), self.id.clone(), subject.to_owned())
296 .map_err(LinkRefusal::Binding)?;
297 let export = exporter
298 .holds(LinkAction::Export, &bound, now)
299 .map_err(LinkRefusal::Export)?;
300 let import = importer
301 .holds(LinkAction::Import, &bound, now)
302 .map_err(LinkRefusal::Import)?;
303 Ok(Crossing { export, import })
304 }
305}