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}