Reworked authentication for users. Now includes option to use database

This commit is contained in:
Josh Stark 2019-03-17 12:50:57 +00:00
parent 44ac92dfcc
commit 6ce1e99438
28 changed files with 1298 additions and 21 deletions

View File

@ -11,7 +11,24 @@ repositories {
mavenCentral()
}
version = '1.0.1'
version = '1.1.0'
sourceSets {
main {
java {
srcDir 'src/main/java'
}
}
test {
java {
srcDir 'src/test/java'
}
}
}
dependencies {
@ -37,11 +54,11 @@ dependencies {
// MISC
compile 'org.apache.commons:commons-lang3:3.7'
compile 'org.bouncycastle:bcprov-jdk15on:1.61'
// Unit Testing
testCompile 'junit:junit:4.11'
testCompile 'org.mockito:mockito-all:1.10.19'
testCompile 'org.mockito:mockito-core:1.10.19'
}
jar {

View File

@ -5,6 +5,9 @@
fleet.app.port=8080
fleet.refresh.interval=60
# If set to DATABASE, fleet.admin.username and fleet.admin.password are not used. They can be omitted.
fleet.admin.authentication.type=PROPERTIES|DATABASE
# User for management of images and repositories
# CHANGE THESE!!
fleet.admin.username=test

View File

@ -0,0 +1,38 @@
/*
* Copyright (c) 2019 LinuxServer.io
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.linuxserver.fleet.auth;
public class UserCredentials {
private final String username;
private final String password;
public UserCredentials(String username, String password) {
this.username = username;
this.password = password;
}
public String getUsername() {
return username;
}
public String getPassword() {
return password;
}
}

View File

@ -0,0 +1,54 @@
/*
* Copyright (c) 2019 LinuxServer.io
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.linuxserver.fleet.auth.authenticator;
import io.linuxserver.fleet.auth.security.PKCS5S2PasswordEncoder;
import io.linuxserver.fleet.core.FleetBeans;
import io.linuxserver.fleet.core.FleetProperties;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class AuthenticatorFactory {
private static final Logger LOGGER = LoggerFactory.getLogger(AuthenticatorFactory.class);
public static UserAuthenticator getAuthenticator(FleetBeans beans) {
FleetProperties properties = beans.getProperties();
AuthenticationType authType = AuthenticationType.valueOf(properties.getAuthenticationType().toUpperCase());
switch (authType) {
case PROPERTIES:
LOGGER.info("Configuring new authenticator: PropertyLoadedUserAuthenticator");
return new PropertyLoadedUserAuthenticator(properties.getAppUsername(), properties.getAppPassword());
case DATABASE:
LOGGER.info("Configuring new authenticator: DatabaseStoredUserAuthenticator");
return new DatabaseStoredUserAuthenticator(beans.getPasswordEncoder(), beans.getUserDelegate());
}
throw new RuntimeException("Provided authentication type is not supported");
}
public enum AuthenticationType {
PROPERTIES, DATABASE
}
}

View File

@ -0,0 +1,60 @@
/*
* Copyright (c) 2019 LinuxServer.io
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.linuxserver.fleet.auth.authenticator;
import io.linuxserver.fleet.auth.AuthenticatedUser;
import io.linuxserver.fleet.auth.AuthenticationResult;
import io.linuxserver.fleet.auth.UserCredentials;
import io.linuxserver.fleet.auth.security.PasswordEncoder;
import io.linuxserver.fleet.delegate.UserDelegate;
import io.linuxserver.fleet.model.User;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class DatabaseStoredUserAuthenticator implements UserAuthenticator {
private static final Logger LOGGER = LoggerFactory.getLogger(DatabaseStoredUserAuthenticator.class);
private final PasswordEncoder passwordEncoder;
private final UserDelegate userDelegate;
public DatabaseStoredUserAuthenticator(PasswordEncoder passwordEncoder, UserDelegate userDelegate) {
this.passwordEncoder = passwordEncoder;
this.userDelegate = userDelegate;
}
@Override
public AuthenticationResult authenticate(UserCredentials userCredentials) {
User user = userDelegate.fetchUserByUsername(userCredentials.getUsername());
if (null == user) {
LOGGER.warn("Attempt to log in with user '{}' failed. Not found.", userCredentials.getUsername());
return AuthenticationResult.notAuthenticated();
}
boolean authenticated = passwordEncoder.matches(userCredentials.getPassword(), user.getPassword());
if (authenticated) {
return new AuthenticationResult(authenticated, new AuthenticatedUser(user.getUsername()));
}
LOGGER.warn("Unable to verify user credentials for user {}", userCredentials.getUsername());
return AuthenticationResult.notAuthenticated();
}
}

View File

@ -15,27 +15,28 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.linuxserver.fleet.delegate;
package io.linuxserver.fleet.auth.authenticator;
import io.linuxserver.fleet.auth.AuthenticatedUser;
import io.linuxserver.fleet.auth.AuthenticationResult;
import io.linuxserver.fleet.auth.UserCredentials;
public class PropertiesAuthenticationDelegate implements AuthenticationDelegate {
public class PropertyLoadedUserAuthenticator implements UserAuthenticator {
private final String adminUsername;
private final String adminPassword;
public PropertiesAuthenticationDelegate(String username, String password) {
public PropertyLoadedUserAuthenticator(String adminUsername, String adminPassword) {
this.adminUsername = username;
this.adminPassword = password;
this.adminUsername = adminUsername;
this.adminPassword = adminPassword;
}
@Override
public AuthenticationResult authenticate(String username, String password) {
public AuthenticationResult authenticate(UserCredentials userCredentials) {
if (adminUsername.equals(username) && adminPassword.equals(password))
return new AuthenticationResult(true, new AuthenticatedUser(username));
if (adminUsername.equals(userCredentials.getUsername()) && adminPassword.equals(userCredentials.getPassword()))
return new AuthenticationResult(true, new AuthenticatedUser(userCredentials.getUsername()));
return AuthenticationResult.notAuthenticated();
}

View File

@ -0,0 +1,38 @@
/*
* Copyright (c) 2019 LinuxServer.io
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.linuxserver.fleet.auth.authenticator;
import io.linuxserver.fleet.auth.AuthenticationResult;
import io.linuxserver.fleet.auth.UserCredentials;
/**
* <p>
* Provides a mechanism for the application to authenticate a login request
* from a user.
* </p>
*/
public interface UserAuthenticator {
/**
* <p>
* Performs an authentication check against the provided credentials and the repository
* of currently stored users.
* </p>
*/
AuthenticationResult authenticate(UserCredentials userCredentials);
}

View File

@ -0,0 +1,168 @@
/*
* Copyright (c) 2019 LinuxServer.io
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.linuxserver.fleet.auth.security;
import io.linuxserver.fleet.auth.security.util.SaltGenerator;
import org.bouncycastle.crypto.PBEParametersGenerator;
import org.bouncycastle.crypto.generators.PKCS5S2ParametersGenerator;
import org.bouncycastle.crypto.params.KeyParameter;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Base64;
/**
* <p>
* Uses the PKCS5S2 crypto algorithm to encode and verify hashed passwords.
* </p>
*/
public class PKCS5S2PasswordEncoder implements PasswordEncoder {
private static final int DEFAULT_HASH_WIDTH = 512;
private static final int DEFAULT_ITERATIONS = 150051;
private final SaltGenerator saltGenerator = new SaltGenerator();
private final byte[] secret;
private final int hashWidth;
private final int iterations;
public PKCS5S2PasswordEncoder(String secret) {
this(secret, DEFAULT_HASH_WIDTH, DEFAULT_ITERATIONS);
}
public PKCS5S2PasswordEncoder(String secret, int hashWidth, int iterations) {
this.secret = secret.getBytes(StandardCharsets.UTF_8);
this.hashWidth = hashWidth;
this.iterations = iterations;
}
@Override
public String encode(String rawPassword) {
if (null == rawPassword) {
throw new IllegalArgumentException("Password must not be null");
}
return toBase64(encode(rawPassword, saltGenerator.generateSalt()));
}
@Override
public boolean matches(String rawPassword, String encodedPassword) {
byte[] decodedHash = fromBase64(encodedPassword);
byte[] saltInHash = extractSalt(decodedHash);
byte[] hashToVerify = encode(rawPassword, saltInHash);
return passwordsMatch(decodedHash, hashToVerify);
}
/**
* <p>
* Compares the two byte arrays by performing a bitwise equality check against each individual
* element of both arrays.
* </p>
*
* @implNote I looked at a couple of implementations for doing this, and I preferred how Spring had implemented it.
*/
private boolean passwordsMatch(byte[] originalPassword, byte[] providedPassword) {
if (originalPassword.length != providedPassword.length) {
return false;
}
int result = 0;
for (int i = 0; i < originalPassword.length; i++) {
result |= originalPassword[i] ^ providedPassword[i];
}
return result == 0;
}
/**
* <p>
* Performs the cryptographic hash against the raw password and the randomly generated salt value. This
* also concatenates the provided secret into the salt.
* </p>
*/
private byte[] encode(String rawPassword, byte[] salt) {
PKCS5S2ParametersGenerator generator = new PKCS5S2ParametersGenerator();
generator.init(
PBEParametersGenerator.PKCS5PasswordToBytes(rawPassword.toCharArray()),
joinArrays(salt, secret),
iterations
);
return joinArrays(salt, ((KeyParameter) generator.generateDerivedMacParameters(hashWidth)).getKey());
}
/**
* <p>
* Converts a byte array into a base-64 encoded string.
* </p>
*/
private String toBase64(byte[] bytes) {
return Base64.getEncoder().encodeToString(bytes);
}
/**
* <p>
* Converts a base64-encoded string into its raw byte value
* </p>
*/
private byte[] fromBase64(String input) {
return Base64.getDecoder().decode(input);
}
/**
* <p>
* Obtains the specific bytes which represent the salt used in a previous hashed password. This is to
* enable the comparision between the existing and new password.
* </p>
*/
private byte[] extractSalt(byte[] decodedHash) {
return extractFromArray(decodedHash, 0, saltGenerator.getKeyLength());
}
/**
* <p>
* Combines two byte arrays together in order.
* </p>
*/
private byte[] joinArrays(byte[] first, byte[] second) {
byte[] result = Arrays.copyOf(first, first.length + second.length);
System.arraycopy(second, 0, result, first.length, second.length);
return result;
}
/**
* <p>
* Extracts a sub-array from the provided array.
* </p>
*/
private byte[] extractFromArray(byte[] array, int begin, int end) {
int length = end - begin;
byte[] subarray = new byte[length];
System.arraycopy(array, begin, subarray, 0, length);
return subarray;
}
}

View File

@ -0,0 +1,55 @@
/*
* Copyright (c) 2019 LinuxServer.io
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.linuxserver.fleet.auth.security;
/**
* <p>
* Provides a mechanism for a password to be encoded using a strong cryptographic algorithm.
* The general idea of this interface has been taken from Spring's own implementation of this.
* <a href="https://docs.spring.io/spring-security/site/docs/4.2.4.RELEASE/apidocs/org/springframework/security/crypto/password/PasswordEncoder.html">PasswordEncoder</a>
* </p>
*/
public interface PasswordEncoder {
/**
* <p>
* Encodes the raw password into a one-way encrypted hash. The result of which should be stored.
* </p>
*
* @param rawPassword
* The raw unencrypted password.
*
* @return
* The hashed result.
*/
String encode(String rawPassword);
/**
* <p>
* Determines if the provided raw password, when encoded, matches the stored encoded password.
* </p>
*
* @param rawPassword
* The raw password to check
* @param encodedPassword
* The originally encoded and stored password
* @return
* true if the passwords match, false if not.
*/
boolean matches(String rawPassword, String encodedPassword);
}

View File

@ -0,0 +1,39 @@
/*
* Copyright (c) 2019 LinuxServer.io
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.linuxserver.fleet.auth.security.util;
import java.security.SecureRandom;
public class SaltGenerator {
private static final int KEY_LENGTH = 16;
public byte[] generateSalt() {
SecureRandom sr = new SecureRandom();
byte[] salt = new byte[KEY_LENGTH];
sr.nextBytes(salt);
return salt;
}
public int getKeyLength() {
return KEY_LENGTH;
}
}

View File

@ -20,6 +20,7 @@ package io.linuxserver.fleet.core;
import io.linuxserver.fleet.web.pages.HomePage;
import io.linuxserver.fleet.web.pages.LoginPage;
import io.linuxserver.fleet.web.pages.ManageRepositoriesPage;
import io.linuxserver.fleet.web.pages.SetupPage;
import io.linuxserver.fleet.web.routes.*;
import io.linuxserver.fleet.web.websocket.SynchronisationWebSocket;
@ -56,11 +57,14 @@ class FleetApp {
beans.getSynchronisationDelegate().registerListener(synchronisationWebSocket);
beans.getWebServer().addWebSocket("/admin/ws/sync", synchronisationWebSocket);
beans.getWebServer().addFilter( "*", new InitialUserFilterRoute(beans.getProperties().getAuthenticationType(), beans.getUserDelegate()));
beans.getWebServer().start();
beans.getWebServer().addPage( "/", new HomePage(beans.getRepositoryDelegate(), beans.getImageDelegate()));
beans.getWebServer().addGetApi( "/api/v1/images", new AllImagesApi(beans.getRepositoryDelegate(), beans.getImageDelegate()));
beans.getWebServer().addPage( "/admin", new ManageRepositoriesPage(beans.getRepositoryDelegate()));
beans.getWebServer().addPage( "/admin/setup", new SetupPage());
beans.getWebServer().addPostRoute( "/admin/setup", new RegisterInitialUserRoute(beans.getUserDelegate()));
beans.getWebServer().addPage( "/admin/login", new LoginPage());
beans.getWebServer().addPostRoute( "/admin/login", new LoginRoute(beans.getAuthenticationDelegate()));
beans.getWebServer().addPostRoute( "/admin/logout", new LogoutRoute());

View File

@ -17,9 +17,13 @@
package io.linuxserver.fleet.core;
import io.linuxserver.fleet.auth.authenticator.AuthenticatorFactory;
import io.linuxserver.fleet.auth.security.PKCS5S2PasswordEncoder;
import io.linuxserver.fleet.auth.security.PasswordEncoder;
import io.linuxserver.fleet.db.DefaultDatabaseConnection;
import io.linuxserver.fleet.db.dao.DefaultImageDAO;
import io.linuxserver.fleet.db.dao.DefaultRepositoryDAO;
import io.linuxserver.fleet.db.dao.DefaultUserDAO;
import io.linuxserver.fleet.db.migration.DatabaseVersion;
import io.linuxserver.fleet.delegate.*;
import io.linuxserver.fleet.dockerhub.DockerHubV2Client;
@ -42,6 +46,8 @@ public class FleetBeans {
private final WebServer webServer;
private final TaskManager taskManager;
private final TaskDelegate taskDelegate;
private final UserDelegate userDelegate;
private final PasswordEncoder passwordEncoder;
/**
* Ensures the database is kept up to date.
@ -54,16 +60,17 @@ public class FleetBeans {
final DefaultDatabaseConnection databaseConnection = new DefaultDatabaseConnection(properties);
passwordEncoder = new PKCS5S2PasswordEncoder(properties.getAppSecret());
databaseVersion = new DatabaseVersion(databaseConnection);
imageDelegate = new ImageDelegate(new DefaultImageDAO(databaseConnection));
repositoryDelegate = new RepositoryDelegate(new DefaultRepositoryDAO(databaseConnection));
dockerHubDelegate = new DockerHubDelegate(new DockerHubV2Client(properties.getDockerHubCredentials()));
authenticationDelegate = new PropertiesAuthenticationDelegate(properties.getAppUsername(), properties.getAppPassword());
webServer = new WebServer(properties.getAppPort());
taskManager = new TaskManager();
synchronisationDelegate = new SynchronisationDelegate(imageDelegate, repositoryDelegate, dockerHubDelegate);
userDelegate = new UserDelegate(passwordEncoder, new DefaultUserDAO(databaseConnection));
taskDelegate = new TaskDelegate(this);
authenticationDelegate = new DefaultAuthenticationDelegate(AuthenticatorFactory.getAuthenticator(this));
}
public FleetProperties getProperties() {
@ -105,4 +112,12 @@ public class FleetBeans {
public TaskDelegate getTaskDelegate() {
return taskDelegate;
}
public UserDelegate getUserDelegate() {
return userDelegate;
}
public PasswordEncoder getPasswordEncoder() {
return passwordEncoder;
}
}

View File

@ -45,6 +45,16 @@ public class FleetProperties {
return getStringProperty("fleet.database.password");
}
public String getAuthenticationType() {
return getStringProperty("fleet.admin.authentication.type");
}
public String getAppSecret() {
String secret = getStringProperty("fleet.admin.secret");
return null == secret ? "" : secret;
}
public String getAppUsername() {
return getStringProperty("fleet.admin.username");
}
@ -82,7 +92,7 @@ public class FleetProperties {
property = System.getProperty(propertyKey);
if (null == property) {
property = System.getenv(propertyKey);
property = System.getenv(propertyKey.replace(".", "_"));
}
}

View File

@ -0,0 +1,157 @@
/*
* Copyright (c) 2019 LinuxServer.io
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.linuxserver.fleet.db.dao;
import io.linuxserver.fleet.db.PoolingDatabaseConnection;
import io.linuxserver.fleet.db.query.InsertUpdateResult;
import io.linuxserver.fleet.db.query.InsertUpdateStatus;
import io.linuxserver.fleet.model.User;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;
import static io.linuxserver.fleet.db.dao.Utils.setNullableInt;
import static io.linuxserver.fleet.db.dao.Utils.setNullableString;
public class DefaultUserDAO implements UserDAO {
private static final Logger LOGGER = LoggerFactory.getLogger(DefaultUserDAO.class);
private final PoolingDatabaseConnection databaseConnection;
public DefaultUserDAO(PoolingDatabaseConnection databaseConnection) {
this.databaseConnection = databaseConnection;
}
@Override
public User fetchUser(int id) {
try (Connection connection = databaseConnection.getConnection()) {
CallableStatement call = connection.prepareCall("{CALL User_Get(?)}");
call.setInt(1, id);
ResultSet results = call.executeQuery();
if (results.next())
return parseUserFromResultSet(results);
} catch (SQLException e) {
LOGGER.error("Unable to retrieve user", e);
}
return null;
}
@Override
public User fetchUserByUsername(String username) {
try (Connection connection = databaseConnection.getConnection()) {
CallableStatement call = connection.prepareCall("{CALL User_GetByName(?)}");
call.setString(1, username);
ResultSet results = call.executeQuery();
if (results.next())
return parseUserFromResultSet(results);
} catch (SQLException e) {
LOGGER.error("Unable to retrieve user", e);
}
return null;
}
@Override
public InsertUpdateResult<User> saveUser(User user) {
try (Connection connection = databaseConnection.getConnection()) {
CallableStatement call = connection.prepareCall("{CALL User_Save(?,?,?,?,?,?)}");
setNullableInt(call, 1, user.getId());
call.setString(2, user.getUsername());
setNullableString(call, 3, user.getPassword());
call.registerOutParameter(4, Types.INTEGER);
call.registerOutParameter(5, Types.INTEGER);
call.registerOutParameter(6, Types.VARCHAR);
call.executeUpdate();
int userId = call.getInt(4);
int status = call.getInt(5);
String statusMessage = call.getString(6);
if (InsertUpdateStatus.OK == status)
return new InsertUpdateResult<>(fetchUser(userId), status, statusMessage);
return new InsertUpdateResult<>(status, statusMessage);
} catch (SQLException e) {
LOGGER.error("Unable to save user", e);
return new InsertUpdateResult<>(null, InsertUpdateStatus.OK, "Unable to save user");
}
}
@Override
public List<User> fetchAllUsers() {
List<User> repositories = new ArrayList<>();
try (Connection connection = databaseConnection.getConnection()) {
CallableStatement call = connection.prepareCall("{CALL User_GetAll()}");
ResultSet results = call.executeQuery();
while (results.next())
repositories.add(parseUserFromResultSet(results));
} catch (SQLException e) {
LOGGER.error("Unable to get all users", e);
}
return repositories;
}
@Override
public void removeUser(User user) {
try (Connection connection = databaseConnection.getConnection()) {
CallableStatement call = connection.prepareCall("{CALL User_Delete(?)}");
call.setInt(1, user.getId());
call.executeUpdate();
} catch (SQLException e) {
LOGGER.error("Error when removing user", e);
}
}
private User parseUserFromResultSet(ResultSet results) throws SQLException {
return new User(
results.getInt("UserId"),
results.getString("UserName"),
results.getString("UserPassword")
).withModifiedTime(results.getTimestamp("ModifiedTime").toLocalDateTime());
}
}

View File

@ -0,0 +1,36 @@
/*
* Copyright (c) 2019 LinuxServer.io
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.linuxserver.fleet.db.dao;
import io.linuxserver.fleet.db.query.InsertUpdateResult;
import io.linuxserver.fleet.model.User;
import java.util.List;
public interface UserDAO {
User fetchUser(int id);
User fetchUserByUsername(String username);
InsertUpdateResult<User> saveUser(User user);
List<User> fetchAllUsers();
void removeUser(User user);
}

View File

@ -0,0 +1,36 @@
/*
* Copyright (c) 2019 LinuxServer.io
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.linuxserver.fleet.delegate;
import io.linuxserver.fleet.auth.AuthenticationResult;
import io.linuxserver.fleet.auth.UserCredentials;
import io.linuxserver.fleet.auth.authenticator.UserAuthenticator;
public class DefaultAuthenticationDelegate implements AuthenticationDelegate {
private final UserAuthenticator authenticator;
public DefaultAuthenticationDelegate(UserAuthenticator authenticator) {
this.authenticator = authenticator;
}
@Override
public AuthenticationResult authenticate(String username, String password) {
return authenticator.authenticate(new UserCredentials(username, password));
}
}

View File

@ -0,0 +1,63 @@
/*
* Copyright (c) 2019 LinuxServer.io
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.linuxserver.fleet.delegate;
import io.linuxserver.fleet.auth.security.PasswordEncoder;
import io.linuxserver.fleet.db.dao.UserDAO;
import io.linuxserver.fleet.db.query.InsertUpdateResult;
import io.linuxserver.fleet.db.query.InsertUpdateStatus;
import io.linuxserver.fleet.exception.SaveException;
import io.linuxserver.fleet.model.User;
public class UserDelegate {
private final PasswordEncoder passwordEncoder;
private final UserDAO userDAO;
public UserDelegate(PasswordEncoder passwordEncoder, UserDAO userDAO) {
this.passwordEncoder = passwordEncoder;
this.userDAO = userDAO;
}
public User fetchUser(int id) {
return userDAO.fetchUser(id);
}
public User fetchUserByUsername(String username) {
return userDAO.fetchUserByUsername(username);
}
public boolean isUserRepositoryEmpty() {
return userDAO.fetchAllUsers().isEmpty();
}
public User createNewUser(String username, String password) throws SaveException {
InsertUpdateResult<User> result = userDAO.saveUser(new User(username, passwordEncoder.encode(password)));
if (result.getStatus() == InsertUpdateStatus.OK)
return result.getResult();
throw new SaveException(result.getStatusMessage());
}
public void removeUser(User user) {
userDAO.removeUser(user);
}
}

View File

@ -0,0 +1,46 @@
/*
* Copyright (c) 2019 LinuxServer.io
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.linuxserver.fleet.model;
public class User extends PersistableItem<User> {
private final String username;
private final String password;
public User(String username, String password) {
super();
this.username = username;
this.password = password;
}
public User(Integer id, String username, String password) {
super(id);
this.username = username;
this.password = password;
}
public String getUsername() {
return username;
}
public String getPassword() {
return password;
}
}

View File

@ -20,10 +20,7 @@ package io.linuxserver.fleet.web;
import io.linuxserver.fleet.auth.AuthenticatedUser;
import io.linuxserver.fleet.model.api.ApiResponse;
import io.linuxserver.fleet.model.api.FleetApiException;
import spark.Route;
import spark.RouteGroup;
import spark.Session;
import spark.TemplateViewRoute;
import spark.*;
import spark.template.freemarker.FreeMarkerEngine;
import static spark.Spark.*;
@ -45,7 +42,6 @@ public class WebServer {
started = true;
path("/admin", configureAuthorisationRoute(""));
path("/admin", configureAuthorisationRoute("/repositories"));
path("/admin", configureAuthorisationRoute("/images"));
path("/admin", configureAuthorisationRoute("/manageImage"));
path("/admin", configureAuthorisationRoute("/manageRepository"));
@ -71,6 +67,10 @@ public class WebServer {
});
}
public void addFilter(String path, Filter route) {
before(path, route);
}
public void addWebSocket(String path, Object object) {
if (started) {

View File

@ -0,0 +1,31 @@
/*
* Copyright (c) 2019 LinuxServer.io
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.linuxserver.fleet.web.pages;
import spark.ModelAndView;
import spark.Request;
import java.util.HashMap;
public class SetupPage extends WebPage {
@Override
protected ModelAndView handle(Request request) {
return new ModelAndView(new HashMap<>(), "setup.ftl");
}
}

View File

@ -0,0 +1,84 @@
/*
* Copyright (c) 2019 LinuxServer.io
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.linuxserver.fleet.web.routes;
import io.linuxserver.fleet.auth.authenticator.AuthenticatorFactory.AuthenticationType;
import io.linuxserver.fleet.delegate.UserDelegate;
import spark.Filter;
import spark.Request;
import spark.Response;
/**
* <p>
* Forces the maintainer of Fleet to set up an initial user before running Fleet.
* </p>
*/
public class InitialUserFilterRoute implements Filter {
private static final String FLEET_USER_UNDEFINED = "fleet.user.undefined";
private static final String[] PATH_EXEMPTIONS = {
"/assets", "/admin/setup"
};
private final UserDelegate userDelegate;
private final boolean databaseAuthenticationEnabled;
public InitialUserFilterRoute(String authenticationType, UserDelegate userDelegate) {
this.userDelegate = userDelegate;
this.databaseAuthenticationEnabled = AuthenticationType.DATABASE.equals(AuthenticationType.valueOf(authenticationType.toUpperCase()));
}
@Override
public void handle(Request request, Response response) {
if (databaseAuthenticationEnabled) {
if (!initialUserNeedsConfiguring() && "/admin/setup".equals(request.pathInfo())) {
response.redirect("/admin");
}
else if (initialUserNeedsConfiguring() && !pathIsExempted(request.pathInfo())) {
response.redirect("/admin/setup");
}
}
}
private boolean initialUserNeedsConfiguring() {
String configured = System.getProperty(FLEET_USER_UNDEFINED);
if (null == configured || "true".equalsIgnoreCase(configured)) {
System.setProperty(FLEET_USER_UNDEFINED, String.valueOf(userDelegate.isUserRepositoryEmpty()));
}
return "true".equalsIgnoreCase(System.getProperty(FLEET_USER_UNDEFINED));
}
private boolean pathIsExempted(String path) {
for (String exemption : PATH_EXEMPTIONS) {
if (path.startsWith(exemption)) {
return true;
}
}
return false;
}
}

View File

@ -0,0 +1,64 @@
/*
* Copyright (c) 2019 LinuxServer.io
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.linuxserver.fleet.web.routes;
import io.linuxserver.fleet.delegate.UserDelegate;
import io.linuxserver.fleet.exception.SaveException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import spark.Request;
import spark.Response;
import spark.Route;
public class RegisterInitialUserRoute implements Route {
private static final Logger LOGGER = LoggerFactory.getLogger(RegisterInitialUserRoute.class);
private UserDelegate userDelegate;
public RegisterInitialUserRoute(UserDelegate userDelegate) {
this.userDelegate = userDelegate;
}
@Override
public Object handle(Request request, Response response) {
String username = request.queryParams("username");
String password = request.queryParams("password");
String verifyPassword = request.queryParams("verify-password");
if (!verifyPassword.equals(password)) {
response.redirect("/admin/setup?passwordMismatch=true");
return null;
}
try {
userDelegate.createNewUser(username, password);
response.redirect("/admin");
} catch (SaveException e) {
response.redirect("/admin/setup?createUserError=true");
LOGGER.error("Unable to create new user", e);
}
return null;
}
}

View File

@ -310,4 +310,35 @@ var synchronisationManager = (function($) {
init: init
}
}(jQuery));
var passwordValidationManager = (function($) {
var comparePasswords = function(password, verifyPassword) {
if (password.val() !== verifyPassword.val()) {
verifyPassword.get(0).setCustomValidity('Mismatch');
} else {
verifyPassword.get(0).setCustomValidity('');
}
};
var init = function() {
var password = $('#password');
var verifyPassword = $('#verify-password');
password.on('keyup', function() {
comparePasswords(password, verifyPassword);
});
verifyPassword.on('keyup', function() {
comparePasswords(password, verifyPassword);
});
};
return {
init: init
};
}(jQuery));

View File

@ -0,0 +1,7 @@
CREATE TABLE Users (
`id` INT NOT NULL auto_increment PRIMARY KEY,
`username` VARCHAR(255) NOT NULL,
`password` VARCHAR(255) DEFAULT NULL,
`modified` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY (`username`)
) ENGINE=InnoDB;

View File

@ -0,0 +1,111 @@
DELIMITER //
CREATE PROCEDURE `User_Save`
(
in_id INT,
in_username VARCHAR(255),
in_password VARCHAR(255),
OUT out_id INT,
OUT out_status INT,
OUT out_message VARCHAR(100)
)
BEGIN
IF in_id IS NULL THEN
INSERT INTO Users
(
`username`,
`password`
)
VALUES
(
in_username,
in_password
);
SET out_id = LAST_INSERT_ID();
ELSE
UPDATE Repositories
SET
`username` = in_username,
`password` = in_password
WHERE
`id` = in_id;
SET out_id = in_id;
END IF;
SET out_status = 0;
SET out_message = 'OK';
END;
//
CREATE PROCEDURE `User_GetAll` ()
BEGIN
SELECT
`id` AS `UserId`,
`username` AS `UserName`,
`password` AS `UserPassword`,
`modified` AS `ModifiedTime`
FROM
Users;
END;
//
CREATE PROCEDURE `User_Get`
(
in_id INT
)
BEGIN
SELECT
`id` AS `UserId`,
`username` AS `UserName`,
`password` AS `UserPassword`,
`modified` AS `ModifiedTime`
FROM
Users
WHERE
`id` = in_id;
END;
//
CREATE PROCEDURE `User_GetByName`
(
in_username VARCHAR(255)
)
BEGIN
SELECT
`id` AS `UserId`,
`username` AS `UserName`,
`password` AS `UserPassword`,
`modified` AS `ModifiedTime`
FROM
Users
WHERE
`username` = in_username;
END;
//
CREATE PROCEDURE `User_Delete`
(
in_id INT
)
BEGIN
DELETE FROM Users WHERE `id` = in_id;
END;
//
DELIMITER ;

View File

@ -69,15 +69,37 @@
$("table.table--sortable").tablesorter();
});
<#if context='admin'>
<#if context="admin">
repositoryManager.init();
synchronisationManager.init();
</#if>
<#if context='home'>
<#if context="home">
imageListManager.init();
</#if>
<#if context="setup" || context="login">
(function() {
'use strict';
window.addEventListener('load', function() {
var forms = document.getElementsByClassName('needs-validation');
var validation = Array.prototype.filter.call(forms, function(form) {
form.addEventListener('submit', function(event) {
if (form.checkValidity() === false) {
event.preventDefault();
event.stopPropagation();
}
form.classList.add('was-validated');
}, false);
});
}, false);
})();
passwordValidationManager.init();
</#if>
</script>
</html>

View File

@ -0,0 +1,46 @@
<#import "./base.ftl" as base>
<@base.base title="Set Up" context="setup">
<div class="container container--white mt-3">
<div class="row">
<div class="col-12 p-3">
<h2>Set Up Fleet</h2>
<p>
It looks like this is the first time you're running Fleet. In order to get started, you need to create
and initial user which will have access to the management pages of the application.
</p>
<h3 class="mb-3">Create a user</h3>
<form action="/admin/setup" method="POST" class="needs-validation" novalidate>
<div class="form-group row">
<label for="username" class="col-sm-2 col-form-label">Username</label>
<div class="col-sm-10">
<input type="text" class="form-control" name="username" id="username" required />
</div>
</div>
<div class="form-group row">
<label for="password" class="col-sm-2 col-form-label">Password</label>
<div class="col-sm-10">
<input type="password" class="form-control" name="password" id="password" required />
</div>
</div>
<div class="form-group row">
<label for="verify-password" class="col-sm-2 col-form-label">Verify Password</label>
<div class="col-sm-10">
<input type="password" class="form-control" name="verify-password" id="verify-password" required />
</div>
</div>
<div class="form-group text-center">
<button type="submit" class="btn btn-primary">Continue</button>
</div>
</form>
</div>
</div>
</div>
</@base.base>

View File

@ -0,0 +1,41 @@
/*
* Copyright (c) 2019 LinuxServer.io
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.linuxserver.fleet.auth.security;
import org.junit.Test;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
public class PKCS5S2PasswordEncoderTest {
private static final String HASH_FOR_PASSWORD = "8SLrEokFZQQrRk1MeDrpgINOHNXKXPMs2r56DMWBCjLXs9oTHsLzEmnwb68oQAc9+1YdKPTdWahAjjqtvO9M8FtQxCC+8yd71+J1VoWizow=";
private PKCS5S2PasswordEncoder encoder = new PKCS5S2PasswordEncoder("superSecret");
@Test
public void shouldGenerateHash() {
assertThat(encoder.matches("password", HASH_FOR_PASSWORD), is(equalTo(true)));
}
@Test
public void test() {
System.out.println(encoder.encode("password").length());
}
}