diff --git a/src/main/java/fr/xephi/authme/command/executable/authme/debug/DataStatistics.java b/src/main/java/fr/xephi/authme/command/executable/authme/debug/DataStatistics.java new file mode 100644 index 00000000..3bf28e05 --- /dev/null +++ b/src/main/java/fr/xephi/authme/command/executable/authme/debug/DataStatistics.java @@ -0,0 +1,72 @@ +package fr.xephi.authme.command.executable.authme.debug; + +import fr.xephi.authme.data.auth.PlayerCache; +import fr.xephi.authme.data.limbo.LimboService; +import fr.xephi.authme.datasource.CacheDataSource; +import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.initialization.HasCleanup; +import fr.xephi.authme.initialization.Reloadable; +import fr.xephi.authme.initialization.SettingsDependent; +import fr.xephi.authme.initialization.factory.SingletonStore; +import org.bukkit.command.CommandSender; + +import javax.inject.Inject; +import java.util.List; +import java.util.Map; + +import static fr.xephi.authme.command.executable.authme.debug.DebugSectionUtils.applyToLimboPlayersMap; + +/** + * Fetches various statistics, particularly regarding in-memory data that is stored. + */ +class DataStatistics implements DebugSection { + + @Inject + private PlayerCache playerCache; + + @Inject + private LimboService limboService; + + @Inject + private DataSource dataSource; + + @Inject + private SingletonStore singletonStore; + + @Override + public String getName() { + return "stats"; + } + + @Override + public String getDescription() { + return "Outputs general data statistics"; + } + + @Override + public void execute(CommandSender sender, List arguments) { + sender.sendMessage("LimboPlayers in memory: " + applyToLimboPlayersMap(limboService, Map::size)); + sender.sendMessage("PlayerCache size: " + playerCache.getLogged() + " (= logged in players)"); + + outputDatabaseStats(sender); + outputInjectorStats(sender); + } + + private void outputDatabaseStats(CommandSender sender) { + sender.sendMessage("Total players in DB: " + dataSource.getAccountsRegistered()); + sender.sendMessage("Total marked as logged in in DB: " + dataSource.getLoggedPlayers().size()); + if (dataSource instanceof CacheDataSource) { + CacheDataSource cacheDataSource = (CacheDataSource) this.dataSource; + sender.sendMessage("Cached PlayerAuth objects: " + cacheDataSource.getCachedAuths().size()); + } + } + + private void outputInjectorStats(CommandSender sender) { + sender.sendMessage( + String.format("Singleton Java classes: %d (Reloadable: %d / SettingsDependent: %d / HasCleanup: %d)", + singletonStore.retrieveAllOfType().size(), + singletonStore.retrieveAllOfType(Reloadable.class).size(), + singletonStore.retrieveAllOfType(SettingsDependent.class).size(), + singletonStore.retrieveAllOfType(HasCleanup.class).size())); + } +} diff --git a/src/main/java/fr/xephi/authme/command/executable/authme/debug/DebugCommand.java b/src/main/java/fr/xephi/authme/command/executable/authme/debug/DebugCommand.java index 5166df0d..5cd7fb4f 100644 --- a/src/main/java/fr/xephi/authme/command/executable/authme/debug/DebugCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/authme/debug/DebugCommand.java @@ -19,9 +19,9 @@ public class DebugCommand implements ExecutableCommand { @Inject private Factory debugSectionFactory; - private Set> sectionClasses = ImmutableSet.of( - PermissionGroups.class, TestEmailSender.class, PlayerAuthViewer.class, LimboPlayerViewer.class, - CountryLookup.class); + private Set> sectionClasses = ImmutableSet.of(PermissionGroups.class, + DataStatistics.class, CountryLookup.class, PlayerAuthViewer.class, LimboPlayerViewer.class, CountryLookup.class, + HasPermissionChecker.class, TestEmailSender.class); private Map sections; diff --git a/src/main/java/fr/xephi/authme/command/executable/authme/debug/DebugSectionUtils.java b/src/main/java/fr/xephi/authme/command/executable/authme/debug/DebugSectionUtils.java index e2861ca4..78960ce8 100644 --- a/src/main/java/fr/xephi/authme/command/executable/authme/debug/DebugSectionUtils.java +++ b/src/main/java/fr/xephi/authme/command/executable/authme/debug/DebugSectionUtils.java @@ -1,15 +1,22 @@ package fr.xephi.authme.command.executable.authme.debug; +import fr.xephi.authme.ConsoleLogger; +import fr.xephi.authme.data.limbo.LimboService; import org.bukkit.Location; +import java.lang.reflect.Field; import java.math.RoundingMode; import java.text.DecimalFormat; +import java.util.Map; +import java.util.function.Function; /** * Utilities used within the DebugSection implementations. */ final class DebugSectionUtils { + private static Field limboEntriesField; + private DebugSectionUtils() { } @@ -52,4 +59,40 @@ final class DebugSectionUtils { df.setRoundingMode(RoundingMode.HALF_UP); return df.format(number); } + + private static Field getLimboPlayerEntriesField() { + if (limboEntriesField == null) { + try { + Field field = LimboService.class.getDeclaredField("entries"); + field.setAccessible(true); + limboEntriesField = field; + } catch (Exception e) { + ConsoleLogger.logException("Could not retrieve LimboService entries field:", e); + } + } + return limboEntriesField; + } + + /** + * Applies the given function to the map in LimboService containing the LimboPlayers. + * As we don't want to expose this information in non-debug settings, this is done with reflection. + * Exceptions are generously caught and {@code null} is returned on failure. + * + * @param limboService the limbo service instance to get the map from + * @param function the function to apply to the map + * @param the result type of the function + * + * @return player names for which there is a LimboPlayer (or error message upon failure) + */ + static U applyToLimboPlayersMap(LimboService limboService, Function function) { + Field limboPlayerEntriesField = getLimboPlayerEntriesField(); + if (limboPlayerEntriesField != null) { + try { + return function.apply((Map) limboEntriesField.get(limboService)); + } catch (Exception e) { + ConsoleLogger.logException("Could not retrieve LimboService values:", e); + } + } + return null; + } } diff --git a/src/main/java/fr/xephi/authme/command/executable/authme/debug/HasPermissionChecker.java b/src/main/java/fr/xephi/authme/command/executable/authme/debug/HasPermissionChecker.java new file mode 100644 index 00000000..0df27bcc --- /dev/null +++ b/src/main/java/fr/xephi/authme/command/executable/authme/debug/HasPermissionChecker.java @@ -0,0 +1,130 @@ +package fr.xephi.authme.command.executable.authme.debug; + +import fr.xephi.authme.permission.AdminPermission; +import fr.xephi.authme.permission.DefaultPermission; +import fr.xephi.authme.permission.PermissionNode; +import fr.xephi.authme.permission.PermissionsManager; +import fr.xephi.authme.permission.PlayerPermission; +import fr.xephi.authme.permission.PlayerStatePermission; +import fr.xephi.authme.service.BukkitService; +import org.bukkit.Bukkit; +import org.bukkit.ChatColor; +import org.bukkit.OfflinePlayer; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + +import javax.inject.Inject; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.function.BiFunction; + +/** + * Checks if a player has a given permission, as checked by AuthMe. + */ +class HasPermissionChecker implements DebugSection { + + static final List> PERMISSION_NODE_CLASSES = + Arrays.asList(AdminPermission.class, PlayerPermission.class, PlayerStatePermission.class); + + @Inject + private PermissionsManager permissionsManager; + + @Inject + private BukkitService bukkitService; + + @Override + public String getName() { + return "perm"; + } + + @Override + public String getDescription() { + return "Checks if player has given permission: /authme debug perm bobby my.perm"; + } + + @Override + public void execute(CommandSender sender, List arguments) { + if (arguments.size() < 2) { + sender.sendMessage("Check if a player has permission:"); + sender.sendMessage("Example: /authme debug perm bobby my.perm.node"); + return; + } + + final String playerName = arguments.get(0); + final String permissionNode = arguments.get(1); + + Player player = bukkitService.getPlayerExact(playerName); + if (player == null) { + OfflinePlayer offlinePlayer = Bukkit.getOfflinePlayer(playerName); + if (offlinePlayer == null) { + sender.sendMessage(ChatColor.DARK_RED + "Player '" + playerName + "' does not exist"); + } else { + sender.sendMessage("Player '" + playerName + "' not online; checking with offline player"); + performPermissionCheck(offlinePlayer, permissionNode, permissionsManager::hasPermissionOffline, sender); + } + } else { + performPermissionCheck(player, permissionNode, permissionsManager::hasPermission, sender); + } + } + + /** + * Performs a permission check and informs the given sender of the result. {@code permissionChecker} is the + * permission check to perform with the given {@code node} and the {@code player}. + * + * @param player the player to check a permission for + * @param node the node of the permission to check + * @param permissionChecker permission checking function + * @param sender the sender to inform of the result + * @param

the player type + */ + private static

void performPermissionCheck( + P player, String node, BiFunction permissionChecker, CommandSender sender) { + + PermissionNode permNode = getPermissionNode(sender, node); + if (permissionChecker.apply(player, permNode)) { + sender.sendMessage(ChatColor.DARK_GREEN + "Success: player '" + player.getName() + + "' has permission '" + node + "'"); + } else { + sender.sendMessage(ChatColor.DARK_RED + "Check failed: player '" + player.getName() + + "' does NOT have permission '" + node + "'"); + } + + } + + /** + * Based on the given permission node (String), tries to find the according AuthMe {@link PermissionNode} + * instance, or creates a new one if not available. + * + * @param sender the sender (used to inform him if no AuthMe PermissionNode can be matched) + * @param node the node to search for + * @return the node as {@link PermissionNode} object + */ + private static PermissionNode getPermissionNode(CommandSender sender, String node) { + Optional permNode = PERMISSION_NODE_CLASSES.stream() + .map(Class::getEnumConstants) + .flatMap(Arrays::stream) + .filter(perm -> perm.getNode().equals(node)) + .findFirst(); + if (permNode.isPresent()) { + return permNode.get(); + } else { + sender.sendMessage("Did not detect AuthMe permission; using default permission = DENIED"); + return createPermNode(node); + } + } + + private static PermissionNode createPermNode(String node) { + return new PermissionNode() { + @Override + public String getNode() { + return node; + } + + @Override + public DefaultPermission getDefaultPermission() { + return DefaultPermission.NOT_ALLOWED; + } + }; + } +} diff --git a/src/main/java/fr/xephi/authme/command/executable/authme/debug/LimboPlayerViewer.java b/src/main/java/fr/xephi/authme/command/executable/authme/debug/LimboPlayerViewer.java index c93ad239..bf7f9b3c 100644 --- a/src/main/java/fr/xephi/authme/command/executable/authme/debug/LimboPlayerViewer.java +++ b/src/main/java/fr/xephi/authme/command/executable/authme/debug/LimboPlayerViewer.java @@ -1,6 +1,5 @@ package fr.xephi.authme.command.executable.authme.debug; -import fr.xephi.authme.ConsoleLogger; import fr.xephi.authme.data.limbo.LimboPlayer; import fr.xephi.authme.data.limbo.LimboService; import fr.xephi.authme.data.limbo.persistence.LimboPersistence; @@ -10,15 +9,13 @@ import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; import javax.inject.Inject; -import java.lang.reflect.Field; -import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Optional; -import java.util.Set; import java.util.function.Function; import static fr.xephi.authme.command.executable.authme.debug.DebugSectionUtils.formatLocation; +import static fr.xephi.authme.command.executable.authme.debug.DebugSectionUtils.applyToLimboPlayersMap; /** * Shows the data stored in LimboPlayers and the equivalent properties on online players. @@ -34,8 +31,6 @@ class LimboPlayerViewer implements DebugSection { @Inject private BukkitService bukkitService; - private Field limboServiceEntries; - @Override public String getName() { return "limbo"; @@ -50,7 +45,7 @@ class LimboPlayerViewer implements DebugSection { public void execute(CommandSender sender, List arguments) { if (arguments.isEmpty()) { sender.sendMessage("/authme debug limbo : show a player's limbo info"); - sender.sendMessage("Available limbo records: " + getLimboKeys()); + sender.sendMessage("Available limbo records: " + applyToLimboPlayersMap(limboService, Map::keySet)); return; } @@ -73,35 +68,6 @@ class LimboPlayerViewer implements DebugSection { sender.sendMessage("Note: group is not shown for Player. Use /authme debug groups"); } - /** - * Gets the names of the LimboPlayers in the LimboService. As we don't want to expose this - * information in non-debug settings, this is done over reflections. Since this is not a - * crucial feature, we generously catch all Exceptions - * - * @return player names for which there is a LimboPlayer (or error message upon failure) - */ - @SuppressWarnings("unchecked") - private Set getLimboKeys() { - // Lazy initialization - if (limboServiceEntries == null) { - try { - Field limboServiceEntries = LimboService.class.getDeclaredField("entries"); - limboServiceEntries.setAccessible(true); - this.limboServiceEntries = limboServiceEntries; - } catch (Exception e) { - ConsoleLogger.logException("Could not retrieve LimboService entries field:", e); - return Collections.singleton("Error retrieving LimboPlayer collection"); - } - } - - try { - return (Set) ((Map) limboServiceEntries.get(limboService)).keySet(); - } catch (Exception e) { - ConsoleLogger.logException("Could not retrieve LimboService values:", e); - return Collections.singleton("Error retrieving LimboPlayer values"); - } - } - /** * Displays the info for the given LimboPlayer and Player to the provided CommandSender. */ diff --git a/src/test/java/fr/xephi/authme/command/executable/authme/debug/DebugSectionUtilsTest.java b/src/test/java/fr/xephi/authme/command/executable/authme/debug/DebugSectionUtilsTest.java index caf61788..e8443260 100644 --- a/src/test/java/fr/xephi/authme/command/executable/authme/debug/DebugSectionUtilsTest.java +++ b/src/test/java/fr/xephi/authme/command/executable/authme/debug/DebugSectionUtilsTest.java @@ -1,11 +1,20 @@ package fr.xephi.authme.command.executable.authme.debug; +import fr.xephi.authme.ReflectionTestUtils; import fr.xephi.authme.TestHelper; +import fr.xephi.authme.data.limbo.LimboPlayer; +import fr.xephi.authme.data.limbo.LimboService; import org.bukkit.Location; import org.junit.Test; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; + import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.sameInstance; import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.mock; /** * Test for {@link DebugSectionUtils}. @@ -43,4 +52,18 @@ public class DebugSectionUtilsTest { public void shouldHaveHiddenConstructor() { TestHelper.validateHasOnlyPrivateEmptyConstructor(DebugSectionUtils.class); } + + @Test + public void shouldFetchMapInLimboService() { + // given + LimboService limboService = mock(LimboService.class); + Map limboMap = new HashMap<>(); + ReflectionTestUtils.setField(LimboService.class, limboService, "entries", limboMap); + + // when + Map map = DebugSectionUtils.applyToLimboPlayersMap(limboService, Function.identity()); + + // then + assertThat(map, sameInstance(limboMap)); + } } diff --git a/src/test/java/fr/xephi/authme/command/executable/authme/debug/HasPermissionCheckerTest.java b/src/test/java/fr/xephi/authme/command/executable/authme/debug/HasPermissionCheckerTest.java new file mode 100644 index 00000000..9f7c6a92 --- /dev/null +++ b/src/test/java/fr/xephi/authme/command/executable/authme/debug/HasPermissionCheckerTest.java @@ -0,0 +1,97 @@ +package fr.xephi.authme.command.executable.authme.debug; + +import fr.xephi.authme.ClassCollector; +import fr.xephi.authme.TestHelper; +import fr.xephi.authme.permission.AdminPermission; +import fr.xephi.authme.permission.PermissionNode; +import fr.xephi.authme.permission.PermissionsManager; +import fr.xephi.authme.service.BukkitService; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import java.util.List; +import java.util.stream.Collectors; + +import static java.util.Arrays.asList; +import static java.util.Collections.emptyList; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasSize; +import static org.junit.Assert.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.atLeast; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.hamcrest.MockitoHamcrest.argThat; + +/** + * Test for {@link HasPermissionChecker}. + */ +@RunWith(MockitoJUnitRunner.class) +public class HasPermissionCheckerTest { + + @InjectMocks + private HasPermissionChecker hasPermissionChecker; + + @Mock + private PermissionsManager permissionsManager; + + @Mock + private BukkitService bukkitService; + + @Test + public void shouldListAllPermissionNodeClasses() { + // given + List> permissionClasses = + new ClassCollector(TestHelper.SOURCES_FOLDER, TestHelper.PROJECT_ROOT) + .collectClasses(PermissionNode.class).stream() + .filter(clz -> !clz.isInterface()) + .collect(Collectors.toList()); + + // when / then + assertThat(HasPermissionChecker.PERMISSION_NODE_CLASSES.containsAll(permissionClasses), equalTo(true)); + assertThat(HasPermissionChecker.PERMISSION_NODE_CLASSES, hasSize(permissionClasses.size())); + } + + @Test + public void shouldShowUsageInfo() { + // given + CommandSender sender = mock(CommandSender.class); + + // when + hasPermissionChecker.execute(sender, emptyList()); + + // then + ArgumentCaptor msgCaptor = ArgumentCaptor.forClass(String.class); + verify(sender, atLeast(2)).sendMessage(msgCaptor.capture()); + assertThat( + msgCaptor.getAllValues().stream().anyMatch(msg -> msg.contains("/authme debug perm bobby my.perm.node")), + equalTo(true)); + } + + @Test + public void shouldShowSuccessfulTestWithRegularPlayer() { + // given + String name = "Chuck"; + Player player = mock(Player.class); + given(bukkitService.getPlayerExact(name)).willReturn(player); + PermissionNode permission = AdminPermission.CHANGE_EMAIL; + given(permissionsManager.hasPermission(player, permission)).willReturn(true); + CommandSender sender = mock(CommandSender.class); + + // when + hasPermissionChecker.execute(sender, asList(name, permission.getNode())); + + // then + verify(bukkitService).getPlayerExact(name); + verify(permissionsManager).hasPermission(player, permission); + verify(sender).sendMessage(argThat(containsString("Success: player '" + player.getName() + + "' has permission '" + permission.getNode() + "'"))); + } +}