1use serde::{Deserialize, Serialize};
7
8pub mod advisory;
9pub mod live;
10pub mod lys_pass_adapter;
11pub mod prepared;
12mod refusal;
13pub mod root_presence;
14pub mod root_presence_start;
15pub mod routes;
16pub use refusal::{AccessDecisionError, AccessRefusal, NeededGrant, RefusalCause, RequiredGrant};
17pub mod session;
18
19pub const LIMINAL_PASS_AUDIENCE: &str = "liminal";
21
22#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
24#[serde(rename_all = "snake_case")]
25pub enum DecisionClass {
26 Hot,
28 Deliberate,
30}
31
32#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
34#[serde(rename_all = "snake_case")]
35pub enum AccessAction {
36 Publish,
38 Subscribe,
40 Call,
42 Serve,
44 LinkExport,
46 LinkImport,
48 DeployService,
50 Admin,
52}
53
54impl AccessAction {
55 #[must_use]
57 pub const fn class(self) -> DecisionClass {
58 match self {
59 Self::Publish | Self::Subscribe | Self::Call | Self::Serve => DecisionClass::Hot,
60 Self::LinkExport | Self::LinkImport | Self::DeployService | Self::Admin => {
61 DecisionClass::Deliberate
62 }
63 }
64 }
65}
66
67#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
69#[serde(rename_all = "snake_case")]
70pub enum HotAction {
71 Publish,
73 Subscribe,
75 Call,
77 Serve,
79}
80
81impl From<HotAction> for AccessAction {
82 fn from(value: HotAction) -> Self {
83 match value {
84 HotAction::Publish => Self::Publish,
85 HotAction::Subscribe => Self::Subscribe,
86 HotAction::Call => Self::Call,
87 HotAction::Serve => Self::Serve,
88 }
89 }
90}
91
92#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
94#[error("action {0:?} requires a live Lys decision")]
95pub struct DeliberateAction(pub AccessAction);
96
97impl TryFrom<AccessAction> for HotAction {
98 type Error = DeliberateAction;
99 fn try_from(value: AccessAction) -> Result<Self, Self::Error> {
100 match value {
101 AccessAction::Publish => Ok(Self::Publish),
102 AccessAction::Subscribe => Ok(Self::Subscribe),
103 AccessAction::Call => Ok(Self::Call),
104 AccessAction::Serve => Ok(Self::Serve),
105 AccessAction::LinkExport
106 | AccessAction::LinkImport
107 | AccessAction::DeployService
108 | AccessAction::Admin => Err(DeliberateAction(value)),
109 }
110 }
111}
112
113#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
115#[serde(rename_all = "snake_case")]
116pub enum ResourceKind {
117 Network,
119 Subject,
121 Link,
123 Service,
125}
126
127#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
129pub struct AccessResource {
130 pub kind: ResourceKind,
132 pub id: String,
134}
135
136#[derive(Debug, PartialEq, Eq)]
141pub struct SubjectBinding {
142 network_id: String,
143 resource: AccessResource,
144 subject: String,
145}
146
147#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
149#[error("invalid placed subject binding: {0}")]
150pub struct SubjectBindingError(pub &'static str);
151
152impl SubjectBinding {
153 pub fn new(
158 network_id: String,
159 resource_id: String,
160 subject: String,
161 ) -> Result<Self, SubjectBindingError> {
162 for (name, value) in [
163 ("network", &network_id),
164 ("resource", &resource_id),
165 ("subject", &subject),
166 ] {
167 if value.is_empty()
168 || value.chars().any(char::is_whitespace)
169 || value.chars().any(char::is_control)
170 {
171 return Err(SubjectBindingError(name));
172 }
173 }
174 if subject.contains(['*', '>']) {
175 return Err(SubjectBindingError("subject pattern"));
176 }
177 Ok(Self {
178 network_id,
179 resource: AccessResource {
180 kind: ResourceKind::Subject,
181 id: resource_id,
182 },
183 subject,
184 })
185 }
186
187 #[must_use]
189 pub fn network_id(&self) -> &str {
190 self.network_id.as_str()
191 }
192
193 #[must_use]
195 pub const fn resource(&self) -> &AccessResource {
196 &self.resource
197 }
198
199 #[must_use]
201 pub fn subject(&self) -> &str {
202 self.subject.as_str()
203 }
204}
205
206pub const LYS_APP: &str = "liminal";
208
209impl ResourceKind {
210 #[must_use]
212 pub const fn qualified(self) -> &'static str {
213 match self {
214 Self::Network => "liminal.network",
215 Self::Subject => "liminal.subject",
216 Self::Link => "liminal.link",
217 Self::Service => "liminal.service",
218 }
219 }
220}
221
222impl AccessAction {
223 #[must_use]
225 pub const fn name(self) -> &'static str {
226 match self {
227 Self::Publish => "publish",
228 Self::Subscribe => "subscribe",
229 Self::Call => "call",
230 Self::Serve => "serve",
231 Self::LinkExport => "link_export",
232 Self::LinkImport => "link_import",
233 Self::DeployService => "deploy_service",
234 Self::Admin => "admin",
235 }
236 }
237}
238
239#[derive(Clone, Debug, PartialEq, Eq)]
241pub struct GrantReceipt {
242 pub grant: String,
244}
245
246pub trait LysPassConsumer {
254 type Pass;
256 type Holder: ?Sized;
258 type AdmissionError: std::error::Error;
260
261 fn verify_offline(
266 &self,
267 credential: &[u8],
268 audience: &str,
269 now: u64,
270 ) -> Result<Self::Pass, Self::AdmissionError>;
271
272 fn verified_holder<'p>(&self, pass: &'p Self::Pass) -> &'p Self::Holder;
274
275 fn verified_holder_kind<'p>(&self, _pass: &'p Self::Pass) -> Option<&'p str> {
281 None
282 }
283
284 fn verified_issued_at(&self, _pass: &Self::Pass) -> Option<u64> {
288 None
289 }
290
291 fn decide_hot(
296 &self,
297 pass: &Self::Pass,
298 subject: &SubjectBinding,
299 action: HotAction,
300 now: u64,
301 ) -> Result<GrantReceipt, AccessDecisionError>;
302
303 fn decide_link(
309 &self,
310 pass: &Self::Pass,
311 subject: &SubjectBinding,
312 action: crate::link::LinkAction,
313 now: u64,
314 ) -> Result<GrantReceipt, AccessDecisionError>;
315}
316
317pub struct AdmittedLysPass<P> {
319 pass: P,
320}
321
322impl<P> std::fmt::Debug for AdmittedLysPass<P> {
323 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
324 formatter
325 .debug_struct("AdmittedLysPass")
326 .finish_non_exhaustive()
327 }
328}
329
330pub fn admit_lys_pass<C: LysPassConsumer>(
335 consumer: &C,
336 credential: &[u8],
337 now: u64,
338) -> Result<AdmittedLysPass<C::Pass>, C::AdmissionError> {
339 consumer
340 .verify_offline(credential, LIMINAL_PASS_AUDIENCE, now)
341 .map(|pass| AdmittedLysPass { pass })
342}
343
344pub fn decide_message<C: LysPassConsumer>(
352 consumer: &C,
353 pass: &AdmittedLysPass<C::Pass>,
354 subject: &SubjectBinding,
355 action: HotAction,
356 now: u64,
357) -> Result<GrantReceipt, AccessDecisionError> {
358 consumer.decide_hot(&pass.pass, subject, action, now)
359}
360
361#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
363#[serde(rename_all = "snake_case")]
364pub enum GrantMode {
365 Outright,
367 ByDraft,
369 ByTwo,
371}
372
373#[derive(Debug, PartialEq, Eq)]
375pub enum DeliberateDecision {
376 Outright(GrantReceipt),
378 ByDraft {
380 grant: String,
382 draft: String,
384 },
385 ByTwo {
387 grant: String,
389 draft: String,
391 },
392}
393
394pub trait LysDeliberateConsumer: LysPassConsumer {
400 fn hold_permission(
405 &self,
406 _pass: &Self::Pass,
407 _request: prepared::PermissionRequest<'_>,
408 ) -> Result<serde_json::Value, lys_pass::Error> {
409 Err(lys_pass::Error::CannotAsk(
410 "prepared permission unavailable",
411 ))
412 }
413
414 fn hold_site(
419 &self,
420 _pass: &Self::Pass,
421 _request: prepared::SiteRequest<'_>,
422 ) -> Result<serde_json::Value, lys_pass::Error> {
423 Err(lys_pass::Error::CannotAsk(
424 "prepared construction unavailable",
425 ))
426 }
427
428 fn decide_live(
433 &self,
434 pass: &Self::Pass,
435 resource: &AccessResource,
436 action: AccessAction,
437 now: u64,
438 ) -> Result<DeliberateDecision, AccessDecisionError>;
439}
440
441#[must_use]
443pub fn verified_lys_holder<'p, C: LysPassConsumer>(
444 consumer: &C,
445 pass: &'p AdmittedLysPass<C::Pass>,
446) -> &'p C::Holder {
447 consumer.verified_holder(&pass.pass)
448}
449
450pub fn decide_deliberate<C: LysDeliberateConsumer>(
458 consumer: &C,
459 pass: &AdmittedLysPass<C::Pass>,
460 resource: &AccessResource,
461 action: AccessAction,
462 now: u64,
463) -> Result<DeliberateDecision, AccessDecisionError> {
464 consumer.decide_live(&pass.pass, resource, action, now)
465}
466
467pub trait LysPolicySource {
472 type Snapshot;
474 type Error: std::error::Error;
476
477 fn read_policy(&self, resource: &AccessResource) -> Result<Self::Snapshot, Self::Error>;
482}
483
484#[path = "auth_lys/route_class.rs"]
485mod route_class;
486
487#[cfg(test)]
488#[path = "auth_lys/advisory_tests.rs"]
489mod advisory_tests;
490#[cfg(test)]
491#[path = "auth_lys/decision_sites_census_test.rs"]
492mod decision_sites_census_test;
493#[cfg(test)]
494#[path = "auth_lys/root_presence_start_tests.rs"]
495mod root_presence_start_tests;
496#[cfg(test)]
497#[path = "auth_lys/root_presence_tests.rs"]
498mod root_presence_tests;
499pub use route_class::AccessRouteClass;
500
501pub use live::drafts;