Skip to main content

steel_core/server/
permissions.rs

1use steel_utils::Identifier;
2
3use crate::permission::PermissionMetadataValue;
4
5use super::{
6    Arc, FnServerJob, OP_GROUP, PermissionGroupManager, PermissionGroupManagerError,
7    PermissionGroupUpdateError, PermissionGroupsConfig, PermissionSet, PermissionSubjectState,
8    Player, PlayerPermissionUpdateError, Server, ServerJobContext, Uuid,
9};
10
11const BYPASSES_PLAYER_LIMIT: Identifier =
12    Identifier::new_static(Identifier::STEEL_NAMESPACE, "bypasses_player_limit");
13
14pub(super) fn validate_player_permission_group_update<E>(
15    manager: &PermissionGroupManager,
16    previous_groups: &[String],
17    updated_groups: &[String],
18) -> Result<(), PlayerPermissionUpdateError<E>> {
19    for group in updated_groups {
20        let already_assigned = previous_groups.iter().any(|current| current == group);
21        if !already_assigned && !manager.contains_group(group) {
22            return Err(PlayerPermissionUpdateError::UnknownGroup(group.clone()));
23        }
24    }
25    Ok(())
26}
27
28impl Server {
29    /// Resolves vanilla's `bypassesPlayerLimit` flag from global boolean metadata
30    /// `steel:bypasses_player_limit`, including inherited groups. Operator membership
31    /// alone does not grant it.
32    #[must_use]
33    pub(crate) fn can_bypass_player_limit(&self, uuid: Uuid) -> bool {
34        let subject = self.player_permission_state(uuid).unwrap_or_default();
35        self.permission_groups
36            .effective_metadata(subject.groups(), subject.metadata_overrides())
37            .resolve(&BYPASSES_PLAYER_LIMIT)
38            .and_then(PermissionMetadataValue::as_bool)
39            == Some(true)
40    }
41
42    pub(super) fn apply_cached_or_default_permission_state(&self, player: &Player) -> u64 {
43        let state = self
44            .player_permission_states
45            .read()
46            .get(player.gameprofile.id)
47            .cloned()
48            .unwrap_or_default();
49        self.apply_player_permission_state(player, state)
50    }
51
52    fn apply_player_permission_state(&self, player: &Player, state: PermissionSubjectState) -> u64 {
53        let (groups, overrides, metadata_overrides) = state.into_parts();
54        for group in &groups {
55            if !self.permission_groups.contains_group(group) {
56                log::warn!(
57                    "Player {} has unknown permission group {group}",
58                    player.gameprofile.name
59                );
60            }
61        }
62        let effective = self
63            .permission_groups
64            .effective_permissions(&groups, &overrides);
65        let effective_metadata = self
66            .permission_groups
67            .effective_metadata(&groups, &metadata_overrides);
68        player.set_permission_state(
69            groups,
70            overrides,
71            metadata_overrides,
72            effective,
73            effective_metadata,
74        )
75    }
76
77    /// Returns one player's cached persisted permission state.
78    #[must_use]
79    pub fn player_permission_state(&self, uuid: Uuid) -> Option<PermissionSubjectState> {
80        self.player_permission_states.read().get(uuid).cloned()
81    }
82
83    /// Returns whether the latest published subject state assigns the operator group.
84    #[must_use]
85    pub(crate) fn is_operator(&self, uuid: Uuid) -> bool {
86        self.player_permission_states
87            .read()
88            .get(uuid)
89            .is_some_and(|state| state.groups().iter().any(|group| group == OP_GROUP))
90    }
91
92    /// Captures effective command permissions from the latest published subject and group state.
93    #[must_use]
94    pub(crate) fn command_permission_snapshot(&self, uuid: Uuid) -> PermissionSet {
95        let subject = self.player_permission_state(uuid).unwrap_or_default();
96        self.permission_groups
97            .effective_permissions(subject.groups(), subject.overrides())
98    }
99
100    /// Atomically edits one player's persisted permission state.
101    ///
102    /// Persistence completes before the cache is published. An online player is
103    /// refreshed from the latest cached snapshot at the server job tick stage.
104    ///
105    /// # Errors
106    ///
107    /// Returns an edit error, an unknown newly assigned group, or a storage error.
108    pub async fn try_update_player_permissions<T, E>(
109        self: &Arc<Self>,
110        uuid: Uuid,
111        update: impl FnOnce(PermissionSubjectState) -> Result<(PermissionSubjectState, T), E> + Send,
112    ) -> Result<(PermissionSubjectState, T), PlayerPermissionUpdateError<E>>
113    where
114        T: Send,
115        E: Send,
116    {
117        let _guard = self.player_permission_updates.lock().await;
118        let mut states = self.player_permission_states.read().clone();
119        let current = states.get(uuid).cloned().unwrap_or_default();
120        let previous_groups = current.groups().to_vec();
121        let (updated, result) = update(current).map_err(PlayerPermissionUpdateError::Edit)?;
122        validate_player_permission_group_update(
123            &self.permission_groups,
124            &previous_groups,
125            updated.groups(),
126        )?;
127
128        if updated.is_empty() {
129            states.remove(uuid);
130        } else {
131            states.set(uuid, updated.clone());
132        }
133        self.player_data_storage
134            .save_permission_subjects(&states)
135            .await?;
136
137        *self.player_permission_states.write() = states;
138        self.queue_player_permission_refresh(uuid);
139        Ok((updated, result))
140    }
141
142    /// Replaces the complete permission group config and refreshes online players.
143    ///
144    /// # Errors
145    ///
146    /// Returns an error when validation or persistence fails.
147    pub async fn replace_permission_groups(
148        self: &Arc<Self>,
149        config: PermissionGroupsConfig,
150    ) -> Result<(), PermissionGroupManagerError> {
151        self.permission_groups.replace_config(config).await?;
152        self.queue_online_permission_group_refresh();
153        Ok(())
154    }
155
156    /// Edits the latest permission group config and refreshes online players.
157    ///
158    /// # Errors
159    ///
160    /// Returns an error when validation or persistence fails.
161    pub async fn update_permission_groups(
162        self: &Arc<Self>,
163        update: impl FnOnce(&mut PermissionGroupsConfig) + Send,
164    ) -> Result<(), PermissionGroupManagerError> {
165        self.permission_groups.update_config(update).await?;
166        self.queue_online_permission_group_refresh();
167        Ok(())
168    }
169
170    /// Applies a fallible permission group edit and refreshes online players.
171    ///
172    /// # Errors
173    ///
174    /// Returns the caller edit error or a validation/persistence error.
175    pub async fn try_update_permission_groups<T, E>(
176        self: &Arc<Self>,
177        update: impl FnOnce(&mut PermissionGroupsConfig) -> Result<T, E> + Send,
178    ) -> Result<T, PermissionGroupUpdateError<E>>
179    where
180        T: Send,
181        E: Send,
182    {
183        let result = self.permission_groups.try_update_config(update).await?;
184        self.queue_online_permission_group_refresh();
185        Ok(result)
186    }
187
188    fn queue_player_permission_refresh(self: &Arc<Self>, uuid: Uuid) {
189        self.jobs
190            .spawn(FnServerJob::new(move |context: &mut ServerJobContext| {
191                if let Some(server) = context.server() {
192                    server.refresh_player_permission_state(uuid);
193                }
194            }));
195    }
196
197    pub(crate) fn refresh_player_permission_state(self: &Arc<Self>, uuid: Uuid) {
198        let Some(player) = self.online_players.get_by_uuid(&uuid) else {
199            return;
200        };
201        let state = self.player_permission_state(uuid).unwrap_or_default();
202        self.apply_player_permission_state(&player, state);
203        self.resend_player_permission_context(&player);
204    }
205
206    fn queue_online_permission_group_refresh(self: &Arc<Self>) {
207        self.jobs
208            .spawn(FnServerJob::new(|context: &mut ServerJobContext| {
209                if let Some(server) = context.server() {
210                    server.refresh_online_permission_groups();
211                }
212            }));
213    }
214
215    fn refresh_online_permission_groups(self: &Arc<Self>) {
216        for player in self.get_players() {
217            let state = self
218                .player_permission_state(player.gameprofile.id)
219                .unwrap_or_default();
220            self.apply_player_permission_state(&player, state);
221            self.resend_player_permission_context(&player);
222        }
223    }
224}