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    ///
101    /// # Errors
102    ///
103    /// Returns [`Error::Invalid`] when the kind, resource id or action is
104    /// empty or contains control characters.
105    pub fn new(kind: &str, id: &str, action: &str) -> Result<Self, Error> {
106        if [kind, id, action]
107            .iter()
108            .any(|value| value.is_empty() || value.chars().any(char::is_control))
109        {
110            return Err(Error::Invalid(
111                "permission target is empty or contains controls",
112            ));
113        }
114        Ok(Self {
115            kind: kind.to_owned(),
116            id: id.to_owned(),
117            action: action.to_owned(),
118        })
119    }
120}
121
122/// An offline answer retaining the exact granting receipt.
123#[derive(Debug, Clone, Copy, PartialEq, Eq)]
124pub enum Decision<'a> {
125    /// An outright grant permits the act.
126    Allowed {
127        /// The effective grant id.
128        grant: &'a str,
129    },
130    /// A grant exists but the act requires approval.
131    Held {
132        /// The effective grant id.
133        grant: &'a str,
134        /// The required approval mode.
135        mode: Mode,
136    },
137    /// No effective grant in this pass permits the act.
138    Refused,
139}
140
141pub(crate) fn in_prefix(kind: &str, prefix: &str) -> bool {
142    kind == prefix
143        || kind
144            .strip_prefix(prefix)
145            .is_some_and(|rest| rest.starts_with('.'))
146}
147
148pub(crate) fn audience_owns(kind: &str, audience: &str) -> bool {
149    kind.split_once('.')
150        .map_or(audience == "lys", |(owner, _)| owner == audience)
151}