diff --git a/CHANGELOG.md b/CHANGELOG.md index 05d386a..38da7d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ content repos pin the tag they run (see the end of this file). - The header knows who is here: on a site with sign-in, a link top right to sign in, or the person's name and the way out. A page with a `slideshow` gets the wordmark as a bar of its own, the strip full width under it, and the question after. - "My cases": `where: { field: email, equals: "{user.email}" }` lists the records that carry the signed-in person's own address, filled from the session and never from the caller; nobody signed in matches nothing. An attachment is fetched only through a list that shows its record, so a list of my cases opens my files and nobody else's. - Form controls scale with the text around them (em, not rem): fields, pills, buttons and labels follow a site's body size. The date picker's calendar icon is the size of the text beside it, and the file picker's button takes the field's colours instead of the browser's. +- `KANIDM_GROUP_PREFIX`: a site on a Kanidm it shares with others keeps plain group names in its content; the prefix is put on when portal adds or removes a member, and the client's claim map takes it off. - Fixed: `maps` never reached the browser (`serde(skip)`), so a page with a map drew it on the server and dropped it on hydration. ## 0.6.1 (2026-09-30) diff --git a/docs/operations.md b/docs/operations.md index 3b39dd1..e04ee72 100644 --- a/docs/operations.md +++ b/docs/operations.md @@ -71,6 +71,7 @@ values. | `KANIDM_URL`, `OAUTH2_CLIENT_ID`, `OAUTH2_CLIENT_SECRET` | sign-in | | `PEOPLE_BACKEND` | `kanidm` or `memberships` (see [Memberships](memberships.md)); unset, Kanidm when `KANIDM_API_TOKEN` is set, memberships otherwise | | `KANIDM_API_TOKEN` | invites and `grants:` through Kanidm | +| `KANIDM_GROUP_PREFIX` | for a Kanidm shared with other sites: the content's `kunder` is the group `kunder` there, and the OAuth2 client's claim map gives it back to portal as `kunder` | | `PHONE_COUNTRY` | the calling code a bare phone number gets, `+47` unless set | | `OIDC_EXTRA_SCOPES` | scopes to ask the provider for besides `openid profile email`: `phoneNumber` for Vipps. Not for Kanidm, which denies a sign-in that asks for a scope the person does not hold | | `SEED_ADMIN_EMAIL` or `SEED_ADMIN_PHONE`, `SEED_ADMIN_NAME`, `SEED_GROUP` | the first person in, invited once at start into the most privileged group (or `SEED_GROUP`) | diff --git a/src/people.rs b/src/people.rs index e318b66..db8db9c 100644 --- a/src/people.rs +++ b/src/people.rs @@ -184,6 +184,12 @@ fn name_from_search(body: &str) -> Option { pub struct Kanidm { base: String, token: String, + /// What this site's groups are called in a Kanidm it shares with + /// other sites: `KANIDM_GROUP_PREFIX=klingenberg_` makes the + /// content's `kunder` the group `klingenberg_kunder` there. The + /// OAuth2 client's claim map takes the prefix off again, so content + /// and policy never see it. Empty for a Kanidm of the site's own. + group_prefix: String, } impl Kanidm { @@ -192,7 +198,8 @@ impl Kanidm { pub fn from_env() -> Option { let base = std::env::var("KANIDM_URL").ok()?.trim_end_matches('/').to_string(); let token = std::env::var("KANIDM_API_TOKEN").ok().filter(|t| !t.is_empty())?; - Some(Kanidm { base, token }) + let group_prefix = std::env::var("KANIDM_GROUP_PREFIX").unwrap_or_default().trim().to_string(); + Some(Kanidm { base, token, group_prefix }) } async fn call(&self, method: openidconnect::reqwest::Method, path: &str, body: Option) -> anyhow::Result<(u16, String)> { @@ -262,7 +269,7 @@ impl Kanidm { /// Who is in a group, by account name. pub async fn members(&self, group: &str) -> anyhow::Result> { - let (status, text) = self.call(openidconnect::reqwest::Method::GET, &format!("/v1/group/{group}/_attr/member"), None).await?; + let (status, text) = self.call(openidconnect::reqwest::Method::GET, &format!("/v1/group/{}{group}/_attr/member", self.group_prefix), None).await?; if !(200..300).contains(&status) { anyhow::bail!("kanidm: reading the members of {group}: http {status}: {text}"); } @@ -273,7 +280,7 @@ impl Kanidm { /// service account manages, like adding them was. pub async fn remove_member(&self, group: &str, username: &str) -> anyhow::Result<()> { let (status, text) = self - .call(openidconnect::reqwest::Method::DELETE, &format!("/v1/group/{group}/_attr/member"), Some(serde_json::json!([username]))) + .call(openidconnect::reqwest::Method::DELETE, &format!("/v1/group/{}{group}/_attr/member", self.group_prefix), Some(serde_json::json!([username]))) .await?; if !(200..300).contains(&status) { anyhow::bail!("kanidm: removing {username} from {group}: http {status}: {text}"); @@ -340,7 +347,7 @@ impl Kanidm { // tidying up after itself. already_member = !created && self.members(group).await.is_ok_and(|m| m.contains(&username)); let (status, text) = self - .call(openidconnect::reqwest::Method::POST, &format!("/v1/group/{group}/_attr/member"), Some(serde_json::json!([username]))) + .call(openidconnect::reqwest::Method::POST, &format!("/v1/group/{}{group}/_attr/member", self.group_prefix), Some(serde_json::json!([username]))) .await?; if !(200..300).contains(&status) { anyhow::bail!("kanidm: adding {username} to {group}: http {status}: {text}");