1use super::{
14 AccessAction, AccessDecisionError, AccessRefusal, AccessResource, DeliberateDecision,
15 GrantMode, GrantReceipt, HotAction, LysDeliberateConsumer, LysPassConsumer, RefusalCause,
16 ResourceKind, SubjectBinding,
17};
18use crate::link::LinkAction;
19use lys_pass::{Decision, Error, KeySet, Target, VerifiedPass};
20use std::sync::Arc;
21
22pub const REQUEST_PATH: &str = "/#/requests";
24
25pub struct LysPass {
29 verified: Arc<VerifiedPass>,
30 token: String,
31}
32
33impl std::fmt::Debug for LysPass {
34 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
35 formatter
36 .debug_struct("LysPass")
37 .field("holder", &self.holder())
38 .finish_non_exhaustive()
39 }
40}
41
42impl LysPass {
43 pub(super) fn shared_verified(&self) -> Arc<VerifiedPass> {
44 self.verified.clone()
45 }
46 pub(super) fn raw_token(&self) -> &str {
47 &self.token
48 }
49
50 #[must_use]
52 pub fn verified(&self) -> &VerifiedPass {
53 &self.verified
54 }
55
56 #[must_use]
58 pub fn holder(&self) -> &str {
59 &self.verified.claims().holder.id
60 }
61}
62
63#[derive(Clone, Debug, PartialEq, Eq)]
67pub enum LiveAnswer {
68 Outright {
70 grant: String,
72 },
73 Held {
75 mode: GrantMode,
77 grant: String,
79 draft: String,
81 },
82 Refused {
84 refusal: String,
86 },
87}
88
89pub trait LiveLysAsk: Send + Sync + std::fmt::Debug {
96 fn ask(&self, pass: &str, holder: &str, target: &Target, now: u64)
104 -> Result<LiveAnswer, Error>;
105}
106
107pub struct LysPassAdapter {
109 keys: KeySet,
110 issuer: String,
111 live: Option<Arc<dyn LiveLysAsk>>,
112}
113
114impl std::fmt::Debug for LysPassAdapter {
115 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
116 formatter
117 .debug_struct("LysPassAdapter")
118 .field("issuer", &self.issuer)
119 .field("live", &self.live.is_some())
120 .finish_non_exhaustive()
121 }
122}
123
124impl LysPassAdapter {
125 #[must_use]
129 pub const fn new(keys: KeySet, issuer: String) -> Self {
130 Self {
131 keys,
132 issuer,
133 live: None,
134 }
135 }
136
137 pub fn from_keys_json(keys_json: &str, issuer: String) -> Result<Self, Error> {
142 Ok(Self::new(KeySet::from_json(keys_json)?, issuer))
143 }
144
145 #[must_use]
147 pub fn with_live(mut self, live: Arc<dyn LiveLysAsk>) -> Self {
148 self.live = Some(live);
149 self
150 }
151
152 #[must_use]
154 pub fn issuer(&self) -> &str {
155 &self.issuer
156 }
157
158 #[must_use]
160 pub fn asks_live(&self) -> bool {
161 self.live.is_some()
162 }
163
164 #[must_use]
166 pub fn request_url(&self) -> String {
167 format!("{}{REQUEST_PATH}", self.issuer.trim_end_matches('/'))
168 }
169
170 fn evaluate(
173 &self,
174 pass: &VerifiedPass,
175 subject: &SubjectBinding,
176 action: AccessAction,
177 now: u64,
178 ) -> Result<GrantReceipt, AccessDecisionError> {
179 evaluate_grant(pass, subject.resource(), action, now, &self.request_url())
180 }
181}
182
183fn evaluate_grant(
186 pass: &VerifiedPass,
187 resource: &AccessResource,
188 action: AccessAction,
189 now: u64,
190 request_url: &str,
191) -> Result<GrantReceipt, AccessDecisionError> {
192 let refused = |cause| {
193 crate::server::connection::services::plan_subject(&resource.id)
194 .filter(|_| resource.kind == ResourceKind::Subject && action == AccessAction::Publish)
195 .map_or_else(
196 || crate::agents::binding::refusal(resource, action, request_url, cause),
197 |name| AccessDecisionError {
198 cause,
199 refusal: AccessRefusal::for_target(
200 "liminal",
201 "liminal.service",
202 name,
203 "plan",
204 request_url.to_owned(),
205 ),
206 },
207 )
208 };
209 match crate::server::connection::services::evaluate_site_subject(pass, resource, action, now) {
210 Ok(Decision::Allowed { grant }) => Ok(GrantReceipt {
211 grant: grant.to_owned(),
212 }),
213 Ok(Decision::Held { .. }) => Err(refused(RefusalCause::HotMode)),
214 Ok(Decision::Refused) => Err(refused(RefusalCause::MissingGrant)),
215 Err(error) => Err(refused(cause_of(&error))),
216 }
217}
218
219#[must_use]
221pub const fn action_name(action: HotAction) -> &'static str {
222 match action {
223 HotAction::Publish => "publish",
224 HotAction::Subscribe => "subscribe",
225 HotAction::Call => "call",
226 HotAction::Serve => "serve",
227 }
228}
229
230#[must_use]
232pub const fn cause_of(error: &Error) -> RefusalCause {
233 match error {
234 Error::Expired | Error::NotYetValid { .. } => RefusalCause::Expired,
235 Error::WrongAudience | Error::WrongIssuer | Error::RightsOutsideAudience => {
236 RefusalCause::WrongAudience
237 }
238 _ => RefusalCause::Signature,
239 }
240}
241
242#[must_use]
245pub const fn live_cause_of(error: &Error) -> RefusalCause {
246 match error {
247 Error::Transport(_) | Error::CannotAsk(_) | Error::Url(_) => {
248 RefusalCause::DeliberateUnavailable
249 }
250 other => cause_of(other),
251 }
252}
253
254impl LysPassConsumer for LysPassAdapter {
255 type Pass = LysPass;
256 type Holder = str;
257 type AdmissionError = Error;
258
259 fn verify_offline(
260 &self,
261 credential: &[u8],
262 audience: &str,
263 now: u64,
264 ) -> Result<LysPass, Error> {
265 let token =
266 std::str::from_utf8(credential).map_err(|_| Error::Invalid("pass is not UTF-8"))?;
267 let token = token.trim();
268 let verified = VerifiedPass::verify(token, &self.keys, &self.issuer, audience, now)?;
269 Ok(LysPass {
270 verified: Arc::new(verified),
271 token: token.to_owned(),
272 })
273 }
274
275 fn verified_holder<'p>(&self, pass: &'p LysPass) -> &'p str {
276 pass.holder()
277 }
278
279 fn verified_holder_kind<'p>(&self, pass: &'p LysPass) -> Option<&'p str> {
280 Some(pass.verified.claims().holder.kind.as_str())
281 }
282
283 fn verified_issued_at(&self, pass: &LysPass) -> Option<u64> {
284 Some(pass.verified.claims().iat)
285 }
286
287 fn decide_hot(
288 &self,
289 pass: &LysPass,
290 subject: &SubjectBinding,
291 action: HotAction,
292 now: u64,
293 ) -> Result<GrantReceipt, AccessDecisionError> {
294 self.evaluate(&pass.verified, subject, AccessAction::from(action), now)
295 }
296
297 fn decide_link(
298 &self,
299 pass: &LysPass,
300 subject: &SubjectBinding,
301 action: LinkAction,
302 now: u64,
303 ) -> Result<GrantReceipt, AccessDecisionError> {
304 self.evaluate(&pass.verified, subject, action.access_action(), now)
305 }
306}
307
308impl LysDeliberateConsumer for LysPassAdapter {
309 fn hold_permission(
310 &self,
311 pass: &LysPass,
312 request: super::prepared::PermissionRequest<'_>,
313 ) -> Result<serde_json::Value, Error> {
314 request.execute(pass)
315 }
316
317 fn hold_site(
318 &self,
319 pass: &LysPass,
320 request: super::prepared::SiteRequest<'_>,
321 ) -> Result<serde_json::Value, Error> {
322 request.execute(pass)
323 }
324
325 fn decide_live(
326 &self,
327 pass: &LysPass,
328 resource: &AccessResource,
329 action: AccessAction,
330 now: u64,
331 ) -> Result<DeliberateDecision, AccessDecisionError> {
332 let refused = |cause| AccessDecisionError {
333 cause,
334 refusal: AccessRefusal::new(resource.clone(), action, self.request_url()),
335 };
336 let Some(live) = self.live.as_ref() else {
337 return Err(refused(RefusalCause::DeliberateUnavailable));
338 };
339 if pass.verified.claims().exp <= now {
342 return Err(refused(RefusalCause::Expired));
343 }
344 let target = Target::new(resource.kind.qualified(), &resource.id, action.name())
345 .map_err(|error| refused(cause_of(&error)))?;
346 match live.ask(&pass.token, pass.holder(), &target, now) {
347 Ok(LiveAnswer::Outright { grant }) => {
348 Ok(DeliberateDecision::Outright(GrantReceipt { grant }))
349 }
350 Ok(LiveAnswer::Held {
351 mode: GrantMode::ByDraft,
352 grant,
353 draft,
354 }) => Ok(DeliberateDecision::ByDraft { grant, draft }),
355 Ok(LiveAnswer::Held {
356 mode: GrantMode::ByTwo,
357 grant,
358 draft,
359 }) => Ok(DeliberateDecision::ByTwo { grant, draft }),
360 Ok(LiveAnswer::Held {
363 mode: GrantMode::Outright,
364 ..
365 }) => Err(refused(RefusalCause::DeliberateUnavailable)),
366 Ok(LiveAnswer::Refused { .. }) => Err(refused(RefusalCause::MissingGrant)),
367 Err(error) => {
368 tracing::warn!(resource = %resource.id, action = action.name(), %error, "a live Lys decision could not be made");
370 Err(refused(live_cause_of(&error)))
371 }
372 }
373 }
374}
375
376pub struct MachineLinkRights {
385 pass: LysPass,
386 request_url: String,
387}
388
389impl std::fmt::Debug for MachineLinkRights {
390 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
391 formatter
392 .debug_struct("MachineLinkRights")
393 .field("holder", &self.pass.holder())
394 .field("request_url", &self.request_url)
395 .finish_non_exhaustive()
396 }
397}
398
399impl MachineLinkRights {
400 #[must_use]
403 pub fn new(adapter: &LysPassAdapter, pass: LysPass) -> Self {
404 Self {
405 pass,
406 request_url: adapter.request_url(),
407 }
408 }
409
410 #[must_use]
412 pub fn holder(&self) -> &str {
413 self.pass.holder()
414 }
415
416 pub(crate) fn holds_resource(
417 &self,
418 action: LinkAction,
419 resource: &AccessResource,
420 now: u64,
421 ) -> Result<GrantReceipt, AccessDecisionError> {
422 evaluate_grant(
423 &self.pass.verified,
424 resource,
425 action.access_action(),
426 now,
427 &self.request_url,
428 )
429 }
430}
431
432impl crate::link::LinkRights for MachineLinkRights {
433 fn holds(
434 &self,
435 action: LinkAction,
436 subject: &SubjectBinding,
437 now: u64,
438 ) -> Result<GrantReceipt, AccessDecisionError> {
439 self.holds_resource(action, subject.resource(), now)
440 }
441}