diff --git a/cloud-core/src/main/java/org/incendo/cloud/CommandTree.java b/cloud-core/src/main/java/org/incendo/cloud/CommandTree.java index c8dac67cc..f6b6f531a 100644 --- a/cloud-core/src/main/java/org/incendo/cloud/CommandTree.java +++ b/cloud-core/src/main/java/org/incendo/cloud/CommandTree.java @@ -226,11 +226,7 @@ private CommandTree(final @NonNull CommandManager commandManager) { final PermissionResult permissionResult = this.determinePermissionResult(commandContext.sender(), root); if (permissionResult.denied()) { return CompletableFutures.failedFuture( - new NoPermissionException( - permissionResult, - commandContext.sender(), - this.getComponentChain(root) - ) + this.noPermissionOrSyntax(permissionResult, commandContext.sender(), root) ); } @@ -329,11 +325,7 @@ private CommandTree(final @NonNull CommandManager commandManager) { ); if (check.denied()) { return CompletableFutures.failedFuture( - new NoPermissionException( - check, - commandContext.sender(), - this.getComponentChain(root) - ) + this.noPermissionOrSyntax(check, commandContext.sender(), root) ); } return CompletableFuture.completedFuture(root.command()); @@ -350,6 +342,72 @@ private CommandTree(final @NonNull CommandManager commandManager) { }); } + @SuppressWarnings({"unchecked", "rawtypes"}) + private Exception noPermissionOrSyntax( + final PermissionResult permissionResult, + final C sender, + final CommandNode root + ) { + final boolean convert = this.commandManager.settings().get(ManagerSetting.HIDE_COMMAND_EXISTENCE); + if (!convert) { + return new NoPermissionException( + permissionResult, + sender, + this.getComponentChain(root) + ); + } + + if (this.childPermitted(root, sender)) { + return new InvalidSyntaxException( + this.commandManager.commandSyntaxFormatter().apply(sender, (List) this.getComponentChain(root), root), + sender, this.getComponentChain(root) + ); + } + + final @Nullable List> parentChain = this.permittedParentChain(root, sender); + if (parentChain != null) { + return new InvalidSyntaxException( + this.commandManager.commandSyntaxFormatter().apply( + sender, + parentChain.stream().map(CommandNode::component) + .filter(Objects::nonNull).collect(Collectors.toList()), + root + ), + sender, this.getComponentChain(root) + ); + } + + return new NoPermissionException( + permissionResult, + sender, + this.getComponentChain(root) + ); + } + + private boolean childPermitted(final CommandNode node, final C sender) { + if (this.determinePermissionResult(sender, node).allowed()) { + return true; + } + for (final CommandNode child : node.children()) { + if (this.childPermitted(child, sender)) { + return true; + } + } + return false; + } + + private @Nullable List> permittedParentChain(final CommandNode node, final C sender) { + final @Nullable CommandNode parent = node.parent(); + if (parent != null) { + if (this.determinePermissionResult(sender, parent).allowed()) { + return this.getChain(parent); + } else { + return this.permittedParentChain(parent, sender); + } + } + return null; + } + private @Nullable CompletableFuture<@Nullable Command> attemptParseUnambiguousChild( final @NonNull List<@NonNull CommandComponent> parsedArguments, final @NonNull CommandContext commandContext, @@ -381,11 +439,7 @@ private CommandTree(final @NonNull CommandManager commandManager) { final PermissionResult childCheck = this.determinePermissionResult(sender, child); if (!commandInput.isEmpty() && childCheck.denied()) { return CompletableFutures.failedFuture( - new NoPermissionException( - childCheck, - sender, - this.getComponentChain(child) - ) + this.noPermissionOrSyntax(childCheck, sender, child) ); } @@ -448,11 +502,7 @@ private CommandTree(final @NonNull CommandManager commandManager) { return CompletableFuture.completedFuture(command); } return CompletableFutures.failedFuture( - new NoPermissionException( - check, - sender, - this.getComponentChain(root) - ) + this.noPermissionOrSyntax(check, sender, root) ); } else { // The child is not a leaf, but may have an intermediary executor, attempt to use it @@ -477,11 +527,7 @@ private CommandTree(final @NonNull CommandManager commandManager) { } return CompletableFutures.failedFuture( - new NoPermissionException( - check, - sender, - this.getComponentChain(root) - ) + this.noPermissionOrSyntax(check, sender, root) ); } } diff --git a/cloud-core/src/main/java/org/incendo/cloud/setting/ManagerSetting.java b/cloud-core/src/main/java/org/incendo/cloud/setting/ManagerSetting.java index 234a0b489..55dc6ac71 100644 --- a/cloud-core/src/main/java/org/incendo/cloud/setting/ManagerSetting.java +++ b/cloud-core/src/main/java/org/incendo/cloud/setting/ManagerSetting.java @@ -61,5 +61,13 @@ public enum ManagerSetting implements Setting { * and code inspecting the command tree may need to be adjusted. */ @API(status = API.Status.EXPERIMENTAL) - LIBERAL_FLAG_PARSING + LIBERAL_FLAG_PARSING, + + /** + * When the sender does not have permission for the parsed command, but does have permission for a preceding or following + * node, the command tree will return a {@link org.incendo.cloud.exception.InvalidSyntaxException} instead of a + * {@link org.incendo.cloud.exception.NoPermissionException}. + */ + @API(status = API.Status.EXPERIMENTAL) + HIDE_COMMAND_EXISTENCE } diff --git a/cloud-core/src/test/java/org/incendo/cloud/PermissionTest.java b/cloud-core/src/test/java/org/incendo/cloud/PermissionTest.java index 3bd7b0364..571d50598 100644 --- a/cloud-core/src/test/java/org/incendo/cloud/PermissionTest.java +++ b/cloud-core/src/test/java/org/incendo/cloud/PermissionTest.java @@ -24,12 +24,14 @@ package org.incendo.cloud; import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Function; import org.checkerframework.checker.nullness.qual.NonNull; +import org.incendo.cloud.exception.InvalidSyntaxException; import org.incendo.cloud.exception.NoPermissionException; import org.incendo.cloud.execution.CommandResult; import org.incendo.cloud.execution.ExecutionCoordinator; @@ -37,10 +39,14 @@ import org.incendo.cloud.permission.Permission; import org.incendo.cloud.permission.PermissionResult; import org.incendo.cloud.permission.PredicatePermission; +import org.incendo.cloud.setting.ManagerSetting; import org.incendo.cloud.suggestion.Suggestion; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; @@ -54,7 +60,8 @@ @ExtendWith(MockitoExtension.class) class PermissionTest { - @Mock(strictness = Mock.Strictness.LENIENT) private Function permissionFunction; + @Mock(strictness = Mock.Strictness.LENIENT) + private Function permissionFunction; private CommandManager manager; @@ -117,9 +124,16 @@ void testSubCommandPermission() { assertThat(exception).hasCauseThat().isInstanceOf(NoPermissionException.class); } - @Test - void testPermittedNodeFollowingNotPermittedNode() { + @ParameterizedTest + @MethodSource + void testPermittedNodeFollowingNotPermittedNode( + final Class expectedException, + final List settings + ) { // Arrange + for (final ManagerSetting setting : settings) { + this.manager.settings().set(setting, true); + } this.manager.command(this.manager.commandBuilder("root") .literal("no") .permission("0")); @@ -136,10 +150,18 @@ void testPermittedNodeFollowingNotPermittedNode() { .executeCommand(sender, "root no yes"); // Assert - assertThat(noPermission).hasFailureThat().isInstanceOf(NoPermissionException.class); + assertThat(noPermission).hasFailureThat().isInstanceOf(expectedException); assertThat(permitted).hasResult(); } + @SuppressWarnings("unused") + private static List testPermittedNodeFollowingNotPermittedNode() { + return Arrays.asList( + Arguments.of(NoPermissionException.class, Collections.emptyList()), + Arguments.of(InvalidSyntaxException.class, Arrays.asList(ManagerSetting.HIDE_COMMAND_EXISTENCE)) + ); + } + @Test void testAndPermissionsMissingOne() { // Arrange