From 5a100ef9886fae4495c58d01e4b156a2f2e444a6 Mon Sep 17 00:00:00 2001 From: Pierre Maurice Schwang Date: Sat, 31 May 2025 15:28:26 +0200 Subject: [PATCH] feat: inheritance-aware plot limits / permission ranges --- .../bukkit/inject/PermissionModule.java | 22 +++ .../BukkitRangedPermissionResolver.java | 107 +++++++++++++++ .../LuckPermsRangedPermissionResolver.java | 95 +++++++++++++ .../bukkit/player/BukkitPlayer.java | 70 +--------- .../com/plotsquared/core/PlotPlatform.java | 5 + .../core/configuration/Settings.java | 18 ++- .../permissions/RangedPermissionResolver.java | 126 ++++++++++++++++++ 7 files changed, 372 insertions(+), 71 deletions(-) create mode 100644 Bukkit/src/main/java/com/plotsquared/bukkit/permissions/BukkitRangedPermissionResolver.java create mode 100644 Bukkit/src/main/java/com/plotsquared/bukkit/permissions/LuckPermsRangedPermissionResolver.java create mode 100644 Core/src/main/java/com/plotsquared/core/permissions/RangedPermissionResolver.java diff --git a/Bukkit/src/main/java/com/plotsquared/bukkit/inject/PermissionModule.java b/Bukkit/src/main/java/com/plotsquared/bukkit/inject/PermissionModule.java index a2a8c50f9..7c29f7713 100644 --- a/Bukkit/src/main/java/com/plotsquared/bukkit/inject/PermissionModule.java +++ b/Bukkit/src/main/java/com/plotsquared/bukkit/inject/PermissionModule.java @@ -22,12 +22,20 @@ import com.google.inject.AbstractModule; import com.google.inject.Provides; import com.google.inject.Singleton; import com.plotsquared.bukkit.permissions.BukkitPermissionHandler; +import com.plotsquared.bukkit.permissions.BukkitRangedPermissionResolver; +import com.plotsquared.bukkit.permissions.LuckPermsRangedPermissionResolver; import com.plotsquared.bukkit.permissions.VaultPermissionHandler; +import com.plotsquared.core.configuration.Settings; import com.plotsquared.core.permissions.PermissionHandler; +import com.plotsquared.core.permissions.RangedPermissionResolver; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.bukkit.Bukkit; public class PermissionModule extends AbstractModule { + private static final Logger LOGGER = LogManager.getLogger("PlotSquared/" + PermissionModule.class.getSimpleName()); + @Provides @Singleton PermissionHandler providePermissionHandler() { @@ -40,4 +48,18 @@ public class PermissionModule extends AbstractModule { return new BukkitPermissionHandler(); } + @Provides + @Singleton + RangedPermissionResolver provideRangedPermissionResolver() { + if (Settings.Permissions.USE_LUCKPERMS_RANGE_RESOLVER) { + if (Bukkit.getPluginManager().isPluginEnabled("LuckPerms")) { + LOGGER.info("Using experimental LuckPerms ranged permission resolver"); + return new LuckPermsRangedPermissionResolver(); + } + LOGGER.warn("Enabled LuckPerms ranged permission resolver, but LuckPerms is not installed. " + + "Falling back to default Bukkit ranged permission resolver"); + } + return new BukkitRangedPermissionResolver(); + } + } diff --git a/Bukkit/src/main/java/com/plotsquared/bukkit/permissions/BukkitRangedPermissionResolver.java b/Bukkit/src/main/java/com/plotsquared/bukkit/permissions/BukkitRangedPermissionResolver.java new file mode 100644 index 000000000..22349f952 --- /dev/null +++ b/Bukkit/src/main/java/com/plotsquared/bukkit/permissions/BukkitRangedPermissionResolver.java @@ -0,0 +1,107 @@ +/* + * PlotSquared, a land and world management plugin for Minecraft. + * Copyright (C) IntellectualSites + * Copyright (C) IntellectualSites team and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.plotsquared.bukkit.permissions; + +import com.plotsquared.bukkit.player.BukkitPlayer; +import com.plotsquared.core.permissions.RangedPermissionResolver; +import com.plotsquared.core.player.PlotPlayer; +import com.plotsquared.core.util.MathMan; +import org.bukkit.permissions.PermissionAttachmentInfo; +import org.checkerframework.checker.index.qual.NonNegative; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; + +import java.util.Set; +import java.util.stream.IntStream; + +public class BukkitRangedPermissionResolver implements RangedPermissionResolver { + + private static boolean CHECK_EFFECTIVE = true; + + @Override + public @NonNegative int getPermissionRange( + final @NonNull PlotPlayer generic, + final @NonNull String stub, + final @Nullable String worldContext, + @NonNegative final int range + ) { + if (!(generic instanceof BukkitPlayer player)) { + throw new IllegalArgumentException("PlotPlayer is not a BukkitPlayer"); + } + if (hasWildcardRange(player, stub, worldContext)) { + return INFINITE_RANGE_VALUE; + } + int max = 0; + if (CHECK_EFFECTIVE) { + boolean hasAny = false; + String stubPlus = stub + "."; + final Set effective = player.getPlatformPlayer().getEffectivePermissions(); + if (!effective.isEmpty()) { + for (PermissionAttachmentInfo attach : effective) { + // Ignore all "false" permissions + if (!attach.getValue()) { + continue; + } + String permStr = attach.getPermission(); + if (permStr.startsWith(stubPlus)) { + hasAny = true; + String end = permStr.substring(stubPlus.length()); + if (MathMan.isInteger(end)) { + int val = Integer.parseInt(end); + if (val > range) { + return val; + } + if (val > max) { + max = val; + } + } + } + } + if (hasAny) { + return max; + } + // Workaround + for (PermissionAttachmentInfo attach : effective) { + String permStr = attach.getPermission(); + if (permStr.startsWith("plots.") && !permStr.equals("plots.use")) { + return max; + } + } + CHECK_EFFECTIVE = false; + } + } + for (int i = range; i > 0; i--) { + if (player.hasPermission(worldContext, stub + "." + i)) { + return i; + } + } + return max; + } + + @Override + public @NonNull IntStream streamFullPermissionRange( + final @NonNull PlotPlayer player, + final @NonNull String stub, + final @Nullable String worldContext, + @NonNegative final int range + ) { + return IntStream.of(getPermissionRange(player, stub, worldContext, range)); + } + +} diff --git a/Bukkit/src/main/java/com/plotsquared/bukkit/permissions/LuckPermsRangedPermissionResolver.java b/Bukkit/src/main/java/com/plotsquared/bukkit/permissions/LuckPermsRangedPermissionResolver.java new file mode 100644 index 000000000..d589ce251 --- /dev/null +++ b/Bukkit/src/main/java/com/plotsquared/bukkit/permissions/LuckPermsRangedPermissionResolver.java @@ -0,0 +1,95 @@ +/* + * PlotSquared, a land and world management plugin for Minecraft. + * Copyright (C) IntellectualSites + * Copyright (C) IntellectualSites team and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.plotsquared.bukkit.permissions; + +import com.plotsquared.core.permissions.RangedPermissionResolver; +import com.plotsquared.core.player.PlotPlayer; +import com.plotsquared.core.util.MathMan; +import net.luckperms.api.LuckPerms; +import net.luckperms.api.context.ImmutableContextSet; +import net.luckperms.api.model.user.User; +import net.luckperms.api.node.NodeType; +import net.luckperms.api.node.types.PermissionNode; +import net.luckperms.api.query.QueryOptions; +import org.bukkit.Bukkit; +import org.checkerframework.checker.index.qual.NonNegative; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; + +import java.util.Objects; +import java.util.stream.IntStream; + +public class LuckPermsRangedPermissionResolver implements RangedPermissionResolver { + + private final LuckPerms luckPerms; + + public LuckPermsRangedPermissionResolver() { + this.luckPerms = Objects.requireNonNull( + Bukkit.getServicesManager().getRegistration(LuckPerms.class), + "LuckPerms is not available" + ).getProvider(); + } + + @Override + public @NonNegative int getPermissionRange( + final @NonNull PlotPlayer player, + final @NonNull String stub, + final @Nullable String worldContext, + final @NonNegative int range + ) { + // no need to use LuckPerms for basic checks + if (this.hasWildcardRange(player, stub, worldContext)) { + return INFINITE_RANGE_VALUE; + } + return this.streamFullPermissionRange(player, stub, worldContext, range) + .sorted() + .reduce((first, second) -> second) + .orElse(0); + } + + @NonNull + public IntStream streamFullPermissionRange( + final @NonNull PlotPlayer player, + final @NonNull String stub, + final @Nullable String worldContext, + @NonNegative final int range + ) { + final User user = this.luckPerms.getUserManager().getUser(player.getUUID()); + if (user == null) { + throw new IllegalStateException("Luckperms User is null - is the Player online? (UUID: %s)".formatted(player.getUUID())); + } + final QueryOptions queryOptions = worldContext == null ? + QueryOptions.nonContextual() : + QueryOptions.contextual(ImmutableContextSet.of("world", worldContext)); + return user.resolveInheritedNodes(queryOptions).stream() + // only support normal permission nodes (regex permission nodes would be a pita to support) + .filter(NodeType.PERMISSION::matches) + .map(node -> ((PermissionNode) node).getPermission()) + // check that the node actually has additional data after the stub + .filter(permission -> permission.startsWith(stub + ".")) + // extract the raw data after the stub + .map(permission -> permission.substring(stub.length() + 1)) + // check if data is integer and parse + .filter(MathMan::isInteger) + .mapToInt(Integer::parseInt) + // only use values that are positive + .filter(value -> value > -1); + } + +} diff --git a/Bukkit/src/main/java/com/plotsquared/bukkit/player/BukkitPlayer.java b/Bukkit/src/main/java/com/plotsquared/bukkit/player/BukkitPlayer.java index bd9c99287..25dda34fb 100644 --- a/Bukkit/src/main/java/com/plotsquared/bukkit/player/BukkitPlayer.java +++ b/Bukkit/src/main/java/com/plotsquared/bukkit/player/BukkitPlayer.java @@ -24,14 +24,12 @@ import com.plotsquared.core.PlotSquared; import com.plotsquared.core.configuration.Settings; import com.plotsquared.core.events.TeleportCause; import com.plotsquared.core.location.Location; -import com.plotsquared.core.permissions.Permission; import com.plotsquared.core.permissions.PermissionHandler; import com.plotsquared.core.player.ConsolePlayer; import com.plotsquared.core.player.PlotPlayer; import com.plotsquared.core.plot.PlotWeather; import com.plotsquared.core.plot.world.PlotAreaManager; import com.plotsquared.core.util.EventDispatcher; -import com.plotsquared.core.util.MathMan; import com.plotsquared.core.util.WorldUtil; import com.sk89q.worldedit.bukkit.BukkitAdapter; import com.sk89q.worldedit.extension.platform.Actor; @@ -47,13 +45,11 @@ import org.bukkit.entity.Player; import org.bukkit.event.Event; import org.bukkit.event.EventException; import org.bukkit.event.player.PlayerTeleportEvent; -import org.bukkit.permissions.PermissionAttachmentInfo; import org.bukkit.plugin.RegisteredListener; import org.bukkit.potion.PotionEffectType; import org.checkerframework.checker.index.qual.NonNegative; import org.checkerframework.checker.nullness.qual.NonNull; -import java.util.Set; import java.util.UUID; import static com.sk89q.worldedit.world.gamemode.GameModes.ADVENTURE; @@ -150,77 +146,13 @@ public class BukkitPlayer extends PlotPlayer { } } - @SuppressWarnings("StringSplitter") @Override @NonNegative public int hasPermissionRange( final @NonNull String stub, @NonNegative final int range ) { - if (hasPermission(Permission.PERMISSION_ADMIN.toString())) { - return Integer.MAX_VALUE; - } - final String[] nodes = stub.split("\\."); - final StringBuilder n = new StringBuilder(); - // Wildcard check from less specific permission to more specific permission - for (int i = 0; i < (nodes.length - 1); i++) { - n.append(nodes[i]).append("."); - if (!stub.equals(n + Permission.PERMISSION_STAR.toString())) { - if (hasPermission(n + Permission.PERMISSION_STAR.toString())) { - return Integer.MAX_VALUE; - } - } - } - // Wildcard check for the full permission - if (hasPermission(stub + ".*")) { - return Integer.MAX_VALUE; - } - // Permission value cache for iterative check - int max = 0; - if (CHECK_EFFECTIVE) { - boolean hasAny = false; - String stubPlus = stub + "."; - final Set effective = player.getEffectivePermissions(); - if (!effective.isEmpty()) { - for (PermissionAttachmentInfo attach : effective) { - // Ignore all "false" permissions - if (!attach.getValue()) { - continue; - } - String permStr = attach.getPermission(); - if (permStr.startsWith(stubPlus)) { - hasAny = true; - String end = permStr.substring(stubPlus.length()); - if (MathMan.isInteger(end)) { - int val = Integer.parseInt(end); - if (val > range) { - return val; - } - if (val > max) { - max = val; - } - } - } - } - if (hasAny) { - return max; - } - // Workaround - for (PermissionAttachmentInfo attach : effective) { - String permStr = attach.getPermission(); - if (permStr.startsWith("plots.") && !permStr.equals("plots.use")) { - return max; - } - } - CHECK_EFFECTIVE = false; - } - } - for (int i = range; i > 0; i--) { - if (hasPermission(stub + "." + i)) { - return i; - } - } - return max; + return PlotSquared.platform().rangedPermissionResolver().getPermissionRange(this, stub, null, range); } @Override diff --git a/Core/src/main/java/com/plotsquared/core/PlotPlatform.java b/Core/src/main/java/com/plotsquared/core/PlotPlatform.java index 844d553b6..c73a46623 100644 --- a/Core/src/main/java/com/plotsquared/core/PlotPlatform.java +++ b/Core/src/main/java/com/plotsquared/core/PlotPlatform.java @@ -31,6 +31,7 @@ import com.plotsquared.core.generator.IndependentPlotGenerator; import com.plotsquared.core.inject.annotations.DefaultGenerator; import com.plotsquared.core.location.World; import com.plotsquared.core.permissions.PermissionHandler; +import com.plotsquared.core.permissions.RangedPermissionResolver; import com.plotsquared.core.player.PlotPlayer; import com.plotsquared.core.plot.expiration.ExpireManager; import com.plotsquared.core.plot.world.PlotAreaManager; @@ -349,6 +350,10 @@ public interface PlotPlatform

extends LocaleHolder { return injector().getInstance(PermissionHandler.class); } + default @NonNull RangedPermissionResolver rangedPermissionResolver() { + return injector().getInstance(RangedPermissionResolver.class); + } + /** * Get the {@link ServicePipeline} implementation * diff --git a/Core/src/main/java/com/plotsquared/core/configuration/Settings.java b/Core/src/main/java/com/plotsquared/core/configuration/Settings.java index 04e88a40c..190f7c1fb 100644 --- a/Core/src/main/java/com/plotsquared/core/configuration/Settings.java +++ b/Core/src/main/java/com/plotsquared/core/configuration/Settings.java @@ -525,8 +525,8 @@ public class Settings extends Config { public static final class Limit { @Comment("Should the limit be global (over multiple worlds)") - public static boolean GLOBAL = - false; + public static boolean GLOBAL = false; + @Comment({"The max range of integer permissions to check for, e.g. 'plots.plot.127' or 'plots.set.flag.mob-cap.127'", "The value covers the permission range to check, you need to assign the permission to players/groups still", "Modifying the value does NOT change the amount of plots players can claim"}) @@ -737,6 +737,7 @@ public class Settings extends Config { @Comment("If \"instabreak\" should consider the used tool.") public static boolean INSTABREAK_CONSIDER_TOOL = false; + } @Comment({"Enable or disable parts of the plugin", @@ -830,4 +831,17 @@ public class Settings extends Config { } + @Comment({"Permission-Resolver specific settings"}) + public static final class Permissions { + + @Comment({ + "use the new LuckPerms resolver for ranged permissions (e.g., plots.plot.).", + "in contrary to the default resolver, this resolver can handle inheritance of permissions.", + "e.g. if the player has multiple matching permissions, for example, due to multiple group memberships " + + "(plots.plot.5 & plots.plot.15), this resolver would use 15 as the limit as opposed to the previous 5." + }) + public static boolean USE_LUCKPERMS_RANGE_RESOLVER = false; + + } + } diff --git a/Core/src/main/java/com/plotsquared/core/permissions/RangedPermissionResolver.java b/Core/src/main/java/com/plotsquared/core/permissions/RangedPermissionResolver.java new file mode 100644 index 000000000..91aa592f8 --- /dev/null +++ b/Core/src/main/java/com/plotsquared/core/permissions/RangedPermissionResolver.java @@ -0,0 +1,126 @@ +/* + * PlotSquared, a land and world management plugin for Minecraft. + * Copyright (C) IntellectualSites + * Copyright (C) IntellectualSites team and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.plotsquared.core.permissions; + +import com.plotsquared.core.player.PlotPlayer; +import org.checkerframework.checker.index.qual.NonNegative; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; + +import java.util.stream.IntStream; + +/** + * Represents a resolver for ranged permissions. Return values depend on the actual implementation (see Bukkit module). + *
+ * Even though this interface is not linked to platform implementations by design, implementation-specific details are added to + * the Javadocs. + * + * @since TODO + */ +public interface RangedPermissionResolver { + + int INFINITE_RANGE_VALUE = Integer.MAX_VALUE; + + /** + * Gets the applicable range value of a player for a specific permission stub + * ({@code plots.plot} would check for {@code plots.plot.}). + *
+ * The standard bukkit implementation would return the lowest numeric value, while the LuckPerms specific resolver would + * try returning the highest possible value. + * + * @param player the permission holder + * @param stub the permission stub to check against + * @param worldContext the optional world context of the action requiring the range + * @param range the maximum permission range to check against (for the default bukkit resolver) + * @return the applicable range value of the player for the given permission stub + * @since TODO + */ + @NonNegative + int getPermissionRange( + final @NonNull PlotPlayer player, + final @NonNull String stub, + final @Nullable String worldContext, + final @NonNegative int range + ); + + /** + * Gets a stream of all applicable permission range values for the given stub. The stream is unordered by default. If a + * specific order is needed, use the stateful {@link IntStream#sorted()} operation. + *
+ * The standard bukkit implementation will only return a stream containing a single value equal to + * {@link #getPermissionRange(PlotPlayer, String, String, int)}. For LuckPerms, all applicable node values will be in the + * stream. + * + * @param player the permission holder + * @param stub the permission stub to check against + * @param worldContext the optional world context of the action requiring the range + * @param range the maximum permission range to check against (for the default bukkit resolver) + * @return a stream of all applicable permission range values for the given stub + * @since TODO + */ + @NonNull + IntStream streamFullPermissionRange( + final @NonNull PlotPlayer player, + final @NonNull String stub, + final @Nullable String worldContext, + final @NonNegative int range + ); + + /** + * Checks if the given player has a wildcard range for the given permission stub. + *
+ * For example, if checking for the stub {@code plots.plot}, this method would check for: + *

    + *
  • {@code *}
  • + *
  • {@code plots.admin}
  • + *
  • {@code plots.plot.*}
  • + *
  • {@code plots.*}
  • + *
+ * + * @param player the permission holder + * @param stub the permission stub to check against + * @param worldContext the optional world context of the action requiring the range + * @return {@code true} if the player has a wildcard range for the given permission stub, else {@code false} + * @since TODO + */ + default boolean hasWildcardRange( + final @NonNull PlotPlayer player, + final @NonNull String stub, + final @Nullable String worldContext + ) { + if (player.hasPermission(Permission.PERMISSION_STAR) || + player.hasPermission(Permission.PERMISSION_ADMIN) || + player.hasPermission(worldContext, stub + ".*")) { + return true; + } + String node = stub; + while (true) { + int lastIndex = node.lastIndexOf('.'); + if (lastIndex == -1) { + break; + } + node = node.substring(0, lastIndex); + if (player.hasPermission(worldContext, node + ".*")) { + return true; + } + } + return false; + } + +}