1use serde::{Deserialize, Serialize};
7
8pub mod live;
9pub mod lys_pass_adapter;
10pub mod prepared;
11mod refusal;
12pub mod root_presence;
13pub mod root_presence_start;
14pub mod routes;
15pub use refusal::{AccessDecisionError, AccessRefusal, NeededGrant, RefusalCause, RequiredGrant};
16pub mod session;
17
18pub const LIMINAL_PASS_AUDIENCE: &str = "liminal";
20
21#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
23#[serde(rename_all = "snake_case")]
24pub enum DecisionClass {
25 Hot,
27 Deliberate,
29}
30
31#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
33#[serde(rename_all = "snake_case")]
34pub enum AccessAction {
35 Publish,
37 Subscribe,
39 Call,
41 Serve,
43 LinkExport,
45 LinkImport,
47 DeployService,
49 Admin,
51}
52
53impl AccessAction {
54 #[must_use]
56 pub const fn class(self) -> DecisionClass {
57 match self {
58 Self::Publish | Self::Subscribe | Self::Call | Self::Serve => DecisionClass::Hot,
59 Self::LinkExport | Self::LinkImport | Self::DeployService | Self::Admin => {
60 DecisionClass::Deliberate
61 }
62 }
63 }
64}
65
66#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
68#[serde(rename_all = "snake_case")]
69pub enum HotAction {
70 Publish,
72 Subscribe,
74 Call,
76 Serve,
78}
79
80impl From<HotAction> for AccessAction {
81 fn from(value: HotAction) -> Self {
82 match value {
83 HotAction::Publish => Self::Publish,
84 HotAction::Subscribe => Self::Subscribe,
85 HotAction::Call => Self::Call,
86 HotAction::Serve => Self::Serve,
87 }
88 }
89}
90
91#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
93#[error("action {0:?} requires a live Lys decision")]
94pub struct DeliberateAction(pub AccessAction);
95
96impl TryFrom<AccessAction> for HotAction {
97 type Error = DeliberateAction;
98 fn try_from(value: AccessAction) -> Result<Self, Self::Error> {
99 match value {
100 AccessAction::Publish => Ok(Self::Publish),
101 AccessAction::Subscribe => Ok(Self::Subscribe),
102 AccessAction::Call => Ok(Self::Call),
103 AccessAction::Serve => Ok(Self::Serve),
104 AccessAction::LinkExport
105 | AccessAction::LinkImport
106 | AccessAction::DeployService
107 | AccessAction::Admin => Err(DeliberateAction(value)),
108 }
109 }
110}
111
112#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
114#[serde(rename_all = "snake_case")]
115pub enum ResourceKind {
116 Network,
118 Subject,
120 Link,
122 Service,
124}
125
126#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
128pub struct AccessResource {
129 pub kind: ResourceKind,
131 pub id: String,
133}
134
135#[derive(Debug, PartialEq, Eq)]
140pub struct SubjectBinding {
141 network_id: String,
142 resource: AccessResource,
143 subject: String,
144}
145
146#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
148#[error("invalid placed subject binding: {0}")]
149pub struct SubjectBindingError(pub &'static str);
150
151impl SubjectBinding {
152 pub fn new(
157 network_id: String,
158 resource_id: String,
159 subject: String,
160 ) -> Result<Self, SubjectBindingError> {
161 for (name, value) in [
162 ("network", &network_id),
163 ("resource", &resource_id),
164 ("subject", &subject),
165 ] {
166 if value.is_empty()
167 || value.chars().any(char::is_whitespace)
168 || value.chars().any(char::is_control)
169 {
170 return Err(SubjectBindingError(name));
171 }
172 }
173 if subject.contains(['*', '>']) {
174 return Err(SubjectBindingError("subject pattern"));
175 }
176 Ok(Self {
177 network_id,
178 resource: AccessResource {
179 kind: ResourceKind::Subject,
180 id: resource_id,
181 },
182 subject,
183 })
184 }
185
186 #[must_use]
188 pub fn network_id(&self) -> &str {
189 self.network_id.as_str()
190 }
191
192 #[must_use]
194 pub const fn resource(&self) -> &AccessResource {
195 &self.resource
196 }
197
198 #[must_use]
200 pub fn subject(&self) -> &str {
201 self.subject.as_str()
202 }
203}
204
205pub const LYS_APP: &str = "liminal";
207
208impl ResourceKind {
209 #[must_use]
211 pub const fn qualified(self) -> &'static str {
212 match self {
213 Self::Network => "liminal.network",
214 Self::Subject => "liminal.subject",
215 Self::Link => "liminal.link",
216 Self::Service => "liminal.service",
217 }
218 }
219}
220
221impl AccessAction {
222 #[must_use]
224 pub const fn name(self) -> &'static str {
225 match self {
226 Self::Publish => "publish",
227 Self::Subscribe => "subscribe",
228 Self::Call => "call",
229 Self::Serve => "serve",
230 Self::LinkExport => "link_export",
231 Self::LinkImport => "link_import",
232 Self::DeployService => "deploy_service",
233 Self::Admin => "admin",
234 }
235 }
236}
237
238#[derive(Clone, Debug, PartialEq, Eq)]
240pub struct GrantReceipt {
241 pub grant: String,
243}
244
245pub trait LysPassConsumer {
253 type Pass;
255 type Holder: ?Sized;
257 type AdmissionError: std::error::Error;
259
260 fn verify_offline(
265 &self,
266 credential: &[u8],
267 audience: &str,
268 now: u64,
269 ) -> Result<Self::Pass, Self::AdmissionError>;
270
271 fn verified_holder<'p>(&self, pass: &'p Self::Pass) -> &'p Self::Holder;
273
274 fn verified_holder_kind<'p>(&self, _pass: &'p Self::Pass) -> Option<&'p str> {
280 None
281 }
282
283 fn verified_issued_at(&self, _pass: &Self::Pass) -> Option<u64> {
287 None
288 }
289
290 fn decide_hot(
295 &self,
296 pass: &Self::Pass,
297 subject: &SubjectBinding,
298 action: HotAction,
299 now: u64,
300 ) -> Result<GrantReceipt, AccessDecisionError>;
301
302 fn decide_link(
308 &self,
309 pass: &Self::Pass,
310 subject: &SubjectBinding,
311 action: crate::link::LinkAction,
312 now: u64,
313 ) -> Result<GrantReceipt, AccessDecisionError>;
314}
315
316pub struct AdmittedLysPass<P> {
318 pass: P,
319}
320
321impl<P> std::fmt::Debug for AdmittedLysPass<P> {
322 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
323 formatter
324 .debug_struct("AdmittedLysPass")
325 .finish_non_exhaustive()
326 }
327}
328
329pub fn admit_lys_pass<C: LysPassConsumer>(
334 consumer: &C,
335 credential: &[u8],
336 now: u64,
337) -> Result<AdmittedLysPass<C::Pass>, C::AdmissionError> {
338 consumer
339 .verify_offline(credential, LIMINAL_PASS_AUDIENCE, now)
340 .map(|pass| AdmittedLysPass { pass })
341}
342
343pub fn decide_message<C: LysPassConsumer>(
351 consumer: &C,
352 pass: &AdmittedLysPass<C::Pass>,
353 subject: &SubjectBinding,
354 action: HotAction,
355 now: u64,
356) -> Result<GrantReceipt, AccessDecisionError> {
357 consumer.decide_hot(&pass.pass, subject, action, now)
358}
359
360#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
362#[serde(rename_all = "snake_case")]
363pub enum GrantMode {
364 Outright,
366 ByDraft,
368 ByTwo,
370}
371
372#[derive(Debug, PartialEq, Eq)]
374pub enum DeliberateDecision {
375 Outright(GrantReceipt),
377 ByDraft {
379 grant: String,
381 draft: String,
383 },
384 ByTwo {
386 grant: String,
388 draft: String,
390 },
391}
392
393pub trait LysDeliberateConsumer: LysPassConsumer {
399 fn hold_permission(
404 &self,
405 _pass: &Self::Pass,
406 _request: prepared::PermissionRequest<'_>,
407 ) -> Result<serde_json::Value, lys_pass::Error> {
408 Err(lys_pass::Error::CannotAsk(
409 "prepared permission unavailable",
410 ))
411 }
412
413 fn hold_site(
418 &self,
419 _pass: &Self::Pass,
420 _request: prepared::SiteRequest<'_>,
421 ) -> Result<serde_json::Value, lys_pass::Error> {
422 Err(lys_pass::Error::CannotAsk(
423 "prepared construction unavailable",
424 ))
425 }
426
427 fn decide_live(
432 &self,
433 pass: &Self::Pass,
434 resource: &AccessResource,
435 action: AccessAction,
436 now: u64,
437 ) -> Result<DeliberateDecision, AccessDecisionError>;
438}
439
440#[must_use]
442pub fn verified_lys_holder<'p, C: LysPassConsumer>(
443 consumer: &C,
444 pass: &'p AdmittedLysPass<C::Pass>,
445) -> &'p C::Holder {
446 consumer.verified_holder(&pass.pass)
447}
448
449pub fn decide_deliberate<C: LysDeliberateConsumer>(
457 consumer: &C,
458 pass: &AdmittedLysPass<C::Pass>,
459 resource: &AccessResource,
460 action: AccessAction,
461 now: u64,
462) -> Result<DeliberateDecision, AccessDecisionError> {
463 consumer.decide_live(&pass.pass, resource, action, now)
464}
465
466pub trait LysPolicySource {
471 type Snapshot;
473 type Error: std::error::Error;
475
476 fn read_policy(&self, resource: &AccessResource) -> Result<Self::Snapshot, Self::Error>;
481}
482
483#[path = "auth_lys/route_class.rs"]
484mod route_class;
485
486#[cfg(test)]
487#[path = "auth_lys/root_presence_start_tests.rs"]
488mod root_presence_start_tests;
489#[cfg(test)]
490#[path = "auth_lys/root_presence_tests.rs"]
491mod root_presence_tests;
492pub use route_class::AccessRouteClass;
493
494pub use live::drafts;