use std::sync::{Arc, LazyLock, Weak};
pub use nornir_identity_directory::import::{ImportOp, ImportPlan, Imported};
pub use nornir_identity_directory::scim::bearer::{self, Bearer, Refusal, RefusalGuard, Scope};
pub use nornir_identity_directory::scim::{
error_answer, group_resource, handle, user_resource, Answer, Call, Effect, CONTENT_TYPE,
ERROR_SCHEMA, GROUP_SCHEMA, LIST_SCHEMA, MAX_BODY, MAX_PAGE, PATCH_SCHEMA, USER_SCHEMA,
};
use nornir_identity_directory::model::{
DirectoryStore, Group, NewUser, StoreError, User, UserPatch as DirPatch,
};
use super::plane::{DirUser, GroupRow, ProvisionAsk, Room, SsoPlane, UserPatch};
use crate::seam::ControlError;
pub static REFUSALS: LazyLock<RefusalGuard> = LazyLock::new(RefusalGuard::new);
fn to_store(err: ControlError) -> StoreError {
match err {
ControlError::NotFound(m) => StoreError::NotFound(m),
ControlError::AlreadyExists(m) => StoreError::AlreadyExists(m),
ControlError::InvalidArgument(m) => StoreError::Invalid(m),
ControlError::FailedPrecondition(m) => StoreError::Precondition(m),
ControlError::PermissionDenied(m) => StoreError::Denied(m),
ControlError::ResourceExhausted(m) => StoreError::Exhausted(m),
other => StoreError::Internal(other.to_string()),
}
}
fn user_of(row: DirUser) -> User {
let u = row.user;
User {
principal_id: u.principal_id,
user_name: u.user_name,
display_name: u.display_name,
email: u.email,
external_id: u.external_id,
active: u.active,
created_at_unix_ms: u.created_at_unix_ms,
deleted: row.deleted,
}
}
fn patch_of(p: &DirPatch) -> UserPatch {
UserPatch {
user_name: p.user_name.clone(),
display_name: p.display_name.clone(),
email: p.email.clone(),
external_id: p.external_id.clone(),
active: p.active,
}
}
pub struct PlaneDirectory<'a> {
pub plane: &'a dyn SsoPlane,
pub account: &'a str,
pub room: Room,
pub actor: &'a str,
pub now_unix_ms: i64,
}
impl PlaneDirectory<'_> {
fn ask(&self, user: &NewUser) -> ProvisionAsk {
ProvisionAsk {
account: self.account.to_owned(),
user_name: user.user_name.clone(),
display_name: user.display_name.clone(),
email: user.email.clone(),
external_id: user.external_id.clone(),
active: user.active,
source: user.source.as_str(),
room: self.room.clone(),
actor: self.actor.to_owned(),
now_unix_ms: self.now_unix_ms,
}
}
}
impl DirectoryStore for PlaneDirectory<'_> {
fn users(&self) -> Result<Vec<User>, StoreError> {
Ok(self
.plane
.directory(self.account)
.map_err(to_store)?
.into_iter()
.map(user_of)
.collect())
}
fn provision(&self, user: &NewUser) -> Result<User, StoreError> {
self.plane
.provision(self.ask(user))
.map(user_of)
.map_err(to_store)
}
fn update_user(
&self,
principal_id: &str,
patch: &DirPatch,
) -> Result<(User, bool), StoreError> {
self.plane
.update_user(self.account, principal_id, patch_of(patch), self.actor)
.map(|(u, changed)| (user_of(u), changed))
.map_err(to_store)
}
fn delete_user(&self, principal_id: &str) -> Result<User, StoreError> {
self.plane
.delete_user(self.account, principal_id, self.actor)
.map(user_of)
.map_err(to_store)
}
fn groups(&self) -> Result<Vec<Group>, StoreError> {
Ok(self
.plane
.groups(self.account)
.map_err(to_store)?
.into_iter()
.map(|g| Group {
id: g.id,
display_name: g.display_name,
external_id: g.external_id,
members: g.members,
created_at_unix_ms: g.created_at_unix_ms,
})
.collect())
}
fn put_group(&self, g: Group) -> Result<Group, StoreError> {
let saved = self
.plane
.put_group(GroupRow {
id: g.id,
account: self.account.to_owned(),
display_name: g.display_name,
external_id: g.external_id,
members: g.members,
created_at_unix_ms: g.created_at_unix_ms,
})
.map_err(to_store)?;
Ok(Group {
id: saved.id,
display_name: saved.display_name,
external_id: saved.external_id,
members: saved.members,
created_at_unix_ms: saved.created_at_unix_ms,
})
}
fn remove_group(&self, id: &str) -> Result<bool, StoreError> {
self.plane.remove_group(self.account, id).map_err(to_store)
}
fn new_group_id(&self) -> String {
crate::webauthn::hex(&crate::webauthn::random_bytes(12))
}
fn now_unix_ms(&self) -> i64 {
self.now_unix_ms
}
fn apply_import(&self, plan: &ImportPlan, commit: bool) -> Result<(), StoreError> {
let rows: Vec<(String, ImportRow)> = plan
.ops
.iter()
.map(|op| match op {
ImportOp::Create { line, user } => (
format!("line {line}: {}", user.user_name),
ImportRow::Create(self.ask(user)),
),
ImportOp::Update {
line,
principal_id,
user_name,
patch,
..
} => (
format!("line {line}: {user_name}"),
ImportRow::Update {
principal_id: principal_id.clone(),
patch: patch_of(patch),
},
),
})
.collect();
self.plane
.import(self.account, &rows, self.actor, self.now_unix_ms, commit)
.map_err(to_store)
}
}
#[derive(Debug, Clone)]
pub enum ImportRow {
Create(ProvisionAsk),
Update {
principal_id: String,
patch: UserPatch,
},
}
pub type ImportFinished = Arc<dyn Fn(&Result<Imported, String>) + Send + Sync>;
#[derive(Clone)]
pub struct ImportOrder {
pub account: String,
pub csv: Arc<str>,
pub dry_run: bool,
pub room: Room,
pub actor: String,
pub plane: Weak<dyn crate::seam::ControlPlane>,
pub finished: ImportFinished,
}
impl std::fmt::Debug for ImportOrder {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ImportOrder")
.field("account", &self.account)
.field("bytes", &self.csv.len())
.field("dry_run", &self.dry_run)
.finish_non_exhaustive()
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct ImportJob {
pub job_id: String,
pub state: &'static str,
pub error: String,
pub done_rows: u64,
pub total_rows: u64,
pub outcome: Imported,
}
pub fn run_import(
order: &ImportOrder,
now_unix_ms: i64,
progress: &dyn Fn(u64, u64),
) -> Result<Imported, String> {
let control = order
.plane
.upgrade()
.ok_or_else(|| "the server is shutting down; nothing was imported".to_owned())?;
let plane = control.sso().ok_or_else(|| {
"single sign-on needs an identity table; this control plane has none".to_owned()
})?;
let store = PlaneDirectory {
plane,
account: &order.account,
room: order.room.clone(),
actor: &order.actor,
now_unix_ms,
};
nornir_identity_directory::import::run(&store, &order.csv, order.dry_run, progress)
}