The header knows who is here; my cases; controls that scale #10
@@ -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.
|
- 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.
|
- "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.
|
- 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.
|
- 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)
|
## 0.6.1 (2026-09-30)
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ values.
|
|||||||
| `KANIDM_URL`, `OAUTH2_CLIENT_ID`, `OAUTH2_CLIENT_SECRET` | sign-in |
|
| `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 |
|
| `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_API_TOKEN` | invites and `grants:` through Kanidm |
|
||||||
|
| `KANIDM_GROUP_PREFIX` | for a Kanidm shared with other sites: the content's `kunder` is the group `<prefix>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 |
|
| `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 |
|
| `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`) |
|
| `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`) |
|
||||||
|
|||||||
+11
-4
@@ -184,6 +184,12 @@ fn name_from_search(body: &str) -> Option<String> {
|
|||||||
pub struct Kanidm {
|
pub struct Kanidm {
|
||||||
base: String,
|
base: String,
|
||||||
token: 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 {
|
impl Kanidm {
|
||||||
@@ -192,7 +198,8 @@ impl Kanidm {
|
|||||||
pub fn from_env() -> Option<Self> {
|
pub fn from_env() -> Option<Self> {
|
||||||
let base = std::env::var("KANIDM_URL").ok()?.trim_end_matches('/').to_string();
|
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())?;
|
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<serde_json::Value>) -> anyhow::Result<(u16, String)> {
|
async fn call(&self, method: openidconnect::reqwest::Method, path: &str, body: Option<serde_json::Value>) -> anyhow::Result<(u16, String)> {
|
||||||
@@ -262,7 +269,7 @@ impl Kanidm {
|
|||||||
|
|
||||||
/// Who is in a group, by account name.
|
/// Who is in a group, by account name.
|
||||||
pub async fn members(&self, group: &str) -> anyhow::Result<Vec<String>> {
|
pub async fn members(&self, group: &str) -> anyhow::Result<Vec<String>> {
|
||||||
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) {
|
if !(200..300).contains(&status) {
|
||||||
anyhow::bail!("kanidm: reading the members of {group}: http {status}: {text}");
|
anyhow::bail!("kanidm: reading the members of {group}: http {status}: {text}");
|
||||||
}
|
}
|
||||||
@@ -273,7 +280,7 @@ impl Kanidm {
|
|||||||
/// service account manages, like adding them was.
|
/// service account manages, like adding them was.
|
||||||
pub async fn remove_member(&self, group: &str, username: &str) -> anyhow::Result<()> {
|
pub async fn remove_member(&self, group: &str, username: &str) -> anyhow::Result<()> {
|
||||||
let (status, text) = self
|
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?;
|
.await?;
|
||||||
if !(200..300).contains(&status) {
|
if !(200..300).contains(&status) {
|
||||||
anyhow::bail!("kanidm: removing {username} from {group}: http {status}: {text}");
|
anyhow::bail!("kanidm: removing {username} from {group}: http {status}: {text}");
|
||||||
@@ -340,7 +347,7 @@ impl Kanidm {
|
|||||||
// tidying up after itself.
|
// tidying up after itself.
|
||||||
already_member = !created && self.members(group).await.is_ok_and(|m| m.contains(&username));
|
already_member = !created && self.members(group).await.is_ok_and(|m| m.contains(&username));
|
||||||
let (status, text) = self
|
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?;
|
.await?;
|
||||||
if !(200..300).contains(&status) {
|
if !(200..300).contains(&status) {
|
||||||
anyhow::bail!("kanidm: adding {username} to {group}: http {status}: {text}");
|
anyhow::bail!("kanidm: adding {username} to {group}: http {status}: {text}");
|
||||||
|
|||||||
Reference in New Issue
Block a user