From b3738a7a63436af1b58d90bc1a761096efe30c74 Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Sun, 27 Aug 2023 12:15:55 +0100 Subject: [PATCH] Use permission APIs for the /computercraft command - Add a generic PermissionRegistry interface. This behaves similarly to our ShaderMod interface, searching all providers until it finds a compatible one. We could just make this part of the platform code instead, but this allows us to support multiple systems on Fabric, where things are less standardised. This interface behaves like a registry, rather than a straight `getPermission(node, player)` method, as Forge requires us to list our nodes up-front. - Add Forge (using the built-in system) and Fabric (using fabric-permissions-api) implementations of the above interface. - Register permission nodes for our commands, and use those instead. This does mean that the permissions check for the root /computercraft command now requires enumerating all child commands (and so potential does 7 permission lookups), but hopefully this isn't too bad in practice. - Remove UserLevel.OWNER - we never used this anywhere, and I can't imagine we'll want to in the future. --- .../cc-tweaked.java-convention.gradle.kts | 1 + gradle/libs.versions.toml | 4 +- .../computercraft/shared/ModRegistry.java | 17 ++++ .../shared/command/CommandComputerCraft.java | 37 ++++----- .../shared/command/CommandUtils.java | 5 +- .../shared/command/UserLevel.java | 45 +++-------- .../command/builder/CommandBuilder.java | 3 +- .../builder/HelpingArgumentBuilder.java | 22 ++---- .../integration/PermissionRegistry.java | 79 +++++++++++++++++++ .../integration/FabricPermissionRegistry.java | 40 ++++++++++ .../integration/ForgePermissionRegistry.java | 65 +++++++++++++++ 11 files changed, 249 insertions(+), 69 deletions(-) create mode 100644 projects/common/src/main/java/dan200/computercraft/shared/integration/PermissionRegistry.java create mode 100644 projects/fabric/src/main/java/dan200/computercraft/shared/integration/FabricPermissionRegistry.java create mode 100644 projects/forge/src/main/java/dan200/computercraft/shared/integration/ForgePermissionRegistry.java diff --git a/buildSrc/src/main/kotlin/cc-tweaked.java-convention.gradle.kts b/buildSrc/src/main/kotlin/cc-tweaked.java-convention.gradle.kts index 398d1e429..d349dc40e 100644 --- a/buildSrc/src/main/kotlin/cc-tweaked.java-convention.gradle.kts +++ b/buildSrc/src/main/kotlin/cc-tweaked.java-convention.gradle.kts @@ -66,6 +66,7 @@ repositories { includeGroup("me.shedaniel") includeGroup("mezz.jei") includeModule("com.terraformersmc", "modmenu") + includeModule("me.lucko", "fabric-permissions-api") } } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e0fa896a4..ca34fa6b5 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -34,6 +34,7 @@ slf4j = "1.7.36" # Minecraft mods emi = "1.0.8+1.19.4" +fabricPermissions = "0.2.20221016" iris = "1.5.2+1.19.4" jei = "13.1.0.11" modmenu = "6.1.0-rc.1" @@ -93,6 +94,7 @@ slf4j = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" } # Minecraft mods fabric-api = { module = "net.fabricmc.fabric-api:fabric-api", version.ref = "fabric-api" } fabric-loader = { module = "net.fabricmc:fabric-loader", version.ref = "fabric-loader" } +fabricPermissions = { module = "me.lucko:fabric-permissions-api", version.ref = "fabricPermissions" } emi = { module = "dev.emi:emi-xplat-mojmap", version.ref = "emi" } iris = { module = "maven.modrinth:iris", version.ref = "iris" } jei-api = { module = "mezz.jei:jei-1.19.4-common-api", version.ref = "jei" } @@ -154,7 +156,7 @@ externalMods-common = ["jei-api", "nightConfig-core", "nightConfig-toml"] externalMods-forge-compile = ["oculus", "jei-api"] externalMods-forge-runtime = ["jei-forge"] externalMods-fabric = ["nightConfig-core", "nightConfig-toml"] -externalMods-fabric-compile = ["iris", "jei-api", "rei-api", "rei-builtin"] +externalMods-fabric-compile = ["fabricPermissions", "iris", "jei-api", "rei-api", "rei-builtin"] externalMods-fabric-runtime = ["jei-fabric", "modmenu"] # Testing diff --git a/projects/common/src/main/java/dan200/computercraft/shared/ModRegistry.java b/projects/common/src/main/java/dan200/computercraft/shared/ModRegistry.java index a903eed6e..01248317e 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/ModRegistry.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/ModRegistry.java @@ -15,6 +15,7 @@ import dan200.computercraft.core.util.Colour; import dan200.computercraft.impl.PocketUpgrades; import dan200.computercraft.impl.TurtleUpgrades; +import dan200.computercraft.shared.command.UserLevel; import dan200.computercraft.shared.command.arguments.ComputerArgumentType; import dan200.computercraft.shared.command.arguments.ComputersArgumentType; import dan200.computercraft.shared.command.arguments.RepeatArgumentType; @@ -37,6 +38,7 @@ import dan200.computercraft.shared.data.PlayerCreativeLootCondition; import dan200.computercraft.shared.details.BlockDetails; import dan200.computercraft.shared.details.ItemDetails; +import dan200.computercraft.shared.integration.PermissionRegistry; import dan200.computercraft.shared.media.items.DiskItem; import dan200.computercraft.shared.media.items.PrintoutItem; import dan200.computercraft.shared.media.items.RecordMedia; @@ -77,6 +79,7 @@ import dan200.computercraft.shared.turtle.upgrades.*; import dan200.computercraft.shared.util.ImpostorRecipe; import dan200.computercraft.shared.util.ImpostorShapelessRecipe; +import net.minecraft.commands.CommandSourceStack; import net.minecraft.commands.synchronization.ArgumentTypeInfo; import net.minecraft.commands.synchronization.SingletonArgumentInfo; import net.minecraft.core.BlockPos; @@ -98,6 +101,7 @@ import net.minecraft.world.level.storage.loot.predicates.LootItemConditionType; import java.util.function.BiFunction; +import java.util.function.Predicate; /** * Registers ComputerCraft's registry entries and additional objects, such as {@link CauldronInteraction}s and @@ -366,6 +370,18 @@ private static RegistryEntry IMPOSTOR_SHAPELESS = REGISTRY.register("impostor_shapeless", ImpostorShapelessRecipe.Serializer::new); } + public static class Permissions { + static final PermissionRegistry REGISTRY = PermissionRegistry.create(); + + public static final Predicate PERMISSION_DUMP = REGISTRY.registerCommand("dump", UserLevel.OWNER_OP); + public static final Predicate PERMISSION_SHUTDOWN = REGISTRY.registerCommand("shutdown", UserLevel.OWNER_OP); + public static final Predicate PERMISSION_TURN_ON = REGISTRY.registerCommand("turn_on", UserLevel.OWNER_OP); + public static final Predicate PERMISSION_TP = REGISTRY.registerCommand("tp", UserLevel.OP); + public static final Predicate PERMISSION_TRACK = REGISTRY.registerCommand("track", UserLevel.OWNER_OP); + public static final Predicate PERMISSION_QUEUE = REGISTRY.registerCommand("queue", UserLevel.ANYONE); + public static final Predicate PERMISSION_VIEW = REGISTRY.registerCommand("view", UserLevel.OP); + } + /** * Register any objects which don't have to be done on the main thread. */ @@ -379,6 +395,7 @@ public static void register() { ArgumentTypes.REGISTRY.register(); LootItemConditionTypes.REGISTRY.register(); RecipeSerializers.REGISTRY.register(); + Permissions.REGISTRY.register(); // Register bundled power providers ComputerCraftAPI.registerBundledRedstoneProvider(new DefaultBundledRedstoneProvider()); diff --git a/projects/common/src/main/java/dan200/computercraft/shared/command/CommandComputerCraft.java b/projects/common/src/main/java/dan200/computercraft/shared/command/CommandComputerCraft.java index 93b96d069..77e36447f 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/command/CommandComputerCraft.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/command/CommandComputerCraft.java @@ -11,6 +11,7 @@ import com.mojang.brigadier.suggestion.Suggestions; import dan200.computercraft.core.computer.ComputerSide; import dan200.computercraft.core.metrics.Metrics; +import dan200.computercraft.shared.ModRegistry; import dan200.computercraft.shared.command.arguments.ComputersArgumentType; import dan200.computercraft.shared.command.text.TableBuilder; import dan200.computercraft.shared.computer.core.ComputerFamily; @@ -60,7 +61,7 @@ private CommandComputerCraft() { public static void register(CommandDispatcher dispatcher) { dispatcher.register(choice("computercraft") .then(literal("dump") - .requires(UserLevel.OWNER_OP) + .requires(ModRegistry.Permissions.PERMISSION_DUMP) .executes(context -> { var table = new TableBuilder("DumpAll", "Computer", "On", "Position"); @@ -118,7 +119,7 @@ public static void register(CommandDispatcher dispatcher) { }))) .then(command("shutdown") - .requires(UserLevel.OWNER_OP) + .requires(ModRegistry.Permissions.PERMISSION_SHUTDOWN) .argManyValue("computers", manyComputers(), s -> ServerContext.get(s.getServer()).registry().getComputers()) .executes((context, computerSelectors) -> { var shutdown = 0; @@ -132,7 +133,7 @@ public static void register(CommandDispatcher dispatcher) { })) .then(command("turn-on") - .requires(UserLevel.OWNER_OP) + .requires(ModRegistry.Permissions.PERMISSION_TURN_ON) .argManyValue("computers", manyComputers(), s -> ServerContext.get(s.getServer()).registry().getComputers()) .executes((context, computerSelectors) -> { var on = 0; @@ -146,7 +147,7 @@ public static void register(CommandDispatcher dispatcher) { })) .then(command("tp") - .requires(UserLevel.OP) + .requires(ModRegistry.Permissions.PERMISSION_TP) .arg("computer", oneComputer()) .executes(context -> { var computer = getComputerArgument(context, "computer"); @@ -171,7 +172,7 @@ public static void register(CommandDispatcher dispatcher) { })) .then(command("queue") - .requires(UserLevel.ANYONE) + .requires(ModRegistry.Permissions.PERMISSION_QUEUE) .arg( RequiredArgumentBuilder.argument("computer", manyComputers()) .suggests((context, builder) -> Suggestions.empty()) @@ -193,7 +194,7 @@ public static void register(CommandDispatcher dispatcher) { })) .then(command("view") - .requires(UserLevel.OP) + .requires(ModRegistry.Permissions.PERMISSION_VIEW) .arg("computer", oneComputer()) .executes(context -> { var player = context.getSource().getPlayerOrException(); @@ -213,8 +214,8 @@ public AbstractContainerMenu createMenu(int id, Inventory player, Player entity) })) .then(choice("track") + .requires(ModRegistry.Permissions.PERMISSION_TRACK) .then(command("start") - .requires(UserLevel.OWNER_OP) .executes(context -> { getMetricsInstance(context.getSource()).start(); @@ -227,7 +228,6 @@ public AbstractContainerMenu createMenu(int id, Inventory player, Player entity) })) .then(command("stop") - .requires(UserLevel.OWNER_OP) .executes(context -> { var timings = getMetricsInstance(context.getSource()); if (!timings.stop()) throw NOT_TRACKING_EXCEPTION.create(); @@ -236,7 +236,6 @@ public AbstractContainerMenu createMenu(int id, Inventory player, Player entity) })) .then(command("dump") - .requires(UserLevel.OWNER_OP) .argManyValue("fields", metric(), DEFAULT_FIELDS) .executes((context, fields) -> { AggregatedMetric sort; @@ -270,23 +269,25 @@ private static Component linkComputer(CommandSourceStack source, @Nullable Serve out.append(" (id " + computerId + ")"); // And, if we're a player, some useful links - if (serverComputer != null && UserLevel.OP.test(source) && isPlayer(source)) { - out - .append(" ") - .append(link( + if (serverComputer != null && isPlayer(source)) { + if (ModRegistry.Permissions.PERMISSION_TP.test(source)) { + out.append(" ").append(link( text("\u261b"), "/computercraft tp " + serverComputer.getInstanceID(), Component.translatable("commands.computercraft.tp.action") - )) - .append(" ") - .append(link( + )); + } + + if (ModRegistry.Permissions.PERMISSION_VIEW.test(source)) { + out.append(" ").append(link( text("\u20e2"), "/computercraft view " + serverComputer.getInstanceID(), Component.translatable("commands.computercraft.view.action") )); + } } - if (UserLevel.OWNER.test(source) && isPlayer(source)) { + if (isPlayer(source) && UserLevel.isOwner(source)) { var linkPath = linkStorage(source, computerId); if (linkPath != null) out.append(" ").append(linkPath); } @@ -295,7 +296,7 @@ private static Component linkComputer(CommandSourceStack source, @Nullable Serve } private static Component linkPosition(CommandSourceStack context, ServerComputer computer) { - if (UserLevel.OP.test(context)) { + if (ModRegistry.Permissions.PERMISSION_TP.test(context)) { return link( position(computer.getPosition()), "/computercraft tp " + computer.getInstanceID(), diff --git a/projects/common/src/main/java/dan200/computercraft/shared/command/CommandUtils.java b/projects/common/src/main/java/dan200/computercraft/shared/command/CommandUtils.java index 2494759c9..57d935dab 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/command/CommandUtils.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/command/CommandUtils.java @@ -10,7 +10,6 @@ import dan200.computercraft.shared.platform.PlatformHelper; import net.minecraft.commands.CommandSourceStack; import net.minecraft.commands.SharedSuggestionProvider; -import net.minecraft.server.level.ServerPlayer; import java.util.Arrays; import java.util.Locale; @@ -22,8 +21,8 @@ private CommandUtils() { } public static boolean isPlayer(CommandSourceStack output) { - var sender = output.getEntity(); - return sender instanceof ServerPlayer player && !PlatformHelper.get().isFakePlayer(player); + var player = output.getPlayer(); + return player != null && !PlatformHelper.get().isFakePlayer(player); } @SuppressWarnings("unchecked") diff --git a/projects/common/src/main/java/dan200/computercraft/shared/command/UserLevel.java b/projects/common/src/main/java/dan200/computercraft/shared/command/UserLevel.java index c8bdbb0f2..ab7709b90 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/command/UserLevel.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/command/UserLevel.java @@ -5,7 +5,7 @@ package dan200.computercraft.shared.command; import net.minecraft.commands.CommandSourceStack; -import net.minecraft.world.entity.player.Player; +import net.minecraft.server.level.ServerPlayer; import java.util.function.Predicate; @@ -13,11 +13,6 @@ * The level a user must be at in order to execute a command. */ public enum UserLevel implements Predicate { - /** - * Only can be used by the owner of the server: namely the server console or the player in SSP. - */ - OWNER, - /** * Can only be used by ops. */ @@ -35,7 +30,6 @@ public enum UserLevel implements Predicate { public int toLevel() { return switch (this) { - case OWNER -> 4; case OP, OWNER_OP -> 2; case ANYONE -> 0; }; @@ -44,39 +38,26 @@ public int toLevel() { @Override public boolean test(CommandSourceStack source) { if (this == ANYONE) return true; - if (this == OWNER) return isOwner(source); if (this == OWNER_OP && isOwner(source)) return true; return source.hasPermission(toLevel()); } - /** - * Take the union of two {@link UserLevel}s. - *

- * This satisfies the property that for all sources {@code s}, {@code a.test(s) || b.test(s) == (a ∪ b).test(s)}. - * - * @param left The first user level to take the union of. - * @param right The second user level to take the union of. - * @return The union of two levels. - */ - public static UserLevel union(UserLevel left, UserLevel right) { - if (left == right) return left; - - // x ∪ ANYONE = ANYONE - if (left == ANYONE || right == ANYONE) return ANYONE; - - // x ∪ OWNER = OWNER - if (left == OWNER) return right; - if (right == OWNER) return left; - - // At this point, we have x != y and x, y ∈ { OP, OWNER_OP }. - return OWNER_OP; + public boolean test(ServerPlayer source) { + if (this == ANYONE) return true; + if (this == OWNER_OP && isOwner(source)) return true; + return source.hasPermissions(toLevel()); } - private static boolean isOwner(CommandSourceStack source) { + public static boolean isOwner(CommandSourceStack source) { var server = source.getServer(); - var sender = source.getEntity(); + var player = source.getPlayer(); return server.isDedicatedServer() ? source.getEntity() == null && source.hasPermission(4) && source.getTextName().equals("Server") - : sender instanceof Player player && server.isSingleplayerOwner(player.getGameProfile()); + : player != null && server.isSingleplayerOwner(player.getGameProfile()); + } + + public static boolean isOwner(ServerPlayer player) { + var server = player.getServer(); + return server != null && server.isSingleplayerOwner(player.getGameProfile()); } } diff --git a/projects/common/src/main/java/dan200/computercraft/shared/command/builder/CommandBuilder.java b/projects/common/src/main/java/dan200/computercraft/shared/command/builder/CommandBuilder.java index 2a395ff7b..a150b8c88 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/command/builder/CommandBuilder.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/command/builder/CommandBuilder.java @@ -44,7 +44,8 @@ public static CommandBuilder command(String literal) { } public CommandBuilder requires(Predicate predicate) { - requires = requires == null ? predicate : requires.and(predicate); + if (requires != null) throw new IllegalStateException("Requires already set"); + requires = predicate; return this; } diff --git a/projects/common/src/main/java/dan200/computercraft/shared/command/builder/HelpingArgumentBuilder.java b/projects/common/src/main/java/dan200/computercraft/shared/command/builder/HelpingArgumentBuilder.java index dfe2fecc3..9bd797108 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/command/builder/HelpingArgumentBuilder.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/command/builder/HelpingArgumentBuilder.java @@ -10,7 +10,6 @@ import com.mojang.brigadier.context.CommandContext; import com.mojang.brigadier.tree.CommandNode; import com.mojang.brigadier.tree.LiteralCommandNode; -import dan200.computercraft.shared.command.UserLevel; import net.minecraft.ChatFormatting; import net.minecraft.commands.CommandSourceStack; import net.minecraft.network.chat.ClickEvent; @@ -31,6 +30,7 @@ */ public final class HelpingArgumentBuilder extends LiteralArgumentBuilder { private final Collection children = new ArrayList<>(); + private @Nullable Predicate requirement; private HelpingArgumentBuilder(String literal) { super(literal); @@ -41,26 +41,20 @@ public static HelpingArgumentBuilder choice(String literal) { } @Override - public LiteralArgumentBuilder requires(Predicate requirement) { - throw new IllegalStateException("Cannot use requires on a HelpingArgumentBuilder"); + public HelpingArgumentBuilder requires(Predicate requirement) { + this.requirement = requirement; + return this; } @Override public Predicate getRequirement() { - // The requirement of this node is the union of all child's requirements. + if (requirement != null) return requirement; + var requirements = Stream.concat( children.stream().map(ArgumentBuilder::getRequirement), getArguments().stream().map(CommandNode::getRequirement) ).toList(); - - // If all requirements are a UserLevel, take the union of those instead. - var userLevel = UserLevel.OWNER; - for (var requirement : requirements) { - if (!(requirement instanceof UserLevel level)) return x -> requirements.stream().anyMatch(y -> y.test(x)); - userLevel = UserLevel.union(userLevel, level); - } - - return userLevel; + return x -> requirements.stream().anyMatch(y -> y.test(x)); } @Override @@ -181,7 +175,7 @@ private static Component getHelp(CommandContext context, Com .append(Component.translatable("commands." + id + ".desc")); for (var child : node.getChildren()) { - if (!child.getRequirement().test(context.getSource()) || !(child instanceof LiteralCommandNode)) { + if (!child.canUse(context.getSource()) || !(child instanceof LiteralCommandNode)) { continue; } diff --git a/projects/common/src/main/java/dan200/computercraft/shared/integration/PermissionRegistry.java b/projects/common/src/main/java/dan200/computercraft/shared/integration/PermissionRegistry.java new file mode 100644 index 000000000..5360f4445 --- /dev/null +++ b/projects/common/src/main/java/dan200/computercraft/shared/integration/PermissionRegistry.java @@ -0,0 +1,79 @@ +// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers +// +// SPDX-License-Identifier: MPL-2.0 + +package dan200.computercraft.shared.integration; + +import com.mojang.brigadier.builder.ArgumentBuilder; +import dan200.computercraft.shared.command.CommandComputerCraft; +import dan200.computercraft.shared.command.UserLevel; +import dan200.computercraft.shared.platform.RegistrationHelper; +import net.minecraft.commands.CommandSourceStack; + +import javax.annotation.OverridingMethodsMustInvokeSuper; +import java.util.Optional; +import java.util.ServiceLoader; +import java.util.function.Predicate; + +/** + * A registry of nodes in a permission system. + *

+ * This acts as an abstraction layer over permission systems such Forge's built-in permissions API, or Fabric's + * unofficial fabric-permissions-api-v0. + *

+ * This behaves similarly to {@link RegistrationHelper} (aka Forge's deferred registry), in that you {@linkplain #create() + * create a registry}, {@linkplain #registerCommand(String, UserLevel) add nodes to it} and then finally {@linkplain + * #register()} all created nodes. + * + * @see dan200.computercraft.shared.ModRegistry.Permissions + */ +public abstract class PermissionRegistry { + private boolean frozen = false; + + /** + * Register a permission node for a command. The registered node should be of the form {@code "command." + command}. + * + * @param command The name of the command. This should be one of the subcommands under the {@code /computercraft} + * subcommand, and not something general. + * @param fallback The default/fallback permission check. + * @return The resulting predicate which should be passed to {@link ArgumentBuilder#requires(Predicate)}. + * @see CommandComputerCraft + */ + public abstract Predicate registerCommand(String command, UserLevel fallback); + + /** + * Check that the registry has not been frozen (namely {@link #register()} has been called). This should be called + * before registering each node. + */ + protected void checkNotFrozen() { + if (frozen) throw new IllegalStateException("Permission registry has been frozen."); + } + + /** + * Freeze the permissions registry and register the underlying nodes. + */ + @OverridingMethodsMustInvokeSuper + public void register() { + frozen = true; + } + + public interface Provider { + Optional get(); + } + + public static PermissionRegistry create() { + return ServiceLoader.load(Provider.class) + .stream() + .flatMap(x -> x.get().get().stream()) + .findFirst() + .orElseGet(DefaultPermissionRegistry::new); + } + + private static class DefaultPermissionRegistry extends PermissionRegistry { + @Override + public Predicate registerCommand(String command, UserLevel fallback) { + checkNotFrozen(); + return fallback; + } + } +} diff --git a/projects/fabric/src/main/java/dan200/computercraft/shared/integration/FabricPermissionRegistry.java b/projects/fabric/src/main/java/dan200/computercraft/shared/integration/FabricPermissionRegistry.java new file mode 100644 index 000000000..7d0705a54 --- /dev/null +++ b/projects/fabric/src/main/java/dan200/computercraft/shared/integration/FabricPermissionRegistry.java @@ -0,0 +1,40 @@ +// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers +// +// SPDX-License-Identifier: MPL-2.0 + +package dan200.computercraft.shared.integration; + +import com.google.auto.service.AutoService; +import dan200.computercraft.api.ComputerCraftAPI; +import dan200.computercraft.shared.command.UserLevel; +import me.lucko.fabric.api.permissions.v0.Permissions; +import net.fabricmc.loader.api.FabricLoader; +import net.minecraft.commands.CommandSourceStack; + +import java.util.Optional; +import java.util.function.Predicate; + +/** + * An implementation of {@link PermissionRegistry} using Fabric's unofficial {@linkplain Permissions permissions api}. + */ +public final class FabricPermissionRegistry extends PermissionRegistry { + private FabricPermissionRegistry() { + } + + @Override + public Predicate registerCommand(String command, UserLevel fallback) { + checkNotFrozen(); + var name = ComputerCraftAPI.MOD_ID + ".command." + command; + return source -> Permissions.getPermissionValue(source, name).orElseGet(() -> fallback.test(source)); + } + + @AutoService(PermissionRegistry.Provider.class) + public static final class Provider implements PermissionRegistry.Provider { + @Override + public Optional get() { + return FabricLoader.getInstance().isModLoaded("fabric-permissions-api-v0") + ? Optional.of(new FabricPermissionRegistry()) + : Optional.empty(); + } + } +} diff --git a/projects/forge/src/main/java/dan200/computercraft/shared/integration/ForgePermissionRegistry.java b/projects/forge/src/main/java/dan200/computercraft/shared/integration/ForgePermissionRegistry.java new file mode 100644 index 000000000..eaa734ea8 --- /dev/null +++ b/projects/forge/src/main/java/dan200/computercraft/shared/integration/ForgePermissionRegistry.java @@ -0,0 +1,65 @@ +// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers +// +// SPDX-License-Identifier: MPL-2.0 + +package dan200.computercraft.shared.integration; + +import com.google.auto.service.AutoService; +import dan200.computercraft.api.ComputerCraftAPI; +import dan200.computercraft.shared.command.UserLevel; +import net.minecraft.commands.CommandSourceStack; +import net.minecraftforge.common.MinecraftForge; +import net.minecraftforge.server.permission.PermissionAPI; +import net.minecraftforge.server.permission.events.PermissionGatherEvent; +import net.minecraftforge.server.permission.nodes.PermissionNode; +import net.minecraftforge.server.permission.nodes.PermissionType; +import net.minecraftforge.server.permission.nodes.PermissionTypes; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.function.Predicate; + +/** + * An implementation of {@link PermissionRegistry} using Forge's {@link PermissionAPI}. + */ +public final class ForgePermissionRegistry extends PermissionRegistry { + private final List> nodes = new ArrayList<>(); + + private ForgePermissionRegistry() { + } + + private PermissionNode registerNode(String nodeName, PermissionType type, PermissionNode.PermissionResolver defaultResolver) { + checkNotFrozen(); + var node = new PermissionNode<>(ComputerCraftAPI.MOD_ID, nodeName, type, defaultResolver); + nodes.add(node); + return node; + } + + @Override + public Predicate registerCommand(String command, UserLevel fallback) { + var node = registerNode( + "command." + command, PermissionTypes.BOOLEAN, + (player, uuid, context) -> player != null && fallback.test(player) + ); + + return source -> { + var player = source.getPlayer(); + return player == null ? fallback.test(source) : PermissionAPI.getPermission(player, node); + }; + } + + @Override + public void register() { + super.register(); + MinecraftForge.EVENT_BUS.addListener((PermissionGatherEvent.Nodes event) -> event.addNodes(nodes)); + } + + @AutoService(PermissionRegistry.Provider.class) + public static final class Provider implements PermissionRegistry.Provider { + @Override + public Optional get() { + return Optional.of(new ForgePermissionRegistry()); + } + } +}