Add three-tier services operator model (operator, administrator, root)
All checks were successful
CI / check (push) Successful in 4m22s
All checks were successful
CI / check (push) Successful in 4m22s
This commit is contained in:
parent
a10662bbc6
commit
fc214ab0d3
20 changed files with 177 additions and 74 deletions
106
api/src/lib.rs
106
api/src/lib.rs
|
|
@ -317,33 +317,54 @@ impl ChanModeCap {
|
||||||
|
|
||||||
// A services-operator privilege. Coarse and typed (not a string namespace), so
|
// A services-operator privilege. Coarse and typed (not a string namespace), so
|
||||||
// the compiler checks every use and there is one gating mechanism, not two.
|
// the compiler checks every use and there is one gating mechanism, not two.
|
||||||
|
// A services-operator privilege, ordered as tiers: a higher one implies the lower
|
||||||
|
// ones (see `Privs::has`), so three named tiers — operator, administrator, root —
|
||||||
|
// fall out of single privileges.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum Priv {
|
pub enum Priv {
|
||||||
// See other users' hidden info (INFO fields, LIST filters).
|
// See other users' hidden info (INFO fields, LIST filters).
|
||||||
Auspex,
|
Auspex,
|
||||||
|
// Front-line moderation: network bans (AKILL/x-lines), KICK, KILL, SESSION,
|
||||||
|
// IGNORE, MODE, SPAMFILTER, NOTIFY, stats — the day-to-day operator tools.
|
||||||
|
Oper,
|
||||||
// Suspend / unsuspend accounts and channels.
|
// Suspend / unsuspend accounts and channels.
|
||||||
Suspend,
|
Suspend,
|
||||||
// Modify or drop other users' accounts and channels (SASET, DROP, GETEMAIL).
|
// Persistent data + broadcast: modify/drop accounts+channels (SASET, DROP,
|
||||||
|
// GETEMAIL, NOEXPIRE), FORBID, GLOBAL, DEFCON, SVS, JUPE, and the BotServ /
|
||||||
|
// MemoServ / InfoServ staff tools.
|
||||||
Admin,
|
Admin,
|
||||||
|
// Control the services daemon and the oper roster: OPER (grant/revoke opers),
|
||||||
|
// SHUTDOWN / RESTART, REHASH, OperServ SET, DebugServ.
|
||||||
|
Root,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Priv {
|
impl Priv {
|
||||||
/// Every privilege, for iteration and validation.
|
/// Every privilege, low tier to high, for iteration and validation.
|
||||||
pub const ALL: [Priv; 3] = [Priv::Auspex, Priv::Suspend, Priv::Admin];
|
pub const ALL: [Priv; 5] = [Priv::Auspex, Priv::Oper, Priv::Suspend, Priv::Admin, Priv::Root];
|
||||||
|
|
||||||
/// This privilege's canonical name — the `[[oper]]` config token. Exhaustive,
|
/// This privilege's canonical name — the `[[oper]]` config token. Exhaustive,
|
||||||
/// so adding a variant forces adding its name (no silent gap).
|
/// so adding a variant forces adding its name (no silent gap).
|
||||||
pub fn name(self) -> &'static str {
|
pub fn name(self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
Priv::Auspex => "auspex",
|
Priv::Auspex => "auspex",
|
||||||
|
Priv::Oper => "oper",
|
||||||
Priv::Suspend => "suspend",
|
Priv::Suspend => "suspend",
|
||||||
Priv::Admin => "admin",
|
Priv::Admin => "admin",
|
||||||
|
Priv::Root => "root",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse a privilege name (case-insensitive); `None` if unrecognised.
|
/// Parse a privilege name (case-insensitive), accepting the tier aliases
|
||||||
|
/// `operator`/`administrator`; `None` if unrecognised.
|
||||||
pub fn from_name(s: &str) -> Option<Priv> {
|
pub fn from_name(s: &str) -> Option<Priv> {
|
||||||
Self::ALL.into_iter().find(|p| s.eq_ignore_ascii_case(p.name()))
|
match s.to_ascii_lowercase().as_str() {
|
||||||
|
"auspex" => Some(Priv::Auspex),
|
||||||
|
"oper" | "operator" => Some(Priv::Oper),
|
||||||
|
"suspend" => Some(Priv::Suspend),
|
||||||
|
"admin" | "administrator" => Some(Priv::Admin),
|
||||||
|
"root" => Some(Priv::Root),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The recognised names as a comma list, for "valid: …" error messages.
|
/// The recognised names as a comma list, for "valid: …" error messages.
|
||||||
|
|
@ -358,13 +379,45 @@ impl Priv {
|
||||||
pub struct Privs(u8);
|
pub struct Privs(u8);
|
||||||
|
|
||||||
impl Privs {
|
impl Privs {
|
||||||
|
fn bit(p: Priv) -> u8 {
|
||||||
|
1 << p as u8
|
||||||
|
}
|
||||||
// Grant a privilege (builder style: `Privs::default().with(Priv::Admin)`).
|
// Grant a privilege (builder style: `Privs::default().with(Priv::Admin)`).
|
||||||
pub fn with(self, p: Priv) -> Self {
|
pub fn with(self, p: Priv) -> Self {
|
||||||
Privs(self.0 | (1 << p as u8))
|
Privs(self.0 | Self::bit(p))
|
||||||
}
|
}
|
||||||
// Whether this set holds `p`.
|
// Whether this set can exercise `p`, honouring the tier chain: a higher tier
|
||||||
|
// implies the lower ones — Root ⊃ Admin ⊃ Oper ⊃ Auspex — and Admin also bundles
|
||||||
|
// Suspend. So an `admin` gates through an `oper`-level command and can view, but
|
||||||
|
// a stand-alone `suspend` grant is only that (it doesn't imply Auspex).
|
||||||
pub fn has(self, p: Priv) -> bool {
|
pub fn has(self, p: Priv) -> bool {
|
||||||
self.0 & (1 << p as u8) != 0
|
let b = Self::bit;
|
||||||
|
let mask = match p {
|
||||||
|
Priv::Auspex => b(Priv::Auspex) | b(Priv::Oper) | b(Priv::Admin) | b(Priv::Root),
|
||||||
|
Priv::Oper => b(Priv::Oper) | b(Priv::Admin) | b(Priv::Root),
|
||||||
|
Priv::Suspend => b(Priv::Suspend) | b(Priv::Admin) | b(Priv::Root),
|
||||||
|
Priv::Admin => b(Priv::Admin) | b(Priv::Root),
|
||||||
|
Priv::Root => b(Priv::Root),
|
||||||
|
};
|
||||||
|
self.0 & mask != 0
|
||||||
|
}
|
||||||
|
// Whether `p` was granted literally (no tier implication) — for display, not
|
||||||
|
// gating.
|
||||||
|
pub fn contains(self, p: Priv) -> bool {
|
||||||
|
self.0 & Self::bit(p) != 0
|
||||||
|
}
|
||||||
|
// The named tier this set belongs to, by its highest granted privilege.
|
||||||
|
pub fn tier(self) -> &'static str {
|
||||||
|
if self.contains(Priv::Root) {
|
||||||
|
"Services Root"
|
||||||
|
} else if self.contains(Priv::Admin) {
|
||||||
|
"Services Administrator"
|
||||||
|
} else if self.any() {
|
||||||
|
// oper, or an auspex/suspend-only grant — a (limited) operator.
|
||||||
|
"Services Operator"
|
||||||
|
} else {
|
||||||
|
"not an operator"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Whether this is a services operator at all (holds any privilege).
|
// Whether this is a services operator at all (holds any privilege).
|
||||||
pub fn any(self) -> bool {
|
pub fn any(self) -> bool {
|
||||||
|
|
@ -397,9 +450,9 @@ impl Privs {
|
||||||
Self::parse_names(names).0
|
Self::parse_names(names).0
|
||||||
}
|
}
|
||||||
|
|
||||||
// The privilege names held, for display.
|
// The privilege names granted literally (not implied), for display.
|
||||||
pub fn names(self) -> Vec<&'static str> {
|
pub fn names(self) -> Vec<&'static str> {
|
||||||
Priv::ALL.into_iter().filter(|p| self.has(*p)).map(Priv::name).collect()
|
Priv::ALL.into_iter().filter(|p| self.contains(*p)).map(Priv::name).collect()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2533,25 +2586,38 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn privs_bitset_grants_and_checks() {
|
fn privs_are_tiered_and_imply_the_lower_ones() {
|
||||||
let p = Privs::default().with(Priv::Auspex).with(Priv::Admin);
|
let admin = Privs::default().with(Priv::Admin);
|
||||||
assert!(p.has(Priv::Auspex) && p.has(Priv::Admin));
|
// Admin is a tier: it can exercise the lower privileges too.
|
||||||
assert!(!p.has(Priv::Suspend), "only granted privileges are held");
|
assert!(admin.has(Priv::Admin) && admin.has(Priv::Suspend) && admin.has(Priv::Oper) && admin.has(Priv::Auspex));
|
||||||
assert!(p.any());
|
// but not the tier above it,
|
||||||
|
assert!(!admin.has(Priv::Root));
|
||||||
|
// and `contains` reports only what was granted literally (for display).
|
||||||
|
assert!(admin.contains(Priv::Admin) && !admin.contains(Priv::Suspend));
|
||||||
|
assert_eq!(admin.tier(), "Services Administrator");
|
||||||
|
|
||||||
|
// An operator can moderate but not administrate; root can do everything.
|
||||||
|
let oper = Privs::default().with(Priv::Oper);
|
||||||
|
assert!(oper.has(Priv::Oper) && oper.has(Priv::Auspex) && !oper.has(Priv::Suspend) && !oper.has(Priv::Admin));
|
||||||
|
assert_eq!(oper.tier(), "Services Operator");
|
||||||
|
let root = Privs::default().with(Priv::Root);
|
||||||
|
assert!(root.has(Priv::Root) && root.has(Priv::Admin) && root.has(Priv::Oper) && root.has(Priv::Auspex));
|
||||||
|
assert_eq!(root.tier(), "Services Root");
|
||||||
|
|
||||||
assert!(!Privs::default().any(), "empty set is not an oper");
|
assert!(!Privs::default().any(), "empty set is not an oper");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_names_reports_typos_instead_of_dropping_them() {
|
fn parse_names_reports_typos_instead_of_dropping_them() {
|
||||||
// Known names are parsed (case-insensitive); unknown ones are collected so
|
// Known names are parsed (case-insensitive); unknown ones are collected so
|
||||||
// a config typo is surfaced, not silently ignored.
|
// a config typo is surfaced, not silently ignored. "root" is a real priv now.
|
||||||
let (privs, unknown) = Privs::parse_names(&["Admin", "auspex", "admn", "root"]);
|
let (privs, unknown) = Privs::parse_names(&["Admin", "auspex", "admn", "root"]);
|
||||||
assert!(privs.has(Priv::Admin) && privs.has(Priv::Auspex));
|
assert!(privs.contains(Priv::Admin) && privs.contains(Priv::Auspex) && privs.contains(Priv::Root));
|
||||||
assert!(!privs.has(Priv::Suspend));
|
assert_eq!(unknown, vec!["admn".to_string()]);
|
||||||
assert_eq!(unknown, vec!["admn".to_string(), "root".to_string()]);
|
|
||||||
assert_eq!(Priv::from_name("SUSPEND"), Some(Priv::Suspend));
|
assert_eq!(Priv::from_name("SUSPEND"), Some(Priv::Suspend));
|
||||||
|
assert_eq!(Priv::from_name("administrator"), Some(Priv::Admin)); // tier alias
|
||||||
assert_eq!(Priv::from_name("nope"), None);
|
assert_eq!(Priv::from_name("nope"), None);
|
||||||
assert_eq!(Priv::valid_names(), "auspex, suspend, admin");
|
assert_eq!(Priv::valid_names(), "auspex, oper, suspend, admin, root");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
|
|
@ -42,12 +42,18 @@ protocol = 1206 # InspIRCd link protocol version (1206 = insp4, 1205
|
||||||
# [modules]
|
# [modules]
|
||||||
# services = ["nickserv", "chanserv"]
|
# services = ["nickserv", "chanserv"]
|
||||||
|
|
||||||
# Services operators: accounts granted privileges. Omit for a network with no
|
# Services operators, in three tiers of increasing power:
|
||||||
# opers. Privileges: "auspex" (see others' hidden info), "suspend"
|
# operator — day-to-day moderation: view hidden info, network bans, kick,
|
||||||
# (suspend/unsuspend), "admin" (modify or drop other accounts/channels).
|
# kill, session limits, ignores, mode, spam filters, log search.
|
||||||
|
# administrator — the above plus account/channel data: suspend, drop, set flags,
|
||||||
|
# forbid, global notices, defcon, memo/info/bot administration.
|
||||||
|
# root — the above plus daemon control: add/remove opers, set, rehash,
|
||||||
|
# restart, shutdown, the activity feed.
|
||||||
|
# Name a tier with `type`, or list individual privileges with `privs` (auspex,
|
||||||
|
# oper, suspend, admin, root). Omit the whole block for a network with no opers.
|
||||||
# [[oper]]
|
# [[oper]]
|
||||||
# account = "yournick"
|
# account = "yournick"
|
||||||
# privs = ["auspex", "suspend", "admin"]
|
# type = "root"
|
||||||
|
|
||||||
# Staff audit feed: notable service actions (registrations, drops, vhosts,
|
# Staff audit feed: notable service actions (registrations, drops, vhosts,
|
||||||
# suspensions, akicks, access and bot changes, oper host config) are announced
|
# suspensions, akicks, access and bot changes, oper host config) are announced
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,8 @@ use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
// drop commands from a matching user. A mask with an '@' matches nick!*@host
|
// drop commands from a matching user. A mask with an '@' matches nick!*@host
|
||||||
// (ident isn't tracked); a bare mask matches the nick. Admin-only, node-local.
|
// (ident isn't tracked); a bare mask matches the nick. Admin-only, node-local.
|
||||||
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) {
|
||||||
if !from.privs.has(Priv::Admin) {
|
if !from.privs.has(Priv::Oper) {
|
||||||
ctx.notice(me, from.uid, "Access denied — IGNORE needs the \x02admin\x02 privilege.");
|
ctx.notice(me, from.uid, "Access denied — IGNORE needs the \x02operator\x02 privilege.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
match args.get(1).map(|s| s.to_ascii_uppercase()).as_deref() {
|
match args.get(1).map(|s| s.to_ascii_uppercase()).as_deref() {
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,8 @@ use echo_api::{Priv, Sender, ServiceCtx, Store};
|
||||||
// note to an account or channel (a `#name` is a channel, else an account). The
|
// note to an account or channel (a `#name` is a channel, else an account). The
|
||||||
// note is shown to operators in that service's INFO. Admin-only.
|
// note is shown to operators in that service's INFO. Admin-only.
|
||||||
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) {
|
||||||
if !from.privs.has(Priv::Admin) {
|
if !from.privs.has(Priv::Oper) {
|
||||||
ctx.notice(me, from.uid, "Access denied — INFO needs the \x02admin\x02 privilege.");
|
ctx.notice(me, from.uid, "Access denied — INFO needs the \x02operator\x02 privilege.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
match args.get(1).map(|s| s.to_ascii_uppercase()).as_deref() {
|
match args.get(1).map(|s| s.to_ascii_uppercase()).as_deref() {
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,8 @@ use echo_api::{NetView, Priv, Sender, ServiceCtx};
|
||||||
// KICK <#channel> <nick> [reason]: remove a user from a channel, sourced from
|
// KICK <#channel> <nick> [reason]: remove a user from a channel, sourced from
|
||||||
// OperServ so it's clearly a staff action. Admin-only.
|
// OperServ so it's clearly a staff action. Admin-only.
|
||||||
pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, net: &dyn NetView) {
|
pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, net: &dyn NetView) {
|
||||||
if !from.privs.has(Priv::Admin) {
|
if !from.privs.has(Priv::Oper) {
|
||||||
ctx.notice(me, from.uid, "Access denied — KICK needs the \x02admin\x02 privilege.");
|
ctx.notice(me, from.uid, "Access denied — KICK needs the \x02operator\x02 privilege.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let Some(&chan) = args.get(1).filter(|c| c.starts_with('#') || c.starts_with('&')) else {
|
let Some(&chan) = args.get(1).filter(|c| c.starts_with('#') || c.starts_with('&')) else {
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,8 @@ use echo_api::{NetView, Priv, Sender, ServiceCtx};
|
||||||
|
|
||||||
// KILL <nick> [reason]: disconnect a user from the network. Admin-only.
|
// KILL <nick> [reason]: disconnect a user from the network. Admin-only.
|
||||||
pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, net: &dyn NetView) {
|
pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, net: &dyn NetView) {
|
||||||
if !from.privs.has(Priv::Admin) {
|
if !from.privs.has(Priv::Oper) {
|
||||||
ctx.notice(me, from.uid, "Access denied — KILL needs the \x02admin\x02 privilege.");
|
ctx.notice(me, from.uid, "Access denied — KILL needs the \x02operator\x02 privilege.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let Some(&target) = args.get(1) else {
|
let Some(&target) = args.get(1) else {
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,8 @@ use echo_api::{NetView, Priv, Sender, ServiceCtx, Store};
|
||||||
// (forced, so it applies regardless of the current TS). Admin-only. Status-mode
|
// (forced, so it applies regardless of the current TS). Admin-only. Status-mode
|
||||||
// targets may be given as nicks — they're resolved to uids for the ircd.
|
// targets may be given as nicks — they're resolved to uids for the ircd.
|
||||||
pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, net: &dyn NetView, db: &dyn Store) {
|
pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, net: &dyn NetView, db: &dyn Store) {
|
||||||
if !from.privs.has(Priv::Admin) {
|
if !from.privs.has(Priv::Oper) {
|
||||||
ctx.notice(me, from.uid, "Access denied — MODE needs the \x02admin\x02 privilege.");
|
ctx.notice(me, from.uid, "Access denied — MODE needs the \x02operator\x02 privilege.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let Some(&chan) = args.get(1).filter(|c| c.starts_with('#') || c.starts_with('&')) else {
|
let Some(&chan) = args.get(1).filter(|c| c.starts_with('#') || c.starts_with('&')) else {
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,8 @@ const ALL_FLAGS: &str = "cdjkmnoptusS";
|
||||||
// An oper watch list: users matching a mask have their flagged events announced
|
// An oper watch list: users matching a mask have their flagged events announced
|
||||||
// to the staff feed. Admin-only, like the rest of the ban family.
|
// to the staff feed. Admin-only, like the rest of the ban family.
|
||||||
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) {
|
||||||
if !from.privs.has(Priv::Admin) {
|
if !from.privs.has(Priv::Oper) {
|
||||||
ctx.notice(me, from.uid, "Access denied — NOTIFY needs the \x02admin\x02 privilege.");
|
ctx.notice(me, from.uid, "Access denied — NOTIFY needs the \x02operator\x02 privilege.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
match args.get(1).map(|s| s.to_ascii_uppercase()).as_deref() {
|
match args.get(1).map(|s| s.to_ascii_uppercase()).as_deref() {
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,21 @@
|
||||||
use echo_api::{parse_duration, Priv, Sender, ServiceCtx, Store};
|
use echo_api::{parse_duration, Priv, Privs, Sender, ServiceCtx, Store};
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
// OPER ADD <account> <priv[,priv…]> [+duration] | OPER DEL <account> | OPER LIST:
|
// OPER ADD <account> <priv[,priv…]> [+duration] | OPER DEL <account> | OPER LIST:
|
||||||
// manage runtime services operators (merged with the declarative config opers).
|
// manage runtime services operators (merged with the declarative config opers).
|
||||||
// The privileges are auspex, suspend, admin; a +duration makes the grant expire.
|
// Privileges (low to high): auspex, oper, suspend, admin, root — or name a tier
|
||||||
// Admin-only — only an admin grants oper.
|
// directly (operator/administrator/root), which grants the matching privilege.
|
||||||
|
// Root-only — granting operators is the top-tier power.
|
||||||
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) {
|
||||||
if !from.privs.has(Priv::Admin) {
|
if !from.privs.has(Priv::Root) {
|
||||||
ctx.notice(me, from.uid, "Access denied — OPER needs the \x02admin\x02 privilege.");
|
ctx.notice(me, from.uid, "Access denied — OPER needs the \x02root\x02 privilege.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
match args.get(1).map(|s| s.to_ascii_uppercase()).as_deref() {
|
match args.get(1).map(|s| s.to_ascii_uppercase()).as_deref() {
|
||||||
Some("ADD") | Some("SET") => add(me, from, args.get(2).copied(), args.get(3).copied(), args.get(4).copied(), ctx, db),
|
Some("ADD") | Some("SET") => add(me, from, args.get(2).copied(), args.get(3).copied(), args.get(4).copied(), ctx, db),
|
||||||
Some("DEL") | Some("REMOVE") => del(me, from, args.get(2).copied(), ctx, db),
|
Some("DEL") | Some("REMOVE") => del(me, from, args.get(2).copied(), ctx, db),
|
||||||
Some("LIST") | Some("VIEW") => list(me, from, ctx, db),
|
Some("LIST") | Some("VIEW") => list(me, from, ctx, db),
|
||||||
_ => ctx.notice(me, from.uid, "Syntax: OPER ADD <account> <priv[,priv]> [+duration] | OPER DEL <account> | OPER LIST — privs: auspex, suspend, admin"),
|
_ => ctx.notice(me, from.uid, "Syntax: OPER ADD <account> <priv[,priv] | tier> [+duration] | OPER DEL <account> | OPER LIST — tiers: operator, administrator, root; privs: auspex, oper, suspend, admin, root"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -76,7 +77,8 @@ fn list(me: &str, from: &Sender, ctx: &mut ServiceCtx, db: &mut dyn Store) {
|
||||||
}
|
}
|
||||||
for (account, privs, expires) in &opers {
|
for (account, privs, expires) in &opers {
|
||||||
let window = if expires.is_some() { " (temporary)" } else { "" };
|
let window = if expires.is_some() { " (temporary)" } else { "" };
|
||||||
ctx.notice(me, from.uid, format!(" \x02{account}\x02 — {}{window}", privs.join(", ")));
|
let tier = Privs::from_names(privs).tier();
|
||||||
|
ctx.notice(me, from.uid, format!(" \x02{account}\x02 [{tier}] — {}{window}", privs.join(", ")));
|
||||||
}
|
}
|
||||||
ctx.notice(me, from.uid, format!("End of runtime operator list ({} shown).", opers.len()));
|
ctx.notice(me, from.uid, format!("End of runtime operator list ({} shown).", opers.len()));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,14 @@
|
||||||
use echo_api::{Sender, ServiceCtx};
|
use echo_api::{Priv, Sender, ServiceCtx};
|
||||||
|
|
||||||
// REDACT <#channel|nick> <msgid> [reason]: delete a message by its IRCv3 msgid.
|
// REDACT <#channel|nick> <msgid> [reason]: delete a message by its IRCv3 msgid.
|
||||||
// The msgid comes from the reporting oper's client (which saw the tagged message);
|
// The msgid comes from the reporting oper's client (which saw the tagged message);
|
||||||
// echo relays it as a server-trusted redaction, so no ircd oper privilege is needed
|
// echo relays it as a server-trusted redaction, so no ircd oper privilege is needed
|
||||||
// (draft/message-redaction). One-shot — nothing is stored.
|
// (draft/message-redaction). One-shot — nothing is stored.
|
||||||
pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx) {
|
pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx) {
|
||||||
|
if !from.privs.has(Priv::Oper) {
|
||||||
|
ctx.notice(me, from.uid, "Access denied — REDACT needs the \x02operator\x02 privilege.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
let (Some(&target), Some(&msgid)) = (args.get(1), args.get(2)) else {
|
let (Some(&target), Some(&msgid)) = (args.get(1), args.get(2)) else {
|
||||||
ctx.notice(me, from.uid, "Syntax: REDACT <#channel|nick> <msgid> [reason]");
|
ctx.notice(me, from.uid, "Syntax: REDACT <#channel|nick> <msgid> [reason]");
|
||||||
return;
|
return;
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,8 @@ use echo_api::{Priv, Sender, ServiceCtx};
|
||||||
// endpoint still need a RESTART. The engine does the re-read off the command
|
// endpoint still need a RESTART. The engine does the re-read off the command
|
||||||
// path and notices the outcome (or a parse error, keeping the old config).
|
// path and notices the outcome (or a parse error, keeping the old config).
|
||||||
pub fn handle(me: &str, from: &Sender, ctx: &mut ServiceCtx) {
|
pub fn handle(me: &str, from: &Sender, ctx: &mut ServiceCtx) {
|
||||||
if !from.privs.has(Priv::Admin) {
|
if !from.privs.has(Priv::Root) {
|
||||||
ctx.notice(me, from.uid, "Access denied — REHASH needs the \x02admin\x02 privilege.");
|
ctx.notice(me, from.uid, "Access denied — REHASH needs the \x02root\x02 privilege.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
ctx.rehash(me, from.uid);
|
ctx.rehash(me, from.uid);
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,8 @@ use echo_api::{NetView, Priv, Sender, ServiceCtx, Store};
|
||||||
// EXCEPTION ADD <ip-mask> <limit> [reason] | DEL <ip-mask> | LIST: manage the
|
// EXCEPTION ADD <ip-mask> <limit> [reason] | DEL <ip-mask> | LIST: manage the
|
||||||
// per-IP-mask allowances that override the default limit. All admin-only.
|
// per-IP-mask allowances that override the default limit. All admin-only.
|
||||||
pub fn handle_session(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, net: &dyn NetView) {
|
pub fn handle_session(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, net: &dyn NetView) {
|
||||||
if !from.privs.has(Priv::Admin) {
|
if !from.privs.has(Priv::Oper) {
|
||||||
ctx.notice(me, from.uid, "Access denied — SESSION needs the \x02admin\x02 privilege.");
|
ctx.notice(me, from.uid, "Access denied — SESSION needs the \x02operator\x02 privilege.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
match args.get(1).map(|s| s.to_ascii_uppercase()).as_deref() {
|
match args.get(1).map(|s| s.to_ascii_uppercase()).as_deref() {
|
||||||
|
|
@ -33,7 +33,7 @@ pub fn handle_session(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceC
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn handle_exception(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db: &mut dyn Store) {
|
pub fn handle_exception(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db: &mut dyn Store) {
|
||||||
if !from.privs.has(Priv::Admin) {
|
if !from.privs.has(Priv::Oper) {
|
||||||
ctx.notice(me, from.uid, "Access denied — EXCEPTION needs the \x02admin\x02 privilege.");
|
ctx.notice(me, from.uid, "Access denied — EXCEPTION needs the \x02admin\x02 privilege.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,8 @@ use echo_api::{Priv, Sender, ServiceCtx, Store};
|
||||||
// While on, no new registrations or changes are accepted; existing data and
|
// While on, no new registrations or changes are accepted; existing data and
|
||||||
// gossip replication keep working. Admin-only. No argument reports the state.
|
// gossip replication keep working. Admin-only. No argument reports the state.
|
||||||
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) {
|
||||||
if !from.privs.has(Priv::Admin) {
|
if !from.privs.has(Priv::Root) {
|
||||||
ctx.notice(me, from.uid, "Access denied — SET is for services operators.");
|
ctx.notice(me, from.uid, "Access denied — SET needs the \x02root\x02 privilege.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
match args.get(1).map(|s| s.to_ascii_uppercase()).as_deref() {
|
match args.get(1).map(|s| s.to_ascii_uppercase()).as_deref() {
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,8 @@ use echo_api::{Priv, Sender, ServiceCtx};
|
||||||
// first so users know why services vanished.
|
// first so users know why services vanished.
|
||||||
pub fn handle(me: &str, from: &Sender, restart: bool, args: &[&str], ctx: &mut ServiceCtx) {
|
pub fn handle(me: &str, from: &Sender, restart: bool, args: &[&str], ctx: &mut ServiceCtx) {
|
||||||
let cmd = if restart { "RESTART" } else { "SHUTDOWN" };
|
let cmd = if restart { "RESTART" } else { "SHUTDOWN" };
|
||||||
if !from.privs.has(Priv::Admin) {
|
if !from.privs.has(Priv::Root) {
|
||||||
ctx.notice(me, from.uid, format!("Access denied — {cmd} needs the \x02admin\x02 privilege."));
|
ctx.notice(me, from.uid, format!("Access denied — {cmd} needs the \x02root\x02 privilege."));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let reason = if args.len() > 1 { args[1..].join(" ") } else { format!("Services {}", if restart { "restarting" } else { "shutting down" }) };
|
let reason = if args.len() > 1 { args[1..].join(" ") } else { format!("Services {}", if restart { "restarting" } else { "shutting down" }) };
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,8 @@ const DEFAULT_FLAGS: &str = "*";
|
||||||
// expression (the ircd's filter engine), e.g. `.*free.*bitcoin.*`. `action` is what
|
// expression (the ircd's filter engine), e.g. `.*free.*bitcoin.*`. `action` is what
|
||||||
// happens on a match: gline / zline / block / silent / kill / shun / warn / none.
|
// happens on a match: gline / zline / block / silent / kill / shun / warn / none.
|
||||||
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) {
|
||||||
if !from.privs.has(Priv::Admin) {
|
if !from.privs.has(Priv::Oper) {
|
||||||
ctx.notice(me, from.uid, "Access denied — SPAMFILTER needs the \x02admin\x02 privilege.");
|
ctx.notice(me, from.uid, "Access denied — SPAMFILTER needs the \x02operator\x02 privilege.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
match args.get(1).map(|s| s.to_ascii_uppercase()).as_deref() {
|
match args.get(1).map(|s| s.to_ascii_uppercase()).as_deref() {
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,13 @@
|
||||||
use echo_api::{Sender, ServiceCtx, Store, XlineKind};
|
use echo_api::{Priv, Sender, ServiceCtx, Store, XlineKind};
|
||||||
|
|
||||||
// STATS: an at-a-glance summary of the enforcement state OperServ holds — how
|
// STATS: an at-a-glance summary of the enforcement state OperServ holds — how
|
||||||
// many network bans of each kind and how many services ignores are live.
|
// many network bans of each kind and how many services ignores are live.
|
||||||
// Read-only, so any operator may run it (the module is already oper-gated).
|
// Read-only, so any operator may run it (the module is already oper-gated).
|
||||||
pub fn handle(me: &str, from: &Sender, db: &mut dyn Store, ctx: &mut ServiceCtx) {
|
pub fn handle(me: &str, from: &Sender, db: &mut dyn Store, ctx: &mut ServiceCtx) {
|
||||||
|
if !from.privs.has(Priv::Oper) {
|
||||||
|
ctx.notice(me, from.uid, "Access denied — STATS needs the \x02operator\x02 privilege.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
let akills = db.akills();
|
let akills = db.akills();
|
||||||
let glines = akills.iter().filter(|a| a.kind == XlineKind::Gline).count();
|
let glines = akills.iter().filter(|a| a.kind == XlineKind::Gline).count();
|
||||||
let qlines = akills.iter().filter(|a| a.kind == XlineKind::Qline).count();
|
let qlines = akills.iter().filter(|a| a.kind == XlineKind::Qline).count();
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
use echo_api::{parse_duration, Sender, ServiceCtx, Store, XlineKind};
|
use echo_api::{parse_duration, Priv, Sender, ServiceCtx, Store, XlineKind};
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
// A network-ban command family (AKILL, SQLINE, …): the ircd X-line `kind`, the
|
// A network-ban command family (AKILL, SQLINE, …): the ircd X-line `kind`, the
|
||||||
|
|
@ -13,6 +13,10 @@ pub struct Xline {
|
||||||
|
|
||||||
impl Xline {
|
impl Xline {
|
||||||
pub fn handle(&self, me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db: &mut dyn Store) {
|
pub fn handle(&self, me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db: &mut dyn Store) {
|
||||||
|
if !from.privs.has(Priv::Oper) {
|
||||||
|
ctx.notice(me, from.uid, format!("Access denied — {} needs the \x02operator\x02 privilege.", self.name));
|
||||||
|
return;
|
||||||
|
}
|
||||||
match args.get(1).map(|s| s.to_ascii_uppercase()).as_deref() {
|
match args.get(1).map(|s| s.to_ascii_uppercase()).as_deref() {
|
||||||
Some("ADD") => self.add(me, from, &args[2..], ctx, db),
|
Some("ADD") => self.add(me, from, &args[2..], ctx, db),
|
||||||
Some("DEL") | Some("REMOVE") => self.del(me, from, args.get(2).copied(), ctx, db),
|
Some("DEL") | Some("REMOVE") => self.del(me, from, args.get(2).copied(), ctx, db),
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,7 @@ fn require_channel_admin(me: &str, from: &Sender, chan: &str, ctx: &mut ServiceC
|
||||||
ctx.notice(me, from.uid, format!("\x02{chan}\x02 isn't registered."));
|
ctx.notice(me, from.uid, format!("\x02{chan}\x02 isn't registered."));
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
if from.account != Some(founder.as_str()) && !from.privs.has(Priv::Admin) {
|
if from.account != Some(founder.as_str()) && !from.privs.has(Priv::Oper) {
|
||||||
ctx.notice(me, from.uid, format!("Only \x02{chan}\x02's founder can see its stats."));
|
ctx.notice(me, from.uid, format!("Only \x02{chan}\x02's founder can see its stats."));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -173,7 +173,23 @@ pub struct Log {
|
||||||
pub struct Oper {
|
pub struct Oper {
|
||||||
pub account: String,
|
pub account: String,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub privs: Vec<String>, // "auspex" | "suspend" | "admin"
|
pub privs: Vec<String>, // fine-grained: auspex, oper, suspend, admin, root
|
||||||
|
// A tier shortcut: "operator", "administrator", or "root" — expands to the
|
||||||
|
// matching privilege (which implies the lower tiers). Combined with `privs`.
|
||||||
|
#[serde(default, rename = "type")]
|
||||||
|
pub oper_type: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Oper {
|
||||||
|
// Every privilege-name granted, from both the `type` shortcut and the explicit
|
||||||
|
// `privs` list.
|
||||||
|
fn all_priv_names(&self) -> Vec<String> {
|
||||||
|
let mut names = self.privs.clone();
|
||||||
|
if let Some(t) = &self.oper_type {
|
||||||
|
names.push(t.clone());
|
||||||
|
}
|
||||||
|
names
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
|
|
@ -181,7 +197,7 @@ impl Config {
|
||||||
pub fn opers(&self) -> std::collections::HashMap<String, echo_api::Privs> {
|
pub fn opers(&self) -> std::collections::HashMap<String, echo_api::Privs> {
|
||||||
let mut map = std::collections::HashMap::new();
|
let mut map = std::collections::HashMap::new();
|
||||||
for o in &self.oper {
|
for o in &self.oper {
|
||||||
map.insert(o.account.to_ascii_lowercase(), echo_api::Privs::from_names(&o.privs));
|
map.insert(o.account.to_ascii_lowercase(), echo_api::Privs::from_names(&o.all_priv_names()));
|
||||||
}
|
}
|
||||||
map
|
map
|
||||||
}
|
}
|
||||||
|
|
@ -192,7 +208,7 @@ impl Config {
|
||||||
self.oper
|
self.oper
|
||||||
.iter()
|
.iter()
|
||||||
.flat_map(|o| {
|
.flat_map(|o| {
|
||||||
let (_, unknown) = echo_api::Privs::parse_names(&o.privs);
|
let (_, unknown) = echo_api::Privs::parse_names(&o.all_priv_names());
|
||||||
unknown.into_iter().map(move |name| (o.account.clone(), name))
|
unknown.into_iter().map(move |name| (o.account.clone(), name))
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
|
|
|
||||||
|
|
@ -673,7 +673,7 @@
|
||||||
// OperServ REHASH is admin-only and hands the reload to the engine (resolved
|
// OperServ REHASH is admin-only and hands the reload to the engine (resolved
|
||||||
// in the link layer) addressed back to the oper who ran it.
|
// in the link layer) addressed back to the oper who ran it.
|
||||||
#[test]
|
#[test]
|
||||||
fn operserv_rehash_is_admin_only() {
|
fn operserv_rehash_is_root_only() {
|
||||||
use echo_operserv::OperServ;
|
use echo_operserv::OperServ;
|
||||||
let path = std::env::temp_dir().join("echo-opsrehash.jsonl");
|
let path = std::env::temp_dir().join("echo-opsrehash.jsonl");
|
||||||
let _ = std::fs::remove_file(&path);
|
let _ = std::fs::remove_file(&path);
|
||||||
|
|
@ -690,22 +690,22 @@
|
||||||
);
|
);
|
||||||
e.set_sid("42S".into());
|
e.set_sid("42S".into());
|
||||||
let mut opers = std::collections::HashMap::new();
|
let mut opers = std::collections::HashMap::new();
|
||||||
opers.insert("alice".to_string(), Privs::default().with(echo_api::Priv::Admin));
|
opers.insert("alice".to_string(), Privs::default().with(echo_api::Priv::Root));
|
||||||
opers.insert("bob".to_string(), Privs::default().with(echo_api::Priv::Auspex));
|
opers.insert("bob".to_string(), Privs::default().with(echo_api::Priv::Admin));
|
||||||
e.set_opers(opers);
|
e.set_opers(opers);
|
||||||
e.handle(NetEvent::UserConnect { uid: "000AAAAAB".into(), nick: "alice".into(), host: "h".into(), ip: "0.0.0.0".into() });
|
e.handle(NetEvent::UserConnect { uid: "000AAAAAB".into(), nick: "alice".into(), host: "h".into(), ip: "0.0.0.0".into() });
|
||||||
e.handle(NetEvent::Privmsg { from: "000AAAAAB".into(), to: "42SAAAAAA".into(), text: "IDENTIFY sesame".into() });
|
e.handle(NetEvent::Privmsg { from: "000AAAAAB".into(), to: "42SAAAAAA".into(), text: "IDENTIFY sesame".into() });
|
||||||
e.handle(NetEvent::UserConnect { uid: "000AAAAAC".into(), nick: "bob".into(), host: "h".into(), ip: "0.0.0.0".into() });
|
e.handle(NetEvent::UserConnect { uid: "000AAAAAC".into(), nick: "bob".into(), host: "h".into(), ip: "0.0.0.0".into() });
|
||||||
e.handle(NetEvent::Privmsg { from: "000AAAAAC".into(), to: "42SAAAAAA".into(), text: "IDENTIFY sesame".into() });
|
e.handle(NetEvent::Privmsg { from: "000AAAAAC".into(), to: "42SAAAAAA".into(), text: "IDENTIFY sesame".into() });
|
||||||
|
|
||||||
// Admin: hands off the reload, addressed back to the requesting oper.
|
// Root: hands off the reload, addressed back to the requesting oper.
|
||||||
let a = e.handle(NetEvent::Privmsg { from: "000AAAAAB".into(), to: "42SAAAAAH".into(), text: "REHASH".into() });
|
let a = e.handle(NetEvent::Privmsg { from: "000AAAAAB".into(), to: "42SAAAAAH".into(), text: "REHASH".into() });
|
||||||
assert!(a.iter().any(|x| matches!(x, NetAction::Rehash { requester, agent } if requester == "000AAAAAB" && agent == "42SAAAAAH")), "{a:?}");
|
assert!(a.iter().any(|x| matches!(x, NetAction::Rehash { requester, agent } if requester == "000AAAAAB" && agent == "42SAAAAAH")), "{a:?}");
|
||||||
|
|
||||||
// Oper without admin: denied, no reload handed off.
|
// An administrator (not root): denied, no reload handed off.
|
||||||
let b = e.handle(NetEvent::Privmsg { from: "000AAAAAC".into(), to: "42SAAAAAH".into(), text: "REHASH".into() });
|
let b = e.handle(NetEvent::Privmsg { from: "000AAAAAC".into(), to: "42SAAAAAH".into(), text: "REHASH".into() });
|
||||||
assert!(!b.iter().any(|x| matches!(x, NetAction::Rehash { .. })), "non-admin must not rehash: {b:?}");
|
assert!(!b.iter().any(|x| matches!(x, NetAction::Rehash { .. })), "a non-root oper must not rehash: {b:?}");
|
||||||
assert!(b.iter().any(|x| matches!(x, NetAction::Notice { text, .. } if text.contains("admin"))), "{b:?}");
|
assert!(b.iter().any(|x| matches!(x, NetAction::Notice { text, .. } if text.contains("root"))), "{b:?}");
|
||||||
}
|
}
|
||||||
|
|
||||||
// REHASH re-reads the file, applies the reloadable settings, and announces the
|
// REHASH re-reads the file, applies the reloadable settings, and announces the
|
||||||
|
|
@ -1901,7 +1901,7 @@
|
||||||
db,
|
db,
|
||||||
);
|
);
|
||||||
let mut opers = std::collections::HashMap::new();
|
let mut opers = std::collections::HashMap::new();
|
||||||
opers.insert("boss".to_string(), Privs::default().with(echo_api::Priv::Admin));
|
opers.insert("boss".to_string(), Privs::default().with(echo_api::Priv::Root));
|
||||||
e.set_opers(opers);
|
e.set_opers(opers);
|
||||||
e.handle(NetEvent::UserConnect { uid: "000AAAAAB".into(), nick: "boss".into(), host: "h".into(), ip: "0.0.0.0".into() });
|
e.handle(NetEvent::UserConnect { uid: "000AAAAAB".into(), nick: "boss".into(), host: "h".into(), ip: "0.0.0.0".into() });
|
||||||
e.handle(NetEvent::Privmsg { from: "000AAAAAB".into(), to: "42SAAAAAA".into(), text: "IDENTIFY pw".into() });
|
e.handle(NetEvent::Privmsg { from: "000AAAAAB".into(), to: "42SAAAAAA".into(), text: "IDENTIFY pw".into() });
|
||||||
|
|
@ -1941,7 +1941,7 @@
|
||||||
db,
|
db,
|
||||||
);
|
);
|
||||||
let mut opers = std::collections::HashMap::new();
|
let mut opers = std::collections::HashMap::new();
|
||||||
opers.insert("boss".to_string(), Privs::default().with(echo_api::Priv::Admin));
|
opers.insert("boss".to_string(), Privs::default().with(echo_api::Priv::Root));
|
||||||
e.set_opers(opers);
|
e.set_opers(opers);
|
||||||
e.handle(NetEvent::UserConnect { uid: "000AAAAAB".into(), nick: "boss".into(), host: "h".into(), ip: "0.0.0.0".into() });
|
e.handle(NetEvent::UserConnect { uid: "000AAAAAB".into(), nick: "boss".into(), host: "h".into(), ip: "0.0.0.0".into() });
|
||||||
e.handle(NetEvent::Privmsg { from: "000AAAAAB".into(), to: "42SAAAAAA".into(), text: "IDENTIFY pw".into() });
|
e.handle(NetEvent::Privmsg { from: "000AAAAAB".into(), to: "42SAAAAAA".into(), text: "IDENTIFY pw".into() });
|
||||||
|
|
@ -4019,7 +4019,7 @@
|
||||||
);
|
);
|
||||||
e.set_sid("42S".into());
|
e.set_sid("42S".into());
|
||||||
let mut opers = std::collections::HashMap::new();
|
let mut opers = std::collections::HashMap::new();
|
||||||
opers.insert("staff".to_string(), Privs::default().with(echo_api::Priv::Admin).with(echo_api::Priv::Auspex));
|
opers.insert("staff".to_string(), Privs::default().with(echo_api::Priv::Root));
|
||||||
opers.insert("mod".to_string(), Privs::default().with(echo_api::Priv::Suspend));
|
opers.insert("mod".to_string(), Privs::default().with(echo_api::Priv::Suspend));
|
||||||
e.set_opers(opers);
|
e.set_opers(opers);
|
||||||
let os = |e: &mut Engine, uid: &str, t: &str| e.handle(NetEvent::Privmsg { from: uid.into(), to: "42SAAAAAH".into(), text: t.into() });
|
let os = |e: &mut Engine, uid: &str, t: &str| e.handle(NetEvent::Privmsg { from: uid.into(), to: "42SAAAAAH".into(), text: t.into() });
|
||||||
|
|
@ -4344,7 +4344,7 @@
|
||||||
);
|
);
|
||||||
e.set_sid("42S".into());
|
e.set_sid("42S".into());
|
||||||
let mut opers = std::collections::HashMap::new();
|
let mut opers = std::collections::HashMap::new();
|
||||||
opers.insert("staff".to_string(), Privs::default().with(echo_api::Priv::Admin));
|
opers.insert("staff".to_string(), Privs::default().with(echo_api::Priv::Root));
|
||||||
e.set_opers(opers);
|
e.set_opers(opers);
|
||||||
let os = |e: &mut Engine, uid: &str, t: &str| e.handle(NetEvent::Privmsg { from: uid.into(), to: "42SAAAAAH".into(), text: t.into() });
|
let os = |e: &mut Engine, uid: &str, t: &str| e.handle(NetEvent::Privmsg { from: uid.into(), to: "42SAAAAAH".into(), text: t.into() });
|
||||||
let denied = |out: &[NetAction]| out.iter().any(|a| matches!(a, NetAction::Notice { text, .. } if text.contains("Access denied")));
|
let denied = |out: &[NetAction]| out.iter().any(|a| matches!(a, NetAction::Notice { text, .. } if text.contains("Access denied")));
|
||||||
|
|
@ -4389,7 +4389,7 @@
|
||||||
);
|
);
|
||||||
e.set_sid("42S".into());
|
e.set_sid("42S".into());
|
||||||
let mut opers = std::collections::HashMap::new();
|
let mut opers = std::collections::HashMap::new();
|
||||||
opers.insert("staff".to_string(), Privs::default().with(echo_api::Priv::Admin));
|
opers.insert("staff".to_string(), Privs::default().with(echo_api::Priv::Root));
|
||||||
e.set_opers(opers);
|
e.set_opers(opers);
|
||||||
let os = |e: &mut Engine, uid: &str, t: &str| e.handle(NetEvent::Privmsg { from: uid.into(), to: "42SAAAAAH".into(), text: t.into() });
|
let os = |e: &mut Engine, uid: &str, t: &str| e.handle(NetEvent::Privmsg { from: uid.into(), to: "42SAAAAAH".into(), text: t.into() });
|
||||||
let denied = |out: &[NetAction]| out.iter().any(|a| matches!(a, NetAction::Notice { text, .. } if text.contains("Access denied")));
|
let denied = |out: &[NetAction]| out.iter().any(|a| matches!(a, NetAction::Notice { text, .. } if text.contains("Access denied")));
|
||||||
|
|
@ -5205,8 +5205,9 @@ fn svc_login(e: &mut Engine, acct: &str) {
|
||||||
|
|
||||||
// Grant the account full operator privileges.
|
// Grant the account full operator privileges.
|
||||||
fn svc_oper(e: &mut Engine, acct: &str) {
|
fn svc_oper(e: &mut Engine, acct: &str) {
|
||||||
|
// A full (root-tier) operator: `root` implies every lower privilege.
|
||||||
let mut opers = std::collections::HashMap::new();
|
let mut opers = std::collections::HashMap::new();
|
||||||
opers.insert(acct.to_string(), echo_api::Privs::from_names(&["admin", "suspend", "auspex"]));
|
opers.insert(acct.to_string(), echo_api::Privs::from_names(&["root"]));
|
||||||
e.set_opers(opers);
|
e.set_opers(opers);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue