Add identity and target authorization package
OIDC validation, principal mapping, remora:v1 roles, and current-authz filtering with kind acceptance on Keycloak and ZITADEL.
This commit is contained in:
parent
91ac6dee85
commit
bf028161ae
1 changed files with 3153 additions and 21 deletions
96
crates/remora-identity/src/store.rs
Normal file
96
crates/remora-identity/src/store.rs
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
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,
|
||||
})
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue