use std::collections::HashMap;
pub use fraiseql_core::security::EnrichmentOutcome;
use fraiseql_core::security::{
BoxFuture, ENRICHED_NAMESPACE_PREFIX, EnrichmentMark, IdentityEnricher, SecurityContext,
};
use super::{failure::IdentityResolution, resolver::IdentityResolver};
pub async fn enrich_security_context(
resolver: &IdentityResolver,
ctx: &mut SecurityContext,
) -> EnrichmentOutcome {
if ctx.attributes.keys().any(|k| k.starts_with(ENRICHED_NAMESPACE_PREFIX)) {
ctx.mark_enrichment(EnrichmentMark::Resolved);
return EnrichmentOutcome::Proceed;
}
let sub = ctx.user_id.0.clone();
let claims = claims_for_binding(ctx);
match resolver.resolve(&sub, &claims).await {
IdentityResolution::Resolved(fields) => {
for (field, value) in fields {
ctx.attributes.insert(format!("{ENRICHED_NAMESPACE_PREFIX}{field}"), value);
}
ctx.mark_enrichment(EnrichmentMark::Resolved);
EnrichmentOutcome::Proceed
},
IdentityResolution::Denied(_) => EnrichmentOutcome::Denied,
IdentityResolution::Unavailable(_) => EnrichmentOutcome::Unavailable,
}
}
pub async fn resolve_request_identity(
resolver: Option<&IdentityResolver>,
security_context: Option<&mut SecurityContext>,
) -> EnrichmentOutcome {
let (Some(resolver), Some(ctx)) = (resolver, security_context) else {
return EnrichmentOutcome::Proceed;
};
enrich_security_context(resolver, ctx).await
}
fn claims_for_binding(ctx: &SecurityContext) -> HashMap<String, serde_json::Value> {
let mut claims = ctx.attributes.clone();
claims
.entry("sub".to_owned())
.or_insert_with(|| serde_json::Value::String(ctx.user_id.0.clone()));
if let Some(tenant) = &ctx.tenant_id {
let value = serde_json::Value::String(tenant.0.clone());
claims.entry("tenant_id".to_owned()).or_insert_with(|| value.clone());
claims.entry("org_id".to_owned()).or_insert(value);
}
if let Some(email) = &ctx.email {
claims
.entry("email".to_owned())
.or_insert_with(|| serde_json::Value::String(email.clone()));
}
if let Some(name) = &ctx.display_name {
let value = serde_json::Value::String(name.clone());
claims.entry("name".to_owned()).or_insert_with(|| value.clone());
claims.entry("display_name".to_owned()).or_insert(value);
}
if let Some(iss) = &ctx.issuer {
claims
.entry("iss".to_owned())
.or_insert_with(|| serde_json::Value::String(iss.clone()));
}
let snapshot: serde_json::Map<String, serde_json::Value> =
claims.iter().map(|(k, v)| (k.clone(), v.clone())).collect();
claims.entry("claims".to_owned()).or_insert(serde_json::Value::Object(snapshot));
claims
}
impl IdentityEnricher for IdentityResolver {
fn enrich<'a>(&'a self, ctx: &'a mut SecurityContext) -> BoxFuture<'a, EnrichmentOutcome> {
Box::pin(enrich_security_context(self, ctx))
}
}