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 steel_utils::Identifier;
8use text_components::TextComponent;
9use tokio::{sync::oneshot, task::JoinHandle};
10
11use super::super::{
12    brigadier::{ArgumentType, CommandNodeBuilder, CommandSyntaxError},
13    execution::{
14        CommandPermissionSource, CommandResultSuspension, CommandResultSuspensionPoll,
15        CommandSource, CommandSuspensionOrder, GameProfileArgument, SteelArgumentType,
16        SteelCommandContext, SteelCommandRuntime, argument, literal,
17    },
18    registration::CommandRegistration,
19};
20use crate::permission::{
21    PermissionContext, PermissionEntry, PermissionExpr, PermissionKey, PermissionMetadataEntry,
22    PermissionMetadataExpression, PermissionMetadataValue, PermissionResolutionSource,
23    PermissionRuleExpression, PermissionState, PermissionSubjectState,
24};
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
508        .integer("priority")
509        .ok_or_else(|| missing_argument("priority"))?;
510    start(context, Operation::GroupPriority { group, priority })
511}
512
513fn group_inherit_list(
514    context: &SteelCommandContext<CommandSource>,
515) -> Result<PermsCommandSuspension, CommandSyntaxError> {
516    let group = managed_group(context)?;
517    start(context, Operation::GroupInheritanceList(group))
518}
519
520fn group_inherit_add(
521    context: &SteelCommandContext<CommandSource>,
522) -> Result<PermsCommandSuspension, CommandSyntaxError> {
523    group_inheritance(context, true)
524}
525
526fn group_inherit_remove(
527    context: &SteelCommandContext<CommandSource>,
528) -> Result<PermsCommandSuspension, CommandSyntaxError> {
529    group_inheritance(context, false)
530}
531
532fn group_inheritance(
533    context: &SteelCommandContext<CommandSource>,
534    add: bool,
535) -> Result<PermsCommandSuspension, CommandSyntaxError> {
536    let group = managed_group(context)?;
537    let parent = group_argument(context, "parent")?;
538    require_group_management(context.source(), &parent)?;
539    start(context, Operation::GroupInheritance { group, parent, add })
540}
541
542fn group_metadata_set(
543    context: &SteelCommandContext<CommandSource>,
544) -> Result<PermsCommandSuspension, CommandSyntaxError> {
545    group_metadata(context, Some(metadata_value(context)?))
546}
547
548fn group_metadata_unset(
549    context: &SteelCommandContext<CommandSource>,
550) -> Result<PermsCommandSuspension, CommandSyntaxError> {
551    group_metadata(context, None)
552}
553
554fn group_metadata(
555    context: &SteelCommandContext<CommandSource>,
556    value: Option<PermissionMetadataValue>,
557) -> Result<PermsCommandSuspension, CommandSyntaxError> {
558    require_metadata_management(context.source())?;
559    let group = managed_group(context)?;
560    start(
561        context,
562        Operation::GroupMetadata {
563            group,
564            expression: metadata_expression(context)?,
565            value,
566        },
567    )
568}
569
570fn groups_list(
571    context: &SteelCommandContext<CommandSource>,
572) -> Result<PermsCommandSuspension, CommandSyntaxError> {
573    start(context, Operation::GroupsList)
574}
575
576fn default_group_add(
577    context: &SteelCommandContext<CommandSource>,
578) -> Result<PermsCommandSuspension, CommandSyntaxError> {
579    default_group(context, true)
580}
581
582fn default_group_remove(
583    context: &SteelCommandContext<CommandSource>,
584) -> Result<PermsCommandSuspension, CommandSyntaxError> {
585    default_group(context, false)
586}
587
588fn default_group(
589    context: &SteelCommandContext<CommandSource>,
590    add: bool,
591) -> Result<PermsCommandSuspension, CommandSyntaxError> {
592    let group = group_argument(context, "group")?;
593    require_group_management(context.source(), &group)?;
594    start(context, Operation::DefaultGroup { group, add })
595}
596
597fn targets(
598    context: &SteelCommandContext<CommandSource>,
599) -> Result<GameProfileArgument, CommandSyntaxError> {
600    context
601        .game_profile_argument("targets")
602        .cloned()
603        .ok_or_else(|| missing_argument("targets"))
604}
605
606fn permission_expression(
607    context: &SteelCommandContext<CommandSource>,
608) -> Result<PermissionRuleExpression, CommandSyntaxError> {
609    context
610        .permission_rule_expression("permission")
611        .cloned()
612        .ok_or_else(|| missing_argument("permission"))
613}
614
615fn metadata_expression(
616    context: &SteelCommandContext<CommandSource>,
617) -> Result<PermissionMetadataExpression, CommandSyntaxError> {
618    context
619        .permission_metadata_expression("metadata")
620        .cloned()
621        .ok_or_else(|| missing_argument("metadata"))
622}
623
624fn metadata_value(
625    context: &SteelCommandContext<CommandSource>,
626) -> Result<PermissionMetadataValue, CommandSyntaxError> {
627    if let Some(value) = context.long("metadata_int_value") {
628        return Ok(PermissionMetadataValue::Integer(value));
629    }
630    if let Some(value) = context.boolean("metadata_bool_value") {
631        return Ok(PermissionMetadataValue::Bool(value));
632    }
633    if let Some(value) = context.string("metadata_string_value") {
634        return Ok(PermissionMetadataValue::String(value.to_owned()));
635    }
636    Err(missing_argument("metadata value"))
637}
638
639fn group_argument(
640    context: &SteelCommandContext<CommandSource>,
641    name: &str,
642) -> Result<String, CommandSyntaxError> {
643    context
644        .permission_group(name)
645        .map(|group| group.as_str().to_owned())
646        .ok_or_else(|| missing_argument(name))
647}
648
649fn managed_group(
650    context: &SteelCommandContext<CommandSource>,
651) -> Result<String, CommandSyntaxError> {
652    let group = group_argument(context, "group")?;
653    require_group_management(context.source(), &group)?;
654    Ok(group)
655}
656
657fn missing_argument(name: &str) -> CommandSyntaxError {
658    CommandSyntaxError::dynamic(format!("Missing permission command argument '{name}'"))
659}
660
661fn require_permission_management(
662    source: &CommandSource,
663    permission: &PermissionKey,
664) -> Result<(), CommandSyntaxError> {
665    require_dynamic_permission(
666        source,
667        format!("steel.permission.manage.{}", permission.as_str()),
668    )
669}
670
671fn require_group_management(source: &CommandSource, group: &str) -> Result<(), CommandSyntaxError> {
672    require_dynamic_permission(source, format!("steel.permission.group.{group}"))
673}
674
675fn require_metadata_management(source: &CommandSource) -> Result<(), CommandSyntaxError> {
676    require_dynamic_permission(source, METADATA_PERMISSION.to_owned())
677}
678
679fn require_dynamic_permission(
680    source: &CommandSource,
681    value: String,
682) -> Result<(), CommandSyntaxError> {
683    let key = PermissionKey::parse(value.clone()).map_err(|error| {
684        CommandSyntaxError::dynamic(format!("Invalid management permission '{value}': {error}"))
685    })?;
686    if CommandPermissionSource::has_permission(source, &PermissionExpr::key(key)) {
687        Ok(())
688    } else {
689        Err(CommandSyntaxError::dynamic(format!(
690            "Requires permission {value}"
691        )))
692    }
693}
694
695#[expect(
696    clippy::unnecessary_wraps,
697    reason = "suspended command callbacks use one fallible constructor signature"
698)]
699fn start(
700    context: &SteelCommandContext<CommandSource>,
701    operation: Operation,
702) -> Result<PermsCommandSuspension, CommandSyntaxError> {
703    let order = operation.suspension_order();
704    let source = context.source().clone();
705    let task_source = source.clone();
706    let (sender, receiver) = oneshot::channel();
707    let task = tokio::spawn(async move {
708        let result = run_operation(&task_source, operation).await;
709        let _ = sender.send(result);
710    });
711    Ok(PermsCommandSuspension {
712        source,
713        order,
714        broadcast_to_admins: order == CommandSuspensionOrder::Global,
715        receiver,
716        task: Some(task),
717    })
718}
719
720async fn run_operation(
721    source: &CommandSource,
722    operation: Operation,
723) -> Result<OperationResult, CommandSyntaxError> {
724    match operation {
725        Operation::UserInfo(targets) => user_info_operation(source, targets).await,
726        Operation::UserPermission {
727            targets,
728            expression,
729            state,
730        } => user_permission_operation(source, targets, expression, state).await,
731        Operation::UserCheck {
732            targets,
733            expression,
734        } => user_check_operation(source, targets, expression).await,
735        Operation::UserMetadata {
736            targets,
737            expression,
738            value,
739        } => user_metadata_operation(source, targets, expression, value).await,
740        Operation::UserMetadataCheck {
741            targets,
742            expression,
743        } => user_metadata_check_operation(source, targets, expression).await,
744        Operation::UserGroup {
745            targets,
746            group,
747            add,
748        } => user_group_operation(source, targets, group, add).await,
749        Operation::GroupInfo(group) => group_info_operation(source, group),
750        Operation::GroupCreate(group) => group_create_operation(source, group).await,
751        Operation::GroupDelete(group) => group_delete_operation(source, group).await,
752        Operation::GroupPermission {
753            group,
754            expression,
755            state,
756        } => group_permission_operation(source, group, expression, state).await,
757        Operation::GroupPriority { group, priority } => {
758            group_priority_operation(source, group, priority).await
759        }
760        Operation::GroupInheritanceList(group) => group_inheritance_list_operation(source, group),
761        Operation::GroupInheritance { group, parent, add } => {
762            group_inheritance_operation(source, group, parent, add).await
763        }
764        Operation::GroupMetadata {
765            group,
766            expression,
767            value,
768        } => group_metadata_operation(source, group, expression, value).await,
769        Operation::GroupsList => Ok(groups_list_operation(source)),
770        Operation::DefaultGroup { group, add } => default_group_operation(source, group, add).await,
771    }
772}
773
774async fn user_info_operation(
775    source: &CommandSource,
776    targets: GameProfileArgument,
777) -> Result<OperationResult, CommandSyntaxError> {
778    let targets = targets.resolve(source).await?;
779    let show_metadata = has_dynamic_permission(source, METADATA_PERMISSION);
780    let mut messages = Vec::new();
781    for target in &targets {
782        let state = source
783            .server()
784            .player_permission_state(target.uuid)
785            .unwrap_or_default();
786        let groups = state
787            .groups()
788            .iter()
789            .filter(|group| can_manage_group(source, group))
790            .cloned()
791            .collect::<Vec<_>>();
792        let rules = state
793            .overrides()
794            .entries()
795            .iter()
796            .filter(|entry| can_manage_permission(source, entry.key()))
797            .map(|entry| {
798                format!(
799                    "{}={}",
800                    PermissionRuleExpression::new(entry.key().clone(), entry.context().clone()),
801                    state_name(entry.state())
802                )
803            })
804            .collect::<Vec<_>>();
805        let metadata = if show_metadata {
806            state
807                .metadata_overrides()
808                .entries()
809                .iter()
810                .map(|entry| {
811                    format!(
812                        "{}={}",
813                        PermissionMetadataExpression::new(
814                            entry.key().clone(),
815                            entry.context().clone()
816                        ),
817                        entry.value()
818                    )
819                })
820                .collect::<Vec<_>>()
821        } else {
822            Vec::new()
823        };
824        messages.push(TextComponent::plain(format!(
825            "{}: groups [{}], rules [{}], metadata [{}]",
826            target.name,
827            groups.join(", "),
828            rules.join(", "),
829            metadata.join(", ")
830        )));
831    }
832    Ok(OperationResult {
833        result: count(targets.len()),
834        messages,
835    })
836}
837
838async fn user_permission_operation(
839    source: &CommandSource,
840    targets: GameProfileArgument,
841    expression: PermissionRuleExpression,
842    state: Option<PermissionState>,
843) -> Result<OperationResult, CommandSyntaxError> {
844    let targets = targets.resolve(source).await?;
845    let mut changed = 0;
846    let mut messages = Vec::new();
847    for target in targets {
848        let edit_expression = expression.clone();
849        let result = source
850            .server()
851            .try_update_player_permissions(target.uuid, move |subject| {
852                let (groups, mut overrides, metadata) = subject.into_parts();
853                let exact = overrides.entries().iter().filter(|entry| {
854                    entry.key() == edit_expression.key()
855                        && entry.context() == edit_expression.context()
856                });
857                let exact = exact.map(PermissionEntry::state).collect::<Vec<_>>();
858                let did_change = match state {
859                    Some(state) => {
860                        let changed = exact.as_slice() != [state];
861                        overrides.set_in(
862                            edit_expression.key().clone(),
863                            edit_expression.context().clone(),
864                            state,
865                        );
866                        changed
867                    }
868                    None => overrides.unset_in(edit_expression.key(), edit_expression.context()),
869                };
870                Ok::<_, Infallible>((
871                    PermissionSubjectState::new_with_metadata(groups, overrides, metadata),
872                    did_change,
873                ))
874            })
875            .await
876            .map_err(dynamic_error)?;
877        if result.1 {
878            changed += 1;
879        }
880        messages.push(TextComponent::plain(format!(
881            "{}: {} {}",
882            target.name,
883            state.map_or("unset", state_name),
884            expression
885        )));
886    }
887    Ok(OperationResult {
888        result: changed,
889        messages,
890    })
891}
892
893async fn user_check_operation(
894    source: &CommandSource,
895    targets: GameProfileArgument,
896    expression: PermissionRuleExpression,
897) -> Result<OperationResult, CommandSyntaxError> {
898    let targets = targets.resolve(source).await?;
899    let context =
900        PermissionContext::from_rule_context(expression.context()).map_err(dynamic_error)?;
901    let mut messages = Vec::new();
902    for target in &targets {
903        let state = source
904            .server()
905            .player_permission_state(target.uuid)
906            .unwrap_or_default();
907        let effective = source
908            .server()
909            .permission_groups
910            .effective_permissions(state.groups(), state.overrides());
911        let resolution = effective.resolve_key_in_detailed(expression.key(), &context);
912        let detail = resolution.as_ref().map_or_else(
913            || "unset".to_owned(),
914            |resolution| {
915                format!(
916                    "{} via {} ({})",
917                    state_name(resolution.state()),
918                    resolution_source(resolution.source()),
919                    PermissionRuleExpression::new(
920                        resolution.key().clone(),
921                        resolution.context().clone()
922                    )
923                )
924            },
925        );
926        messages.push(TextComponent::plain(format!(
927            "{}: {} -> {detail}",
928            target.name, expression
929        )));
930    }
931    Ok(OperationResult {
932        result: count(targets.len()),
933        messages,
934    })
935}
936
937async fn user_metadata_operation(
938    source: &CommandSource,
939    targets: GameProfileArgument,
940    expression: PermissionMetadataExpression,
941    value: Option<PermissionMetadataValue>,
942) -> Result<OperationResult, CommandSyntaxError> {
943    let targets = targets.resolve(source).await?;
944    let mut changed = 0;
945    let mut messages = Vec::new();
946    for target in targets {
947        let edit_expression = expression.clone();
948        let edit_value = value.clone();
949        let result = source
950            .server()
951            .try_update_player_permissions(target.uuid, move |subject| {
952                let (groups, overrides, mut metadata) = subject.into_parts();
953                let previous = metadata.entries().iter().find(|entry| {
954                    entry.key() == edit_expression.key()
955                        && entry.context() == edit_expression.context()
956                });
957                let did_change = match edit_value {
958                    Some(value) => {
959                        let changed = previous.map(PermissionMetadataEntry::value) != Some(&value);
960                        metadata.set_in(
961                            edit_expression.key().clone(),
962                            edit_expression.context().clone(),
963                            value,
964                        );
965                        changed
966                    }
967                    None => metadata.unset_in(edit_expression.key(), edit_expression.context()),
968                };
969                Ok::<_, Infallible>((
970                    PermissionSubjectState::new_with_metadata(groups, overrides, metadata),
971                    did_change,
972                ))
973            })
974            .await
975            .map_err(dynamic_error)?;
976        if result.1 {
977            changed += 1;
978        }
979        messages.push(TextComponent::plain(format!(
980            "{}: {} {}",
981            target.name,
982            value
983                .as_ref()
984                .map_or_else(|| "unset".to_owned(), |value| format!("set {value}")),
985            expression
986        )));
987    }
988    Ok(OperationResult {
989        result: changed,
990        messages,
991    })
992}
993
994async fn user_metadata_check_operation(
995    source: &CommandSource,
996    targets: GameProfileArgument,
997    expression: PermissionMetadataExpression,
998) -> Result<OperationResult, CommandSyntaxError> {
999    let targets = targets.resolve(source).await?;
1000    let context =
1001        PermissionContext::from_rule_context(expression.context()).map_err(dynamic_error)?;
1002    let mut messages = Vec::new();
1003    for target in &targets {
1004        let state = source
1005            .server()
1006            .player_permission_state(target.uuid)
1007            .unwrap_or_default();
1008        let effective = source
1009            .server()
1010            .permission_groups
1011            .effective_metadata(state.groups(), state.metadata_overrides());
1012        let resolution = effective.resolve_in_detailed(expression.key(), &context);
1013        let detail = resolution.as_ref().map_or_else(
1014            || "unset".to_owned(),
1015            |resolution| {
1016                format!(
1017                    "{} via {} ({})",
1018                    resolution.value(),
1019                    resolution_source(resolution.source()),
1020                    PermissionMetadataExpression::new(
1021                        resolution.key().clone(),
1022                        resolution.context().clone()
1023                    )
1024                )
1025            },
1026        );
1027        messages.push(TextComponent::plain(format!(
1028            "{}: {} -> {detail}",
1029            target.name, expression
1030        )));
1031    }
1032    Ok(OperationResult {
1033        result: count(targets.len()),
1034        messages,
1035    })
1036}
1037
1038async fn user_group_operation(
1039    source: &CommandSource,
1040    targets: GameProfileArgument,
1041    group: String,
1042    add: bool,
1043) -> Result<OperationResult, CommandSyntaxError> {
1044    let targets = targets.resolve(source).await?;
1045    let mut changed = 0;
1046    let mut messages = Vec::new();
1047    for target in targets {
1048        let edit_group = group.clone();
1049        let result = source
1050            .server()
1051            .try_update_player_permissions(target.uuid, move |subject| {
1052                let (mut groups, overrides, metadata) = subject.into_parts();
1053                let present = groups.iter().any(|assigned| assigned == &edit_group);
1054                let did_change = if add {
1055                    if !present {
1056                        groups.push(edit_group);
1057                    }
1058                    !present
1059                } else {
1060                    groups.retain(|assigned| assigned != &edit_group);
1061                    present
1062                };
1063                Ok::<_, Infallible>((
1064                    PermissionSubjectState::new_with_metadata(groups, overrides, metadata),
1065                    did_change,
1066                ))
1067            })
1068            .await
1069            .map_err(dynamic_error)?;
1070        if result.1 {
1071            changed += 1;
1072        }
1073        messages.push(TextComponent::plain(format!(
1074            "{}: {} group {group}",
1075            target.name,
1076            if add { "added" } else { "removed" }
1077        )));
1078    }
1079    Ok(OperationResult {
1080        result: changed,
1081        messages,
1082    })
1083}
1084
1085fn group_info_operation(
1086    source: &CommandSource,
1087    group: String,
1088) -> Result<OperationResult, CommandSyntaxError> {
1089    let config = source.server().permission_groups.config_snapshot();
1090    let Some(group_config) = config.groups.get(&group) else {
1091        return Err(CommandSyntaxError::dynamic(format!(
1092            "Unknown permission group '{group}'"
1093        )));
1094    };
1095    let allow = group_config
1096        .allow
1097        .iter()
1098        .filter(|expression| manageable_expression(source, expression))
1099        .cloned()
1100        .collect::<Vec<_>>();
1101    let deny = group_config
1102        .deny
1103        .iter()
1104        .filter(|expression| manageable_expression(source, expression))
1105        .cloned()
1106        .collect::<Vec<_>>();
1107    let metadata = if has_dynamic_permission(source, METADATA_PERMISSION) {
1108        group_config
1109            .metadata
1110            .iter()
1111            .map(|rule| format!("{}={}", rule.key, rule.value))
1112            .collect::<Vec<_>>()
1113    } else {
1114        Vec::new()
1115    };
1116    let inherits = group_config
1117        .inherits
1118        .iter()
1119        .filter(|parent| can_manage_group(source, parent))
1120        .cloned()
1121        .collect::<Vec<_>>();
1122    Ok(OperationResult {
1123        result: 1,
1124        messages: vec![TextComponent::plain(format!(
1125            "Group '{group}': priority {}, inherits [{}], allow [{}], deny [{}], metadata [{}]",
1126            group_config.priority,
1127            inherits.join(", "),
1128            allow.join(", "),
1129            deny.join(", "),
1130            metadata.join(", ")
1131        ))],
1132    })
1133}
1134
1135async fn group_create_operation(
1136    source: &CommandSource,
1137    group: String,
1138) -> Result<OperationResult, CommandSyntaxError> {
1139    let edit_group = group.clone();
1140    let changed = source
1141        .server()
1142        .try_update_permission_groups(move |config| config::create_group(config, &edit_group))
1143        .await
1144        .map_err(dynamic_error)?;
1145    Ok(change_result(changed, format!("Created group '{group}'")))
1146}
1147
1148async fn group_delete_operation(
1149    source: &CommandSource,
1150    group: String,
1151) -> Result<OperationResult, CommandSyntaxError> {
1152    let edit_group = group.clone();
1153    let changed = source
1154        .server()
1155        .try_update_permission_groups(move |config| config::delete_group(config, &edit_group))
1156        .await
1157        .map_err(dynamic_error)?;
1158    Ok(change_result(changed, format!("Deleted group '{group}'")))
1159}
1160
1161async fn group_permission_operation(
1162    source: &CommandSource,
1163    group: String,
1164    expression: PermissionRuleExpression,
1165    state: Option<PermissionState>,
1166) -> Result<OperationResult, CommandSyntaxError> {
1167    let edit_group = group.clone();
1168    let edit_expression = expression.clone();
1169    let changed = source
1170        .server()
1171        .try_update_permission_groups(move |config| {
1172            config::set_permission(config, &edit_group, &edit_expression, state)
1173        })
1174        .await
1175        .map_err(dynamic_error)?;
1176    Ok(change_result(
1177        changed,
1178        format!(
1179            "Group '{group}': {} {expression}",
1180            state.map_or("unset", state_name)
1181        ),
1182    ))
1183}
1184
1185async fn group_priority_operation(
1186    source: &CommandSource,
1187    group: String,
1188    priority: i32,
1189) -> Result<OperationResult, CommandSyntaxError> {
1190    let edit_group = group.clone();
1191    let changed = source
1192        .server()
1193        .try_update_permission_groups(move |config| {
1194            config::set_priority(config, &edit_group, priority)
1195        })
1196        .await
1197        .map_err(dynamic_error)?;
1198    Ok(change_result(
1199        changed,
1200        format!("Group '{group}': priority {priority}"),
1201    ))
1202}
1203
1204fn group_inheritance_list_operation(
1205    source: &CommandSource,
1206    group: String,
1207) -> Result<OperationResult, CommandSyntaxError> {
1208    let config = source.server().permission_groups.config_snapshot();
1209    let Some(group_config) = config.groups.get(&group) else {
1210        return Err(CommandSyntaxError::dynamic(format!(
1211            "Unknown permission group '{group}'"
1212        )));
1213    };
1214    let parents = group_config
1215        .inherits
1216        .iter()
1217        .filter(|parent| can_manage_group(source, parent))
1218        .cloned()
1219        .collect::<Vec<_>>();
1220    Ok(OperationResult {
1221        result: count(parents.len()),
1222        messages: vec![TextComponent::plain(format!(
1223            "Group '{group}' inherits [{}]",
1224            parents.join(", ")
1225        ))],
1226    })
1227}
1228
1229async fn group_inheritance_operation(
1230    source: &CommandSource,
1231    group: String,
1232    parent: String,
1233    add: bool,
1234) -> Result<OperationResult, CommandSyntaxError> {
1235    let edit_group = group.clone();
1236    let edit_parent = parent.clone();
1237    let changed = source
1238        .server()
1239        .try_update_permission_groups(move |config| {
1240            config::set_inheritance(config, &edit_group, &edit_parent, add)
1241        })
1242        .await
1243        .map_err(dynamic_error)?;
1244    Ok(change_result(
1245        changed,
1246        format!(
1247            "Group '{group}': {} inheritance '{parent}'",
1248            if add { "added" } else { "removed" }
1249        ),
1250    ))
1251}
1252
1253async fn group_metadata_operation(
1254    source: &CommandSource,
1255    group: String,
1256    expression: PermissionMetadataExpression,
1257    value: Option<PermissionMetadataValue>,
1258) -> Result<OperationResult, CommandSyntaxError> {
1259    let edit_group = group.clone();
1260    let edit_expression = expression.clone();
1261    let edit_value = value.clone();
1262    let changed = source
1263        .server()
1264        .try_update_permission_groups(move |config| {
1265            config::set_metadata(config, &edit_group, &edit_expression, edit_value)
1266        })
1267        .await
1268        .map_err(dynamic_error)?;
1269    Ok(change_result(
1270        changed,
1271        format!(
1272            "Group '{group}': {} {expression}",
1273            value.map_or_else(|| "unset".to_owned(), |value| format!("set {value}"))
1274        ),
1275    ))
1276}
1277
1278fn groups_list_operation(source: &CommandSource) -> OperationResult {
1279    let config = source.server().permission_groups.config_snapshot();
1280    let defaults = config
1281        .default_groups
1282        .iter()
1283        .filter(|group| can_manage_group(source, group))
1284        .cloned()
1285        .collect::<Vec<_>>();
1286    let groups = config
1287        .groups
1288        .keys()
1289        .filter(|group| can_manage_group(source, group))
1290        .cloned()
1291        .collect::<Vec<_>>();
1292    OperationResult {
1293        result: count(groups.len()),
1294        messages: vec![TextComponent::plain(format!(
1295            "Permission groups: defaults [{}], groups [{}]",
1296            defaults.join(", "),
1297            groups.join(", ")
1298        ))],
1299    }
1300}
1301
1302async fn default_group_operation(
1303    source: &CommandSource,
1304    group: String,
1305    add: bool,
1306) -> Result<OperationResult, CommandSyntaxError> {
1307    let edit_group = group.clone();
1308    let changed = source
1309        .server()
1310        .try_update_permission_groups(move |config| {
1311            config::set_default_group(config, &edit_group, add)
1312        })
1313        .await
1314        .map_err(dynamic_error)?;
1315    Ok(change_result(
1316        changed,
1317        format!(
1318            "Group '{group}': {} default assignment",
1319            if add { "added" } else { "removed" }
1320        ),
1321    ))
1322}
1323
1324fn manageable_expression(source: &CommandSource, expression: &str) -> bool {
1325    PermissionRuleExpression::parse(expression)
1326        .is_ok_and(|expression| can_manage_permission(source, expression.key()))
1327}
1328
1329fn can_manage_permission(source: &CommandSource, permission: &PermissionKey) -> bool {
1330    has_dynamic_permission(
1331        source,
1332        &format!("steel.permission.manage.{}", permission.as_str()),
1333    )
1334}
1335
1336fn can_manage_group(source: &CommandSource, group: &str) -> bool {
1337    has_dynamic_permission(source, &format!("steel.permission.group.{group}"))
1338}
1339
1340fn has_dynamic_permission(source: &CommandSource, value: &str) -> bool {
1341    PermissionKey::parse(value)
1342        .is_ok_and(|key| CommandPermissionSource::has_permission(source, &PermissionExpr::key(key)))
1343}
1344
1345const fn state_name(state: PermissionState) -> &'static str {
1346    match state {
1347        PermissionState::Allow => "allow",
1348        PermissionState::Deny => "deny",
1349    }
1350}
1351
1352fn resolution_source(source: &PermissionResolutionSource) -> String {
1353    match source {
1354        PermissionResolutionSource::Group { name, priority } => {
1355            format!("group {name} priority {priority}")
1356        }
1357        PermissionResolutionSource::Subject => "subject override".to_owned(),
1358    }
1359}
1360
1361fn change_result(changed: bool, message: String) -> OperationResult {
1362    OperationResult {
1363        result: i32::from(changed),
1364        messages: vec![TextComponent::plain(if changed {
1365            message
1366        } else {
1367            format!("No change: {message}")
1368        })],
1369    }
1370}
1371
1372fn count(value: usize) -> i32 {
1373    value.min(i32::MAX as usize) as i32
1374}
1375
1376fn dynamic_error(error: impl fmt::Display) -> CommandSyntaxError {
1377    CommandSyntaxError::dynamic(error.to_string())
1378}
1379
1380struct PermsCommandSuspension {
1381    source: CommandSource,
1382    order: CommandSuspensionOrder,
1383    broadcast_to_admins: bool,
1384    receiver: oneshot::Receiver<Result<OperationResult, CommandSyntaxError>>,
1385    task: Option<JoinHandle<()>>,
1386}
1387
1388impl CommandResultSuspension for PermsCommandSuspension {
1389    fn order(&self) -> CommandSuspensionOrder {
1390        self.order
1391    }
1392
1393    fn poll(&mut self) -> CommandResultSuspensionPoll {
1394        match self.receiver.try_recv() {
1395            Ok(result) => {
1396                self.task = None;
1397                CommandResultSuspensionPoll::Ready(result.map(|result| {
1398                    for message in &result.messages {
1399                        self.source.send_success(message, self.broadcast_to_admins);
1400                    }
1401                    result.result
1402                }))
1403            }
1404            Err(oneshot::error::TryRecvError::Empty) => CommandResultSuspensionPoll::Pending,
1405            Err(oneshot::error::TryRecvError::Closed) => {
1406                self.task = None;
1407                CommandResultSuspensionPoll::Ready(Err(CommandSyntaxError::dynamic(
1408                    "perms command task ended without a result",
1409                )))
1410            }
1411        }
1412    }
1413
1414    fn cancel(&mut self) {
1415        if let Some(task) = self.task.take() {
1416            task.abort();
1417        }
1418    }
1419}
1420
1421#[cfg(test)]
1422mod tests {
1423    use steel_registry::init_vanilla_registry;
1424
1425    use super::{GROUP_ALL_PERMISSION, MANAGE_ALL_PERMISSION, METADATA_PERMISSION};
1426    use crate::command::{
1427        CommandRegistry,
1428        brigadier::{CommandDispatcher, NodeId},
1429        builtins::{create_dispatcher, create_registered_dispatcher},
1430        execution::{CommandSource, SteelCommandRuntime},
1431    };
1432    use crate::permission::PermissionKey;
1433
1434    type Dispatcher = CommandDispatcher<CommandSource, SteelCommandRuntime>;
1435
1436    fn child(dispatcher: &Dispatcher, parent: NodeId, name: &str) -> NodeId {
1437        let Some(child) = dispatcher.children(parent).and_then(|children| {
1438            children.iter().copied().find(|child| {
1439                dispatcher
1440                    .node(*child)
1441                    .is_some_and(|node| node.name() == name)
1442            })
1443        }) else {
1444            panic!("missing command node '{name}'");
1445        };
1446        child
1447    }
1448
1449    #[test]
1450    fn perms_exposes_the_management_surface_without_old_aliases() {
1451        init_vanilla_registry();
1452        let Ok(dispatcher) = create_dispatcher() else {
1453            panic!("built-in dispatcher should build");
1454        };
1455        let roots = dispatcher.children(dispatcher.root());
1456        let Some(roots) = roots else {
1457            panic!("dispatcher root should exist");
1458        };
1459        assert!(!roots.iter().any(|root| {
1460            dispatcher
1461                .node(*root)
1462                .is_some_and(|node| matches!(node.name(), "steelperms" | "sp"))
1463        }));
1464
1465        let perms = child(&dispatcher, dispatcher.root(), "perms");
1466        let user = child(&dispatcher, perms, "user");
1467        let targets = child(&dispatcher, user, "targets");
1468        for name in [
1469            "info", "allow", "deny", "unset", "check", "metadata", "group",
1470        ] {
1471            child(&dispatcher, targets, name);
1472        }
1473        let group = child(&dispatcher, perms, "group");
1474        let group_name = child(&dispatcher, group, "group");
1475        for name in [
1476            "create", "info", "delete", "allow", "deny", "unset", "priority", "inherit", "metadata",
1477        ] {
1478            child(&dispatcher, group_name, name);
1479        }
1480        let groups = child(&dispatcher, perms, "groups");
1481        child(&dispatcher, groups, "list");
1482        child(&dispatcher, groups, "default");
1483    }
1484
1485    #[test]
1486    fn perms_discovery_contains_static_admin_and_granular_command_permissions() {
1487        init_vanilla_registry();
1488        let Ok(registered) = create_registered_dispatcher(CommandRegistry::new()) else {
1489            panic!("built-in dispatcher should build");
1490        };
1491        let permissions = registered
1492            .permissions
1493            .iter()
1494            .map(PermissionKey::as_str)
1495            .collect::<Vec<_>>();
1496
1497        for expected in [
1498            MANAGE_ALL_PERMISSION,
1499            GROUP_ALL_PERMISSION,
1500            METADATA_PERMISSION,
1501            "steel.command.perms.user.allow",
1502            "steel.command.perms.user.metadata.set",
1503            "steel.command.perms.group.inherit.add",
1504        ] {
1505            assert!(permissions.contains(&expected), "missing {expected}");
1506        }
1507    }
1508}