1use std::{
16 collections::HashMap,
17 env,
18 fmt::Debug,
19 ops::Add,
20 time::{Duration, SystemTime, UNIX_EPOCH},
21};
22
23use jsonwebtoken::{self, DecodingKey, EncodingKey, Header};
24use serde::{Deserialize, Serialize};
25use thiserror::Error;
26
27use crate::get_env_keys;
28
29pub const DEFAULT_TTL: Duration = Duration::from_secs(3600 * 6); #[derive(Debug, Error)]
32pub enum AccessTokenError {
33 #[error("Invalid API Key or Secret Key")]
34 InvalidKeys,
35 #[error("Invalid environment")]
36 InvalidEnv(#[from] env::VarError),
37 #[error("invalid claims: {0}")]
38 InvalidClaims(&'static str),
39 #[error("failed to encode jwt")]
40 Encoding(#[from] jsonwebtoken::errors::Error),
41}
42
43#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
44#[serde(default, rename_all = "camelCase")]
45pub struct VideoGrants {
46 #[serde(skip_serializing_if = "is_default")]
48 pub room_create: bool,
49 #[serde(skip_serializing_if = "is_default")]
50 pub room_list: bool,
51 #[serde(skip_serializing_if = "is_default")]
52 pub room_record: bool,
53
54 #[serde(skip_serializing_if = "is_default")]
56 pub room_admin: bool,
57 #[serde(skip_serializing_if = "is_default")]
58 pub room_join: bool,
59 #[serde(skip_serializing_if = "is_default")]
60 pub room: String,
61 #[serde(skip_serializing_if = "is_default")]
62 pub destination_room: String,
63
64 #[serde(skip_serializing_if = "is_default")]
66 pub can_publish: Option<bool>,
67 #[serde(skip_serializing_if = "is_default")]
68 pub can_subscribe: Option<bool>,
69 #[serde(skip_serializing_if = "is_default")]
70 pub can_publish_data: Option<bool>,
71
72 #[serde(skip_serializing_if = "is_default")]
75 pub can_publish_sources: Vec<String>, #[serde(skip_serializing_if = "is_default")]
79 pub can_update_own_metadata: Option<bool>,
80
81 #[serde(skip_serializing_if = "is_default")]
83 pub ingress_admin: bool, #[serde(skip_serializing_if = "is_default")]
87 pub hidden: bool,
88
89 #[serde(skip_serializing_if = "is_default")]
91 pub recorder: bool,
92
93 #[serde(skip_serializing_if = "is_default")]
95 pub agent: bool,
96}
97
98fn is_default<T: Default + PartialEq>(v: &T) -> bool {
99 *v == T::default()
100}
101
102impl VideoGrants {
103 pub fn can_publish(&self) -> bool {
104 self.can_publish.unwrap_or(true)
105 }
106
107 pub fn can_subscribe(&self) -> bool {
108 self.can_subscribe.unwrap_or(true)
109 }
110
111 pub fn can_publish_data(&self) -> bool {
112 self.can_publish_data.unwrap_or_else(|| self.can_publish())
113 }
114
115 pub fn can_update_own_metadata(&self) -> bool {
116 self.can_update_own_metadata.unwrap_or(false)
117 }
118}
119
120#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
121#[serde(rename_all = "camelCase")]
122pub struct SIPGrants {
123 #[serde(default, skip_serializing_if = "is_default")]
125 pub admin: bool,
126 #[serde(default, skip_serializing_if = "is_default")]
128 pub call: bool,
129}
130
131impl Default for SIPGrants {
132 fn default() -> Self {
133 Self { admin: false, call: false }
134 }
135}
136
137#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
138#[serde(default, rename_all = "camelCase")]
139pub struct AgentGrants {
140 #[serde(skip_serializing_if = "is_default")]
142 pub admin: bool,
143 #[serde(skip_serializing_if = "is_default")]
145 pub simulation_admin: bool,
146 #[serde(skip_serializing_if = "is_default")]
148 pub dispatch_admin: bool,
149}
150
151#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
152#[serde(default, rename_all = "camelCase")]
153pub struct AgentEndpointGrants {
154 #[serde(skip_serializing_if = "is_default")]
156 pub call: bool,
157 #[serde(skip_serializing_if = "is_default")]
159 pub agent_name: String,
160 #[serde(skip_serializing_if = "is_default")]
162 pub deployment: String,
163}
164
165impl AgentEndpointGrants {
166 pub fn allows(&self, agent_name: &str, deployment: &str) -> bool {
167 self.call
168 && (self.agent_name.is_empty() || self.agent_name == agent_name)
169 && (self.deployment.is_empty() || self.deployment == deployment)
170 }
171}
172
173#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
174#[serde(default, rename_all = "camelCase")]
175pub struct AgentThreadGrants {
176 #[serde(skip_serializing_if = "is_default")]
177 pub all: bool,
178 #[serde(skip_serializing_if = "is_default")]
179 pub scope: String,
180 #[serde(skip_serializing_if = "is_default")]
181 pub thread_ids: Vec<String>,
182 #[serde(skip_serializing_if = "is_default")]
183 pub list: bool,
184 #[serde(skip_serializing_if = "is_default")]
185 pub create: bool,
186 #[serde(skip_serializing_if = "is_default")]
187 pub write: bool,
188 #[serde(skip_serializing_if = "is_default")]
189 pub read: bool,
190 #[serde(skip_serializing_if = "is_default")]
191 pub delete: bool,
192 #[serde(skip_serializing_if = "is_default")]
194 pub sql: bool,
195}
196
197impl AgentThreadGrants {
198 pub fn can_create(&self, scope: &str) -> bool {
199 self.create && self.in_scope(scope)
200 }
201
202 pub fn can_list(&self, scope: &str) -> bool {
203 self.list && self.in_scope(scope)
204 }
205
206 pub fn can_update(&self) -> bool {
207 self.all && self.write
208 }
209
210 pub fn can_read(&self, thread_id: &str) -> bool {
211 self.read && self.includes(thread_id)
212 }
213
214 pub fn can_write(&self, thread_id: &str) -> bool {
215 self.write && self.includes(thread_id)
216 }
217
218 pub fn can_delete(&self, thread_id: &str) -> bool {
219 self.delete && self.includes(thread_id)
220 }
221
222 pub fn can_sql(&self, thread_id: &str) -> bool {
223 self.sql && self.includes(thread_id)
224 }
225
226 fn in_scope(&self, scope: &str) -> bool {
227 self.all || (!self.scope.is_empty() && self.scope == scope)
228 }
229
230 fn includes(&self, thread_id: &str) -> bool {
231 !thread_id.is_empty() && (self.all || self.thread_ids.iter().any(|id| id == thread_id))
232 }
233}
234
235#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
236#[serde(default, rename_all = "camelCase")]
237pub struct AgentDatabaseGrants {
238 #[serde(skip_serializing_if = "is_default")]
240 pub database_ids: Vec<String>,
241 #[serde(skip_serializing_if = "is_default")]
242 pub read: bool,
243 #[serde(skip_serializing_if = "is_default")]
244 pub write: bool,
245 #[serde(skip_serializing_if = "is_default")]
246 pub delete: bool,
247 #[serde(skip_serializing_if = "is_default")]
249 pub create: bool,
250 #[serde(skip_serializing_if = "is_default")]
252 pub list: bool,
253}
254
255impl AgentDatabaseGrants {
256 pub fn can_read(&self, database_id: &str) -> bool {
257 self.read && self.includes(database_id)
258 }
259
260 pub fn can_write(&self, database_id: &str) -> bool {
261 self.write && self.includes(database_id)
262 }
263
264 pub fn can_delete(&self, database_id: &str) -> bool {
265 self.delete && self.includes(database_id)
266 }
267
268 pub fn can_create(&self) -> bool {
269 self.create
270 }
271
272 pub fn can_list(&self) -> bool {
273 self.list
274 }
275
276 fn includes(&self, database_id: &str) -> bool {
277 !database_id.is_empty() && self.database_ids.iter().any(|id| id == database_id)
278 }
279}
280
281#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
283#[serde(default, rename_all = "camelCase")]
284#[non_exhaustive]
285pub struct InferenceGrants {
286 #[serde(skip_serializing_if = "is_default")]
288 pub perform: bool,
289}
290
291impl InferenceGrants {
292 pub fn with_perform(mut self) -> Self {
294 self.perform = true;
295 self
296 }
297}
298
299#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
301#[serde(default, rename_all = "camelCase")]
302#[non_exhaustive]
303pub struct ObservabilityGrants {
304 #[serde(skip_serializing_if = "is_default")]
306 pub write: bool,
307}
308
309impl ObservabilityGrants {
310 pub fn with_write(mut self) -> Self {
312 self.write = true;
313 self
314 }
315}
316
317#[derive(Debug, Clone, Serialize, Default, Deserialize, PartialEq)]
318#[serde(default)]
319#[serde(rename_all = "camelCase")]
320pub struct Claims {
321 pub exp: usize, pub iss: String, pub nbf: usize,
324 #[serde(skip_serializing_if = "is_default")]
325 pub sub: String, #[serde(skip_serializing_if = "is_default")]
328 pub name: String,
329 #[serde(skip_serializing_if = "is_default")]
330 pub kind: String,
331 #[serde(skip_serializing_if = "is_default")]
332 pub video: VideoGrants,
333 #[serde(skip_serializing_if = "is_default")]
334 pub sip: SIPGrants,
335 #[serde(skip_serializing_if = "is_default")]
336 pub agent: AgentGrants,
337 #[serde(skip_serializing_if = "is_default")]
338 pub agent_endpoint: AgentEndpointGrants,
339 #[serde(skip_serializing_if = "is_default")]
340 pub agent_thread: AgentThreadGrants,
341 #[serde(skip_serializing_if = "is_default")]
342 pub agent_database: AgentDatabaseGrants,
343 #[serde(skip_serializing_if = "is_default")]
344 pub inference: InferenceGrants,
345 #[serde(skip_serializing_if = "is_default")]
346 pub observability: ObservabilityGrants,
347 #[serde(skip_serializing_if = "is_default")]
348 pub sha256: String, #[serde(skip_serializing_if = "is_default")]
350 pub metadata: String,
351 #[serde(skip_serializing_if = "is_default")]
352 pub attributes: HashMap<String, String>,
353 #[serde(skip_serializing_if = "is_default")]
354 pub room_config: Option<livekit_protocol::RoomConfiguration>,
355}
356
357impl Claims {
358 pub fn from_unverified(token: &str) -> Result<Self, AccessTokenError> {
359 crate::jwt_provider::ensure_installed();
360 let token = jsonwebtoken::dangerous::insecure_decode::<Claims>(token)?;
361 Ok(token.claims)
362 }
363}
364
365#[derive(Clone)]
366pub struct AccessToken {
367 api_key: String,
368 api_secret: String,
369 claims: Claims,
370}
371
372impl Debug for AccessToken {
373 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
374 f.debug_struct("AccessToken")
376 .field("api_key", &self.api_key)
377 .field("claims", &self.claims)
378 .finish()
379 }
380}
381
382impl AccessToken {
383 pub fn with_api_key(api_key: &str, api_secret: &str) -> Self {
384 let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap();
385 Self {
386 api_key: api_key.to_owned(),
387 api_secret: api_secret.to_owned(),
388 claims: Claims {
389 exp: now.add(DEFAULT_TTL).as_secs() as usize,
390 iss: api_key.to_owned(),
391 nbf: now.as_secs() as usize,
392 sub: Default::default(),
393 name: Default::default(),
394 kind: Default::default(),
395 video: VideoGrants::default(),
396 sip: SIPGrants::default(),
397 agent: AgentGrants::default(),
398 agent_endpoint: AgentEndpointGrants::default(),
399 agent_thread: AgentThreadGrants::default(),
400 agent_database: AgentDatabaseGrants::default(),
401 inference: InferenceGrants::default(),
402 observability: ObservabilityGrants::default(),
403 sha256: Default::default(),
404 metadata: Default::default(),
405 attributes: HashMap::new(),
406 room_config: Default::default(),
407 },
408 }
409 }
410
411 #[cfg(test)]
412 pub fn from_parts(api_key: &str, api_secret: &str, claims: Claims) -> Self {
413 Self { api_key: api_key.to_owned(), api_secret: api_secret.to_owned(), claims }
414 }
415
416 pub fn new() -> Result<Self, AccessTokenError> {
417 let (api_key, api_secret) = get_env_keys()?;
419 Ok(Self::with_api_key(&api_key, &api_secret))
420 }
421
422 pub fn with_ttl(mut self, ttl: Duration) -> Self {
423 let time = SystemTime::now().duration_since(UNIX_EPOCH).unwrap() + ttl;
424 self.claims.exp = time.as_secs() as usize;
425 self
426 }
427
428 pub fn with_grants(mut self, grants: VideoGrants) -> Self {
429 self.claims.video = grants;
430 self
431 }
432
433 pub fn with_sip_grants(mut self, grants: SIPGrants) -> Self {
434 self.claims.sip = grants;
435 self
436 }
437
438 pub fn with_agent_grants(mut self, grants: AgentGrants) -> Self {
439 self.claims.agent = grants;
440 self
441 }
442
443 pub fn with_agent_endpoint_grants(mut self, grants: AgentEndpointGrants) -> Self {
444 self.claims.agent_endpoint = grants;
445 self
446 }
447
448 pub fn with_agent_thread_grants(mut self, grants: AgentThreadGrants) -> Self {
449 self.claims.agent_thread = grants;
450 self
451 }
452
453 pub fn with_agent_database_grants(mut self, grants: AgentDatabaseGrants) -> Self {
454 self.claims.agent_database = grants;
455 self
456 }
457
458 pub fn with_inference_grants(mut self, grants: InferenceGrants) -> Self {
459 self.claims.inference = grants;
460 self
461 }
462
463 pub fn with_observability_grants(mut self, grants: ObservabilityGrants) -> Self {
465 self.claims.observability = grants;
466 self
467 }
468
469 pub fn with_identity(mut self, identity: &str) -> Self {
470 self.claims.sub = identity.to_owned();
471 self
472 }
473
474 pub fn with_name(mut self, name: &str) -> Self {
475 self.claims.name = name.to_owned();
476 self
477 }
478
479 pub fn with_kind(mut self, kind: &str) -> Self {
480 self.claims.kind = kind.to_owned();
481 self
482 }
483
484 pub fn with_metadata(mut self, metadata: &str) -> Self {
485 self.claims.metadata = metadata.to_owned();
486 self
487 }
488
489 pub fn with_attributes<I, K, V>(mut self, attributes: I) -> Self
490 where
491 I: IntoIterator<Item = (K, V)>,
492 K: Into<String>,
493 V: Into<String>,
494 {
495 self.claims.attributes =
496 attributes.into_iter().map(|(k, v)| (k.into(), v.into())).collect::<HashMap<_, _>>();
497 self
498 }
499
500 pub fn with_sha256(mut self, sha256: &str) -> Self {
501 self.claims.sha256 = sha256.to_owned();
502 self
503 }
504
505 pub fn with_room_config(mut self, config: livekit_protocol::RoomConfiguration) -> Self {
506 self.claims.room_config = Some(config);
507 self
508 }
509
510 pub fn to_jwt(self) -> Result<String, AccessTokenError> {
511 crate::jwt_provider::ensure_installed();
512 if self.api_key.is_empty() || self.api_secret.is_empty() {
513 return Err(AccessTokenError::InvalidKeys);
514 }
515
516 if self.claims.video.room_join
517 && (self.claims.sub.is_empty() || self.claims.video.room.is_empty())
518 {
519 return Err(AccessTokenError::InvalidClaims(
520 "token grants room_join but doesn't have an identity or room",
521 ));
522 }
523
524 Ok(jsonwebtoken::encode(
525 &Header::new(jsonwebtoken::Algorithm::HS256),
526 &self.claims,
527 &EncodingKey::from_secret(self.api_secret.as_ref()),
528 )?)
529 }
530}
531
532#[derive(Clone)]
533pub struct TokenVerifier {
534 api_key: String,
535 api_secret: String,
536}
537
538impl Debug for TokenVerifier {
539 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
540 f.debug_struct("TokenVerifier").field("api_key", &self.api_key).finish()
541 }
542}
543
544impl TokenVerifier {
545 pub fn with_api_key(api_key: &str, api_secret: &str) -> Self {
546 Self { api_key: api_key.to_owned(), api_secret: api_secret.to_owned() }
547 }
548
549 pub fn new() -> Result<Self, AccessTokenError> {
550 let (api_key, api_secret) = get_env_keys()?;
551 Ok(Self::with_api_key(&api_key, &api_secret))
552 }
553
554 pub fn verify(&self, token: &str) -> Result<Claims, AccessTokenError> {
555 crate::jwt_provider::ensure_installed();
556 let mut validation = jsonwebtoken::Validation::new(jsonwebtoken::Algorithm::HS256);
557 validation.validate_exp = true;
558 validation.validate_nbf = true;
559 validation.set_issuer(&[&self.api_key]);
560
561 let token = jsonwebtoken::decode::<Claims>(
562 token,
563 &DecodingKey::from_secret(self.api_secret.as_ref()),
564 &validation,
565 )?;
566
567 Ok(token.claims)
568 }
569}
570
571#[cfg(test)]
572mod tests {
573 use std::time::Duration;
574
575 use super::{
576 AccessToken, AgentDatabaseGrants, AgentEndpointGrants, AgentGrants, AgentThreadGrants,
577 Claims, InferenceGrants, ObservabilityGrants, SIPGrants, TokenVerifier, VideoGrants,
578 };
579
580 const TEST_API_KEY: &str = "myapikey";
581 const TEST_API_SECRET: &str = "thiskeyistotallyunsafe";
582 const TEST_TOKEN: &str = include_str!("test_token.txt");
583
584 #[test]
585 fn test_access_token() {
586 let room_config = livekit_protocol::RoomConfiguration {
587 name: "name".to_string(),
588 agents: vec![livekit_protocol::RoomAgentDispatch {
589 agent_name: "test-agent".to_string(),
590 metadata: "test-metadata".to_string(),
591 ..Default::default()
592 }],
593 ..Default::default()
594 };
595
596 let token = AccessToken::with_api_key(TEST_API_KEY, TEST_API_SECRET)
597 .with_ttl(Duration::from_secs(60))
598 .with_identity("test")
599 .with_name("test")
600 .with_grants(VideoGrants::default())
601 .with_room_config(room_config.clone())
602 .to_jwt()
603 .unwrap();
604
605 let verifier = TokenVerifier::with_api_key(TEST_API_KEY, TEST_API_SECRET);
606 let claims = verifier.verify(&token).unwrap();
607
608 assert_eq!(claims.sub, "test");
609 assert_eq!(claims.name, "test");
610 assert_eq!(claims.iss, TEST_API_KEY);
611 assert_eq!(claims.room_config, Some(room_config));
612
613 let incorrect_issuer = TokenVerifier::with_api_key("incorrect", TEST_API_SECRET);
614 assert!(incorrect_issuer.verify(&token).is_err());
615
616 let incorrect_token = TokenVerifier::with_api_key(TEST_API_KEY, "incorrect");
617 assert!(incorrect_token.verify(&token).is_err());
618 }
619
620 #[test]
621 fn test_verify_token_with_room_config() {
622 let verifier = TokenVerifier::with_api_key(TEST_API_KEY, TEST_API_SECRET);
623 let claims = verifier.verify(TEST_TOKEN).expect("Failed to verify token.");
625
626 assert_eq!(
627 super::Claims {
628 sub: "identity".to_string(),
629 name: "name".to_string(),
630 room_config: Some(livekit_protocol::RoomConfiguration {
631 agents: vec![livekit_protocol::RoomAgentDispatch {
632 agent_name: "test-agent".to_string(),
633 metadata: "test-metadata".to_string(),
634 ..Default::default()
635 }],
636 ..Default::default()
637 }),
638 ..claims.clone()
639 },
640 claims
641 );
642 }
643
644 #[test]
645 fn test_unverified_token() {
646 let claims = Claims::from_unverified(TEST_TOKEN).expect("Failed to parse token");
647
648 assert_eq!(claims.sub, "identity");
649 assert_eq!(claims.name, "name");
650 assert_eq!(claims.iss, TEST_API_KEY);
651 assert_eq!(
652 claims.room_config,
653 Some(livekit_protocol::RoomConfiguration {
654 agents: vec![livekit_protocol::RoomAgentDispatch {
655 agent_name: "test-agent".to_string(),
656 metadata: "test-metadata".to_string(),
657 ..Default::default()
658 }],
659 ..Default::default()
660 })
661 );
662
663 let token = AccessToken::with_api_key(TEST_API_KEY, TEST_API_SECRET)
664 .with_ttl(Duration::from_secs(60))
665 .with_identity("test")
666 .with_name("test")
667 .with_grants(VideoGrants {
668 room_join: true,
669 room: "test-room".to_string(),
670 ..Default::default()
671 })
672 .to_jwt()
673 .unwrap();
674
675 let claims = Claims::from_unverified(&token).expect("Failed to parse fresh token");
676 assert_eq!(claims.sub, "test");
677 assert_eq!(claims.name, "test");
678 assert_eq!(claims.video.room, "test-room");
679 assert!(claims.video.room_join);
680
681 let parts: Vec<&str> = token.split('.').collect();
682 let malformed_token = format!("{}.{}.wrongsignature", parts[0], parts[1]);
683
684 let claims = Claims::from_unverified(&malformed_token)
685 .expect("Failed to parse token with wrong signature");
686 assert_eq!(claims.sub, "test");
687 assert_eq!(claims.name, "test");
688 }
689
690 #[test]
691 fn test_agent_grant_and_kind() {
692 let token = AccessToken::with_api_key(TEST_API_KEY, TEST_API_SECRET)
693 .with_ttl(Duration::from_secs(60))
694 .with_identity("agent-1")
695 .with_kind("agent")
696 .with_grants(VideoGrants {
697 room_join: true,
698 room: "test-room".to_string(),
699 agent: true,
700 ..Default::default()
701 })
702 .to_jwt()
703 .expect("Failed to create token");
704
705 let verifier = TokenVerifier::with_api_key(TEST_API_KEY, TEST_API_SECRET);
706 let claims = verifier.verify(&token).expect("Failed to verify token.");
707 assert_eq!(claims.kind, "agent");
708 assert!(claims.video.agent);
709
710 let payload = |token: &str| {
711 let _ = Claims::from_unverified(token).expect("Failed to parse token");
712 jsonwebtoken::dangerous::insecure_decode::<serde_json::Value>(token)
713 .expect("Failed to decode token")
714 .claims
715 };
716 let bare = AccessToken::with_api_key(TEST_API_KEY, TEST_API_SECRET)
717 .with_ttl(Duration::from_secs(60))
718 .with_grants(VideoGrants::default())
719 .to_jwt()
720 .expect("Failed to create token");
721 let p = payload(&bare);
722 let mut keys: Vec<&str> = p.as_object().unwrap().keys().map(String::as_str).collect();
723 keys.sort_unstable();
724 assert_eq!(keys, ["exp", "iss", "nbf"], "{p}");
725
726 let agent = AccessToken::with_api_key(TEST_API_KEY, TEST_API_SECRET)
727 .with_ttl(Duration::from_secs(60))
728 .with_grants(VideoGrants {
729 agent: true,
730 can_publish: Some(false),
731 ..Default::default()
732 })
733 .to_jwt()
734 .expect("Failed to create token");
735 let p = payload(&agent);
736 assert_eq!(p["video"], serde_json::json!({"agent": true, "canPublish": false}), "{p}");
737 let claims = Claims::from_unverified(&agent).expect("Failed to parse token");
738 assert!(claims.video.agent && !claims.video.can_publish() && claims.video.can_subscribe());
739 assert!(!claims.video.can_publish_data(), "absent canPublishData follows canPublish");
740 }
741
742 #[test]
743 fn test_defaults_are_not_serialized() {
744 assert_eq!(serde_json::to_string(&VideoGrants::default()).unwrap(), "{}");
745 let parsed: VideoGrants = serde_json::from_str("{}").unwrap();
746 assert_eq!(parsed, VideoGrants::default());
747 assert_eq!(serde_json::to_string(&parsed).unwrap(), "{}");
748 let explicit = VideoGrants {
749 can_publish: Some(false),
750 can_publish_data: Some(true),
751 ..Default::default()
752 };
753 assert_eq!(
754 serde_json::to_string(&explicit).unwrap(),
755 r#"{"canPublish":false,"canPublishData":true}"#
756 );
757 assert!(explicit.can_publish_data());
758
759 assert_eq!(serde_json::to_string(&SIPGrants::default()).unwrap(), "{}");
760 let parsed: SIPGrants = serde_json::from_str("{}").unwrap();
761 assert_eq!(parsed, SIPGrants::default());
762 assert_eq!(serde_json::to_string(&parsed).unwrap(), "{}");
763
764 let claims = Claims { exp: 1, iss: "k".to_string(), nbf: 0, ..Default::default() };
765 let json = r#"{"exp":1,"iss":"k","nbf":0}"#;
766 assert_eq!(serde_json::to_string(&claims).unwrap(), json);
767 let parsed: Claims = serde_json::from_str(json).unwrap();
768 assert_eq!(parsed, claims);
769 assert_eq!(serde_json::to_string(&parsed).unwrap(), json);
770 }
771
772 #[test]
773 fn test_agent_grants_claim_names() {
774 let token = AccessToken::with_api_key(TEST_API_KEY, TEST_API_SECRET)
775 .with_agent_grants(AgentGrants { dispatch_admin: true, ..Default::default() })
776 .with_agent_endpoint_grants(AgentEndpointGrants {
777 call: true,
778 agent_name: "a".into(),
779 ..Default::default()
780 })
781 .with_agent_thread_grants(AgentThreadGrants {
782 scope: "s".into(),
783 thread_ids: vec!["t".into()],
784 list: true,
785 read: true,
786 sql: true,
787 ..Default::default()
788 })
789 .with_agent_database_grants(AgentDatabaseGrants {
790 database_ids: vec!["DB_a".into()],
791 read: true,
792 ..Default::default()
793 })
794 .to_jwt()
795 .unwrap();
796 let p =
797 jsonwebtoken::dangerous::insecure_decode::<serde_json::Value>(&token).unwrap().claims;
798 assert_eq!(p["agent"], serde_json::json!({"dispatchAdmin": true}));
799 assert_eq!(p["agentEndpoint"], serde_json::json!({"call": true, "agentName": "a"}));
800 assert_eq!(
801 p["agentThread"],
802 serde_json::json!({
803 "scope": "s", "threadIds": ["t"], "list": true, "read": true, "sql": true
804 })
805 );
806 assert_eq!(p["agentDatabase"], serde_json::json!({"databaseIds": ["DB_a"], "read": true}));
807 let claims = Claims::from_unverified(&token).unwrap();
808 assert!(claims.agent_thread.can_list("s") && claims.agent_thread.can_read("t"));
809 assert!(!claims.agent_thread.can_read("u") && !claims.agent_thread.can_create("s"));
810 assert!(claims.agent_thread.can_sql("t") && !claims.agent_thread.can_sql("u"));
811 assert!(claims.agent_database.can_read("DB_a") && !claims.agent_database.can_write("DB_a"));
812 assert!(!claims.agent_database.can_read("DB_b") && !claims.agent_database.can_read(""));
813 assert!(claims.agent_endpoint.allows("a", "default"));
814
815 let db = AgentDatabaseGrants {
816 database_ids: vec!["DB_a".into()],
817 delete: true,
818 create: true,
819 list: true,
820 ..Default::default()
821 };
822 assert_eq!(
823 serde_json::to_value(&db).unwrap(),
824 serde_json::json!({
825 "databaseIds": ["DB_a"], "delete": true, "create": true, "list": true
826 })
827 );
828 assert!(db.can_delete("DB_a") && !db.can_delete("DB_b") && !db.can_read("DB_a"));
829 assert!(db.can_create() && db.can_list());
830 assert!(!AgentDatabaseGrants::default().can_create());
831 let all = AgentThreadGrants { all: true, sql: true, ..Default::default() };
832 assert!(all.can_sql("AT_t") && !all.can_sql(""));
833 }
834
835 #[test]
836 fn test_inference_grants_claim_name() {
837 let token = AccessToken::with_api_key(TEST_API_KEY, TEST_API_SECRET)
838 .with_inference_grants(InferenceGrants::default().with_perform())
839 .to_jwt()
840 .unwrap();
841 let p =
842 jsonwebtoken::dangerous::insecure_decode::<serde_json::Value>(&token).unwrap().claims;
843 assert_eq!(p["inference"], serde_json::json!({"perform": true}));
844 assert!(Claims::from_unverified(&token).unwrap().inference.perform);
845 }
846
847 #[test]
848 fn test_observability_grants_claim_name() {
849 let token = AccessToken::with_api_key(TEST_API_KEY, TEST_API_SECRET)
850 .with_observability_grants(ObservabilityGrants::default().with_write())
851 .to_jwt()
852 .unwrap();
853 let p =
854 jsonwebtoken::dangerous::insecure_decode::<serde_json::Value>(&token).unwrap().claims;
855 assert_eq!(p["observability"], serde_json::json!({"write": true}));
856 assert!(Claims::from_unverified(&token).unwrap().observability.write);
857 }
858}