1use serde::{Deserialize, Serialize};
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
5#[serde(rename_all = "snake_case")]
6pub enum KeyClass {
7 Secret,
9 Publishable,
11}
12
13#[derive(Debug, Clone, Default, Serialize, Deserialize)]
15pub struct Limits {
16 #[serde(skip_serializing_if = "Option::is_none")]
18 pub max_connections: Option<u32>,
19 #[serde(skip_serializing_if = "Option::is_none")]
21 pub max_subscriptions: Option<u32>,
22 #[serde(skip_serializing_if = "Option::is_none")]
24 pub max_snapshot_rows: Option<u32>,
25 #[serde(skip_serializing_if = "Option::is_none")]
27 pub max_messages_per_minute: Option<u32>,
28 #[serde(skip_serializing_if = "Option::is_none")]
30 pub max_bytes_per_minute: Option<u64>,
31 #[serde(skip_serializing_if = "Option::is_none")]
33 pub max_http_requests_per_minute: Option<u32>,
34 #[serde(skip_serializing_if = "Option::is_none")]
36 pub max_http_batch_addresses: Option<u32>,
37 #[serde(skip_serializing_if = "Option::is_none")]
39 pub max_transaction_inspect_requests_per_minute: Option<u32>,
40 #[serde(skip_serializing_if = "Option::is_none")]
42 pub max_transaction_send_requests_per_minute: Option<u32>,
43 #[serde(skip_serializing_if = "Option::is_none")]
45 pub max_transaction_status_requests_per_minute: Option<u32>,
46 #[serde(skip_serializing_if = "Option::is_none")]
48 pub max_transaction_request_bytes: Option<u32>,
49 #[serde(skip_serializing_if = "Option::is_none")]
51 pub max_transaction_bytes: Option<u32>,
52 #[serde(skip_serializing_if = "Option::is_none")]
54 pub max_transaction_concurrency: Option<u32>,
55}
56
57#[derive(Debug, Clone, Serialize, Deserialize)]
59pub struct SessionClaims {
60 pub iss: String,
62 pub sub: String,
64 pub aud: String,
66 pub iat: u64,
68 pub nbf: u64,
70 pub exp: u64,
72 pub jti: String,
74 pub scope: String,
76 pub metering_key: String,
78 #[serde(skip_serializing_if = "Option::is_none")]
80 pub deployment_id: Option<String>,
81 #[serde(skip_serializing_if = "Option::is_none")]
83 pub origin: Option<String>,
84 #[serde(skip_serializing_if = "Option::is_none", rename = "client_ip")]
86 pub client_ip: Option<String>,
87 #[serde(skip_serializing_if = "Option::is_none")]
89 pub limits: Option<Limits>,
90 #[serde(skip_serializing_if = "Option::is_none")]
92 pub plan: Option<String>,
93 #[serde(rename = "key_class")]
95 pub key_class: KeyClass,
96}
97
98impl SessionClaims {
99 pub fn builder(
101 iss: impl Into<String>,
102 sub: impl Into<String>,
103 aud: impl Into<String>,
104 ) -> SessionClaimsBuilder {
105 SessionClaimsBuilder::new(iss, sub, aud)
106 }
107
108 pub fn is_expired(&self, now: u64) -> bool {
110 self.exp <= now
111 }
112
113 pub fn is_valid(&self, now: u64) -> bool {
115 self.nbf <= now && self.iat <= now
116 }
117}
118
119pub struct SessionClaimsBuilder {
121 iss: String,
122 sub: String,
123 aud: String,
124 iat: u64,
125 nbf: u64,
126 exp: u64,
127 jti: String,
128 scope: String,
129 metering_key: String,
130 deployment_id: Option<String>,
131 origin: Option<String>,
132 client_ip: Option<String>,
133 limits: Option<Limits>,
134 plan: Option<String>,
135 key_class: KeyClass,
136}
137
138impl SessionClaimsBuilder {
139 fn new(iss: impl Into<String>, sub: impl Into<String>, aud: impl Into<String>) -> Self {
140 use std::time::{SystemTime, UNIX_EPOCH};
141 let now = SystemTime::now()
142 .duration_since(UNIX_EPOCH)
143 .expect("time should not be before epoch")
144 .as_secs();
145
146 Self {
147 iss: iss.into(),
148 sub: sub.into(),
149 aud: aud.into(),
150 iat: now,
151 nbf: now,
152 exp: now + crate::DEFAULT_SESSION_TTL_SECONDS,
153 jti: uuid::Uuid::new_v4().to_string(),
154 scope: "read".to_string(),
155 metering_key: String::new(),
156 deployment_id: None,
157 origin: None,
158 client_ip: None,
159 limits: None,
160 plan: None,
161 key_class: KeyClass::Publishable,
162 }
163 }
164
165 pub fn with_ttl(mut self, ttl_seconds: u64) -> Self {
166 self.exp = self.iat + ttl_seconds;
167 self
168 }
169
170 pub fn with_scope(mut self, scope: impl Into<String>) -> Self {
171 self.scope = scope.into();
172 self
173 }
174
175 pub fn with_metering_key(mut self, key: impl Into<String>) -> Self {
176 self.metering_key = key.into();
177 self
178 }
179
180 pub fn with_deployment_id(mut self, id: impl Into<String>) -> Self {
181 self.deployment_id = Some(id.into());
182 self
183 }
184
185 pub fn with_origin(mut self, origin: impl Into<String>) -> Self {
186 self.origin = Some(origin.into());
187 self
188 }
189
190 pub fn with_client_ip(mut self, client_ip: impl Into<String>) -> Self {
191 self.client_ip = Some(client_ip.into());
192 self
193 }
194
195 pub fn with_limits(mut self, limits: Limits) -> Self {
196 self.limits = Some(limits);
197 self
198 }
199
200 pub fn with_plan(mut self, plan: impl Into<String>) -> Self {
201 self.plan = Some(plan.into());
202 self
203 }
204
205 pub fn with_key_class(mut self, key_class: KeyClass) -> Self {
206 self.key_class = key_class;
207 self
208 }
209
210 pub fn with_jti(mut self, jti: impl Into<String>) -> Self {
211 self.jti = jti.into();
212 self
213 }
214
215 pub fn build(self) -> SessionClaims {
216 SessionClaims {
217 iss: self.iss,
218 sub: self.sub,
219 aud: self.aud,
220 iat: self.iat,
221 nbf: self.nbf,
222 exp: self.exp,
223 jti: self.jti,
224 scope: self.scope,
225 metering_key: self.metering_key,
226 deployment_id: self.deployment_id,
227 origin: self.origin,
228 client_ip: self.client_ip,
229 limits: self.limits,
230 plan: self.plan,
231 key_class: self.key_class,
232 }
233 }
234}
235
236#[derive(Debug, Clone)]
238pub struct AuthContext {
239 pub subject: String,
241 pub issuer: String,
243 pub key_class: KeyClass,
245 pub metering_key: String,
247 pub deployment_id: Option<String>,
249 pub expires_at: u64,
251 pub scope: String,
253 pub limits: Limits,
255 pub plan: Option<String>,
257 pub origin: Option<String>,
259 pub client_ip: Option<String>,
261 pub jti: String,
263}
264
265impl AuthContext {
266 pub fn has_scope(&self, required: &str) -> bool {
268 self.scope.split_whitespace().any(|scope| scope == required)
269 }
270
271 pub fn from_claims(claims: SessionClaims) -> Self {
273 Self {
274 subject: claims.sub,
275 issuer: claims.iss,
276 key_class: claims.key_class,
277 metering_key: claims.metering_key,
278 deployment_id: claims.deployment_id,
279 expires_at: claims.exp,
280 scope: claims.scope,
281 limits: claims.limits.unwrap_or_default(),
282 plan: claims.plan,
283 origin: claims.origin,
284 client_ip: claims.client_ip,
285 jti: claims.jti,
286 }
287 }
288}
289
290#[cfg(test)]
291mod tests {
292 use super::*;
293
294 #[test]
295 fn scopes_are_exact_and_independent() {
296 let context = AuthContext::from_claims(
297 SessionClaims::builder("issuer", "subject", "audience")
298 .with_scope("read transaction:inspect transaction:send-extra")
299 .build(),
300 );
301
302 assert!(context.has_scope("read"));
303 assert!(context.has_scope("transaction:inspect"));
304 assert!(!context.has_scope("transaction:send"));
305 assert!(!context.has_scope("transaction"));
306 }
307
308 #[test]
309 fn old_limits_claims_remain_deserializable() {
310 let limits: Limits = serde_json::from_value(serde_json::json!({
311 "max_connections": 2
312 }))
313 .unwrap();
314
315 assert_eq!(limits.max_connections, Some(2));
316 assert_eq!(limits.max_transaction_bytes, None);
317 }
318
319 #[test]
320 fn transaction_limits_round_trip_additively() {
321 let limits = Limits {
322 max_transaction_inspect_requests_per_minute: Some(120),
323 max_transaction_send_requests_per_minute: Some(12),
324 max_transaction_status_requests_per_minute: Some(240),
325 max_transaction_request_bytes: Some(4096),
326 max_transaction_bytes: Some(1232),
327 max_transaction_concurrency: Some(4),
328 ..Limits::default()
329 };
330 let value = serde_json::to_value(&limits).unwrap();
331 let decoded: Limits = serde_json::from_value(value).unwrap();
332
333 assert_eq!(decoded.max_transaction_bytes, Some(1232));
334 assert_eq!(decoded.max_transaction_concurrency, Some(4));
335 }
336}