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