From 0b6a55494696d7371d29ed082b7684e64043a625 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20S=C3=B6derberg?= <4096670+Citymonstret@users.noreply.github.com> Date: Wed, 13 Jan 2021 13:23:30 +0100 Subject: [PATCH] :sparkles: Implement predicate permissions (#210) Co-authored-by: Josh Taylor --- CHANGELOG.md | 1 + .../java/cloud/commandframework/Command.java | 19 +++++ .../commandframework/CommandManager.java | 5 ++ .../permission/PredicatePermission.java | 75 +++++++++++++++++++ .../WrappingPredicatePermission.java | 59 +++++++++++++++ .../CommandPermissionTest.java | 19 +++++ .../examples/bukkit/ExamplePlugin.java | 34 +++++++++ 7 files changed, 212 insertions(+) create mode 100644 cloud-core/src/main/java/cloud/commandframework/permission/PredicatePermission.java create mode 100644 cloud-core/src/main/java/cloud/commandframework/permission/WrappingPredicatePermission.java diff --git a/CHANGELOG.md b/CHANGELOG.md index e85510cc..07b97cad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Expose the Command which led to `InvalidCommandSenderException`s - Expose the CommandContext which led to `CommandExecutionException`s - Added helper methods for command flags to MutableCommandBuilder + - Add predicate permissions ## [1.3.0] - 2020-12-18 diff --git a/cloud-core/src/main/java/cloud/commandframework/Command.java b/cloud-core/src/main/java/cloud/commandframework/Command.java index 0e076647..a06a0d99 100644 --- a/cloud-core/src/main/java/cloud/commandframework/Command.java +++ b/cloud-core/src/main/java/cloud/commandframework/Command.java @@ -34,6 +34,7 @@ import cloud.commandframework.meta.CommandMeta; import cloud.commandframework.meta.SimpleCommandMeta; import cloud.commandframework.permission.CommandPermission; import cloud.commandframework.permission.Permission; +import cloud.commandframework.permission.PredicatePermission; import cloud.commandframework.types.tuples.Pair; import cloud.commandframework.types.tuples.Triplet; import io.leangen.geantyref.TypeToken; @@ -1032,6 +1033,24 @@ public class Command { ); } + /** + * Specify a command permission + * + * @param permission Command permission + * @return New builder instance using the command permission + */ + public @NonNull Builder permission(final @NonNull PredicatePermission permission) { + return new Builder<>( + this.commandManager, + this.commandMeta, + this.senderType, + this.commandComponents, + this.commandExecutionHandler, + permission, + this.flags + ); + } + /** * Specify a command permission * diff --git a/cloud-core/src/main/java/cloud/commandframework/CommandManager.java b/cloud-core/src/main/java/cloud/commandframework/CommandManager.java index 4d1565db..cc142263 100644 --- a/cloud-core/src/main/java/cloud/commandframework/CommandManager.java +++ b/cloud-core/src/main/java/cloud/commandframework/CommandManager.java @@ -55,6 +55,7 @@ import cloud.commandframework.meta.CommandMeta; import cloud.commandframework.permission.CommandPermission; import cloud.commandframework.permission.OrPermission; import cloud.commandframework.permission.Permission; +import cloud.commandframework.permission.PredicatePermission; import cloud.commandframework.services.ServicePipeline; import cloud.commandframework.services.State; import io.leangen.geantyref.TypeToken; @@ -277,6 +278,7 @@ public abstract class CommandManager { * @param permission Permission node * @return {@code true} if the sender has the permission, else {@code false} */ + @SuppressWarnings("unchecked") public boolean hasPermission( final @NonNull C sender, final @NonNull CommandPermission permission @@ -287,6 +289,9 @@ public abstract class CommandManager { if (permission instanceof Permission) { return hasPermission(sender, permission.toString()); } + if (permission instanceof PredicatePermission) { + return ((PredicatePermission) permission).hasPermission(sender); + } for (final CommandPermission innerPermission : permission.getPermissions()) { final boolean hasPermission = this.hasPermission(sender, innerPermission); if (permission instanceof OrPermission) { diff --git a/cloud-core/src/main/java/cloud/commandframework/permission/PredicatePermission.java b/cloud-core/src/main/java/cloud/commandframework/permission/PredicatePermission.java new file mode 100644 index 00000000..a3f2470c --- /dev/null +++ b/cloud-core/src/main/java/cloud/commandframework/permission/PredicatePermission.java @@ -0,0 +1,75 @@ +// +// MIT License +// +// Copyright (c) 2020 Alexander Söderberg & Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +package cloud.commandframework.permission; + +import cloud.commandframework.keys.CloudKey; +import cloud.commandframework.keys.CloudKeyHolder; +import cloud.commandframework.keys.SimpleCloudKey; +import org.checkerframework.checker.nullness.qual.NonNull; + +import java.util.Collection; +import java.util.Collections; +import java.util.function.Predicate; + +/** + * A functional {@link CommandPermission} implementation + * + * @param Command sender type + * @since 1.4.0 + */ +@FunctionalInterface +public interface PredicatePermission extends CommandPermission, CloudKeyHolder { + + /** + * Create a new predicate permission + * + * @param key Key that identifies the permission node + * @param predicate Predicate that determines whether or not the sender has the permission + * @param Command sender type + * @return Created permission node + */ + static PredicatePermission of(final @NonNull CloudKey key, final @NonNull Predicate predicate) { + return new WrappingPredicatePermission<>(key, predicate); + } + + @Override + @SuppressWarnings("FunctionalInterfaceMethodChanged") + default @NonNull CloudKey getKey() { + return SimpleCloudKey.of(this.getClass().getSimpleName()); + } + + /** + * Check whether or not the given sender has this permission + * + * @param sender Sender to check for + * @return {@code true} if the sender has the given permission, else {@code false} + */ + boolean hasPermission(C sender); + + @Override + default @NonNull Collection<@NonNull CommandPermission> getPermissions() { + return Collections.singleton(this); + } + +} diff --git a/cloud-core/src/main/java/cloud/commandframework/permission/WrappingPredicatePermission.java b/cloud-core/src/main/java/cloud/commandframework/permission/WrappingPredicatePermission.java new file mode 100644 index 00000000..32b9a876 --- /dev/null +++ b/cloud-core/src/main/java/cloud/commandframework/permission/WrappingPredicatePermission.java @@ -0,0 +1,59 @@ +// +// MIT License +// +// Copyright (c) 2020 Alexander Söderberg & Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +package cloud.commandframework.permission; + +import cloud.commandframework.keys.CloudKey; +import org.checkerframework.checker.nullness.qual.NonNull; + +import java.util.function.Predicate; + +final class WrappingPredicatePermission implements PredicatePermission { + + private final CloudKey key; + private final Predicate predicate; + + WrappingPredicatePermission( + final @NonNull CloudKey key, + final @NonNull Predicate predicate + ) { + this.key = key; + this.predicate = predicate; + } + + @Override + public boolean hasPermission(final C sender) { + return this.predicate.test(sender); + } + + @Override + public @NonNull CloudKey getKey() { + return this.key; + } + + @Override + public String toString() { + return this.key.getName(); + } + +} diff --git a/cloud-core/src/test/java/cloud/commandframework/CommandPermissionTest.java b/cloud-core/src/test/java/cloud/commandframework/CommandPermissionTest.java index b94df642..545a5b05 100644 --- a/cloud-core/src/test/java/cloud/commandframework/CommandPermissionTest.java +++ b/cloud-core/src/test/java/cloud/commandframework/CommandPermissionTest.java @@ -25,13 +25,16 @@ package cloud.commandframework; import cloud.commandframework.arguments.standard.IntegerArgument; import cloud.commandframework.execution.CommandExecutionCoordinator; +import cloud.commandframework.keys.SimpleCloudKey; import cloud.commandframework.meta.CommandMeta; import cloud.commandframework.meta.SimpleCommandMeta; +import cloud.commandframework.permission.PredicatePermission; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import java.util.concurrent.CompletionException; +import java.util.concurrent.atomic.AtomicBoolean; class CommandPermissionTest { @@ -64,6 +67,22 @@ class CommandPermissionTest { ); } + @Test + void testPredicatePermissions() { + final AtomicBoolean condition = new AtomicBoolean(true); + manager.command(manager.commandBuilder("predicate").permission(PredicatePermission.of( + SimpleCloudKey.of("boolean"), $ -> condition.get() + ))); + // First time should succeed + manager.executeCommand(new TestCommandSender(), "predicate").join(); + // Now we force it to fail + condition.set(false); + Assertions.assertThrows( + CompletionException.class, + () -> manager.executeCommand(new TestCommandSender(), "predicate").join() + ); + } + private static final class PermissionOutputtingCommandManager extends CommandManager { diff --git a/examples/example-bukkit/src/main/java/cloud/commandframework/examples/bukkit/ExamplePlugin.java b/examples/example-bukkit/src/main/java/cloud/commandframework/examples/bukkit/ExamplePlugin.java index e9b2d5f1..ab55228d 100644 --- a/examples/example-bukkit/src/main/java/cloud/commandframework/examples/bukkit/ExamplePlugin.java +++ b/examples/example-bukkit/src/main/java/cloud/commandframework/examples/bukkit/ExamplePlugin.java @@ -26,6 +26,8 @@ package cloud.commandframework.examples.bukkit; import cloud.commandframework.ArgumentDescription; import cloud.commandframework.Command; import cloud.commandframework.CommandTree; +import cloud.commandframework.Description; +import cloud.commandframework.keys.SimpleCloudKey; import cloud.commandframework.minecraft.extras.MinecraftExceptionHandler; import cloud.commandframework.minecraft.extras.MinecraftHelp; import cloud.commandframework.annotations.AnnotationParser; @@ -59,6 +61,7 @@ import cloud.commandframework.meta.CommandMeta; import cloud.commandframework.minecraft.extras.RichDescription; import cloud.commandframework.minecraft.extras.TextColorArgument; import cloud.commandframework.paper.PaperCommandManager; +import cloud.commandframework.permission.PredicatePermission; import cloud.commandframework.tasks.TaskConsumer; import cloud.commandframework.types.tuples.Triplet; import io.leangen.geantyref.TypeToken; @@ -70,6 +73,7 @@ import net.kyori.adventure.text.format.TextDecoration; import org.apache.commons.lang.StringUtils; import org.bukkit.Bukkit; import org.bukkit.ChatColor; +import org.bukkit.GameMode; import org.bukkit.Location; import org.bukkit.Material; import org.bukkit.World; @@ -84,6 +88,10 @@ import org.bukkit.util.Vector; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; import java.util.Collections; import java.util.List; import java.util.concurrent.TimeUnit; @@ -213,6 +221,15 @@ public final class ExamplePlugin extends JavaPlugin { } private void constructCommands() { + // Add a custom permission checker + this.annotationParser.registerBuilderModifier( + GameModeRequirement.class, + (requirement, builder) -> builder.permission( + PredicatePermission.of(SimpleCloudKey.of("gamemode"), sender -> + !(sender instanceof Player) || ((Player) sender).getGameMode() == requirement.value() + ) + ) + ); // // Parse all @CommandMethod-annotated methods // @@ -470,4 +487,21 @@ public final class ExamplePlugin extends JavaPlugin { .execute(() -> sender.sendMessage("You have been teleported!")); } + + /** + * Command must have the given game mode + */ + @Target(ElementType.METHOD) + @Retention(RetentionPolicy.RUNTIME) + public @interface GameModeRequirement { + + /** + * The required game mode + * + * @return Required game mode + */ + GameMode value(); + + } + }