Skip to main content

lys_pass/
rights.rs

1//! Rights name resources already reached by the issuer, never a local graph.
2
3use serde::{Deserialize, Serialize};
4
5use crate::Error;
6
7/// How a grant may be exercised.
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
9#[serde(rename_all = "snake_case")]
10#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema), schema(as = PassMode))]
11pub enum Mode {
12    /// The product may act immediately.
13    Outright,
14    /// One authorized approval is required.
15    ByDraft,
16    /// Two distinct authorized approvals are required.
17    ByTwo,
18}
19
20/// The signed holder and the person responsible for a non-person.
21#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
22#[serde(deny_unknown_fields)]
23pub struct Holder {
24    /// The directory identity id.
25    pub id: String,
26    /// The directory identity kind.
27    pub kind: String,
28    /// The responsible person's identity, required for a non-person.
29    pub responsible: Option<String>,
30}
31
32/// One explicitly reached resource.
33#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
34#[serde(deny_unknown_fields)]
35#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema), schema(as = PassResource))]
36pub struct Resource {
37    /// The application's qualified kind.
38    pub kind: String,
39    /// The exact placed resource id.
40    pub id: String,
41}
42
43/// A grant's effective actions on an explicitly reached resource.
44#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
45#[serde(deny_unknown_fields)]
46pub struct Right {
47    /// The resource after issuer-side reach expansion.
48    pub resource: Resource,
49    /// The effective actions after issuer-side role expansion.
50    pub actions: Vec<String>,
51    /// The required approval mode.
52    pub mode: Mode,
53    /// The authority receipt a product records with an act.
54    pub grant: String,
55}
56
57/// The access pass's signed claims.
58#[derive(Debug, Clone, Serialize, Deserialize)]
59pub struct Claims {
60    /// The trusted Lys issuer.
61    pub iss: String,
62    /// The holder's identity, equal to the holder id.
63    pub sub: String,
64    /// The one application audience.
65    pub aud: String,
66    /// The issue instant in Unix seconds.
67    pub iat: u64,
68    /// The first invalid instant in Unix seconds.
69    pub exp: u64,
70    /// An optional first valid instant.
71    #[serde(skip_serializing_if = "Option::is_none")]
72    pub nbf: Option<u64>,
73    /// The holder and responsibility assertion.
74    pub holder: Holder,
75    /// Base64url SHA-256 digests of the agent's non-withdrawn Ed25519 certificate keys.
76    /// An older pass or a non-agent holder carries no key assertion.
77    #[serde(default, skip_serializing_if = "Vec::is_empty")]
78    pub agent_key_thumbprints: Vec<String>,
79    /// Effective rights, already expanded by Lys.
80    pub rights: Vec<Right>,
81    /// The audience whose rights could not fit; no partial offline decision.
82    #[serde(skip_serializing_if = "Option::is_none")]
83    pub rights_truncated: Option<String>,
84}
85
86/// A product's exact permission question.
87#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
88#[serde(deny_unknown_fields)]
89pub struct Target {
90    /// The resource kind.
91    pub kind: String,
92    /// The resource id.
93    pub id: String,
94    /// The action requested.
95    pub action: String,
96}
97
98impl Target {
99    /// Validate the boundary without adding a length limit or default.
100    pub fn new(kind: &str, id: &str, action: &str) -> Result<Self, Error> {
101        if [kind, id, action]
102            .iter()
103            .any(|value| value.is_empty() || value.chars().any(char::is_control))
104        {
105            return Err(Error::Invalid(
106                "permission target is empty or contains controls",
107            ));
108        }
109        Ok(Self {
110            kind: kind.to_owned(),
111            id: id.to_owned(),
112            action: action.to_owned(),
113        })
114    }
115}
116
117/// An offline answer retaining the exact granting receipt.
118#[derive(Debug, Clone, Copy, PartialEq, Eq)]
119pub enum Decision<'a> {
120    /// An outright grant permits the act.
121    Allowed {
122        /// The effective grant id.
123        grant: &'a str,
124    },
125    /// A grant exists but the act requires approval.
126    Held {
127        /// The effective grant id.
128        grant: &'a str,
129        /// The required approval mode.
130        mode: Mode,
131    },
132    /// No effective grant in this pass permits the act.
133    Refused,
134}
135
136pub(crate) fn in_prefix(kind: &str, prefix: &str) -> bool {
137    kind == prefix
138        || kind
139            .strip_prefix(prefix)
140            .is_some_and(|rest| rest.starts_with('.'))
141}
142
143pub(crate) fn audience_owns(kind: &str, audience: &str) -> bool {
144    kind.split_once('.')
145        .map_or(audience == "lys", |(owner, _)| owner == audience)
146}