LuckPerms-DBA/bukkit/src/main/java/me/lucko/luckperms/bukkit/vault/VaultPermissionHook.java
2018-11-10 20:21:08 +00:00

443 lines
17 KiB
Java

/*
* This file is part of LuckPerms, licensed under the MIT License.
*
* Copyright (c) lucko (Luck) <luck@lucko.me>
* Copyright (c) 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 me.lucko.luckperms.bukkit.vault;
import com.google.common.base.Preconditions;
import me.lucko.luckperms.api.Contexts;
import me.lucko.luckperms.api.Node;
import me.lucko.luckperms.api.Tristate;
import me.lucko.luckperms.api.context.ContextSet;
import me.lucko.luckperms.api.context.MutableContextSet;
import me.lucko.luckperms.bukkit.LPBukkitPlugin;
import me.lucko.luckperms.common.caching.type.PermissionCache;
import me.lucko.luckperms.common.command.CommandManager;
import me.lucko.luckperms.common.config.ConfigKeys;
import me.lucko.luckperms.common.model.Group;
import me.lucko.luckperms.common.model.PermissionHolder;
import me.lucko.luckperms.common.model.User;
import me.lucko.luckperms.common.node.factory.NodeFactory;
import me.lucko.luckperms.common.verbose.event.MetaCheckEvent;
import me.lucko.luckperms.common.verbose.event.PermissionCheckEvent;
import net.milkbowl.vault.permission.Permission;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import java.util.Arrays;
import java.util.Objects;
import java.util.Optional;
import java.util.UUID;
/**
* An implementation of the Vault {@link Permission} API using LuckPerms.
*
* LuckPerms is a multithreaded permissions plugin, and some actions require considerable
* time to execute. (database queries, re-population of caches, etc) In these cases, the
* operations required to make the edit apply will be processed immediately, but the process
* of saving the change to the plugin storage will happen in the background.
*
* Methods that have to query data from the database will throw exceptions when called
* from the main thread. Users of the Vault API expect these methods to be "main thread friendly",
* which they simply cannot be, as LP utilises databases for data storage. Server admins
* willing to take the risk of lagging their server can disable these exceptions in the config file.
*/
public class VaultPermissionHook extends AbstractVaultPermission {
// the plugin instance
private final LPBukkitPlugin plugin;
public VaultPermissionHook(LPBukkitPlugin plugin) {
this.plugin = plugin;
this.worldMappingFunction = world -> isIgnoreWorld() ? null : world;
}
public LPBukkitPlugin getPlugin() {
return this.plugin;
}
@Override
public String getName() {
return "LuckPerms";
}
@Override
public UUID lookupUuid(String player) {
Objects.requireNonNull(player, "player");
// are they online?
Player onlinePlayer = Bukkit.getPlayerExact(player);
if (onlinePlayer != null) {
return onlinePlayer.getUniqueId();
}
// are we on the main thread?
if (!this.plugin.getBootstrap().isServerStarting() && Bukkit.isPrimaryThread() && !this.plugin.getConfiguration().get(ConfigKeys.VAULT_UNSAFE_LOOKUPS)) {
throw new RuntimeException(
"The operation to lookup a UUID for '" + player + "' was cancelled by LuckPerms. This is NOT a bug. \n" +
"The lookup request was made on the main server thread. It is not safe to execute a request to \n" +
"load username data from the database in this context. \n" +
"If you are a plugin author, please either make your request asynchronously, \n" +
"or provide an 'OfflinePlayer' object with the UUID already populated. \n" +
"Alternatively, server admins can disable this catch by setting 'vault-unsafe-lookups' to true \n" +
"in the LP config, but should consider the consequences (lag) before doing so."
);
}
// lookup a username from the database
UUID uuid = this.plugin.getStorage().getPlayerUuid(player.toLowerCase()).join();
if (uuid == null) {
uuid = this.plugin.getBootstrap().lookupUuid(player).orElse(null);
}
// unable to find a user, throw an exception
if (uuid == null) {
throw new IllegalArgumentException("Unable to find a UUID for player '" + player + "'.");
}
return uuid;
}
public User lookupUser(UUID uuid) {
Objects.requireNonNull(uuid, "uuid");
// loaded already?
User user = this.plugin.getUserManager().getIfLoaded(uuid);
if (user != null) {
return user;
}
// are we on the main thread?
if (!this.plugin.getBootstrap().isServerStarting() && Bukkit.isPrimaryThread() && !this.plugin.getConfiguration().get(ConfigKeys.VAULT_UNSAFE_LOOKUPS)) {
throw new RuntimeException(
"The operation to load user data for '" + uuid + "' was cancelled by LuckPerms. This is NOT a bug. \n" +
"The lookup request was made on the main server thread. It is not safe to execute a request to \n" +
"load username data from the database in this context. \n" +
"If you are a plugin author, please consider making your request asynchronously. \n" +
"Alternatively, server admins can disable this catch by setting 'vault-unsafe-lookups' to true \n" +
"in the LP config, but should consider the consequences (lag) before doing so."
);
}
// load an instance from the DB
return this.plugin.getStorage().loadUser(uuid, null).join();
}
@Override
public String[] getGroups() {
return this.plugin.getGroupManager().getAll().values().stream()
.map(Group::getPlainDisplayName)
.toArray(String[]::new);
}
@Override
public boolean userHasPermission(String world, UUID uuid, String permission) {
Objects.requireNonNull(uuid, "uuid");
Objects.requireNonNull(permission, "permission");
User user = lookupUser(uuid);
Contexts contexts = contextForLookup(user, world);
PermissionCache permissionData = user.getCachedData().getPermissionData(contexts);
Tristate result = permissionData.getPermissionValue(permission, PermissionCheckEvent.Origin.THIRD_PARTY_API);
if (log()) {
logMsg("#userHasPermission: %s - %s - %s - %s", user.getPlainDisplayName(), contexts.getContexts().toMultimap(), permission, result);
}
return result.asBoolean();
}
@Override
public boolean userAddPermission(String world, UUID uuid, String permission) {
Objects.requireNonNull(uuid, "uuid");
Objects.requireNonNull(permission, "permission");
User user = lookupUser(uuid);
return holderAddPermission(user, permission, world);
}
@Override
public boolean userRemovePermission(String world, UUID uuid, String permission) {
Objects.requireNonNull(uuid, "uuid");
Objects.requireNonNull(permission, "permission");
User user = lookupUser(uuid);
return holderRemovePermission(user, permission, world);
}
@Override
public boolean userInGroup(String world, UUID uuid, String group) {
Objects.requireNonNull(uuid, "uuid");
Objects.requireNonNull(group, "group");
return userHasPermission(world, uuid, NodeFactory.groupNode(rewriteGroupName(group)));
}
@Override
public boolean userAddGroup(String world, UUID uuid, String group) {
Objects.requireNonNull(uuid, "uuid");
Objects.requireNonNull(group, "group");
return checkGroupExists(group) && userAddPermission(world, uuid, NodeFactory.groupNode(rewriteGroupName(group)));
}
@Override
public boolean userRemoveGroup(String world, UUID uuid, String group) {
Objects.requireNonNull(uuid, "uuid");
Objects.requireNonNull(group, "group");
return checkGroupExists(group) && userRemovePermission(world, uuid, NodeFactory.groupNode(rewriteGroupName(group)));
}
@Override
public String[] userGetGroups(String world, UUID uuid) {
Objects.requireNonNull(uuid, "uuid");
User user = lookupUser(uuid);
ContextSet contexts = contextForLookup(user, world).getContexts();
String[] ret = user.enduringData().immutable().values().stream()
.filter(Node::isGroupNode)
.filter(n -> n.shouldApplyWithContext(contexts))
.map(n -> {
Group group = this.plugin.getGroupManager().getIfLoaded(n.getGroupName());
if (group != null) {
return group.getPlainDisplayName();
}
return n.getGroupName();
})
.toArray(String[]::new);
if (log()) {
logMsg("#userGetGroups: %s - %s - %s", user.getPlainDisplayName(), contexts, Arrays.toString(ret));
}
return ret;
}
@Override
public String userGetPrimaryGroup(String world, UUID uuid) {
Objects.requireNonNull(uuid, "uuid");
User user = lookupUser(uuid);
String value = user.getPrimaryGroup().getValue();
Group group = getGroup(value);
if (group != null) {
value = group.getPlainDisplayName();
}
this.plugin.getVerboseHandler().offerMetaCheckEvent(MetaCheckEvent.Origin.THIRD_PARTY_API, user.getPlainDisplayName(), ContextSet.empty(), "primarygroup", value);
if (log()) {
logMsg("#userGetPrimaryGroup: %s - %s - %s", user.getPlainDisplayName(), world, value);
}
return value;
}
@Override
public boolean groupHasPermission(String world, String name, String permission) {
Objects.requireNonNull(name, "name");
Objects.requireNonNull(permission, "permission");
Group group = getGroup(name);
if (group == null) {
return false;
}
Contexts contexts = contextForLookup(null, world);
PermissionCache permissionData = group.getCachedData().getPermissionData(contexts);
Tristate result = permissionData.getPermissionValue(permission, PermissionCheckEvent.Origin.THIRD_PARTY_API);
if (log()) {
logMsg("#groupHasPermission: %s - %s - %s - %s", group.getName(), contexts.getContexts().toMultimap(), permission, result);
}
return result.asBoolean();
}
@Override
public boolean groupAddPermission(String world, String name, String permission) {
Objects.requireNonNull(name, "name");
Objects.requireNonNull(permission, "permission");
Group group = getGroup(name);
if (group == null) {
return false;
}
return holderAddPermission(group, permission, world);
}
@Override
public boolean groupRemovePermission(String world, String name, String permission) {
Objects.requireNonNull(name, "name");
Objects.requireNonNull(permission, "permission");
Group group = getGroup(name);
if (group == null) {
return false;
}
return holderRemovePermission(group, permission, world);
}
// utility methods for getting user and group instances
private Group getGroup(String name) {
return this.plugin.getGroupManager().getByDisplayName(name);
}
private boolean checkGroupExists(String group) {
return this.plugin.getGroupManager().getByDisplayName(group) != null;
}
private String rewriteGroupName(String name) {
Group group = this.plugin.getGroupManager().getByDisplayName(name);
if (group != null) {
return group.getName();
}
return name;
}
// logging
private boolean log() {
return this.plugin.getConfiguration().get(ConfigKeys.VAULT_DEBUG);
}
private void logMsg(String format, Object... args) {
this.plugin.getLogger().info("[VAULT-PERMS] " + String.format(format, args)
.replace(CommandManager.SECTION_CHAR, '$')
.replace(CommandManager.AMPERSAND_CHAR, '$')
);
}
// utility method for getting a contexts instance for a given vault lookup.
Contexts contextForLookup(User user, String world) {
MutableContextSet context;
Player player = Optional.ofNullable(user).flatMap(u -> this.plugin.getBootstrap().getPlayer(u.getUuid())).orElse(null);
if (player != null) {
context = this.plugin.getContextManager().getApplicableContext(player).mutableCopy();
} else {
context = this.plugin.getContextManager().getStaticContext().mutableCopy();
}
String playerWorld = player == null ? null : player.getWorld().getName();
// if world is null, we want to do a lookup in the players current context
// if world is not null, we want to do a lookup in that specific world
if (world != null && !world.isEmpty() && !world.equalsIgnoreCase(playerWorld)) {
// remove already accumulated worlds
context.removeAll(Contexts.WORLD_KEY);
// add the vault world
context.add(Contexts.WORLD_KEY, world.toLowerCase());
}
// if we're using a special vault server
if (useVaultServer()) {
// remove the normal server context from the set
context.remove(Contexts.SERVER_KEY, getServer());
// add the vault specific server
if (!getVaultServer().equals("global")) {
context.add(Contexts.SERVER_KEY, getVaultServer());
}
}
return Contexts.of(context, isIncludeGlobal(), true, true, true, true, false);
}
// utility methods for modifying the state of PermissionHolders
private boolean holderAddPermission(PermissionHolder holder, String permission, String world) {
Objects.requireNonNull(permission, "permission is null");
Preconditions.checkArgument(!permission.isEmpty(), "permission is an empty string");
if (log()) {
logMsg("#holderAddPermission: %s - %s - %s", holder.getPlainDisplayName(), permission, world);
}
if (holder.setPermission(NodeFactory.make(permission, true, getVaultServer(), world)).asBoolean()) {
return holderSave(holder);
}
return false;
}
private boolean holderRemovePermission(PermissionHolder holder, String permission, String world) {
Objects.requireNonNull(permission, "permission is null");
Preconditions.checkArgument(!permission.isEmpty(), "permission is an empty string");
if (log()) {
logMsg("#holderRemovePermission: %s - %s - %s", holder.getPlainDisplayName(), permission, world);
}
if (holder.unsetPermission(NodeFactory.make(permission, getVaultServer(), world)).asBoolean()) {
return holderSave(holder);
}
return false;
}
boolean holderSave(PermissionHolder holder) {
if (holder.getType().isUser()) {
User u = (User) holder;
// we don't need to join this call - the save operation
// can happen in the background.
this.plugin.getStorage().saveUser(u);
} else if (holder.getType().isGroup()) {
Group g = (Group) holder;
// invalidate caches - they have potentially been affected by
// this change.
this.plugin.getGroupManager().invalidateAllGroupCaches();
this.plugin.getUserManager().invalidateAllUserCaches();
// we don't need to join this call - the save operation
// can happen in the background.
this.plugin.getStorage().saveGroup(g);
}
return true;
}
// helper methods to just pull values from the config.
String getServer() {
return this.plugin.getConfiguration().get(ConfigKeys.SERVER);
}
String getVaultServer() {
return this.plugin.getConfiguration().get(ConfigKeys.VAULT_SERVER);
}
boolean isIncludeGlobal() {
return this.plugin.getConfiguration().get(ConfigKeys.VAULT_INCLUDING_GLOBAL);
}
boolean isIgnoreWorld() {
return this.plugin.getConfiguration().get(ConfigKeys.VAULT_IGNORE_WORLD);
}
private boolean useVaultServer() {
return this.plugin.getConfiguration().get(ConfigKeys.USE_VAULT_SERVER);
}
}