Merge pull request 'New-install privacy defaults, and expired bans purged daily' (#85) from feature/new-install-privacy-defaults into main
This commit was merged in pull request #85.
This commit is contained in:
@@ -14,7 +14,7 @@ use aws_lc_rs::{
|
||||
use registry::{
|
||||
schema::{
|
||||
enums::*,
|
||||
prelude::{ObjectType, SocketAddr},
|
||||
prelude::{Object, ObjectType, SocketAddr},
|
||||
structs::*,
|
||||
},
|
||||
types::{duration::Duration, error::Error, list::List, map::Map},
|
||||
@@ -388,6 +388,28 @@ async fn insert_safe_defaults(bp: &mut Bootstrap) -> trc::Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
// inbuxa: personal-data catalog, defaults D2, D3, D4 and D6 (settled
|
||||
// 2026-09-28): privacy-leaning values, for new installs only. A server
|
||||
// with roles is not new, and keeps its settings whether saved or left at
|
||||
// the default. Each singleton is read, changed and written back whole, so
|
||||
// anything already in it stays.
|
||||
#[cfg(not(feature = "test_mode"))]
|
||||
if bp.registry.count_object(ObjectType::Role).await? == 0 {
|
||||
let mut security = bp.setting_infallible::<Security>().await;
|
||||
let mut classifier = bp.setting_infallible::<SpamClassifier>().await;
|
||||
let mut pyzor = bp.setting_infallible::<SpamPyzor>().await;
|
||||
let mut retention = bp.setting_infallible::<DataRetention>().await;
|
||||
new_install_privacy_defaults(&mut security, &mut classifier, &mut pyzor, &mut retention);
|
||||
for object in [
|
||||
Object::from(security),
|
||||
classifier.into(),
|
||||
pyzor.into(),
|
||||
retention.into(),
|
||||
] {
|
||||
bp.registry.write(RegistryWrite::insert(&object)).await?;
|
||||
}
|
||||
}
|
||||
|
||||
if bp.registry.count_object(ObjectType::Role).await? == 0 {
|
||||
let permissions = DefaultPermissions::default();
|
||||
let mut role_ids = Vec::with_capacity(4);
|
||||
@@ -560,3 +582,81 @@ async fn insert_safe_defaults(bp: &mut Bootstrap) -> trc::Result<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// inbuxa: the new-install values of defaults D2, D3, D4 and D6 from the
|
||||
/// personal-data catalog spec. Automatic IP bans expire after 30 days instead
|
||||
/// of never; spam training samples are kept 90 days instead of 180; Pyzor,
|
||||
/// which sends a digest of each message's text to a public server, is off;
|
||||
/// delivery history is kept 14 days instead of 30.
|
||||
fn new_install_privacy_defaults(
|
||||
security: &mut Security,
|
||||
classifier: &mut SpamClassifier,
|
||||
pyzor: &mut SpamPyzor,
|
||||
retention: &mut DataRetention,
|
||||
) {
|
||||
const DAY: u64 = 24 * 60 * 60 * 1000;
|
||||
let ban_period = Some(Duration::from_millis(30 * DAY));
|
||||
security.auth_ban_period = ban_period;
|
||||
security.abuse_ban_period = ban_period;
|
||||
security.loiter_ban_period = ban_period;
|
||||
security.scan_ban_period = ban_period;
|
||||
classifier.hold_samples_for = Duration::from_millis(90 * DAY);
|
||||
pyzor.enable = false;
|
||||
retention.hold_traces_for = Some(Duration::from_millis(14 * DAY));
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
const DAY: u64 = 24 * 60 * 60 * 1000;
|
||||
|
||||
#[test]
|
||||
fn new_installs_get_the_privacy_defaults() {
|
||||
let (mut security, mut classifier, mut pyzor, mut retention) = (
|
||||
Security::default(),
|
||||
SpamClassifier::default(),
|
||||
SpamPyzor::default(),
|
||||
DataRetention::default(),
|
||||
);
|
||||
// What an install gets without them: bans that never lift, 180-day
|
||||
// samples, Pyzor on, 30-day traces.
|
||||
assert_eq!(security.auth_ban_period, None);
|
||||
assert!(pyzor.enable);
|
||||
|
||||
new_install_privacy_defaults(&mut security, &mut classifier, &mut pyzor, &mut retention);
|
||||
|
||||
for period in [
|
||||
security.auth_ban_period,
|
||||
security.abuse_ban_period,
|
||||
security.loiter_ban_period,
|
||||
security.scan_ban_period,
|
||||
] {
|
||||
assert_eq!(period.map(|p| p.into_inner().as_millis() as u64), Some(30 * DAY));
|
||||
}
|
||||
assert_eq!(classifier.hold_samples_for.into_inner().as_millis() as u64, 90 * DAY);
|
||||
assert!(!pyzor.enable);
|
||||
assert_eq!(
|
||||
retention.hold_traces_for.map(|p| p.into_inner().as_millis() as u64),
|
||||
Some(14 * DAY)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn everything_else_in_the_settings_stays() {
|
||||
let mut retention = DataRetention {
|
||||
archive_deleted_items_for: Some(Duration::from_millis(7 * DAY)),
|
||||
..Default::default()
|
||||
};
|
||||
let before = retention.clone();
|
||||
new_install_privacy_defaults(
|
||||
&mut Security::default(),
|
||||
&mut SpamClassifier::default(),
|
||||
&mut SpamPyzor::default(),
|
||||
&mut retention,
|
||||
);
|
||||
assert_eq!(retention.archive_deleted_items_for, before.archive_deleted_items_for);
|
||||
assert_eq!(retention.hold_metrics_for, before.hold_metrics_for);
|
||||
assert_eq!(retention.expunge_trash_after, before.expunge_trash_after);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -426,6 +426,37 @@ impl Server {
|
||||
}
|
||||
}
|
||||
|
||||
impl Server {
|
||||
/// inbuxa: personal-data catalog, D2: removes bans whose period is over.
|
||||
/// They already stop blocking when they expire, and go when settings are
|
||||
/// next loaded; the daily clean-up makes sure a server that seldom
|
||||
/// reloads doesn't keep them.
|
||||
pub async fn purge_expired_blocked_ips(&self) -> trc::Result<()> {
|
||||
let now = now() as i64;
|
||||
let mut expired = Vec::new();
|
||||
for ip in self.registry().list::<BlockedIp>().await? {
|
||||
if ip.object.expires_at.as_ref().is_some_and(|at| at.timestamp() <= now) {
|
||||
let address = ip.object.address.clone();
|
||||
let object = Object {
|
||||
inner: ip.object.into(),
|
||||
revision: ip.revision,
|
||||
};
|
||||
self.registry()
|
||||
.write(RegistryWrite::delete_object(ip.id, &object))
|
||||
.await?;
|
||||
expired.push(trc::Value::from(address.into_inner().0));
|
||||
}
|
||||
}
|
||||
if !expired.is_empty() {
|
||||
trc::event!(
|
||||
Security(trc::SecurityEvent::IpBlockExpired),
|
||||
Details = expired
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl BlockedIps {
|
||||
pub async fn parse(bp: &mut Bootstrap) -> Self {
|
||||
let mut ips = Self::default();
|
||||
|
||||
@@ -47353,7 +47353,9 @@ impl Default for WebHook {
|
||||
level: TracingLevel::Info,
|
||||
lossy: false,
|
||||
events: Default::default(),
|
||||
events_policy: EventPolicy::Exclude,
|
||||
// inbuxa: personal-data catalog, D7: a new webhook sends nothing
|
||||
// until its events are chosen
|
||||
events_policy: EventPolicy::Include,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -269,6 +269,11 @@ async fn store_maintenance(
|
||||
trc::error!(err.details("Failed to re-apply account locks"));
|
||||
}
|
||||
|
||||
// inbuxa: personal-data catalog, D2: bans past their period go
|
||||
if let Err(err) = server.purge_expired_blocked_ips().await {
|
||||
trc::error!(err.details("Failed to purge expired IP bans"));
|
||||
}
|
||||
|
||||
// inbuxa: AU-7: audit records past their retention go; a
|
||||
// failure leaves them for the next run
|
||||
if let Err(err) = server.audit_purge().await {
|
||||
|
||||
@@ -151,7 +151,7 @@ These live in inbuxa's own key space (`SUBSPACE_INBUXA`) unless noted.
|
||||
| OAuth codes and tokens | credential | holder | `x:OidcProvider.*Expiry` | tokens are sealed and stateless (not stored); codes in the in-memory store with TTL | in-memory store | tenant | `http/src/auth/oauth/auth.rs`, `token.rs` | codes 10 min |
|
||||
| Rate-limit state | network, identifier (login names) | holder, correspondent | `x:Http.rateLimit*`, `x:Imap.maxRequestRate`, `x:Security.*BanRate` | the rate's period | in-memory store | server | `common/src/auth/rate_limit.rs`, `network/security.rs` | on |
|
||||
| Greylist | identifier (sender/recipient pairs, plain) | correspondent, holder | `x:SpamSettings.greylistFor` | that period | in-memory store | server | `smtp/src/inbound/rcpt.rs` | **off** |
|
||||
| Automatic IP bans (`x:BlockedIp`) | network | correspondent, holder | `x:Security.authBanRate`, `abuseBanRate`, `loiterBanRate`, `scanBanRate` (on); `*BanPeriod` (**no default**) | **unbounded: a ban with no period never expires, and no purge of expired bans was found**; each ban is also an audit record | data store (registry) | server | `common/src/network/security.rs` `block_ip` | **collected, permanent** |
|
||||
| Automatic IP bans (`x:BlockedIp`) | network | correspondent, holder | `x:Security.authBanRate`, `abuseBanRate`, `loiterBanRate`, `scanBanRate` (on); `*BanPeriod` (**no default**) | **unbounded: a ban with no period never expires**; an expired ban's record goes when settings next load; each ban is also an audit record | data store (registry) | server | `common/src/network/security.rs` `block_ip` | **collected, permanent** |
|
||||
| Allowed IPs | network | administrator's choice | manual; `expiresAt` | optional | data store | server | registry | none |
|
||||
|
||||
### 2.6 Spam filter and AI
|
||||
@@ -197,8 +197,10 @@ not a judgment; what to do about each is John's call.
|
||||
2. **Log files are never deleted.** Daily rotation opens a new file; nothing
|
||||
removes old ones, and no logrotate configuration ships. At the default
|
||||
level every in-session line carries the client IP.
|
||||
3. **Automatic IP bans are permanent.** No `*BanPeriod` has a default, and no
|
||||
purge of expired `x:BlockedIp` records was found.
|
||||
3. **Automatic IP bans are permanent.** No `*BanPeriod` has a default, so a
|
||||
ban never expires. (Corrected 2026-09-28: a ban that does expire stops
|
||||
blocking, and its record is deleted when settings are next loaded, in
|
||||
`BlockedIps::parse`; the investigation missed that path.)
|
||||
4. **Some records outlive the account.** Deleting an account doesn't clear
|
||||
inbuxa's own key space: legacy-protocol last use, account locks, masked
|
||||
address records and audit records stay (audit records by design).
|
||||
@@ -394,6 +396,17 @@ default; fails on a stale entry.
|
||||
Phase 3; existing servers keep their settings. D7 is also covered by the bug
|
||||
fix for finding 1 (Settled 6).
|
||||
|
||||
**As built (2026-09-28).** D2, D3, D4, D6 are written on first boot of a new
|
||||
install only (no roles yet), each singleton read and written back whole
|
||||
(`manager/defaults.rs`, `new_install_privacy_defaults`); expired bans are
|
||||
also purged daily (`purge_expired_blocked_ips`). D7 changes the default for
|
||||
webhooks created from now on; stored webhooks keep theirs (the registry
|
||||
stores every field). **Held:** D1, because `x:TracerLog` is also stored
|
||||
inside `x:Bootstrap` with fields after it, so adding a field changes that
|
||||
object's stored format; a fork-owned setting is proposed instead, for John
|
||||
to decide. D5, because the spam-rules loader it touches is being reworked
|
||||
by the v0.16.24 import.
|
||||
|
||||
| # | Change | Trade-off |
|
||||
|---|---|---|
|
||||
| D1 | A **log retention** setting on `x:TracerLog` (delete rotated files older than N days), default 30 days for new installs | Needs code (a new field, so a schema edit); older logs gone for troubleshooting; operators wanting longer set it |
|
||||
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
-jadTddv9zRK0gp8fBFYRmhwmXopxPxF2yjc86bSKRM
|
||||
MiWRzsz0AHdRshG_8JimktRqBO_pHGAUBHFcfV5jwI4
|
||||
@@ -221,6 +221,21 @@ pub async fn test(test: &mut TestServer) {
|
||||
)
|
||||
.await;
|
||||
|
||||
// inbuxa: personal-data catalog, D2: the daily clean-up removes the
|
||||
// expired ban's record, without waiting for settings to reload
|
||||
test.server.purge_expired_blocked_ips().await.unwrap();
|
||||
assert_eq!(
|
||||
admin
|
||||
.registry_query_ids(
|
||||
ObjectType::BlockedIp,
|
||||
[(Property::Address, "10.0.0.2")],
|
||||
Vec::<&str>::new(),
|
||||
)
|
||||
.await,
|
||||
Vec::<Id>::new(),
|
||||
"the expired ban's record is gone"
|
||||
);
|
||||
|
||||
// Make sure the IP remains unblocked after reload
|
||||
admin.registry_create_object(Action::ReloadBlockedIps).await;
|
||||
validate_password_with_ip(
|
||||
|
||||
Reference in New Issue
Block a user