1mod 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}