//! Pure-Rust SAML 2.0 Service Provider (v0.5.1).
//!
//! This module implements the SP half of a SAML Web-Browser-SSO profile:
//!
//! * [`metadata`] — parse the IdP's `EntityDescriptor` (SSO URLs + signing
//!   certificates) and build *our* SP metadata for the IdP admin to import.
//! * [`authn_request`] — build an `AuthnRequest` and encode it for the
//!   HTTP-Redirect binding.
//! * [`response`] — decode a `SAMLResponse`, **verify its XML signature**
//!   against the IdP's pinned certificate (via the pure-Rust `bergshamra`
//!   crate — no OpenSSL/libxml2/xmlsec, so the static musl binary stays
//!   C-free), then enforce the SP-side semantic checks (Status, Destination,
//!   Audience, time bounds) that are where SAML SPs actually get attacked.
//!
//! Stateful checks (replay of assertion IDs, correlating `InResponseTo`
//! against requests *we* issued, gating IdP-initiated login) live in the
//! HTTP layer — [`response::consume`] is deliberately stateless and returns
//! the IDs the caller needs to perform them.
//!
//! Access model: any assertion the IdP authenticates and we cryptographically
//! verify yields an operator [`VerifiedPrincipal`]. OpenPXE is single-tier —
//! there is no per-user role table — and the local admin account remains a
//! guaranteed fallback owner regardless of SSO state.

pub mod authn_request;
pub mod metadata;
pub mod response;

pub use authn_request::AuthnRequest;
pub use metadata::IdpMetadata;
pub use response::{VerifiedPrincipal, VerifiedResponse};

use thiserror::Error;

/// Default clock-skew tolerance applied to assertion time bounds. SAML IdPs
/// and SPs rarely have perfectly synced clocks; 60s matches common practice
/// (Shibboleth/FleetDM defaults are in this ballpark).
pub const DEFAULT_CLOCK_SKEW_SECS: i64 = 60;

/// Runtime SP parameters, derived from [`crate::SsoConfig`] + the advertised
/// public base URL by the HTTP layer.
#[derive(Debug, Clone)]
pub struct SpParams {
    /// Our SP Entity ID (the `<Issuer>` we send and the `Audience` we require
    /// in responses). Defaults to the public base URL when the operator left
    /// the Entity ID field blank.
    pub entity_id: String,
    /// The Assertion Consumer Service URL the IdP POSTs the response to —
    /// `<public_base_url>/api/sso/acs`.
    pub acs_url: String,
}

/// Everything that can go wrong consuming a SAML response. Kept coarse on
/// purpose: the HTTP layer logs the detail and shows the operator a generic
/// "SSO sign-in failed" — we never leak which specific check tripped to the
/// browser, since that aids an attacker probing the SP.
#[derive(Debug, Error)]
pub enum SamlError {
    #[error("SAML XML parse error: {0}")]
    Xml(String),
    #[error("IdP metadata is missing a required element: {0}")]
    Metadata(String),
    #[error("no usable IdP signing certificate in metadata")]
    NoSigningCert,
    #[error("signature verification failed: {0}")]
    Signature(String),
    #[error("the signature does not cover the assertion we read")]
    SignatureScope,
    #[error("SAML response status was not Success: {0}")]
    Status(String),
    #[error("response is missing a required element: {0}")]
    MissingElement(String),
    #[error("encrypted assertions are not supported in this release")]
    EncryptedAssertionUnsupported,
    #[error("expected exactly one assertion, found {0}")]
    AssertionCount(usize),
    #[error("issuer mismatch: response was not issued by the configured IdP")]
    IssuerMismatch,
    #[error("audience mismatch: assertion is not addressed to this service provider")]
    AudienceMismatch,
    #[error("response destination does not match our ACS URL")]
    DestinationMismatch,
    #[error("assertion is expired or not yet valid")]
    TimeBounds,
    #[error("invalid SAML timestamp: {0}")]
    Timestamp(String),
    #[error("base64 decode failed: {0}")]
    Base64(String),
}

#[cfg(test)]
mod tests;
