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