modules: capability-scoped Store/NetView boundary (Tier 2)
Services no longer receive the concrete account store. on_command now takes &mut dyn Store + &dyn NetView (both defined in fedserv-api); the engine's Db and Network implement them. Reads hand back plain views (AccountView, ChannelView, ...) carrying only non-secret fields, so the event log, gossip, and credential material (password hash, SCRAM verifiers) are unreachable from a module. All command modules ported; behaviour unchanged.
This commit is contained in:
parent
931b8727e9
commit
8ed1a9ab70
36 changed files with 487 additions and 148 deletions
165
src/engine/db.rs
165
src/engine/db.rs
|
|
@ -11,6 +11,13 @@ use tokio::sync::broadcast;
|
|||
|
||||
use super::scram::{self, Hash};
|
||||
|
||||
// Error kinds, the emailed-code purpose, and the module-facing views live in the
|
||||
// fedserv-api SDK crate; re-exported so the engine keeps naming them locally and
|
||||
// modules importing `crate::engine::db::{ChanError, ...}` are unaffected.
|
||||
pub use fedserv_api::{
|
||||
AccountView, ChanAccessView, ChanAkickView, ChanError, ChannelView, CertError, CodeKind, RegError, Store,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Account {
|
||||
pub name: String,
|
||||
|
|
@ -158,11 +165,6 @@ impl ChannelInfo {
|
|||
.map(|a| if a.level == "voice" { "+v" } else { "+o" })
|
||||
}
|
||||
|
||||
/// Whether `account` has operator access (founder or access-list op).
|
||||
pub fn is_op(&self, account: &str) -> bool {
|
||||
self.join_mode(account) == Some("+o")
|
||||
}
|
||||
|
||||
/// The matching auto-kick entry for `hostmask` (nick!user@host), if any.
|
||||
pub fn akick_match(&self, hostmask: &str) -> Option<&ChanAkick> {
|
||||
self.akick.iter().find(|k| glob_match(&k.mask, hostmask))
|
||||
|
|
@ -421,12 +423,6 @@ impl EventLog {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum RegError {
|
||||
Exists,
|
||||
Internal,
|
||||
}
|
||||
|
||||
// What an ingested entry did to a locally-known account, so the engine can log
|
||||
// out sessions that were relying on it.
|
||||
pub enum AccountChange {
|
||||
|
|
@ -434,21 +430,6 @@ pub enum AccountChange {
|
|||
Dropped(String),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum CertError {
|
||||
Invalid, // not a plausible fingerprint
|
||||
InUse, // already registered (to any account)
|
||||
NoAccount, // target account does not exist
|
||||
Internal, // persistence failed
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ChanError {
|
||||
Exists, // channel already registered
|
||||
NoChannel, // channel is not registered
|
||||
Internal, // persistence failed
|
||||
}
|
||||
|
||||
// A fingerprint is hex (hash digest), optionally colon-separated. Bound the
|
||||
// length so a junk value can't bloat an account.
|
||||
fn valid_fp(fp: &str) -> bool {
|
||||
|
|
@ -482,13 +463,6 @@ pub struct Db {
|
|||
codes: HashMap<String, (CodeKind, String, Instant)>,
|
||||
}
|
||||
|
||||
// What an emailed code authorises.
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
pub enum CodeKind {
|
||||
Reset,
|
||||
Confirm,
|
||||
}
|
||||
|
||||
fn key(name: &str) -> String {
|
||||
name.to_ascii_lowercase()
|
||||
}
|
||||
|
|
@ -1210,6 +1184,131 @@ fn verify_password(password: &str, hash: &str) -> bool {
|
|||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
// The module-facing account/channel store. Every method forwards to Db's own
|
||||
// (fully-qualified so it is the inherent method, never this trait method), with
|
||||
// reads projected into credential-free views. Internal callers keep using the
|
||||
// richer inherent methods directly.
|
||||
impl Store for Db {
|
||||
fn exists(&self, name: &str) -> bool {
|
||||
Db::exists(self, name)
|
||||
}
|
||||
fn account(&self, name: &str) -> Option<AccountView> {
|
||||
Db::account(self, name).map(|a| AccountView {
|
||||
name: a.name.clone(),
|
||||
email: a.email.clone(),
|
||||
ts: a.ts,
|
||||
verified: a.verified,
|
||||
})
|
||||
}
|
||||
fn resolve_account(&self, name: &str) -> Option<&str> {
|
||||
Db::resolve_account(self, name)
|
||||
}
|
||||
fn authenticate(&self, name: &str, password: &str) -> Option<&str> {
|
||||
Db::authenticate(self, name, password)
|
||||
}
|
||||
fn grouped_nicks(&self, account: &str) -> Vec<String> {
|
||||
Db::grouped_nicks(self, account)
|
||||
}
|
||||
fn certfps(&self, account: &str) -> &[String] {
|
||||
Db::certfps(self, account)
|
||||
}
|
||||
fn is_verified(&self, account: &str) -> bool {
|
||||
Db::is_verified(self, account)
|
||||
}
|
||||
fn channel(&self, name: &str) -> Option<ChannelView> {
|
||||
Db::channel(self, name).map(channel_view)
|
||||
}
|
||||
fn channels(&self) -> Vec<ChannelView> {
|
||||
Db::channels(self).map(channel_view).collect()
|
||||
}
|
||||
fn channels_owned_by(&self, account: &str) -> Vec<String> {
|
||||
Db::channels_owned_by(self, account)
|
||||
}
|
||||
fn email_enabled(&self) -> bool {
|
||||
Db::email_enabled(self)
|
||||
}
|
||||
fn email_brand(&self) -> &str {
|
||||
Db::email_brand(self)
|
||||
}
|
||||
fn email_accent(&self) -> &str {
|
||||
Db::email_accent(self)
|
||||
}
|
||||
fn email_logo(&self) -> &str {
|
||||
Db::email_logo(self)
|
||||
}
|
||||
fn issue_code(&mut self, account: &str, kind: CodeKind) -> String {
|
||||
Db::issue_code(self, account, kind)
|
||||
}
|
||||
fn take_code(&mut self, account: &str, kind: CodeKind, code: &str) -> bool {
|
||||
Db::take_code(self, account, kind, code)
|
||||
}
|
||||
fn verify_account(&mut self, account: &str) -> Result<(), RegError> {
|
||||
Db::verify_account(self, account)
|
||||
}
|
||||
fn set_email(&mut self, account: &str, email: Option<String>) -> Result<(), RegError> {
|
||||
Db::set_email(self, account, email)
|
||||
}
|
||||
fn group_nick(&mut self, nick: &str, account: &str) -> Result<(), RegError> {
|
||||
Db::group_nick(self, nick, account)
|
||||
}
|
||||
fn ungroup_nick(&mut self, nick: &str) -> Result<bool, RegError> {
|
||||
Db::ungroup_nick(self, nick)
|
||||
}
|
||||
fn drop_account(&mut self, account: &str) -> Result<bool, RegError> {
|
||||
Db::drop_account(self, account)
|
||||
}
|
||||
fn certfp_add(&mut self, account: &str, fp: &str) -> Result<(), CertError> {
|
||||
Db::certfp_add(self, account, fp)
|
||||
}
|
||||
fn certfp_del(&mut self, account: &str, fp: &str) -> Result<bool, CertError> {
|
||||
Db::certfp_del(self, account, fp)
|
||||
}
|
||||
fn register_channel(&mut self, name: &str, founder: &str) -> Result<(), ChanError> {
|
||||
Db::register_channel(self, name, founder)
|
||||
}
|
||||
fn drop_channel(&mut self, name: &str) -> Result<(), ChanError> {
|
||||
Db::drop_channel(self, name)
|
||||
}
|
||||
fn set_mlock(&mut self, name: &str, on: &str, off: &str) -> Result<(), ChanError> {
|
||||
Db::set_mlock(self, name, on, off)
|
||||
}
|
||||
fn set_desc(&mut self, channel: &str, desc: &str) -> Result<(), ChanError> {
|
||||
Db::set_desc(self, channel, desc)
|
||||
}
|
||||
fn set_entrymsg(&mut self, channel: &str, msg: &str) -> Result<(), ChanError> {
|
||||
Db::set_entrymsg(self, channel, msg)
|
||||
}
|
||||
fn set_founder(&mut self, channel: &str, account: &str) -> Result<(), ChanError> {
|
||||
Db::set_founder(self, channel, account)
|
||||
}
|
||||
fn access_add(&mut self, channel: &str, account: &str, level: &str) -> Result<(), ChanError> {
|
||||
Db::access_add(self, channel, account, level)
|
||||
}
|
||||
fn access_del(&mut self, channel: &str, account: &str) -> Result<bool, ChanError> {
|
||||
Db::access_del(self, channel, account)
|
||||
}
|
||||
fn akick_add(&mut self, channel: &str, mask: &str, reason: &str) -> Result<(), ChanError> {
|
||||
Db::akick_add(self, channel, mask, reason)
|
||||
}
|
||||
fn akick_del(&mut self, channel: &str, mask: &str) -> Result<bool, ChanError> {
|
||||
Db::akick_del(self, channel, mask)
|
||||
}
|
||||
}
|
||||
|
||||
fn channel_view(c: &ChannelInfo) -> ChannelView {
|
||||
ChannelView {
|
||||
name: c.name.clone(),
|
||||
founder: c.founder.clone(),
|
||||
ts: c.ts,
|
||||
lock_on: c.lock_on.clone(),
|
||||
lock_off: c.lock_off.clone(),
|
||||
access: c.access.iter().map(|a| ChanAccessView { account: a.account.clone(), level: a.level.clone() }).collect(),
|
||||
akick: c.akick.iter().map(|k| ChanAkickView { mask: k.mask.clone(), reason: k.reason.clone() }).collect(),
|
||||
desc: c.desc.clone(),
|
||||
entrymsg: c.entrymsg.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
|
|||
|
|
@ -1,29 +1,4 @@
|
|||
use crate::engine::db::Db;
|
||||
use crate::engine::state::Network;
|
||||
|
||||
// Sender + ServiceCtx (the command context a module receives) live in the
|
||||
// fedserv-api SDK crate; re-exported so modules keep using
|
||||
// `crate::engine::service::{Sender, ServiceCtx}`.
|
||||
pub use fedserv_api::{Sender, ServiceCtx};
|
||||
|
||||
// A pseudo-client (NickServ, ChanServ, ...). Introduced at burst, receives the
|
||||
// commands users message it, reads/writes the account store, and pushes actions.
|
||||
pub trait Service: Send {
|
||||
fn nick(&self) -> &str;
|
||||
fn uid(&self) -> &str;
|
||||
fn host(&self) -> &str {
|
||||
"services.local"
|
||||
}
|
||||
fn gecos(&self) -> &str;
|
||||
// Whether this service owns channel modes (ChanServ), so the engine can source
|
||||
// channel mode changes from it.
|
||||
fn manages_channels(&self) -> bool {
|
||||
false
|
||||
}
|
||||
// Whether this is the account service (NickServ), so the engine can source
|
||||
// account-related notices from it.
|
||||
fn manages_accounts(&self) -> bool {
|
||||
false
|
||||
}
|
||||
fn on_command(&mut self, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, net: &Network, db: &mut Db);
|
||||
}
|
||||
// Sender, ServiceCtx, and the Service trait a pseudo-client implements all live
|
||||
// in the fedserv-api SDK crate; re-exported so the engine and the service
|
||||
// modules keep using `crate::engine::service::{...}`.
|
||||
pub use fedserv_api::{Sender, Service, ServiceCtx};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
use std::collections::{HashMap, HashSet};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
// The read-only network view a module sees; re-exported so the engine keeps
|
||||
// naming it locally.
|
||||
pub use fedserv_api::{NetView, SeenView};
|
||||
|
||||
// Live network view, rebuilt from the uplink's burst each connect (ephemeral —
|
||||
// unlike the account store, which persists).
|
||||
#[derive(Default)]
|
||||
|
|
@ -155,3 +159,39 @@ impl Network {
|
|||
self.seen.get(&lc(nick))
|
||||
}
|
||||
}
|
||||
|
||||
// The module-facing network view. Reads forward to Network's own accessors,
|
||||
// with the seen record projected into a plain view.
|
||||
impl NetView for Network {
|
||||
fn uid_by_nick(&self, nick: &str) -> Option<&str> {
|
||||
Network::uid_by_nick(self, nick)
|
||||
}
|
||||
fn nick_of(&self, uid: &str) -> Option<&str> {
|
||||
Network::nick_of(self, uid)
|
||||
}
|
||||
fn host_of(&self, uid: &str) -> Option<&str> {
|
||||
Network::host_of(self, uid)
|
||||
}
|
||||
fn account_of(&self, uid: &str) -> Option<&str> {
|
||||
Network::account_of(self, uid)
|
||||
}
|
||||
fn uids_logged_into(&self, account: &str) -> Vec<String> {
|
||||
Network::uids_logged_into(self, account)
|
||||
}
|
||||
fn is_op(&self, channel: &str, uid: &str) -> bool {
|
||||
Network::is_op(self, channel, uid)
|
||||
}
|
||||
fn channel_members(&self, channel: &str) -> Vec<String> {
|
||||
Network::channel_members(self, channel).map(str::to_string).collect()
|
||||
}
|
||||
fn channel_key(&self, channel: &str) -> Option<&str> {
|
||||
Network::channel_key(self, channel)
|
||||
}
|
||||
fn last_seen(&self, nick: &str) -> Option<SeenView> {
|
||||
Network::last_seen(self, nick).map(|s| SeenView {
|
||||
nick: s.nick.clone(),
|
||||
ts: s.ts,
|
||||
what: s.what.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue