session.rs (24835B)
1 #![forbid(unsafe_code)] 2 3 use std::collections::{HashMap, HashSet}; 4 use std::sync::Arc; 5 use std::time::{Duration, Instant}; 6 7 use serde::{Deserialize, Serialize}; 8 use tokio::sync::Mutex; 9 10 use nostr::nips::nip46::NostrConnectRequest; 11 use radroots_nostr::prelude::{RadrootsNostrClient, RadrootsNostrKeys, RadrootsNostrPublicKey}; 12 13 #[derive(Clone)] 14 pub struct Nip46SessionStore { 15 inner: Arc<Mutex<HashMap<String, Nip46Session>>>, 16 used_secrets: Arc<Mutex<HashSet<String>>>, 17 } 18 19 #[derive(Clone)] 20 pub struct PendingNostrRequest { 21 pub request_id: String, 22 pub client_pubkey: RadrootsNostrPublicKey, 23 pub request: NostrConnectRequest, 24 } 25 26 pub struct Nip46AuthorizeOutcome { 27 pub pending: Option<PendingNostrRequest>, 28 } 29 30 #[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)] 31 #[serde(rename_all = "snake_case")] 32 pub enum Nip46SessionRole { 33 InboundLocalSigner, 34 OutboundRemoteSigner, 35 } 36 37 #[derive(Clone, Debug, Serialize)] 38 pub struct Nip46SessionView { 39 pub session_id: String, 40 pub role: Nip46SessionRole, 41 pub client_pubkey: String, 42 pub signer_pubkey: String, 43 pub user_pubkey: Option<String>, 44 pub relays: Vec<String>, 45 pub permissions: Vec<String>, 46 pub name: Option<String>, 47 pub url: Option<String>, 48 pub image: Option<String>, 49 pub auth_required: bool, 50 pub authorized: bool, 51 pub auth_url: Option<String>, 52 pub expires_in_secs: Option<u64>, 53 #[serde(default, skip_serializing_if = "Option::is_none")] 54 pub signer_authority: Option<Nip46SessionAuthority>, 55 } 56 57 #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] 58 pub struct Nip46SessionAuthority { 59 pub provider_runtime_id: String, 60 pub account_identity_id: String, 61 #[serde(default, skip_serializing_if = "Option::is_none")] 62 pub provider_session_id: Option<String>, 63 } 64 65 #[derive(Clone)] 66 pub struct Nip46Session { 67 pub id: String, 68 pub client: RadrootsNostrClient, 69 pub client_keys: RadrootsNostrKeys, 70 pub client_pubkey: RadrootsNostrPublicKey, 71 pub remote_signer_pubkey: RadrootsNostrPublicKey, 72 pub user_pubkey: Option<RadrootsNostrPublicKey>, 73 pub relays: Vec<String>, 74 pub perms: Vec<String>, 75 pub name: Option<String>, 76 pub url: Option<String>, 77 pub image: Option<String>, 78 pub expires_at: Option<Instant>, 79 pub auth_required: bool, 80 pub authorized: bool, 81 pub auth_url: Option<String>, 82 pub pending_request: Option<PendingNostrRequest>, 83 pub signer_authority: Option<Nip46SessionAuthority>, 84 } 85 86 impl Nip46SessionStore { 87 pub fn new() -> Self { 88 Self { 89 inner: Arc::new(Mutex::new(HashMap::new())), 90 used_secrets: Arc::new(Mutex::new(HashSet::new())), 91 } 92 } 93 94 pub async fn insert(&self, session: Nip46Session) { 95 let mut sessions = self.inner.lock().await; 96 sessions.insert(session.id.clone(), session); 97 } 98 99 pub async fn get(&self, session_id: &str) -> Option<Nip46Session> { 100 let mut sessions = self.inner.lock().await; 101 let expired = sessions 102 .get(session_id) 103 .map(|session| session.is_expired()) 104 .unwrap_or(false); 105 if expired { 106 sessions.remove(session_id); 107 return None; 108 } 109 sessions.get(session_id).cloned() 110 } 111 112 pub async fn remove(&self, session_id: &str) -> bool { 113 let mut sessions = self.inner.lock().await; 114 sessions.remove(session_id).is_some() 115 } 116 117 pub async fn set_user_pubkey(&self, session_id: &str, pubkey: RadrootsNostrPublicKey) -> bool { 118 let mut sessions = self.inner.lock().await; 119 match sessions.get_mut(session_id) { 120 Some(session) => { 121 if session.is_expired() { 122 sessions.remove(session_id); 123 return false; 124 } 125 session.user_pubkey = Some(pubkey); 126 true 127 } 128 None => false, 129 } 130 } 131 132 pub async fn require_auth(&self, session_id: &str, auth_url: String) -> bool { 133 let mut sessions = self.inner.lock().await; 134 match sessions.get_mut(session_id) { 135 Some(session) => { 136 if session.is_expired() { 137 sessions.remove(session_id); 138 return false; 139 } 140 session.auth_required = true; 141 session.authorized = false; 142 session.auth_url = Some(auth_url); 143 session.pending_request = None; 144 true 145 } 146 None => false, 147 } 148 } 149 150 pub async fn authorize(&self, session_id: &str) -> Option<Nip46AuthorizeOutcome> { 151 let mut sessions = self.inner.lock().await; 152 match sessions.get_mut(session_id) { 153 Some(session) => { 154 if session.is_expired() { 155 sessions.remove(session_id); 156 return None; 157 } 158 session.authorized = true; 159 Some(Nip46AuthorizeOutcome { 160 pending: session.pending_request.take(), 161 }) 162 } 163 None => None, 164 } 165 } 166 167 pub async fn set_pending_request( 168 &self, 169 session_id: &str, 170 pending: PendingNostrRequest, 171 ) -> bool { 172 let mut sessions = self.inner.lock().await; 173 match sessions.get_mut(session_id) { 174 Some(session) => { 175 if session.is_expired() { 176 sessions.remove(session_id); 177 return false; 178 } 179 session.pending_request = Some(pending); 180 true 181 } 182 None => false, 183 } 184 } 185 186 pub async fn list(&self) -> Vec<Nip46Session> { 187 let mut sessions = self.inner.lock().await; 188 sessions.retain(|_, session| !session.is_expired()); 189 let mut listed: Vec<Nip46Session> = sessions.values().cloned().collect(); 190 listed.sort_by(|left, right| left.id.cmp(&right.id)); 191 listed 192 } 193 194 pub async fn claim_secret(&self, secret: &str) -> bool { 195 let mut secrets = self.used_secrets.lock().await; 196 if secrets.contains(secret) { 197 return false; 198 } 199 secrets.insert(secret.to_string()); 200 true 201 } 202 } 203 204 impl Nip46Session { 205 pub fn normalize_authority( 206 authority: Option<Nip46SessionAuthority>, 207 ) -> Result<Option<Nip46SessionAuthority>, String> { 208 authority 209 .map(|authority| authority.normalized()) 210 .transpose() 211 } 212 213 pub fn is_expired(&self) -> bool { 214 self.expires_at 215 .map(|expires_at| expires_at <= Instant::now()) 216 .unwrap_or(false) 217 } 218 219 pub fn role(&self) -> Nip46SessionRole { 220 if self.client_keys.public_key() == self.remote_signer_pubkey { 221 Nip46SessionRole::InboundLocalSigner 222 } else { 223 Nip46SessionRole::OutboundRemoteSigner 224 } 225 } 226 227 pub fn public_view(&self) -> Nip46SessionView { 228 Nip46SessionView { 229 session_id: self.id.clone(), 230 role: self.role(), 231 client_pubkey: self.client_pubkey.to_hex(), 232 signer_pubkey: self.remote_signer_pubkey.to_hex(), 233 user_pubkey: self.user_pubkey.as_ref().map(|pubkey| pubkey.to_hex()), 234 relays: self.relays.clone(), 235 permissions: self.perms.clone(), 236 name: self.name.clone(), 237 url: self.url.clone(), 238 image: self.image.clone(), 239 auth_required: self.auth_required, 240 authorized: self.authorized, 241 auth_url: self.auth_url.clone(), 242 expires_in_secs: self.expires_at.map(remaining_secs), 243 signer_authority: self.signer_authority.clone(), 244 } 245 } 246 } 247 248 impl Nip46SessionAuthority { 249 pub fn normalized(mut self) -> Result<Self, String> { 250 self.provider_runtime_id = self.provider_runtime_id.trim().to_owned(); 251 self.account_identity_id = self.account_identity_id.trim().to_owned(); 252 self.provider_session_id = self 253 .provider_session_id 254 .as_deref() 255 .map(str::trim) 256 .filter(|value| !value.is_empty()) 257 .map(ToOwned::to_owned); 258 if self.provider_runtime_id.is_empty() { 259 return Err("signer_authority.provider_runtime_id cannot be empty".to_owned()); 260 } 261 if self.account_identity_id.is_empty() { 262 return Err("signer_authority.account_identity_id cannot be empty".to_owned()); 263 } 264 Ok(self) 265 } 266 } 267 268 fn remaining_secs(expires_at: Instant) -> u64 { 269 if expires_at <= Instant::now() { 270 0 271 } else { 272 expires_at 273 .saturating_duration_since(Instant::now()) 274 .as_secs() 275 } 276 } 277 278 pub fn filter_perms(requested: &[String], allowed: &[String]) -> Vec<String> { 279 if allowed.is_empty() { 280 return Vec::new(); 281 } 282 let allows_sign_event = allowed.iter().any(|entry| entry == "sign_event"); 283 requested 284 .iter() 285 .filter_map(|perm| { 286 if allowed.iter().any(|allow| allow == perm) { 287 return Some(perm.clone()); 288 } 289 if allows_sign_event && perm.starts_with("sign_event:") { 290 return Some(perm.clone()); 291 } 292 None 293 }) 294 .collect() 295 } 296 297 pub fn sign_event_allowed(perms: &[String], kind: u32) -> bool { 298 if perms.iter().any(|entry| entry == "sign_event") { 299 return true; 300 } 301 let entry = format!("sign_event:{kind}"); 302 perms.iter().any(|perm| perm == &entry) 303 } 304 305 pub fn session_expires_at(ttl_secs: u64) -> Option<Instant> { 306 if ttl_secs == 0 { 307 None 308 } else { 309 Some(Instant::now() + Duration::from_secs(ttl_secs)) 310 } 311 } 312 313 #[cfg(test)] 314 #[cfg_attr(coverage_nightly, coverage(off))] 315 mod tests { 316 use super::*; 317 318 fn build_session(id: &str, expires_at: Option<Instant>) -> Nip46Session { 319 let keys = RadrootsNostrKeys::generate(); 320 let client = RadrootsNostrClient::new(keys.clone()); 321 let pubkey = keys.public_key(); 322 Nip46Session { 323 id: id.to_string(), 324 client, 325 client_keys: keys, 326 client_pubkey: pubkey, 327 remote_signer_pubkey: pubkey, 328 user_pubkey: None, 329 relays: Vec::new(), 330 perms: Vec::new(), 331 name: None, 332 url: None, 333 image: None, 334 expires_at, 335 auth_required: false, 336 authorized: true, 337 auth_url: None, 338 pending_request: None, 339 signer_authority: None, 340 } 341 } 342 343 #[tokio::test] 344 async fn session_store_removes_expired() { 345 let store = Nip46SessionStore::new(); 346 let session = build_session("expired", Some(Instant::now() - Duration::from_secs(1))); 347 store.insert(session).await; 348 let found = store.get("expired").await; 349 assert!(found.is_none()); 350 let found_again = store.get("expired").await; 351 assert!(found_again.is_none()); 352 } 353 354 #[test] 355 fn public_view_marks_inbound_local_signer_sessions() { 356 let session = build_session("inbound", None); 357 358 let view = session.public_view(); 359 360 assert_eq!(view.session_id, "inbound"); 361 assert_eq!(view.role, Nip46SessionRole::InboundLocalSigner); 362 assert_eq!(view.client_pubkey, session.client_pubkey.to_hex()); 363 assert_eq!(view.signer_pubkey, session.remote_signer_pubkey.to_hex()); 364 assert_eq!(view.permissions, session.perms); 365 } 366 367 #[test] 368 fn public_view_marks_outbound_remote_signer_sessions() { 369 let client_keys = RadrootsNostrKeys::generate(); 370 let remote_signer_keys = RadrootsNostrKeys::generate(); 371 let session = Nip46Session { 372 id: "outbound".to_string(), 373 client: RadrootsNostrClient::new(client_keys.clone()), 374 client_keys: client_keys.clone(), 375 client_pubkey: client_keys.public_key(), 376 remote_signer_pubkey: remote_signer_keys.public_key(), 377 user_pubkey: None, 378 relays: vec!["wss://relay.example.com".to_string()], 379 perms: vec!["sign_event".to_string()], 380 name: Some("remote signer".to_string()), 381 url: Some("https://signer.example.com".to_string()), 382 image: None, 383 expires_at: Some(Instant::now() + Duration::from_secs(30)), 384 auth_required: true, 385 authorized: false, 386 auth_url: Some("https://signer.example.com/auth".to_string()), 387 pending_request: None, 388 signer_authority: None, 389 }; 390 391 let view = session.public_view(); 392 393 assert_eq!(view.session_id, "outbound"); 394 assert_eq!(view.role, Nip46SessionRole::OutboundRemoteSigner); 395 assert_eq!(view.client_pubkey, session.client_pubkey.to_hex()); 396 assert_eq!(view.signer_pubkey, session.remote_signer_pubkey.to_hex()); 397 assert_eq!(view.relays, session.relays); 398 assert_eq!(view.permissions, session.perms); 399 assert!(view.auth_required); 400 assert!(!view.authorized); 401 assert_eq!(view.auth_url, session.auth_url); 402 assert!(view.expires_in_secs.is_some()); 403 } 404 405 #[test] 406 fn public_view_keeps_remote_signer_and_user_pubkeys_distinct() { 407 let client_keys = RadrootsNostrKeys::generate(); 408 let remote_signer_keys = RadrootsNostrKeys::generate(); 409 let user_keys = RadrootsNostrKeys::generate(); 410 let session = Nip46Session { 411 id: "hydrated-outbound".to_string(), 412 client: RadrootsNostrClient::new(client_keys.clone()), 413 client_keys: client_keys.clone(), 414 client_pubkey: client_keys.public_key(), 415 remote_signer_pubkey: remote_signer_keys.public_key(), 416 user_pubkey: Some(user_keys.public_key()), 417 relays: vec!["wss://relay.example.com".to_string()], 418 perms: vec!["sign_event:30402".to_string()], 419 name: Some("remote signer".to_string()), 420 url: None, 421 image: None, 422 expires_at: Some(Instant::now() + Duration::from_secs(30)), 423 auth_required: false, 424 authorized: true, 425 auth_url: None, 426 pending_request: None, 427 signer_authority: None, 428 }; 429 430 let view = session.public_view(); 431 let expected_user_pubkey = user_keys.public_key().to_hex(); 432 433 assert_eq!(view.role, Nip46SessionRole::OutboundRemoteSigner); 434 assert_eq!(view.signer_pubkey, remote_signer_keys.public_key().to_hex()); 435 assert_eq!( 436 view.user_pubkey.as_deref(), 437 Some(expected_user_pubkey.as_str()) 438 ); 439 assert_ne!(view.signer_pubkey, expected_user_pubkey); 440 } 441 442 #[tokio::test] 443 async fn session_store_keeps_active() { 444 let store = Nip46SessionStore::new(); 445 let session = build_session("active", Some(Instant::now() + Duration::from_secs(60))); 446 store.insert(session).await; 447 let found = store.get("active").await; 448 assert!(found.is_some()); 449 } 450 451 #[tokio::test] 452 async fn session_store_list_filters_expired() { 453 let store = Nip46SessionStore::new(); 454 store 455 .insert(build_session( 456 "expired", 457 Some(Instant::now() - Duration::from_secs(1)), 458 )) 459 .await; 460 store 461 .insert(build_session( 462 "active", 463 Some(Instant::now() + Duration::from_secs(10)), 464 )) 465 .await; 466 let listed = store.list().await; 467 assert_eq!(listed.len(), 1); 468 assert_eq!(listed[0].id, "active"); 469 } 470 471 #[test] 472 fn filter_perms_allows_sign_event_kinds() { 473 let requested = vec![ 474 "sign_event:1".to_string(), 475 "sign_event:4".to_string(), 476 "nip04_encrypt".to_string(), 477 ]; 478 let allowed = vec!["sign_event".to_string(), "nip04_encrypt".to_string()]; 479 let filtered = filter_perms(&requested, &allowed); 480 assert_eq!( 481 filtered, 482 vec![ 483 "sign_event:1".to_string(), 484 "sign_event:4".to_string(), 485 "nip04_encrypt".to_string() 486 ] 487 ); 488 } 489 490 #[test] 491 fn sign_event_allowed_respects_kinds() { 492 let perms = vec!["sign_event:1".to_string()]; 493 assert!(sign_event_allowed(&perms, 1)); 494 assert!(!sign_event_allowed(&perms, 3)); 495 } 496 497 #[tokio::test] 498 async fn claim_secret_rejects_reuse() { 499 let store = Nip46SessionStore::new(); 500 assert!(store.claim_secret("secret").await); 501 assert!(!store.claim_secret("secret").await); 502 } 503 504 #[tokio::test] 505 async fn session_store_remove_reports_presence() { 506 let store = Nip46SessionStore::new(); 507 store.insert(build_session("remove", None)).await; 508 assert!(store.remove("remove").await); 509 assert!(!store.remove("remove").await); 510 } 511 512 #[test] 513 fn session_expires_at_handles_zero_and_positive() { 514 assert!(session_expires_at(0).is_none()); 515 assert!(session_expires_at(10).is_some()); 516 } 517 518 #[test] 519 fn session_is_expired_respects_future_and_none() { 520 let session = build_session("active", Some(Instant::now() + Duration::from_secs(1))); 521 assert!(!session.is_expired()); 522 let session = build_session("never", None); 523 assert!(!session.is_expired()); 524 } 525 526 #[test] 527 fn session_is_expired_for_past_deadline() { 528 let session = build_session("expired", Some(Instant::now() - Duration::from_secs(1))); 529 assert!(session.is_expired()); 530 } 531 532 #[tokio::test] 533 async fn session_store_set_user_pubkey_handles_missing_and_expired() { 534 let store = Nip46SessionStore::new(); 535 let keys = RadrootsNostrKeys::generate(); 536 assert!(!store.set_user_pubkey("missing", keys.public_key()).await); 537 538 let session = build_session( 539 "expired-user", 540 Some(Instant::now() - Duration::from_secs(1)), 541 ); 542 store.insert(session).await; 543 assert!( 544 !store 545 .set_user_pubkey("expired-user", keys.public_key()) 546 .await 547 ); 548 } 549 550 #[tokio::test] 551 async fn session_store_set_user_pubkey_sets_value_for_active_session() { 552 let store = Nip46SessionStore::new(); 553 let session = build_session( 554 "active-user", 555 Some(Instant::now() + Duration::from_secs(30)), 556 ); 557 let keys = RadrootsNostrKeys::generate(); 558 let pubkey = keys.public_key(); 559 store.insert(session).await; 560 assert!(store.set_user_pubkey("active-user", pubkey).await); 561 let found = store.get("active-user").await.expect("session"); 562 assert_eq!(found.user_pubkey, Some(pubkey)); 563 } 564 565 #[tokio::test] 566 async fn session_store_require_auth_sets_flags_and_clears_pending() { 567 let store = Nip46SessionStore::new(); 568 let mut session = build_session("auth", Some(Instant::now() + Duration::from_secs(30))); 569 let keys = RadrootsNostrKeys::generate(); 570 session.pending_request = Some(PendingNostrRequest { 571 request_id: "req-1".to_string(), 572 client_pubkey: keys.public_key(), 573 request: NostrConnectRequest::Ping, 574 }); 575 store.insert(session).await; 576 577 assert!(store.require_auth("auth", "https://auth".to_string()).await); 578 let found = store.get("auth").await.expect("session"); 579 assert!(found.auth_required); 580 assert!(!found.authorized); 581 assert_eq!(found.auth_url, Some("https://auth".to_string())); 582 assert!(found.pending_request.is_none()); 583 } 584 585 #[tokio::test] 586 async fn session_store_require_auth_handles_missing_and_expired() { 587 let store = Nip46SessionStore::new(); 588 assert!( 589 !store 590 .require_auth("missing", "https://auth".to_string()) 591 .await 592 ); 593 594 store 595 .insert(build_session( 596 "expired-auth", 597 Some(Instant::now() - Duration::from_secs(1)), 598 )) 599 .await; 600 assert!( 601 !store 602 .require_auth("expired-auth", "https://auth".to_string()) 603 .await 604 ); 605 } 606 607 #[tokio::test] 608 async fn session_store_authorize_returns_pending() { 609 let store = Nip46SessionStore::new(); 610 let mut session = 611 build_session("authorize", Some(Instant::now() + Duration::from_secs(30))); 612 let keys = RadrootsNostrKeys::generate(); 613 session.pending_request = Some(PendingNostrRequest { 614 request_id: "req-2".to_string(), 615 client_pubkey: keys.public_key(), 616 request: NostrConnectRequest::GetPublicKey, 617 }); 618 store.insert(session).await; 619 620 let outcome = store.authorize("authorize").await.expect("outcome"); 621 assert!(outcome.pending.is_some()); 622 let found = store.get("authorize").await.expect("session"); 623 assert!(found.authorized); 624 } 625 626 #[tokio::test] 627 async fn session_store_authorize_handles_missing_and_expired() { 628 let store = Nip46SessionStore::new(); 629 assert!(store.authorize("missing").await.is_none()); 630 631 store 632 .insert(build_session( 633 "expired-authorize", 634 Some(Instant::now() - Duration::from_secs(1)), 635 )) 636 .await; 637 assert!(store.authorize("expired-authorize").await.is_none()); 638 } 639 640 #[tokio::test] 641 async fn session_store_set_pending_request_handles_missing_and_expired() { 642 let store = Nip46SessionStore::new(); 643 let keys = RadrootsNostrKeys::generate(); 644 let pending = PendingNostrRequest { 645 request_id: "req-3".to_string(), 646 client_pubkey: keys.public_key(), 647 request: NostrConnectRequest::Ping, 648 }; 649 assert!(!store.set_pending_request("missing", pending.clone()).await); 650 651 let session = build_session( 652 "expired-pending", 653 Some(Instant::now() - Duration::from_secs(1)), 654 ); 655 store.insert(session).await; 656 assert!(!store.set_pending_request("expired-pending", pending).await); 657 } 658 659 #[tokio::test] 660 async fn session_store_set_pending_request_succeeds_for_active_session() { 661 let store = Nip46SessionStore::new(); 662 store 663 .insert(build_session( 664 "pending", 665 Some(Instant::now() + Duration::from_secs(30)), 666 )) 667 .await; 668 let keys = RadrootsNostrKeys::generate(); 669 let pending = PendingNostrRequest { 670 request_id: "req-active".to_string(), 671 client_pubkey: keys.public_key(), 672 request: NostrConnectRequest::Ping, 673 }; 674 assert!(store.set_pending_request("pending", pending).await); 675 let found = store.get("pending").await.expect("session"); 676 assert!(found.pending_request.is_some()); 677 } 678 679 #[tokio::test] 680 async fn session_store_list_sorts_ids() { 681 let store = Nip46SessionStore::new(); 682 store 683 .insert(build_session( 684 "b", 685 Some(Instant::now() + Duration::from_secs(10)), 686 )) 687 .await; 688 store 689 .insert(build_session( 690 "a", 691 Some(Instant::now() + Duration::from_secs(10)), 692 )) 693 .await; 694 let listed = store.list().await; 695 assert_eq!(listed.len(), 2); 696 assert_eq!(listed[0].id, "a"); 697 assert_eq!(listed[1].id, "b"); 698 } 699 700 #[test] 701 fn filter_perms_empty_allowed_returns_empty() { 702 let requested = vec!["nip04_encrypt".to_string()]; 703 let filtered = filter_perms(&requested, &[]); 704 assert!(filtered.is_empty()); 705 } 706 707 #[test] 708 fn filter_perms_exact_match_and_rejects_unlisted() { 709 let requested = vec![ 710 "nip04_encrypt".to_string(), 711 "nip44_encrypt".to_string(), 712 "sign_event:1".to_string(), 713 ]; 714 let allowed = vec!["nip04_encrypt".to_string()]; 715 let filtered = filter_perms(&requested, &allowed); 716 assert_eq!(filtered, vec!["nip04_encrypt".to_string()]); 717 } 718 719 #[test] 720 fn filter_perms_sign_event_global_does_not_allow_unrelated_perm() { 721 let requested = vec!["nip44_encrypt".to_string()]; 722 let allowed = vec!["sign_event".to_string()]; 723 let filtered = filter_perms(&requested, &allowed); 724 assert!(filtered.is_empty()); 725 } 726 727 #[test] 728 fn sign_event_allowed_accepts_global_permission() { 729 let perms = vec!["sign_event".to_string()]; 730 assert!(sign_event_allowed(&perms, 4)); 731 } 732 }