#![cfg_attr(not(feature = "sso"), allow(unused_imports, dead_code))]
use std::sync::Arc;
use base64::Engine as _;
use gunnar_proto::{SamlSettings, SsoProvider};
use super::{Egress, Flow, SsoState};
#[cfg(feature = "sso")]
pub use nornir_identity_sso::saml::Verified;
pub fn settings(provider: &SsoProvider) -> Result<&SamlSettings, String> {
provider
.saml
.as_ref()
.filter(|s| {
!s.idp_entity_id.trim().is_empty()
&& !s.idp_sso_url.trim().is_empty()
&& !s.idp_certificates.is_empty()
})
.ok_or_else(|| {
format!(
"{}'s SAML provider has no IdP entity id, sign-on URL or certificate",
provider.account
)
})
}
pub fn certificates(settings: &SamlSettings) -> Result<Vec<Vec<u8>>, String> {
settings
.idp_certificates
.iter()
.map(|c| {
base64::engine::general_purpose::STANDARD
.decode(c.trim())
.map_err(|e| format!("a stored IdP certificate does not decode: {e}"))
})
.collect()
}
pub fn fingerprint(der: &[u8]) -> String {
nornir_identity_sso::fingerprint(der)
}
#[cfg(feature = "sso")]
fn shared(provider: &SsoProvider) -> Result<nornir_identity_sso::saml::SamlProvider, String> {
let s = settings(provider)?;
Ok(nornir_identity_sso::saml::SamlProvider {
account: provider.account.clone(),
console_origin: provider.console_origin.clone(),
idp_entity_id: s.idp_entity_id.clone(),
idp_sso_url: s.idp_sso_url.clone(),
certificates_der: certificates(s)?,
username_attribute: s.username_attribute.clone(),
display_name_attribute: s.display_name_attribute.clone(),
})
}
pub fn trusting(inner: Arc<dyn Egress>, ca: Vec<Vec<u8>>) -> Arc<dyn Egress> {
if ca.is_empty() {
return inner;
}
Arc::new(Trusting { inner, ca })
}
struct Trusting {
inner: Arc<dyn Egress>,
ca: Vec<Vec<u8>>,
}
impl Egress for Trusting {
fn get(&self, url: &str) -> Result<(u16, String), String> {
self.inner.get_trusting(url, &self.ca)
}
fn post_form(
&self,
url: &str,
authorization: &str,
body: &str,
) -> Result<(u16, String), String> {
self.inner
.post_form_trusting(url, authorization, body, &self.ca)
}
fn get_trusting(&self, url: &str, ca: &[Vec<u8>]) -> Result<(u16, String), String> {
self.inner.get_trusting(url, ca)
}
fn post_form_trusting(
&self,
url: &str,
authorization: &str,
body: &str,
ca: &[Vec<u8>],
) -> Result<(u16, String), String> {
self.inner.post_form_trusting(url, authorization, body, ca)
}
}
#[cfg(feature = "sso")]
pub async fn resolve_input(
state: &Arc<SsoState>,
egress: Option<Arc<dyn Egress>>,
input: &SamlSettings,
) -> Result<SamlSettings, String> {
let ask = nornir_identity_sso::saml::SamlInput {
metadata_xml: input.idp_metadata_xml.clone(),
metadata_url: input.idp_metadata_url.clone(),
idp_entity_id: input.idp_entity_id.clone(),
idp_sso_url: input.idp_sso_url.clone(),
certificates: input.idp_certificates.clone(),
username_attribute: input.username_attribute.clone(),
display_name_attribute: input.display_name_attribute.clone(),
};
let state = Arc::clone(state);
let resolved = tokio::task::spawn_blocking(move || {
nornir_identity_sso::saml::resolve_input(&state, egress.as_deref(), &ask)
})
.await
.map_err(|e| format!("the metadata fetch did not finish: {e}"))??;
Ok(SamlSettings {
idp_metadata_xml: String::new(),
idp_metadata_url: resolved.metadata_url,
idp_entity_id: resolved.idp_entity_id,
idp_sso_url: resolved.idp_sso_url,
idp_certificates: resolved
.certificates_der
.iter()
.map(|der| base64::engine::general_purpose::STANDARD.encode(der))
.collect(),
username_attribute: resolved.username_attribute,
display_name_attribute: resolved.display_name_attribute,
})
}
#[cfg(feature = "sso")]
pub fn begin(provider: &SsoProvider, now: i64) -> Result<(String, String, Flow), String> {
nornir_identity_sso::saml::begin(&shared(provider)?, now)
}
#[cfg(feature = "sso")]
pub fn finish(
state: &SsoState,
provider: &SsoProvider,
flow: &Flow,
saml_response: &str,
now: i64,
) -> Result<Verified, String> {
nornir_identity_sso::saml::finish(state, &shared(provider)?, flow, saml_response, now)
}
#[cfg(feature = "sso")]
pub fn sp_metadata(provider: &SsoProvider) -> String {
nornir_identity_sso::saml::sp_metadata(&provider.console_origin, &provider.account)
}
#[cfg(not(feature = "sso"))]
mod off {
use super::*;
const NOT_BUILT: &str =
"company SSO (OIDC/SAML) is not compiled into this build (gunnar-grpc feature `sso`)";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Verified {
pub issuer: String,
pub subject: String,
pub user_name: String,
pub display_name: String,
pub email: String,
pub assertion_id: String,
pub not_on_or_after_unix: i64,
}
#[allow(clippy::unused_async)]
pub async fn resolve_input(
_: &Arc<SsoState>,
_: Option<Arc<dyn Egress>>,
_: &SamlSettings,
) -> Result<SamlSettings, String> {
Err(NOT_BUILT.to_owned())
}
pub fn begin(_: &SsoProvider, _: i64) -> Result<(String, String, Flow), String> {
Err(NOT_BUILT.to_owned())
}
pub fn finish(
_: &SsoState,
_: &SsoProvider,
_: &Flow,
_: &str,
_: i64,
) -> Result<Verified, String> {
Err(NOT_BUILT.to_owned())
}
pub fn sp_metadata(_: &SsoProvider) -> String {
String::new()
}
}
#[cfg(not(feature = "sso"))]
pub use off::{begin, finish, resolve_input, sp_metadata, Verified};