remora/crates/remora-identity/src/store.rs
ms2k bf028161ae Add identity and target authorization package
OIDC validation, principal mapping, remora:v1 roles, and current-authz
filtering with kind acceptance on Keycloak and ZITADEL.
2026-07-16 18:27:42 +09:00

96 lines
2.7 KiB
Rust

use remora_domain::OpaqueId;
use remora_durable::DurableError;
use sqlx::PgPool;
use time::OffsetDateTime;
use crate::error::IdentityError;
use crate::principal::{ExternalIdentity, Principal, principal_id_for};
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct PrincipalRecord {
pub principal_id: OpaqueId,
pub issuer: String,
pub subject: String,
pub created_at: OffsetDateTime,
}
/// Resolve or create the durable principal mapping for an external identity.
pub async fn resolve_principal(
pool: &PgPool,
external: ExternalIdentity,
) -> Result<Principal, IdentityError> {
let principal_id = principal_id_for(&external.issuer, &external.subject)?;
sqlx::query(
"INSERT INTO principal_identities (principal_id, issuer, subject)
VALUES ($1, $2, $3)
ON CONFLICT (issuer, subject) DO NOTHING",
)
.bind(principal_id.as_str())
.bind(&external.issuer)
.bind(&external.subject)
.execute(pool)
.await
.map_err(DurableError::from)?;
let record = load_by_external(pool, &external.issuer, &external.subject)
.await?
.ok_or_else(|| {
IdentityError::Config("principal insert succeeded but row is missing".into())
})?;
// Prefer the stored id so a future non-deterministic scheme remains stable.
Ok(Principal::from_external(record.principal_id, external))
}
pub async fn load_by_id(
pool: &PgPool,
principal_id: &OpaqueId,
) -> Result<Option<PrincipalRecord>, IdentityError> {
let row = sqlx::query_as::<_, PrincipalRow>(
"SELECT principal_id, issuer, subject, created_at
FROM principal_identities
WHERE principal_id = $1",
)
.bind(principal_id.as_str())
.fetch_optional(pool)
.await
.map_err(DurableError::from)?;
row.map(PrincipalRow::into_record).transpose()
}
pub async fn load_by_external(
pool: &PgPool,
issuer: &str,
subject: &str,
) -> Result<Option<PrincipalRecord>, IdentityError> {
let row = sqlx::query_as::<_, PrincipalRow>(
"SELECT principal_id, issuer, subject, created_at
FROM principal_identities
WHERE issuer = $1 AND subject = $2",
)
.bind(issuer)
.bind(subject)
.fetch_optional(pool)
.await
.map_err(DurableError::from)?;
row.map(PrincipalRow::into_record).transpose()
}
#[derive(sqlx::FromRow)]
struct PrincipalRow {
principal_id: String,
issuer: String,
subject: String,
created_at: OffsetDateTime,
}
impl PrincipalRow {
fn into_record(self) -> Result<PrincipalRecord, IdentityError> {
Ok(PrincipalRecord {
principal_id: OpaqueId::parse(self.principal_id)?,
issuer: self.issuer,
subject: self.subject,
created_at: self.created_at,
})
}
}