1use crate::account_policy::{redact_identity, AccountPolicyError, AccountPolicyRegistry};
2use crate::compression::CompressedPayload;
3use crate::websocket::admission::{WebSocketAdmissionProvider, WebSocketConnectionPermit};
4use crate::websocket::auth::{AuthContext, AuthDeny, AuthErrorCode};
5use crate::websocket::rate_limiter::{RateLimitResult, WebSocketRateLimiter};
6use arete_auth::Limits;
7use bytes::Bytes;
8use dashmap::DashMap;
9use futures_util::stream::SplitSink;
10use futures_util::SinkExt;
11use std::collections::{HashMap, HashSet};
12use std::net::SocketAddr;
13use std::sync::Arc;
14use std::time::{Duration, Instant, SystemTime};
15use tokio::net::TcpStream;
16use tokio::sync::{mpsc, Mutex as AsyncMutex, RwLock};
17use tokio_tungstenite::tungstenite::protocol::frame::coding::CloseCode;
18use tokio_tungstenite::tungstenite::protocol::CloseFrame;
19use tokio_tungstenite::{tungstenite::Message, WebSocketStream};
20use tokio_util::sync::CancellationToken;
21use tracing::{debug, info, warn};
22use uuid::Uuid;
23
24pub type WebSocketSender = SplitSink<WebSocketStream<TcpStream>, Message>;
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum SendError {
29 ClientNotFound,
31 ClientBackpressured,
33 ClientDisconnected,
35}
36
37impl std::fmt::Display for SendError {
38 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39 match self {
40 SendError::ClientNotFound => write!(f, "client not found"),
41 SendError::ClientBackpressured => write!(f, "client backpressured and disconnected"),
42 SendError::ClientDisconnected => write!(f, "client disconnected"),
43 }
44 }
45}
46
47impl std::error::Error for SendError {}
48
49#[derive(Debug)]
51struct EgressTracker {
52 bytes_this_minute: u64,
54 window_start: SystemTime,
56}
57
58#[derive(Debug)]
60struct MessageRateTracker {
61 messages_this_minute: u32,
62 window_start: SystemTime,
63}
64
65impl MessageRateTracker {
66 fn new() -> Self {
67 Self {
68 messages_this_minute: 0,
69 window_start: SystemTime::now(),
70 }
71 }
72
73 fn maybe_reset_window(&mut self) {
74 let now = SystemTime::now();
75 if now.duration_since(self.window_start).unwrap_or_default() >= Duration::from_secs(60) {
76 self.messages_this_minute = 0;
77 self.window_start = now;
78 }
79 }
80
81 fn record_message(&mut self, limit: u32) -> bool {
82 self.maybe_reset_window();
83 if self.messages_this_minute + 1 > limit {
84 false
85 } else {
86 self.messages_this_minute += 1;
87 true
88 }
89 }
90
91 fn current_usage(&mut self) -> u32 {
92 self.maybe_reset_window();
93 self.messages_this_minute
94 }
95}
96
97impl EgressTracker {
98 fn new() -> Self {
99 Self {
100 bytes_this_minute: 0,
101 window_start: SystemTime::now(),
102 }
103 }
104
105 fn maybe_reset_window(&mut self) {
107 let now = SystemTime::now();
108 if now.duration_since(self.window_start).unwrap_or_default() >= Duration::from_secs(60) {
109 self.bytes_this_minute = 0;
110 self.window_start = now;
111 }
112 }
113
114 fn record_bytes(&mut self, bytes: usize, limit: u64) -> bool {
116 self.maybe_reset_window();
117 let bytes_u64 = bytes as u64;
118 if self.bytes_this_minute + bytes_u64 > limit {
119 false
120 } else {
121 self.bytes_this_minute += bytes_u64;
122 true
123 }
124 }
125
126 fn current_usage(&mut self) -> u64 {
128 self.maybe_reset_window();
129 self.bytes_this_minute
130 }
131}
132
133#[derive(Debug)]
138struct AccountUsage {
139 egress: std::sync::Mutex<EgressTracker>,
140 messages: std::sync::Mutex<MessageRateTracker>,
141 last_seen: std::sync::Mutex<Instant>,
142}
143
144impl AccountUsage {
145 fn new() -> Self {
146 Self {
147 egress: std::sync::Mutex::new(EgressTracker::new()),
148 messages: std::sync::Mutex::new(MessageRateTracker::new()),
149 last_seen: std::sync::Mutex::new(Instant::now()),
150 }
151 }
152
153 fn touch(&self) {
154 if let Ok(mut last_seen) = self.last_seen.lock() {
155 *last_seen = Instant::now();
156 }
157 }
158
159 fn record_egress(&self, bytes: usize, limit: u64) -> bool {
160 self.touch();
161 match self.egress.lock() {
162 Ok(mut tracker) => tracker.record_bytes(bytes, limit),
163 Err(_) => true,
164 }
165 }
166
167 fn record_message(&self, limit: u32) -> bool {
168 self.touch();
169 match self.messages.lock() {
170 Ok(mut tracker) => tracker.record_message(limit),
171 Err(_) => true,
172 }
173 }
174
175 fn idle_for(&self) -> Duration {
176 self.last_seen
177 .lock()
178 .map(|last_seen| last_seen.elapsed())
179 .unwrap_or(Duration::MAX)
180 }
181}
182
183#[derive(Debug)]
185pub struct ClientInfo {
186 pub id: Uuid,
187 pub last_seen: SystemTime,
188 pub sender: mpsc::Sender<Message>,
189 subscriptions: Arc<RwLock<HashMap<String, CancellationToken>>>,
190 pub auth_context: Option<AuthContext>,
192 pub remote_addr: SocketAddr,
194 egress_tracker: std::sync::Mutex<EgressTracker>,
196 message_rate_tracker: std::sync::Mutex<MessageRateTracker>,
198 close_frame: Arc<std::sync::OnceLock<CloseFrame>>,
202 admission_permit: Option<Arc<dyn WebSocketConnectionPermit>>,
203 admission_lifecycle: Arc<AsyncMutex<()>>,
206 connection_token: CancellationToken,
210}
211
212impl Drop for ClientInfo {
213 fn drop(&mut self) {
217 self.connection_token.cancel();
218 match self.subscriptions.try_read() {
222 Ok(subscriptions) => subscriptions.values().for_each(CancellationToken::cancel),
223 Err(_) => {
224 let subscriptions = self.subscriptions.clone();
225 if let Ok(runtime) = tokio::runtime::Handle::try_current() {
226 runtime.spawn(async move {
227 subscriptions
228 .read()
229 .await
230 .values()
231 .for_each(CancellationToken::cancel);
232 });
233 }
234 }
235 }
236 }
237}
238
239impl ClientInfo {
240 pub fn new(
241 id: Uuid,
242 sender: mpsc::Sender<Message>,
243 auth_context: Option<AuthContext>,
244 remote_addr: SocketAddr,
245 ) -> Self {
246 Self {
247 id,
248 last_seen: SystemTime::now(),
249 sender,
250 subscriptions: Arc::new(RwLock::new(HashMap::new())),
251 auth_context,
252 remote_addr,
253 egress_tracker: std::sync::Mutex::new(EgressTracker::new()),
254 message_rate_tracker: std::sync::Mutex::new(MessageRateTracker::new()),
255 close_frame: Arc::new(std::sync::OnceLock::new()),
256 admission_permit: None,
257 admission_lifecycle: Arc::new(AsyncMutex::new(())),
258 connection_token: CancellationToken::new(),
259 }
260 }
261
262 pub fn record_egress(&self, bytes: usize) -> Option<u64> {
264 if let Ok(mut tracker) = self.egress_tracker.lock() {
265 if let Some(ref ctx) = self.auth_context {
266 if let Some(limit) = ctx.limits.max_bytes_per_minute {
267 if tracker.record_bytes(bytes, limit) {
268 return Some(tracker.current_usage());
269 } else {
270 return None; }
272 }
273 }
274 return Some(tracker.current_usage());
276 }
277 None
278 }
279
280 pub fn record_inbound_message(&self) -> Option<u32> {
282 if let Ok(mut tracker) = self.message_rate_tracker.lock() {
283 if let Some(ref ctx) = self.auth_context {
284 if let Some(limit) = ctx.limits.max_messages_per_minute {
285 if tracker.record_message(limit) {
286 return Some(tracker.current_usage());
287 } else {
288 return None;
289 }
290 }
291 }
292
293 return Some(tracker.current_usage());
294 }
295
296 None
297 }
298
299 pub fn update_last_seen(&mut self) {
300 self.last_seen = SystemTime::now();
301 }
302
303 pub fn is_stale(&self, timeout: Duration) -> bool {
304 self.last_seen.elapsed().unwrap_or(Duration::MAX) > timeout
305 }
306
307 pub async fn add_subscription(
308 &self,
309 subscription_id: String,
310 token: CancellationToken,
311 ) -> bool {
312 let _lifecycle = self.admission_lifecycle.lock().await;
313 let mut subs = self.subscriptions.write().await;
314 match subs.entry(subscription_id) {
315 std::collections::hash_map::Entry::Vacant(entry) => {
316 entry.insert(token);
317 true
318 }
319 std::collections::hash_map::Entry::Occupied(_) => false,
320 }
321 }
322
323 pub async fn remove_subscription(&self, subscription_id: &str) -> bool {
324 let _lifecycle = self.admission_lifecycle.lock().await;
325 let mut subs = self.subscriptions.write().await;
326 if let Some(token) = subs.remove(subscription_id) {
327 token.cancel();
328 debug!("Cancelled subscription: {}", subscription_id);
329 true
330 } else {
331 debug!(
332 "Subscription not found for cancellation: {}",
333 subscription_id
334 );
335 false
336 }
337 }
338
339 pub async fn cancel_all_subscriptions(&self) {
340 let _lifecycle = self.admission_lifecycle.lock().await;
341 let subs = self.subscriptions.read().await;
342 for (subscription_id, token) in subs.iter() {
343 token.cancel();
344 debug!("Cancelled subscription on disconnect: {}", subscription_id);
345 }
346 }
347
348 pub async fn subscription_count(&self) -> usize {
349 self.subscriptions.read().await.len()
350 }
351}
352
353#[derive(Debug, Clone)]
358pub struct RateLimitConfig {
359 pub max_connections_per_ip: Option<usize>,
361 pub max_connections_per_metering_key: Option<usize>,
363 pub max_connections_per_origin: Option<usize>,
365 pub client_timeout: Duration,
367 pub message_queue_size: usize,
369 pub max_reconnect_attempts: Option<u32>,
371 pub message_rate_window: Duration,
373 pub egress_rate_window: Duration,
375 pub default_limits: Option<Limits>,
378}
379
380impl Default for RateLimitConfig {
381 fn default() -> Self {
382 Self {
383 max_connections_per_ip: None,
384 max_connections_per_metering_key: None,
385 max_connections_per_origin: None,
386 client_timeout: Duration::from_secs(300),
387 message_queue_size: 512,
388 max_reconnect_attempts: None,
389 message_rate_window: Duration::from_secs(60),
390 egress_rate_window: Duration::from_secs(60),
391 default_limits: None,
392 }
393 }
394}
395
396impl RateLimitConfig {
397 pub fn from_env() -> Self {
412 let mut config = Self::default();
413
414 if let Ok(val) = std::env::var("ARETE_WS_MAX_CONNECTIONS_PER_IP") {
415 if let Ok(max) = val.parse() {
416 config.max_connections_per_ip = Some(max);
417 }
418 }
419
420 if let Ok(val) = std::env::var("ARETE_WS_MAX_CONNECTIONS_PER_METERING_KEY") {
421 if let Ok(max) = val.parse() {
422 config.max_connections_per_metering_key = Some(max);
423 }
424 }
425
426 if let Ok(val) = std::env::var("ARETE_WS_MAX_CONNECTIONS_PER_ORIGIN") {
427 if let Ok(max) = val.parse() {
428 config.max_connections_per_origin = Some(max);
429 }
430 }
431
432 if let Ok(val) = std::env::var("ARETE_WS_CLIENT_TIMEOUT_SECS") {
433 if let Ok(secs) = val.parse() {
434 config.client_timeout = Duration::from_secs(secs);
435 }
436 }
437
438 if let Ok(val) = std::env::var("ARETE_WS_MESSAGE_QUEUE_SIZE") {
439 if let Ok(size) = val.parse() {
440 config.message_queue_size = size;
441 }
442 }
443
444 if let Ok(val) = std::env::var("ARETE_WS_RATE_LIMIT_WINDOW_SECS") {
445 if let Ok(secs) = val.parse() {
446 config.message_rate_window = Duration::from_secs(secs);
447 config.egress_rate_window = Duration::from_secs(secs);
448 }
449 }
450
451 let mut default_limits = Limits::default();
453 let mut has_default_limits = false;
454
455 if let Ok(val) = std::env::var("ARETE_WS_DEFAULT_MAX_CONNECTIONS") {
456 if let Ok(max) = val.parse() {
457 default_limits.max_connections = Some(max);
458 has_default_limits = true;
459 }
460 }
461
462 if let Ok(val) = std::env::var("ARETE_WS_DEFAULT_MAX_SUBSCRIPTIONS") {
463 if let Ok(max) = val.parse() {
464 default_limits.max_subscriptions = Some(max);
465 has_default_limits = true;
466 }
467 }
468
469 if let Ok(val) = std::env::var("ARETE_WS_DEFAULT_MAX_SNAPSHOT_ROWS") {
470 if let Ok(max) = val.parse() {
471 default_limits.max_snapshot_rows = Some(max);
472 has_default_limits = true;
473 }
474 }
475
476 if let Ok(val) = std::env::var("ARETE_WS_DEFAULT_MAX_MESSAGES_PER_MINUTE") {
477 if let Ok(max) = val.parse() {
478 default_limits.max_messages_per_minute = Some(max);
479 has_default_limits = true;
480 }
481 }
482
483 if let Ok(val) = std::env::var("ARETE_WS_DEFAULT_MAX_BYTES_PER_MINUTE") {
484 if let Ok(max) = val.parse() {
485 default_limits.max_bytes_per_minute = Some(max);
486 has_default_limits = true;
487 }
488 }
489
490 if has_default_limits {
491 config.default_limits = Some(default_limits);
492 }
493
494 config
495 }
496
497 pub fn with_max_connections_per_ip(mut self, max: usize) -> Self {
499 self.max_connections_per_ip = Some(max);
500 self
501 }
502
503 pub fn with_timeout(mut self, timeout: Duration) -> Self {
505 self.client_timeout = timeout;
506 self
507 }
508
509 pub fn with_message_queue_size(mut self, size: usize) -> Self {
511 self.message_queue_size = size;
512 self
513 }
514
515 pub fn with_rate_limit_window(mut self, window: Duration) -> Self {
517 self.message_rate_window = window;
518 self.egress_rate_window = window;
519 self
520 }
521
522 pub fn with_default_limits(mut self, limits: Limits) -> Self {
527 self.default_limits = Some(limits);
528 self
529 }
530}
531
532const MAX_TRACKED_ACCOUNT_USAGE: usize = crate::account_policy::DEFAULT_MAX_TRACKED_ACCOUNTS;
535
536const ACCOUNT_USAGE_IDLE_TTL: Duration = crate::account_policy::DEFAULT_ACCOUNT_IDLE_TTL;
538
539#[cfg(not(test))]
542const REMOVED_CLIENT_WRITE_GRACE: Duration = Duration::from_secs(5);
543#[cfg(test)]
544const REMOVED_CLIENT_WRITE_GRACE: Duration = Duration::from_millis(500);
545
546#[derive(Clone)]
555pub struct ClientManager {
556 clients: Arc<DashMap<Uuid, ClientInfo>>,
557 rate_limit_config: RateLimitConfig,
558 rate_limiter: Option<Arc<WebSocketRateLimiter>>,
560 account_policies: Arc<AccountPolicyRegistry>,
562 account_usage: Arc<DashMap<String, AccountUsage>>,
564 admission_provider: Option<Arc<dyn WebSocketAdmissionProvider>>,
566 refresh_admission: Arc<AsyncMutex<()>>,
569}
570
571impl ClientManager {
572 pub fn new() -> Self {
573 Self::with_config(RateLimitConfig::default())
574 }
575
576 pub fn with_config(config: RateLimitConfig) -> Self {
578 Self {
579 clients: Arc::new(DashMap::new()),
580 rate_limit_config: config,
581 rate_limiter: None,
582 account_policies: Arc::new(AccountPolicyRegistry::default()),
583 account_usage: Arc::new(DashMap::new()),
584 admission_provider: None,
585 refresh_admission: Arc::new(AsyncMutex::new(())),
586 }
587 }
588
589 pub fn with_admission_provider(
591 mut self,
592 provider: Arc<dyn WebSocketAdmissionProvider>,
593 ) -> Self {
594 self.admission_provider = Some(provider);
595 self
596 }
597
598 #[allow(clippy::result_large_err)]
599 pub(crate) fn reserve_connection_admission(
600 &self,
601 context: &AuthContext,
602 ) -> Result<Option<Arc<dyn WebSocketConnectionPermit>>, AuthDeny> {
603 self.admission_provider
604 .as_ref()
605 .map(|provider| provider.reserve_connection(context))
606 .unwrap_or(Ok(None))
607 }
608
609 pub fn from_env() -> Self {
613 Self::with_config(RateLimitConfig::from_env())
614 }
615
616 pub fn with_timeout(mut self, timeout: Duration) -> Self {
618 self.rate_limit_config.client_timeout = timeout;
619 self
620 }
621
622 pub fn with_message_queue_size(mut self, queue_size: usize) -> Self {
624 self.rate_limit_config.message_queue_size = queue_size;
625 self
626 }
627
628 pub fn with_max_connections_per_ip(mut self, max: usize) -> Self {
630 self.rate_limit_config.max_connections_per_ip = Some(max);
631 self
632 }
633
634 pub fn with_rate_limit_window(mut self, window: Duration) -> Self {
636 self.rate_limit_config.message_rate_window = window;
637 self.rate_limit_config.egress_rate_window = window;
638 self
639 }
640
641 pub fn with_default_limits(mut self, limits: Limits) -> Self {
646 self.rate_limit_config.default_limits = Some(limits);
647 self
648 }
649
650 pub fn with_rate_limiter(mut self, rate_limiter: Arc<WebSocketRateLimiter>) -> Self {
652 self.rate_limiter = Some(rate_limiter);
653 self
654 }
655
656 pub fn rate_limiter(&self) -> Option<&WebSocketRateLimiter> {
658 self.rate_limiter.as_ref().map(|r| r.as_ref())
659 }
660
661 pub fn rate_limit_config(&self) -> &RateLimitConfig {
663 &self.rate_limit_config
664 }
665
666 pub fn account_policies(&self) -> &AccountPolicyRegistry {
668 &self.account_policies
669 }
670
671 fn record_account_egress(&self, ctx: &AuthContext, bytes: usize) -> bool {
673 if ctx.is_legacy_policy() || ctx.account_key.is_none() {
674 return true;
675 }
676 let Some(limit) = ctx.account_limits.max_bytes_per_minute else {
677 return true;
678 };
679 let account = ctx.account_key();
680 if let Some(usage) = self.account_usage.get(account) {
681 return usage.record_egress(bytes, limit);
682 }
683 if self.account_usage.len() >= MAX_TRACKED_ACCOUNT_USAGE {
684 warn!(
685 account = %redact_identity(account),
686 "account usage tracking at capacity; aggregate egress guard skipped"
687 );
688 return true;
689 }
690 self.account_usage
691 .entry(account.to_string())
692 .or_insert_with(AccountUsage::new)
693 .record_egress(bytes, limit)
694 }
695
696 fn record_account_message(&self, ctx: &AuthContext) -> bool {
698 if ctx.is_legacy_policy() || ctx.account_key.is_none() {
699 return true;
700 }
701 let Some(limit) = ctx.account_limits.max_messages_per_minute else {
702 return true;
703 };
704 let account = ctx.account_key();
705 if let Some(usage) = self.account_usage.get(account) {
706 return usage.record_message(limit);
707 }
708 if self.account_usage.len() >= MAX_TRACKED_ACCOUNT_USAGE {
709 warn!(
710 account = %redact_identity(account),
711 "account usage tracking at capacity; aggregate message guard skipped"
712 );
713 return true;
714 }
715 self.account_usage
716 .entry(account.to_string())
717 .or_insert_with(AccountUsage::new)
718 .record_message(limit)
719 }
720
721 fn enforce_egress_budgets(&self, client_id: Uuid, bytes: usize) -> Result<(), SendError> {
723 let (connection_ok, account_ok, permit_id) = {
724 let Some(client) = self.clients.get(&client_id) else {
725 return Err(SendError::ClientNotFound);
726 };
727 let connection_ok = client.record_egress(bytes).is_some();
728 let account_ok = !connection_ok
729 || client
730 .auth_context
731 .as_ref()
732 .map(|ctx| self.record_account_egress(ctx, bytes))
733 .unwrap_or(true);
734 let permit_id = client.admission_permit.as_ref().map(|permit| permit.id());
735 (connection_ok, account_ok, permit_id)
736 };
737 if !connection_ok {
738 warn!("Client {} exceeded egress limit, disconnecting", client_id);
739 self.clients.remove(&client_id);
740 return Err(SendError::ClientDisconnected);
741 }
742 if !account_ok {
743 warn!(
744 "Client {} exceeded the account egress limit, disconnecting",
745 client_id
746 );
747 self.clients.remove(&client_id);
748 return Err(SendError::ClientDisconnected);
749 }
750 if let (Some(provider), Some(permit_id)) = (&self.admission_provider, permit_id) {
751 if let Err(deny) = provider.check_egress(permit_id, bytes) {
752 warn!(client = %client_id, reason = %deny.reason, "admission provider denied egress");
753 self.clients.remove(&client_id);
754 return Err(SendError::ClientDisconnected);
755 }
756 }
757 Ok(())
758 }
759
760 #[allow(clippy::result_large_err)]
766 pub fn add_client(
767 &self,
768 client_id: Uuid,
769 ws_sender: WebSocketSender,
770 auth_context: Option<AuthContext>,
771 remote_addr: SocketAddr,
772 ) -> Result<(), AuthDeny> {
773 let permit = if let Some(context) = auth_context.as_ref() {
774 self.reserve_connection_admission(context)?
775 } else {
776 None
777 };
778 self.add_client_with_admission(client_id, ws_sender, auth_context, remote_addr, permit);
779 Ok(())
780 }
781
782 pub(crate) fn add_client_with_admission(
783 &self,
784 client_id: Uuid,
785 mut ws_sender: WebSocketSender,
786 auth_context: Option<AuthContext>,
787 remote_addr: SocketAddr,
788 admission_permit: Option<Arc<dyn WebSocketConnectionPermit>>,
789 ) {
790 let (client_tx, mut client_rx) =
791 mpsc::channel::<Message>(self.rate_limit_config.message_queue_size);
792 let mut client_info = ClientInfo::new(client_id, client_tx, auth_context, remote_addr);
793 client_info.admission_permit = admission_permit;
794 let close_frame = client_info.close_frame.clone();
795
796 let clients_ref = self.clients.clone();
797 let connection_token = client_info.connection_token.clone();
798 tokio::spawn(async move {
799 let deliver = async {
800 while let Some(message) = client_rx.recv().await {
801 if let Err(e) = ws_sender.send(message).await {
802 warn!("Failed to send message to client {}: {}", client_id, e);
803 break;
804 }
805 }
806 if let Some(frame) = close_frame.get() {
812 let _ = ws_sender.send(Message::Close(Some(frame.clone()))).await;
813 }
814 let _ = ws_sender.close().await;
815 };
816 tokio::pin!(deliver);
817 tokio::select! {
822 () = &mut deliver => {}
823 () = connection_token.cancelled() => {
824 if tokio::time::timeout(REMOVED_CLIENT_WRITE_GRACE, &mut deliver)
825 .await
826 .is_err()
827 {
828 warn!(
829 "Client {} did not take its remaining frames in time; dropping its socket",
830 client_id
831 );
832 }
833 }
834 }
835 clients_ref.remove(&client_id);
836 debug!("WebSocket sender task for client {} stopped", client_id);
837 });
838
839 self.clients.insert(client_id, client_info);
840 info!("Client {} registered from {}", client_id, remote_addr);
841 }
842
843 pub fn remove_client(&self, client_id: Uuid) {
845 if self.clients.remove(&client_id).is_some() {
846 info!("Client {} removed", client_id);
847 }
848 }
849
850 pub fn update_client_auth(&self, client_id: Uuid, auth_context: AuthContext) -> bool {
854 self.try_update_client_auth(client_id, auth_context)
855 .unwrap_or(false)
856 }
857
858 #[allow(clippy::result_large_err)]
867 pub fn try_update_client_auth(
868 &self,
869 client_id: Uuid,
870 auth_context: AuthContext,
871 ) -> Result<bool, AuthDeny> {
872 let Some(lifecycle) = self
873 .clients
874 .get(&client_id)
875 .map(|client| client.admission_lifecycle.clone())
876 else {
877 return Ok(false);
878 };
879 let _lifecycle = lifecycle.try_lock().map_err(|_| {
880 AuthDeny::new(
881 AuthErrorCode::InternalError,
882 "Subscription admission is busy",
883 )
884 })?;
885
886 let (subscriptions, current_permit) = {
887 let Some(client) = self.clients.get(&client_id) else {
888 return Ok(false);
889 };
890 if !Arc::ptr_eq(&client.admission_lifecycle, &lifecycle) {
891 return Ok(false);
892 }
893 (
894 client.subscriptions.clone(),
895 client.admission_permit.clone(),
896 )
897 };
898 let subscriptions = subscriptions.try_read().map_err(|_| {
899 AuthDeny::new(
900 AuthErrorCode::InternalError,
901 "Subscription admission is busy",
902 )
903 })?;
904 let active_subscriptions = subscriptions.keys().cloned().collect::<Vec<_>>();
905 drop(subscriptions);
906 let _refresh_admission = self.refresh_admission.try_lock().map_err(|_| {
909 AuthDeny::new(
910 AuthErrorCode::InternalError,
911 "Subscription admission is busy",
912 )
913 })?;
914 self.check_refresh_admission(client_id, &auth_context, active_subscriptions.len())?;
915
916 let permit = match &self.admission_provider {
917 Some(provider) => {
918 provider.refresh_connection(current_permit, &auth_context, &active_subscriptions)?
919 }
920 None => current_permit,
921 };
922
923 let Some(mut client) = self.clients.get_mut(&client_id) else {
924 return Ok(false);
925 };
926 if !Arc::ptr_eq(&client.admission_lifecycle, &lifecycle) {
927 return Ok(false);
928 }
929 client.admission_permit = permit;
930 client.auth_context = Some(auth_context);
931 debug!("Updated auth context for client {}", client_id);
932 Ok(true)
933 }
934
935 #[allow(clippy::result_large_err)]
942 fn check_refresh_admission(
943 &self,
944 client_id: Uuid,
945 auth_context: &AuthContext,
946 active_subscriptions: usize,
947 ) -> Result<(), AuthDeny> {
948 let publishable = auth_context.key_class == arete_auth::KeyClass::Publishable;
949 self.check_identity_limits(auth_context, Some(client_id))
950 .and_then(|()| {
951 let max_subs = auth_context.limits.max_subscriptions.or_else(|| {
952 self.rate_limit_config
953 .default_limits
954 .as_ref()
955 .and_then(|limits| limits.max_subscriptions)
956 });
957 match max_subs {
958 Some(max_subs) if active_subscriptions > max_subs as usize => {
959 Err(AuthDeny::new(
960 AuthErrorCode::SubscriptionLimitExceeded,
961 format!(
962 "Subscription limit exceeded: {} of {} subscriptions for client {}",
963 active_subscriptions, max_subs, client_id
964 ),
965 )
966 .with_suggested_action(
967 "Unsubscribe from views before refreshing to this token",
968 ))
969 }
970 _ => Ok(()),
971 }
972 })
973 .map_err(|deny| {
974 if publishable {
975 deny.for_publishable_session()
976 } else {
977 deny
978 }
979 })
980 }
981
982 pub async fn update_client_auth_async(
986 &self,
987 client_id: Uuid,
988 auth_context: AuthContext,
989 ) -> bool {
990 self.try_update_client_auth_async(client_id, auth_context)
991 .await
992 .unwrap_or(false)
993 }
994
995 #[allow(clippy::result_large_err)]
996 pub(crate) async fn try_update_client_auth_async(
997 &self,
998 client_id: Uuid,
999 auth_context: AuthContext,
1000 ) -> Result<bool, AuthDeny> {
1001 let Some(lifecycle) = self
1002 .clients
1003 .get(&client_id)
1004 .map(|client| client.admission_lifecycle.clone())
1005 else {
1006 return Ok(false);
1007 };
1008 let _lifecycle = lifecycle.lock().await;
1009
1010 let (subscriptions, current_permit) = {
1011 let Some(client) = self.clients.get(&client_id) else {
1012 return Ok(false);
1013 };
1014 if !Arc::ptr_eq(&client.admission_lifecycle, &lifecycle) {
1015 return Ok(false);
1016 }
1017 (
1018 client.subscriptions.clone(),
1019 client.admission_permit.clone(),
1020 )
1021 };
1022 let subscriptions = subscriptions.read().await;
1023 let active_subscriptions = subscriptions.keys().cloned().collect::<Vec<_>>();
1024 drop(subscriptions);
1025 let _refresh_admission = self.refresh_admission.lock().await;
1028 self.check_refresh_admission(client_id, &auth_context, active_subscriptions.len())?;
1029
1030 let permit = match &self.admission_provider {
1031 Some(provider) => {
1032 provider.refresh_connection(current_permit, &auth_context, &active_subscriptions)?
1033 }
1034 None => current_permit,
1035 };
1036
1037 let Some(mut client) = self.clients.get_mut(&client_id) else {
1038 return Ok(false);
1039 };
1040 if !Arc::ptr_eq(&client.admission_lifecycle, &lifecycle) {
1041 return Ok(false);
1042 }
1043 client.admission_permit = permit;
1044 client.auth_context = Some(auth_context);
1045 debug!("Updated auth context for client {}", client_id);
1046 Ok(true)
1047 }
1048
1049 pub fn check_and_remove_expired(&self, client_id: Uuid) -> bool {
1054 let now = std::time::SystemTime::now()
1055 .duration_since(std::time::UNIX_EPOCH)
1056 .unwrap_or_default()
1057 .as_secs();
1058 let Some((_, client)) = self.clients.remove_if(&client_id, |_, client| {
1062 client
1063 .auth_context
1064 .as_ref()
1065 .is_some_and(|ctx| ctx.expires_at <= now)
1066 }) else {
1067 return false;
1068 };
1069 if let Some(ctx) = &client.auth_context {
1070 warn!(
1071 "Client {} token expired (expired at {}), disconnecting",
1072 client_id, ctx.expires_at
1073 );
1074 }
1075 let _ = client.close_frame.set(CloseFrame {
1079 code: CloseCode::Policy,
1080 reason: format!(
1081 "{}: Authentication token expired",
1082 AuthErrorCode::TokenExpired.as_str()
1083 )
1084 .into(),
1085 });
1086 true
1087 }
1088
1089 pub fn client_count(&self) -> usize {
1094 self.clients.len()
1095 }
1096
1097 pub fn send_to_client(&self, client_id: Uuid, data: Arc<Bytes>) -> Result<(), SendError> {
1106 if self.check_and_remove_expired(client_id) {
1108 return Err(SendError::ClientDisconnected);
1109 }
1110
1111 self.enforce_egress_budgets(client_id, data.len())?;
1113
1114 let sender = {
1115 let client = self
1116 .clients
1117 .get(&client_id)
1118 .ok_or(SendError::ClientNotFound)?;
1119 client.sender.clone()
1120 };
1121
1122 let msg = Message::Binary((*data).clone());
1123 match sender.try_send(msg) {
1124 Ok(()) => Ok(()),
1125 Err(mpsc::error::TrySendError::Full(_)) => {
1126 warn!(
1127 "Client {} backpressured (queue full), disconnecting",
1128 client_id
1129 );
1130 self.clients.remove(&client_id);
1131 Err(SendError::ClientBackpressured)
1132 }
1133 Err(mpsc::error::TrySendError::Closed(_)) => {
1134 debug!("Client {} channel closed", client_id);
1135 self.clients.remove(&client_id);
1136 Err(SendError::ClientDisconnected)
1137 }
1138 }
1139 }
1140
1141 pub async fn send_to_client_async(
1150 &self,
1151 client_id: Uuid,
1152 data: Arc<Bytes>,
1153 ) -> Result<(), SendError> {
1154 if self.check_and_remove_expired(client_id) {
1156 return Err(SendError::ClientDisconnected);
1157 }
1158
1159 self.enforce_egress_budgets(client_id, data.len())?;
1161
1162 let sender = {
1163 let client = self
1164 .clients
1165 .get(&client_id)
1166 .ok_or(SendError::ClientNotFound)?;
1167 client.sender.clone()
1168 };
1169
1170 let msg = Message::Binary((*data).clone());
1171 sender
1172 .send(msg)
1173 .await
1174 .map_err(|_| SendError::ClientDisconnected)
1175 }
1176
1177 pub async fn send_text_to_client(
1182 &self,
1183 client_id: Uuid,
1184 text: String,
1185 ) -> Result<(), SendError> {
1186 if self.check_and_remove_expired(client_id) {
1188 return Err(SendError::ClientDisconnected);
1189 }
1190
1191 let sender = {
1192 let client = self
1193 .clients
1194 .get(&client_id)
1195 .ok_or(SendError::ClientNotFound)?;
1196 client.sender.clone()
1197 };
1198
1199 let msg = Message::Text(text.into());
1200 sender
1201 .send(msg)
1202 .await
1203 .map_err(|_| SendError::ClientDisconnected)
1204 }
1205
1206 pub async fn send_compressed_async(
1211 &self,
1212 client_id: Uuid,
1213 payload: CompressedPayload,
1214 ) -> Result<(), SendError> {
1215 if self.check_and_remove_expired(client_id) {
1217 return Err(SendError::ClientDisconnected);
1218 }
1219
1220 let (sender, bytes_to_record) = {
1221 let client = self
1222 .clients
1223 .get(&client_id)
1224 .ok_or(SendError::ClientNotFound)?;
1225
1226 let bytes = match &payload {
1227 CompressedPayload::Compressed(bytes) => bytes.len(),
1228 CompressedPayload::Uncompressed(bytes) => bytes.len(),
1229 };
1230
1231 (client.sender.clone(), bytes)
1232 };
1233
1234 self.enforce_egress_budgets(client_id, bytes_to_record)?;
1236
1237 let msg = match payload {
1238 CompressedPayload::Compressed(bytes) => Message::Binary(bytes),
1239 CompressedPayload::Uncompressed(bytes) => Message::Binary(bytes),
1240 };
1241 sender
1242 .send(msg)
1243 .await
1244 .map_err(|_| SendError::ClientDisconnected)
1245 }
1246
1247 pub fn update_client_last_seen(&self, client_id: Uuid) {
1249 if let Some(mut client) = self.clients.get_mut(&client_id) {
1250 client.update_last_seen();
1251 }
1252 }
1253
1254 #[allow(clippy::result_large_err)]
1256 pub fn check_inbound_message_allowed(&self, client_id: Uuid) -> Result<(), AuthDeny> {
1257 if self.check_and_remove_expired(client_id) {
1258 return Err(AuthDeny::new(
1259 crate::websocket::auth::AuthErrorCode::TokenExpired,
1260 "Authentication token expired",
1261 ));
1262 }
1263
1264 let (connection_ok, account_ok, permit_id) = {
1265 let Some(client) = self.clients.get(&client_id) else {
1266 return Err(AuthDeny::new(
1267 crate::websocket::auth::AuthErrorCode::InternalError,
1268 "Client not found",
1269 ));
1270 };
1271 let connection_ok = client.record_inbound_message().is_some();
1272 let account_ok = !connection_ok
1273 || client
1274 .auth_context
1275 .as_ref()
1276 .map(|ctx| self.record_account_message(ctx))
1277 .unwrap_or(true);
1278 let permit_id = client.admission_permit.as_ref().map(|permit| permit.id());
1279 (connection_ok, account_ok, permit_id)
1280 };
1281
1282 if connection_ok && account_ok {
1283 if let (Some(provider), Some(permit_id)) = (&self.admission_provider, permit_id) {
1284 provider.check_inbound_message(permit_id)?;
1285 }
1286 return Ok(());
1287 }
1288 let scope = if connection_ok {
1289 "inbound account websocket messages"
1290 } else {
1291 "inbound websocket messages"
1292 };
1293 Err(
1294 AuthDeny::rate_limited(self.rate_limit_config.message_rate_window, scope).with_context(
1295 format!("client {} exceeded the inbound message budget", client_id),
1296 ),
1297 )
1298 }
1299
1300 pub fn has_client(&self, client_id: Uuid) -> bool {
1302 self.clients.contains_key(&client_id)
1303 }
1304
1305 pub fn connection_token(&self, client_id: Uuid) -> CancellationToken {
1311 match self.clients.get(&client_id) {
1312 Some(client) => client.connection_token.clone(),
1313 None => {
1314 let token = CancellationToken::new();
1315 token.cancel();
1316 token
1317 }
1318 }
1319 }
1320
1321 pub async fn add_client_subscription(
1322 &self,
1323 client_id: Uuid,
1324 subscription_id: String,
1325 token: CancellationToken,
1326 ) -> bool {
1327 self.try_add_client_subscription(client_id, subscription_id, token)
1328 .await
1329 .unwrap_or(false)
1330 }
1331
1332 #[allow(clippy::result_large_err)]
1333 pub(crate) async fn try_add_client_subscription(
1334 &self,
1335 client_id: Uuid,
1336 subscription_id: String,
1337 token: CancellationToken,
1338 ) -> Result<bool, AuthDeny> {
1339 let Some(lifecycle) = self
1340 .clients
1341 .get(&client_id)
1342 .map(|client| client.admission_lifecycle.clone())
1343 else {
1344 return Ok(false);
1345 };
1346 let _lifecycle = lifecycle.lock().await;
1347 let Some((subscriptions, permit)) = self.clients.get(&client_id).and_then(|client| {
1348 Arc::ptr_eq(&client.admission_lifecycle, &lifecycle).then(|| {
1349 (
1350 client.subscriptions.clone(),
1351 client.admission_permit.clone(),
1352 )
1353 })
1354 }) else {
1355 return Ok(false);
1356 };
1357 let permit_id = permit.as_ref().map(|permit| permit.id());
1358 if let (Some(provider), Some(permit_id)) = (&self.admission_provider, permit_id) {
1359 if !provider.add_subscription(permit_id, &subscription_id)? {
1360 return Ok(false);
1361 }
1362 }
1363 let mut subscriptions = subscriptions.write().await;
1364 let client = self.clients.get(&client_id);
1365 let registered = client
1366 .as_ref()
1367 .is_some_and(|client| Arc::ptr_eq(&client.admission_lifecycle, &lifecycle));
1368 if !registered || subscriptions.contains_key(&subscription_id) {
1369 drop(client);
1370 drop(subscriptions);
1371 if let (Some(provider), Some(permit_id)) = (&self.admission_provider, permit_id) {
1372 provider.remove_subscription(permit_id, &subscription_id);
1373 }
1374 return Ok(false);
1375 }
1376 subscriptions.insert(subscription_id, token);
1377 drop(client);
1378 Ok(true)
1379 }
1380
1381 pub async fn remove_client_subscription(&self, client_id: Uuid, subscription_id: &str) -> bool {
1382 let Some(lifecycle) = self
1383 .clients
1384 .get(&client_id)
1385 .map(|client| client.admission_lifecycle.clone())
1386 else {
1387 return false;
1388 };
1389 let _lifecycle = lifecycle.lock().await;
1390 let Some((subscriptions, permit)) = self.clients.get(&client_id).and_then(|client| {
1391 Arc::ptr_eq(&client.admission_lifecycle, &lifecycle).then(|| {
1392 (
1393 client.subscriptions.clone(),
1394 client.admission_permit.clone(),
1395 )
1396 })
1397 }) else {
1398 return false;
1399 };
1400 let permit_id = permit.as_ref().map(|permit| permit.id());
1401 let mut subscriptions = subscriptions.write().await;
1402 let client = self.clients.get(&client_id);
1403 if !client
1404 .as_ref()
1405 .is_some_and(|client| Arc::ptr_eq(&client.admission_lifecycle, &lifecycle))
1406 {
1407 return false;
1408 }
1409 let token = subscriptions.remove(subscription_id);
1410 drop(client);
1411 drop(subscriptions);
1412 if let Some(token) = token {
1413 token.cancel();
1414 if let (Some(provider), Some(permit_id)) = (&self.admission_provider, permit_id) {
1415 provider.remove_subscription(permit_id, subscription_id);
1416 }
1417 true
1418 } else {
1419 false
1420 }
1421 }
1422
1423 pub async fn replace_client_subscription_token(
1428 &self,
1429 client_id: Uuid,
1430 subscription_id: &str,
1431 cancel_token: CancellationToken,
1432 ) -> bool {
1433 let Some(lifecycle) = self
1434 .clients
1435 .get(&client_id)
1436 .map(|client| client.admission_lifecycle.clone())
1437 else {
1438 return false;
1439 };
1440 let _lifecycle = lifecycle.lock().await;
1441 let Some(subscriptions) = self.clients.get(&client_id).and_then(|client| {
1442 Arc::ptr_eq(&client.admission_lifecycle, &lifecycle)
1443 .then(|| client.subscriptions.clone())
1444 }) else {
1445 return false;
1446 };
1447 let mut subscriptions = subscriptions.write().await;
1448 match subscriptions.get_mut(subscription_id) {
1449 Some(token) => {
1450 std::mem::replace(token, cancel_token).cancel();
1451 true
1452 }
1453 None => false,
1454 }
1455 }
1456
1457 pub async fn cancel_all_client_subscriptions(&self, client_id: Uuid) {
1458 let Some(lifecycle) = self
1459 .clients
1460 .get(&client_id)
1461 .map(|client| client.admission_lifecycle.clone())
1462 else {
1463 return;
1464 };
1465 let _lifecycle = lifecycle.lock().await;
1466 let Some(subscriptions) = self.clients.get(&client_id).and_then(|client| {
1467 Arc::ptr_eq(&client.admission_lifecycle, &lifecycle)
1468 .then(|| client.subscriptions.clone())
1469 }) else {
1470 return;
1471 };
1472 let subscriptions = subscriptions.read().await;
1473 for (subscription_id, token) in subscriptions.iter() {
1474 token.cancel();
1475 debug!("Cancelled subscription on disconnect: {}", subscription_id);
1476 }
1477 }
1478
1479 pub fn cleanup_stale_clients(&self) -> usize {
1481 let timeout = self.rate_limit_config.client_timeout;
1482 let mut stale_clients = Vec::new();
1483
1484 for entry in self.clients.iter() {
1485 if entry.value().is_stale(timeout) {
1486 stale_clients.push(*entry.key());
1487 }
1488 }
1489
1490 let removed_count = stale_clients.len();
1491 for client_id in stale_clients {
1492 self.clients.remove(&client_id);
1493 info!("Removed stale client {}", client_id);
1494 }
1495
1496 removed_count
1497 }
1498
1499 fn cleanup_account_state(&self) {
1502 let live_accounts: HashSet<String> = self
1503 .clients
1504 .iter()
1505 .filter_map(|entry| {
1506 entry
1507 .value()
1508 .auth_context
1509 .as_ref()
1510 .and_then(|ctx| ctx.account_key.clone())
1511 })
1512 .collect();
1513 self.account_usage.retain(|account, usage| {
1514 live_accounts.contains(account) || usage.idle_for() < ACCOUNT_USAGE_IDLE_TTL
1515 });
1516 self.account_policies
1517 .evict_idle(|account| live_accounts.contains(account));
1518 }
1519
1520 pub fn start_cleanup_task(&self) -> tokio::task::JoinHandle<()> {
1525 let client_manager = self.clone();
1526
1527 tokio::spawn(async move {
1528 let mut interval = tokio::time::interval(Duration::from_secs(30));
1529
1530 loop {
1531 interval.tick().await;
1532 let removed = client_manager.cleanup_stale_clients();
1533 if removed > 0 {
1534 info!("Cleaned up {} stale clients", removed);
1535 }
1536 client_manager.cleanup_account_state();
1537 if let Some(provider) = &client_manager.admission_provider {
1538 provider.cleanup();
1539 }
1540 }
1541 })
1542 }
1543
1544 #[allow(clippy::result_large_err)]
1555 pub async fn check_connection_allowed(
1556 &self,
1557 remote_addr: SocketAddr,
1558 auth_context: &Option<AuthContext>,
1559 ) -> Result<(), AuthDeny> {
1560 let publishable = auth_context
1561 .as_ref()
1562 .is_some_and(|ctx| ctx.key_class == arete_auth::KeyClass::Publishable);
1563 self.check_connection_limits(remote_addr, auth_context)
1564 .await
1565 .map_err(|deny| {
1566 if publishable {
1567 deny.for_publishable_session()
1568 } else {
1569 deny
1570 }
1571 })
1572 }
1573
1574 #[allow(clippy::result_large_err)]
1575 async fn check_connection_limits(
1576 &self,
1577 remote_addr: SocketAddr,
1578 auth_context: &Option<AuthContext>,
1579 ) -> Result<(), AuthDeny> {
1580 if let Some(ref rate_limiter) = self.rate_limiter {
1582 match rate_limiter.check_handshake(remote_addr).await {
1584 RateLimitResult::Allowed { .. } => {}
1585 RateLimitResult::Denied { retry_after, limit } => {
1586 return Err(AuthDeny::rate_limited(retry_after, "websocket handshakes")
1587 .with_context(format!(
1588 "handshake rate limit of {} per minute exceeded for {}",
1589 limit, remote_addr
1590 )));
1591 }
1592 }
1593
1594 if let Some(ref ctx) = auth_context {
1595 match rate_limiter
1599 .check_connection_for_consumer(
1600 ctx.consumer_key(),
1601 ctx.limits.max_connection_attempts_per_minute,
1602 )
1603 .await
1604 {
1605 RateLimitResult::Allowed { .. } => {}
1606 RateLimitResult::Denied { retry_after, limit } => {
1607 return Err(AuthDeny::rate_limited(retry_after, "websocket connections")
1608 .with_context(format!(
1609 "connection rate limit for consumer {} of {} per minute exceeded",
1610 ctx.consumer_key(),
1611 limit
1612 )));
1613 }
1614 }
1615
1616 if ctx.is_legacy_policy() || ctx.account_key.is_some() {
1620 match rate_limiter
1621 .check_connection_for_account(
1622 ctx.account_key(),
1623 ctx.account_limits.max_connection_attempts_per_minute,
1624 )
1625 .await
1626 {
1627 RateLimitResult::Allowed { .. } => {}
1628 RateLimitResult::Denied { retry_after, limit } => {
1629 return Err(AuthDeny::rate_limited(
1630 retry_after,
1631 "metered websocket connections",
1632 )
1633 .with_context(format!(
1634 "connection rate limit for account {} of {} per minute exceeded",
1635 ctx.account_key(),
1636 limit
1637 )));
1638 }
1639 }
1640 }
1641 }
1642 }
1643
1644 if let Some(max_per_ip) = self.rate_limit_config.max_connections_per_ip {
1646 let current_ip_connections = self.count_connections_for_ip(&remote_addr);
1647 if current_ip_connections >= max_per_ip {
1648 return Err(AuthDeny::connection_limit_exceeded(
1649 &format!("ip {}", remote_addr.ip()),
1650 current_ip_connections,
1651 max_per_ip,
1652 ));
1653 }
1654 }
1655
1656 if let Some(ctx) = auth_context {
1657 self.check_identity_limits(ctx, None)?;
1658 }
1659 Ok(())
1660 }
1661
1662 #[allow(clippy::result_large_err)]
1672 fn check_identity_limits(
1673 &self,
1674 ctx: &AuthContext,
1675 refreshing: Option<Uuid>,
1676 ) -> Result<(), AuthDeny> {
1677 if ctx.is_legacy_policy() {
1680 let legacy_policy_token = self.account_policies.record_legacy_token();
1681 debug!(legacy_policy_token, "legacy policy token admitted");
1682 } else if let (Some(account), Some(policy_version)) =
1683 (ctx.account_key.as_deref(), ctx.policy_version)
1684 {
1685 match self
1686 .account_policies
1687 .observe(account, policy_version, &ctx.account_limits)
1688 {
1689 Ok(()) => {}
1690 Err(AccountPolicyError::StaleVersion { presented, current }) => {
1691 debug!(
1692 account = %redact_identity(account),
1693 presented,
1694 current,
1695 "stale policy version rejected"
1696 );
1697 return Err(AuthDeny::new(
1698 crate::websocket::auth::AuthErrorCode::TokenExpired,
1699 "session policy version is stale; refresh the session token",
1700 ));
1701 }
1702 Err(AccountPolicyError::ConflictingLimits { version }) => {
1703 warn!(
1704 account = %redact_identity(account),
1705 version,
1706 "signed account limits conflict for one policy version"
1707 );
1708 return Err(AuthDeny::new(
1709 crate::websocket::auth::AuthErrorCode::InternalError,
1710 "signed account limits conflict with previously observed policy",
1711 ));
1712 }
1713 Err(AccountPolicyError::CapacityExhausted) => {
1714 warn!("account policy state at capacity; denying admission");
1715 return Err(AuthDeny::new(
1716 crate::websocket::auth::AuthErrorCode::InternalError,
1717 "account policy state is at capacity; retry shortly",
1718 ));
1719 }
1720 }
1721 }
1722
1723 let max_connections = ctx.limits.max_connections.or_else(|| {
1727 self.rate_limit_config
1728 .default_limits
1729 .as_ref()
1730 .and_then(|l| l.max_connections)
1731 });
1732 if let Some(max_connections) = max_connections {
1733 let current_connections =
1734 self.count_connections_for_consumer(ctx.consumer_key(), refreshing);
1735 if current_connections >= max_connections as usize {
1736 return Err(AuthDeny::connection_limit_exceeded(
1737 &format!("consumer {}", ctx.consumer_key()),
1738 current_connections,
1739 max_connections as usize,
1740 ));
1741 }
1742 }
1743
1744 if !ctx.is_legacy_policy() && ctx.account_key.is_some() {
1747 if let Some(max_account_connections) = ctx.account_limits.max_connections {
1748 let current_account_connections =
1749 self.count_connections_for_account(ctx.account_key(), refreshing);
1750 if current_account_connections >= max_account_connections as usize {
1751 return Err(AuthDeny::connection_limit_exceeded(
1752 &format!("account {}", ctx.account_key()),
1753 current_account_connections,
1754 max_account_connections as usize,
1755 ));
1756 }
1757 }
1758 }
1759
1760 if let Some(max_per_metering_key) = self.rate_limit_config.max_connections_per_metering_key
1762 {
1763 let current_metering_connections =
1764 self.count_connections_for_metering_key(&ctx.metering_key, refreshing);
1765 if current_metering_connections >= max_per_metering_key {
1766 return Err(AuthDeny::connection_limit_exceeded(
1767 &format!("metering key {}", ctx.metering_key),
1768 current_metering_connections,
1769 max_per_metering_key,
1770 ));
1771 }
1772 }
1773
1774 if let Some(max_per_origin) = self.rate_limit_config.max_connections_per_origin {
1776 if let Some(ref origin) = ctx.origin {
1777 let current_origin_connections =
1778 self.count_connections_for_origin(origin, refreshing);
1779 if current_origin_connections >= max_per_origin {
1780 return Err(AuthDeny::connection_limit_exceeded(
1781 &format!("origin {}", origin),
1782 current_origin_connections,
1783 max_per_origin,
1784 ));
1785 }
1786 }
1787 }
1788 Ok(())
1789 }
1790
1791 fn count_connections_for_ip(&self, remote_addr: &SocketAddr) -> usize {
1793 let ip = remote_addr.ip();
1794 self.clients
1795 .iter()
1796 .filter(|entry| entry.value().remote_addr.ip() == ip)
1797 .count()
1798 }
1799
1800 fn count_connections_for_consumer(&self, consumer: &str, exclude: Option<Uuid>) -> usize {
1802 self.clients
1803 .iter()
1804 .filter(|entry| Some(*entry.key()) != exclude)
1805 .filter(|entry| {
1806 entry
1807 .value()
1808 .auth_context
1809 .as_ref()
1810 .map(|ctx| ctx.consumer_key() == consumer)
1811 .unwrap_or(false)
1812 })
1813 .count()
1814 }
1815
1816 fn count_connections_for_account(&self, account: &str, exclude: Option<Uuid>) -> usize {
1818 self.clients
1819 .iter()
1820 .filter(|entry| Some(*entry.key()) != exclude)
1821 .filter(|entry| {
1822 entry
1823 .value()
1824 .auth_context
1825 .as_ref()
1826 .map(|ctx| ctx.account_key() == account)
1827 .unwrap_or(false)
1828 })
1829 .count()
1830 }
1831
1832 fn count_connections_for_metering_key(
1834 &self,
1835 metering_key: &str,
1836 exclude: Option<Uuid>,
1837 ) -> usize {
1838 self.clients
1839 .iter()
1840 .filter(|entry| Some(*entry.key()) != exclude)
1841 .filter(|entry| {
1842 entry
1843 .value()
1844 .auth_context
1845 .as_ref()
1846 .map(|ctx| ctx.metering_key == metering_key)
1847 .unwrap_or(false)
1848 })
1849 .count()
1850 }
1851
1852 fn count_connections_for_origin(&self, origin: &str, exclude: Option<Uuid>) -> usize {
1854 self.clients
1855 .iter()
1856 .filter(|entry| Some(*entry.key()) != exclude)
1857 .filter(|entry| {
1858 entry
1859 .value()
1860 .auth_context
1861 .as_ref()
1862 .and_then(|ctx| ctx.origin.as_ref())
1863 .map(|o| o == origin)
1864 .unwrap_or(false)
1865 })
1866 .count()
1867 }
1868
1869 #[allow(clippy::result_large_err)]
1873 pub async fn check_subscription_allowed(&self, client_id: Uuid) -> Result<(), AuthDeny> {
1874 let context = {
1875 let Some(client) = self.clients.get(&client_id) else {
1876 return Ok(());
1877 };
1878 let current_subs = client.subscription_count().await;
1879
1880 if let Some(ref ctx) = client.auth_context {
1882 let max_subs = ctx.limits.max_subscriptions.or_else(|| {
1883 self.rate_limit_config
1884 .default_limits
1885 .as_ref()
1886 .and_then(|l| l.max_subscriptions)
1887 });
1888 if let Some(max_subs) = max_subs {
1889 if current_subs >= max_subs as usize {
1890 return Err(AuthDeny::new(
1891 crate::websocket::auth::AuthErrorCode::SubscriptionLimitExceeded,
1892 format!(
1893 "Subscription limit exceeded: {} of {} subscriptions for client {}",
1894 current_subs, max_subs, client_id
1895 ),
1896 )
1897 .with_suggested_action(
1898 "Unsubscribe from an existing view before creating another subscription",
1899 ));
1900 }
1901 }
1902 }
1903 client.auth_context.clone()
1904 };
1905
1906 if let (Some(rate_limiter), Some(ctx)) = (self.rate_limiter.as_ref(), context.as_ref()) {
1909 if !ctx.is_legacy_policy() {
1910 if let RateLimitResult::Denied { retry_after, limit } = rate_limiter
1911 .check_subscription_create_for_consumer(
1912 ctx.consumer_key(),
1913 ctx.limits.max_subscription_creates_per_minute,
1914 )
1915 .await
1916 {
1917 return Err(AuthDeny::rate_limited(retry_after, "subscription creates")
1918 .with_context(format!(
1919 "subscription-create rate limit for consumer {} of {} per minute exceeded",
1920 ctx.consumer_key(),
1921 limit
1922 )));
1923 }
1924
1925 if ctx.account_key.is_some() {
1926 if let RateLimitResult::Denied { retry_after, limit } = rate_limiter
1927 .check_subscription_create_for_account(
1928 ctx.account_key(),
1929 ctx.account_limits.max_subscription_creates_per_minute,
1930 )
1931 .await
1932 {
1933 return Err(AuthDeny::rate_limited(
1934 retry_after,
1935 "account subscription creates",
1936 )
1937 .with_context(format!(
1938 "subscription-create rate limit for account {} of {} per minute exceeded",
1939 ctx.account_key(),
1940 limit
1941 )));
1942 }
1943 }
1944 }
1945 }
1946 Ok(())
1947 }
1948
1949 pub fn get_metering_key(&self, client_id: Uuid) -> Option<String> {
1951 self.clients.get(&client_id).and_then(|client| {
1952 client
1953 .auth_context
1954 .as_ref()
1955 .map(|ctx| ctx.metering_key.clone())
1956 })
1957 }
1958
1959 pub fn get_auth_context(&self, client_id: Uuid) -> Option<AuthContext> {
1961 self.clients
1962 .get(&client_id)
1963 .and_then(|client| client.auth_context.clone())
1964 }
1965
1966 #[allow(clippy::result_large_err)]
1970 pub fn check_snapshot_allowed(
1971 &self,
1972 client_id: Uuid,
1973 requested_rows: u32,
1974 ) -> Result<(), AuthDeny> {
1975 if let Some(client) = self.clients.get(&client_id) {
1976 if let Some(ref ctx) = client.auth_context {
1977 let max_rows = ctx.limits.max_snapshot_rows.or_else(|| {
1978 self.rate_limit_config
1979 .default_limits
1980 .as_ref()
1981 .and_then(|l| l.max_snapshot_rows)
1982 });
1983 if let Some(max_rows) = max_rows {
1984 if requested_rows > max_rows {
1985 return Err(AuthDeny::new(
1986 crate::websocket::auth::AuthErrorCode::SnapshotLimitExceeded,
1987 format!(
1988 "Snapshot limit exceeded: requested {} rows, max allowed is {} for client {}",
1989 requested_rows, max_rows, client_id
1990 ),
1991 )
1992 .with_suggested_action(
1993 "Request fewer rows or lower the snapshotLimit on the subscription",
1994 ));
1995 }
1996 }
1997 }
1998 }
1999 Ok(())
2000 }
2001}
2002
2003impl Default for ClientManager {
2004 fn default() -> Self {
2005 Self::new()
2006 }
2007}
2008
2009#[cfg(test)]
2010#[path = "admission_tests.rs"]
2011mod admission_tests;
2012
2013#[cfg(test)]
2014mod tests {
2015 use super::*;
2016 use crate::websocket::auth::AuthContext;
2017 use arete_auth::{KeyClass, Limits};
2018 use std::net::{IpAddr, Ipv4Addr, SocketAddr};
2019
2020 fn create_test_auth_context(subject: &str, limits: Limits) -> AuthContext {
2021 AuthContext {
2022 subject: subject.to_string(),
2023 issuer: "test-issuer".to_string(),
2024 audience: "test-audience".to_string(),
2025 key_class: KeyClass::Publishable,
2026 metering_key: format!("meter-{}", subject),
2027 deployment_id: None,
2028 target_kind: None,
2029 target_id: None,
2030 program_id: None,
2031 program_release_hash: None,
2032 expires_at: u64::MAX,
2033 scope: "read".to_string(),
2034 limits,
2035 plan: None,
2036 origin: None,
2037 client_ip: None,
2038 jti: uuid::Uuid::new_v4().to_string(),
2039 actor_key: None,
2040 account_key: None,
2041 consumer_key: None,
2042 policy_version: None,
2043 account_limits: Limits::default(),
2044 row_scopes: None,
2045 }
2046 }
2047
2048 fn create_v2_auth_context(
2049 consumer: &str,
2050 account: &str,
2051 policy_version: u32,
2052 limits: Limits,
2053 account_limits: Limits,
2054 ) -> AuthContext {
2055 AuthContext {
2056 subject: "user:1".to_string(),
2057 issuer: "test-issuer".to_string(),
2058 audience: "test-audience".to_string(),
2059 key_class: KeyClass::Publishable,
2060 metering_key: account.to_string(),
2061 deployment_id: None,
2062 target_kind: None,
2063 target_id: None,
2064 program_id: None,
2065 program_release_hash: None,
2066 expires_at: u64::MAX,
2067 scope: "read".to_string(),
2068 limits,
2069 plan: Some("pro".to_string()),
2070 origin: None,
2071 client_ip: None,
2072 jti: uuid::Uuid::new_v4().to_string(),
2073 actor_key: Some("user:1".to_string()),
2074 account_key: Some(account.to_string()),
2075 consumer_key: Some(consumer.to_string()),
2076 policy_version: Some(policy_version),
2077 account_limits,
2078 row_scopes: None,
2079 }
2080 }
2081
2082 fn insert_client(manager: &ClientManager, context: AuthContext) -> Uuid {
2083 let (sender, receiver) = mpsc::channel(8);
2084 std::mem::forget(receiver);
2086 let client_id = Uuid::new_v4();
2087 manager.clients.insert(
2088 client_id,
2089 ClientInfo::new(
2090 client_id,
2091 sender,
2092 Some(context),
2093 create_test_socket_addr("127.0.0.1"),
2094 ),
2095 );
2096 client_id
2097 }
2098
2099 fn create_test_socket_addr(ip: &str) -> SocketAddr {
2100 SocketAddr::new(
2101 ip.parse::<IpAddr>()
2102 .unwrap_or(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))),
2103 12345,
2104 )
2105 }
2106
2107 #[test]
2108 fn test_egress_tracker_basic() {
2109 let mut tracker = EgressTracker::new();
2110
2111 assert!(tracker.record_bytes(500, 1000));
2113 assert_eq!(tracker.current_usage(), 500);
2114
2115 assert!(tracker.record_bytes(400, 1000));
2117 assert_eq!(tracker.current_usage(), 900);
2118
2119 assert!(!tracker.record_bytes(200, 1000));
2121 assert_eq!(tracker.current_usage(), 900); }
2123
2124 #[test]
2125 fn test_egress_tracker_window_reset() {
2126 let mut tracker = EgressTracker::new();
2127
2128 assert!(tracker.record_bytes(100, 100));
2130 assert!(!tracker.record_bytes(1, 100));
2131
2132 tracker.bytes_this_minute = 0;
2134 tracker.window_start = SystemTime::now() - Duration::from_secs(61);
2135
2136 assert!(tracker.record_bytes(50, 100));
2138 }
2139
2140 #[test]
2141 fn test_message_rate_tracker_basic() {
2142 let mut tracker = MessageRateTracker::new();
2143
2144 assert!(tracker.record_message(2));
2145 assert_eq!(tracker.current_usage(), 1);
2146
2147 assert!(tracker.record_message(2));
2148 assert_eq!(tracker.current_usage(), 2);
2149
2150 assert!(!tracker.record_message(2));
2151 assert_eq!(tracker.current_usage(), 2);
2152 }
2153
2154 #[tokio::test]
2155 async fn test_client_inbound_message_limit() {
2156 let (tx, _rx) = mpsc::channel(1);
2157 let client = ClientInfo::new(
2158 Uuid::new_v4(),
2159 tx,
2160 Some(create_test_auth_context(
2161 "user-1",
2162 Limits {
2163 max_messages_per_minute: Some(2),
2164 ..Default::default()
2165 },
2166 )),
2167 create_test_socket_addr("127.0.0.1"),
2168 );
2169
2170 assert_eq!(client.record_inbound_message(), Some(1));
2171 assert_eq!(client.record_inbound_message(), Some(2));
2172 assert_eq!(client.record_inbound_message(), None);
2173 }
2174
2175 #[tokio::test]
2176 async fn duplicate_subscription_id_is_rejected_without_replacement() {
2177 let (tx, _rx) = mpsc::channel(1);
2178 let client = ClientInfo::new(
2179 Uuid::new_v4(),
2180 tx,
2181 None,
2182 create_test_socket_addr("127.0.0.1"),
2183 );
2184 let first = CancellationToken::new();
2185 let duplicate = CancellationToken::new();
2186
2187 assert!(
2188 client
2189 .add_subscription("opaque-id".to_string(), first.clone())
2190 .await
2191 );
2192 assert!(
2193 !client
2194 .add_subscription("opaque-id".to_string(), duplicate.clone())
2195 .await
2196 );
2197 assert!(!first.is_cancelled());
2198 assert!(!duplicate.is_cancelled());
2199
2200 assert!(client.remove_subscription("opaque-id").await);
2201 assert!(first.is_cancelled());
2202 assert!(!duplicate.is_cancelled());
2203 }
2204
2205 #[tokio::test]
2206 async fn test_no_limits() {
2207 let manager = ClientManager::new();
2208 let addr = create_test_socket_addr("127.0.0.1");
2209
2210 assert!(manager.check_connection_allowed(addr, &None).await.is_ok());
2212
2213 let auth_context = create_test_auth_context("test", Limits::default());
2215 assert!(manager
2216 .check_connection_allowed(addr, &Some(auth_context))
2217 .await
2218 .is_ok());
2219 }
2220
2221 #[tokio::test]
2222 async fn test_per_subject_connection_limit() {
2223 let manager = ClientManager::new();
2224
2225 let limits = Limits {
2226 max_connections: Some(2),
2227 ..Default::default()
2228 };
2229
2230 let auth_context = create_test_auth_context("user-1", limits);
2231 let addr = create_test_socket_addr("127.0.0.1");
2232
2233 assert!(manager
2235 .check_connection_allowed(addr, &Some(auth_context.clone()))
2236 .await
2237 .is_ok());
2238 }
2239
2240 #[tokio::test]
2241 async fn test_per_ip_connection_limit() {
2242 let manager = ClientManager::new().with_max_connections_per_ip(2);
2243 let addr = create_test_socket_addr("192.168.1.1");
2244
2245 assert!(manager.check_connection_allowed(addr, &None).await.is_ok());
2247 }
2248
2249 #[test]
2251 fn rate_limit_config_default() {
2252 let config = RateLimitConfig::default();
2253 assert!(config.max_connections_per_ip.is_none());
2254 assert_eq!(config.client_timeout, Duration::from_secs(300));
2255 assert_eq!(config.message_queue_size, 512);
2256 assert!(config.max_reconnect_attempts.is_none());
2257 assert_eq!(config.message_rate_window, Duration::from_secs(60));
2258 assert_eq!(config.egress_rate_window, Duration::from_secs(60));
2259 }
2260
2261 #[test]
2262 fn rate_limit_config_builder_methods() {
2263 let config = RateLimitConfig::default()
2264 .with_max_connections_per_ip(10)
2265 .with_timeout(Duration::from_secs(600))
2266 .with_message_queue_size(1024)
2267 .with_rate_limit_window(Duration::from_secs(120));
2268
2269 assert_eq!(config.max_connections_per_ip, Some(10));
2270 assert_eq!(config.client_timeout, Duration::from_secs(600));
2271 assert_eq!(config.message_queue_size, 1024);
2272 assert_eq!(config.message_rate_window, Duration::from_secs(120));
2273 assert_eq!(config.egress_rate_window, Duration::from_secs(120));
2274 }
2275
2276 #[tokio::test]
2277 async fn client_manager_with_config() {
2278 let config = RateLimitConfig::default()
2279 .with_max_connections_per_ip(5)
2280 .with_timeout(Duration::from_secs(120))
2281 .with_message_queue_size(256);
2282
2283 let manager = ClientManager::with_config(config);
2284 let addr = create_test_socket_addr("10.0.0.1");
2285
2286 assert_eq!(manager.rate_limit_config().max_connections_per_ip, Some(5));
2288 assert_eq!(
2289 manager.rate_limit_config().client_timeout,
2290 Duration::from_secs(120)
2291 );
2292 assert_eq!(manager.rate_limit_config().message_queue_size, 256);
2293
2294 assert!(manager.check_connection_allowed(addr, &None).await.is_ok());
2296 }
2297
2298 #[tokio::test]
2299 async fn client_manager_builder_pattern() {
2300 let manager = ClientManager::new()
2301 .with_max_connections_per_ip(10)
2302 .with_timeout(Duration::from_secs(180))
2303 .with_message_queue_size(1024)
2304 .with_rate_limit_window(Duration::from_secs(90));
2305
2306 assert_eq!(manager.rate_limit_config().max_connections_per_ip, Some(10));
2307 assert_eq!(
2308 manager.rate_limit_config().client_timeout,
2309 Duration::from_secs(180)
2310 );
2311 assert_eq!(manager.rate_limit_config().message_queue_size, 1024);
2312 assert_eq!(
2313 manager.rate_limit_config().message_rate_window,
2314 Duration::from_secs(90)
2315 );
2316 }
2317
2318 #[tokio::test]
2320 async fn connection_limit_enforcement_with_actual_clients() {
2321 let manager = ClientManager::new().with_max_connections_per_ip(2);
2322 let addr1 = create_test_socket_addr("192.168.1.1");
2323 let addr2 = create_test_socket_addr("192.168.1.2");
2324
2325 let auth1 = create_test_auth_context("user-1", Limits::default());
2327 assert!(manager
2328 .check_connection_allowed(addr1, &Some(auth1.clone()))
2329 .await
2330 .is_ok());
2331
2332 let auth2 = create_test_auth_context("user-2", Limits::default());
2337 assert!(manager
2338 .check_connection_allowed(addr1, &Some(auth2.clone()))
2339 .await
2340 .is_ok());
2341
2342 let auth3 = create_test_auth_context("user-3", Limits::default());
2344 assert!(manager
2345 .check_connection_allowed(addr2, &Some(auth3.clone()))
2346 .await
2347 .is_ok());
2348 }
2349
2350 #[tokio::test]
2352 async fn subscription_limit_enforcement() {
2353 let manager = ClientManager::new();
2354 let addr = create_test_socket_addr("127.0.0.1");
2355
2356 let auth = create_test_auth_context(
2358 "user-1",
2359 Limits {
2360 max_subscriptions: Some(2),
2361 ..Default::default()
2362 },
2363 );
2364
2365 assert!(manager
2367 .check_connection_allowed(addr, &Some(auth.clone()))
2368 .await
2369 .is_ok());
2370
2371 assert_eq!(auth.limits.max_subscriptions, Some(2));
2374 }
2375
2376 #[tokio::test]
2378 async fn snapshot_limit_enforcement() {
2379 let manager = ClientManager::new();
2380 let addr = create_test_socket_addr("127.0.0.1");
2381
2382 let auth = create_test_auth_context(
2383 "user-1",
2384 Limits {
2385 max_snapshot_rows: Some(1000),
2386 ..Default::default()
2387 },
2388 );
2389
2390 assert!(manager
2391 .check_connection_allowed(addr, &Some(auth.clone()))
2392 .await
2393 .is_ok());
2394
2395 }
2398
2399 #[tokio::test]
2400 async fn two_consumers_share_the_account_connection_cap() {
2401 let manager = ClientManager::new();
2402 let account_limits = Limits {
2403 max_connections: Some(1),
2404 ..Limits::default()
2405 };
2406 let consumer_a = create_v2_auth_context(
2407 "consumer:a",
2408 "account:42",
2409 1,
2410 Limits::default(),
2411 account_limits.clone(),
2412 );
2413 insert_client(&manager, consumer_a);
2414
2415 let consumer_b = create_v2_auth_context(
2418 "consumer:b",
2419 "account:42",
2420 1,
2421 Limits::default(),
2422 account_limits.clone(),
2423 );
2424 let addr = create_test_socket_addr("127.0.0.1");
2425 assert!(manager
2426 .check_connection_allowed(addr, &Some(consumer_b))
2427 .await
2428 .is_err());
2429
2430 let other_account = create_v2_auth_context(
2432 "consumer:c",
2433 "account:43",
2434 1,
2435 Limits::default(),
2436 account_limits,
2437 );
2438 assert!(manager
2439 .check_connection_allowed(addr, &Some(other_account))
2440 .await
2441 .is_ok());
2442 }
2443
2444 #[tokio::test]
2445 async fn publishable_sessions_see_a_fixed_connection_limit_reason() {
2446 let manager = ClientManager::new();
2447 let account_limits = Limits {
2448 max_connections: Some(1),
2449 ..Limits::default()
2450 };
2451 insert_client(
2452 &manager,
2453 create_v2_auth_context(
2454 "consumer:a",
2455 "account:42",
2456 1,
2457 Limits::default(),
2458 account_limits.clone(),
2459 ),
2460 );
2461 let addr = create_test_socket_addr("127.0.0.1");
2462
2463 let visitor = create_v2_auth_context(
2464 "consumer:b",
2465 "account:42",
2466 1,
2467 Limits::default(),
2468 account_limits.clone(),
2469 );
2470 let deny = manager
2471 .check_connection_allowed(addr, &Some(visitor))
2472 .await
2473 .unwrap_err();
2474 assert_eq!(deny.code, AuthErrorCode::ConnectionLimitExceeded);
2475 let response = serde_json::to_string(&deny.to_error_response()).unwrap();
2476 assert_eq!(deny.reason, "Connection limit reached");
2477 for leaked in ["account:42", "consumer:b", "1 of 1"] {
2478 assert!(!response.contains(leaked), "{leaked} in {response}");
2479 }
2480
2481 let mut server = create_v2_auth_context(
2483 "consumer:c",
2484 "account:42",
2485 1,
2486 Limits::default(),
2487 account_limits,
2488 );
2489 server.key_class = KeyClass::Secret;
2490 let deny = manager
2491 .check_connection_allowed(addr, &Some(server))
2492 .await
2493 .unwrap_err();
2494 assert!(
2495 deny.reason.contains("account account:42"),
2496 "{}",
2497 deny.reason
2498 );
2499 }
2500
2501 #[tokio::test]
2502 async fn one_consumer_is_limited_independently_of_its_account() {
2503 let manager = ClientManager::new();
2504 let limits = Limits {
2505 max_connections: Some(1),
2506 ..Limits::default()
2507 };
2508 let account_limits = Limits {
2509 max_connections: Some(10),
2510 ..Limits::default()
2511 };
2512 let consumer_a = create_v2_auth_context(
2513 "consumer:a",
2514 "account:42",
2515 1,
2516 limits.clone(),
2517 account_limits.clone(),
2518 );
2519 insert_client(&manager, consumer_a.clone());
2520
2521 let addr = create_test_socket_addr("127.0.0.1");
2522 assert!(manager
2524 .check_connection_allowed(addr, &Some(consumer_a))
2525 .await
2526 .is_err());
2527
2528 let consumer_b =
2530 create_v2_auth_context("consumer:b", "account:42", 1, limits, account_limits);
2531 assert!(manager
2532 .check_connection_allowed(addr, &Some(consumer_b))
2533 .await
2534 .is_ok());
2535 }
2536
2537 #[tokio::test]
2538 async fn policy_version_upgrade_stale_and_conflict_rules_apply() {
2539 let manager = ClientManager::new();
2540 let addr = create_test_socket_addr("127.0.0.1");
2541 let limits_v1 = Limits {
2542 max_connections: Some(5),
2543 ..Limits::default()
2544 };
2545 let limits_v2 = Limits {
2546 max_connections: Some(2),
2547 ..Limits::default()
2548 };
2549
2550 let v1 = create_v2_auth_context(
2552 "consumer:a",
2553 "account:42",
2554 1,
2555 Limits::default(),
2556 limits_v1.clone(),
2557 );
2558 assert!(manager
2559 .check_connection_allowed(addr, &Some(v1.clone()))
2560 .await
2561 .is_ok());
2562
2563 let v2 = create_v2_auth_context(
2565 "consumer:a",
2566 "account:42",
2567 2,
2568 Limits::default(),
2569 limits_v2.clone(),
2570 );
2571 assert!(manager
2572 .check_connection_allowed(addr, &Some(v2))
2573 .await
2574 .is_ok());
2575
2576 let stale = manager.check_connection_allowed(addr, &Some(v1)).await;
2578 assert!(stale.is_err());
2579
2580 let conflict =
2582 create_v2_auth_context("consumer:a", "account:42", 2, Limits::default(), limits_v1);
2583 assert!(manager
2584 .check_connection_allowed(addr, &Some(conflict))
2585 .await
2586 .is_err());
2587 }
2588
2589 #[tokio::test]
2590 async fn legacy_tokens_keep_old_behavior_and_are_counted() {
2591 let manager = ClientManager::new();
2592 let addr = create_test_socket_addr("127.0.0.1");
2593 let legacy = create_test_auth_context(
2594 "user-1",
2595 Limits {
2596 max_connections: Some(2),
2597 ..Limits::default()
2598 },
2599 );
2600 assert!(legacy.is_legacy_policy());
2601
2602 assert!(manager
2603 .check_connection_allowed(addr, &Some(legacy.clone()))
2604 .await
2605 .is_ok());
2606 assert_eq!(manager.account_policies().legacy_token_count(), 1);
2607 assert_eq!(manager.account_policies().tracked_accounts(), 0);
2609
2610 insert_client(&manager, legacy.clone());
2612 insert_client(&manager, legacy.clone());
2613 assert!(manager
2614 .check_connection_allowed(addr, &Some(legacy))
2615 .await
2616 .is_err());
2617 }
2618
2619 #[tokio::test]
2620 async fn account_message_budget_is_shared_across_consumers() {
2621 let manager = ClientManager::new();
2622 let account_limits = Limits {
2623 max_messages_per_minute: Some(2),
2624 ..Limits::default()
2625 };
2626 let client_a = insert_client(
2627 &manager,
2628 create_v2_auth_context(
2629 "consumer:a",
2630 "account:42",
2631 1,
2632 Limits::default(),
2633 account_limits.clone(),
2634 ),
2635 );
2636 let client_b = insert_client(
2637 &manager,
2638 create_v2_auth_context(
2639 "consumer:b",
2640 "account:42",
2641 1,
2642 Limits::default(),
2643 account_limits,
2644 ),
2645 );
2646
2647 assert!(manager.check_inbound_message_allowed(client_a).is_ok());
2648 assert!(manager.check_inbound_message_allowed(client_b).is_ok());
2649 assert!(manager.check_inbound_message_allowed(client_a).is_err());
2651 }
2652
2653 #[tokio::test]
2654 async fn account_state_evicts_when_idle_and_unreferenced() {
2655 let manager = ClientManager::new();
2656 let addr = create_test_socket_addr("127.0.0.1");
2657 let context = create_v2_auth_context(
2658 "consumer:a",
2659 "account:42",
2660 1,
2661 Limits::default(),
2662 Limits::default(),
2663 );
2664 assert!(manager
2665 .check_connection_allowed(addr, &Some(context))
2666 .await
2667 .is_ok());
2668 assert_eq!(manager.account_policies().tracked_accounts(), 1);
2669
2670 manager.account_policies().evict_idle(|_| false);
2673 assert_eq!(
2674 manager.account_policies().tracked_accounts(),
2675 1,
2676 "TTL keeps fresh entries"
2677 );
2678 manager.cleanup_account_state();
2679 assert_eq!(
2680 manager.account_policies().tracked_accounts(),
2681 1,
2682 "fresh entries survive sweep"
2683 );
2684 }
2685
2686 #[test]
2687 fn expired_token_is_removed_without_deadlocking() {
2688 let manager = ClientManager::new();
2689 let mut context = create_test_auth_context("user-1", Limits::default());
2690 context.expires_at = 1;
2691 let client_id = insert_client(&manager, context);
2692
2693 let (done_tx, done_rx) = std::sync::mpsc::channel();
2696 let sender = manager.clone();
2697 std::thread::spawn(move || {
2698 let result = sender.send_to_client(client_id, Arc::new(Bytes::from_static(b"update")));
2699 let _ = done_tx.send(result);
2700 });
2701
2702 let result = done_rx
2703 .recv_timeout(Duration::from_secs(5))
2704 .expect("send_to_client deadlocked on an expired token");
2705 assert_eq!(result, Err(SendError::ClientDisconnected));
2706 assert!(!manager.has_client(client_id));
2707 }
2708
2709 #[tokio::test]
2710 async fn an_expired_client_keeps_its_close_reason_when_its_queue_is_full() {
2711 let manager = ClientManager::new();
2712 let (sender, mut queue) = mpsc::channel(1);
2713 sender
2714 .try_send(Message::Text("backlog".into()))
2715 .expect("room for one message");
2716 let client_id = Uuid::new_v4();
2717 let mut context = create_test_auth_context("user-1", Limits::default());
2718 context.expires_at = 1;
2719 let client = ClientInfo::new(
2720 client_id,
2721 sender,
2722 Some(context),
2723 create_test_socket_addr("127.0.0.1"),
2724 );
2725 let close_frame = client.close_frame.clone();
2726 manager.clients.insert(client_id, client);
2727
2728 assert!(manager.check_and_remove_expired(client_id));
2729
2730 let frame = close_frame
2731 .get()
2732 .expect("the close reason is recorded even though the queue is full");
2733 assert_eq!(frame.code, CloseCode::Policy);
2734 assert_eq!(
2735 frame.reason.as_str(),
2736 "token-expired: Authentication token expired"
2737 );
2738 assert!(matches!(queue.recv().await, Some(Message::Text(_))));
2742 assert!(queue.recv().await.is_none());
2743 }
2744
2745 #[test]
2746 fn unexpired_token_is_kept() {
2747 let manager = ClientManager::new();
2748 let client_id = insert_client(
2749 &manager,
2750 create_test_auth_context("user-1", Limits::default()),
2751 );
2752
2753 assert!(!manager.check_and_remove_expired(client_id));
2754 assert!(manager.has_client(client_id));
2755 }
2756
2757 #[tokio::test]
2761 async fn eviction_cancels_every_subscription_of_the_evicted_client() {
2762 async fn subscribed(
2763 manager: &ClientManager,
2764 context: AuthContext,
2765 ) -> (Uuid, Vec<CancellationToken>) {
2766 let client_id = insert_client(manager, context);
2767 let connection = manager.connection_token(client_id);
2768 let derived = connection.child_token();
2769 let registered = CancellationToken::new();
2770 assert!(
2771 manager
2772 .add_client_subscription(client_id, "derived".into(), derived.clone())
2773 .await
2774 );
2775 assert!(
2776 manager
2777 .add_client_subscription(client_id, "registered".into(), registered.clone())
2778 .await
2779 );
2780 (client_id, vec![connection, derived, registered])
2781 }
2782
2783 let manager = ClientManager::new().with_timeout(Duration::ZERO);
2785 let (client_id, tokens) = subscribed(
2786 &manager,
2787 create_test_auth_context("user-1", Limits::default()),
2788 )
2789 .await;
2790 std::thread::sleep(Duration::from_millis(2));
2791 assert_eq!(manager.cleanup_stale_clients(), 1);
2792 assert!(!manager.has_client(client_id));
2793 assert!(
2794 tokens.iter().all(CancellationToken::is_cancelled),
2795 "stale eviction"
2796 );
2797
2798 let manager = ClientManager::new();
2800 let mut expired = create_test_auth_context("user-2", Limits::default());
2801 expired.expires_at = 1;
2802 let (client_id, tokens) = subscribed(&manager, expired).await;
2803 assert!(manager.check_and_remove_expired(client_id));
2804 assert!(
2805 tokens.iter().all(CancellationToken::is_cancelled),
2806 "expiry eviction"
2807 );
2808
2809 let manager = ClientManager::new();
2811 let (client_id, tokens) = subscribed(
2812 &manager,
2813 create_test_auth_context("user-3", Limits::default()),
2814 )
2815 .await;
2816 let payload = Arc::new(Bytes::from_static(b"{}"));
2817 while manager.send_to_client(client_id, payload.clone()).is_ok() {}
2818 assert!(!manager.has_client(client_id));
2819 assert!(
2820 tokens.iter().all(CancellationToken::is_cancelled),
2821 "backpressure eviction"
2822 );
2823
2824 assert!(manager.connection_token(Uuid::new_v4()).is_cancelled());
2826 }
2827
2828 #[tokio::test]
2833 async fn refresh_cannot_move_a_socket_into_an_account_at_its_connection_cap() {
2834 let manager = ClientManager::new();
2835 let capped = Limits {
2836 max_connections: Some(1),
2837 ..Limits::default()
2838 };
2839 let into_capped = || {
2840 create_v2_auth_context(
2841 "consumer:b",
2842 "account:b",
2843 1,
2844 Limits::default(),
2845 capped.clone(),
2846 )
2847 };
2848 let holder = insert_client(&manager, into_capped());
2849 let other = insert_client(
2850 &manager,
2851 create_v2_auth_context(
2852 "consumer:c",
2853 "account:c",
2854 1,
2855 Limits::default(),
2856 Limits::default(),
2857 ),
2858 );
2859
2860 let deny = manager
2861 .try_update_client_auth_async(other, into_capped())
2862 .await
2863 .unwrap_err();
2864 assert_eq!(deny.code, AuthErrorCode::ConnectionLimitExceeded);
2865 assert_eq!(
2866 manager
2867 .get_auth_context(other)
2868 .unwrap()
2869 .account_key
2870 .as_deref(),
2871 Some("account:c"),
2872 "a refused refresh keeps the current identity"
2873 );
2874 assert!(manager
2875 .try_update_client_auth(other, into_capped())
2876 .is_err());
2877
2878 assert!(manager
2879 .try_update_client_auth_async(holder, into_capped())
2880 .await
2881 .unwrap());
2882 }
2883
2884 #[tokio::test]
2886 async fn refresh_cannot_move_a_socket_into_a_consumer_at_its_connection_cap() {
2887 let manager = ClientManager::new();
2888 let capped = || {
2889 create_v2_auth_context(
2890 "consumer:b",
2891 "account:a",
2892 1,
2893 Limits {
2894 max_connections: Some(1),
2895 ..Limits::default()
2896 },
2897 Limits::default(),
2898 )
2899 };
2900 insert_client(&manager, capped());
2901 let other = insert_client(
2902 &manager,
2903 create_v2_auth_context(
2904 "consumer:c",
2905 "account:a",
2906 1,
2907 Limits::default(),
2908 Limits::default(),
2909 ),
2910 );
2911
2912 let deny = manager
2913 .try_update_client_auth_async(other, capped())
2914 .await
2915 .unwrap_err();
2916 assert_eq!(deny.code, AuthErrorCode::ConnectionLimitExceeded);
2917 }
2918
2919 #[tokio::test]
2922 async fn refresh_into_a_stale_policy_version_is_refused() {
2923 let manager = ClientManager::new();
2924 let addr = create_test_socket_addr("127.0.0.1");
2925 let version = |policy_version: u32, max_connections: u32| {
2926 create_v2_auth_context(
2927 "consumer:a",
2928 "account:a",
2929 policy_version,
2930 Limits::default(),
2931 Limits {
2932 max_connections: Some(max_connections),
2933 ..Limits::default()
2934 },
2935 )
2936 };
2937 manager
2938 .check_connection_allowed(addr, &Some(version(2, 2)))
2939 .await
2940 .unwrap();
2941 let client_id = insert_client(&manager, version(2, 2));
2942
2943 let deny = manager
2944 .try_update_client_auth_async(client_id, version(1, 50))
2945 .await
2946 .unwrap_err();
2947 assert_eq!(deny.code, AuthErrorCode::TokenExpired);
2948 assert_eq!(
2949 manager.get_auth_context(client_id).unwrap().policy_version,
2950 Some(2)
2951 );
2952 }
2953
2954 #[tokio::test]
2956 async fn refresh_refuses_a_token_whose_subscription_cap_is_below_open_subscriptions() {
2957 let manager = ClientManager::new();
2958 let with_cap = |max_subscriptions: u32| {
2959 create_test_auth_context(
2960 "user-1",
2961 Limits {
2962 max_subscriptions: Some(max_subscriptions),
2963 ..Limits::default()
2964 },
2965 )
2966 };
2967 let client_id = insert_client(&manager, with_cap(5));
2968 for id in ["a", "b", "c"] {
2969 assert!(
2970 manager
2971 .add_client_subscription(client_id, id.into(), CancellationToken::new())
2972 .await
2973 );
2974 }
2975
2976 let deny = manager
2977 .try_update_client_auth_async(client_id, with_cap(2))
2978 .await
2979 .unwrap_err();
2980 assert_eq!(deny.code, AuthErrorCode::SubscriptionLimitExceeded);
2981 assert_eq!(
2982 manager
2983 .get_auth_context(client_id)
2984 .unwrap()
2985 .limits
2986 .max_subscriptions,
2987 Some(5)
2988 );
2989 assert!(manager
2990 .try_update_client_auth_async(client_id, with_cap(3))
2991 .await
2992 .unwrap());
2993 }
2994
2995 #[tokio::test]
2997 async fn test_rate_limiter_integration() {
2998 use crate::websocket::rate_limiter::{RateLimiterConfig, WebSocketRateLimiter};
2999
3000 let rate_limiter = Arc::new(WebSocketRateLimiter::new(RateLimiterConfig::default()));
3001 let manager = ClientManager::new().with_rate_limiter(rate_limiter);
3002 let addr = create_test_socket_addr("127.0.0.1");
3003
3004 let auth = create_test_auth_context("user-1", Limits::default());
3006 assert!(manager
3007 .check_connection_allowed(addr, &Some(auth))
3008 .await
3009 .is_ok());
3010 }
3011}