Skip to main content

steel_core/player/player_data_storage/
permissions.rs

1use std::collections::BTreeMap;
2
3use serde::{Deserialize, Serialize};
4use tokio::io;
5use toml::ser::Error as TomlSerializeError;
6use uuid::Uuid;
7
8use crate::permission::{
9    PermissionEntry, PermissionMetadataEntry, PermissionMetadataExpression, PermissionMetadataSet,
10    PermissionMetadataValue, PermissionRuleExpression, PermissionSegment, PermissionSet,
11    PermissionState, PermissionSubjectIndex, PermissionSubjectState,
12};
13
14#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
15#[serde(default, deny_unknown_fields)]
16pub(super) struct PlayerPermissionsFile {
17    pub(super) players: BTreeMap<String, PlayerPermissionEntryFile>,
18}
19
20#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
21#[serde(default, deny_unknown_fields)]
22pub(super) struct PlayerPermissionEntryFile {
23    pub(super) groups: Vec<String>,
24    pub(super) allow: Vec<String>,
25    pub(super) deny: Vec<String>,
26    pub(super) metadata: Vec<PlayerPermissionMetadataEntryFile>,
27}
28
29#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
30#[serde(deny_unknown_fields)]
31pub(super) struct PlayerPermissionMetadataEntryFile {
32    pub(super) key: String,
33    pub(super) value: PermissionMetadataValue,
34}
35
36impl PlayerPermissionsFile {
37    pub(super) fn from_subject_index(subjects: &PermissionSubjectIndex) -> Self {
38        let mut file = Self::default();
39        for (uuid, state) in subjects.entries() {
40            set_permission_subject(&mut file, uuid, state);
41        }
42        file
43    }
44
45    pub(super) fn validate(&self) -> io::Result<()> {
46        for (uuid, entry) in &self.players {
47            let uuid = parse_uuid(uuid)?;
48            entry.validate(uuid)?;
49        }
50        Ok(())
51    }
52
53    pub(super) fn into_subject_index(self) -> io::Result<PermissionSubjectIndex> {
54        let mut subjects = PermissionSubjectIndex::new();
55        for (uuid_text, entry) in self.players {
56            let uuid = parse_uuid(&uuid_text)?;
57            if subjects.get(uuid).is_some() {
58                return Err(io::Error::new(
59                    io::ErrorKind::InvalidData,
60                    format!("duplicate player permission UUID '{uuid_text}' resolves to {uuid}"),
61                ));
62            }
63            subjects.set(uuid, entry.into_subject_state(uuid)?);
64        }
65        Ok(subjects)
66    }
67}
68
69impl PlayerPermissionEntryFile {
70    fn validate(&self, uuid: Uuid) -> io::Result<()> {
71        validate_groups(uuid, &self.groups)?;
72        for expression in &self.allow {
73            parse_permission_expression(uuid, expression, "allow")?;
74        }
75        for expression in &self.deny {
76            parse_permission_expression(uuid, expression, "deny")?;
77        }
78        for entry in &self.metadata {
79            parse_metadata_expression(uuid, &entry.key)?;
80        }
81        Ok(())
82    }
83
84    fn from_subject_state(state: &PermissionSubjectState) -> Self {
85        let mut allow = Vec::new();
86        let mut deny = Vec::new();
87        for entry in state.overrides().entries() {
88            let expression =
89                PermissionRuleExpression::new(entry.key().clone(), entry.context().clone())
90                    .to_string();
91            match entry.state() {
92                PermissionState::Allow => allow.push(expression),
93                PermissionState::Deny => deny.push(expression),
94            }
95        }
96        Self {
97            groups: state.groups().to_vec(),
98            allow,
99            deny,
100            metadata: state
101                .metadata_overrides()
102                .entries()
103                .iter()
104                .map(|entry| PlayerPermissionMetadataEntryFile {
105                    key: PermissionMetadataExpression::new(
106                        entry.key().clone(),
107                        entry.context().clone(),
108                    )
109                    .to_string(),
110                    value: entry.value().clone(),
111                })
112                .collect(),
113        }
114    }
115
116    fn into_subject_state(self, uuid: Uuid) -> io::Result<PermissionSubjectState> {
117        validate_groups(uuid, &self.groups)?;
118        let mut overrides = PermissionSet::new();
119        for expression in self.allow {
120            let expression = parse_permission_expression(uuid, &expression, "allow")?;
121            let (key, context) = expression.into_parts();
122            overrides.push(PermissionEntry::allow_with_context(key, context));
123        }
124        for expression in self.deny {
125            let expression = parse_permission_expression(uuid, &expression, "deny")?;
126            let (key, context) = expression.into_parts();
127            overrides.push(PermissionEntry::deny_with_context(key, context));
128        }
129        let mut metadata = PermissionMetadataSet::new();
130        for entry in self.metadata {
131            let expression = parse_metadata_expression(uuid, &entry.key)?;
132            let (key, context) = expression.into_parts();
133            metadata.push(PermissionMetadataEntry::new_with_context(
134                key,
135                context,
136                entry.value,
137            ));
138        }
139        Ok(PermissionSubjectState::new_with_metadata(
140            self.groups,
141            overrides,
142            metadata,
143        ))
144    }
145}
146
147fn parse_uuid(value: &str) -> io::Result<Uuid> {
148    Uuid::parse_str(value).map_err(|error| {
149        io::Error::new(
150            io::ErrorKind::InvalidData,
151            format!("invalid player permission UUID '{value}': {error}"),
152        )
153    })
154}
155
156fn validate_groups(uuid: Uuid, groups: &[String]) -> io::Result<()> {
157    for group in groups {
158        PermissionSegment::parse(group.as_str()).map_err(|error| {
159            io::Error::new(
160                io::ErrorKind::InvalidData,
161                format!("invalid permission group '{group}' for {uuid}: {error}"),
162            )
163        })?;
164    }
165    Ok(())
166}
167
168fn parse_permission_expression(
169    uuid: Uuid,
170    expression: &str,
171    state: &str,
172) -> io::Result<PermissionRuleExpression> {
173    PermissionRuleExpression::parse(expression).map_err(|error| {
174        io::Error::new(
175            io::ErrorKind::InvalidData,
176            format!("invalid {state} permission expression for {uuid}: {error}"),
177        )
178    })
179}
180
181fn parse_metadata_expression(
182    uuid: Uuid,
183    expression: &str,
184) -> io::Result<PermissionMetadataExpression> {
185    PermissionMetadataExpression::parse(expression).map_err(|error| {
186        io::Error::new(
187            io::ErrorKind::InvalidData,
188            format!("invalid permission metadata expression for {uuid}: {error}"),
189        )
190    })
191}
192
193pub(super) fn set_permission_subject(
194    file: &mut PlayerPermissionsFile,
195    uuid: Uuid,
196    state: &PermissionSubjectState,
197) {
198    if state.is_empty() {
199        file.players.remove(&uuid.to_string());
200        return;
201    }
202    file.players.insert(
203        uuid.to_string(),
204        PlayerPermissionEntryFile::from_subject_state(state),
205    );
206}
207
208pub(super) fn serialize_player_permissions_file(
209    file: &PlayerPermissionsFile,
210) -> Result<String, TomlSerializeError> {
211    let mut output = String::new();
212    if file.players.is_empty() {
213        output.push_str("players = {}\n");
214        return Ok(output);
215    }
216
217    for (uuid, entry) in &file.players {
218        output.push_str("[players.");
219        output.push_str(&toml_value(uuid)?);
220        output.push_str("]\n");
221        push_player_permission_entry(&mut output, entry)?;
222        output.push('\n');
223    }
224    Ok(output)
225}
226
227fn push_player_permission_entry(
228    output: &mut String,
229    entry: &PlayerPermissionEntryFile,
230) -> Result<(), TomlSerializeError> {
231    push_string_array_field(output, "groups", &entry.groups)?;
232    push_string_array_field(output, "allow", &entry.allow)?;
233    push_string_array_field(output, "deny", &entry.deny)?;
234    push_permission_metadata_entries(output, &entry.metadata)
235}
236
237fn push_string_array_field(
238    output: &mut String,
239    key: &str,
240    values: &[String],
241) -> Result<(), TomlSerializeError> {
242    if values.is_empty() {
243        output.push_str(key);
244        output.push_str(" = []\n");
245        return Ok(());
246    }
247
248    output.push_str(key);
249    output.push_str(" = [\n");
250    for value in values {
251        output.push_str("    ");
252        output.push_str(&toml_value(value)?);
253        output.push_str(",\n");
254    }
255    output.push_str("]\n");
256    Ok(())
257}
258
259fn push_permission_metadata_entries(
260    output: &mut String,
261    metadata: &[PlayerPermissionMetadataEntryFile],
262) -> Result<(), TomlSerializeError> {
263    if metadata.is_empty() {
264        output.push_str("metadata = []\n");
265        return Ok(());
266    }
267
268    output.push_str("metadata = [\n");
269    for entry in metadata {
270        output.push_str("    { key = ");
271        output.push_str(&toml_value(&entry.key)?);
272        output.push_str(", value = ");
273        output.push_str(&permission_metadata_value_toml(&entry.value)?);
274        output.push_str(" },\n");
275    }
276    output.push_str("]\n");
277    Ok(())
278}
279
280fn toml_value<T: Serialize + ?Sized>(value: &T) -> Result<String, TomlSerializeError> {
281    #[derive(Serialize)]
282    struct Field<'a, T: Serialize + ?Sized> {
283        value: &'a T,
284    }
285
286    let serialized = toml::to_string(&Field { value })?;
287    let serialized = serialized.trim_end();
288    Ok(serialized
289        .strip_prefix("value = ")
290        .unwrap_or(serialized)
291        .to_owned())
292}
293
294fn permission_metadata_value_toml(
295    value: &PermissionMetadataValue,
296) -> Result<String, TomlSerializeError> {
297    match value {
298        PermissionMetadataValue::Bool(value) => toml_value(value),
299        PermissionMetadataValue::Integer(value) => toml_value(value),
300        PermissionMetadataValue::String(value) => toml_value(value),
301    }
302}
303
304#[cfg(test)]
305mod tests {
306    use super::*;
307    use crate::permission::{
308        PermissionKey, PermissionMetadataValue, PermissionRuleContext,
309        parse_permission_metadata_key,
310    };
311
312    fn key(value: &str) -> PermissionKey {
313        match PermissionKey::parse(value) {
314            Ok(key) => key,
315            Err(error) => panic!("test permission key should parse: {error}"),
316        }
317    }
318
319    #[test]
320    fn subject_file_round_trip_preserves_groups_and_contextual_rules() {
321        let uuid = Uuid::from_u128(1);
322        let mut overrides = PermissionSet::new();
323        overrides.allow(key("steel.fly"));
324        overrides.deny_in(
325            key("steel.build"),
326            PermissionRuleContext::domain("survival")
327                .unwrap_or_else(|error| panic!("test domain should parse: {error}")),
328        );
329        let metadata_key = match parse_permission_metadata_key("plugin:max_homes") {
330            Ok(key) => key,
331            Err(error) => panic!("test metadata key should parse: {error}"),
332        };
333        let mut metadata = PermissionMetadataSet::new();
334        metadata.set_in(
335            metadata_key,
336            PermissionRuleContext::domain("survival")
337                .unwrap_or_else(|error| panic!("test domain should parse: {error}")),
338            PermissionMetadataValue::Integer(5),
339        );
340        let state = PermissionSubjectState::new_with_metadata(
341            vec!["retired_group".to_owned()],
342            overrides,
343            metadata,
344        );
345        let mut file = PlayerPermissionsFile::default();
346        set_permission_subject(&mut file, uuid, &state);
347
348        let serialized = match serialize_player_permissions_file(&file) {
349            Ok(serialized) => serialized,
350            Err(error) => panic!("subject file should serialize: {error}"),
351        };
352
353        assert!(serialized.contains("{ key = \"plugin:max_homes{domain=survival}\", value = 5 }"));
354        let parsed = match toml::from_str::<PlayerPermissionsFile>(&serialized) {
355            Ok(parsed) => parsed,
356            Err(error) => panic!("subject file should parse: {error}"),
357        };
358        let parsed = match parsed.into_subject_index() {
359            Ok(parsed) => parsed,
360            Err(error) => panic!("subjects should validate: {error}"),
361        };
362        let Some(parsed) = parsed.get(uuid) else {
363            panic!("subject should exist");
364        };
365
366        assert_eq!(parsed, &state);
367    }
368
369    #[test]
370    fn subject_file_rejects_invalid_group_names() {
371        let uuid = Uuid::from_u128(2);
372        let mut file = PlayerPermissionsFile::default();
373        file.players.insert(
374            uuid.to_string(),
375            PlayerPermissionEntryFile {
376                groups: vec!["Admin Group".to_owned()],
377                ..PlayerPermissionEntryFile::default()
378            },
379        );
380
381        let error = file.validate();
382        assert!(error.is_err_and(|error| {
383            error
384                .to_string()
385                .contains("invalid permission group 'Admin Group'")
386        }));
387    }
388
389    #[test]
390    fn empty_subject_state_removes_the_file_entry() {
391        let uuid = Uuid::from_u128(3);
392        let mut file = PlayerPermissionsFile::default();
393        file.players
394            .insert(uuid.to_string(), PlayerPermissionEntryFile::default());
395
396        set_permission_subject(&mut file, uuid, &PermissionSubjectState::default());
397
398        assert!(file.players.is_empty());
399    }
400
401    #[test]
402    fn subject_file_rejects_duplicate_uuid_spellings() {
403        let uuid = Uuid::from_u128(4);
404        let mut file = PlayerPermissionsFile::default();
405        file.players.insert(
406            uuid.to_string(),
407            PlayerPermissionEntryFile {
408                groups: vec!["op".to_owned()],
409                ..PlayerPermissionEntryFile::default()
410            },
411        );
412        file.players.insert(
413            uuid.simple().to_string(),
414            PlayerPermissionEntryFile {
415                groups: vec!["builder".to_owned()],
416                ..PlayerPermissionEntryFile::default()
417            },
418        );
419
420        let error = file.into_subject_index();
421
422        assert!(error.is_err_and(|error| {
423            error
424                .to_string()
425                .contains("duplicate player permission UUID")
426        }));
427    }
428}