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