Release 0.3.46
Test / test (push) Successful in 1m7s
Publish release / publish (push) Successful in 3m45s

An invite finds an existing account by its email and only adds it to
the group.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Bendik Aagaard Lynghaug
2026-09-23 22:18:28 +02:00
co-authored by Claude Fable 5.1
parent 3c68814030
commit 2ac34b6167
4 changed files with 47 additions and 5 deletions
+41 -3
View File
@@ -44,6 +44,13 @@ fn person_lookup_found(status: u16, body: &str) -> Option<bool> {
}
}
/// The first entry's `name` in a `/v1/raw/search` answer, which is
/// `{"entries": [{"attrs": {"name": ["…"], …}}]}`.
fn name_from_search(body: &str) -> Option<String> {
let v: serde_json::Value = serde_json::from_str(body).ok()?;
v.get("entries")?.as_array()?.first()?.get("attrs")?.get("name")?.as_array()?.first()?.as_str().map(str::to_string)
}
pub struct Kanidm {
base: String,
token: String,
@@ -77,11 +84,34 @@ impl Kanidm {
person_lookup_found(status, &text).ok_or_else(|| anyhow::anyhow!("kanidm: looking up {username}: http {status}"))
}
/// The account that already carries this email, if the token may
/// see it (the service account in `idm_people_pii_read`). None
/// when nobody has it, or when the search is not permitted: then
/// the username derived from the email decides, as before.
async fn find_by_mail(&self, email: &str) -> anyhow::Result<Option<String>> {
let (status, text) = self
.call(
openidconnect::reqwest::Method::POST,
"/v1/raw/search",
Some(serde_json::json!({ "filter": { "eq": ["mail", email.trim().to_lowercase()] } })),
)
.await?;
if !(200..300).contains(&status) {
tracing::warn!(status, "kanidm: searching people by mail is not permitted for this token; matching by username instead");
return Ok(None);
}
Ok(name_from_search(&text))
}
/// The whole onboarding, in the order that leaves the least behind
/// if a step fails: create, then email, then group, then the link.
/// if a step fails: find or create, then email, then group, then
/// the link. A person who already has an account, under whatever
/// username, is only added to the group; their credentials are
/// left alone and no link is made.
pub async fn invite(&self, name: &str, email: &str, group: &str) -> anyhow::Result<Invited> {
let username = username_for(email);
let created = if self.person_exists(&username).await? {
let existing = self.find_by_mail(email).await?;
let username = existing.clone().unwrap_or_else(|| username_for(email));
let created = if existing.is_some() || self.person_exists(&username).await? {
false
} else {
let (status, text) = self
@@ -171,6 +201,14 @@ mod tests {
assert_eq!(username_for("Ærlig@x.no"), "rlig");
}
#[test]
fn a_search_by_mail_yields_the_accounts_own_name() {
assert_eq!(name_from_search(r#"{"entries":[{"attrs":{"name":["ole.n"],"mail":["ole@x.no"]}}]}"#), Some("ole.n".into()));
assert_eq!(name_from_search(r#"{"entries":[]}"#), None);
assert_eq!(name_from_search("null"), None);
assert_eq!(name_from_search("not json"), None);
}
#[test]
fn a_null_person_answer_means_nobody_there() {
assert_eq!(person_lookup_found(200, "null"), Some(false));