opertypes: oper classes + types — reusable capability classes and named roles (WHOIS title, auto usermodes/snomasks/vhost + level on oper-up, per-type command enforcement via on_pre_command); ships 5 built-in (helpop/globop/admin/servadmin/netadmin); oper blocks gain type=<id>; a typeless oper keeps full access

This commit is contained in:
Jean Chevronnet 2026-08-20 10:05:11 +00:00
parent f3ce0e61ef
commit 01516d5fdc
No known key found for this signature in database
GPG key ID: 439666D63A9477E4
6 changed files with 509 additions and 5 deletions

View file

@ -298,12 +298,20 @@ impl Command for Whois {
s.numeric(uid, RPL_WHOISCHANNELS, &format!("{nick} :{line}"));
}
}
// 313: is an IRC operator (hidden by +H unless the asker is an oper)
// 313: is a/an <oper type> (the type's title, else plain "IRC operator");
// hidden by +H unless the asker is an oper.
if oper && (!hideoper || asker_oper) {
let title = crate::modules::opertypes::title_of(s, tuid)
.unwrap_or_else(|| "IRC operator".to_string());
let article = if title.chars().next().is_some_and(|c| "aeiouAEIOU".contains(c)) {
"an"
} else {
"a"
};
s.numeric(
uid,
RPL_WHOISOPERATOR,
&format!("{nick} :is an IRC operator"),
&format!("{nick} :is {article} {title}"),
);
}
// 320: oper-set SWHOIS line. No redundant target-nick param — just the

View file

@ -128,6 +128,7 @@ impl Command for Oper {
return CmdResult::Fail;
};
let (hash, level) = (block.password.clone(), block.level);
let otype = block.oper_type.clone();
// fingerprint login: the block demands a specific TLS client-cert SHA-256
// fingerprint, so the user must be on a matching certificate.
if let Some(want_fp) = &block.fingerprint {
@ -152,14 +153,16 @@ impl Command for Oper {
if hash == "*" {
s.oper_up(uid);
crate::modules::operlevels::set(s, uid, level);
crate::modules::opertypes::apply(s, uid, otype.as_deref());
return CmdResult::Ok;
}
// a KDF password (bcrypt / pbkdf2) is slow — verify it off the core thread
// (result arrives as OperAuth), so it can't freeze the server or be a DoS.
if crate::modules::password_hash::is_slow(&hash) {
let ot = otype.clone();
let ok = s.spawn_crypto(move || {
let ok = crate::modules::password_hash::verify(&hash, &pass);
crate::ircd::Event::OperAuth { uid, ok, level }
crate::ircd::Event::OperAuth { uid, ok, level, oper_type: ot }
});
if !ok {
s.numeric(uid, ERR_PASSWDMISMATCH, ":Too many auth attempts, try again");
@ -171,6 +174,7 @@ impl Command for Oper {
if crate::modules::password_hash::verify(&hash, &pass) {
s.oper_up(uid);
crate::modules::operlevels::set(s, uid, level); // operlevels: KILL protection
crate::modules::opertypes::apply(s, uid, otype.as_deref());
CmdResult::Ok
} else {
s.numeric(uid, ERR_PASSWDMISMATCH, ":Password incorrect");