veloren_server/settings/whitelist.rs
1//! Versioned whitelist settings files.
2
3// NOTE: Needed to allow the second-to-last migration to call `try_into()`.
4
5use super::{MIGRATION_UPGRADE_GUARANTEE, WHITELIST_FILENAME as FILENAME};
6use crate::settings::editable::{EditableSetting, Version};
7use core::convert::{Infallible, TryFrom, TryInto};
8use serde::{Deserialize, Serialize};
9
10/// NOTE: Always replace this with the latest whitelist version. Then update the
11/// `WhitelistRaw`, the `TryFrom<WhitelistRaw>` for `Whitelist`, the previously
12/// most recent module, and add a new module for the latest version! Please
13/// respect the migration upgrade guarantee found in the parent module with any
14/// upgrade.
15pub use self::v1::*;
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 WhitelistRaw {
21 V0(v0::Whitelist),
22 V1(Whitelist),
23}
24
25impl From<Whitelist> for WhitelistRaw {
26 fn from(value: Whitelist) -> Self {
27 // Replace variant with that of current latest version.
28 Self::V1(value)
29 }
30}
31
32#[expect(clippy::infallible_try_from)] // TODO: evaluate
33impl TryFrom<WhitelistRaw> for (Version, Whitelist) {
34 type Error = <Whitelist as EditableSetting>::Error;
35
36 fn try_from(value: WhitelistRaw) -> Result<Self, <Whitelist as EditableSetting>::Error> {
37 use WhitelistRaw::*;
38 Ok(match value {
39 // Old versions
40 V0(value) => (Version::Old, value.try_into()?),
41 // Latest version (move to old section using the pattern of other old version when it
42 // is no longer latest).
43 V1(mut value) => (value.validate()?, value),
44 })
45 }
46}
47
48type Final = Whitelist;
49
50impl EditableSetting for Whitelist {
51 type Error = Infallible;
52 type Legacy = legacy::Whitelist;
53 type Setting = WhitelistRaw;
54
55 const FILENAME: &'static str = FILENAME;
56}
57
58mod legacy {
59 use super::{Final, MIGRATION_UPGRADE_GUARANTEE, v0 as next};
60 use authc::Uuid;
61 use core::convert::TryInto;
62 use hashbrown::HashSet;
63 use serde::{Deserialize, Serialize};
64
65 #[derive(Deserialize, Serialize, Default)]
66 #[serde(transparent)]
67 pub struct Whitelist(pub(super) HashSet<Uuid>);
68
69 impl From<Whitelist> for Final {
70 /// Legacy migrations can be migrated to the latest version through the
71 /// process of "chaining" migrations, starting from
72 /// `next::Whitelist`.
73 ///
74 /// Note that legacy files are always valid, which is why we implement
75 /// `From` rather than `TryFrom`.
76 fn from(value: Whitelist) -> Self {
77 next::Whitelist::migrate(value)
78 .try_into()
79 .expect(MIGRATION_UPGRADE_GUARANTEE)
80 }
81 }
82}
83
84/// This module represents a whitelist version that isn't actually used. It is
85/// here and part of the migration process to provide an example for how to
86/// perform a migration for an old version; please use this as a reference when
87/// constructing new migrations.
88mod v0 {
89 use super::{Final, MIGRATION_UPGRADE_GUARANTEE, legacy as prev, v1 as next};
90 use crate::settings::editable::{EditableSetting, Version};
91 use authc::Uuid;
92 use core::convert::{TryFrom, TryInto};
93 use hashbrown::HashSet;
94 use serde::{Deserialize, Serialize};
95
96 #[derive(Clone, Deserialize, Serialize, Default)]
97 #[serde(transparent)]
98 pub struct Whitelist(pub(super) HashSet<Uuid>);
99
100 impl Whitelist {
101 /// One-off migration from the previous version. This must be
102 /// guaranteed to produce a valid settings file as long as it is
103 /// called with a valid settings file from the previous version.
104 pub(super) fn migrate(prev: prev::Whitelist) -> Self { Whitelist(prev.0) }
105
106 /// Perform any needed validation on this whitelist that can't be done
107 /// using parsing.
108 ///
109 /// The returned version being "Old" indicates the loaded setting has
110 /// been modified during validation (this is why validate takes
111 /// `&mut self`).
112 pub(super) fn validate(&mut self) -> Result<Version, <Final as EditableSetting>::Error> {
113 Ok(Version::Latest)
114 }
115 }
116
117 /// Pretty much every `TryFrom` implementation except that of the very last
118 /// version should look exactly like this.
119 #[expect(clippy::infallible_try_from)] // TODO: evaluate
120 impl TryFrom<Whitelist> for Final {
121 type Error = <Final as EditableSetting>::Error;
122
123 #[expect(clippy::useless_conversion)]
124 fn try_from(mut value: Whitelist) -> Result<Final, Self::Error> {
125 value.validate()?;
126 Ok(next::Whitelist::migrate(value)
127 .try_into()
128 .expect(MIGRATION_UPGRADE_GUARANTEE))
129 }
130 }
131}
132
133mod v1 {
134 use super::{Final, v0 as prev};
135 use crate::settings::editable::{EditableSetting, Version};
136 use authc::Uuid;
137 use chrono::{Utc, prelude::*};
138 use common::comp::AdminRole;
139 use core::ops::{Deref, DerefMut};
140 use hashbrown::HashMap;
141 use serde::{Deserialize, Serialize};
142 /* use super::v2 as next; */
143
144 /// Important: even if the role we are storing here appears to be identical
145 /// to one used in another versioned store (like admin::Role), we *must*
146 /// have our own versioned copy! This ensures that if there's an update
147 /// to the role somewhere else, the conversion function between them
148 /// will break, letting people make an intelligent decision.
149 ///
150 /// In particular, *never remove variants from this enum* (or any other enum
151 /// in a versioned settings file) without bumping the version and
152 /// writing a migration that understands how to properly deal with
153 /// existing instances of the old variant (you can delete From instances
154 /// for the old variants at this point). Otherwise, we will lose
155 /// compatibility with old settings files, since we won't be able to
156 /// deserialize them!
157 #[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
158 pub enum Role {
159 Moderator = 0,
160 Admin = 1,
161 }
162
163 impl From<AdminRole> for Role {
164 fn from(value: AdminRole) -> Self {
165 match value {
166 AdminRole::Moderator => Self::Moderator,
167 AdminRole::Admin => Self::Admin,
168 }
169 }
170 }
171
172 impl From<Role> for AdminRole {
173 fn from(value: Role) -> Self {
174 match value {
175 Role::Moderator => Self::Moderator,
176 Role::Admin => Self::Admin,
177 }
178 }
179 }
180
181 #[derive(Clone, Deserialize, Serialize)]
182 /// NOTE: May not be present if performed from the command line or from a
183 /// legacy file.
184 pub struct WhitelistInfo {
185 pub username_when_whitelisted: String,
186 pub whitelisted_by: Uuid,
187 /// NOTE: May not be up to date, if we allow username changes.
188 pub whitelisted_by_username: String,
189 /// NOTE: Role of the whitelisting user at the time of the ban.
190 pub whitelisted_by_role: Role,
191 }
192
193 #[derive(Clone, Deserialize, Serialize)]
194 pub struct WhitelistRecord {
195 /// Date when the user was added to the whitelist.
196 pub date: DateTime<Utc>,
197 /// NOTE: Should only be None for migrations from legacy data.
198 pub info: Option<WhitelistInfo>,
199 }
200
201 impl WhitelistRecord {
202 pub fn whitelisted_by_role(&self) -> Role {
203 self.info.as_ref().map(|info| info.whitelisted_by_role)
204 // We know all legacy bans were performed by an admin, since we had no other roles
205 // at the time.
206 .unwrap_or(Role::Admin)
207 }
208 }
209
210 #[derive(Clone, Deserialize, Serialize, Default)]
211 #[serde(transparent)]
212 pub struct Whitelist(pub(super) HashMap<Uuid, WhitelistRecord>);
213
214 impl Deref for Whitelist {
215 type Target = HashMap<Uuid, WhitelistRecord>;
216
217 fn deref(&self) -> &Self::Target { &self.0 }
218 }
219
220 impl DerefMut for Whitelist {
221 fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 }
222 }
223
224 impl Whitelist {
225 /// One-off migration from the previous version. This must be
226 /// guaranteed to produce a valid settings file as long as it is
227 /// called with a valid settings file from the previous version.
228 pub(super) fn migrate(prev: prev::Whitelist) -> Self {
229 // The whitelist start date for migrations from legacy is the current one; we
230 // could record that they actually have an unknown start date, but
231 // this would just complicate the format.
232 let date = Utc::now();
233 // We don't have any of the information we need for the whitelist for legacy
234 // records.
235 Whitelist(
236 prev.0
237 .into_iter()
238 .map(|uid| {
239 (uid, WhitelistRecord {
240 date,
241 // We have none of the information needed for `WhitelistInfo` for old
242 // whitelist records.
243 info: None,
244 })
245 })
246 .collect(),
247 )
248 }
249
250 /// Perform any needed validation on this whitelist that can't be done
251 /// using parsing.
252 ///
253 /// The returned version being "Old" indicates the loaded setting has
254 /// been modified during validation (this is why validate takes
255 /// `&mut self`).
256 pub(super) fn validate(&mut self) -> Result<Version, <Final as EditableSetting>::Error> {
257 Ok(Version::Latest)
258 }
259 }
260
261 // NOTE: Whenever there is a version upgrade, copy this note as well as the
262 // commented-out code below to the next version, then uncomment the code
263 // for this version.
264 /* impl TryFrom<Whitelist> for Final {
265 type Error = <Final as EditableSetting>::Error;
266
267 fn try_from(mut value: Whitelist) -> Result<Final, Self::Error> {
268 value.validate()?;
269 Ok(next::Whitelist::migrate(value).try_into().expect(MIGRATION_UPGRADE_GUARANTEE))
270 }
271 } */
272}