1use crate::auth_lys::session::LysSession;
15use crate::auth_lys::{
16 AccessAction, AccessDecisionError, AccessResource, AccessRouteClass, AdmittedLysPass,
17 DeliberateDecision, GrantMode, GrantReceipt, HotAction, LysDeliberateConsumer, RefusalCause,
18 SubjectBinding, SubjectBindingError, decide_deliberate, decide_message,
19};
20use serde::{Deserialize, Serialize};
21
22pub mod actions;
23pub mod broker;
24pub mod budgets;
25pub mod context_watch;
26pub mod keys;
27pub mod lifecycle;
28pub mod lys_http;
29pub mod production;
30pub mod relay;
31pub mod seats;
32
33#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
35#[serde(rename_all = "kebab-case")]
36pub enum ServiceKind {
37 ContextWatch,
38 Seats,
39 Budgets,
40 Alarms,
41 Launches,
42 SessionKeys,
43}
44
45impl ServiceKind {
46 pub const ALL: [Self; 6] = [
48 Self::ContextWatch,
49 Self::Seats,
50 Self::Budgets,
51 Self::Alarms,
52 Self::Launches,
53 Self::SessionKeys,
54 ];
55
56 #[must_use]
58 pub const fn name(self) -> &'static str {
59 match self {
60 Self::ContextWatch => "context-watch",
61 Self::Seats => "seats",
62 Self::Budgets => "budgets",
63 Self::Alarms => "alarms",
64 Self::Launches => "launches",
65 Self::SessionKeys => "session-keys",
66 }
67 }
68
69 #[must_use]
72 pub const fn subject_shape(self) -> &'static str {
73 match self {
74 Self::ContextWatch => "context-watch",
75 Self::Seats => "seats.<name>",
76 Self::Budgets => "budgets.<name>",
77 Self::Alarms => "alarms",
78 Self::Launches => "launches",
79 Self::SessionKeys => "sessions.<id>.keys",
80 }
81 }
82
83 #[must_use]
85 pub const fn default_class(self) -> AccessRouteClass {
86 match self {
87 Self::SessionKeys => AccessRouteClass::Deliberate {
88 mode: GrantMode::ByDraft,
89 },
90 Self::ContextWatch | Self::Seats | Self::Budgets | Self::Alarms | Self::Launches => {
91 AccessRouteClass::Call
92 }
93 }
94 }
95
96 #[must_use]
98 pub fn manifest(self) -> ServiceManifest {
99 ServiceManifest {
100 service: self,
101 subject: self.subject_shape().to_owned(),
102 placed_under: "network".to_owned(),
103 class: self.default_class(),
104 }
105 }
106}
107
108#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
111pub struct ServiceManifest {
112 pub service: ServiceKind,
114 pub subject: String,
116 pub placed_under: String,
118 #[serde(flatten)]
120 pub class: AccessRouteClass,
121}
122
123#[derive(Clone, Debug, PartialEq, Eq)]
125pub struct ServiceSubject<'a> {
126 pub service: ServiceKind,
128 pub instance: Option<&'a str>,
130}
131
132impl ServiceSubject<'_> {
133 #[must_use]
135 pub const fn class(&self) -> AccessRouteClass {
136 self.service.default_class()
137 }
138}
139
140fn segment(value: &str) -> Option<&str> {
141 (!value.is_empty()
142 && !value.contains(['.', '*', '>'])
143 && !value.chars().any(char::is_whitespace))
144 .then_some(value)
145}
146
147#[must_use]
150pub fn service_of(subject: &str) -> Option<ServiceSubject<'_>> {
151 match subject {
152 "context-watch" => Some(ServiceSubject {
153 service: ServiceKind::ContextWatch,
154 instance: None,
155 }),
156 "alarms" => Some(ServiceSubject {
157 service: ServiceKind::Alarms,
158 instance: None,
159 }),
160 "launches" => Some(ServiceSubject {
161 service: ServiceKind::Launches,
162 instance: None,
163 }),
164 _ => {
165 if let Some(name) = subject.strip_prefix("seats.") {
166 return segment(name).map(|name| ServiceSubject {
167 service: ServiceKind::Seats,
168 instance: Some(name),
169 });
170 }
171 if let Some(name) = subject.strip_prefix("budgets.") {
172 return segment(name).map(|name| ServiceSubject {
173 service: ServiceKind::Budgets,
174 instance: Some(name),
175 });
176 }
177 let id = subject.strip_prefix("sessions.")?.strip_suffix(".keys")?;
178 segment(id).map(|id| ServiceSubject {
179 service: ServiceKind::SessionKeys,
180 instance: Some(id),
181 })
182 }
183 }
184}
185
186#[must_use]
190pub fn classify_subject(subject: &str) -> Option<AccessRouteClass> {
191 service_of(subject).map(|resolved| resolved.class())
192}
193
194#[derive(Clone, Debug, PartialEq, Eq)]
196pub struct ServiceEffectError(pub String);
197
198impl std::fmt::Display for ServiceEffectError {
199 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
200 write!(formatter, "service effect failed: {}", self.0)
201 }
202}
203
204impl std::error::Error for ServiceEffectError {}
205
206#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
212pub struct BudgetPolicy {
213 pub holder: String,
216 pub budgets: serde_json::Value,
218 pub stops: serde_json::Value,
221}
222
223#[derive(Clone, Copy, Debug, PartialEq, Eq)]
230pub struct ServiceRequest<'a> {
231 pub operation: &'a str,
233 pub payload: &'a [u8],
235}
236
237pub trait SeatStarter {
240 fn start(&self, seat: &str, operation: &str) -> Result<(), ServiceEffectError>;
245}
246
247pub trait KeySender {
249 fn send(&self, session: &str, keys: &[u8], operation: &str) -> Result<(), ServiceEffectError>;
254}
255
256pub trait BudgetSource {
259 fn read(&self, name: &str) -> Result<Option<BudgetPolicy>, ServiceEffectError>;
262}
263
264pub trait ServiceAction {
266 fn act(
271 &self,
272 service: ServiceKind,
273 payload: &[u8],
274 operation: &str,
275 ) -> Result<(), ServiceEffectError>;
276}
277
278pub struct Services<'a> {
280 pub seats: &'a dyn SeatStarter,
282 pub keys: &'a dyn KeySender,
284 pub budgets: &'a dyn BudgetSource,
286 pub actions: &'a dyn ServiceAction,
288}
289
290impl std::fmt::Debug for Services<'_> {
291 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
292 formatter.debug_struct("Services").finish_non_exhaustive()
293 }
294}
295
296#[derive(Clone, Debug, PartialEq, Eq)]
298pub enum ServiceOutcome {
299 Acted {
301 receipt: GrantReceipt,
303 },
304 Held {
306 mode: GrantMode,
308 grant: String,
310 draft: String,
312 },
313 Policy(BudgetPolicy),
315}
316
317#[derive(Debug, PartialEq, Eq)]
319pub enum ServiceRefusal {
320 NotAService,
322 Binding(SubjectBindingError),
324 Decision(AccessDecisionError),
327 PolicyUnavailable,
329 NoOperation,
332 Effect(ServiceEffectError),
334}
335
336impl std::fmt::Display for ServiceRefusal {
337 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
338 match self {
339 Self::NotAService => formatter.write_str("no service serves this subject"),
340 Self::Binding(SubjectBindingError(member)) => {
341 write!(formatter, "subject binding refused: {member}")
342 }
343 Self::Decision(error) => write!(formatter, "{error}"),
344 Self::PolicyUnavailable => {
345 formatter.write_str("budget policy unavailable from Lys; liminal sets none")
346 }
347 Self::Effect(error) => write!(formatter, "{error}"),
348 Self::NoOperation => {
349 formatter.write_str("service request names no operation id; nothing was done")
350 }
351 }
352 }
353}
354
355impl std::error::Error for ServiceRefusal {}
356
357pub fn serve<C: LysDeliberateConsumer>(
368 services: &Services<'_>,
369 consumer: Option<&C>,
370 pass: &AdmittedLysPass<C::Pass>,
371 network_id: &str,
372 subject: &str,
373 request: &ServiceRequest<'_>,
374 now: u64,
375) -> Result<ServiceOutcome, ServiceRefusal> {
376 let decider = consumer.map(|consumer| ConsumerDecider { consumer, pass });
377 serve_with(
378 services,
379 decider
380 .as_ref()
381 .map(|decider| -> &dyn ServiceDecider { decider }),
382 network_id,
383 subject,
384 request,
385 now,
386 )
387}
388
389pub fn serve_session(
398 services: &Services<'_>,
399 session: Option<&dyn LysSession>,
400 network_id: &str,
401 subject: &str,
402 request: &ServiceRequest<'_>,
403 now: u64,
404) -> Result<ServiceOutcome, ServiceRefusal> {
405 let decider = session.map(SessionDecider);
406 serve_with(
407 services,
408 decider
409 .as_ref()
410 .map(|decider| -> &dyn ServiceDecider { decider }),
411 network_id,
412 subject,
413 request,
414 now,
415 )
416}
417
418pub trait ServiceDecider {
425 fn hot(
430 &self,
431 subject: &SubjectBinding,
432 action: HotAction,
433 now: u64,
434 ) -> Result<GrantReceipt, AccessDecisionError>;
435
436 fn live(
441 &self,
442 resource: &AccessResource,
443 action: AccessAction,
444 now: u64,
445 ) -> Result<DeliberateDecision, AccessDecisionError>;
446}
447
448struct ConsumerDecider<'a, C: LysDeliberateConsumer> {
449 consumer: &'a C,
450 pass: &'a AdmittedLysPass<C::Pass>,
451}
452
453impl<C: LysDeliberateConsumer> ServiceDecider for ConsumerDecider<'_, C> {
454 fn hot(
455 &self,
456 subject: &SubjectBinding,
457 action: HotAction,
458 now: u64,
459 ) -> Result<GrantReceipt, AccessDecisionError> {
460 decide_message(self.consumer, self.pass, subject, action, now)
461 }
462
463 fn live(
464 &self,
465 resource: &AccessResource,
466 action: AccessAction,
467 now: u64,
468 ) -> Result<DeliberateDecision, AccessDecisionError> {
469 decide_deliberate(self.consumer, self.pass, resource, action, now)
470 }
471}
472
473struct SessionDecider<'a>(&'a dyn LysSession);
474
475impl ServiceDecider for SessionDecider<'_> {
476 fn hot(
477 &self,
478 subject: &SubjectBinding,
479 action: HotAction,
480 now: u64,
481 ) -> Result<GrantReceipt, AccessDecisionError> {
482 self.0.decide(subject, action, now)
483 }
484
485 fn live(
486 &self,
487 resource: &AccessResource,
488 action: AccessAction,
489 now: u64,
490 ) -> Result<DeliberateDecision, AccessDecisionError> {
491 self.0.decide_live(resource, action, now)
492 }
493}
494
495fn serve_with(
496 services: &Services<'_>,
497 decider: Option<&dyn ServiceDecider>,
498 network_id: &str,
499 subject: &str,
500 request: &ServiceRequest<'_>,
501 now: u64,
502) -> Result<ServiceOutcome, ServiceRefusal> {
503 let resolved = service_of(subject).ok_or(ServiceRefusal::NotAService)?;
504 if request.operation.trim().is_empty() {
505 return Err(ServiceRefusal::NoOperation);
506 }
507 let binding = SubjectBinding::new(
508 network_id.to_owned(),
509 subject.to_owned(),
510 subject.to_owned(),
511 )
512 .map_err(ServiceRefusal::Binding)?;
513 let Some(decider) = decider else {
514 return Err(ServiceRefusal::Decision(unavailable(&binding, &resolved)));
515 };
516 match resolved.class() {
517 AccessRouteClass::Deliberate { .. } => {
518 let decision = decider
519 .live(binding.resource(), HotAction::Call.into(), now)
520 .map_err(ServiceRefusal::Decision)?;
521 match decision {
522 DeliberateDecision::Outright(receipt) => {
523 effect(services, &resolved, request)?;
524 Ok(ServiceOutcome::Acted { receipt })
525 }
526 DeliberateDecision::ByDraft { grant, draft } => Ok(ServiceOutcome::Held {
527 mode: GrantMode::ByDraft,
528 grant,
529 draft,
530 }),
531 DeliberateDecision::ByTwo { grant, draft } => Ok(ServiceOutcome::Held {
532 mode: GrantMode::ByTwo,
533 grant,
534 draft,
535 }),
536 }
537 }
538 AccessRouteClass::Call
539 | AccessRouteClass::Publish
540 | AccessRouteClass::Subscribe
541 | AccessRouteClass::Serve => {
542 let receipt = decider
543 .hot(&binding, HotAction::Call, now)
544 .map_err(ServiceRefusal::Decision)?;
545 if resolved.service == ServiceKind::Budgets {
546 let name = resolved.instance.unwrap_or_default();
547 let policy = services
548 .budgets
549 .read(name)
550 .map_err(ServiceRefusal::Effect)?;
551 return policy.map_or(Err(ServiceRefusal::PolicyUnavailable), |policy| {
552 Ok(ServiceOutcome::Policy(policy))
553 });
554 }
555 effect(services, &resolved, request)?;
556 Ok(ServiceOutcome::Acted { receipt })
557 }
558 }
559}
560
561fn effect(
562 services: &Services<'_>,
563 resolved: &ServiceSubject<'_>,
564 request: &ServiceRequest<'_>,
565) -> Result<(), ServiceRefusal> {
566 let instance = resolved.instance.unwrap_or_default();
567 match resolved.service {
568 ServiceKind::Seats => services.seats.start(instance, request.operation),
569 ServiceKind::SessionKeys => {
570 services
571 .keys
572 .send(instance, request.payload, request.operation)
573 }
574 ServiceKind::Budgets => Ok(()),
575 ServiceKind::ContextWatch | ServiceKind::Alarms | ServiceKind::Launches => services
576 .actions
577 .act(resolved.service, request.payload, request.operation),
578 }
579 .map_err(ServiceRefusal::Effect)
580}
581
582fn unavailable(binding: &SubjectBinding, resolved: &ServiceSubject<'_>) -> AccessDecisionError {
583 let action = match resolved.class() {
584 AccessRouteClass::Deliberate { .. } => crate::auth_lys::AccessAction::Admin,
585 _ => HotAction::Call.into(),
586 };
587 AccessDecisionError {
588 cause: RefusalCause::DeliberateUnavailable,
589 refusal: crate::auth_lys::AccessRefusal::new(
590 binding.resource().clone(),
591 action,
592 format!("lys://ask/{}", binding.subject()),
593 ),
594 }
595}