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