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}