Added a logout command

Permission: plan.logout.other
Usage: /plan logout * or /plan logout <username>

Used for forcing a logout for users.
This commit is contained in:
Risto Lahtela 2021-03-20 12:33:09 +02:00
parent fb4b272844
commit 07cca1f008
12 changed files with 97 additions and 13 deletions

View File

@ -105,6 +105,7 @@ public class PlanCommand {
.subcommand(registerCommand())
.subcommand(unregisterCommand())
.subcommand(logoutCommand())
.subcommand(webUsersCommand())
.subcommand(acceptCommand())
@ -239,6 +240,17 @@ public class PlanCommand {
.build();
}
private Subcommand logoutCommand() {
return Subcommand.builder()
.requirePermission(Permissions.LOGOUT_OTHER)
.requiredArgument(locale.getString(HelpLang.ARG_USERNAME), locale.getString(HelpLang.DESC_ARG_USERNAME))
.description(locale.getString(HelpLang.LOGOUT))
.inDepthDescription(locale.getString(DeepHelpLang.LOGOUT))
.onCommand(registrationCommands::onLogoutCommand)
.onTabComplete(this::webUserNames)
.build();
}
private List<String> webUserNames(CMDSender sender, Arguments arguments) {
if (!sender.hasPermission(Permissions.UNREGISTER_OTHER)) {
return Collections.emptyList();

View File

@ -234,4 +234,20 @@ public class RegistrationCommands {
}
});
}
public void onLogoutCommand(CMDSender sender, Arguments arguments) {
Optional<String> username = arguments.get(0);
if (!username.isPresent()) {
throw new IllegalArgumentException(locale.getString(CommandLang.FAIL_REQ_ONE_ARG, locale.getString(HelpLang.ARG_USERNAME) + "/*"));
}
String loggingOut = username.get();
if ("*".equals(loggingOut)) {
activeCookieStore.removeAll();
} else {
ActiveCookieStore.removeUserCookie(loggingOut);
}
}
}

View File

@ -25,14 +25,13 @@ import com.djrapitops.plan.storage.database.DBSystem;
import com.djrapitops.plan.storage.database.queries.objects.WebUserQueries;
import com.djrapitops.plan.storage.database.transactions.events.CookieChangeTransaction;
import net.playeranalytics.plugin.scheduling.RunnableFactory;
import net.playeranalytics.plugin.scheduling.Task;
import net.playeranalytics.plugin.scheduling.TimeAmount;
import org.apache.commons.codec.digest.DigestUtils;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
@ -47,6 +46,8 @@ public class ActiveCookieStore implements SubSystem {
private final RunnableFactory runnableFactory;
private final Processing processing;
private final Collection<Task> expiryTasks;
@Inject
public ActiveCookieStore(
PlanConfig config,
@ -60,6 +61,8 @@ public class ActiveCookieStore implements SubSystem {
this.dbSystem = dbSystem;
this.processing = processing;
this.runnableFactory = runnableFactory;
expiryTasks = new ArrayList<>();
}
private static void removeCookieStatic(String cookie) {
@ -84,14 +87,22 @@ public class ActiveCookieStore implements SubSystem {
USERS_BY_COOKIE.putAll(dbSystem.getDatabase().query(WebUserQueries.fetchActiveCookies()));
for (Map.Entry<String, Long> entry : dbSystem.getDatabase().query(WebUserQueries.getCookieExpiryTimes()).entrySet()) {
long timeToExpiry = Math.max(entry.getValue() - System.currentTimeMillis(), 0L);
runnableFactory.create(() -> removeCookie(entry.getKey()))
.runTaskLaterAsynchronously(TimeAmount.toTicks(timeToExpiry, TimeUnit.MILLISECONDS));
expiryTasks.add(runnableFactory.create(() -> removeCookie(entry.getKey()))
.runTaskLaterAsynchronously(TimeAmount.toTicks(timeToExpiry, TimeUnit.MILLISECONDS)));
}
}
@Override
public void disable() {
USERS_BY_COOKIE.clear();
expiryTasks.forEach(task -> {
try {
task.cancel();
} catch (Exception e) {
// Ignore, task has already been cancelled
}
});
expiryTasks.clear();
}
public Optional<User> checkCookie(String cookie) {
@ -102,6 +113,8 @@ public class ActiveCookieStore implements SubSystem {
String cookie = DigestUtils.sha256Hex(user.getUsername() + UUID.randomUUID() + System.currentTimeMillis());
USERS_BY_COOKIE.put(cookie, user);
saveNewCookie(user, cookie, System.currentTimeMillis());
expiryTasks.add(runnableFactory.create(() -> removeCookie(cookie))
.runTaskLaterAsynchronously(TimeAmount.toTicks(cookieExpiresAfter, TimeUnit.MILLISECONDS)));
return cookie;
}
@ -122,4 +135,9 @@ public class ActiveCookieStore implements SubSystem {
private void deleteCookie(String username) {
dbSystem.getDatabase().executeTransaction(CookieChangeTransaction.removeCookie(username));
}
public void removeAll() {
disable();
dbSystem.getDatabase().executeTransaction(CookieChangeTransaction.removeAll());
}
}

View File

@ -34,6 +34,7 @@ public enum Permissions {
REGISTER_OTHER("plan.register.other"),
UNREGISTER_SELF("plan.unregister.self"),
UNREGISTER_OTHER("plan.unregister.other"),
LOGOUT_OTHER("plan.logout.other"),
INFO("plan.info"),
RELOAD("plan.reload"),
DISABLE("plan.disable"),

View File

@ -31,6 +31,7 @@ public enum DeepHelpLang implements Lang {
INGAME("In Depth Help - /plan ingame", "Displays some information about the player in game."),
REGISTER("In Depth Help - /plan register", "Use without arguments to get link to register page. Use --code [code] after registration to get a user."),
UNREGISTER("In Depth Help - /plan unregister", "Use without arguments to unregister player linked user, or with username argument to unregister another user."),
LOGOUT("In Depth Help - /plan logout", "Give username argument to log out another user from the panel, give * as argument to log out everyone."),
INFO("In Depth Help - /plan info", "Display the current status of the plugin."),
RELOAD("In Depth Help - /plan reload", "Disable and enable the plugin to reload any changes in config."),
DISABLE("In Depth Help - /plan disable", "Disable the plugin or part of it until next reload/restart."),

View File

@ -69,7 +69,8 @@ public enum HelpLang implements Lang {
DB_UNINSTALLED("Command Help - /plan db uninstalled", "Set a server as uninstalled in the database."),
EXPORT("Command Help - /plan export", "Export html or json files manually"),
IMPORT("Command Help - /plan import", "Import data"),
JSON("Command Help - /plan json", "View json of Player's raw data.");
JSON("Command Help - /plan json", "View json of Player's raw data."),
LOGOUT("Command Help - /plan logout", "Log out other users from the panel.");
private final String identifier;
private final String defaultValue;

View File

@ -46,6 +46,8 @@ public class CookieTable {
public static final String DELETE_OLDER_STATEMENT = "DELETE FROM " + TABLE_NAME +
WHERE + EXPIRES + "<=?";
public static final String DELETE_ALL_STATEMENT = "DELETE FROM " + TABLE_NAME;
private CookieTable() {
/* Static information class */

View File

@ -45,6 +45,7 @@ public class RemoveEverythingTransaction extends ThrowawayTransaction {
clearTable(TPSTable.TABLE_NAME);
clearTable(SecurityTable.TABLE_NAME);
clearTable(ServerTable.TABLE_NAME);
clearTable(CookieTable.TABLE_NAME);
clearTable(ExtensionPlayerValueTable.TABLE_NAME);
clearTable(ExtensionServerValueTable.TABLE_NAME);
clearTable(ExtensionGroupsTable.TABLE_NAME);

View File

@ -43,9 +43,20 @@ public class CookieChangeTransaction extends Transaction {
return new CookieChangeTransaction(username, null, null);
}
public static CookieChangeTransaction removeAll() {
return new CookieChangeTransaction(null, null, null);
}
@Override
protected void performOperations() {
if (cookie == null) {
if (username == null) {
execute(new ExecStatement(CookieTable.DELETE_ALL_STATEMENT) {
@Override
public void prepare(PreparedStatement statement) throws SQLException {
// No parameters
}
});
} else if (cookie == null) {
execute(new ExecStatement(CookieTable.DELETE_BY_USER_STATEMENT) {
@Override
public void prepare(PreparedStatement statement) throws SQLException {

View File

@ -60,6 +60,8 @@ permissions:
default: true
plan.unregister.other:
default: op
plan.logout.other:
default: op
plan.info:
default: op
plan.reload:

View File

@ -1,3 +1,19 @@
/*
* This file is part of Player Analytics (Plan).
*
* Plan is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License v3 as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Plan 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Plan. If not, see <https://www.gnu.org/licenses/>.
*/
package com.djrapitops.plan.delivery.webserver.auth;
import com.djrapitops.plan.delivery.domain.WebUser;

View File

@ -98,9 +98,7 @@ public interface WebUserQueriesTest extends DatabaseTestPreparer {
cookieStore.removeCookie(cookie);
Map<String, User> result = db().query(WebUserQueries.fetchActiveCookies());
Map<String, User> expected = Collections.emptyMap();
assertEquals(expected, result);
assertTrue(db().query(WebUserQueries.fetchActiveCookies()).isEmpty());
}
@Test
@ -116,8 +114,13 @@ public interface WebUserQueriesTest extends DatabaseTestPreparer {
assertFalse(cookieStore.checkCookie(cookie).isPresent());
Map<String, User> result = db().query(WebUserQueries.fetchActiveCookies());
Map<String, User> expected = Collections.emptyMap();
assertEquals(expected, result);
assertTrue(db().query(WebUserQueries.fetchActiveCookies()).isEmpty());
}
@Test
default void removeEverythingRemovesCookies() {
activeCookieStoreSavesCookies();
db().executeTransaction(new RemoveEverythingTransaction());
assertTrue(db().query(WebUserQueries.fetchActiveCookies()).isEmpty());
}
}