Linked web users to players or console

PageExtension API:
- Added WebUser#getUsername
- Changed WebUser#getName to return player's name or 'console'
- API version 5.1-R0.4
This commit is contained in:
Risto Lahtela 2020-05-03 23:26:01 +03:00
parent 2f282dc7cc
commit ea2ae5d3e1
26 changed files with 464 additions and 150 deletions

View File

@ -7,7 +7,7 @@ dependencies {
compileOnly "com.google.code.gson:gson:$gsonVersion"
}
ext.apiVersion = '5.1-R0.3'
ext.apiVersion = '5.1-R0.4'
bintray {
user = System.getenv('BINTRAY_USER')

View File

@ -17,21 +17,34 @@
package com.djrapitops.plan.delivery.web.resolver.request;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
public final class WebUser {
private final String name;
private final String playerName;
private final String username;
private final Set<String> permissions;
public WebUser(String name) {
this.name = name;
public WebUser(String playerName) {
this.playerName = playerName;
this.username = playerName;
this.permissions = new HashSet<>();
}
public WebUser(String name, String... permissions) {
this(name);
public WebUser(String playerName, String username, Collection<String> permissions) {
this.playerName = playerName;
this.username = username;
this.permissions = new HashSet<>(permissions);
}
/**
* @deprecated WebUser now has username and player name.
*/
@Deprecated
public WebUser(String playerName, String... permissions) {
this(playerName);
this.permissions.addAll(Arrays.asList(permissions));
}
@ -40,6 +53,10 @@ public final class WebUser {
}
public String getName() {
return name;
return playerName;
}
public String getUsername() {
return username;
}
}

View File

@ -0,0 +1,65 @@
/*
* 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.commands;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
/**
* Utility for managing command arguments.
*
* @author Rsl1122
*/
public class Arguments {
private final List<String> args;
public Arguments(String[] args) {
this.args = Arrays.asList(args);
}
public Arguments(List<String> args) {
this.args = args;
}
public Optional<String> get(int index) {
return index < args.size() ? Optional.of(args.get(index)) : Optional.empty();
}
public Optional<Integer> getInteger(int index) {
return get(index).map(Integer::parseInt);
}
public Optional<String> getAfter(String argumentIdentifier) {
for (int i = 0; i < args.size(); i++) {
String argument = args.get(i);
if (argumentIdentifier.equals(argument)) {
return get(i + 1);
}
}
return Optional.empty();
}
public boolean contains(String argument) {
return args.contains(argument);
}
public List<String> asList() {
return args;
}
}

View File

@ -16,11 +16,13 @@
*/
package com.djrapitops.plan.commands.subcommands;
import com.djrapitops.plan.delivery.domain.WebUser;
import com.djrapitops.plan.commands.Arguments;
import com.djrapitops.plan.delivery.domain.auth.User;
import com.djrapitops.plan.delivery.webserver.Addresses;
import com.djrapitops.plan.delivery.webserver.auth.FailReason;
import com.djrapitops.plan.delivery.webserver.auth.RegistrationBin;
import com.djrapitops.plan.exceptions.database.DBOpException;
import com.djrapitops.plan.identification.UUIDUtility;
import com.djrapitops.plan.processing.Processing;
import com.djrapitops.plan.settings.Permissions;
import com.djrapitops.plan.settings.locale.Locale;
@ -39,13 +41,13 @@ import com.djrapitops.plugin.command.Sender;
import com.djrapitops.plugin.logging.L;
import com.djrapitops.plugin.logging.console.PluginLogger;
import com.djrapitops.plugin.logging.error.ErrorHandler;
import com.djrapitops.plugin.utilities.Verify;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.util.Arrays;
import java.util.List;
import java.util.Collections;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.ExecutionException;
/**
@ -65,6 +67,7 @@ public class RegisterCommand extends CommandNode {
private final Locale locale;
private final Processing processing;
private final DBSystem dbSystem;
private final UUIDUtility uuidUtility;
private final Addresses addresses;
private final PluginLogger logger;
private final ErrorHandler errorHandler;
@ -75,15 +78,17 @@ public class RegisterCommand extends CommandNode {
Processing processing,
Addresses addresses,
DBSystem dbSystem,
UUIDUtility uuidUtility,
PluginLogger logger,
ErrorHandler errorHandler
) {
// No Permission Requirement
super("register", "", CommandType.PLAYER_OR_ARGS);
super("register", "", CommandType.ALL);
this.locale = locale;
this.processing = processing;
this.addresses = addresses;
this.uuidUtility = uuidUtility;
this.logger = logger;
this.dbSystem = dbSystem;
this.errorHandler = errorHandler;
@ -104,27 +109,23 @@ public class RegisterCommand extends CommandNode {
return;
}
if (args.length < 1) {
throw new IllegalArgumentException(locale.getString(CommandLang.FAIL_REQ_ARGS, 1, Arrays.toString(getArguments())));
if (args.length == 0) {
String url = addresses.getMainAddress().orElseGet(() -> {
sender.sendMessage(locale.getString(CommandLang.NO_ADDRESS_NOTIFY));
return addresses.getFallbackLocalhostAddress();
}) + "/register";
String linkPrefix = locale.getString(CommandLang.LINK_PREFIX);
sender.sendMessage(linkPrefix);
sender.sendLink(" ", locale.getString(CommandLang.LINK_CLICK_ME), url);
return;
}
List<String> argumentList = Arrays.asList(args);
boolean newRegister = argumentList.contains("--code");
if (newRegister) {
if (CommandUtils.isPlayer(sender)) {
register(argumentList, getPermissionLevel(sender), sender);
} else if (argumentList.contains("superuser")) {
register(argumentList, 0, sender);
} else {
sender.sendMessage("§cInvalid arguments.");
}
Arguments arguments = new Arguments(args);
Optional<String> code = arguments.getAfter("--code");
if (code.isPresent()) {
registerUsingCode(sender, code.get());
} else {
// Legacy support
if (CommandUtils.isPlayer(sender)) {
playerRegister(args, sender);
} else {
consoleRegister(args, sender, notEnoughArgsMsg);
}
registerUsingLegacy(sender, arguments);
}
} catch (PassEncryptUtil.CannotPerformOperationException e) {
errorHandler.log(L.WARN, this.getClass(), e);
@ -138,43 +139,33 @@ public class RegisterCommand extends CommandNode {
}
}
private void consoleRegister(String[] args, Sender sender, String notEnoughArgsMsg) throws PassEncryptUtil.CannotPerformOperationException {
Verify.isTrue(args.length >= 3, () -> new IllegalArgumentException(notEnoughArgsMsg));
String userName = args[1];
Verify.isTrue(userName.length() <= 100, () -> new IllegalArgumentException("Username can only be 100 characters long."));
int permLevel = Integer.parseInt(args[2]);
String passHash = PassEncryptUtil.createHash(args[0]);
registerUser(new WebUser(userName, passHash, permLevel), sender);
}
private void register(List<String> args, int permissionLevel, Sender sender) {
String code = "";
for (String arg : args) {
if (arg.length() == 12) code = arg;
}
if (code.isEmpty()) {
throw new IllegalArgumentException(locale.getString(CommandLang.FAIL_REQ_ARGS, 1, "--code !<code>!"));
}
Optional<WebUser> user = RegistrationBin.register(code, permissionLevel);
if (!user.isPresent()) {
public void registerUsingCode(Sender sender, String code) {
UUID linkedToUUID = CommandUtils.isPlayer(sender) ? uuidUtility.getUUIDOf(sender.getName()) : null;
Optional<User> user = RegistrationBin.register(code, linkedToUUID);
if (user.isPresent()) {
registerUser(user.get(), sender, getPermissionLevel(sender));
} else {
sender.sendMessage("§c" + locale.getString(FailReason.USER_DOES_NOT_EXIST));
} else {
registerUser(user.get(), sender);
}
}
private void playerRegister(String[] args, Sender sender) throws PassEncryptUtil.CannotPerformOperationException {
boolean registerSenderAsUser = args.length == 1;
if (registerSenderAsUser) {
String user = sender.getName();
String pass = PassEncryptUtil.createHash(args[0]);
int permLvl = getPermissionLevel(sender);
registerUser(new WebUser(user, pass, permLvl), sender);
} else if (sender.hasPermission(Permissions.MANAGE_WEB.getPermission())) {
consoleRegister(args, sender, notEnoughArgsMsg);
public void registerUsingLegacy(Sender sender, Arguments arguments) {
String password = arguments.get(0)
.orElseThrow(() -> new IllegalArgumentException(locale.getString(CommandLang.FAIL_REQ_ARGS, 1, Arrays.toString(getArguments()))));
String passwordHash = PassEncryptUtil.createHash(password);
int permissionLevel = arguments.getInteger(2)
.filter(arg -> sender.hasPermission(Permissions.MANAGE_WEB.getPerm())) // argument only allowed with plan.webmanage
.orElseGet(() -> getPermissionLevel(sender));
if (CommandUtils.isPlayer(sender)) {
String playerName = sender.getName();
UUID linkedToUUID = uuidUtility.getUUIDOf(playerName);
String username = arguments.get(1).orElse(playerName);
registerUser(new User(username, playerName, linkedToUUID, passwordHash, Collections.emptyList()), sender, permissionLevel);
} else {
sender.sendMessage(locale.getString(CommandLang.FAIL_NO_PERMISSION));
String username = arguments.get(1)
.orElseThrow(() -> new IllegalArgumentException(notEnoughArgsMsg));
registerUser(new User(username, "console", null, passwordHash, Collections.emptyList()), sender, permissionLevel);
}
}
@ -194,22 +185,22 @@ public class RegisterCommand extends CommandNode {
return 100;
}
private void registerUser(WebUser webUser, Sender sender) {
private void registerUser(User user, Sender sender, int permissionLevel) {
processing.submitCritical(() -> {
String userName = webUser.getName();
String username = user.getUsername();
try {
Database database = dbSystem.getDatabase();
boolean userExists = database.query(WebUserQueries.fetchWebUser(userName)).isPresent();
boolean userExists = database.query(WebUserQueries.fetchUser(username)).isPresent();
if (userExists) {
sender.sendMessage(locale.getString(CommandLang.FAIL_WEB_USER_EXISTS));
return;
}
database.executeTransaction(new RegisterWebUserTransaction(webUser))
database.executeTransaction(new RegisterWebUserTransaction(user, permissionLevel))
.get(); // Wait for completion
sender.sendMessage(locale.getString(CommandLang.WEB_USER_REGISTER_SUCCESS, userName));
sender.sendMessage(locale.getString(CommandLang.WEB_USER_REGISTER_SUCCESS, username));
sendLink(sender);
logger.info(locale.getString(CommandLang.WEB_USER_REGISTER_NOTIFY, userName, webUser.getPermLevel()));
logger.info(locale.getString(CommandLang.WEB_USER_REGISTER_NOTIFY, username, permissionLevel));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} catch (DBOpException | ExecutionException e) {

View File

@ -30,18 +30,32 @@ import java.util.Objects;
@Deprecated
public class WebUser {
private final String user;
private final String username;
private final String saltedPassHash;
private final int permLevel;
public WebUser(String user, String saltedPassHash, int permLevel) {
this.user = user;
public WebUser(String username, String saltedPassHash, int permLevel) {
this.username = username;
this.saltedPassHash = saltedPassHash;
this.permLevel = permLevel;
}
public String getName() {
return user;
public static List<String> getPermissionsForLevel(int level) {
List<String> permissions = new ArrayList<>();
if (level <= 0) {
permissions.add("page.network");
permissions.add("page.server");
permissions.add("page.debug");
// TODO Add JSON Permissions
}
if (level <= 1) {
permissions.add("page.players");
permissions.add("page.player.other");
}
if (level <= 2) {
permissions.add("page.player.self");
}
return permissions;
}
public String getSaltedPassHash() {
@ -52,38 +66,28 @@ public class WebUser {
return permLevel;
}
public String getName() {
return username;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
WebUser webUser = (WebUser) o;
return permLevel == webUser.permLevel &&
Objects.equals(user, webUser.user) &&
Objects.equals(username, webUser.username) &&
Objects.equals(saltedPassHash, webUser.saltedPassHash);
}
@Override
public int hashCode() {
return Objects.hash(user, saltedPassHash, permLevel);
return Objects.hash(username, saltedPassHash, permLevel);
}
public com.djrapitops.plan.delivery.web.resolver.request.WebUser toNewWebUser() {
List<String> permissions = new ArrayList<>();
if (permLevel <= 0) {
permissions.add("page.network");
permissions.add("page.server");
permissions.add("page.debug");
// TODO Add JSON Permissions
}
if (permLevel <= 1) {
permissions.add("page.players");
permissions.add("page.player.other");
}
if (permLevel <= 2) {
permissions.add("page.player.self");
}
return new com.djrapitops.plan.delivery.web.resolver.request.WebUser(
user, permissions.toArray(new String[0])
username, getPermissionsForLevel(permLevel).toArray(new String[0])
);
}
}

View File

@ -0,0 +1,69 @@
/*
* 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.domain.auth;
import com.djrapitops.plan.delivery.web.resolver.request.WebUser;
import com.djrapitops.plan.utilities.PassEncryptUtil;
import java.util.Collection;
import java.util.UUID;
/**
* Represents a registered user in the database.
*
* @author Rsl1122
*/
public class User {
private final String username;
private final String linkedTo;
private final UUID linkedToUUID; // null for 'console'
private final String passwordHash;
private final Collection<String> permissions;
public User(String username, String linkedTo, UUID linkedToUUID, String passwordHash, Collection<String> permissions) {
this.username = username;
this.linkedTo = linkedTo;
this.linkedToUUID = linkedToUUID;
this.passwordHash = passwordHash;
this.permissions = permissions;
}
public boolean doesPasswordMatch(String password) {
return PassEncryptUtil.verifyPassword(password, passwordHash);
}
public WebUser toWebUser() {
return new WebUser(linkedTo, username, permissions);
}
public String getUsername() {
return username;
}
public String getLinkedTo() {
return linkedTo;
}
public UUID getLinkedToUUID() {
return linkedToUUID;
}
public String getPasswordHash() {
return passwordHash;
}
}

View File

@ -16,6 +16,7 @@
*/
package com.djrapitops.plan.delivery.webserver;
import com.djrapitops.plan.delivery.domain.auth.User;
import com.djrapitops.plan.delivery.web.resolver.Response;
import com.djrapitops.plan.delivery.web.resolver.request.Request;
import com.djrapitops.plan.delivery.web.resolver.request.URIPath;
@ -153,8 +154,8 @@ public class RequestHandler implements HttpHandler {
private WebUser getWebUser(HttpExchange exchange) {
return getAuthentication(exchange.getRequestHeaders())
.map(Authentication::getWebUser) // Can throw WebUserAuthException
.map(com.djrapitops.plan.delivery.domain.WebUser::toNewWebUser)
.map(Authentication::getUser) // Can throw WebUserAuthException
.map(User::toWebUser)
.orElse(null);
}

View File

@ -16,7 +16,7 @@
*/
package com.djrapitops.plan.delivery.webserver.auth;
import com.djrapitops.plan.delivery.domain.WebUser;
import com.djrapitops.plan.delivery.domain.auth.User;
import org.apache.commons.codec.digest.DigestUtils;
import java.util.HashMap;
@ -26,14 +26,14 @@ import java.util.UUID;
public class ActiveCookieStore {
private static final Map<String, WebUser> USERS_BY_COOKIE = new HashMap<>();
private static final Map<String, User> USERS_BY_COOKIE = new HashMap<>();
public static Optional<WebUser> checkCookie(String cookie) {
public static Optional<User> checkCookie(String cookie) {
return Optional.ofNullable(USERS_BY_COOKIE.get(cookie));
}
public static String generateNewCookie(WebUser user) {
String cookie = DigestUtils.sha256Hex(user.getName() + UUID.randomUUID() + System.currentTimeMillis());
public static String generateNewCookie(User user) {
String cookie = DigestUtils.sha256Hex(user.getUsername() + UUID.randomUUID() + System.currentTimeMillis());
USERS_BY_COOKIE.put(cookie, user);
return cookie;
}
@ -42,8 +42,8 @@ public class ActiveCookieStore {
USERS_BY_COOKIE.remove(cookie);
}
public static void removeCookie(WebUser user) {
USERS_BY_COOKIE.entrySet().stream().filter(entry -> entry.getValue().getName().equals(user.getName()))
public static void removeCookie(User user) {
USERS_BY_COOKIE.entrySet().stream().filter(entry -> entry.getValue().getUsername().equals(user.getUsername()))
.findAny()
.map(Map.Entry::getKey)
.ifPresent(ActiveCookieStore::removeCookie);

View File

@ -16,7 +16,7 @@
*/
package com.djrapitops.plan.delivery.webserver.auth;
import com.djrapitops.plan.delivery.domain.WebUser;
import com.djrapitops.plan.delivery.domain.auth.User;
import com.djrapitops.plan.exceptions.WebUserAuthException;
/**
@ -32,6 +32,6 @@ public interface Authentication {
* @return Web user for the authentication.
* @throws WebUserAuthException If user can't be authenticated
*/
WebUser getWebUser();
User getUser();
}

View File

@ -16,14 +16,13 @@
*/
package com.djrapitops.plan.delivery.webserver.auth;
import com.djrapitops.plan.delivery.domain.WebUser;
import com.djrapitops.plan.delivery.domain.auth.User;
import com.djrapitops.plan.exceptions.PassEncryptException;
import com.djrapitops.plan.exceptions.WebUserAuthException;
import com.djrapitops.plan.exceptions.database.DBOpException;
import com.djrapitops.plan.storage.database.Database;
import com.djrapitops.plan.storage.database.queries.objects.WebUserQueries;
import com.djrapitops.plan.utilities.Base64Util;
import com.djrapitops.plan.utilities.PassEncryptUtil;
import org.apache.commons.lang3.StringUtils;
import java.util.Arrays;
@ -47,7 +46,7 @@ public class BasicAuthentication implements Authentication {
}
@Override
public WebUser getWebUser() {
public User getUser() {
String decoded = Base64Util.decode(authenticationString);
String[] userInfo = StringUtils.split(decoded, ':');
@ -55,7 +54,7 @@ public class BasicAuthentication implements Authentication {
throw new WebUserAuthException(FailReason.USER_AND_PASS_NOT_SPECIFIED, Arrays.toString(userInfo));
}
String user = userInfo[0];
String username = userInfo[0];
String passwordRaw = userInfo[1];
Database.State dbState = database.getState();
@ -64,14 +63,14 @@ public class BasicAuthentication implements Authentication {
}
try {
WebUser webUser = database.query(WebUserQueries.fetchWebUser(user))
.orElseThrow(() -> new WebUserAuthException(FailReason.USER_DOES_NOT_EXIST, user));
User user = database.query(WebUserQueries.fetchUser(username))
.orElseThrow(() -> new WebUserAuthException(FailReason.USER_DOES_NOT_EXIST, username));
boolean correctPass = PassEncryptUtil.verifyPassword(passwordRaw, webUser.getSaltedPassHash());
boolean correctPass = user.doesPasswordMatch(passwordRaw);
if (!correctPass) {
throw new WebUserAuthException(FailReason.USER_PASS_MISMATCH, user);
throw new WebUserAuthException(FailReason.USER_PASS_MISMATCH, username);
}
return webUser;
return user;
} catch (DBOpException | PassEncryptException e) {
throw new WebUserAuthException(e);
}

View File

@ -16,7 +16,7 @@
*/
package com.djrapitops.plan.delivery.webserver.auth;
import com.djrapitops.plan.delivery.domain.WebUser;
import com.djrapitops.plan.delivery.domain.auth.User;
public class CookieAuthentication implements Authentication {
@ -27,7 +27,7 @@ public class CookieAuthentication implements Authentication {
}
@Override
public WebUser getWebUser() {
public User getUser() {
return ActiveCookieStore.checkCookie(cookie).orElse(null);
}
}

View File

@ -16,13 +16,11 @@
*/
package com.djrapitops.plan.delivery.webserver.auth;
import com.djrapitops.plan.delivery.domain.WebUser;
import com.djrapitops.plan.delivery.domain.auth.User;
import com.djrapitops.plan.utilities.PassEncryptUtil;
import org.apache.commons.codec.digest.DigestUtils;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.*;
/**
* Holds registrations of users before they are confirmed.
@ -33,18 +31,18 @@ public class RegistrationBin {
private static final Map<String, AwaitingForRegistration> REGISTRATION_BIN = new HashMap<>();
public static String addInfoForRegistration(String username, String password) throws PassEncryptUtil.CannotPerformOperationException {
public static String addInfoForRegistration(String username, String password) {
String hash = PassEncryptUtil.createHash(password);
String code = DigestUtils.sha256Hex(username + password + System.currentTimeMillis()).substring(0, 12);
REGISTRATION_BIN.put(code, new AwaitingForRegistration(username, hash));
return code;
}
public static Optional<WebUser> register(String code, int permissionLevel) {
public static Optional<User> register(String code, UUID linkedToUUID) {
AwaitingForRegistration found = REGISTRATION_BIN.get(code);
if (found == null) return Optional.empty();
REGISTRATION_BIN.remove(code);
return Optional.of(found.toWebUser(permissionLevel));
return Optional.of(found.toUser(linkedToUUID));
}
public static boolean contains(String code) {
@ -60,8 +58,8 @@ public class RegistrationBin {
this.passwordHash = passwordHash;
}
public WebUser toWebUser(int permissionLevel) {
return new WebUser(username, passwordHash, permissionLevel);
public User toUser(UUID linkedToUUID) {
return new User(username, null, linkedToUUID, passwordHash, Collections.emptyList());
}
}
}

View File

@ -16,7 +16,7 @@
*/
package com.djrapitops.plan.delivery.webserver.resolver.auth;
import com.djrapitops.plan.delivery.domain.WebUser;
import com.djrapitops.plan.delivery.domain.auth.User;
import com.djrapitops.plan.delivery.web.resolver.NoAuthResolver;
import com.djrapitops.plan.delivery.web.resolver.Response;
import com.djrapitops.plan.delivery.web.resolver.exception.BadRequestException;
@ -29,7 +29,6 @@ import com.djrapitops.plan.exceptions.WebUserAuthException;
import com.djrapitops.plan.exceptions.database.DBOpException;
import com.djrapitops.plan.storage.database.DBSystem;
import com.djrapitops.plan.storage.database.queries.objects.WebUserQueries;
import com.djrapitops.plan.utilities.PassEncryptUtil;
import javax.inject.Inject;
import javax.inject.Singleton;
@ -52,7 +51,7 @@ public class LoginResolver implements NoAuthResolver {
@Override
public Optional<Response> resolve(Request request) {
try {
String cookie = ActiveCookieStore.generateNewCookie(getWebUser(request));
String cookie = ActiveCookieStore.generateNewCookie(getUser(request));
return Optional.of(getResponse(cookie));
} catch (DBOpException | PassEncryptException e) {
throw new WebUserAuthException(e);
@ -67,17 +66,17 @@ public class LoginResolver implements NoAuthResolver {
.build();
}
public WebUser getWebUser(Request request) throws PassEncryptUtil.CannotPerformOperationException, PassEncryptUtil.InvalidHashException {
public User getUser(Request request) {
URIQuery query = request.getQuery();
String username = query.get("user").orElseThrow(() -> new BadRequestException("'user' parameter not defined"));
String password = query.get("password").orElseThrow(() -> new BadRequestException("'password' parameter not defined"));
WebUser webUser = dbSystem.getDatabase().query(WebUserQueries.fetchWebUser(username))
User user = dbSystem.getDatabase().query(WebUserQueries.fetchUser(username))
.orElseThrow(() -> new BadRequestException(FailReason.USER_DOES_NOT_EXIST.getReason() + ": " + username));
boolean correctPass = PassEncryptUtil.verifyPassword(password, webUser.getSaltedPassHash());
boolean correctPass = user.doesPasswordMatch(password);
if (!correctPass) {
throw new WebUserAuthException(FailReason.USER_PASS_MISMATCH);
}
return webUser;
return user;
}
}

View File

@ -57,7 +57,7 @@ public class RegisterResolver implements NoAuthResolver {
String username = query.get("user").orElseThrow(() -> new BadRequestException("'user' parameter not defined"));
boolean alreadyExists = dbSystem.getDatabase().query(WebUserQueries.fetchWebUser(username)).isPresent();
boolean alreadyExists = dbSystem.getDatabase().query(WebUserQueries.fetchUser(username)).isPresent();
if (alreadyExists) throw new BadRequestException("User '" + username + "' already exists!");
String password = query.get("password").orElseThrow(() -> new BadRequestException("'password' parameter not defined"));

View File

@ -16,7 +16,7 @@
*/
package com.djrapitops.plan.exceptions;
public class PassEncryptException extends Exception {
public class PassEncryptException extends IllegalArgumentException {
public PassEncryptException(String s) {
super(s);

View File

@ -170,7 +170,9 @@ public abstract class SQLDB extends AbstractDatabase {
new ExtensionTableRowValueLengthPatch(),
new CommandUsageTableRemovalPatch(),
new RegisterDateMinimizationPatch(),
new BadNukkitRegisterValuePatch()
new BadNukkitRegisterValuePatch(),
new LinkedToSecurityTablePatch(),
new LinkUsersToPlayersSecurityTablePatch()
};
}

View File

@ -17,10 +17,12 @@
package com.djrapitops.plan.storage.database.queries.objects;
import com.djrapitops.plan.delivery.domain.WebUser;
import com.djrapitops.plan.delivery.domain.auth.User;
import com.djrapitops.plan.storage.database.queries.Query;
import com.djrapitops.plan.storage.database.queries.QueryAllStatement;
import com.djrapitops.plan.storage.database.queries.QueryStatement;
import com.djrapitops.plan.storage.database.sql.tables.SecurityTable;
import com.djrapitops.plan.storage.database.sql.tables.UsersTable;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
@ -28,11 +30,12 @@ import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import static com.djrapitops.plan.storage.database.sql.building.Sql.*;
/**
* Queries for {@link WebUser} objects.
* Queries for web user objects.
*
* @author Rsl1122
*/
@ -47,6 +50,7 @@ public class WebUserQueries {
*
* @return List of Plan WebUsers.
*/
@Deprecated
public static Query<List<WebUser>> fetchAllPlanWebUsers() {
String sql = SELECT + '*' + FROM + SecurityTable.TABLE_NAME + ORDER_BY + SecurityTable.PERMISSION_LEVEL + " ASC";
@ -66,6 +70,31 @@ public class WebUserQueries {
};
}
public static Query<Optional<User>> fetchUser(String username) {
String sql = SELECT + '*' + FROM + SecurityTable.TABLE_NAME +
LEFT_JOIN + UsersTable.TABLE_NAME + " on " + SecurityTable.LINKED_TO + "=" + UsersTable.USER_UUID +
WHERE + SecurityTable.USERNAME + "=? LIMIT 1";
return new QueryStatement<Optional<User>>(sql) {
@Override
public void prepare(PreparedStatement statement) throws SQLException {
statement.setString(1, username);
}
@Override
public Optional<User> processResults(ResultSet set) throws SQLException {
if (set.next()) {
String linkedTo = set.getString(UsersTable.USER_NAME);
UUID linkedToUUID = linkedTo != null ? UUID.fromString(set.getString(SecurityTable.LINKED_TO)) : null;
String passwordHash = set.getString(SecurityTable.SALT_PASSWORD_HASH);
List<String> permissions = WebUser.getPermissionsForLevel(set.getInt(SecurityTable.PERMISSION_LEVEL));
return Optional.of(new User(username, linkedTo != null ? linkedTo : "console", linkedToUUID, passwordHash, permissions));
}
return Optional.empty();
}
};
}
@Deprecated
public static Query<Optional<WebUser>> fetchWebUser(String called) {
String sql = SELECT + '*' + FROM + SecurityTable.TABLE_NAME +
WHERE + SecurityTable.USERNAME + "=? LIMIT 1";

View File

@ -18,7 +18,6 @@ package com.djrapitops.plan.storage.database.sql.tables;
import com.djrapitops.plan.storage.database.DBType;
import com.djrapitops.plan.storage.database.sql.building.CreateTableBuilder;
import com.djrapitops.plan.storage.database.sql.building.Insert;
import com.djrapitops.plan.storage.database.sql.building.Sql;
/**
@ -31,10 +30,15 @@ public class SecurityTable {
public static final String TABLE_NAME = "plan_security";
public static final String USERNAME = "username";
public static final String LINKED_TO = "linked_to_uuid";
public static final String SALT_PASSWORD_HASH = "salted_pass_hash";
public static final String PERMISSION_LEVEL = "permission_level";
public static final String INSERT_STATEMENT = Insert.values(TABLE_NAME, USERNAME, SALT_PASSWORD_HASH, PERMISSION_LEVEL);
public static final String INSERT_STATEMENT = "INSERT INTO " + TABLE_NAME + " (" +
USERNAME + ',' +
LINKED_TO + ',' +
SALT_PASSWORD_HASH + ',' +
PERMISSION_LEVEL + ") VALUES (?,?,?,?)";
private SecurityTable() {
/* Static information class */
@ -43,6 +47,7 @@ public class SecurityTable {
public static String createTableSQL(DBType dbType) {
return CreateTableBuilder.create(TABLE_NAME, dbType)
.column(USERNAME, Sql.varchar(100)).notNull().unique()
.column(LINKED_TO, Sql.varchar(36))
.column(SALT_PASSWORD_HASH, Sql.varchar(100)).notNull().unique()
.column(PERMISSION_LEVEL, Sql.INT).notNull()
.toString();

View File

@ -16,25 +16,28 @@
*/
package com.djrapitops.plan.storage.database.transactions.commands;
import com.djrapitops.plan.delivery.domain.WebUser;
import com.djrapitops.plan.delivery.domain.auth.User;
import com.djrapitops.plan.storage.database.sql.tables.SecurityTable;
import com.djrapitops.plan.storage.database.transactions.ExecStatement;
import com.djrapitops.plan.storage.database.transactions.Transaction;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.sql.Types;
/**
* Transaction to save a new Plan {@link WebUser} to the database.
* Transaction to save a new Plan {@link User} to the database.
*
* @author Rsl1122
*/
public class RegisterWebUserTransaction extends Transaction {
private final WebUser webUser;
private final User user;
private final int permissionLevel;
public RegisterWebUserTransaction(WebUser webUser) {
this.webUser = webUser;
public RegisterWebUserTransaction(User user, int permissionLevel) {
this.user = user;
this.permissionLevel = permissionLevel;
}
@Override
@ -42,9 +45,14 @@ public class RegisterWebUserTransaction extends Transaction {
execute(new ExecStatement(SecurityTable.INSERT_STATEMENT) {
@Override
public void prepare(PreparedStatement statement) throws SQLException {
statement.setString(1, webUser.getName());
statement.setString(2, webUser.getSaltedPassHash());
statement.setInt(3, webUser.getPermLevel());
statement.setString(1, user.getUsername());
if (user.getLinkedToUUID() == null) {
statement.setNull(2, Types.VARCHAR);
} else {
statement.setString(2, user.getLinkedToUUID().toString());
}
statement.setString(3, user.getPasswordHash());
statement.setInt(4, permissionLevel);
}
});
}

View File

@ -0,0 +1,78 @@
/*
* 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.storage.database.transactions.patches;
import com.djrapitops.plan.storage.database.queries.HasMoreThanZeroQueryStatement;
import com.djrapitops.plan.storage.database.queries.QueryAllStatement;
import com.djrapitops.plan.storage.database.sql.building.Sql;
import com.djrapitops.plan.storage.database.sql.tables.SecurityTable;
import com.djrapitops.plan.storage.database.sql.tables.UsersTable;
import com.djrapitops.plan.storage.database.transactions.ExecBatchStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Map;
import static com.djrapitops.plan.storage.database.sql.building.Sql.*;
public class LinkUsersToPlayersSecurityTablePatch extends Patch {
@Override
public boolean hasBeenApplied() {
String sql = SELECT + "COUNT(1) as c" + FROM + SecurityTable.TABLE_NAME +
WHERE + SecurityTable.LINKED_TO + "=''";
return !query(new HasMoreThanZeroQueryStatement(sql) {
@Override
public void prepare(PreparedStatement statement) {
// No preparation necessary
}
});
}
@Override
protected void applyPatch() {
String querySQL = SELECT + UsersTable.USER_UUID + ',' + SecurityTable.USERNAME +
FROM + SecurityTable.TABLE_NAME +
LEFT_JOIN + UsersTable.TABLE_NAME + " on " + UsersTable.USER_NAME + "=" + SecurityTable.USERNAME +
WHERE + SecurityTable.LINKED_TO + "=''";
String sql = "UPDATE " + SecurityTable.TABLE_NAME + " SET " + SecurityTable.LINKED_TO + "=?" +
WHERE + SecurityTable.USERNAME + "=?";
Map<String, String> byUsername = query(new QueryAllStatement<Map<String, String>>(querySQL) {
@Override
public Map<String, String> processResults(ResultSet set) throws SQLException {
Map<String, String> byUsername = new HashMap<>();
while (set.next()) {
byUsername.put(set.getString(SecurityTable.USERNAME), set.getString(UsersTable.USER_UUID));
}
return byUsername;
}
});
execute(new ExecBatchStatement(sql) {
@Override
public void prepare(PreparedStatement statement) throws SQLException {
for (Map.Entry<String, String> usernameUUIDPair : byUsername.entrySet()) {
Sql.setStringOrNull(statement, 1, usernameUUIDPair.getValue());
statement.setString(2, usernameUUIDPair.getKey());
statement.addBatch();
}
}
});
}
}

View File

@ -0,0 +1,33 @@
/*
* 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.storage.database.transactions.patches;
import com.djrapitops.plan.storage.database.sql.building.Sql;
import com.djrapitops.plan.storage.database.sql.tables.SecurityTable;
public class LinkedToSecurityTablePatch extends Patch {
@Override
public boolean hasBeenApplied() {
return hasColumn(SecurityTable.TABLE_NAME, SecurityTable.LINKED_TO);
}
@Override
protected void applyPatch() {
addColumn(SecurityTable.TABLE_NAME, SecurityTable.LINKED_TO + ' ' + Sql.varchar(36) + " DEFAULT ''");
}
}

View File

@ -55,11 +55,18 @@ public class PassEncryptUtil {
throw new IllegalStateException("Utility class");
}
public static String createHash(String password) throws CannotPerformOperationException {
/**
* Create a hash of password + salt.
*
* @param password Password
* @return Hash + salt
* @throws CannotPerformOperationException If the hash creation fails
*/
public static String createHash(String password) {
return createHash(password.toCharArray());
}
private static String createHash(char[] password) throws CannotPerformOperationException {
private static String createHash(char[] password) {
// Generate a random salt
SecureRandom random = new SecureRandom();
byte[] salt = new byte[SALT_BYTE_SIZE];
@ -77,11 +84,20 @@ public class PassEncryptUtil {
+ ":" + toBase64(hash);
}
public static boolean verifyPassword(String password, String correctHash) throws CannotPerformOperationException, InvalidHashException {
/**
* Verify that a password matches a hash.
*
* @param password Password
* @param correctHash hash created with {@link PassEncryptUtil#createHash(String)}
* @return true if match
* @throws CannotPerformOperationException If hashing fails
* @throws InvalidHashException If the hash is missing details.
*/
public static boolean verifyPassword(String password, String correctHash) {
return verifyPassword(password.toCharArray(), correctHash);
}
private static boolean verifyPassword(char[] password, String correctHash) throws CannotPerformOperationException, InvalidHashException {
private static boolean verifyPassword(char[] password, String correctHash) {
// Decode the hash into its parameters
String[] params = StringUtils.split(correctHash, ':');
if (params.length != HASH_SECTIONS) {
@ -160,7 +176,7 @@ public class PassEncryptUtil {
return diff == 0;
}
private static byte[] pbkdf2(char[] password, byte[] salt, int iterations, int bytes) throws CannotPerformOperationException {
private static byte[] pbkdf2(char[] password, byte[] salt, int iterations, int bytes) {
try {
PBEKeySpec spec = new PBEKeySpec(password, salt, iterations, bytes * 8);
SecretKeyFactory skf = SecretKeyFactory.getInstance(PBKDF2_ALGORITHM);

View File

@ -59,7 +59,7 @@ class JksHttpsServerTest implements HttpsServerTest {
system.enable();
WebUser webUser = new WebUser("test", PassEncryptUtil.createHash("testPass"), 0);
system.getDatabaseSystem().getDatabase().executeTransaction(new RegisterWebUserTransaction(webUser));
system.getDatabaseSystem().getDatabase().executeTransaction(new RegisterWebUserTransaction(webUser, ));
}
@AfterAll

View File

@ -64,7 +64,7 @@ class Pkcs12HttpsServerTest implements HttpsServerTest {
system.enable();
WebUser webUser = new WebUser("test", PassEncryptUtil.createHash("testPass"), 0);
system.getDatabaseSystem().getDatabase().executeTransaction(new RegisterWebUserTransaction(webUser));
system.getDatabaseSystem().getDatabase().executeTransaction(new RegisterWebUserTransaction(webUser, ));
}
@AfterAll

View File

@ -68,7 +68,7 @@ public interface DatabaseBackupTest extends DatabaseTestPreparer {
);
WebUser webUser = new WebUser(TestConstants.PLAYER_ONE_NAME, RandomData.randomString(100), 0);
db().executeTransaction(new RegisterWebUserTransaction(webUser));
db().executeTransaction(new RegisterWebUserTransaction(webUser, ));
}
@Test

View File

@ -34,7 +34,7 @@ public interface WebUserQueriesTest extends DatabaseTestPreparer {
@Test
default void webUserIsRegistered() {
WebUser expected = new WebUser(TestConstants.PLAYER_ONE_NAME, "RandomGarbageBlah", 0);
db().executeTransaction(new RegisterWebUserTransaction(expected));
db().executeTransaction(new RegisterWebUserTransaction(expected, ));
forcePersistenceCheck();
Optional<WebUser> found = db().query(WebUserQueries.fetchWebUser(TestConstants.PLAYER_ONE_NAME));