groupserv: gate channel access on the c flag, document !group
All checks were successful
CI / check (push) Successful in 3m43s
All checks were successful
CI / check (push) Successful in 3m43s
This commit is contained in:
parent
b87a0c56a2
commit
96654b5e5a
6 changed files with 23 additions and 14 deletions
|
|
@ -4,7 +4,7 @@ use echo_api::{Sender, ServiceCtx};
|
||||||
// ACCESS <#channel> LIST | ADD <account> <op|voice> | DEL <account>
|
// ACCESS <#channel> LIST | ADD <account> <op|voice> | DEL <account>
|
||||||
pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db: &mut dyn Store) {
|
pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db: &mut dyn Store) {
|
||||||
let Some(&chan) = args.get(1) else {
|
let Some(&chan) = args.get(1) else {
|
||||||
ctx.notice(me, from.uid, "Syntax: ACCESS <#channel> LIST | ADD <account> <op|voice> | DEL <account>");
|
ctx.notice(me, from.uid, "Syntax: ACCESS <#channel> LIST | ADD <account|!group> <op|voice> | DEL <account|!group>");
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
match args.get(2).map(|s| s.to_ascii_uppercase()).as_deref() {
|
match args.get(2).map(|s| s.to_ascii_uppercase()).as_deref() {
|
||||||
|
|
@ -50,7 +50,7 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db:
|
||||||
Err(_) => ctx.notice(me, from.uid, "Sorry, that didn't work. Please try again in a moment."),
|
Err(_) => ctx.notice(me, from.uid, "Sorry, that didn't work. Please try again in a moment."),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => ctx.notice(me, from.uid, "Syntax: ACCESS <#channel> LIST | ADD <account> <op|voice> | DEL <account>"),
|
_ => ctx.notice(me, from.uid, "Syntax: ACCESS <#channel> LIST | ADD <account|!group> <op|voice> | DEL <account|!group>"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,7 @@ const TOPICS: &[HelpEntry] = &[
|
||||||
HelpEntry { cmd: "INFO", summary: "show channel information", detail: "Syntax: \x02INFO <#channel>\x02\nShows a channel's registration and settings." },
|
HelpEntry { cmd: "INFO", summary: "show channel information", detail: "Syntax: \x02INFO <#channel>\x02\nShows a channel's registration and settings." },
|
||||||
HelpEntry { cmd: "LIST", summary: "list registered channels", detail: "Syntax: \x02LIST\x02\nLists registered channels." },
|
HelpEntry { cmd: "LIST", summary: "list registered channels", detail: "Syntax: \x02LIST\x02\nLists registered channels." },
|
||||||
HelpEntry { cmd: "SET", summary: "change founder or settings", detail: "Syntax: \x02SET <#channel> FOUNDER <account> | DESC <text> | URL [address] | EMAIL [address] | SUCCESSOR <account>|OFF | SIGNKICK|PRIVATE|PEACE|SECUREOPS|RESTRICTED|AUTOOP|KEEPTOPIC|TOPICLOCK {ON|OFF}\x02\nTransfers the founder or changes a channel setting." },
|
HelpEntry { cmd: "SET", summary: "change founder or settings", detail: "Syntax: \x02SET <#channel> FOUNDER <account> | DESC <text> | URL [address] | EMAIL [address] | SUCCESSOR <account>|OFF | SIGNKICK|PRIVATE|PEACE|SECUREOPS|RESTRICTED|AUTOOP|KEEPTOPIC|TOPICLOCK {ON|OFF}\x02\nTransfers the founder or changes a channel setting." },
|
||||||
HelpEntry { cmd: "ACCESS", summary: "manage the access list", detail: "Syntax: \x02ACCESS <#channel> LIST | ADD <account> <op|voice> | DEL <account>\x02\nManages the channel access list." },
|
HelpEntry { cmd: "ACCESS", summary: "manage the access list", detail: "Syntax: \x02ACCESS <#channel> LIST | ADD <account|!group> <op|voice> | DEL <account|!group>\x02\nManages the channel access list. The target may be a GroupServ \x02!group\x02; each of its members holding the group's \x02c\x02 flag then inherits the access." },
|
||||||
HelpEntry { cmd: "FLAGS", summary: "granular per-account flags", detail: "Syntax: \x02FLAGS <#channel> [account [+/-flags]]\x02\nViews or changes granular per-account channel flags." },
|
HelpEntry { cmd: "FLAGS", summary: "granular per-account flags", detail: "Syntax: \x02FLAGS <#channel> [account [+/-flags]]\x02\nViews or changes granular per-account channel flags." },
|
||||||
HelpEntry { cmd: "AOP/SOP/VOP", summary: "tiered access shortcuts", detail: "Syntax: \x02AOP|SOP|VOP <#channel> ADD <account> | DEL <account> | LIST\x02\nTiered shortcuts over ACCESS: AOP and SOP grant op, VOP grants voice." },
|
HelpEntry { cmd: "AOP/SOP/VOP", summary: "tiered access shortcuts", detail: "Syntax: \x02AOP|SOP|VOP <#channel> ADD <account> | DEL <account> | LIST\x02\nTiered shortcuts over ACCESS: AOP and SOP grant op, VOP grants voice." },
|
||||||
HelpEntry { cmd: "STATUS", summary: "show a user's access", detail: "Syntax: \x02STATUS <#channel> [nick]\x02\nShows a user's access level on a channel." },
|
HelpEntry { cmd: "STATUS", summary: "show a user's access", detail: "Syntax: \x02STATUS <#channel> [nick]\x02\nShows a user's access level on a channel." },
|
||||||
|
|
|
||||||
|
|
@ -25,16 +25,16 @@ mod del;
|
||||||
#[path = "flags.rs"]
|
#[path = "flags.rs"]
|
||||||
mod flags;
|
mod flags;
|
||||||
|
|
||||||
const BLURB: &str = "GroupServ manages user groups: a \x02!name\x02 owning member accounts. A channel can grant access to a group, and every member then inherits it.";
|
const BLURB: &str = "GroupServ manages user groups: a \x02!name\x02 owning member accounts. Add a group to a channel's access (\x02/msg ChanServ ACCESS #chan ADD !name op\x02) and each member holding the group's \x02c\x02 flag inherits it.";
|
||||||
|
|
||||||
const TOPICS: &[HelpEntry] = &[
|
const TOPICS: &[HelpEntry] = &[
|
||||||
HelpEntry { cmd: "REGISTER", summary: "create a group you found", detail: "Syntax: \x02REGISTER <!group>\x02\nCreates a group with you as founder. A group name starts with '!', e.g. !staff." },
|
HelpEntry { cmd: "REGISTER", summary: "create a group you found", detail: "Syntax: \x02REGISTER <!group>\x02\nCreates a group with you as founder. A group name starts with '!', e.g. !staff." },
|
||||||
HelpEntry { cmd: "DROP", summary: "delete a group you founded", detail: "Syntax: \x02DROP <!group>\x02\nDeletes a group. Founder only." },
|
HelpEntry { cmd: "DROP", summary: "delete a group you founded", detail: "Syntax: \x02DROP <!group>\x02\nDeletes a group. Founder only." },
|
||||||
HelpEntry { cmd: "INFO", summary: "show a group's founder and size", detail: "Syntax: \x02INFO <!group>\x02\nShows a group's founder and member count." },
|
HelpEntry { cmd: "INFO", summary: "show a group's founder and size", detail: "Syntax: \x02INFO <!group>\x02\nShows a group's founder and member count." },
|
||||||
HelpEntry { cmd: "LIST", summary: "list groups you belong to", detail: "Syntax: \x02LIST\x02\nLists the groups you belong to. Operators see every group." },
|
HelpEntry { cmd: "LIST", summary: "list groups you belong to", detail: "Syntax: \x02LIST\x02\nLists the groups you belong to. Operators see every group." },
|
||||||
HelpEntry { cmd: "ADD", summary: "add a member", detail: "Syntax: \x02ADD <!group> <account>\x02\nAdds a plain member. Needs the founder or the 'f' flag." },
|
HelpEntry { cmd: "ADD", summary: "add a member", detail: "Syntax: \x02ADD <!group> <account>\x02\nAdds a plain member (no flags). To let them inherit the group's channel access, give them the \x02c\x02 flag with FLAGS. Needs the founder or the 'f' flag." },
|
||||||
HelpEntry { cmd: "DEL", summary: "remove a member", detail: "Syntax: \x02DEL <!group> <account>\x02\nRemoves a member. Needs the founder or the 'f' flag." },
|
HelpEntry { cmd: "DEL", summary: "remove a member", detail: "Syntax: \x02DEL <!group> <account>\x02\nRemoves a member. Needs the founder or the 'f' flag." },
|
||||||
HelpEntry { cmd: "FLAGS", summary: "view or change group flags", detail: "Syntax: \x02FLAGS <!group> [account [+/-flags]]\x02\nLists, shows, or changes group-access flags (F founder, f manage, i invite, c channel-access, s set, m memo). Changing needs the founder or the 'f' flag." },
|
HelpEntry { cmd: "FLAGS", summary: "view or change group flags", detail: "Syntax: \x02FLAGS <!group> [account [+/-flags]]\x02\nLists, shows, or changes group-access flags (F founder, f manage, i invite, c inherit the group's channel access, s set, m memo). Changing needs the founder or the 'f' flag." },
|
||||||
];
|
];
|
||||||
|
|
||||||
pub struct GroupServ {
|
pub struct GroupServ {
|
||||||
|
|
|
||||||
|
|
@ -478,9 +478,15 @@ impl Db {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether an account is in a group (its founder or a member).
|
/// Whether an account is in a group (its founder or a member).
|
||||||
pub fn is_group_member(&self, name: &str, account: &str) -> bool {
|
// Whether `account` inherits a group's channel access (when the group is on a
|
||||||
|
// channel's access list): the group's founder, or a member holding the 'c'
|
||||||
|
// (channel-access) flag. Plain membership is not enough — the flag gates it.
|
||||||
|
fn group_grants_channel_access(&self, name: &str, account: &str) -> bool {
|
||||||
let k = key(name);
|
let k = key(name);
|
||||||
self.net.groups.iter().find(|g| key(&g.name) == k).is_some_and(|g| g.founder.eq_ignore_ascii_case(account) || g.members.iter().any(|m| m.account.eq_ignore_ascii_case(account)))
|
self.net.groups.iter().find(|g| key(&g.name) == k).is_some_and(|g| {
|
||||||
|
g.founder.eq_ignore_ascii_case(account)
|
||||||
|
|| g.members.iter().any(|m| m.account.eq_ignore_ascii_case(account) && m.flags.contains('c'))
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An account's effective channel capabilities, combining its direct access
|
/// An account's effective channel capabilities, combining its direct access
|
||||||
|
|
@ -494,7 +500,7 @@ impl Db {
|
||||||
let mut caps = Caps::default();
|
let mut caps = Caps::default();
|
||||||
for a in &c.access {
|
for a in &c.access {
|
||||||
let applies = match a.account.strip_prefix('!') {
|
let applies = match a.account.strip_prefix('!') {
|
||||||
Some(g) => self.is_group_member(&format!("!{g}"), account),
|
Some(g) => self.group_grants_channel_access(&format!("!{g}"), account),
|
||||||
None => a.account.eq_ignore_ascii_case(account),
|
None => a.account.eq_ignore_ascii_case(account),
|
||||||
};
|
};
|
||||||
if applies {
|
if applies {
|
||||||
|
|
|
||||||
|
|
@ -751,14 +751,14 @@
|
||||||
// Sanity: every reference is in place.
|
// Sanity: every reference is in place.
|
||||||
assert_eq!(db.channel("#foo").unwrap().join_mode("alice"), Some("+o"), "alice starts as an op");
|
assert_eq!(db.channel("#foo").unwrap().join_mode("alice"), Some("+o"), "alice starts as an op");
|
||||||
assert_eq!(db.channel_successor("#bar").as_deref(), Some("alice"));
|
assert_eq!(db.channel_successor("#bar").as_deref(), Some("alice"));
|
||||||
assert!(db.is_group_member("!bobgrp", "alice"));
|
assert!(db.group("!bobgrp").unwrap().members.iter().any(|m| m.account.eq_ignore_ascii_case("alice")));
|
||||||
assert!(db.group("!alicegrp").is_some());
|
assert!(db.group("!alicegrp").is_some());
|
||||||
|
|
||||||
// Dropping alice erases all of them.
|
// Dropping alice erases all of them.
|
||||||
assert!(db.drop_account("alice").unwrap());
|
assert!(db.drop_account("alice").unwrap());
|
||||||
assert_eq!(db.channel("#foo").unwrap().join_mode("alice"), None, "access grant purged");
|
assert_eq!(db.channel("#foo").unwrap().join_mode("alice"), None, "access grant purged");
|
||||||
assert_eq!(db.channel_successor("#bar"), None, "successorship purged");
|
assert_eq!(db.channel_successor("#bar"), None, "successorship purged");
|
||||||
assert!(!db.is_group_member("!bobgrp", "alice"), "group membership purged");
|
assert!(!db.group("!bobgrp").unwrap().members.iter().any(|m| m.account.eq_ignore_ascii_case("alice")), "group membership purged");
|
||||||
assert!(db.group("!alicegrp").is_none(), "founderless group dropped");
|
assert!(db.group("!alicegrp").is_none(), "founderless group dropped");
|
||||||
|
|
||||||
// Re-registering the name must not inherit the old op access.
|
// Re-registering the name must not inherit the old op access.
|
||||||
|
|
|
||||||
|
|
@ -4438,7 +4438,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// GroupServ interconnection: a channel grants access to a !group, and every
|
// GroupServ interconnection: a channel grants access to a !group, and every
|
||||||
// group member inherits that channel access (auto-op on join).
|
// group member holding the c (channel-access) flag inherits it (auto-op on join).
|
||||||
#[test]
|
#[test]
|
||||||
fn groupserv_grants_channel_access() {
|
fn groupserv_grants_channel_access() {
|
||||||
use echo_chanserv::ChanServ;
|
use echo_chanserv::ChanServ;
|
||||||
|
|
@ -4474,8 +4474,11 @@
|
||||||
assert!(has(&gs(&mut e, "000AAAAAA", "ADD !team bob"), "Added"), "bob added");
|
assert!(has(&gs(&mut e, "000AAAAAA", "ADD !team bob"), "Added"), "bob added");
|
||||||
assert!(has(&cs(&mut e, "000AAAAAA", "FLAGS #room !team +o"), "now holds"), "group granted channel op");
|
assert!(has(&cs(&mut e, "000AAAAAA", "FLAGS #room !team +o"), "now holds"), "group granted channel op");
|
||||||
|
|
||||||
// bob, a group member, is auto-opped on join; carol (not in the group) isn't.
|
// A member without the c (channel-access) flag does NOT inherit; +c makes
|
||||||
assert!(opped(&e.handle(NetEvent::Join { uid: "000AAAAAB".into(), channel: "#room".into(), op: false }), "000AAAAAB"), "group member auto-opped");
|
// bob auto-opped on join. carol (not in the group) never is.
|
||||||
|
assert!(!opped(&e.handle(NetEvent::Join { uid: "000AAAAAB".into(), channel: "#room".into(), op: false }), "000AAAAAB"), "no c flag, no access");
|
||||||
|
gs(&mut e, "000AAAAAA", "FLAGS !team bob +c");
|
||||||
|
assert!(opped(&e.handle(NetEvent::Join { uid: "000AAAAAB".into(), channel: "#room".into(), op: false }), "000AAAAAB"), "c flag -> auto-opped");
|
||||||
assert!(!opped(&e.handle(NetEvent::Join { uid: "000AAAAAC".into(), channel: "#room".into(), op: false }), "000AAAAAC"), "non-member not opped");
|
assert!(!opped(&e.handle(NetEvent::Join { uid: "000AAAAAC".into(), channel: "#room".into(), op: false }), "000AAAAAC"), "non-member not opped");
|
||||||
|
|
||||||
// Removing bob from the group revokes his inherited channel access.
|
// Removing bob from the group revokes his inherited channel access.
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue