veloren_server/settings/banlist.rs
1//! Versioned banlist settings files.
2
3// NOTE: Needed to allow the second-to-last migration to call try_into().
4
5use super::{BANLIST_FILENAME as FILENAME, MIGRATION_UPGRADE_GUARANTEE};
6use crate::settings::editable::{EditableSetting, Version};
7use authc::Uuid;
8use core::convert::{TryFrom, TryInto};
9use serde::{Deserialize, Serialize};
10
11/// NOTE: Always replace this with the latest banlist version. Then update the
12/// `BanlistRaw`, the `TryFrom<BanlistRaw>` for `Banlist`, the previously most
13/// recent module, and add a new module for the latest version! Please respect
14/// the migration upgrade guarantee found in the parent module with any upgrade.
15pub use self::v2::*;
16
17/// Versioned settings files, one per version (v0 is only here as an example; we
18/// do not expect to see any actual v0 settings files).
19#[derive(Deserialize, Serialize)]
20pub enum BanlistRaw {
21 V0(v0::Banlist),
22 V1(v1::Banlist),
23 V2(v2::Banlist),
24}
25
26impl From<Banlist> for BanlistRaw {
27 fn from(value: Banlist) -> Self {
28 // Replace variant with that of current latest version.
29 Self::V2(value)
30 }
31}
32
33impl TryFrom<BanlistRaw> for (Version, Banlist) {
34 type Error = <Banlist as EditableSetting>::Error;
35
36 fn try_from(value: BanlistRaw) -> Result<Self, <Banlist as EditableSetting>::Error> {
37 use BanlistRaw::*;
38 Ok(match value {
39 // Old versions
40 V0(value) => (Version::Old, value.try_into()?),
41 V1(value) => (Version::Old, value.try_into()?),
42 // Latest version (move to old section using the pattern of other old version when it
43 // is no longer latest).
44 V2(mut value) => (value.validate()?, value),
45 })
46 }
47}
48
49type Final = Banlist;
50
51impl EditableSetting for Banlist {
52 type Error = BanError;
53 type Legacy = legacy::Banlist;
54 type Setting = BanlistRaw;
55
56 const FILENAME: &'static str = FILENAME;
57}
58
59#[derive(Clone, Copy, Debug)]
60pub enum BanKind {
61 Ban,
62 Unban,
63}
64
65#[derive(Clone, Copy, Debug)]
66pub enum BanErrorKind {
67 /// An end date went past a start date.
68 InvalidDateRange {
69 start_date: chrono::DateTime<chrono::Utc>,
70 end_date: chrono::DateTime<chrono::Utc>,
71 },
72 /// Cannot unban an already-unbanned user.
73 AlreadyUnbanned,
74 /// Permission denied to perform requested action.
75 PermissionDenied(BanKind),
76 /// Cannot have a UUID linked IP ban if there is not a corresponding UUID
77 /// ban. In this case the corresponding entry in the UUID ban map is missing
78 /// completely.
79 CorrespondingUuidBanMissing,
80 /// Cannot have a UUID linked IP ban if there is not a corresponding UUID
81 /// ban. In this case there is a corresponding entry, but it may be an unban
82 /// instead of a ban, the expiration date may not match, or the ban info
83 /// doesn't match.
84 CorrespondingUuidBanMismatch,
85 /// Ban info is an optional field to support legacy data, of which IP bans
86 /// and their linked UUID bans are not included.
87 NonLegacyBanMissingBanInfo,
88 /// Multiple active IP bans should not link to the same UUID since that UUID
89 /// should also be banned (and thus no IPs can be banned via that user).
90 ActiveIpBansShareUuid,
91}
92
93#[derive(Debug)]
94pub enum BanError {
95 Uuid {
96 kind: BanErrorKind,
97 /// Uuid of affected user
98 uuid: Uuid,
99 /// Username of affected user (as of ban/unban time).
100 username: String,
101 },
102 // Note, we specifically don't expose the IP address here since this is
103 // shown to users of the ban commands.
104 Ip {
105 kind: BanErrorKind,
106 /// Uuid of affected user
107 uuid: Option<Uuid>,
108 /// `username_when_performed` from the associated uuid ban entry, if the
109 /// associated entry is missing (which would cause a validation
110 /// error) or there is no associated entry (uuid is None) then
111 /// this will be None.
112 username_from_uuid_entry: Option<String>,
113 },
114}
115
116/// NOTE: This isn't serialized so we can place it outside the versioned
117/// modules.
118///
119/// `BanAction` name already taken.
120///
121/// Note, `IpBan` will also apply a regular ban, while `UnbanIp` will only
122/// remove the IP ban.
123pub enum BanOperation {
124 // We don't use `Ban` struct because the info field is optional for
125 // legacy reasons.
126 Ban {
127 reason: String,
128 info: BanInfo,
129 /// See [`Ban::upgrade_to_ip_ban`]
130 upgrade_to_ip: bool,
131 /// NOTE: Should always be higher than the `now` date provided to
132 /// [`Banlist::ban_operation`] , if this is present!
133 end_date: Option<chrono::DateTime<chrono::Utc>>,
134 },
135 UpgradeToIpBan {
136 ip: NormalizedIpAddr,
137 },
138 BanIp {
139 reason: String,
140 info: BanInfo,
141 /// NOTE: Should always be higher than the `now` date provided to
142 /// [`Banlist::ban_operation`] , if this is present!
143 end_date: Option<chrono::DateTime<chrono::Utc>>,
144 ip: NormalizedIpAddr,
145 },
146 Unban {
147 info: BanInfo,
148 },
149 UnbanIp {
150 info: BanInfo,
151 /// The Uuid linked to the IP ban (currently no functionality to created
152 /// or remove non-uuid linked IP bans even though the model can
153 /// support them)
154 uuid: Uuid,
155 },
156}
157
158#[derive(Debug)]
159pub enum BanOperationError {
160 /// Operation cancelled without performing any changes for some reason.
161 NoEffect,
162 /// Validation or IO error.
163 EditFailed(crate::settings::editable::Error<Final>),
164}
165
166mod legacy {
167 use super::{Final, MIGRATION_UPGRADE_GUARANTEE, v0 as next};
168 use authc::Uuid;
169 use core::convert::TryInto;
170 use hashbrown::HashMap;
171 use serde::{Deserialize, Serialize};
172
173 #[derive(Deserialize, Serialize)]
174 pub struct BanRecord {
175 pub username_when_banned: String,
176 pub reason: String,
177 }
178
179 #[derive(Deserialize, Serialize, Default)]
180 #[serde(transparent)]
181 pub struct Banlist(pub(super) HashMap<Uuid, BanRecord>);
182
183 impl From<Banlist> for Final {
184 /// Legacy migrations can be migrated to the latest version through the
185 /// process of "chaining" migrations, starting from
186 /// `next::Banlist`.
187 ///
188 /// Note that legacy files are always valid, which is why we implement
189 /// `From` rather than `TryFrom`.
190 fn from(value: Banlist) -> Self {
191 next::Banlist::migrate(value)
192 .try_into()
193 .expect(MIGRATION_UPGRADE_GUARANTEE)
194 }
195 }
196}
197
198/// This module represents a banlist version that isn't actually used. It is
199/// here and part of the migration process to provide an example for how to
200/// perform a migration for an old version; please use this as a reference when
201/// constructing new migrations.
202mod v0 {
203 use super::{Final, MIGRATION_UPGRADE_GUARANTEE, legacy as prev, v1 as next};
204 use crate::settings::editable::{EditableSetting, Version};
205 use authc::Uuid;
206 use core::convert::{TryFrom, TryInto};
207 use hashbrown::HashMap;
208 use serde::{Deserialize, Serialize};
209
210 #[derive(Clone, Deserialize, Serialize)]
211 pub struct BanRecord {
212 pub username_when_banned: String,
213 pub reason: String,
214 }
215
216 #[derive(Clone, Deserialize, Serialize, Default)]
217 #[serde(transparent)]
218 pub struct Banlist(pub(super) HashMap<Uuid, BanRecord>);
219
220 impl Banlist {
221 /// One-off migration from the previous version. This must be
222 /// guaranteed to produce a valid settings file as long as it is
223 /// called with a valid settings file from the previous version.
224 pub(super) fn migrate(prev: prev::Banlist) -> Self {
225 Banlist(
226 prev.0
227 .into_iter()
228 .map(
229 |(
230 uid,
231 prev::BanRecord {
232 username_when_banned,
233 reason,
234 },
235 )| {
236 (uid, BanRecord {
237 username_when_banned,
238 reason,
239 })
240 },
241 )
242 .collect(),
243 )
244 }
245
246 /// Perform any needed validation on this banlist that can't be done
247 /// using parsing.
248 ///
249 /// The returned version being "Old" indicates the loaded setting has
250 /// been modified during validation (this is why validate takes
251 /// `&mut self`).
252 pub(super) fn validate(&mut self) -> Result<Version, <Final as EditableSetting>::Error> {
253 Ok(Version::Latest)
254 }
255 }
256
257 /// Pretty much every TryFrom implementation except that of the very last
258 /// version should look exactly like this.
259 impl TryFrom<Banlist> for Final {
260 type Error = <Final as EditableSetting>::Error;
261
262 fn try_from(mut value: Banlist) -> Result<Final, Self::Error> {
263 value.validate()?;
264 Ok(next::Banlist::migrate(value)
265 .try_into()
266 .expect(MIGRATION_UPGRADE_GUARANTEE))
267 }
268 }
269}
270
271mod v1 {
272 use super::{
273 BanError, BanErrorKind, BanKind, Final, MIGRATION_UPGRADE_GUARANTEE, v0 as prev, v2 as next,
274 };
275 use crate::settings::editable::{EditableSetting, Version};
276 use authc::Uuid;
277 use chrono::{Utc, prelude::*};
278 use common::comp::AdminRole;
279 use core::ops::Deref;
280 use hashbrown::HashMap;
281 use serde::{Deserialize, Serialize};
282 use tracing::warn;
283
284 /// Important: even if the role we are storing here appears to be identical
285 /// to one used in another versioned store (like admin::Role), we *must*
286 /// have our own versioned copy! This ensures that if there's an update
287 /// to the role somewhere else, the conversion function between them
288 /// will break, letting people make an intelligent decision.
289 ///
290 /// In particular, *never remove variants from this enum* (or any other enum
291 /// in a versioned settings file) without bumping the version and
292 /// writing a migration that understands how to properly deal with
293 /// existing instances of the old variant (you can delete From instances
294 /// for the old variants at this point). Otherwise, we will lose
295 /// compatibility with old settings files, since we won't be able to
296 /// deserialize them!
297 #[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
298 pub enum Role {
299 Moderator = 0,
300 Admin = 1,
301 }
302
303 impl From<AdminRole> for Role {
304 fn from(value: AdminRole) -> Self {
305 match value {
306 AdminRole::Moderator => Self::Moderator,
307 AdminRole::Admin => Self::Admin,
308 }
309 }
310 }
311
312 impl From<Role> for AdminRole {
313 fn from(value: Role) -> Self {
314 match value {
315 Role::Moderator => Self::Moderator,
316 Role::Admin => Self::Admin,
317 }
318 }
319 }
320
321 #[derive(Clone, Deserialize, Serialize)]
322 /// NOTE: May not be present if performed from the command line or from a
323 /// legacy file.
324 pub struct BanInfo {
325 pub performed_by: Uuid,
326 /// NOTE: May not be up to date, if we allow username changes.
327 pub performed_by_username: String,
328 /// NOTE: Role of the banning user at the time of the ban.
329 pub performed_by_role: Role,
330 }
331
332 #[derive(Clone, Deserialize, Serialize)]
333 pub struct Ban {
334 pub reason: String,
335 /// NOTE: Should only be None for migrations from legacy data.
336 pub info: Option<BanInfo>,
337 /// NOTE: Should always be higher than start_date, if both are
338 /// present!
339 pub end_date: Option<DateTime<Utc>>,
340 }
341
342 impl Ban {
343 /// Returns true if the ban is expired, false otherwise.
344 pub fn is_expired(&self, now: DateTime<Utc>) -> bool {
345 self.end_date.is_some_and(|end_date| end_date <= now)
346 }
347
348 pub fn performed_by_role(&self) -> Role {
349 self.info.as_ref().map(|info| info.performed_by_role)
350 // We know all legacy bans were performed by an admin, since we had no other roles
351 // at the time.
352 .unwrap_or(Role::Admin)
353 }
354 }
355
356 type Unban = BanInfo;
357
358 #[derive(Clone, Deserialize, Serialize)]
359 pub enum BanAction {
360 Unban(Unban),
361 Ban(Ban),
362 }
363
364 #[derive(Clone, Deserialize, Serialize)]
365 pub struct BanRecord {
366 /// Username of the user upon whom the action was performed, when it was
367 /// performed.
368 pub username_when_performed: String,
369 pub action: BanAction,
370 /// NOTE: When migrating from legacy versions, this will just be the
371 /// time of the first migration (only applies to BanRecord).
372 pub date: DateTime<Utc>,
373 }
374
375 impl BanRecord {
376 /// Returns true if this record represents an expired ban, false
377 /// otherwise.
378 fn is_expired(&self, now: DateTime<Utc>) -> bool {
379 match &self.action {
380 BanAction::Ban(ban) => ban.is_expired(now),
381 BanAction::Unban(_) => true,
382 }
383 }
384
385 /// The history vector in a BanEntry is stored forwards (from oldest
386 /// entry to newest), so `prev_record` is the previous entry in
387 /// this vector when iterating forwards (by array index).
388 ///
389 /// Errors are:
390 ///
391 /// AlreadyUnbanned if an unban comes after anything but a ban.
392 ///
393 /// Permission(Unban) if an unban attempt is by a user with a lower role
394 /// level than the original banning party.
395 ///
396 /// PermissionDenied(Ban) if a ban length is made shorter by a user with
397 /// a role level than the original banning party.
398 ///
399 /// InvalidDateRange if the end date of the ban exceeds the start date.
400 fn validate(&self, prev_record: Option<&BanRecord>) -> Result<(), BanErrorKind> {
401 // Check to make sure the actions temporally line up--if they don't, we will
402 // prevent warn an administrator (since this may indicate a system
403 // clock issue and could require manual editing to resolve).
404 // However, we will not actually invalidate the ban list for this, in case
405 // this would otherwise prevent people from adding a new ban.
406 //
407 // We also deliberately leave the bad order intact, in case this reflects
408 // history more accurately than the system clock does.
409 if let Some(prev_record) = prev_record {
410 if prev_record.date > self.date {
411 warn!(
412 "Ban list history is inconsistent, or a just-added ban was behind a \
413 historical entry in the ban
414 record; please investigate the contents of the file (might indicate a \
415 system clock change?)."
416 );
417 }
418 }
419 let ban = match (&self.action, prev_record.map(|record| &record.action)) {
420 // A ban is always valid if it follows an unban.
421 (BanAction::Ban(ban), None) | (BanAction::Ban(ban), Some(BanAction::Unban(_))) => {
422 ban
423 },
424 // A ban record following a ban is valid if either the role of the person doing the
425 // banning is at least the privilege level of the person who did the ban, or the
426 // ban's new end time is at least the previous end time.
427 (BanAction::Ban(new_ban), Some(BanAction::Ban(old_ban))) => {
428 match (new_ban.end_date, old_ban.end_date) {
429 // New role ≥ old role
430 _ if new_ban.performed_by_role() >= old_ban.performed_by_role() => new_ban,
431 // Permanent ban retracted to temp ban.
432 (Some(_), None) => {
433 return Err(BanErrorKind::PermissionDenied(BanKind::Ban));
434 },
435 // Temp ban retracted to shorter temp ban.
436 (Some(new_date), Some(old_date)) if new_date < old_date => {
437 return Err(BanErrorKind::PermissionDenied(BanKind::Ban));
438 },
439 // Anything else (extension to permanent ban, or temp ban extension to
440 // longer temp ban).
441 _ => new_ban,
442 }
443 },
444 // An unban record is invalid if it does not follow a ban.
445 (BanAction::Unban(_), None) | (BanAction::Unban(_), Some(BanAction::Unban(_))) => {
446 return Err(BanErrorKind::AlreadyUnbanned);
447 },
448 // An unban record following a ban is valid if the role of the person doing the
449 // unbanning is at least the privilege level of the person who did the ban.
450 (BanAction::Unban(unban), Some(BanAction::Ban(ban))) => {
451 return if unban.performed_by_role >= ban.performed_by_role() {
452 Ok(())
453 } else {
454 Err(BanErrorKind::PermissionDenied(BanKind::Unban))
455 };
456 },
457 };
458
459 // End date of a ban must be at least as big as the start date.
460 if let Some(end_date) = ban.end_date {
461 if self.date > end_date {
462 return Err(BanErrorKind::InvalidDateRange {
463 start_date: self.date,
464 end_date,
465 });
466 }
467 }
468 Ok(())
469 }
470 }
471
472 #[derive(Clone, Deserialize, Serialize)]
473 pub struct BanEntry {
474 /// The latest ban record for this user.
475 pub current: BanRecord,
476 /// Historical ban records for this user, stored in order from oldest to
477 /// newest.
478 pub history: Vec<BanRecord>,
479 /// A *hint* about whether the system thinks this entry is expired,
480 /// mostly to make it easier for someone manually going through
481 /// a file to see whether an entry is currently in effect or
482 /// not. This is based off the contents of `current`.
483 pub expired: bool,
484 }
485
486 impl Deref for BanEntry {
487 type Target = BanRecord;
488
489 fn deref(&self) -> &Self::Target { &self.current }
490 }
491
492 impl BanEntry {
493 /// Both validates, and updates the hint bit if it's inconsistent with
494 /// reality.
495 ///
496 /// If we were invalid, returns an error. Otherwise, returns Ok(v),
497 /// where v is Latest if the hint bit was modified, Old
498 /// otherwise.
499 fn validate(
500 &mut self,
501 now: DateTime<Utc>,
502 uuid: Uuid,
503 ) -> Result<Version, <Final as EditableSetting>::Error> {
504 let make_error = |current_entry: &BanRecord| {
505 let username = current_entry.username_when_performed.clone();
506 move |kind| BanError::Uuid {
507 kind,
508 uuid,
509 username,
510 }
511 };
512 // First, go forwards through history (also forwards in terms of the iterator
513 // direction), validating each entry in turn.
514 let mut prev_entry = None;
515 for current_entry in &self.history {
516 current_entry
517 .validate(prev_entry)
518 .map_err(make_error(current_entry))?;
519 prev_entry = Some(current_entry);
520 }
521
522 // History has now been validated, so validate the current entry.
523 self.current
524 .validate(prev_entry)
525 .map_err(make_error(&self.current))?;
526
527 // Make sure the expired hint is correct, and if not indicate that we should
528 // resave the file.
529 let is_expired = self.current.is_expired(now);
530 if self.expired != is_expired {
531 self.expired = is_expired;
532 Ok(Version::Old)
533 } else {
534 Ok(Version::Latest)
535 }
536 }
537 }
538
539 #[derive(Clone, Deserialize, Serialize, Default)]
540 #[serde(transparent)]
541 pub struct Banlist(pub(super) HashMap<Uuid, BanEntry>);
542
543 impl Banlist {
544 /// One-off migration from the previous version. This must be
545 /// guaranteed to produce a valid settings file as long as it is
546 /// called with a valid settings file from the previous version.
547 pub(super) fn migrate(prev: prev::Banlist) -> Self {
548 // The ban start date for migrations from legacy is the current one; we could
549 // record that they actually have an unknown start date, but this
550 // would just complicate the format.
551 let date = Utc::now();
552 Banlist(
553 prev.0
554 .into_iter()
555 .map(
556 |(
557 uuid,
558 prev::BanRecord {
559 username_when_banned,
560 reason,
561 },
562 )| {
563 (uuid, BanEntry {
564 current: BanRecord {
565 username_when_performed: username_when_banned,
566 // We only recorded unbans pre-migration.
567 action: BanAction::Ban(Ban {
568 reason,
569 // We don't know who banned this user pre-migration.
570 info: None,
571 // All bans pre-migration are of unlimited duration.
572 end_date: None,
573 }),
574 date,
575 },
576 // Old bans never expire, so set the expiration hint to false.
577 expired: false,
578 // There is no known ban history yet.
579 history: Vec::new(),
580 })
581 },
582 )
583 .collect(),
584 )
585 }
586
587 /// Perform any needed validation on this banlist that can't be done
588 /// using parsing.
589 ///
590 /// The returned version being "Old" indicates the loaded setting has
591 /// been modified during validation (this is why validate takes
592 /// `&mut self`).
593 pub(super) fn validate(&mut self) -> Result<Version, <Final as EditableSetting>::Error> {
594 let mut version = Version::Latest;
595 let now = Utc::now();
596 for (&uuid, value) in self.0.iter_mut() {
597 if matches!(value.validate(now, uuid)?, Version::Old) {
598 // Update detected.
599 version = Version::Old;
600 }
601 }
602 Ok(version)
603 }
604 }
605
606 impl TryFrom<Banlist> for Final {
607 type Error = <Final as EditableSetting>::Error;
608
609 #[expect(clippy::useless_conversion)]
610 fn try_from(mut value: Banlist) -> Result<Final, Self::Error> {
611 value.validate()?;
612 Ok(next::Banlist::migrate(value)
613 .try_into()
614 .expect(MIGRATION_UPGRADE_GUARANTEE))
615 }
616 }
617}
618
619mod v2 {
620 use super::{
621 BanError, BanErrorKind, BanKind, BanOperation, BanOperationError, Final, v1 as prev,
622 };
623 use crate::settings::editable::{EditableSetting, Version};
624 use authc::Uuid;
625 use chrono::{Utc, prelude::*};
626 use common::comp::AdminRole;
627 use core::{mem, ops::Deref};
628 use hashbrown::{HashMap, hash_map};
629 use serde::{Deserialize, Serialize};
630 use std::net::{IpAddr, Ipv6Addr};
631 use tracing::warn;
632 /* use super::v3 as next; */
633
634 /// Important: even if the role we are storing here appears to be identical
635 /// to one used in another versioned store (like admin::Role), we *must*
636 /// have our own versioned copy! This ensures that if there's an update
637 /// to the role somewhere else, the conversion function between them
638 /// will break, letting people make an intelligent decision.
639 ///
640 /// In particular, *never remove variants from this enum* (or any other enum
641 /// in a versioned settings file) without bumping the version and
642 /// writing a migration that understands how to properly deal with
643 /// existing instances of the old variant (you can delete From instances
644 /// for the old variants at this point). Otherwise, we will lose
645 /// compatibility with old settings files, since we won't be able to
646 /// deserialize them!
647 #[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
648 pub enum Role {
649 Moderator = 0,
650 Admin = 1,
651 }
652
653 impl From<AdminRole> for Role {
654 fn from(value: AdminRole) -> Self {
655 match value {
656 AdminRole::Moderator => Self::Moderator,
657 AdminRole::Admin => Self::Admin,
658 }
659 }
660 }
661
662 impl From<Role> for AdminRole {
663 fn from(value: Role) -> Self {
664 match value {
665 Role::Moderator => Self::Moderator,
666 Role::Admin => Self::Admin,
667 }
668 }
669 }
670
671 #[derive(Clone, PartialEq, Eq, Deserialize, Serialize)]
672 /// NOTE: May not be present if performed from the command line or from a
673 /// legacy file.
674 pub struct BanInfo {
675 pub performed_by: Uuid,
676 /// NOTE: May not be up to date, if we allow username changes.
677 pub performed_by_username: String,
678 /// NOTE: Role of the banning user at the time of the ban.
679 pub performed_by_role: Role,
680 }
681
682 impl BanInfo {
683 fn migrate(
684 prev::BanInfo {
685 performed_by,
686 performed_by_username,
687 performed_by_role,
688 }: prev::BanInfo,
689 ) -> Self {
690 Self {
691 performed_by,
692 performed_by_username,
693 performed_by_role: match performed_by_role {
694 prev::Role::Moderator => Role::Moderator,
695 prev::Role::Admin => Role::Admin,
696 },
697 }
698 }
699 }
700
701 #[derive(Clone, Deserialize, Serialize)]
702 pub struct Ban {
703 pub reason: String,
704 /// NOTE: Should only be None for migrations from legacy data.
705 pub info: Option<BanInfo>,
706 /// NOTE: Should always be higher than the `date` in the record
707 /// containing this, if this is present!
708 pub end_date: Option<DateTime<Utc>>,
709 /// Upgrade this regular ban to an IP ban once the player attempts to
710 /// connect. Setting this to `true` in an IP ban has no effect.
711 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
712 pub upgrade_to_ip: bool,
713 }
714
715 impl Ban {
716 /// Returns true if the ban is expired, false otherwise.
717 pub fn is_expired(&self, now: DateTime<Utc>) -> bool {
718 self.end_date.is_some_and(|end_date| end_date <= now)
719 }
720
721 pub fn performed_by_role(&self) -> Role {
722 self.info.as_ref().map(|info| info.performed_by_role)
723 // We know all legacy bans were performed by an admin, since we had no other roles
724 // at the time.
725 .unwrap_or(Role::Admin)
726 }
727
728 pub fn info(&self) -> common_net::msg::server::BanInfo {
729 common_net::msg::server::BanInfo {
730 reason: self.reason.clone(),
731 until: self.end_date.map(|date| date.timestamp()),
732 }
733 }
734 }
735
736 type Unban = BanInfo;
737
738 #[derive(Clone, Deserialize, Serialize)]
739 pub enum BanAction {
740 Unban(Unban),
741 Ban(Ban),
742 }
743
744 impl BanAction {
745 pub fn ban(&self) -> Option<&Ban> {
746 match self {
747 BanAction::Unban(_) => None,
748 BanAction::Ban(ban) => Some(ban),
749 }
750 }
751 }
752
753 #[derive(Clone, Deserialize, Serialize)]
754 pub struct BanRecord {
755 /// Username of the user upon whom the action was performed, when it was
756 /// performed.
757 pub username_when_performed: String,
758 pub action: BanAction,
759 /// NOTE: When migrating from legacy versions, this will just be the
760 /// time of the first migration (only applies to BanRecord).
761 pub date: DateTime<Utc>,
762 }
763
764 impl BanRecord {
765 /// Returns true if this record represents an expired ban, false
766 /// otherwise.
767 fn is_expired(&self, now: DateTime<Utc>) -> bool {
768 match &self.action {
769 BanAction::Ban(ban) => ban.is_expired(now),
770 BanAction::Unban(_) => true,
771 }
772 }
773
774 fn migrate(
775 prev::BanRecord {
776 username_when_performed,
777 action,
778 date,
779 }: prev::BanRecord,
780 ) -> Self {
781 BanRecord {
782 username_when_performed,
783 action: match action {
784 prev::BanAction::Ban(prev::Ban {
785 reason,
786 info,
787 end_date,
788 }) => BanAction::Ban(Ban {
789 reason,
790 info: info.map(BanInfo::migrate),
791 end_date,
792 upgrade_to_ip: false,
793 }),
794 prev::BanAction::Unban(info) => BanAction::Unban(BanInfo::migrate(info)),
795 },
796 date,
797 }
798 }
799
800 /// The history vector in a BanEntry is stored forwards (from oldest
801 /// entry to newest), so `prev_record` is the previous entry in
802 /// this vector when iterating forwards (by array index).
803 ///
804 /// Errors are:
805 ///
806 /// AlreadyUnbanned if an unban comes after anything but a ban.
807 ///
808 /// Permission(Unban) if an unban attempt is by a user with a lower role
809 /// level than the original banning party.
810 ///
811 /// PermissionDenied(Ban) if a ban length is made shorter by a user with
812 /// a role level than the original banning party.
813 ///
814 /// InvalidDateRange if the end date of the ban exceeds the start date.
815 fn validate(&self, prev_record: Option<&BanRecord>) -> Result<(), BanErrorKind> {
816 // Check to make sure the actions temporally line up--if they don't, we will
817 // prevent warn an administrator (since this may indicate a system
818 // clock issue and could require manual editing to resolve).
819 // However, we will not actually invalidate the ban list for this, in case
820 // this would otherwise prevent people from adding a new ban.
821 //
822 // We also deliberately leave the bad order intact, in case this reflects
823 // history more accurately than the system clock does.
824 if let Some(prev_record) = prev_record {
825 if prev_record.date > self.date {
826 warn!(
827 "Ban list history is inconsistent, or a just-added ban was behind a \
828 historical entry in the ban
829 record; please investigate the contents of the file (might indicate a \
830 system clock change?)."
831 );
832 }
833 }
834 let ban = match (&self.action, prev_record.map(|record| &record.action)) {
835 // A ban is always valid if it follows an unban.
836 (BanAction::Ban(ban), None) | (BanAction::Ban(ban), Some(BanAction::Unban(_))) => {
837 ban
838 },
839 // A ban record following a ban is valid if either the role of the person doing the
840 // banning is at least the privilege level of the person who did the ban, or the
841 // ban's new end time is at least the previous end time.
842 (BanAction::Ban(new_ban), Some(BanAction::Ban(old_ban))) => {
843 match (new_ban.end_date, old_ban.end_date) {
844 // New role ≥ old role
845 _ if new_ban.performed_by_role() >= old_ban.performed_by_role() => new_ban,
846 // Permanent ban retracted to temp ban.
847 (Some(_), None) => {
848 return Err(BanErrorKind::PermissionDenied(BanKind::Ban));
849 },
850 // Temp ban retracted to shorter temp ban.
851 (Some(new_date), Some(old_date)) if new_date < old_date => {
852 return Err(BanErrorKind::PermissionDenied(BanKind::Ban));
853 },
854 // Anything else (extension to permanent ban, or temp ban extension to
855 // longer temp ban).
856 _ => new_ban,
857 }
858 },
859 // An unban record is invalid if it does not follow a ban.
860 (BanAction::Unban(_), None) | (BanAction::Unban(_), Some(BanAction::Unban(_))) => {
861 return Err(BanErrorKind::AlreadyUnbanned);
862 },
863 // An unban record following a ban is valid if the role of the person doing the
864 // unbanning is at least the privilege level of the person who did the ban.
865 (BanAction::Unban(unban), Some(BanAction::Ban(ban))) => {
866 return if unban.performed_by_role >= ban.performed_by_role() {
867 Ok(())
868 } else {
869 Err(BanErrorKind::PermissionDenied(BanKind::Unban))
870 };
871 },
872 };
873
874 // End date of a ban must be at least as big as the start date.
875 if let Some(end_date) = ban.end_date {
876 if self.date > end_date {
877 return Err(BanErrorKind::InvalidDateRange {
878 start_date: self.date,
879 end_date,
880 });
881 }
882 }
883 Ok(())
884 }
885 }
886
887 #[derive(Clone, Deserialize, Serialize)]
888 pub struct BanEntry {
889 /// The latest ban record for this user.
890 pub current: BanRecord,
891 /// Historical ban records for this user, stored in order from oldest to
892 /// newest.
893 pub history: Vec<BanRecord>,
894 /// A *hint* about whether the system thinks this entry is expired,
895 /// mostly to make it easier for someone manually going through
896 /// a file to see whether an entry is currently in effect or
897 /// not. This is based off the contents of `current`.
898 pub expired: bool,
899 }
900
901 impl Deref for BanEntry {
902 type Target = BanRecord;
903
904 fn deref(&self) -> &Self::Target { &self.current }
905 }
906
907 impl BanEntry {
908 fn migrate(
909 prev::BanEntry {
910 current,
911 history,
912 expired,
913 }: prev::BanEntry,
914 ) -> Self {
915 Self {
916 current: BanRecord::migrate(current),
917 history: history.into_iter().map(BanRecord::migrate).collect(),
918 expired,
919 }
920 }
921
922 /// Both validates, and updates the hint bit if it's inconsistent with
923 /// reality.
924 ///
925 /// If we were invalid, returns an error. Otherwise, returns Ok(v),
926 /// where v is Latest if the hint bit was modified, Old
927 /// otherwise.
928 fn validate(
929 &mut self,
930 now: DateTime<Utc>,
931 uuid: Uuid,
932 ) -> Result<Version, <Final as EditableSetting>::Error> {
933 let make_error = |kind, current_entry: &BanRecord| BanError::Uuid {
934 kind,
935 uuid,
936 username: current_entry.username_when_performed.clone(),
937 };
938 // First, go forwards through history (also forwards in terms of the iterator
939 // direction), validating each entry in turn.
940 let mut prev_entry = None;
941 for current_entry in &self.history {
942 current_entry
943 .validate(prev_entry)
944 .map_err(|kind| make_error(kind, current_entry))?;
945 prev_entry = Some(current_entry);
946 }
947
948 // History has now been validated, so validate the current entry.
949 self.current
950 .validate(prev_entry)
951 .map_err(|kind| make_error(kind, &self.current))?;
952
953 // Make sure the expired hint is correct, and if not indicate that we should
954 // resave the file.
955 let is_expired = self.current.is_expired(now);
956 if self.expired != is_expired {
957 self.expired = is_expired;
958 Ok(Version::Old)
959 } else {
960 Ok(Version::Latest)
961 }
962 }
963 }
964
965 #[derive(Clone, Deserialize, Serialize)]
966 pub struct IpBanRecord {
967 /// Uuid of the user through which this IP ban was applied.
968 ///
969 /// This is optional to allow for the potenital of non-user-associated
970 /// IP bans.
971 pub uuid_when_performed: Option<Uuid>,
972 pub action: BanAction,
973 /// NOTE: When migrating from legacy versions, this will just be the
974 /// time of the first migration (only applies to BanRecord).
975 pub date: DateTime<Utc>,
976 }
977
978 impl IpBanRecord {
979 /// Returns true if this record represents an expired ban, false
980 /// otherwise.
981 fn is_expired(&self, now: DateTime<Utc>) -> bool {
982 match &self.action {
983 BanAction::Ban(ban) => ban.is_expired(now),
984 BanAction::Unban(_) => true,
985 }
986 }
987
988 /// The history vector in a IpBanEntry is stored forwards (from oldest
989 /// entry to newest), so `prev_record` is the previous entry in
990 /// this vector when iterating forwards (by array index).
991 ///
992 /// Errors are:
993 ///
994 /// AlreadyUnbanned if an unban comes after anything but a ban.
995 ///
996 /// Permission(Unban) if an unban attempt is by a user with a lower role
997 /// level than the original banning party.
998 ///
999 /// PermissionDenied(Ban) if a ban length is made shorter by a user with
1000 /// a role level than the original banning party.
1001 ///
1002 /// InvalidDateRange if the end date of the ban exceeds the start date.
1003 fn validate(&self, prev_record: Option<&IpBanRecord>) -> Result<(), BanErrorKind> {
1004 // Check to make sure the actions temporally line up--if they don't, we will
1005 // prevent warn an administrator (since this may indicate a system
1006 // clock issue and could require manual editing to resolve).
1007 // However, we will not actually invalidate the ban list for this, in case
1008 // this would otherwise prevent people from adding a new ban.
1009 //
1010 // We also deliberately leave the bad order intact, in case this reflects
1011 // history more accurately than the system clock does.
1012 if let Some(prev_record) = prev_record {
1013 if prev_record.date > self.date {
1014 warn!(
1015 "Ban list history is inconsistent, or a just-added ban was behind a \
1016 historical entry in the ban
1017 record; please investigate the contents of the file (might indicate a \
1018 system clock change?)."
1019 );
1020 }
1021 }
1022 let ban = match (&self.action, prev_record.map(|record| &record.action)) {
1023 // A ban is always valid if it follows an unban.
1024 (BanAction::Ban(ban), None) | (BanAction::Ban(ban), Some(BanAction::Unban(_))) => {
1025 ban
1026 },
1027 // A ban record following a ban is valid if either the role of the person doing the
1028 // banning is at least the privilege level of the person who did the ban, or the
1029 // ban's new end time is at least the previous end time.
1030 (BanAction::Ban(new_ban), Some(BanAction::Ban(old_ban))) => {
1031 match (new_ban.end_date, old_ban.end_date) {
1032 // New role ≥ old role
1033 _ if new_ban.performed_by_role() >= old_ban.performed_by_role() => new_ban,
1034 // Permanent ban retracted to temp ban.
1035 (Some(_), None) => {
1036 return Err(BanErrorKind::PermissionDenied(BanKind::Ban));
1037 },
1038 // Temp ban retracted to shorter temp ban.
1039 (Some(new_date), Some(old_date)) if new_date < old_date => {
1040 return Err(BanErrorKind::PermissionDenied(BanKind::Ban));
1041 },
1042 // Anything else (extension to permanent ban, or temp ban extension to
1043 // longer temp ban).
1044 _ => new_ban,
1045 }
1046 },
1047 // An unban record is invalid if it does not follow a ban.
1048 (BanAction::Unban(_), None) | (BanAction::Unban(_), Some(BanAction::Unban(_))) => {
1049 return Err(BanErrorKind::AlreadyUnbanned);
1050 },
1051 // An unban record following a ban is valid if the role of the person doing the
1052 // unbanning is at least the privilege level of the person who did the ban.
1053 (BanAction::Unban(unban), Some(BanAction::Ban(ban))) => {
1054 return if unban.performed_by_role >= ban.performed_by_role() {
1055 Ok(())
1056 } else {
1057 Err(BanErrorKind::PermissionDenied(BanKind::Unban))
1058 };
1059 },
1060 };
1061
1062 // End date of a ban must be at least as big as the start date.
1063 if let Some(end_date) = ban.end_date {
1064 if self.date > end_date {
1065 return Err(BanErrorKind::InvalidDateRange {
1066 start_date: self.date,
1067 end_date,
1068 });
1069 }
1070 }
1071 Ok(())
1072 }
1073 }
1074
1075 #[derive(Clone, Deserialize, Serialize)]
1076 pub struct IpBanEntry {
1077 /// The latest ban record for this IP.
1078 ///
1079 /// Note: If this IP is currently banned and the current `BanRecord`
1080 /// contains a Uuid, then this user must also be banned in the
1081 /// Uuid ban map. This is enforced by the validation.
1082 pub current: IpBanRecord,
1083 /// Historical ban records for this user, stored in order from oldest to
1084 /// newest.
1085 pub history: Vec<IpBanRecord>,
1086 /// A *hint* about whether the system thinks this entry is expired,
1087 /// mostly to make it easier for someone manually going through
1088 /// a file to see whether an entry is currently in effect or
1089 /// not. This is based off the contents of `current`.
1090 pub expired: bool,
1091 }
1092
1093 impl Deref for IpBanEntry {
1094 type Target = IpBanRecord;
1095
1096 fn deref(&self) -> &Self::Target { &self.current }
1097 }
1098
1099 impl IpBanEntry {
1100 /// Both validates, and updates the hint bit if it's inconsistent with
1101 /// reality.
1102 ///
1103 /// If we were invalid, returns an error. Otherwise, returns Ok(v),
1104 /// where v is Latest if the hint bit was modified, Old
1105 /// otherwise.
1106 fn validate(
1107 &mut self,
1108 now: DateTime<Utc>,
1109 uuid_bans: &HashMap<Uuid, BanEntry>,
1110 ) -> Result<Version, <Final as EditableSetting>::Error> {
1111 let make_error = |kind, current_entry: &IpBanRecord| {
1112 let uuid = current_entry.uuid_when_performed;
1113
1114 BanError::Ip {
1115 kind,
1116 uuid,
1117 username_from_uuid_entry: uuid
1118 .and_then(|u| uuid_bans.get(&u))
1119 .map(|e| e.current.username_when_performed.clone()),
1120 }
1121 };
1122 // First, go forwards through history (also forwards in terms of the iterator
1123 // direction), validating each entry in turn.
1124 let mut prev_entry = None;
1125 for current_entry in &self.history {
1126 current_entry
1127 .validate(prev_entry)
1128 .map_err(|kind| make_error(kind, current_entry))?;
1129 prev_entry = Some(current_entry);
1130 }
1131
1132 // History has now been validated, so validate the current entry.
1133 self.current
1134 .validate(prev_entry)
1135 .map_err(|kind| make_error(kind, &self.current))?;
1136
1137 // If the current entry is an unexpired ban and is linked to a uuid,
1138 // then that uuid must also be banned. These bans must also have the
1139 // same expiration and have matching `BanInfo`.
1140 if let Some(uuid) = self.current.uuid_when_performed {
1141 let uuid_entry = uuid_bans.get(&uuid).ok_or_else(|| {
1142 make_error(BanErrorKind::CorrespondingUuidBanMissing, &self.current)
1143 })?;
1144
1145 if let BanAction::Ban(ip_ban) = &self.current.action
1146 && !self.current.is_expired(now)
1147 {
1148 if let BanAction::Ban(uuid_ban) = &uuid_entry.current.action {
1149 let ip_info = ip_ban.info.as_ref().ok_or_else(|| {
1150 make_error(BanErrorKind::NonLegacyBanMissingBanInfo, &self.current)
1151 })?;
1152 let uuid_info = uuid_ban.info.as_ref().ok_or_else(|| {
1153 make_error(BanErrorKind::NonLegacyBanMissingBanInfo, &self.current)
1154 })?;
1155
1156 // Expiration time must match, so that the banned user
1157 // cannot join and be banned from another IP address.
1158 //
1159 // BanInfo must match as well since these bans should
1160 // have been performed by the same user.
1161 if ip_ban.end_date == uuid_ban.end_date && ip_info == uuid_info {
1162 Ok(())
1163 } else {
1164 Err(make_error(
1165 BanErrorKind::CorrespondingUuidBanMismatch,
1166 &self.current,
1167 ))
1168 }
1169 } else {
1170 Err(make_error(
1171 BanErrorKind::CorrespondingUuidBanMismatch,
1172 &self.current,
1173 ))
1174 }?;
1175 }
1176 }
1177
1178 // Make sure the expired hint is correct, and if not indicate that we should
1179 // resave the file.
1180 let is_expired = self.current.is_expired(now);
1181 if self.expired != is_expired {
1182 self.expired = is_expired;
1183 Ok(Version::Old)
1184 } else {
1185 Ok(Version::Latest)
1186 }
1187 }
1188 }
1189
1190 /// The last 64 bits of IPv6 addresess may vary a lot even when coming from
1191 /// the same client, and taking the full IPv6 for IP bans is thus useless.
1192 ///
1193 /// This newtype ensures that all the last 64 bits of an IPv6 address are
1194 /// set to zero to counter this.
1195 #[derive(Clone, Copy, Debug, Deserialize, Serialize, Hash, PartialEq, Eq)]
1196 #[serde(transparent)]
1197 pub struct NormalizedIpAddr(IpAddr);
1198
1199 impl From<IpAddr> for NormalizedIpAddr {
1200 fn from(ip: IpAddr) -> Self {
1201 Self(match ip {
1202 // Take IPv4 adddresses as-is
1203 IpAddr::V4(ip) => IpAddr::V4(ip),
1204 // Ignore the last 64 bits for IPv6 addresses
1205 IpAddr::V6(ip) => IpAddr::V6(Ipv6Addr::from_bits(
1206 ip.to_bits() & 0xffff_ffff_ffff_ffff_0000_0000_0000_0000_u128,
1207 )),
1208 })
1209 }
1210 }
1211
1212 impl Deref for NormalizedIpAddr {
1213 type Target = IpAddr;
1214
1215 fn deref(&self) -> &Self::Target { &self.0 }
1216 }
1217
1218 #[derive(Clone, Deserialize, Serialize, Default)]
1219 pub struct Banlist {
1220 pub(super) uuid_bans: HashMap<Uuid, BanEntry>,
1221 pub(super) ip_bans: HashMap<NormalizedIpAddr, IpBanEntry>,
1222 }
1223
1224 impl Banlist {
1225 pub fn uuid_bans(&self) -> &HashMap<Uuid, BanEntry> { &self.uuid_bans }
1226
1227 pub fn ip_bans(&self) -> &HashMap<NormalizedIpAddr, IpBanEntry> { &self.ip_bans }
1228
1229 /// Attempt to perform the ban operation `operation` for the user with
1230 /// UUID `uuid` and username `username`, starting from time `now` (the
1231 /// information about the banning party will be in the `operation`
1232 /// record), with a settings file maintained at path root `data_dir`.
1233 ///
1234 /// Banning an IP via a user will also ban that user's UUID.
1235 /// Additionally, a regular UUID unban will also produce an IP unban if
1236 /// a corresponding one is active and linked to the unbanned UUID.
1237 ///
1238 /// If trying to unban an already unbanned player, or trying to ban but
1239 /// the ban status would not immediately change, the "overwrite" boolean
1240 /// should also be set to true.
1241 ///
1242 /// We try to detect duplicates (bans that would have no effect) and
1243 /// return `Err(BanOperationError::NoEffect)` if such effects are
1244 /// encountered.
1245 ///
1246 /// If the errors outlined above are successfully avoided, we attempt
1247 /// the edit either succeeding and returning `Ok(())` or returning
1248 /// `Err(BanOperationError::EditFailed(error))`, which works as follows.
1249 ///
1250 /// If the ban was invalid for any reason, then neither the in-memory
1251 /// banlist nor the on-disk banlist are modified. If the ban
1252 /// entry is valid but the file encounters an error that
1253 /// prevents it from being atomically written to disk, we return an
1254 /// error but retain the change in memory. Otherwise, we
1255 /// complete successfully and atomically write the banlist to
1256 /// disk.
1257 ///
1258 /// Note that the IO operation is only *guaranteed* atomic in the weak
1259 /// sense that either the whole page is written or it isn't; we
1260 /// cannot guarantee that the data we read in order to modify
1261 /// the file was definitely up to date, so we could be missing
1262 /// information if the file was manually edited or a function
1263 /// edits it without going through the usual specs resources.
1264 /// So, please be careful with ad hoc modifications to the file while
1265 /// the server is running.
1266 pub fn ban_operation(
1267 &mut self,
1268 data_dir: &std::path::Path,
1269 now: DateTime<Utc>,
1270 uuid: Uuid,
1271 username_when_performed: String,
1272 operation: BanOperation,
1273 overwrite: bool,
1274 ) -> Result<Option<common_net::msg::server::BanInfo>, BanOperationError> {
1275 let make_record = |action| BanRecord {
1276 username_when_performed,
1277 action,
1278 date: now,
1279 };
1280 let make_ip_record = |action| IpBanRecord {
1281 // Note: we may support banning IPs without associated user in the future.
1282 uuid_when_performed: Some(uuid),
1283 action,
1284 date: now,
1285 };
1286
1287 // Perform an atomic edit.
1288 let edit_result = self.edit(data_dir.as_ref(), |banlist| {
1289 match operation {
1290 BanOperation::Ban {
1291 reason,
1292 info,
1293 end_date,
1294 upgrade_to_ip: upgrade_to_ip_ban,
1295 } => {
1296 let ban = Ban {
1297 reason,
1298 info: Some(info),
1299 end_date,
1300 upgrade_to_ip: upgrade_to_ip_ban,
1301 };
1302 let frontend_info = ban.info();
1303 let action = BanAction::Ban(ban);
1304 let ban_record = make_record(action);
1305 // NOTE: If there is linked IP ban, `overwrite` based changes may fail. In
1306 // the future, we may want to switch this to autoupdate the IP ban.
1307 banlist
1308 .apply_ban_record(uuid, ban_record, overwrite, now)
1309 .map(|_| Some(frontend_info))
1310 },
1311 BanOperation::UpgradeToIpBan { ip } => {
1312 let Some(ban_entry) = banlist
1313 .uuid_bans
1314 .get_mut(&uuid)
1315 .filter(|ban| !ban.current.is_expired(now))
1316 else {
1317 warn!(?uuid, "No active ban exists for this uuid");
1318 return None;
1319 };
1320 let BanAction::Ban(ban) = &mut ban_entry.current.action else {
1321 warn!(?uuid, "This uuid is not banned");
1322 return None;
1323 };
1324 if !mem::take(&mut ban.upgrade_to_ip) {
1325 warn!(?uuid, "The current ban on this uuid is not upgradeable");
1326 return None;
1327 }
1328 let info = ban.info();
1329 let ip_record = make_ip_record(ban_entry.action.clone());
1330
1331 banlist
1332 .apply_ip_ban_record(ip, ip_record, overwrite, now)
1333 .map(|()| Some(info))
1334 },
1335 BanOperation::BanIp {
1336 reason,
1337 info,
1338 end_date,
1339 ip,
1340 } => {
1341 let ban = Ban {
1342 reason,
1343 info: Some(info),
1344 end_date,
1345 upgrade_to_ip: false,
1346 };
1347 let frontend_info = ban.info();
1348 let action = BanAction::Ban(ban);
1349 let ban_record = make_record(action.clone());
1350 let ip_ban_record = make_ip_record(action);
1351
1352 // If a user is able to connect with a banned IP (e.g. a
1353 // moderator), and then `overwrite` is used with the IP
1354 // ban operation on them, this will result in changing
1355 // the user which is linked to this IP ban (the IP will
1356 // no longer be unbanned if the previous user is
1357 // unbanned). This should not cause any issues with our
1358 // validated invariant of having a UUID linked to at
1359 // most 1 active IP ban.
1360 let ban_effect = banlist.apply_ban_record(uuid, ban_record, overwrite, now);
1361 let ip_ban_effect =
1362 banlist.apply_ip_ban_record(ip, ip_ban_record, overwrite, now);
1363 // Only submit edit if one of these had an effect.
1364 ban_effect.or(ip_ban_effect).map(|_| Some(frontend_info))
1365 },
1366 BanOperation::Unban { info } => {
1367 let action = BanAction::Unban(info);
1368 let ban_record = make_record(action.clone());
1369 let ban_effect = banlist.apply_ban_record(uuid, ban_record, overwrite, now);
1370 // If there is a matching IP ban we should remove that as well.
1371 //
1372 // Validation checks that there is only one active IP ban for a particular
1373 // uuid, since if we ensure IP bans also ban the uuid and regular unbans
1374 // remove an existing IP ban if it exists then a user won't be able to
1375 // connect from another IP while there is an active IP ban linked to that
1376 // user.
1377 let ip = banlist
1378 .ip_bans
1379 .iter()
1380 .find(|(_ip, entry)| {
1381 entry.current.uuid_when_performed == Some(uuid)
1382 && !entry.current.is_expired(now)
1383 })
1384 .map(|(ip, _)| *ip);
1385
1386 ip.and_then(|ip| {
1387 let ip_ban_record = make_ip_record(action);
1388 banlist.apply_ip_ban_record(ip, ip_ban_record, overwrite, now)
1389 })
1390 // Only submit edit if one of these had an effect.
1391 .or(ban_effect).map(|_| None)
1392 },
1393 BanOperation::UnbanIp { info, uuid } => {
1394 let ip = banlist
1395 .ip_bans
1396 .iter()
1397 .find(|(_ip, entry)| {
1398 entry.current.uuid_when_performed == Some(uuid)
1399 && !entry.current.is_expired(now)
1400 })
1401 .map(|(ip, _)| *ip);
1402
1403 ip.and_then(|ip| {
1404 // Note: It is kind of redundant to include uuid here (since it's not
1405 // going to change from the ban).
1406 banlist.apply_ip_ban_record(
1407 ip,
1408 make_ip_record(BanAction::Unban(info)),
1409 overwrite,
1410 now,
1411 )
1412 })
1413 .map(|_| None)
1414 },
1415 }
1416 });
1417
1418 match edit_result {
1419 Some((info, Ok(()))) => Ok(info),
1420 Some((_, Err(err))) => Err(BanOperationError::EditFailed(err)),
1421 None => Err(BanOperationError::NoEffect),
1422 }
1423 }
1424
1425 /// Only meant to be called by `Self::ban_operation` within the `edit`
1426 /// closure.
1427 ///
1428 /// Returns `None` to cancel early and abandon the edit.
1429 #[must_use]
1430 fn apply_ban_record(
1431 &mut self,
1432 uuid: Uuid,
1433 record: BanRecord,
1434 overwrite: bool,
1435 now: DateTime<Utc>,
1436 ) -> Option<()> {
1437 match self.uuid_bans.entry(uuid) {
1438 hash_map::Entry::Vacant(v) => {
1439 // If this is an unban, it will have no effect, so return early.
1440 if matches!(record.action, BanAction::Unban(_)) {
1441 return None;
1442 }
1443 // Otherwise, this will at least potentially have an effect
1444 // (assuming it succeeds).
1445 v.insert(BanEntry {
1446 current: record,
1447 history: Vec::new(),
1448 // This is a hint anyway, but expired will also be set to true
1449 // before saving by the call `edit`
1450 // makes to `validate` (through `try_into`), which will set it
1451 // to true in the event that
1452 // the ban time was so short
1453 // that it expired during the interval
1454 // between creating the action and saving it.
1455 //
1456 // TODO: Decide if we even care enough about this case to worry
1457 // about the gap. Probably not, even
1458 // though it does involve time!
1459 expired: false,
1460 });
1461 },
1462 hash_map::Entry::Occupied(mut o) => {
1463 let entry = o.get_mut();
1464 // If overwrite is off, check that this entry (if successful) would
1465 // actually change the ban status.
1466 if !overwrite && entry.current.is_expired(now) == record.is_expired(now) {
1467 return None;
1468 }
1469 // Push the current (most recent) entry to the back of the history
1470 // list.
1471 entry.history.push(mem::replace(&mut entry.current, record));
1472 },
1473 }
1474 Some(())
1475 }
1476
1477 /// Only meant to be called by `Self::ban_operation` within the `edit`
1478 /// closure.
1479 ///
1480 /// Returns `None` to cancel early and abandon the edit.
1481 #[must_use]
1482 fn apply_ip_ban_record(
1483 &mut self,
1484 ip: NormalizedIpAddr,
1485 record: IpBanRecord,
1486 overwrite: bool,
1487 now: DateTime<Utc>,
1488 ) -> Option<()> {
1489 match self.ip_bans.entry(ip) {
1490 hash_map::Entry::Vacant(v) => {
1491 // If this is an unban, it will have no effect, so return early.
1492 if matches!(record.action, BanAction::Unban(_)) {
1493 return None;
1494 }
1495 // Otherwise, this will at least potentially have an effect
1496 // (assuming it succeeds).
1497 v.insert(IpBanEntry {
1498 current: record,
1499 history: Vec::new(),
1500 // This is a hint anyway, but expired will also be set to true
1501 // before saving by the call `edit`
1502 // makes to `validate` (through `try_into`), which will set it
1503 // to true in the event that
1504 // the ban time was so short
1505 // that it expired during the interval
1506 // between creating the action and saving it.
1507 //
1508 // TODO: Decide if we even care enough about this case to worry
1509 // about the gap. Probably not, even
1510 // though it does involve time!
1511 expired: false,
1512 });
1513 },
1514 hash_map::Entry::Occupied(mut o) => {
1515 let entry = o.get_mut();
1516 // If overwrite is off, check that this entry (if successful) would
1517 // actually change the ban status.
1518 if !overwrite && entry.current.is_expired(now) == record.is_expired(now) {
1519 return None;
1520 }
1521 // Push the current (most recent) entry to the back of the history
1522 // list.
1523 entry.history.push(mem::replace(&mut entry.current, record));
1524 },
1525 }
1526 Some(())
1527 }
1528
1529 /// One-off migration from the previous version. This must be
1530 /// guaranteed to produce a valid settings file as long as it is
1531 /// called with a valid settings file from the previous version.
1532 pub(super) fn migrate(prev: prev::Banlist) -> Self {
1533 let prev::Banlist(uuid_map) = prev;
1534 // Mostly the same structure but we introduce a new type of ban in a separate
1535 // map.
1536 Banlist {
1537 uuid_bans: uuid_map
1538 .into_iter()
1539 .map(|(uuid, entry)| (uuid, BanEntry::migrate(entry)))
1540 .collect(),
1541 // Previous version had no concept of IP bans
1542 ip_bans: HashMap::new(),
1543 }
1544 }
1545
1546 /// Perform any needed validation on this banlist that can't be done
1547 /// using parsing.
1548 ///
1549 /// The returned version being "Old" indicates the loaded setting has
1550 /// been modified during validation (this is why validate takes
1551 /// `&mut self`).
1552 pub(super) fn validate(&mut self) -> Result<Version, <Final as EditableSetting>::Error> {
1553 let mut version = Version::Latest;
1554 let now = Utc::now();
1555 let Self { uuid_bans, ip_bans } = self;
1556 for (&uuid, value) in uuid_bans.iter_mut() {
1557 if matches!(value.validate(now, uuid)?, Version::Old) {
1558 // Update detected.
1559 version = Version::Old;
1560 }
1561 }
1562
1563 let mut uuids = hashbrown::HashSet::new();
1564 for (&_ip_addr, value) in ip_bans.iter_mut() {
1565 // Validate that there are not multiple active IP bans
1566 // linked to the same UUID. (since if timing happens to match
1567 // the per entry validation won't catch this)
1568 //
1569 // collapsible_if: more clear not to have side effects in the if condition
1570 #[expect(clippy::collapsible_if)]
1571 if let Some(uuid) = value.current.uuid_when_performed
1572 && !value.current.is_expired(now)
1573 {
1574 if !uuids.insert(uuid) {
1575 return Err(BanError::Ip {
1576 kind: BanErrorKind::ActiveIpBansShareUuid,
1577 uuid: Some(uuid),
1578 username_from_uuid_entry: uuid_bans
1579 .get(&uuid)
1580 .map(|e| e.current.username_when_performed.clone()),
1581 });
1582 }
1583 }
1584 if matches!(value.validate(now, uuid_bans)?, Version::Old) {
1585 // Update detected.
1586 version = Version::Old;
1587 }
1588 }
1589 Ok(version)
1590 }
1591 }
1592
1593 // NOTE: Whenever there is a version upgrade, copy this note as well as the
1594 // commented-out code below to the next version, then uncomment the code
1595 // for this version.
1596 /* impl TryFrom<Banlist> for Final {
1597 type Error = <Final as EditableSetting>::Error;
1598
1599 #[expect(clippy::useless_conversion)]
1600 fn try_from(mut value: Banlist) -> Result<Final, Self::Error> {
1601 value.validate()?;
1602 Ok(next::Banlist::migrate(value).try_into().expect(MIGRATION_UPGRADE_GUARANTEE))
1603 }
1604 } */
1605}