#![cfg_attr(not(feature = "sso"), allow(unused_imports, dead_code))]
use std::sync::Arc;
use gunnar_proto::{OidcSettings, SsoProvider};
use super::{Egress, Flow, SsoState};
#[cfg(feature = "sso")]
pub use nornir_identity_sso::oidc::{CachedIdp, Verified};
pub fn settings(provider: &SsoProvider) -> Result<&OidcSettings, String> {
provider
.oidc
.as_ref()
.filter(|o| !o.issuer.trim().is_empty() && !o.client_id.trim().is_empty())
.ok_or_else(|| {
format!(
"{}'s OIDC provider has no issuer or client id",
provider.account
)
})
}
pub fn ca_of(settings: &OidcSettings) -> Vec<Vec<u8>> {
super::idp_ca::idp_ca_blocks(&settings.idp_ca_pem)
}
#[cfg(feature = "sso")]
fn shared(provider: &SsoProvider) -> Result<nornir_identity_sso::oidc::OidcProvider, String> {
let s = settings(provider)?;
Ok(nornir_identity_sso::oidc::OidcProvider {
account: provider.account.clone(),
console_origin: provider.console_origin.clone(),
issuer: s.issuer.trim().to_owned(),
client_id: s.client_id.trim().to_owned(),
scopes: s.scopes.clone(),
username_claim: s.username_claim.clone(),
display_name_claim: s.display_name_claim.clone(),
ca: ca_of(s),
})
}
#[cfg(feature = "sso")]
async fn blocking<T: Send + 'static>(
f: impl FnOnce() -> Result<T, String> + Send + 'static,
) -> Result<T, String> {
tokio::task::spawn_blocking(f)
.await
.map_err(|e| format!("the identity provider call did not finish: {e}"))?
}
#[cfg(feature = "sso")]
pub async fn idp(
state: &Arc<SsoState>,
egress: Arc<dyn Egress>,
issuer: &str,
ca: &[Vec<u8>],
now: i64,
force: bool,
) -> Result<CachedIdp, String> {
let (state, issuer, ca) = (Arc::clone(state), issuer.to_owned(), ca.to_vec());
blocking(move || nornir_identity_sso::oidc::idp(&state, &*egress, &issuer, &ca, now, force))
.await
}
#[cfg(feature = "sso")]
pub async fn begin(
state: &Arc<SsoState>,
egress: Arc<dyn Egress>,
provider: &SsoProvider,
login_hint: &str,
now: i64,
) -> Result<(String, String, Flow), String> {
let (state, provider, hint) = (Arc::clone(state), shared(provider)?, login_hint.to_owned());
blocking(move || nornir_identity_sso::oidc::begin(&state, &*egress, &provider, &hint, now))
.await
}
#[cfg(feature = "sso")]
pub async fn finish(
state: &Arc<SsoState>,
egress: Arc<dyn Egress>,
provider: &SsoProvider,
flow: &Flow,
code: &str,
client_secret: &str,
now: i64,
) -> Result<Verified, String> {
let (state, provider, flow, code, secret) = (
Arc::clone(state),
shared(provider)?,
flow.clone(),
code.to_owned(),
client_secret.to_owned(),
);
blocking(move || {
nornir_identity_sso::oidc::finish(&state, &*egress, &provider, &flow, &code, &secret, now)
})
.await
}
pub fn discovery_url(issuer: &str) -> String {
nornir_identity_sso::paths::discovery_url(issuer)
}
#[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 email_verified: bool,
pub stable_ids: Vec<String>,
}
#[allow(clippy::unused_async)]
pub async fn idp(
_: &Arc<SsoState>,
_: Arc<dyn Egress>,
_: &str,
_: &[Vec<u8>],
_: i64,
_: bool,
) -> Result<(), String> {
Err(NOT_BUILT.to_owned())
}
#[allow(clippy::unused_async)]
pub async fn begin(
_: &Arc<SsoState>,
_: Arc<dyn Egress>,
_: &SsoProvider,
_: &str,
_: i64,
) -> Result<(String, String, Flow), String> {
Err(NOT_BUILT.to_owned())
}
#[allow(clippy::unused_async)]
pub async fn finish(
_: &Arc<SsoState>,
_: Arc<dyn Egress>,
_: &SsoProvider,
_: &Flow,
_: &str,
_: &str,
_: i64,
) -> Result<Verified, String> {
Err(NOT_BUILT.to_owned())
}
}
#[cfg(not(feature = "sso"))]
pub use off::{begin, finish, idp, Verified};
pub mod gunnar_core_paths {
pub fn redirect_uri(origin: &str) -> String {
format!("{}/auth/sso/oidc/callback", origin.trim_end_matches('/'))
}
pub fn sp_entity_id(origin: &str, account: &str) -> String {
format!(
"{}/auth/sso/saml/{}",
origin.trim_end_matches('/'),
segment(account)
)
}
pub fn login_url(origin: &str, account: &str) -> String {
format!(
"{}/login/sso/{}",
origin.trim_end_matches('/'),
segment(account)
)
}
pub fn scim_base_url(origin: &str) -> String {
format!("{}/scim/v2", origin.trim_end_matches('/'))
}
pub fn segment(raw: &str) -> String {
let mut out = String::with_capacity(raw.len());
for b in raw.bytes() {
if b.is_ascii_alphanumeric() || matches!(b, b'-' | b'.' | b'_' | b'~') {
out.push(b as char);
} else {
out.push_str(&format!("%{b:02X}"));
}
}
out
}
pub fn acs_url(origin: &str) -> String {
format!("{}/auth/sso/saml/acs", origin.trim_end_matches('/'))
}
}