Release 0.3.46
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:
co-authored by
Claude Fable 5.1
parent
3c68814030
commit
2ac34b6167
@@ -4,6 +4,10 @@ One line per change, grouped by the release that shipped it. Newest
|
|||||||
first. A release is a Cargo.toml bump plus a `v<version>` tag; the
|
first. A release is a Cargo.toml bump plus a `v<version>` tag; the
|
||||||
content repos pin the tag they run (see the end of this file).
|
content repos pin the tag they run (see the end of this file).
|
||||||
|
|
||||||
|
## 0.3.46 (2026-09-23)
|
||||||
|
|
||||||
|
- An invite first looks the email up in Kanidm (`/v1/raw/search` on `mail`, when the service account may read people: `idm_people_pii_read`) and only adds an existing account, under whatever username, to the group; no second account, no reset link.
|
||||||
|
|
||||||
## 0.3.45 (2026-09-23)
|
## 0.3.45 (2026-09-23)
|
||||||
|
|
||||||
- A mail stream that cannot be created at boot (subjects owned by an older stream) is a warning, not a failed start; 0.3.44 took the kasse sites down for two minutes on the rename.
|
- A mail stream that cannot be created at boot (subjects owned by an older stream) is a warning, not a failed start; 0.3.44 took the kasse sites down for two minutes on the rename.
|
||||||
|
|||||||
Generated
+1
-1
@@ -3265,7 +3265,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "portal"
|
name = "portal"
|
||||||
version = "0.3.45"
|
version = "0.3.46"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"arc-swap",
|
"arc-swap",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "portal"
|
name = "portal"
|
||||||
version = "0.3.45"
|
version = "0.3.46"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
|
|||||||
+41
-3
@@ -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 {
|
pub struct Kanidm {
|
||||||
base: String,
|
base: String,
|
||||||
token: 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}"))
|
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
|
/// 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> {
|
pub async fn invite(&self, name: &str, email: &str, group: &str) -> anyhow::Result<Invited> {
|
||||||
let username = username_for(email);
|
let existing = self.find_by_mail(email).await?;
|
||||||
let created = if self.person_exists(&username).await? {
|
let username = existing.clone().unwrap_or_else(|| username_for(email));
|
||||||
|
let created = if existing.is_some() || self.person_exists(&username).await? {
|
||||||
false
|
false
|
||||||
} else {
|
} else {
|
||||||
let (status, text) = self
|
let (status, text) = self
|
||||||
@@ -171,6 +201,14 @@ mod tests {
|
|||||||
assert_eq!(username_for("Ærlig@x.no"), "rlig");
|
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]
|
#[test]
|
||||||
fn a_null_person_answer_means_nobody_there() {
|
fn a_null_person_answer_means_nobody_there() {
|
||||||
assert_eq!(person_lookup_found(200, "null"), Some(false));
|
assert_eq!(person_lookup_found(200, "null"), Some(false));
|
||||||
|
|||||||
Reference in New Issue
Block a user