Skip to main content

steel_core/command/builtins/perms/
mod.rs

1//! Steel permission administration under `/perms`.
2
3mod config;
4
5use std::{convert::Infallible, fmt};
6
7use super::super::{
8    brigadier::{ArgumentType, CommandNodeBuilder, CommandSyntaxError},
9    execution::{
10        CommandPermissionSource, CommandResultSuspension, CommandResultSuspensionPoll,
11        CommandSource, CommandSuspensionOrder, GameProfileArgument, SteelArgumentType,
12        SteelCommandContext, SteelCommandRuntime, argument, literal,
13    },
14    registration::CommandRegistration,
15};
16use crate::command::missing_argument;
17use crate::permission::{
18    PermissionContext, PermissionEntry, PermissionExpr, PermissionKey, PermissionMetadataEntry,
19    PermissionMetadataExpression, PermissionMetadataValue, PermissionResolutionSource,
20    PermissionRuleExpression, PermissionState, PermissionSubjectState,
21};
22use steel_utils::Identifier;
23use text_components::TextComponent;
24use tokio::{sync::oneshot, task::JoinHandle};
25
26pub(super) const MANAGE_ALL_PERMISSION: &str = "steel.permission.manage.*";
27pub(super) const GROUP_ALL_PERMISSION: &str = "steel.permission.group.*";
28pub(super) const METADATA_PERMISSION: &str = "steel.permission.metadata";
29
30pub(super) fn registration() -> CommandRegistration<CommandSource> {
31    CommandRegistration::new(Identifier::from_steel("perms"), |_| command())
32        .subcommand_permission(["user", "info"])
33        .subcommand_permission(["user", "allow"])
34        .subcommand_permission(["user", "deny"])
35        .subcommand_permission(["user", "unset"])
36        .subcommand_permission(["user", "check"])
37        .subcommand_permission(["user", "metadata", "set"])
38        .subcommand_permission(["user", "metadata", "check"])
39        .subcommand_permission(["user", "metadata", "unset"])
40        .subcommand_permission(["user", "group", "add"])
41        .subcommand_permission(["user", "group", "remove"])
42        .subcommand_permission(["group", "create"])
43        .subcommand_permission(["group", "info"])
44        .subcommand_permission(["group", "delete"])
45        .subcommand_permission(["group", "allow"])
46        .subcommand_permission(["group", "deny"])
47        .subcommand_permission(["group", "unset"])
48        .subcommand_permission(["group", "priority"])
49        .subcommand_permission(["group", "inherit", "list"])
50        .subcommand_permission(["group", "inherit", "add"])
51        .subcommand_permission(["group", "inherit", "remove"])
52        .subcommand_permission(["group", "metadata", "set"])
53        .subcommand_permission(["group", "metadata", "unset"])
54        .subcommand_permission(["groups", "list"])
55        .subcommand_permission(["groups", "default", "add"])
56        .subcommand_permission(["groups", "default", "remove"])
57}
58
59fn command() -> CommandNodeBuilder<CommandSource, SteelCommandRuntime> {
60    literal("perms")
61        .then(user_command())
62        .then(group_command())
63        .then(groups_command())
64}
65
66fn user_command() -> CommandNodeBuilder<CommandSource, SteelCommandRuntime> {
67    literal("user").then(
68        argument("targets", SteelArgumentType::game_profile())
69            .then(literal("info").executes_suspended(user_info))
70            .then(
71                literal("allow").then(
72                    argument("permission", SteelArgumentType::permission_rule())
73                        .executes_suspended(user_allow),
74                ),
75            )
76            .then(
77                literal("deny").then(
78                    argument("permission", SteelArgumentType::permission_rule())
79                        .executes_suspended(user_deny),
80                ),
81            )
82            .then(
83                literal("unset").then(
84                    argument("permission", SteelArgumentType::user_permission_rule())
85                        .executes_suspended(user_unset),
86                ),
87            )
88            .then(
89                literal("check").then(
90                    argument("permission", SteelArgumentType::permission_rule())
91                        .executes_suspended(user_check),
92                ),
93            )
94            .then(user_metadata_command())
95            .then(
96                literal("group")
97                    .then(
98                        literal("add").then(
99                            argument("group", SteelArgumentType::permission_group(true))
100                                .executes_suspended(user_group_add),
101                        ),
102                    )
103                    .then(
104                        literal("remove").then(
105                            argument("group", SteelArgumentType::permission_group(true))
106                                .executes_suspended(user_group_remove),
107                        ),
108                    ),
109            ),
110    )
111}
112
113fn user_metadata_command() -> CommandNodeBuilder<CommandSource, SteelCommandRuntime> {
114    literal("metadata")
115        .then(metadata_set_command(user_metadata_set))
116        .then(
117            literal("check").then(
118                argument("metadata", SteelArgumentType::permission_metadata())
119                    .executes_suspended(user_metadata_check),
120            ),
121        )
122        .then(
123            literal("unset").then(
124                argument("metadata", SteelArgumentType::user_permission_metadata())
125                    .executes_suspended(user_metadata_unset),
126            ),
127        )
128}
129
130fn group_command() -> CommandNodeBuilder<CommandSource, SteelCommandRuntime> {
131    literal("group").then(
132        argument("group", SteelArgumentType::permission_group(false))
133            .then(literal("create").executes_suspended(group_create))
134            .then(literal("info").executes_suspended(group_info))
135            .then(literal("delete").executes_suspended(group_delete))
136            .then(
137                literal("allow").then(
138                    argument("permission", SteelArgumentType::permission_rule())
139                        .executes_suspended(group_allow),
140                ),
141            )
142            .then(
143                literal("deny").then(
144                    argument("permission", SteelArgumentType::permission_rule())
145                        .executes_suspended(group_deny),
146                ),
147            )
148            .then(
149                literal("unset").then(
150                    argument("permission", SteelArgumentType::group_permission_rule())
151                        .executes_suspended(group_unset),
152                ),
153            )
154            .then(
155                literal("priority").then(
156                    argument("priority", ArgumentType::integer(i32::MIN, i32::MAX))
157                        .executes_suspended(group_priority),
158                ),
159            )
160            .then(
161                literal("inherit")
162                    .then(literal("list").executes_suspended(group_inherit_list))
163                    .then(
164                        literal("add").then(
165                            argument("parent", SteelArgumentType::permission_group(true))
166                                .executes_suspended(group_inherit_add),
167                        ),
168                    )
169                    .then(
170                        literal("remove").then(
171                            argument("parent", SteelArgumentType::permission_group(true))
172                                .executes_suspended(group_inherit_remove),
173                        ),
174                    ),
175            )
176            .then(
177                literal("metadata")
178                    .then(metadata_set_command(group_metadata_set))
179                    .then(
180                        literal("unset").then(
181                            argument("metadata", SteelArgumentType::group_permission_metadata())
182                                .executes_suspended(group_metadata_unset),
183                        ),
184                    ),
185            ),
186    )
187}
188
189fn groups_command() -> CommandNodeBuilder<CommandSource, SteelCommandRuntime> {
190    literal("groups")
191        .then(literal("list").executes_suspended(groups_list))
192        .then(
193            literal("default")
194                .then(
195                    literal("add").then(
196                        argument("group", SteelArgumentType::permission_group(true))
197                            .executes_suspended(default_group_add),
198                    ),
199                )
200                .then(
201                    literal("remove").then(
202                        argument("group", SteelArgumentType::permission_group(true))
203                            .executes_suspended(default_group_remove),
204                    ),
205                ),
206        )
207}
208
209fn metadata_set_command(
210    executor: fn(
211        &SteelCommandContext<CommandSource>,
212    ) -> Result<PermsCommandSuspension, CommandSyntaxError>,
213) -> CommandNodeBuilder<CommandSource, SteelCommandRuntime> {
214    literal("set")
215        .then(
216            literal("int").then(
217                argument("metadata_int_value", ArgumentType::long(i64::MIN, i64::MAX)).then(
218                    argument("metadata", SteelArgumentType::permission_metadata())
219                        .executes_suspended(executor),
220                ),
221            ),
222        )
223        .then(
224            literal("bool").then(
225                argument("metadata_bool_value", ArgumentType::bool()).then(
226                    argument("metadata", SteelArgumentType::permission_metadata())
227                        .executes_suspended(executor),
228                ),
229            ),
230        )
231        .then(
232            literal("string").then(
233                argument("metadata_string_value", ArgumentType::string()).then(
234                    argument("metadata", SteelArgumentType::permission_metadata())
235                        .executes_suspended(executor),
236                ),
237            ),
238        )
239}
240
241#[derive(Clone)]
242enum Operation {
243    UserInfo(GameProfileArgument),
244    UserPermission {
245        targets: GameProfileArgument,
246        expression: PermissionRuleExpression,
247        state: Option<PermissionState>,
248    },
249    UserCheck {
250        targets: GameProfileArgument,
251        expression: PermissionRuleExpression,
252    },
253    UserMetadata {
254        targets: GameProfileArgument,
255        expression: PermissionMetadataExpression,
256        value: Option<PermissionMetadataValue>,
257    },
258    UserMetadataCheck {
259        targets: GameProfileArgument,
260        expression: PermissionMetadataExpression,
261    },
262    UserGroup {
263        targets: GameProfileArgument,
264        group: String,
265        add: bool,
266    },
267    GroupInfo(String),
268    GroupCreate(String),
269    GroupDelete(String),
270    GroupPermission {
271        group: String,
272        expression: PermissionRuleExpression,
273        state: Option<PermissionState>,
274    },
275    GroupPriority {
276        group: String,
277        priority: i32,
278    },
279    GroupInheritanceList(String),
280    GroupInheritance {
281        group: String,
282        parent: String,
283        add: bool,
284    },
285    GroupMetadata {
286        group: String,
287        expression: PermissionMetadataExpression,
288        value: Option<PermissionMetadataValue>,
289    },
290    GroupsList,
291    DefaultGroup {
292        group: String,
293        add: bool,
294    },
295}
296
297impl Operation {
298    const fn suspension_order(&self) -> CommandSuspensionOrder {
299        match self {
300            Self::UserInfo(_)
301            | Self::UserCheck { .. }
302            | Self::UserMetadataCheck { .. }
303            | Self::GroupInfo(_)
304            | Self::GroupInheritanceList(_)
305            | Self::GroupsList => CommandSuspensionOrder::Source,
306            Self::UserPermission { .. }
307            | Self::UserMetadata { .. }
308            | Self::UserGroup { .. }
309            | Self::GroupCreate(_)
310            | Self::GroupDelete(_)
311            | Self::GroupPermission { .. }
312            | Self::GroupPriority { .. }
313            | Self::GroupInheritance { .. }
314            | Self::GroupMetadata { .. }
315            | Self::DefaultGroup { .. } => CommandSuspensionOrder::Global,
316        }
317    }
318}
319
320struct OperationResult {
321    result: i32,
322    messages: Vec<TextComponent>,
323}
324
325fn user_info(
326    context: &SteelCommandContext<CommandSource>,
327) -> Result<PermsCommandSuspension, CommandSyntaxError> {
328    start(context, Operation::UserInfo(targets(context)?))
329}
330
331fn user_allow(
332    context: &SteelCommandContext<CommandSource>,
333) -> Result<PermsCommandSuspension, CommandSyntaxError> {
334    user_permission(context, Some(PermissionState::Allow))
335}
336
337fn user_deny(
338    context: &SteelCommandContext<CommandSource>,
339) -> Result<PermsCommandSuspension, CommandSyntaxError> {
340    user_permission(context, Some(PermissionState::Deny))
341}
342
343fn user_unset(
344    context: &SteelCommandContext<CommandSource>,
345) -> Result<PermsCommandSuspension, CommandSyntaxError> {
346    user_permission(context, None)
347}
348
349fn user_permission(
350    context: &SteelCommandContext<CommandSource>,
351    state: Option<PermissionState>,
352) -> Result<PermsCommandSuspension, CommandSyntaxError> {
353    let expression = permission_expression(context)?;
354    require_permission_management(context.source(), expression.key())?;
355    start(
356        context,
357        Operation::UserPermission {
358            targets: targets(context)?,
359            expression,
360            state,
361        },
362    )
363}
364
365fn user_check(
366    context: &SteelCommandContext<CommandSource>,
367) -> Result<PermsCommandSuspension, CommandSyntaxError> {
368    let expression = permission_expression(context)?;
369    require_permission_management(context.source(), expression.key())?;
370    start(
371        context,
372        Operation::UserCheck {
373            targets: targets(context)?,
374            expression,
375        },
376    )
377}
378
379fn user_metadata_set(
380    context: &SteelCommandContext<CommandSource>,
381) -> Result<PermsCommandSuspension, CommandSyntaxError> {
382    user_metadata(context, Some(metadata_value(context)?))
383}
384
385fn user_metadata_unset(
386    context: &SteelCommandContext<CommandSource>,
387) -> Result<PermsCommandSuspension, CommandSyntaxError> {
388    user_metadata(context, None)
389}
390
391fn user_metadata(
392    context: &SteelCommandContext<CommandSource>,
393    value: Option<PermissionMetadataValue>,
394) -> Result<PermsCommandSuspension, CommandSyntaxError> {
395    require_metadata_management(context.source())?;
396    start(
397        context,
398        Operation::UserMetadata {
399            targets: targets(context)?,
400            expression: metadata_expression(context)?,
401            value,
402        },
403    )
404}
405
406fn user_metadata_check(
407    context: &SteelCommandContext<CommandSource>,
408) -> Result<PermsCommandSuspension, CommandSyntaxError> {
409    require_metadata_management(context.source())?;
410    start(
411        context,
412        Operation::UserMetadataCheck {
413            targets: targets(context)?,
414            expression: metadata_expression(context)?,
415        },
416    )
417}
418
419fn user_group_add(
420    context: &SteelCommandContext<CommandSource>,
421) -> Result<PermsCommandSuspension, CommandSyntaxError> {
422    user_group(context, true)
423}
424
425fn user_group_remove(
426    context: &SteelCommandContext<CommandSource>,
427) -> Result<PermsCommandSuspension, CommandSyntaxError> {
428    user_group(context, false)
429}
430
431fn user_group(
432    context: &SteelCommandContext<CommandSource>,
433    add: bool,
434) -> Result<PermsCommandSuspension, CommandSyntaxError> {
435    let group = group_argument(context, "group")?;
436    require_group_management(context.source(), &group)?;
437    start(
438        context,
439        Operation::UserGroup {
440            targets: targets(context)?,
441            group,
442            add,
443        },
444    )
445}
446
447fn group_info(
448    context: &SteelCommandContext<CommandSource>,
449) -> Result<PermsCommandSuspension, CommandSyntaxError> {
450    let group = managed_group(context)?;
451    start(context, Operation::GroupInfo(group))
452}
453
454fn group_create(
455    context: &SteelCommandContext<CommandSource>,
456) -> Result<PermsCommandSuspension, CommandSyntaxError> {
457    let group = managed_group(context)?;
458    start(context, Operation::GroupCreate(group))
459}
460
461fn group_delete(
462    context: &SteelCommandContext<CommandSource>,
463) -> Result<PermsCommandSuspension, CommandSyntaxError> {
464    let group = managed_group(context)?;
465    start(context, Operation::GroupDelete(group))
466}
467
468fn group_allow(
469    context: &SteelCommandContext<CommandSource>,
470) -> Result<PermsCommandSuspension, CommandSyntaxError> {
471    group_permission(context, Some(PermissionState::Allow))
472}
473
474fn group_deny(
475    context: &SteelCommandContext<CommandSource>,
476) -> Result<PermsCommandSuspension, CommandSyntaxError> {
477    group_permission(context, Some(PermissionState::Deny))
478}
479
480fn group_unset(
481    context: &SteelCommandContext<CommandSource>,
482) -> Result<PermsCommandSuspension, CommandSyntaxError> {
483    group_permission(context, None)
484}
485
486fn group_permission(
487    context: &SteelCommandContext<CommandSource>,
488    state: Option<PermissionState>,
489) -> Result<PermsCommandSuspension, CommandSyntaxError> {
490    let group = managed_group(context)?;
491    let expression = permission_expression(context)?;
492    require_permission_management(context.source(), expression.key())?;
493    start(
494        context,
495        Operation::GroupPermission {
496            group,
497            expression,
498            state,
499        },
500    )
501}
502
503fn group_priority(
504    context: &SteelCommandContext<CommandSource>,
505) -> Result<PermsCommandSuspension, CommandSyntaxError> {
506    let group = managed_group(context)?;
507    let priority = context.integer("priority")?;
508    start(context, Operation::GroupPriority { group, priority })
509}
510
511fn group_inherit_list(
512    context: &SteelCommandContext<CommandSource>,
513) -> Result<PermsCommandSuspension, CommandSyntaxError> {
514    let group = managed_group(context)?;
515    start(context, Operation::GroupInheritanceList(group))
516}
517
518fn group_inherit_add(
519    context: &SteelCommandContext<CommandSource>,
520) -> Result<PermsCommandSuspension, CommandSyntaxError> {
521    group_inheritance(context, true)
522}
523
524fn group_inherit_remove(
525    context: &SteelCommandContext<CommandSource>,
526) -> Result<PermsCommandSuspension, CommandSyntaxError> {
527    group_inheritance(context, false)
528}
529
530fn group_inheritance(
531    context: &SteelCommandContext<CommandSource>,
532    add: bool,
533) -> Result<PermsCommandSuspension, CommandSyntaxError> {
534    let group = managed_group(context)?;
535    let parent = group_argument(context, "parent")?;
536    require_group_management(context.source(), &parent)?;
537    start(context, Operation::GroupInheritance { group, parent, add })
538}
539
540fn group_metadata_set(
541    context: &SteelCommandContext<CommandSource>,
542) -> Result<PermsCommandSuspension, CommandSyntaxError> {
543    group_metadata(context, Some(metadata_value(context)?))
544}
545
546fn group_metadata_unset(
547    context: &SteelCommandContext<CommandSource>,
548) -> Result<PermsCommandSuspension, CommandSyntaxError> {
549    group_metadata(context, None)
550}
551
552fn group_metadata(
553    context: &SteelCommandContext<CommandSource>,
554    value: Option<PermissionMetadataValue>,
555) -> Result<PermsCommandSuspension, CommandSyntaxError> {
556    require_metadata_management(context.source())?;
557    let group = managed_group(context)?;
558    start(
559        context,
560        Operation::GroupMetadata {
561            group,
562            expression: metadata_expression(context)?,
563            value,
564        },
565    )
566}
567
568fn groups_list(
569    context: &SteelCommandContext<CommandSource>,
570) -> Result<PermsCommandSuspension, CommandSyntaxError> {
571    start(context, Operation::GroupsList)
572}
573
574fn default_group_add(
575    context: &SteelCommandContext<CommandSource>,
576) -> Result<PermsCommandSuspension, CommandSyntaxError> {
577    default_group(context, true)
578}
579
580fn default_group_remove(
581    context: &SteelCommandContext<CommandSource>,
582) -> Result<PermsCommandSuspension, CommandSyntaxError> {
583    default_group(context, false)
584}
585
586fn default_group(
587    context: &SteelCommandContext<CommandSource>,
588    add: bool,
589) -> Result<PermsCommandSuspension, CommandSyntaxError> {
590    let group = group_argument(context, "group")?;
591    require_group_management(context.source(), &group)?;
592    start(context, Operation::DefaultGroup { group, add })
593}
594
595fn targets(
596    context: &SteelCommandContext<CommandSource>,
597) -> Result<GameProfileArgument, CommandSyntaxError> {
598    context.game_profile_argument("targets").cloned()
599}
600
601fn permission_expression(
602    context: &SteelCommandContext<CommandSource>,
603) -> Result<PermissionRuleExpression, CommandSyntaxError> {
604    context.permission_rule_expression("permission").cloned()
605}
606
607fn metadata_expression(
608    context: &SteelCommandContext<CommandSource>,
609) -> Result<PermissionMetadataExpression, CommandSyntaxError> {
610    context.permission_metadata_expression("metadata").cloned()
611}
612
613fn metadata_value(
614    context: &SteelCommandContext<CommandSource>,
615) -> Result<PermissionMetadataValue, CommandSyntaxError> {
616    if let Ok(value) = context.long("metadata_int_value") {
617        return Ok(PermissionMetadataValue::Integer(value));
618    }
619    if let Ok(value) = context.boolean("metadata_bool_value") {
620        return Ok(PermissionMetadataValue::Bool(value));
621    }
622    if let Ok(value) = context.string("metadata_string_value") {
623        return Ok(PermissionMetadataValue::String(value.to_owned()));
624    }
625    Err(missing_argument("metadata value"))
626}
627
628fn group_argument(
629    context: &SteelCommandContext<CommandSource>,
630    name: &str,
631) -> Result<String, CommandSyntaxError> {
632    context
633        .permission_group(name)
634        .map(|group| group.as_str().to_owned())
635}
636
637fn managed_group(
638    context: &SteelCommandContext<CommandSource>,
639) -> Result<String, CommandSyntaxError> {
640    let group = group_argument(context, "group")?;
641    require_group_management(context.source(), &group)?;
642    Ok(group)
643}
644
645fn require_permission_management(
646    source: &CommandSource,
647    permission: &PermissionKey,
648) -> Result<(), CommandSyntaxError> {
649    require_dynamic_permission(
650        source,
651        format!("steel.permission.manage.{}", permission.as_str()),
652    )
653}
654
655fn require_group_management(source: &CommandSource, group: &str) -> Result<(), CommandSyntaxError> {
656    require_dynamic_permission(source, format!("steel.permission.group.{group}"))
657}
658
659fn require_metadata_management(source: &CommandSource) -> Result<(), CommandSyntaxError> {
660    require_dynamic_permission(source, METADATA_PERMISSION.to_owned())
661}
662
663fn require_dynamic_permission(
664    source: &CommandSource,
665    value: String,
666) -> Result<(), CommandSyntaxError> {
667    let key = PermissionKey::parse(value.clone()).map_err(|error| {
668        CommandSyntaxError::dynamic(format!("Invalid management permission '{value}': {error}"))
669    })?;
670    if CommandPermissionSource::has_permission(source, &PermissionExpr::key(key)) {
671        Ok(())
672    } else {
673        Err(CommandSyntaxError::dynamic(format!(
674            "Requires permission {value}"
675        )))
676    }
677}
678
679#[expect(
680    clippy::unnecessary_wraps,
681    reason = "suspended command callbacks use one fallible constructor signature"
682)]
683fn start(
684    context: &SteelCommandContext<CommandSource>,
685    operation: Operation,
686) -> Result<PermsCommandSuspension, CommandSyntaxError> {
687    let order = operation.suspension_order();
688    let source = context.source().clone();
689    let task_source = source.clone();
690    let (sender, receiver) = oneshot::channel();
691    let task = tokio::spawn(async move {
692        let result = run_operation(&task_source, operation).await;
693        let _ = sender.send(result);
694    });
695    Ok(PermsCommandSuspension {
696        source,
697        order,
698        broadcast_to_admins: order == CommandSuspensionOrder::Global,
699        receiver,
700        task: Some(task),
701    })
702}
703
704async fn run_operation(
705    source: &CommandSource,
706    operation: Operation,
707) -> Result<OperationResult, CommandSyntaxError> {
708    match operation {
709        Operation::UserInfo(targets) => user_info_operation(source, targets).await,
710        Operation::UserPermission {
711            targets,
712            expression,
713            state,
714        } => user_permission_operation(source, targets, expression, state).await,
715        Operation::UserCheck {
716            targets,
717            expression,
718        } => user_check_operation(source, targets, expression).await,
719        Operation::UserMetadata {
720            targets,
721            expression,
722            value,
723        } => user_metadata_operation(source, targets, expression, value).await,
724        Operation::UserMetadataCheck {
725            targets,
726            expression,
727        } => user_metadata_check_operation(source, targets, expression).await,
728        Operation::UserGroup {
729            targets,
730            group,
731            add,
732        } => user_group_operation(source, targets, group, add).await,
733        Operation::GroupInfo(group) => group_info_operation(source, group),
734        Operation::GroupCreate(group) => group_create_operation(source, group).await,
735        Operation::GroupDelete(group) => group_delete_operation(source, group).await,
736        Operation::GroupPermission {
737            group,
738            expression,
739            state,
740        } => group_permission_operation(source, group, expression, state).await,
741        Operation::GroupPriority { group, priority } => {
742            group_priority_operation(source, group, priority).await
743        }
744        Operation::GroupInheritanceList(group) => group_inheritance_list_operation(source, group),
745        Operation::GroupInheritance { group, parent, add } => {
746            group_inheritance_operation(source, group, parent, add).await
747        }
748        Operation::GroupMetadata {
749            group,
750            expression,
751            value,
752        } => group_metadata_operation(source, group, expression, value).await,
753        Operation::GroupsList => Ok(groups_list_operation(source)),
754        Operation::DefaultGroup { group, add } => default_group_operation(source, group, add).await,
755    }
756}
757
758async fn user_info_operation(
759    source: &CommandSource,
760    targets: GameProfileArgument,
761) -> Result<OperationResult, CommandSyntaxError> {
762    let targets = targets.resolve(source).await?;
763    let show_metadata = has_dynamic_permission(source, METADATA_PERMISSION);
764    let mut messages = Vec::new();
765    for target in &targets {
766        let state = source
767            .server()
768            .player_permission_state(target.uuid)
769            .unwrap_or_default();
770        let groups = state
771            .groups()
772            .iter()
773            .filter(|group| can_manage_group(source, group))
774            .cloned()
775            .collect::<Vec<_>>();
776        let rules = state
777            .overrides()
778            .entries()
779            .iter()
780            .filter(|entry| can_manage_permission(source, entry.key()))
781            .map(|entry| {
782                format!(
783                    "{}={}",
784                    PermissionRuleExpression::new(entry.key().clone(), entry.context().clone()),
785                    state_name(entry.state())
786                )
787            })
788            .collect::<Vec<_>>();
789        let metadata = if show_metadata {
790            state
791                .metadata_overrides()
792                .entries()
793                .iter()
794                .map(|entry| {
795                    format!(
796                        "{}={}",
797                        PermissionMetadataExpression::new(
798                            entry.key().clone(),
799                            entry.context().clone()
800                        ),
801                        entry.value()
802                    )
803                })
804                .collect::<Vec<_>>()
805        } else {
806            Vec::new()
807        };
808        messages.push(TextComponent::plain(format!(
809            "{}: groups [{}], rules [{}], metadata [{}]",
810            target.name,
811            groups.join(", "),
812            rules.join(", "),
813            metadata.join(", ")
814        )));
815    }
816    Ok(OperationResult {
817        result: count(targets.len()),
818        messages,
819    })
820}
821
822async fn user_permission_operation(
823    source: &CommandSource,
824    targets: GameProfileArgument,
825    expression: PermissionRuleExpression,
826    state: Option<PermissionState>,
827) -> Result<OperationResult, CommandSyntaxError> {
828    let targets = targets.resolve(source).await?;
829    let mut changed = 0;
830    let mut messages = Vec::new();
831    for target in targets {
832        let edit_expression = expression.clone();
833        let result = source
834            .server()
835            .try_update_player_permissions(target.uuid, move |subject| {
836                let (groups, mut overrides, metadata) = subject.into_parts();
837                let exact = overrides.entries().iter().filter(|entry| {
838                    entry.key() == edit_expression.key()
839                        && entry.context() == edit_expression.context()
840                });
841                let exact = exact.map(PermissionEntry::state).collect::<Vec<_>>();
842                let did_change = match state {
843                    Some(state) => {
844                        let changed = exact.as_slice() != [state];
845                        overrides.set_in(
846                            edit_expression.key().clone(),
847                            edit_expression.context().clone(),
848                            state,
849                        );
850                        changed
851                    }
852                    None => overrides.unset_in(edit_expression.key(), edit_expression.context()),
853                };
854                Ok::<_, Infallible>((
855                    PermissionSubjectState::new_with_metadata(groups, overrides, metadata),
856                    did_change,
857                ))
858            })
859            .await
860            .map_err(dynamic_error)?;
861        if result.1 {
862            changed += 1;
863        }
864        messages.push(TextComponent::plain(format!(
865            "{}: {} {}",
866            target.name,
867            state.map_or("unset", state_name),
868            expression
869        )));
870    }
871    Ok(OperationResult {
872        result: changed,
873        messages,
874    })
875}
876
877async fn user_check_operation(
878    source: &CommandSource,
879    targets: GameProfileArgument,
880    expression: PermissionRuleExpression,
881) -> Result<OperationResult, CommandSyntaxError> {
882    let targets = targets.resolve(source).await?;
883    let context =
884        PermissionContext::from_rule_context(expression.context()).map_err(dynamic_error)?;
885    let mut messages = Vec::new();
886    for target in &targets {
887        let state = source
888            .server()
889            .player_permission_state(target.uuid)
890            .unwrap_or_default();
891        let effective = source
892            .server()
893            .permission_groups
894            .effective_permissions(state.groups(), state.overrides());
895        let resolution = effective.resolve_key_in_detailed(expression.key(), &context);
896        let detail = resolution.as_ref().map_or_else(
897            || "unset".to_owned(),
898            |resolution| {
899                format!(
900                    "{} via {} ({})",
901                    state_name(resolution.state()),
902                    resolution_source(resolution.source()),
903                    PermissionRuleExpression::new(
904                        resolution.key().clone(),
905                        resolution.context().clone()
906                    )
907                )
908            },
909        );
910        messages.push(TextComponent::plain(format!(
911            "{}: {} -> {detail}",
912            target.name, expression
913        )));
914    }
915    Ok(OperationResult {
916        result: count(targets.len()),
917        messages,
918    })
919}
920
921async fn user_metadata_operation(
922    source: &CommandSource,
923    targets: GameProfileArgument,
924    expression: PermissionMetadataExpression,
925    value: Option<PermissionMetadataValue>,
926) -> Result<OperationResult, CommandSyntaxError> {
927    let targets = targets.resolve(source).await?;
928    let mut changed = 0;
929    let mut messages = Vec::new();
930    for target in targets {
931        let edit_expression = expression.clone();
932        let edit_value = value.clone();
933        let result = source
934            .server()
935            .try_update_player_permissions(target.uuid, move |subject| {
936                let (groups, overrides, mut metadata) = subject.into_parts();
937                let previous = metadata.entries().iter().find(|entry| {
938                    entry.key() == edit_expression.key()
939                        && entry.context() == edit_expression.context()
940                });
941                let did_change = match edit_value {
942                    Some(value) => {
943                        let changed = previous.map(PermissionMetadataEntry::value) != Some(&value);
944                        metadata.set_in(
945                            edit_expression.key().clone(),
946                            edit_expression.context().clone(),
947                            value,
948                        );
949                        changed
950                    }
951                    None => metadata.unset_in(edit_expression.key(), edit_expression.context()),
952                };
953                Ok::<_, Infallible>((
954                    PermissionSubjectState::new_with_metadata(groups, overrides, metadata),
955                    did_change,
956                ))
957            })
958            .await
959            .map_err(dynamic_error)?;
960        if result.1 {
961            changed += 1;
962        }
963        messages.push(TextComponent::plain(format!(
964            "{}: {} {}",
965            target.name,
966            value
967                .as_ref()
968                .map_or_else(|| "unset".to_owned(), |value| format!("set {value}")),
969            expression
970        )));
971    }
972    Ok(OperationResult {
973        result: changed,
974        messages,
975    })
976}
977
978async fn user_metadata_check_operation(
979    source: &CommandSource,
980    targets: GameProfileArgument,
981    expression: PermissionMetadataExpression,
982) -> Result<OperationResult, CommandSyntaxError> {
983    let targets = targets.resolve(source).await?;
984    let context =
985        PermissionContext::from_rule_context(expression.context()).map_err(dynamic_error)?;
986    let mut messages = Vec::new();
987    for target in &targets {
988        let state = source
989            .server()
990            .player_permission_state(target.uuid)
991            .unwrap_or_default();
992        let effective = source
993            .server()
994            .permission_groups
995            .effective_metadata(state.groups(), state.metadata_overrides());
996        let resolution = effective.resolve_in_detailed(expression.key(), &context);
997        let detail = resolution.as_ref().map_or_else(
998            || "unset".to_owned(),
999            |resolution| {
1000                format!(
1001                    "{} via {} ({})",
1002                    resolution.value(),
1003                    resolution_source(resolution.source()),
1004                    PermissionMetadataExpression::new(
1005                        resolution.key().clone(),
1006                        resolution.context().clone()
1007                    )
1008                )
1009            },
1010        );
1011        messages.push(TextComponent::plain(format!(
1012            "{}: {} -> {detail}",
1013            target.name, expression
1014        )));
1015    }
1016    Ok(OperationResult {
1017        result: count(targets.len()),
1018        messages,
1019    })
1020}
1021
1022async fn user_group_operation(
1023    source: &CommandSource,
1024    targets: GameProfileArgument,
1025    group: String,
1026    add: bool,
1027) -> Result<OperationResult, CommandSyntaxError> {
1028    let targets = targets.resolve(source).await?;
1029    let mut changed = 0;
1030    let mut messages = Vec::new();
1031    for target in targets {
1032        let edit_group = group.clone();
1033        let result = source
1034            .server()
1035            .try_update_player_permissions(target.uuid, move |subject| {
1036                let (mut groups, overrides, metadata) = subject.into_parts();
1037                let present = groups.iter().any(|assigned| assigned == &edit_group);
1038                let did_change = if add {
1039                    if !present {
1040                        groups.push(edit_group);
1041                    }
1042                    !present
1043                } else {
1044                    groups.retain(|assigned| assigned != &edit_group);
1045                    present
1046                };
1047                Ok::<_, Infallible>((
1048                    PermissionSubjectState::new_with_metadata(groups, overrides, metadata),
1049                    did_change,
1050                ))
1051            })
1052            .await
1053            .map_err(dynamic_error)?;
1054        if result.1 {
1055            changed += 1;
1056        }
1057        messages.push(TextComponent::plain(format!(
1058            "{}: {} group {group}",
1059            target.name,
1060            if add { "added" } else { "removed" }
1061        )));
1062    }
1063    Ok(OperationResult {
1064        result: changed,
1065        messages,
1066    })
1067}
1068
1069fn group_info_operation(
1070    source: &CommandSource,
1071    group: String,
1072) -> Result<OperationResult, CommandSyntaxError> {
1073    let config = source.server().permission_groups.config_snapshot();
1074    let Some(group_config) = config.groups.get(&group) else {
1075        return Err(CommandSyntaxError::dynamic(format!(
1076            "Unknown permission group '{group}'"
1077        )));
1078    };
1079    let allow = group_config
1080        .allow
1081        .iter()
1082        .filter(|expression| manageable_expression(source, expression))
1083        .cloned()
1084        .collect::<Vec<_>>();
1085    let deny = group_config
1086        .deny
1087        .iter()
1088        .filter(|expression| manageable_expression(source, expression))
1089        .cloned()
1090        .collect::<Vec<_>>();
1091    let metadata = if has_dynamic_permission(source, METADATA_PERMISSION) {
1092        group_config
1093            .metadata
1094            .iter()
1095            .map(|rule| format!("{}={}", rule.key, rule.value))
1096            .collect::<Vec<_>>()
1097    } else {
1098        Vec::new()
1099    };
1100    let inherits = group_config
1101        .inherits
1102        .iter()
1103        .filter(|parent| can_manage_group(source, parent))
1104        .cloned()
1105        .collect::<Vec<_>>();
1106    Ok(OperationResult {
1107        result: 1,
1108        messages: vec![TextComponent::plain(format!(
1109            "Group '{group}': priority {}, inherits [{}], allow [{}], deny [{}], metadata [{}]",
1110            group_config.priority,
1111            inherits.join(", "),
1112            allow.join(", "),
1113            deny.join(", "),
1114            metadata.join(", ")
1115        ))],
1116    })
1117}
1118
1119async fn group_create_operation(
1120    source: &CommandSource,
1121    group: String,
1122) -> Result<OperationResult, CommandSyntaxError> {
1123    let edit_group = group.clone();
1124    let changed = source
1125        .server()
1126        .try_update_permission_groups(move |config| config::create_group(config, &edit_group))
1127        .await
1128        .map_err(dynamic_error)?;
1129    Ok(change_result(changed, format!("Created group '{group}'")))
1130}
1131
1132async fn group_delete_operation(
1133    source: &CommandSource,
1134    group: String,
1135) -> Result<OperationResult, CommandSyntaxError> {
1136    let edit_group = group.clone();
1137    let changed = source
1138        .server()
1139        .try_update_permission_groups(move |config| config::delete_group(config, &edit_group))
1140        .await
1141        .map_err(dynamic_error)?;
1142    Ok(change_result(changed, format!("Deleted group '{group}'")))
1143}
1144
1145async fn group_permission_operation(
1146    source: &CommandSource,
1147    group: String,
1148    expression: PermissionRuleExpression,
1149    state: Option<PermissionState>,
1150) -> Result<OperationResult, CommandSyntaxError> {
1151    let edit_group = group.clone();
1152    let edit_expression = expression.clone();
1153    let changed = source
1154        .server()
1155        .try_update_permission_groups(move |config| {
1156            config::set_permission(config, &edit_group, &edit_expression, state)
1157        })
1158        .await
1159        .map_err(dynamic_error)?;
1160    Ok(change_result(
1161        changed,
1162        format!(
1163            "Group '{group}': {} {expression}",
1164            state.map_or("unset", state_name)
1165        ),
1166    ))
1167}
1168
1169async fn group_priority_operation(
1170    source: &CommandSource,
1171    group: String,
1172    priority: i32,
1173) -> Result<OperationResult, CommandSyntaxError> {
1174    let edit_group = group.clone();
1175    let changed = source
1176        .server()
1177        .try_update_permission_groups(move |config| {
1178            config::set_priority(config, &edit_group, priority)
1179        })
1180        .await
1181        .map_err(dynamic_error)?;
1182    Ok(change_result(
1183        changed,
1184        format!("Group '{group}': priority {priority}"),
1185    ))
1186}
1187
1188fn group_inheritance_list_operation(
1189    source: &CommandSource,
1190    group: String,
1191) -> Result<OperationResult, CommandSyntaxError> {
1192    let config = source.server().permission_groups.config_snapshot();
1193    let Some(group_config) = config.groups.get(&group) else {
1194        return Err(CommandSyntaxError::dynamic(format!(
1195            "Unknown permission group '{group}'"
1196        )));
1197    };
1198    let parents = group_config
1199        .inherits
1200        .iter()
1201        .filter(|parent| can_manage_group(source, parent))
1202        .cloned()
1203        .collect::<Vec<_>>();
1204    Ok(OperationResult {
1205        result: count(parents.len()),
1206        messages: vec![TextComponent::plain(format!(
1207            "Group '{group}' inherits [{}]",
1208            parents.join(", ")
1209        ))],
1210    })
1211}
1212
1213async fn group_inheritance_operation(
1214    source: &CommandSource,
1215    group: String,
1216    parent: String,
1217    add: bool,
1218) -> Result<OperationResult, CommandSyntaxError> {
1219    let edit_group = group.clone();
1220    let edit_parent = parent.clone();
1221    let changed = source
1222        .server()
1223        .try_update_permission_groups(move |config| {
1224            config::set_inheritance(config, &edit_group, &edit_parent, add)
1225        })
1226        .await
1227        .map_err(dynamic_error)?;
1228    Ok(change_result(
1229        changed,
1230        format!(
1231            "Group '{group}': {} inheritance '{parent}'",
1232            if add { "added" } else { "removed" }
1233        ),
1234    ))
1235}
1236
1237async fn group_metadata_operation(
1238    source: &CommandSource,
1239    group: String,
1240    expression: PermissionMetadataExpression,
1241    value: Option<PermissionMetadataValue>,
1242) -> Result<OperationResult, CommandSyntaxError> {
1243    let edit_group = group.clone();
1244    let edit_expression = expression.clone();
1245    let edit_value = value.clone();
1246    let changed = source
1247        .server()
1248        .try_update_permission_groups(move |config| {
1249            config::set_metadata(config, &edit_group, &edit_expression, edit_value)
1250        })
1251        .await
1252        .map_err(dynamic_error)?;
1253    Ok(change_result(
1254        changed,
1255        format!(
1256            "Group '{group}': {} {expression}",
1257            value.map_or_else(|| "unset".to_owned(), |value| format!("set {value}"))
1258        ),
1259    ))
1260}
1261
1262fn groups_list_operation(source: &CommandSource) -> OperationResult {
1263    let config = source.server().permission_groups.config_snapshot();
1264    let defaults = config
1265        .default_groups
1266        .iter()
1267        .filter(|group| can_manage_group(source, group))
1268        .cloned()
1269        .collect::<Vec<_>>();
1270    let groups = config
1271        .groups
1272        .keys()
1273        .filter(|group| can_manage_group(source, group))
1274        .cloned()
1275        .collect::<Vec<_>>();
1276    OperationResult {
1277        result: count(groups.len()),
1278        messages: vec![TextComponent::plain(format!(
1279            "Permission groups: defaults [{}], groups [{}]",
1280            defaults.join(", "),
1281            groups.join(", ")
1282        ))],
1283    }
1284}
1285
1286async fn default_group_operation(
1287    source: &CommandSource,
1288    group: String,
1289    add: bool,
1290) -> Result<OperationResult, CommandSyntaxError> {
1291    let edit_group = group.clone();
1292    let changed = source
1293        .server()
1294        .try_update_permission_groups(move |config| {
1295            config::set_default_group(config, &edit_group, add)
1296        })
1297        .await
1298        .map_err(dynamic_error)?;
1299    Ok(change_result(
1300        changed,
1301        format!(
1302            "Group '{group}': {} default assignment",
1303            if add { "added" } else { "removed" }
1304        ),
1305    ))
1306}
1307
1308fn manageable_expression(source: &CommandSource, expression: &str) -> bool {
1309    PermissionRuleExpression::parse(expression)
1310        .is_ok_and(|expression| can_manage_permission(source, expression.key()))
1311}
1312
1313fn can_manage_permission(source: &CommandSource, permission: &PermissionKey) -> bool {
1314    has_dynamic_permission(
1315        source,
1316        &format!("steel.permission.manage.{}", permission.as_str()),
1317    )
1318}
1319
1320fn can_manage_group(source: &CommandSource, group: &str) -> bool {
1321    has_dynamic_permission(source, &format!("steel.permission.group.{group}"))
1322}
1323
1324fn has_dynamic_permission(source: &CommandSource, value: &str) -> bool {
1325    PermissionKey::parse(value)
1326        .is_ok_and(|key| CommandPermissionSource::has_permission(source, &PermissionExpr::key(key)))
1327}
1328
1329const fn state_name(state: PermissionState) -> &'static str {
1330    match state {
1331        PermissionState::Allow => "allow",
1332        PermissionState::Deny => "deny",
1333    }
1334}
1335
1336fn resolution_source(source: &PermissionResolutionSource) -> String {
1337    match source {
1338        PermissionResolutionSource::Group { name, priority } => {
1339            format!("group {name} priority {priority}")
1340        }
1341        PermissionResolutionSource::Subject => "subject override".to_owned(),
1342    }
1343}
1344
1345fn change_result(changed: bool, message: String) -> OperationResult {
1346    OperationResult {
1347        result: i32::from(changed),
1348        messages: vec![TextComponent::plain(if changed {
1349            message
1350        } else {
1351            format!("No change: {message}")
1352        })],
1353    }
1354}
1355
1356fn count(value: usize) -> i32 {
1357    value.min(i32::MAX as usize) as i32
1358}
1359
1360fn dynamic_error(error: impl fmt::Display) -> CommandSyntaxError {
1361    CommandSyntaxError::dynamic(error.to_string())
1362}
1363
1364struct PermsCommandSuspension {
1365    source: CommandSource,
1366    order: CommandSuspensionOrder,
1367    broadcast_to_admins: bool,
1368    receiver: oneshot::Receiver<Result<OperationResult, CommandSyntaxError>>,
1369    task: Option<JoinHandle<()>>,
1370}
1371
1372impl CommandResultSuspension for PermsCommandSuspension {
1373    fn order(&self) -> CommandSuspensionOrder {
1374        self.order
1375    }
1376
1377    fn poll(&mut self) -> CommandResultSuspensionPoll {
1378        match self.receiver.try_recv() {
1379            Ok(result) => {
1380                self.task = None;
1381                CommandResultSuspensionPoll::Ready(result.map(|result| {
1382                    for message in &result.messages {
1383                        self.source.send_success(message, self.broadcast_to_admins);
1384                    }
1385                    result.result
1386                }))
1387            }
1388            Err(oneshot::error::TryRecvError::Empty) => CommandResultSuspensionPoll::Pending,
1389            Err(oneshot::error::TryRecvError::Closed) => {
1390                self.task = None;
1391                CommandResultSuspensionPoll::Ready(Err(CommandSyntaxError::dynamic(
1392                    "perms command task ended without a result",
1393                )))
1394            }
1395        }
1396    }
1397
1398    fn cancel(&mut self) {
1399        if let Some(task) = self.task.take() {
1400            task.abort();
1401        }
1402    }
1403}
1404
1405#[cfg(test)]
1406mod tests {
1407    use steel_registry::init_vanilla_registry;
1408
1409    use super::{GROUP_ALL_PERMISSION, MANAGE_ALL_PERMISSION, METADATA_PERMISSION};
1410    use crate::command::{
1411        CommandRegistry,
1412        brigadier::{CommandDispatcher, NodeId},
1413        builtins::{create_dispatcher, create_registered_dispatcher},
1414        execution::{CommandSource, SteelCommandRuntime},
1415    };
1416    use crate::permission::PermissionKey;
1417
1418    type Dispatcher = CommandDispatcher<CommandSource, SteelCommandRuntime>;
1419
1420    fn child(dispatcher: &Dispatcher, parent: NodeId, name: &str) -> NodeId {
1421        let Some(child) = dispatcher.children(parent).and_then(|children| {
1422            children.iter().copied().find(|child| {
1423                dispatcher
1424                    .node(*child)
1425                    .is_some_and(|node| node.name() == name)
1426            })
1427        }) else {
1428            panic!("missing command node '{name}'");
1429        };
1430        child
1431    }
1432
1433    #[test]
1434    fn perms_exposes_the_management_surface_without_old_aliases() {
1435        init_vanilla_registry();
1436        let Ok(dispatcher) = create_dispatcher() else {
1437            panic!("built-in dispatcher should build");
1438        };
1439        let roots = dispatcher.children(dispatcher.root());
1440        let Some(roots) = roots else {
1441            panic!("dispatcher root should exist");
1442        };
1443        assert!(!roots.iter().any(|root| {
1444            dispatcher
1445                .node(*root)
1446                .is_some_and(|node| matches!(node.name(), "steelperms" | "sp"))
1447        }));
1448
1449        let perms = child(&dispatcher, dispatcher.root(), "perms");
1450        let user = child(&dispatcher, perms, "user");
1451        let targets = child(&dispatcher, user, "targets");
1452        for name in [
1453            "info", "allow", "deny", "unset", "check", "metadata", "group",
1454        ] {
1455            child(&dispatcher, targets, name);
1456        }
1457        let group = child(&dispatcher, perms, "group");
1458        let group_name = child(&dispatcher, group, "group");
1459        for name in [
1460            "create", "info", "delete", "allow", "deny", "unset", "priority", "inherit", "metadata",
1461        ] {
1462            child(&dispatcher, group_name, name);
1463        }
1464        let groups = child(&dispatcher, perms, "groups");
1465        child(&dispatcher, groups, "list");
1466        child(&dispatcher, groups, "default");
1467    }
1468
1469    #[test]
1470    fn perms_discovery_contains_static_admin_and_granular_command_permissions() {
1471        init_vanilla_registry();
1472        let Ok(registered) = create_registered_dispatcher(CommandRegistry::new()) else {
1473            panic!("built-in dispatcher should build");
1474        };
1475        let permissions = registered
1476            .permissions
1477            .iter()
1478            .map(PermissionKey::as_str)
1479            .collect::<Vec<_>>();
1480
1481        for expected in [
1482            MANAGE_ALL_PERMISSION,
1483            GROUP_ALL_PERMISSION,
1484            METADATA_PERMISSION,
1485            "steel.command.perms.user.allow",
1486            "steel.command.perms.user.metadata.set",
1487            "steel.command.perms.group.inherit.add",
1488        ] {
1489            assert!(permissions.contains(&expected), "missing {expected}");
1490        }
1491    }
1492}