96 lines
2.7 KiB
Rust
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,
|
||
|
|
})
|
||
|
|
}
|
||
|
|
}
|