1use std::collections::BTreeSet;
2
3use serde::{Deserialize, Serialize};
4
5use crate::{Error, Result};
6
7pub const MAX_LABEL_LEN: usize = 256;
9pub const MAX_CAPABILITIES: usize = 64;
10
11pub(crate) fn valid_label(value: &str) -> bool {
12 !value.is_empty()
13 && value.len() <= MAX_LABEL_LEN
14 && !value.chars().any(char::is_control)
15}
16
17macro_rules! label {
18 ($name:ident, $doc:literal) => {
19 #[doc = $doc]
20 #[derive(
21 Clone,
22 Debug,
23 PartialEq,
24 Eq,
25 PartialOrd,
26 Ord,
27 Hash,
28 Serialize,
29 Deserialize,
30 )]
31 #[serde(try_from = "String", into = "String")]
32 pub struct $name(String);
33
34 impl $name {
35 pub fn new(value: impl Into<String>) -> Result<Self> {
36 let value = value.into();
37 if !valid_label(&value) {
38 return Err(Error::InvalidInput);
39 }
40 Ok(Self(value))
41 }
42
43 pub fn as_str(&self) -> &str {
44 &self.0
45 }
46 }
47
48 impl TryFrom<String> for $name {
49 type Error = Error;
50 fn try_from(value: String) -> Result<Self> {
51 Self::new(value)
52 }
53 }
54
55 impl From<$name> for String {
56 fn from(value: $name) -> Self {
57 value.0
58 }
59 }
60 };
61}
62
63label!(Audience, "Exact logical recipient; independent of DNS and transport.");
64label!(
65 Capability,
66 "Exact application-defined permission; no implicit wildcards or hierarchy."
67);
68
69#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
71#[serde(rename_all = "snake_case")]
72pub enum Purpose {
73 Session,
74 Refresh,
75 Access,
76 WebSocket,
77 Internal,
78}
79
80impl Purpose {
81 pub(crate) fn as_bytes(self) -> &'static [u8] {
82 match self {
83 Self::Session => b"session",
84 Self::Refresh => b"refresh",
85 Self::Access => b"access",
86 Self::WebSocket => b"websocket",
87 Self::Internal => b"internal",
88 }
89 }
90}
91
92#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
94pub struct TokenId(pub [u8; 16]);
95
96#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
98#[serde(deny_unknown_fields)]
99pub struct Claims {
100 pub subject: String,
101 pub audience: Audience,
102 pub purpose: Purpose,
103 pub capabilities: BTreeSet<Capability>,
104 pub issued_at: u64,
105 pub not_before: u64,
106 pub expires_at: u64,
107}
108
109impl Claims {
110 pub(crate) fn check_shape(&self) -> Result<()> {
111 if !valid_label(&self.subject)
112 || self.capabilities.len() > MAX_CAPABILITIES
113 || self.issued_at > self.not_before
114 || self.not_before >= self.expires_at
115 {
116 return Err(Error::InvalidInput);
117 }
118 Ok(())
119 }
120}
121
122#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
124#[serde(deny_unknown_fields)]
125pub struct Principal {
126 pub(crate) realm: String,
127 pub(crate) issuer: String,
128 pub(crate) jti: TokenId,
129 #[serde(default, skip_serializing_if = "Option::is_none")]
130 pub(crate) session: Option<crate::fleet::SessionBinding>,
131 pub(crate) claims: Claims,
132}
133
134impl Principal {
135 pub fn session_id(&self) -> Option<crate::SessionId> {
136 self.session.as_ref().map(|session| session.id)
137 }
138 pub fn realm(&self) -> &str {
139 &self.realm
140 }
141 pub fn issuer(&self) -> &str {
142 &self.issuer
143 }
144 pub fn jti(&self) -> TokenId {
145 self.jti
146 }
147 pub fn subject(&self) -> &str {
148 &self.claims.subject
149 }
150 pub fn claims(&self) -> &Claims {
151 &self.claims
152 }
153 pub fn has_capability(
154 &self,
155 capability: &Capability,
156 ) -> bool {
157 self.claims.capabilities.contains(capability)
158 }
159}
160
161#[derive(Clone, Debug)]
163pub struct Validation {
164 pub audience: Audience,
165 pub purpose: Purpose,
166 pub required_capabilities: BTreeSet<Capability>,
167}
168
169pub trait ValidationHook {
175 fn check(
176 &self,
177 principal: &Principal,
178 now: u64,
179 ) -> Result<()>;
180
181 fn check_lapsed(
184 &self,
185 _: &Principal,
186 _: u64,
187 ) -> Result<()> {
188 Err(Error::Expired)
189 }
190}
191
192pub struct AllowReusable;
194
195impl ValidationHook for AllowReusable {
196 fn check(
197 &self,
198 _: &Principal,
199 _: u64,
200 ) -> Result<()> {
201 Ok(())
202 }
203}