session.rs (24677B)
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 crate::host_nostr::{DaemonNostrClient, Keys, PublicKey}; 11 use nostr::nips::nip46::NostrConnectRequest; 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: PublicKey, 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(crate) client: DaemonNostrClient, 69 pub client_keys: Keys, 70 pub client_pubkey: PublicKey, 71 pub remote_signer_pubkey: PublicKey, 72 pub user_pubkey: Option<PublicKey>, 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: PublicKey) -> 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 Default for Nip46SessionStore { 205 fn default() -> Self { 206 Self::new() 207 } 208 } 209 210 impl Nip46Session { 211 pub fn normalize_authority( 212 authority: Option<Nip46SessionAuthority>, 213 ) -> Result<Option<Nip46SessionAuthority>, String> { 214 authority 215 .map(|authority| authority.normalized()) 216 .transpose() 217 } 218 219 pub fn is_expired(&self) -> bool { 220 self.expires_at 221 .map(|expires_at| expires_at <= Instant::now()) 222 .unwrap_or(false) 223 } 224 225 pub fn role(&self) -> Nip46SessionRole { 226 if self.client_keys.public_key() == self.remote_signer_pubkey { 227 Nip46SessionRole::InboundLocalSigner 228 } else { 229 Nip46SessionRole::OutboundRemoteSigner 230 } 231 } 232 233 pub fn public_view(&self) -> Nip46SessionView { 234 Nip46SessionView { 235 session_id: self.id.clone(), 236 role: self.role(), 237 client_pubkey: self.client_pubkey.to_hex(), 238 signer_pubkey: self.remote_signer_pubkey.to_hex(), 239 user_pubkey: self.user_pubkey.as_ref().map(|pubkey| pubkey.to_hex()), 240 relays: self.relays.clone(), 241 permissions: self.perms.clone(), 242 name: self.name.clone(), 243 url: self.url.clone(), 244 image: self.image.clone(), 245 auth_required: self.auth_required, 246 authorized: self.authorized, 247 auth_url: self.auth_url.clone(), 248 expires_in_secs: self.expires_at.map(remaining_secs), 249 signer_authority: self.signer_authority.clone(), 250 } 251 } 252 } 253 254 impl Nip46SessionAuthority { 255 pub fn normalized(mut self) -> Result<Self, String> { 256 self.provider_runtime_id = self.provider_runtime_id.trim().to_owned(); 257 self.account_identity_id = self.account_identity_id.trim().to_owned(); 258 self.provider_session_id = self 259 .provider_session_id 260 .as_deref() 261 .map(str::trim) 262 .filter(|value| !value.is_empty()) 263 .map(ToOwned::to_owned); 264 if self.provider_runtime_id.is_empty() { 265 return Err("signer_authority.provider_runtime_id cannot be empty".to_owned()); 266 } 267 if self.account_identity_id.is_empty() { 268 return Err("signer_authority.account_identity_id cannot be empty".to_owned()); 269 } 270 Ok(self) 271 } 272 } 273 274 fn remaining_secs(expires_at: Instant) -> u64 { 275 if expires_at <= Instant::now() { 276 0 277 } else { 278 expires_at 279 .saturating_duration_since(Instant::now()) 280 .as_secs() 281 } 282 } 283 284 pub fn filter_perms(requested: &[String], allowed: &[String]) -> Vec<String> { 285 if allowed.is_empty() { 286 return Vec::new(); 287 } 288 let allows_sign_event = allowed.iter().any(|entry| entry == "sign_event"); 289 requested 290 .iter() 291 .filter_map(|perm| { 292 if allowed.iter().any(|allow| allow == perm) { 293 return Some(perm.clone()); 294 } 295 if allows_sign_event && perm.starts_with("sign_event:") { 296 return Some(perm.clone()); 297 } 298 None 299 }) 300 .collect() 301 } 302 303 pub fn sign_event_allowed(perms: &[String], kind: u32) -> bool { 304 if perms.iter().any(|entry| entry == "sign_event") { 305 return true; 306 } 307 let entry = format!("sign_event:{kind}"); 308 perms.iter().any(|perm| perm == &entry) 309 } 310 311 pub fn session_expires_at(ttl_secs: u64) -> Option<Instant> { 312 if ttl_secs == 0 { 313 None 314 } else { 315 Some(Instant::now() + Duration::from_secs(ttl_secs)) 316 } 317 } 318 319 #[cfg(test)] 320 #[cfg_attr(coverage_nightly, coverage(off))] 321 mod tests { 322 use super::*; 323 324 fn build_session(id: &str, expires_at: Option<Instant>) -> Nip46Session { 325 let keys = Keys::generate(); 326 let client = DaemonNostrClient::with_keys(keys.clone()); 327 let pubkey = keys.public_key(); 328 Nip46Session { 329 id: id.to_string(), 330 client, 331 client_keys: keys, 332 client_pubkey: pubkey, 333 remote_signer_pubkey: pubkey, 334 user_pubkey: None, 335 relays: Vec::new(), 336 perms: Vec::new(), 337 name: None, 338 url: None, 339 image: None, 340 expires_at, 341 auth_required: false, 342 authorized: true, 343 auth_url: None, 344 pending_request: None, 345 signer_authority: None, 346 } 347 } 348 349 #[tokio::test] 350 async fn session_store_removes_expired() { 351 let store = Nip46SessionStore::new(); 352 let session = build_session("expired", Some(Instant::now() - Duration::from_secs(1))); 353 store.insert(session).await; 354 let found = store.get("expired").await; 355 assert!(found.is_none()); 356 let found_again = store.get("expired").await; 357 assert!(found_again.is_none()); 358 } 359 360 #[test] 361 fn public_view_marks_inbound_local_signer_sessions() { 362 let session = build_session("inbound", None); 363 364 let view = session.public_view(); 365 366 assert_eq!(view.session_id, "inbound"); 367 assert_eq!(view.role, Nip46SessionRole::InboundLocalSigner); 368 assert_eq!(view.client_pubkey, session.client_pubkey.to_hex()); 369 assert_eq!(view.signer_pubkey, session.remote_signer_pubkey.to_hex()); 370 assert_eq!(view.permissions, session.perms); 371 } 372 373 #[test] 374 fn public_view_marks_outbound_remote_signer_sessions() { 375 let client_keys = Keys::generate(); 376 let remote_signer_keys = Keys::generate(); 377 let session = Nip46Session { 378 id: "outbound".to_string(), 379 client: DaemonNostrClient::with_keys(client_keys.clone()), 380 client_keys: client_keys.clone(), 381 client_pubkey: client_keys.public_key(), 382 remote_signer_pubkey: remote_signer_keys.public_key(), 383 user_pubkey: None, 384 relays: vec!["wss://relay.example.com".to_string()], 385 perms: vec!["sign_event".to_string()], 386 name: Some("remote signer".to_string()), 387 url: Some("https://signer.example.com".to_string()), 388 image: None, 389 expires_at: Some(Instant::now() + Duration::from_secs(30)), 390 auth_required: true, 391 authorized: false, 392 auth_url: Some("https://signer.example.com/auth".to_string()), 393 pending_request: None, 394 signer_authority: None, 395 }; 396 397 let view = session.public_view(); 398 399 assert_eq!(view.session_id, "outbound"); 400 assert_eq!(view.role, Nip46SessionRole::OutboundRemoteSigner); 401 assert_eq!(view.client_pubkey, session.client_pubkey.to_hex()); 402 assert_eq!(view.signer_pubkey, session.remote_signer_pubkey.to_hex()); 403 assert_eq!(view.relays, session.relays); 404 assert_eq!(view.permissions, session.perms); 405 assert!(view.auth_required); 406 assert!(!view.authorized); 407 assert_eq!(view.auth_url, session.auth_url); 408 assert!(view.expires_in_secs.is_some()); 409 } 410 411 #[test] 412 fn public_view_keeps_remote_signer_and_user_pubkeys_distinct() { 413 let client_keys = Keys::generate(); 414 let remote_signer_keys = Keys::generate(); 415 let user_keys = Keys::generate(); 416 let session = Nip46Session { 417 id: "hydrated-outbound".to_string(), 418 client: DaemonNostrClient::with_keys(client_keys.clone()), 419 client_keys: client_keys.clone(), 420 client_pubkey: client_keys.public_key(), 421 remote_signer_pubkey: remote_signer_keys.public_key(), 422 user_pubkey: Some(user_keys.public_key()), 423 relays: vec!["wss://relay.example.com".to_string()], 424 perms: vec!["sign_event:30402".to_string()], 425 name: Some("remote signer".to_string()), 426 url: None, 427 image: None, 428 expires_at: Some(Instant::now() + Duration::from_secs(30)), 429 auth_required: false, 430 authorized: true, 431 auth_url: None, 432 pending_request: None, 433 signer_authority: None, 434 }; 435 436 let view = session.public_view(); 437 let expected_user_pubkey = user_keys.public_key().to_hex(); 438 439 assert_eq!(view.role, Nip46SessionRole::OutboundRemoteSigner); 440 assert_eq!(view.signer_pubkey, remote_signer_keys.public_key().to_hex()); 441 assert_eq!( 442 view.user_pubkey.as_deref(), 443 Some(expected_user_pubkey.as_str()) 444 ); 445 assert_ne!(view.signer_pubkey, expected_user_pubkey); 446 } 447 448 #[tokio::test] 449 async fn session_store_keeps_active() { 450 let store = Nip46SessionStore::new(); 451 let session = build_session("active", Some(Instant::now() + Duration::from_secs(60))); 452 store.insert(session).await; 453 let found = store.get("active").await; 454 assert!(found.is_some()); 455 } 456 457 #[tokio::test] 458 async fn session_store_list_filters_expired() { 459 let store = Nip46SessionStore::new(); 460 store 461 .insert(build_session( 462 "expired", 463 Some(Instant::now() - Duration::from_secs(1)), 464 )) 465 .await; 466 store 467 .insert(build_session( 468 "active", 469 Some(Instant::now() + Duration::from_secs(10)), 470 )) 471 .await; 472 let listed = store.list().await; 473 assert_eq!(listed.len(), 1); 474 assert_eq!(listed[0].id, "active"); 475 } 476 477 #[test] 478 fn filter_perms_allows_sign_event_kinds() { 479 let requested = vec![ 480 "sign_event:1".to_string(), 481 "sign_event:4".to_string(), 482 "nip04_encrypt".to_string(), 483 ]; 484 let allowed = vec!["sign_event".to_string(), "nip04_encrypt".to_string()]; 485 let filtered = filter_perms(&requested, &allowed); 486 assert_eq!( 487 filtered, 488 vec![ 489 "sign_event:1".to_string(), 490 "sign_event:4".to_string(), 491 "nip04_encrypt".to_string() 492 ] 493 ); 494 } 495 496 #[test] 497 fn sign_event_allowed_respects_kinds() { 498 let perms = vec!["sign_event:1".to_string()]; 499 assert!(sign_event_allowed(&perms, 1)); 500 assert!(!sign_event_allowed(&perms, 3)); 501 } 502 503 #[tokio::test] 504 async fn claim_secret_rejects_reuse() { 505 let store = Nip46SessionStore::new(); 506 assert!(store.claim_secret("secret").await); 507 assert!(!store.claim_secret("secret").await); 508 } 509 510 #[tokio::test] 511 async fn session_store_remove_reports_presence() { 512 let store = Nip46SessionStore::new(); 513 store.insert(build_session("remove", None)).await; 514 assert!(store.remove("remove").await); 515 assert!(!store.remove("remove").await); 516 } 517 518 #[test] 519 fn session_expires_at_handles_zero_and_positive() { 520 assert!(session_expires_at(0).is_none()); 521 assert!(session_expires_at(10).is_some()); 522 } 523 524 #[test] 525 fn session_is_expired_respects_future_and_none() { 526 let session = build_session("active", Some(Instant::now() + Duration::from_secs(1))); 527 assert!(!session.is_expired()); 528 let session = build_session("never", None); 529 assert!(!session.is_expired()); 530 } 531 532 #[test] 533 fn session_is_expired_for_past_deadline() { 534 let session = build_session("expired", Some(Instant::now() - Duration::from_secs(1))); 535 assert!(session.is_expired()); 536 } 537 538 #[tokio::test] 539 async fn session_store_set_user_pubkey_handles_missing_and_expired() { 540 let store = Nip46SessionStore::new(); 541 let keys = Keys::generate(); 542 assert!(!store.set_user_pubkey("missing", keys.public_key()).await); 543 544 let session = build_session( 545 "expired-user", 546 Some(Instant::now() - Duration::from_secs(1)), 547 ); 548 store.insert(session).await; 549 assert!( 550 !store 551 .set_user_pubkey("expired-user", keys.public_key()) 552 .await 553 ); 554 } 555 556 #[tokio::test] 557 async fn session_store_set_user_pubkey_sets_value_for_active_session() { 558 let store = Nip46SessionStore::new(); 559 let session = build_session( 560 "active-user", 561 Some(Instant::now() + Duration::from_secs(30)), 562 ); 563 let keys = Keys::generate(); 564 let pubkey = keys.public_key(); 565 store.insert(session).await; 566 assert!(store.set_user_pubkey("active-user", pubkey).await); 567 let found = store.get("active-user").await.expect("session"); 568 assert_eq!(found.user_pubkey, Some(pubkey)); 569 } 570 571 #[tokio::test] 572 async fn session_store_require_auth_sets_flags_and_clears_pending() { 573 let store = Nip46SessionStore::new(); 574 let mut session = build_session("auth", Some(Instant::now() + Duration::from_secs(30))); 575 let keys = Keys::generate(); 576 session.pending_request = Some(PendingNostrRequest { 577 request_id: "req-1".to_string(), 578 client_pubkey: keys.public_key(), 579 request: NostrConnectRequest::Ping, 580 }); 581 store.insert(session).await; 582 583 assert!(store.require_auth("auth", "https://auth".to_string()).await); 584 let found = store.get("auth").await.expect("session"); 585 assert!(found.auth_required); 586 assert!(!found.authorized); 587 assert_eq!(found.auth_url, Some("https://auth".to_string())); 588 assert!(found.pending_request.is_none()); 589 } 590 591 #[tokio::test] 592 async fn session_store_require_auth_handles_missing_and_expired() { 593 let store = Nip46SessionStore::new(); 594 assert!( 595 !store 596 .require_auth("missing", "https://auth".to_string()) 597 .await 598 ); 599 600 store 601 .insert(build_session( 602 "expired-auth", 603 Some(Instant::now() - Duration::from_secs(1)), 604 )) 605 .await; 606 assert!( 607 !store 608 .require_auth("expired-auth", "https://auth".to_string()) 609 .await 610 ); 611 } 612 613 #[tokio::test] 614 async fn session_store_authorize_returns_pending() { 615 let store = Nip46SessionStore::new(); 616 let mut session = 617 build_session("authorize", Some(Instant::now() + Duration::from_secs(30))); 618 let keys = Keys::generate(); 619 session.pending_request = Some(PendingNostrRequest { 620 request_id: "req-2".to_string(), 621 client_pubkey: keys.public_key(), 622 request: NostrConnectRequest::GetPublicKey, 623 }); 624 store.insert(session).await; 625 626 let outcome = store.authorize("authorize").await.expect("outcome"); 627 assert!(outcome.pending.is_some()); 628 let found = store.get("authorize").await.expect("session"); 629 assert!(found.authorized); 630 } 631 632 #[tokio::test] 633 async fn session_store_authorize_handles_missing_and_expired() { 634 let store = Nip46SessionStore::new(); 635 assert!(store.authorize("missing").await.is_none()); 636 637 store 638 .insert(build_session( 639 "expired-authorize", 640 Some(Instant::now() - Duration::from_secs(1)), 641 )) 642 .await; 643 assert!(store.authorize("expired-authorize").await.is_none()); 644 } 645 646 #[tokio::test] 647 async fn session_store_set_pending_request_handles_missing_and_expired() { 648 let store = Nip46SessionStore::new(); 649 let keys = Keys::generate(); 650 let pending = PendingNostrRequest { 651 request_id: "req-3".to_string(), 652 client_pubkey: keys.public_key(), 653 request: NostrConnectRequest::Ping, 654 }; 655 assert!(!store.set_pending_request("missing", pending.clone()).await); 656 657 let session = build_session( 658 "expired-pending", 659 Some(Instant::now() - Duration::from_secs(1)), 660 ); 661 store.insert(session).await; 662 assert!(!store.set_pending_request("expired-pending", pending).await); 663 } 664 665 #[tokio::test] 666 async fn session_store_set_pending_request_succeeds_for_active_session() { 667 let store = Nip46SessionStore::new(); 668 store 669 .insert(build_session( 670 "pending", 671 Some(Instant::now() + Duration::from_secs(30)), 672 )) 673 .await; 674 let keys = Keys::generate(); 675 let pending = PendingNostrRequest { 676 request_id: "req-active".to_string(), 677 client_pubkey: keys.public_key(), 678 request: NostrConnectRequest::Ping, 679 }; 680 assert!(store.set_pending_request("pending", pending).await); 681 let found = store.get("pending").await.expect("session"); 682 assert!(found.pending_request.is_some()); 683 } 684 685 #[tokio::test] 686 async fn session_store_list_sorts_ids() { 687 let store = Nip46SessionStore::new(); 688 store 689 .insert(build_session( 690 "b", 691 Some(Instant::now() + Duration::from_secs(10)), 692 )) 693 .await; 694 store 695 .insert(build_session( 696 "a", 697 Some(Instant::now() + Duration::from_secs(10)), 698 )) 699 .await; 700 let listed = store.list().await; 701 assert_eq!(listed.len(), 2); 702 assert_eq!(listed[0].id, "a"); 703 assert_eq!(listed[1].id, "b"); 704 } 705 706 #[test] 707 fn filter_perms_empty_allowed_returns_empty() { 708 let requested = vec!["nip04_encrypt".to_string()]; 709 let filtered = filter_perms(&requested, &[]); 710 assert!(filtered.is_empty()); 711 } 712 713 #[test] 714 fn filter_perms_exact_match_and_rejects_unlisted() { 715 let requested = vec![ 716 "nip04_encrypt".to_string(), 717 "nip44_encrypt".to_string(), 718 "sign_event:1".to_string(), 719 ]; 720 let allowed = vec!["nip04_encrypt".to_string()]; 721 let filtered = filter_perms(&requested, &allowed); 722 assert_eq!(filtered, vec!["nip04_encrypt".to_string()]); 723 } 724 725 #[test] 726 fn filter_perms_sign_event_global_does_not_allow_unrelated_perm() { 727 let requested = vec!["nip44_encrypt".to_string()]; 728 let allowed = vec!["sign_event".to_string()]; 729 let filtered = filter_perms(&requested, &allowed); 730 assert!(filtered.is_empty()); 731 } 732 733 #[test] 734 fn sign_event_allowed_accepts_global_permission() { 735 let perms = vec!["sign_event".to_string()]; 736 assert!(sign_event_allowed(&perms, 4)); 737 } 738 }