gunnar-grpc 3.0.0

The tonic service implementations and the event bus behind gunnar's gRPC control plane.
Documentation
//! **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 rules, the memory and both protocols are edda's `nornir-identity-sso`
//! (A34 M-4: one copy for every product); this module keeps Gunnar's half:
//!
//! * [`plane`] — the storage seam ([`plane::SsoPlane`]); the identity table
//!   behind it is `gunnar_core::AuthTable`'s.
//! * [`oidc`] and [`saml`] — the proto settings mapped onto the shared
//!   protocols, run on the blocking pool (the egress is blocking).
//! * [`scim`] — SCIM 2.0 Users and Groups, and the CSV import.
//! * [`SsoState`], [`Egress`], the guard ([`url_allowed`], [`PrivateNets`], …)
//!   and the limits — re-exported from the shared crate, so every caller keeps
//!   its path. The egress is 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;

/// E-mail domains proven by DNS (A34-5), the shared crate's.
pub use nornir_identity_sso::domains;
pub use nornir_identity_sso::guard::{
    address_forbidden, address_forbidden_in, addresses_allowed, addresses_allowed_in, url_allowed,
    url_allowed_in, PrivateNets,
};
pub use nornir_identity_sso::rfc3339;
pub use nornir_identity_sso::state::*;
pub use nornir_identity_sso::Egress;
/// Who a verified sign-in is (claim, rebind, JIT) and the refusal codes, the shared crate's.
pub use nornir_identity_sso::{code_of, rules, Refusal};

/// **A34-24: `require_sso` over every credential an employee mints for
/// themself** (the decision; it goes with the require-SSO rule into
/// `nornir-identity-sso`'s login policy, M-4). `required` is the sentence
/// [`plane::SsoPlane::passkey_login_refused`] says for the person (`None`: not
/// an employee, or the account does not require its IdP). An employee of such
/// an account gets a NEW SSH key, passkey or other credential only from the
/// session of a sign-in through the IdP (`on_idp_session`), never from a key
/// that bypasses it: an SSH key added before require-SSO, or by the CLI.
pub fn mint_refused(required: Option<String>, on_idp_session: bool) -> Option<String> {
    let why = required?;
    (!on_idp_session).then(|| {
        format!(
            "{why}: a new credential is made from that sign-in, never from a key that bypasses it"
        )
    })
}

/// **A session key**: one that expires within a web session's lifetime
/// (`max_ms`) of being added, as every key a sign-in enrols does. An SSH key
/// that never expires, or lives longer, is not one.
pub fn is_session_key(added_ms: i64, expires_ms: Option<i64>, max_ms: i64) -> bool {
    expires_ms.is_some_and(|until| until > added_ms && until - added_ms <= max_ms)
}