gunnar-grpc 1.5.0

The tonic service implementations and the event bus behind gunnar's gRPC control plane.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
//! **Company single sign-on** (owner, 2026-10-05): an account's own identity
//! provider — OIDC or SAML 2.0 — SCIM 2.0 provisioning and a CSV import.
//!
//! The pieces:
//!
//! * [`plane`] — the storage seam ([`plane::SsoPlane`]); the identity table
//!   behind it is `gunnar_core::AuthTable`'s.
//! * [`SsoState`] — what one listener holds in memory: the flows begun and
//!   not yet finished (single use, ten minutes), the second-factor tickets
//!   (single use, five minutes), the SAML assertion ids already consumed, the
//!   IdPs' discovery documents and keys (an hour), and the rate windows.
//! * [`oidc`] and [`saml`] — the two protocols, over edda's verifiers
//!   (`nornir_auth_oidc::rp`, `nornir_auth_saml::sp`).
//! * [`scim`] — SCIM 2.0 Users and Groups, and the CSV import.
//! * [`Egress`] — the one way gunnar reaches an IdP over the network, handed
//!   in by `gunnar-server` (its blocking `ureq`): https only, no redirects,
//!   bounded size and time, and never a loopback or private address unless the
//!   operator turned on the test door.

pub mod directory;
pub mod idp_ca;
pub mod oidc;
pub mod plane;
pub mod saml;
pub mod scim;

use std::collections::HashMap;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex, RwLock};

/// How long a begun flow may wait for the IdP: ten minutes.
pub const FLOW_TTL_MS: i64 = 10 * 60_000;
/// How long a second-factor ticket lives: five minutes.
pub const SECOND_TTL_MS: i64 = 5 * 60_000;
/// The most flows a listener holds open at once.
pub const MAX_FLOWS: usize = 10_000;
/// The most flows ONE account holds open at once.
pub const MAX_FLOWS_PER_ACCOUNT: usize = 500;
/// Clock skew tolerated on IdP timestamps: two minutes.
pub const SKEW_SECS: i64 = 120;
/// How long an IdP's discovery document and keys are trusted: an hour.
pub const IDP_CACHE_MS: i64 = 60 * 60_000;
/// The most bytes an IdP answer may be.
pub const MAX_IDP_BODY: usize = 1 << 20;
/// Logins begun per account per minute.
pub const BEGINS_PER_MINUTE: u32 = 120;
/// SCIM requests per bearer per minute.
pub const SCIM_PER_MINUTE: u32 = 600;
/// Failed finishes per account per minute before the account's logins are
/// held back for the rest of the minute.
pub const FAILURES_PER_MINUTE: u32 = 60;

/// **The network, as gunnar reaches an IdP.** Blocking (called from
/// `spawn_blocking`); the implementation enforces [`url_allowed`] again,
/// follows no redirect, and reads at most [`MAX_IDP_BODY`].
pub trait Egress: Send + Sync {
    /// `GET url` → (status, body).
    fn get(&self, url: &str) -> Result<(u16, String), String>;
    /// `POST url` with a form body and an `Authorization` header →
    /// (status, body).
    fn post_form(
        &self,
        url: &str,
        authorization: &str,
        body: &str,
    ) -> Result<(u16, String), String>;
    /// [`Egress::get`] trusting `ca` (DER certificates: ONE account's IdP CA,
    /// `gunnar_core::sso::parse_idp_ca`) beside the public roots. An egress
    /// that cannot add a trust anchor refuses a call that names one.
    fn get_trusting(&self, url: &str, ca: &[Vec<u8>]) -> Result<(u16, String), String> {
        if ca.is_empty() {
            return self.get(url);
        }
        Err("this server's egress cannot trust an identity provider's own CA".to_owned())
    }
    /// [`Egress::post_form`] trusting `ca`, as [`Egress::get_trusting`].
    fn post_form_trusting(
        &self,
        url: &str,
        authorization: &str,
        body: &str,
        ca: &[Vec<u8>],
    ) -> Result<(u16, String), String> {
        if ca.is_empty() {
            return self.post_form(url, authorization, body);
        }
        Err("this server's egress cannot trust an identity provider's own CA".to_owned())
    }
}

/// **The operator's internal networks an IdP may live on** (owner 2026-10-06:
/// a private gunnar whose company IdP is on the company's own network).
/// Set per ESTATE by its operator (the installer's feed,
/// `sso_private_idp_cidrs`), never by an account admin; empty — today's guard
/// — unless set. Only private ranges may be named (RFC 1918, carrier-grade
/// NAT, unique-local IPv6): loopback, link-local (the cloud's metadata
/// service), multicast, documentation and public ranges are refused when the
/// list is read, so no list can open them. An address inside a listed network
/// passes the egress guard; every other address is judged as before, and the
/// pinned resolver still connects to exactly what was checked.
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct PrivateNets(Vec<(std::net::IpAddr, u8)>);

impl PrivateNets {
    /// Read a list (`10.0.2.100/32, fd00::/8`; commas or white space).
    pub fn parse(text: &str) -> Result<PrivateNets, String> {
        use std::net::IpAddr;
        let mut nets = Vec::new();
        for word in text.split(|c: char| c == ',' || c.is_whitespace()).filter(|w| !w.is_empty()) {
            let (addr, len) = word
                .split_once('/')
                .ok_or_else(|| format!("{word:?} is not a network: name it as address/length (10.0.2.100/32)"))?;
            let ip: IpAddr = addr.parse().map_err(|_| format!("{word:?}: {addr:?} is not an address"))?;
            let len: u8 = len.parse().map_err(|_| format!("{word:?}: {len:?} is not a prefix length"))?;
            let private = match ip {
                IpAddr::V4(v4) => {
                    len <= 32 && PRIVATE_V4.iter().any(|(net, plen)| len >= *plen && v4_in(v4, *net, *plen))
                }
                IpAddr::V6(v6) => {
                    // An IPv4 network is named as itself, not smuggled in IPv6.
                    v6.to_ipv4_mapped().is_none()
                        && (7..=128).contains(&len)
                        && v6.segments()[0] & 0xfe00 == 0xfc00
                }
            };
            if !private {
                return Err(format!(
                    "{word:?} is not inside a private network: an operator may list RFC 1918 (10/8, 172.16/12, \
                     192.168/16), carrier-grade NAT (100.64/10) or unique-local IPv6 (fc00::/7) ranges only — never \
                     loopback, link-local (the cloud's metadata service), documentation or public ones"
                ));
            }
            nets.push((ip, len));
        }
        Ok(PrivateNets(nets))
    }

    /// No network is listed.
    pub fn is_empty(&self) -> bool {
        self.0.is_empty()
    }

    /// Is `ip` inside a listed network? (An IPv4 address carried in IPv6 is
    /// judged as the IPv4 address.)
    pub fn contains(&self, ip: std::net::IpAddr) -> bool {
        use std::net::IpAddr;
        let ip = match ip {
            IpAddr::V6(v6) => v6.to_ipv4_mapped().map_or(ip, IpAddr::V4),
            v4 => v4,
        };
        self.0.iter().any(|(net, len)| match (ip, net) {
            (IpAddr::V4(a), IpAddr::V4(n)) => v4_in(a, *n, *len),
            (IpAddr::V6(a), IpAddr::V6(n)) => {
                let mask = if *len == 0 { 0 } else { u128::MAX << (128 - u32::from(*len)) };
                u128::from(a) & mask == u128::from(*n) & mask
            }
            _ => false,
        })
    }
}

/// The IPv4 ranges an operator may list: RFC 1918 and carrier-grade NAT.
const PRIVATE_V4: [(std::net::Ipv4Addr, u8); 4] = [
    (std::net::Ipv4Addr::new(10, 0, 0, 0), 8),
    (std::net::Ipv4Addr::new(172, 16, 0, 0), 12),
    (std::net::Ipv4Addr::new(192, 168, 0, 0), 16),
    (std::net::Ipv4Addr::new(100, 64, 0, 0), 10),
];

fn v4_in(ip: std::net::Ipv4Addr, net: std::net::Ipv4Addr, len: u8) -> bool {
    let mask = if len == 0 { 0 } else { u32::MAX << (32 - u32::from(len)) };
    u32::from(ip) & mask == u32::from(net) & mask
}

impl std::fmt::Display for PrivateNets {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let words: Vec<String> = self.0.iter().map(|(ip, len)| format!("{ip}/{len}")).collect();
        f.write_str(&words.join(","))
    }
}

/// **May gunnar call this URL?** `https` to a named host or a public
/// address; never a loopback, private, link-local or unspecified literal —
/// an account admin types these URLs, and the box must not be made to probe
/// its own network. `allow_loopback_http` (the operator's test door) admits
/// `http://127.0.0.1|localhost|[::1]` and nothing else.
pub fn url_allowed(url: &str, allow_loopback_http: bool) -> Result<(), String> {
    url_allowed_in(url, allow_loopback_http, &PrivateNets::default())
}

/// [`url_allowed`] on an estate whose operator listed internal networks an
/// IdP may live on ([`PrivateNets`]): an https literal inside one passes.
pub fn url_allowed_in(url: &str, allow_loopback_http: bool, nets: &PrivateNets) -> Result<(), String> {
    if allow_loopback_http
        && nornir_auth_oidc::rp::endpoint_ok(url, true)
        && url.to_ascii_lowercase().starts_with("http://")
    {
        return Ok(());
    }
    let lower = url.trim().to_ascii_lowercase();
    let Some(rest) = lower.strip_prefix("https://") else {
        return Err(format!("{url:?} is not an https URL"));
    };
    let authority = rest.split(['/', '?', '#']).next().unwrap_or_default();
    if authority.contains('@') {
        return Err(format!("{url:?} carries credentials"));
    }
    let host = if let Some(v6) = authority.strip_prefix('[') {
        v6.split(']').next().unwrap_or_default().to_owned()
    } else {
        authority
            .rsplit_once(':')
            .map_or(authority, |(h, _)| h)
            .to_owned()
    };
    if host.is_empty() {
        return Err(format!("{url:?} names no host"));
    }
    if host == "localhost"
        || host.ends_with(".localhost")
        || host.ends_with(".internal")
        || host.ends_with(".local")
    {
        return Err(format!("{url:?} is a local name"));
    }
    if let Ok(ip) = host.parse::<std::net::IpAddr>() {
        if let Some(why) = address_forbidden_in(ip, false, nets) {
            return Err(format!("{url:?} is a {why} address"));
        }
    }
    Ok(())
}

/// **Why an address is no IdP's**, or `None` when it may be dialled: a
/// loopback, private, link-local (the cloud metadata service's
/// 169.254.169.254 among them), carrier-grade NAT, unspecified, broadcast,
/// multicast, documentation or reserved address — and the same inside an
/// IPv6 address that carries an IPv4 one (`::ffff:a.b.c.d`, NAT64
/// `64:ff9b::/96`). `allow_loopback` (the operator's test door) admits
/// loopback alone.
pub fn address_forbidden(ip: std::net::IpAddr, allow_loopback: bool) -> Option<&'static str> {
    use std::net::{IpAddr, Ipv4Addr};
    let v6 = match ip {
        IpAddr::V4(v4) => return v4_forbidden(v4, allow_loopback),
        IpAddr::V6(v6) => v6,
    };
    if v6.is_loopback() {
        return (!allow_loopback).then_some("loopback");
    }
    if v6.is_unspecified() {
        return Some("unspecified");
    }
    // An IPv4 address carried inside IPv6 is judged as the IPv4 address it
    // is: mapped (`::ffff:a.b.c.d`), compatible (`::a.b.c.d`), NAT64
    // (`64:ff9b::/96`).
    if let Some(v4) = v6.to_ipv4_mapped() {
        return v4_forbidden(v4, allow_loopback);
    }
    let s = v6.segments();
    let embedded = Ipv4Addr::from((u32::from(s[6]) << 16) | u32::from(s[7]));
    if s[..6] == [0; 6] || (s[0] == 0x64 && s[1] == 0xff9b && s[2..6] == [0; 4]) {
        return v4_forbidden(embedded, allow_loopback);
    }
    match s[0] {
        x if x & 0xfe00 == 0xfc00 => Some("unique-local (private)"),
        x if x & 0xffc0 == 0xfe80 => Some("link-local"),
        x if x & 0xffc0 == 0xfec0 => Some("site-local"),
        x if x & 0xff00 == 0xff00 => Some("multicast"),
        0x2001 if s[1] == 0x0db8 => Some("documentation"),
        _ => None,
    }
}

fn v4_forbidden(v4: std::net::Ipv4Addr, allow_loopback: bool) -> Option<&'static str> {
    let o = v4.octets();
    if v4.is_loopback() {
        return (!allow_loopback).then_some("loopback");
    }
    if v4.is_private() {
        return Some("private");
    }
    if v4.is_link_local() {
        return Some("link-local — the cloud's metadata service lives here");
    }
    if v4.is_unspecified() || o[0] == 0 {
        return Some("unspecified");
    }
    if v4.is_broadcast() {
        return Some("broadcast");
    }
    if v4.is_multicast() {
        return Some("multicast");
    }
    if o[0] == 100 && (64..=127).contains(&o[1]) {
        return Some("carrier-grade NAT (shared, private)");
    }
    if v4.is_documentation() {
        return Some("documentation");
    }
    if o[0] == 198 && (o[1] == 18 || o[1] == 19) {
        return Some("benchmarking");
    }
    if o[0] == 192 && o[1] == 0 && o[2] == 0 {
        return Some("IETF protocol assignments");
    }
    if o[0] >= 240 {
        return Some("reserved");
    }
    None
}

/// **Every address a host name resolved to must be one an IdP may have**
/// ([`address_forbidden`]) — one bad answer among good ones refuses the
/// name, because the connection may land on any of them. No answer at all is
/// a refusal too.
pub fn addresses_allowed(host: &str, addrs: &[std::net::SocketAddr], allow_loopback: bool) -> Result<(), String> {
    addresses_allowed_in(host, addrs, allow_loopback, &PrivateNets::default())
}

/// [`address_forbidden`], with the operator's internal networks admitted.
pub fn address_forbidden_in(ip: std::net::IpAddr, allow_loopback: bool, nets: &PrivateNets) -> Option<&'static str> {
    if nets.contains(ip) {
        return None;
    }
    address_forbidden(ip, allow_loopback)
}

/// [`addresses_allowed`], with the operator's internal networks admitted.
pub fn addresses_allowed_in(host: &str, addrs: &[std::net::SocketAddr], allow_loopback: bool, nets: &PrivateNets) -> Result<(), String> {
    if addrs.is_empty() {
        return Err(format!("{host} resolves to no address"));
    }
    for addr in addrs {
        if let Some(why) = address_forbidden_in(addr.ip(), allow_loopback, nets) {
            return Err(format!("{host} resolves to {} ({why}): an identity provider is never on this box's own network", addr.ip()));
        }
    }
    Ok(())
}

/// A begun flow, waiting for the IdP.
#[derive(Clone)]
pub struct Flow {
    pub account: String,
    pub saml: bool,
    pub console_origin: String,
    pub expires_ms: i64,
    /// OIDC: the nonce and the PKCE verifier (never logged).
    pub nonce: String,
    pub verifier: String,
    pub redirect_uri: String,
    /// SAML: the AuthnRequest's ID (`InResponseTo`).
    pub request_id: String,
}

/// A login verified by the IdP, waiting for its passkey.
#[derive(Clone)]
pub struct Second {
    pub account: String,
    pub principal_id: String,
    pub register: bool,
    pub session_openssh: String,
    pub session_expires_ms: i64,
    pub expires_ms: i64,
}

#[derive(Clone)]
pub struct CachedIdp {
    pub discovery: nornir_auth_oidc::rp::Discovery,
    pub jwks: String,
    pub fetched_ms: i64,
}

#[derive(Default)]
struct Windows {
    /// key → (minute, count)
    counts: HashMap<String, (i64, u32)>,
}

/// **One listener's SSO memory.** Every map is bounded and swept on use.
#[derive(Default)]
pub struct SsoState {
    egress: RwLock<Option<Arc<dyn Egress>>>,
    allow_loopback_http: AtomicBool,
    /// The operator's internal networks an IdP may live on (the feed's
    /// `sso_private_idp_cidrs`); shared with the server's resolver.
    private_nets: Arc<RwLock<PrivateNets>>,
    flows: Mutex<HashMap<String, Flow>>,
    seconds: Mutex<HashMap<String, Second>>,
    saml_seen: Mutex<HashMap<String, i64>>,
    idps: Mutex<HashMap<String, CachedIdp>>,
    windows: Mutex<Windows>,
}

fn lock<T>(m: &Mutex<T>) -> std::sync::MutexGuard<'_, T> {
    match m.lock() {
        Ok(g) => g,
        Err(poisoned) => poisoned.into_inner(),
    }
}

impl SsoState {
    /// Hand in the network (`gunnar-server` does, at start).
    pub fn set_egress(&self, egress: Arc<dyn Egress>) {
        *self.egress.write().unwrap_or_else(|p| p.into_inner()) = Some(egress);
    }

    pub fn egress(&self) -> Option<Arc<dyn Egress>> {
        self.egress
            .read()
            .unwrap_or_else(|p| p.into_inner())
            .clone()
    }

    /// The operator's test door: an IdP on this box's loopback, over http.
    pub fn set_allow_loopback_http(&self, allow: bool) {
        self.allow_loopback_http.store(allow, Ordering::SeqCst);
    }

    pub fn allow_loopback_http(&self) -> bool {
        self.allow_loopback_http.load(Ordering::SeqCst)
    }

    /// The operator's internal networks an IdP may live on (estate config).
    pub fn set_private_nets(&self, nets: PrivateNets) {
        *self.private_nets.write().unwrap_or_else(|p| p.into_inner()) = nets;
    }

    pub fn private_nets(&self) -> PrivateNets {
        self.private_nets.read().unwrap_or_else(|p| p.into_inner()).clone()
    }

    /// The same list, shared: the server's resolver reads it per lookup.
    pub fn private_nets_handle(&self) -> Arc<RwLock<PrivateNets>> {
        Arc::clone(&self.private_nets)
    }

    /// Store a begun flow under the hash of its state. Refused when the
    /// listener or the account holds too many.
    pub fn begin(&self, state_hash: String, flow: Flow, now: i64) -> Result<(), String> {
        let mut flows = lock(&self.flows);
        flows.retain(|_, f| f.expires_ms > now);
        if flows.len() >= MAX_FLOWS {
            return Err(
                "too many sign-ins are waiting for their identity providers; try again in a minute"
                    .to_owned(),
            );
        }
        if flows.values().filter(|f| f.account == flow.account).count() >= MAX_FLOWS_PER_ACCOUNT {
            return Err(format!(
                "too many sign-ins to {} are waiting; try again in a minute",
                flow.account
            ));
        }
        flows.insert(state_hash, flow);
        Ok(())
    }

    /// Take a flow by the hash of its state: single use, whatever happens
    /// next; `None` when unknown or expired.
    pub fn take_flow(&self, state_hash: &str, now: i64) -> Option<Flow> {
        let mut flows = lock(&self.flows);
        flows.remove(state_hash).filter(|f| f.expires_ms > now)
    }

    pub fn park_second(&self, ticket_hash: String, second: Second, now: i64) -> Result<(), String> {
        let mut seconds = lock(&self.seconds);
        seconds.retain(|_, s| s.expires_ms > now);
        if seconds.len() >= MAX_FLOWS {
            return Err("too many sign-ins are waiting for their passkeys".to_owned());
        }
        seconds.insert(ticket_hash, second);
        Ok(())
    }

    pub fn take_second(&self, ticket_hash: &str, now: i64) -> Option<Second> {
        lock(&self.seconds)
            .remove(ticket_hash)
            .filter(|s| s.expires_ms > now)
    }

    /// **A SAML assertion id, once.** False when it was seen before (a
    /// replay); remembered until `until_ms`.
    pub fn first_sight(&self, assertion_id: &str, until_ms: i64, now: i64) -> bool {
        let mut seen = lock(&self.saml_seen);
        seen.retain(|_, until| *until > now);
        if seen.contains_key(assertion_id) {
            return false;
        }
        if seen.len() >= 100_000 {
            // Full of live ids: refuse rather than forget one.
            return false;
        }
        seen.insert(assertion_id.to_owned(), until_ms.max(now + FLOW_TTL_MS));
        true
    }

    pub fn cached_idp(&self, issuer: &str, now: i64) -> Option<CachedIdp> {
        lock(&self.idps)
            .get(issuer)
            .filter(|c| now - c.fetched_ms < IDP_CACHE_MS)
            .cloned()
    }

    pub fn cache_idp(&self, issuer: &str, idp: CachedIdp) {
        let mut idps = lock(&self.idps);
        if idps.len() >= 4096 {
            idps.clear();
        }
        idps.insert(issuer.to_owned(), idp);
    }

    /// **A rate window**: count one event for `key` in the current minute;
    /// false when that makes more than `limit`.
    pub fn allow(&self, key: &str, limit: u32, now: i64) -> bool {
        let minute = now / 60_000;
        let mut w = lock(&self.windows);
        if w.counts.len() > 50_000 {
            w.counts.retain(|_, (m, _)| *m == minute);
        }
        let entry = w.counts.entry(key.to_owned()).or_insert((minute, 0));
        if entry.0 != minute {
            *entry = (minute, 0);
        }
        entry.1 += 1;
        entry.1 <= limit
    }

    /// How many events `key` counted this minute (without counting one).
    pub fn counted(&self, key: &str, now: i64) -> u32 {
        let minute = now / 60_000;
        lock(&self.windows)
            .counts
            .get(key)
            .filter(|(m, _)| *m == minute)
            .map_or(0, |(_, n)| *n)
    }
}

/// `2026-10-05T12:00:00Z` for a unix second (UTC, proleptic Gregorian).
pub fn rfc3339(unix: i64) -> String {
    let days = unix.div_euclid(86_400);
    let secs = unix.rem_euclid(86_400);
    // Howard Hinnant's civil_from_days.
    let z = days + 719_468;
    let era = z.div_euclid(146_097);
    let doe = z - era * 146_097;
    let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
    let y = yoe + era * 400;
    let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
    let mp = (5 * doy + 2) / 153;
    let d = doy - (153 * mp + 2) / 5 + 1;
    let m = if mp < 10 { mp + 3 } else { mp - 9 };
    let y = if m <= 2 { y + 1 } else { y };
    format!(
        "{y:04}-{m:02}-{d:02}T{:02}:{:02}:{:02}Z",
        secs / 3600,
        secs % 3600 / 60,
        secs % 60
    )
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn urls_an_admin_types_cannot_reach_inside() {
        assert!(url_allowed("https://login.example.com/x", false).is_ok());
        for bad in [
            "http://login.example.com/",
            "https://127.0.0.1/",
            "https://10.0.0.5:8443/",
            "https://169.254.169.254/latest/meta-data",
            "https://[::1]/",
            "https://[fd00::1]/",
            "https://localhost/",
            "https://user:pw@login.example.com/",
            "https://100.64.1.1/",
            "ftp://login.example.com/",
        ] {
            assert!(url_allowed(bad, false).is_err(), "{bad} was allowed");
        }
        assert!(url_allowed("http://127.0.0.1:9000/x", true).is_ok());
        assert!(url_allowed("http://10.0.0.5/x", true).is_err());
        assert!(
            url_allowed("https://127.0.0.1/", true).is_err(),
            "the test door is http loopback only"
        );
    }

    /// ★ **A name that resolves inside the box is no IdP** — loopback,
    /// private, link-local (the metadata service), CGNAT, and the same
    /// smuggled inside IPv6; a public address is one.
    #[test]
    fn addresses_inside_the_box_are_refused() {
        use std::net::IpAddr;
        for bad in [
            "127.0.0.1", "127.8.9.10", "10.1.2.3", "172.16.0.1", "192.168.1.1", "169.254.169.254", "100.64.0.1",
            "0.0.0.0", "255.255.255.255", "224.0.0.1", "192.0.2.10", "198.18.0.1", "240.0.0.1",
            "::1", "::", "fd00::1", "fe80::1", "ff02::1", "2001:db8::1", "::ffff:127.0.0.1", "::ffff:10.0.0.1",
            "::ffff:169.254.169.254", "64:ff9b::a00:1", "fec0::1",
        ] {
            let ip: IpAddr = bad.parse().unwrap();
            assert!(address_forbidden(ip, false).is_some(), "{bad} was allowed");
        }
        for good in ["93.184.216.34", "1.1.1.1", "2606:4700:4700::1111", "2a00:1450:4001::200e"] {
            let ip: IpAddr = good.parse().unwrap();
            assert_eq!(address_forbidden(ip, false), None, "{good} was refused");
        }
        // The test door admits loopback and nothing else private.
        assert_eq!(address_forbidden("127.0.0.1".parse().unwrap(), true), None);
        assert_eq!(address_forbidden("::1".parse().unwrap(), true), None);
        assert!(address_forbidden("10.0.0.1".parse().unwrap(), true).is_some());
        assert!(address_forbidden("169.254.169.254".parse().unwrap(), true).is_some());
    }

    #[test]
    fn every_resolved_address_must_pass() {
        let a = |s: &str| -> std::net::SocketAddr { s.parse().unwrap() };
        assert!(addresses_allowed("idp.example.test", &[a("93.184.216.34:443")], false).is_ok());
        let mixed = addresses_allowed("idp.example.test", &[a("93.184.216.34:443"), a("10.0.0.7:443")], false);
        assert!(mixed.unwrap_err().contains("10.0.0.7"), "one private answer among public ones passed");
        assert!(addresses_allowed("idp.example.test", &[], false).is_err());
    }

    /// ★ **The operator's internal networks** (owner 2026-10-06): only
    /// private ranges may be listed — never loopback, link-local (the
    /// metadata service), multicast, documentation, public or a range that
    /// reaches outside a private one.
    #[test]
    fn only_private_networks_may_be_listed() {
        let nets = PrivateNets::parse("10.0.2.100/32, 172.16.0.0/12 192.168.7.0/24,100.64.0.0/10\nfd00::/8").expect("private ranges");
        assert_eq!(nets.to_string(), "10.0.2.100/32,172.16.0.0/12,192.168.7.0/24,100.64.0.0/10,fd00::/8");
        assert_eq!(PrivateNets::parse("").expect("empty"), PrivateNets::default());
        assert_eq!(PrivateNets::parse("  ").expect("blank"), PrivateNets::default());
        for bad in [
            "127.0.0.0/8", "127.0.0.1/32", "169.254.169.254/32", "169.254.0.0/16", "0.0.0.0/0", "8.8.8.0/24",
            "10.0.0.0/7", "192.0.2.0/24", "224.0.0.0/4", "::1/128", "fe80::/10", "::/0", "2001:db8::/32",
            "10.0.0.1", "10.0.0.0/33", "fd00::/129", "not-a-net", "::ffff:10.0.0.0/104",
        ] {
            assert!(PrivateNets::parse(bad).is_err(), "{bad} was accepted");
        }
    }

    /// ★ **An address inside a listed network passes; nothing else changes.**
    #[test]
    fn a_listed_network_admits_its_addresses_and_no_other() {
        use std::net::IpAddr;
        let nets = PrivateNets::parse("10.0.2.100/32,fd00:aa::/32").expect("nets");
        let ip = |s: &str| -> IpAddr { s.parse().unwrap() };
        assert!(nets.contains(ip("10.0.2.100")));
        assert!(nets.contains(ip("::ffff:10.0.2.100")), "v4-in-v6 is judged as v4");
        assert!(nets.contains(ip("fd00:aa::7")));
        assert!(!nets.contains(ip("10.0.2.101")));
        assert_eq!(address_forbidden_in(ip("10.0.2.100"), false, &nets), None);
        assert!(address_forbidden_in(ip("10.0.2.101"), false, &nets).is_some());
        assert!(address_forbidden_in(ip("169.254.169.254"), false, &nets).is_some());
        assert!(address_forbidden_in(ip("127.0.0.1"), false, &nets).is_some());
        assert_eq!(address_forbidden_in(ip("93.184.216.34"), false, &nets), None, "public stays public");
        assert!(url_allowed_in("https://10.0.2.100:8443/realms/cov", false, &nets).is_ok());
        assert!(url_allowed_in("https://10.0.2.101:8443/realms/cov", false, &nets).is_err());
        assert!(url_allowed_in("http://10.0.2.100:8443/realms/cov", false, &nets).is_err(), "still https only");
        assert!(url_allowed("https://10.0.2.100:8443/realms/cov", false).is_err(), "no list: today's guard");
        let a = |s: &str| -> std::net::SocketAddr { s.parse().unwrap() };
        assert!(addresses_allowed_in("idp.corp.internal.example", &[a("10.0.2.100:8443")], false, &nets).is_ok());
        assert!(addresses_allowed_in("idp.corp.internal.example", &[a("10.0.2.100:8443"), a("10.9.9.9:8443")], false, &nets).is_err());
        let s = SsoState::default();
        assert!(s.private_nets().is_empty());
        s.set_private_nets(nets.clone());
        assert_eq!(*s.private_nets_handle().read().unwrap(), nets, "the resolver's handle sees the list");
    }

    #[test]
    fn flows_are_single_use_and_expire() {
        let s = SsoState::default();
        let flow = |account: &str| Flow {
            account: account.into(),
            saml: false,
            console_origin: String::new(),
            expires_ms: 1_000 + FLOW_TTL_MS,
            nonce: String::new(),
            verifier: String::new(),
            redirect_uri: String::new(),
            request_id: String::new(),
        };
        s.begin("a".into(), flow("acme"), 1_000).unwrap();
        assert!(s.take_flow("a", 2_000).is_some());
        assert!(s.take_flow("a", 2_000).is_none(), "a flow was taken twice");
        s.begin("b".into(), flow("acme"), 1_000).unwrap();
        assert!(
            s.take_flow("b", 1_000 + FLOW_TTL_MS + 1).is_none(),
            "an expired flow was taken"
        );
        for i in 0..MAX_FLOWS_PER_ACCOUNT {
            s.begin(format!("x{i}"), flow("acme"), 1_000).unwrap();
        }
        assert!(s.begin("over".into(), flow("acme"), 1_000).is_err());
        assert!(s.begin("other".into(), flow("globex"), 1_000).is_ok());
    }

    #[test]
    fn an_assertion_is_consumed_once() {
        let s = SsoState::default();
        assert!(s.first_sight("_a1", 10_000_000, 1_000));
        assert!(
            !s.first_sight("_a1", 10_000_000, 2_000),
            "a replayed assertion was accepted"
        );
        assert!(s.first_sight("_a2", 10_000_000, 2_000));
    }

    #[test]
    fn rate_windows_count_per_minute() {
        let s = SsoState::default();
        for _ in 0..3 {
            assert!(s.allow("k", 3, 60_000));
        }
        assert!(!s.allow("k", 3, 61_000));
        assert!(s.allow("k", 3, 120_000), "a new minute starts afresh");
    }

    #[test]
    fn rfc3339_is_utc() {
        assert_eq!(rfc3339(0), "1970-01-01T00:00:00Z");
        assert_eq!(rfc3339(1_800_000_000), "2027-01-15T08:00:00Z");
        assert_eq!(rfc3339(951_782_400), "2000-02-29T00:00:00Z");
    }
}