From a2801bff7ceca99b15fe0aa0e7e2c92516170644 Mon Sep 17 00:00:00 2001 From: Luck Date: Tue, 7 Nov 2017 17:05:16 +0000 Subject: [PATCH] Inject a fake map into the super PermissionAttachment instance to fix compat with plugins using reflection to change attachment data (#528) --- .../bukkit/model/LPPermissionAttachment.java | 230 +++++++++++++++++- 1 file changed, 217 insertions(+), 13 deletions(-) diff --git a/bukkit/src/main/java/me/lucko/luckperms/bukkit/model/LPPermissionAttachment.java b/bukkit/src/main/java/me/lucko/luckperms/bukkit/model/LPPermissionAttachment.java index 79f87e68..82e8ca88 100644 --- a/bukkit/src/main/java/me/lucko/luckperms/bukkit/model/LPPermissionAttachment.java +++ b/bukkit/src/main/java/me/lucko/luckperms/bukkit/model/LPPermissionAttachment.java @@ -28,17 +28,25 @@ package me.lucko.luckperms.bukkit.model; import lombok.Getter; import lombok.Setter; +import com.google.common.base.Preconditions; + +import me.lucko.luckperms.api.Node; import me.lucko.luckperms.common.config.ConfigKeys; +import me.lucko.luckperms.common.model.User; import me.lucko.luckperms.common.node.ImmutableTransientNode; import me.lucko.luckperms.common.node.NodeFactory; +import org.bukkit.permissions.PermissibleBase; import org.bukkit.permissions.PermissionAttachment; import org.bukkit.permissions.PermissionRemovedExecutor; import org.bukkit.plugin.Plugin; +import java.lang.reflect.Field; +import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.Map; +import java.util.Set; /** * PermissionAttachment for LuckPerms. @@ -47,6 +55,23 @@ import java.util.Map; */ public class LPPermissionAttachment extends PermissionAttachment { + /** + * The field in PermissionAttachment where the attachments applied permissions + * are *usually* held. + */ + private static final Field PERMISSION_ATTACHMENT_PERMISSIONS_FIELD; + + static { + Field permissionAttachmentPermissionsField; + try { + permissionAttachmentPermissionsField = PermissibleBase.class.getDeclaredField("permissions"); + permissionAttachmentPermissionsField.setAccessible(true); + } catch (NoSuchFieldException e) { + throw new RuntimeException(e); + } + PERMISSION_ATTACHMENT_PERMISSIONS_FIELD = permissionAttachmentPermissionsField; + } + /** * The parent LPPermissible */ @@ -79,6 +104,8 @@ public class LPPermissionAttachment extends PermissionAttachment { super(DummyPlugin.INSTANCE, null); this.permissible = permissible; this.owner = owner; + + injectFakeMap(); } public LPPermissionAttachment(LPPermissible permissible, PermissionAttachment bukkit) { @@ -88,12 +115,41 @@ public class LPPermissionAttachment extends PermissionAttachment { // copy perms.putAll(bukkit.getPermissions()); + + injectFakeMap(); } + /** + * Injects a fake 'permissions' map into the superclass, for (clever/dumb??) plugins + * which attempt to modify attachment permissions using reflection to get around the slow bukkit + * behaviour in the base PermissionAttachment implementation. + * + * The fake map proxies calls back to the methods on this attachment + */ + private void injectFakeMap() { + // inner class - this proxies calls back to us + FakeBackingMap fakeMap = new FakeBackingMap(); + + try { + // what's this doing, ay? + // the field we need to modify is in the superclass - it's set to private + // so we have to use reflection to modify it. + PERMISSION_ATTACHMENT_PERMISSIONS_FIELD.set(this, fakeMap); + } catch (Exception e) { + e.printStackTrace(); + } + } + + /** + * Hooks this attachment with the parent {@link User} instance. + */ public void hook() { hooked = true; permissible.attachments.add(this); for (Map.Entry entry : perms.entrySet()) { + if (entry.getKey() == null || entry.getKey().isEmpty()) { + continue; + } setPermissionInternal(entry.getKey(), entry.getValue()); } } @@ -103,9 +159,20 @@ public class LPPermissionAttachment extends PermissionAttachment { return; } - ImmutableTransientNode node = ImmutableTransientNode.of(NodeFactory.make(name, value), this); - if (permissible.getUser().setTransientPermission(node).asBoolean()) { - permissible.getUser().getRefreshBuffer().request(); + // construct a node for the permission being set + // we use the servers static context to *try* to ensure that the node will apply + Node node = NodeFactory.newBuilder(name) + .setValue(value) + .withExtraContext(permissible.getPlugin().getContextManager().getStaticContext()) + .build(); + + // convert the constructed node to a transient node instance to refer back to this attachment + ImmutableTransientNode transientNode = ImmutableTransientNode.of(node, this); + + // set the transient node + User user = permissible.getUser(); + if (user.setTransientPermission(transientNode).asBoolean()) { + user.getRefreshBuffer().request(); } } @@ -114,8 +181,18 @@ public class LPPermissionAttachment extends PermissionAttachment { return; } - if (permissible.getUser().removeIfTransient(n -> n instanceof ImmutableTransientNode && ((ImmutableTransientNode) n).getOwner() == this && n.getPermission().equals(name))) { - permissible.getUser().getRefreshBuffer().request(); + // remove transient permissions from the holder which were added by this attachment & equal the permission + User user = permissible.getUser(); + if (user.removeIfTransient(n -> n instanceof ImmutableTransientNode && ((ImmutableTransientNode) n).getOwner() == this && n.getPermission().equals(name))) { + user.getRefreshBuffer().request(); + } + } + + private void clearInternal() { + // remove all transient permissions added by this attachment + User user = permissible.getUser(); + if (user.removeIfTransient(n -> n instanceof ImmutableTransientNode && ((ImmutableTransientNode) n).getOwner() == this)) { + user.getRefreshBuffer().request(); } } @@ -125,14 +202,15 @@ public class LPPermissionAttachment extends PermissionAttachment { return false; } - if (permissible.getUser().removeIfTransient(n -> n instanceof ImmutableTransientNode && ((ImmutableTransientNode) n).getOwner() == this)) { - permissible.getUser().getRefreshBuffer().request(); - } + // clear the internal permissions + clearInternal(); + // run the callback if (removalCallback != null) { removalCallback.attachmentRemoved(this); } + // unhook from the permissible hooked = false; permissible.attachments.remove(this); return true; @@ -140,34 +218,48 @@ public class LPPermissionAttachment extends PermissionAttachment { @Override public void setPermission(String name, boolean value) { - Boolean previous = perms.put(name, value); + Preconditions.checkNotNull(name, "name is null"); + Preconditions.checkArgument(!name.isEmpty(), "name is empty"); + + String permission = name.toLowerCase(); + + Boolean previous = perms.put(permission, value); if (previous != null && previous == value) { return; } + // if we're not hooked, thn don't actually apply the change + // it will get applied on hook - if that ever happens if (!hooked) { return; } if (previous != null) { - unsetPermissionInternal(name); + unsetPermissionInternal(permission); } - setPermissionInternal(name, value); + setPermissionInternal(permission, value); } @Override public void unsetPermission(String name) { - Boolean previous = perms.remove(name); + Preconditions.checkNotNull(name, "name is null"); + Preconditions.checkArgument(!name.isEmpty(), "name is empty"); + + String permission = name.toLowerCase(); + + Boolean previous = perms.remove(permission); if (previous == null) { return; } + // if we're not hooked, thn don't actually apply the change + // it will get applied on hook - if that ever happens if (!hooked) { return; } - unsetPermissionInternal(name); + unsetPermissionInternal(permission); } @Override @@ -189,4 +281,116 @@ public class LPPermissionAttachment extends PermissionAttachment { public int hashCode() { return System.identityHashCode(this); } + + /** + * A fake map to be injected into the superclass. This implementation simply + * proxies calls back to this attachment instance. + * + * Some (clever/dumb??) plugins attempt to modify attachment permissions using reflection + * to get around the slow bukkit behaviour in the base PermissionAttachment implementation. + * + * An instance of this map is injected into the super instance so these plugins continue + * to work with LuckPerms. + */ + private final class FakeBackingMap implements Map { + + @Override + public Boolean put(String key, Boolean value) { + + // grab the previous result, so we can still satisfy the method signature of Map + Boolean previous = perms.get(key); + + // proxy the call back through the PermissionAttachment instance + setPermission(key, value); + + // return the previous value + return previous; + } + + @Override + public Boolean remove(Object key) { + // we only accept string keys + if (!(key instanceof String)) { + return null; + } + + String permission = ((String) key); + + // grab the previous result, so we can still satisfy the method signature of Map + Boolean previous = perms.get(permission); + + // proxy the call back through the PermissionAttachment instance + unsetPermission(permission); + + // return the previous value + return previous; + } + + @Override + public void putAll(Map m) { + for (Map.Entry entry : m.entrySet()) { + put(entry.getKey(), entry.getValue()); + } + } + + @Override + public void clear() { + // remove the permissions which have already been applied + if (hooked) { + clearInternal(); + } + + // clear the backing map + perms.clear(); + } + + @Override + public int size() { + // return the size of the permissions map - probably the most accurate value we have + return perms.size(); + } + + @Override + public boolean isEmpty() { + // return if the permissions map is empty - again probably the most accurate thing + // we can return + return perms.isEmpty(); + } + + @Override + public boolean containsKey(Object key) { + // just proxy + return perms.containsKey(key); + } + + @Override + public boolean containsValue(Object value) { + // just proxy + return perms.containsValue(value); + } + + @Override + public Boolean get(Object key) { + // just proxy + return perms.get(key); + } + + @Override + public Set keySet() { + // just proxy + return perms.keySet(); + } + + @Override + public Collection values() { + // just proxy + return perms.values(); + } + + @Override + public Set> entrySet() { + // just proxy + return perms.entrySet(); + } + } }