responsible: { name, contact, group } says the address is a group's
inbox, not a person: no account or mail is made for it, and any
signed-in member of the group gets the suggest-a-change card. A
board's shared address was onboarded as a person before this.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GT37Z1Xtfd9pUuQtMTg6Yt
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
de65392917
commit
014594283a
+35
-16
@@ -461,7 +461,7 @@ fn QuestionView(
|
||||
{question
|
||||
.responsible
|
||||
.as_ref()
|
||||
.filter(|r| is_responsible(user.as_ref(), &r.contact))
|
||||
.filter(|r| is_responsible(user.as_ref(), r))
|
||||
.map(|_| view! { <SuggestEdit question_id=question.id.clone()/> })}
|
||||
{question
|
||||
.responsible
|
||||
@@ -611,12 +611,16 @@ fn Announcements(current_id: String) -> impl IntoView {
|
||||
/// bulk scrapers regex HTML for `mailto:` strings, and this keeps one
|
||||
/// out of what they see. Not a defense against a targeted scrape, just
|
||||
/// against the generic bot crawl.
|
||||
/// Whether the signed-in person is the one a page names as responsible:
|
||||
/// their Kanidm address equals the page's `responsible.contact`
|
||||
/// (`mailto:` off, case ignored).
|
||||
pub fn is_responsible(user: Option<&User>, contact: &str) -> bool {
|
||||
let contact = contact.trim().trim_start_matches("mailto:").trim().to_lowercase();
|
||||
user.and_then(|u| u.email.as_deref()).is_some_and(|e| !contact.is_empty() && e.trim().to_lowercase() == contact)
|
||||
/// Whether the signed-in person answers for a page: their Kanidm
|
||||
/// address equals the page's `responsible.contact` (`mailto:` off,
|
||||
/// case ignored), or they are in the group that holds it.
|
||||
pub fn is_responsible(user: Option<&User>, responsible: &Responsible) -> bool {
|
||||
let Some(user) = user else { return false };
|
||||
if responsible.group.as_deref().is_some_and(|g| !g.is_empty() && user.groups.iter().any(|mine| mine == g)) {
|
||||
return true;
|
||||
}
|
||||
let contact = responsible.contact.trim().trim_start_matches("mailto:").trim().to_lowercase();
|
||||
user.email.as_deref().is_some_and(|e| !contact.is_empty() && e.trim().to_lowercase() == contact)
|
||||
}
|
||||
|
||||
/// One suggestion as the person who made it sees it: what they wrote
|
||||
@@ -2557,8 +2561,8 @@ pub async fn suggest_page_edit(question_id: String, text: String) -> Result<(),
|
||||
}
|
||||
let question = state.questions.load().get(&question_id).cloned().ok_or_else(|| ServerFnError::new("no such page"))?;
|
||||
let responsible = question.responsible.clone().ok_or_else(|| ServerFnError::new("this page names no one responsible"))?;
|
||||
if !is_responsible(user.as_ref(), &responsible.contact) {
|
||||
return Err(ServerFnError::new("only the person responsible for this page may suggest changes to it here"));
|
||||
if !is_responsible(user.as_ref(), &responsible) {
|
||||
return Err(ServerFnError::new("only whoever is responsible for this page may suggest changes to it here"));
|
||||
}
|
||||
let user = user.expect("is_responsible needs a user");
|
||||
let email = user.email.clone().unwrap_or_default();
|
||||
@@ -2901,17 +2905,32 @@ mod down_the_tree_tests {
|
||||
mod responsible_tests {
|
||||
use super::is_responsible;
|
||||
use crate::auth::User;
|
||||
use crate::content::Responsible;
|
||||
|
||||
fn user(email: Option<&str>) -> User {
|
||||
User { sub: "s".into(), username: "kari".into(), display_name: "Kari".into(), groups: vec![], email: email.map(String::from) }
|
||||
fn user(email: Option<&str>, groups: &[&str]) -> User {
|
||||
User { sub: "s".into(), username: "kari".into(), display_name: "Kari".into(), groups: groups.iter().map(|g| g.to_string()).collect(), email: email.map(String::from) }
|
||||
}
|
||||
|
||||
fn named(contact: &str, group: Option<&str>) -> Responsible {
|
||||
Responsible { name: "R".into(), contact: contact.into(), group: group.map(String::from) }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn only_the_person_a_page_names_may_suggest_a_change_to_it() {
|
||||
assert!(is_responsible(Some(&user(Some("kari@vel.no"))), "mailto:Kari@Vel.no"));
|
||||
assert!(!is_responsible(Some(&user(Some("ola@vel.no"))), "kari@vel.no"));
|
||||
assert!(!is_responsible(Some(&user(None)), "kari@vel.no"));
|
||||
assert!(!is_responsible(None, "kari@vel.no"));
|
||||
assert!(!is_responsible(Some(&user(Some(""))), ""));
|
||||
assert!(is_responsible(Some(&user(Some("kari@vel.no"), &[])), &named("mailto:Kari@Vel.no", None)));
|
||||
assert!(!is_responsible(Some(&user(Some("ola@vel.no"), &[])), &named("kari@vel.no", None)));
|
||||
assert!(!is_responsible(Some(&user(None, &[])), &named("kari@vel.no", None)));
|
||||
assert!(!is_responsible(None, &named("kari@vel.no", None)));
|
||||
assert!(!is_responsible(Some(&user(Some(""), &[])), &named("", None)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_page_a_group_answers_for_is_every_members() {
|
||||
let board = named("post@vel.no", Some("styret"));
|
||||
assert!(is_responsible(Some(&user(Some("ola@vel.no"), &["styret"])), &board));
|
||||
assert!(!is_responsible(Some(&user(Some("ola@vel.no"), &["trafikk"])), &board));
|
||||
// The inbox itself, signed in, still counts.
|
||||
assert!(is_responsible(Some(&user(Some("post@vel.no"), &[])), &board));
|
||||
assert!(!is_responsible(None, &board));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -296,6 +296,13 @@ pub struct Responsible {
|
||||
/// A mailto address or plain email - deliberately just a string,
|
||||
/// same as everywhere else content keeps contact info simple.
|
||||
pub contact: String,
|
||||
/// The group that holds the responsibility, when it is a group's
|
||||
/// and not one person's: the board's inbox, not the board's
|
||||
/// secretary. Then the address gets no account of its own (see
|
||||
/// `seed.rs`), and any signed-in member of the group may suggest a
|
||||
/// change to the page.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub group: Option<String>,
|
||||
}
|
||||
|
||||
/// Whether to render `question`'s alternatives or its sign-in gate for
|
||||
|
||||
+14
-2
@@ -20,7 +20,9 @@
|
||||
//!
|
||||
//! **Everyone the content names as responsible.** A page's
|
||||
//! `responsible: { name, contact }` is a person answering for that
|
||||
//! question, so they get an account: at start, and again whenever the
|
||||
//! question - unless `group` says it is a group's, in which case the
|
||||
//! address is an inbox and nobody is made for it. A person gets an
|
||||
//! account: at start, and again whenever the
|
||||
//! content changes, each responsible address Kanidm does not already
|
||||
//! hold is given one, and mailed that they are listed as responsible
|
||||
//! for asking those questions, with the link to sign in. Signed in,
|
||||
@@ -77,7 +79,9 @@ pub fn seed_key(email: &str, group: &str) -> String {
|
||||
|
||||
/// Everyone the content names as responsible, by address (lowercased,
|
||||
/// `mailto:` taken off): their name as the first page gives it, and the
|
||||
/// questions they answer for, in page order.
|
||||
/// questions they answer for, in page order. A page a group answers
|
||||
/// for (`responsible.group`) names nobody: the board's inbox is not a
|
||||
/// person, and its members have their accounts through the desk.
|
||||
pub fn responsible_people(questions: &HashMap<String, Question>) -> BTreeMap<String, (String, Vec<String>)> {
|
||||
let mut ids: Vec<&String> = questions.keys().collect();
|
||||
ids.sort();
|
||||
@@ -85,6 +89,9 @@ pub fn responsible_people(questions: &HashMap<String, Question>) -> BTreeMap<Str
|
||||
for id in ids {
|
||||
let q = &questions[id];
|
||||
let Some(r) = &q.responsible else { continue };
|
||||
if r.group.as_deref().is_some_and(|g| !g.trim().is_empty()) {
|
||||
continue;
|
||||
}
|
||||
let email = contact_email(&r.contact);
|
||||
let Some(email) = email else { continue };
|
||||
let entry = out.entry(email).or_insert_with(|| (r.name.trim().to_string(), Vec::new()));
|
||||
@@ -415,6 +422,11 @@ mod tests {
|
||||
assert_eq!(people.keys().collect::<Vec<_>>(), ["styret@vel.no", "trafikk@vel.no"]);
|
||||
assert_eq!(people["styret@vel.no"].1, ["Hva gjør vi?", "Hva vil du melde?"]);
|
||||
assert_eq!(contact_email("+47 900 00 000"), None);
|
||||
// A group's inbox is nobody: its members have the desk.
|
||||
let mut held: Question = serde_yaml::from_str("name: \"Hva?\"\nresponsible: { name: Styret, contact: post@vel.no, group: styret }\nalternatives:\n - name: A\n").unwrap();
|
||||
held.id = "/d".into();
|
||||
qs.insert("/d".into(), held);
|
||||
assert!(!responsible_people(&qs).contains_key("post@vel.no"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user