1use super::{
14 AccessAction, AccessDecisionError, AccessRefusal, AccessResource, DeliberateDecision,
15 GrantMode, GrantReceipt, HotAction, LysDeliberateConsumer, LysPassConsumer, RefusalCause,
16 ResourceKind, SubjectBinding,
17};
18use crate::config::types::Decisions;
19use crate::link::LinkAction;
20use lys_pass::{Decision, Error, KeySet, Target, VerifiedPass};
21use std::sync::Arc;
22
23pub const REQUEST_PATH: &str = "/#/requests";
25
26pub struct LysPass {
30 verified: Arc<VerifiedPass>,
31 token: String,
32 warned: super::advisory::Warned,
34}
35
36impl std::fmt::Debug for LysPass {
37 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
38 formatter
39 .debug_struct("LysPass")
40 .field("holder", &self.holder())
41 .finish_non_exhaustive()
42 }
43}
44
45impl LysPass {
46 pub(super) fn shared_verified(&self) -> Arc<VerifiedPass> {
47 self.verified.clone()
48 }
49 pub(super) fn raw_token(&self) -> &str {
50 &self.token
51 }
52
53 #[must_use]
55 pub fn verified(&self) -> &VerifiedPass {
56 &self.verified
57 }
58
59 #[must_use]
61 pub fn holder(&self) -> &str {
62 &self.verified.claims().holder.id
63 }
64
65 #[must_use]
67 pub const fn warned(&self) -> &super::advisory::Warned {
68 &self.warned
69 }
70}
71
72#[derive(Clone, Debug, PartialEq, Eq)]
76pub enum LiveAnswer {
77 Outright {
79 grant: String,
81 },
82 Held {
84 mode: GrantMode,
86 grant: String,
88 draft: String,
90 },
91 Refused {
93 refusal: String,
95 },
96}
97
98pub trait LiveLysAsk: Send + Sync + std::fmt::Debug {
105 fn ask(&self, pass: &str, holder: &str, target: &Target, now: u64)
113 -> Result<LiveAnswer, Error>;
114}
115
116pub struct LysPassAdapter {
118 keys: KeySet,
119 issuer: String,
120 live: Option<Arc<dyn LiveLysAsk>>,
121 decisions: Decisions,
122}
123
124impl std::fmt::Debug for LysPassAdapter {
125 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
126 formatter
127 .debug_struct("LysPassAdapter")
128 .field("issuer", &self.issuer)
129 .field("live", &self.live.is_some())
130 .field("decisions", &self.decisions)
131 .finish_non_exhaustive()
132 }
133}
134
135impl LysPassAdapter {
136 #[must_use]
140 pub const fn new(keys: KeySet, issuer: String) -> Self {
141 Self {
142 keys,
143 issuer,
144 live: None,
145 decisions: Decisions::Enforce,
146 }
147 }
148
149 #[must_use]
152 pub const fn with_decisions(mut self, decisions: Decisions) -> Self {
153 self.decisions = decisions;
154 self
155 }
156
157 #[must_use]
159 pub const fn decisions(&self) -> Decisions {
160 self.decisions
161 }
162
163 pub fn from_keys_json(keys_json: &str, issuer: String) -> Result<Self, Error> {
168 Ok(Self::new(KeySet::from_json(keys_json)?, issuer))
169 }
170
171 #[must_use]
173 pub fn with_live(mut self, live: Arc<dyn LiveLysAsk>) -> Self {
174 self.live = Some(live);
175 self
176 }
177
178 #[must_use]
180 pub fn issuer(&self) -> &str {
181 &self.issuer
182 }
183
184 #[must_use]
186 pub fn asks_live(&self) -> bool {
187 self.live.is_some()
188 }
189
190 #[must_use]
192 pub fn request_url(&self) -> String {
193 format!("{}{REQUEST_PATH}", self.issuer.trim_end_matches('/'))
194 }
195
196 fn evaluate(
200 &self,
201 pass: &LysPass,
202 subject: &SubjectBinding,
203 action: AccessAction,
204 now: u64,
205 ) -> Result<GrantReceipt, AccessDecisionError> {
206 let resource = subject.resource();
207 match evaluate_grant(&pass.verified, resource, action, now, &self.request_url()) {
208 Err(error)
209 if error.cause == RefusalCause::MissingGrant
210 && own_station(pass, resource, action) =>
211 {
212 Ok(GrantReceipt {
213 grant: super::advisory::OWN_STATION_GRANT.to_owned(),
214 })
215 }
216 Err(error)
217 if matches!(
218 error.cause,
219 RefusalCause::MissingGrant | RefusalCause::HotMode
220 ) =>
221 {
222 super::advisory::advised_access(
223 self.decisions,
224 &pass.warned,
225 pass.holder(),
226 resource,
227 action,
228 &lacking(&error),
229 )
230 .ok_or(error)
231 }
232 decided => decided,
233 }
234 }
235}
236
237fn own_station(pass: &LysPass, resource: &AccessResource, action: AccessAction) -> bool {
243 let holder = &pass.verified.claims().holder;
244 action == AccessAction::Subscribe
245 && resource.kind == ResourceKind::Subject
246 && holder.kind == "agent"
247 && resource
248 .id
249 .strip_prefix("console.tap.")
250 .unwrap_or(&resource.id)
251 .strip_prefix("agents.")
252 .and_then(|rest| rest.strip_suffix(".inbox"))
253 == Some(holder.id.as_str())
254}
255
256fn lacking(error: &AccessDecisionError) -> String {
259 let needed = &error.refusal.refused.needed;
260 match error.cause {
261 RefusalCause::MissingGrant => format!("{}:{} {}", needed.kind, needed.id, needed.action),
262 cause => cause.to_string(),
263 }
264}
265
266fn evaluate_grant(
269 pass: &VerifiedPass,
270 resource: &AccessResource,
271 action: AccessAction,
272 now: u64,
273 request_url: &str,
274) -> Result<GrantReceipt, AccessDecisionError> {
275 let refused = |cause| {
276 crate::server::connection::services::plan_subject(&resource.id)
277 .filter(|_| resource.kind == ResourceKind::Subject && action == AccessAction::Publish)
278 .map_or_else(
279 || crate::agents::binding::refusal(resource, action, request_url, cause),
280 |name| AccessDecisionError {
281 cause,
282 refusal: AccessRefusal::for_target(
283 "liminal",
284 "liminal.service",
285 name,
286 "plan",
287 request_url.to_owned(),
288 ),
289 },
290 )
291 };
292 match crate::server::connection::services::evaluate_site_subject(pass, resource, action, now) {
293 Ok(Decision::Allowed { grant }) => Ok(GrantReceipt {
294 grant: grant.to_owned(),
295 }),
296 Ok(Decision::Held { .. }) => Err(refused(RefusalCause::HotMode)),
297 Ok(Decision::Refused) => Err(refused(RefusalCause::MissingGrant)),
298 Err(error) => Err(refused(cause_of(&error))),
299 }
300}
301
302#[must_use]
304pub const fn action_name(action: HotAction) -> &'static str {
305 match action {
306 HotAction::Publish => "publish",
307 HotAction::Subscribe => "subscribe",
308 HotAction::Call => "call",
309 HotAction::Serve => "serve",
310 }
311}
312
313#[must_use]
315pub const fn cause_of(error: &Error) -> RefusalCause {
316 match error {
317 Error::Expired | Error::NotYetValid { .. } => RefusalCause::Expired,
318 Error::WrongAudience | Error::WrongIssuer | Error::RightsOutsideAudience => {
319 RefusalCause::WrongAudience
320 }
321 _ => RefusalCause::Signature,
322 }
323}
324
325#[must_use]
328pub const fn live_cause_of(error: &Error) -> RefusalCause {
329 match error {
330 Error::Transport(_) | Error::CannotAsk(_) | Error::Url(_) => {
331 RefusalCause::DeliberateUnavailable
332 }
333 other => cause_of(other),
334 }
335}
336
337impl LysPassConsumer for LysPassAdapter {
338 type Pass = LysPass;
339 type Holder = str;
340 type AdmissionError = Error;
341
342 fn verify_offline(
343 &self,
344 credential: &[u8],
345 audience: &str,
346 now: u64,
347 ) -> Result<LysPass, Error> {
348 let token =
349 std::str::from_utf8(credential).map_err(|_| Error::Invalid("pass is not UTF-8"))?;
350 let token = token.trim();
351 let verified = VerifiedPass::verify(token, &self.keys, &self.issuer, audience, now)?;
352 Ok(LysPass {
353 verified: Arc::new(verified),
354 token: token.to_owned(),
355 warned: super::advisory::Warned::default(),
356 })
357 }
358
359 fn verified_holder<'p>(&self, pass: &'p LysPass) -> &'p str {
360 pass.holder()
361 }
362
363 fn verified_holder_kind<'p>(&self, pass: &'p LysPass) -> Option<&'p str> {
364 Some(pass.verified.claims().holder.kind.as_str())
365 }
366
367 fn verified_issued_at(&self, pass: &LysPass) -> Option<u64> {
368 Some(pass.verified.claims().iat)
369 }
370
371 fn decide_hot(
372 &self,
373 pass: &LysPass,
374 subject: &SubjectBinding,
375 action: HotAction,
376 now: u64,
377 ) -> Result<GrantReceipt, AccessDecisionError> {
378 self.evaluate(pass, subject, AccessAction::from(action), now)
379 }
380
381 fn decide_link(
382 &self,
383 pass: &LysPass,
384 subject: &SubjectBinding,
385 action: LinkAction,
386 now: u64,
387 ) -> Result<GrantReceipt, AccessDecisionError> {
388 self.evaluate(pass, subject, action.access_action(), now)
389 }
390}
391
392pub struct MachineLinkRights {
401 pass: LysPass,
402 request_url: String,
403}
404
405impl std::fmt::Debug for MachineLinkRights {
406 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
407 formatter
408 .debug_struct("MachineLinkRights")
409 .field("holder", &self.pass.holder())
410 .field("request_url", &self.request_url)
411 .finish_non_exhaustive()
412 }
413}
414
415impl MachineLinkRights {
416 #[must_use]
419 pub fn new(adapter: &LysPassAdapter, pass: LysPass) -> Self {
420 Self {
421 pass,
422 request_url: adapter.request_url(),
423 }
424 }
425
426 #[must_use]
428 pub fn holder(&self) -> &str {
429 self.pass.holder()
430 }
431
432 pub(crate) fn holds_resource(
433 &self,
434 action: LinkAction,
435 resource: &AccessResource,
436 now: u64,
437 ) -> Result<GrantReceipt, AccessDecisionError> {
438 evaluate_grant(
439 &self.pass.verified,
440 resource,
441 action.access_action(),
442 now,
443 &self.request_url,
444 )
445 }
446}
447
448impl crate::link::LinkRights for MachineLinkRights {
449 fn holds(
450 &self,
451 action: LinkAction,
452 subject: &SubjectBinding,
453 now: u64,
454 ) -> Result<GrantReceipt, AccessDecisionError> {
455 self.holds_resource(action, subject.resource(), now)
456 }
457}
458
459mod deliberate;