Merge pull request #1 from linuxserver/1.1.0_database_auth

1.1.0 database auth
This commit is contained in:
Josh Stark 2019-03-20 20:40:44 +00:00 committed by GitHub
commit b7aa227d09
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
38 changed files with 10459 additions and 195 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 {
@ -82,7 +99,7 @@ minifyJs {
combineCss {
source = [
'src/main/resources/assets/css/bootstrap.min.css',
'src/main/resources/assets/css/bootstrap.css',
'src/main/resources/assets/css/fleet.css',
'src/main/resources/assets/css/fontawesome-all.css'
]

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,53 @@
/*
* 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 DATABASE:
LOGGER.info("Configuring new authenticator: DatabaseStoredUserAuthenticator");
return new DatabaseStoredUserAuthenticator(beans.getPasswordEncoder(), beans.getUserDelegate());
case PROPERTIES:
default:
LOGGER.info("Configuring new authenticator: PropertyLoadedUserAuthenticator");
return new PropertyLoadedUserAuthenticator(properties.getAppUsername(), properties.getAppPassword());
}
}
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,169 @@
/*
* 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.digests.SHA256Digest;
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(new SHA256Digest());
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

@ -17,25 +17,54 @@
package io.linuxserver.fleet.core;
import io.linuxserver.fleet.auth.AuthenticatedUser;
import io.linuxserver.fleet.auth.authenticator.AuthenticatorFactory.AuthenticationType;
import io.linuxserver.fleet.model.api.ApiResponse;
import io.linuxserver.fleet.model.api.FleetApiException;
import io.linuxserver.fleet.web.JsonTransformer;
import io.linuxserver.fleet.web.SessionAttribute;
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;
import spark.Session;
import java.util.concurrent.TimeUnit;
import static spark.Spark.*;
/**
* <p>
* Primary entry point for the application. All contexts and resources are loaded
* through this class.
* </p>
*/
class FleetApp {
public class FleetApp {
private static final String FLEET_USER_UNDEFINED = "fleet.user.undefined";
private static FleetApp instance;
public static FleetApp instance() {
if (null == instance) {
synchronized (FleetApp.class) {
if (null == instance) {
instance = new FleetApp();
}
}
}
return instance;
}
private final FleetBeans beans;
FleetApp() {
private FleetApp() {
beans = new FleetBeans();
}
@ -52,25 +81,138 @@ class FleetApp {
private void configureWeb() {
port(beans.getProperties().getAppPort());
staticFiles.location("/assets");
staticFiles.expireTime(600);
SynchronisationWebSocket synchronisationWebSocket = new SynchronisationWebSocket();
beans.getSynchronisationDelegate().registerListener(synchronisationWebSocket);
beans.getWebServer().addWebSocket("/admin/ws/sync", synchronisationWebSocket);
beans.getWebServer().start();
webSocket("/admin/ws/sync", synchronisationWebSocket);
init();
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/login", new LoginPage());
beans.getWebServer().addPostRoute( "/admin/login", new LoginRoute(beans.getAuthenticationDelegate()));
beans.getWebServer().addPostRoute( "/admin/logout", new LogoutRoute());
beans.getWebServer().addPostApi( "/admin/manageImage", new ManageImageApi(beans.getImageDelegate()));
beans.getWebServer().addGetApi( "/admin/getImage", new GetImageApi(beans.getImageDelegate()));
beans.getWebServer().addPostApi( "/admin/manageRepository", new ManageRepositoryApi(beans.getRepositoryDelegate()));
beans.getWebServer().addPostApi( "/admin/forceSync", new ForceSyncApi(beans.getTaskDelegate()));
/* -----------------------
* Set Up
* -----------------------
*/
if (initialUserNeedsConfiguring()) {
path("/setup", () -> {
before("", (request, response) -> {
if (!initialUserNeedsConfiguring()) {
halt(401);
}
});
get("", new SetupPage());
post("", new RegisterInitialUserRoute(beans.getUserDelegate()));
});
}
/* -----------------------
* Image List and Log In
* -----------------------
*/
path("/", () -> {
get("", new HomePage(beans.getRepositoryDelegate(), beans.getImageDelegate()));
get("/login", new LoginPage());
post("/login", new LoginRoute(beans.getAuthenticationDelegate()));
post("/logout", new LogoutRoute());
});
/* -----------------------
* API
* -----------------------
*/
path("/api/v1", () -> {
get("/images", new AllImagesApi(beans.getRepositoryDelegate(), beans.getImageDelegate()), new JsonTransformer());
after("/*", (request, response) -> {
response.header("Access-Control-Allow-Origin", "*");
response.header("Access-Control-Allow-Methods", "GET");
response.header("Content-Type","application/json");
});
});
/* -----------------------
* Admin
* -----------------------
*/
path("/admin", () -> {
before("", (request, response) -> {
Session session = request.session(false);
if (null == session)
response.redirect("/login");
else {
AuthenticatedUser user = session.attribute(SessionAttribute.USER);
if (null == user)
response.redirect("/login");
}
});
before("/*", (request, response) -> {
Session session = request.session(false);
if (null == session)
response.redirect("/login");
else {
AuthenticatedUser user = session.attribute(SessionAttribute.USER);
if (null == user)
response.redirect("/login");
}
});
get("", new ManageRepositoriesPage(beans.getRepositoryDelegate()));
get("/api/getImage", new GetImageApi(beans.getImageDelegate()), new JsonTransformer());
post("/api/manageImage", new ManageImageApi(beans.getImageDelegate()), new JsonTransformer());
post("/api/manageRepository", new ManageRepositoryApi(beans.getRepositoryDelegate()), new JsonTransformer());
post("/api/forceSync", new ForceSyncApi(beans.getTaskDelegate()), new JsonTransformer());
after("/api/*", (request, response) -> response.header("Content-Type", "application/json"));
});
/* -----------------------
* API Error Handling
* -----------------------
*/
exception(FleetApiException.class, (exception, request, response) -> {
response.body(new JsonTransformer().render(new ApiResponse<>("ERROR", exception.getMessage())));
response.header("Content-Type", "application/json");
response.status(exception.getStatusCode());
});
}
private void scheduleSync() {
beans.getTaskDelegate().scheduleSynchronisationTask(beans.getProperties().getRefreshIntervalInMinutes(), TimeUnit.MINUTES);
}
private boolean initialUserNeedsConfiguring() {
String configured = System.getProperty(FLEET_USER_UNDEFINED);
if (null == configured || "true".equalsIgnoreCase(configured)) {
System.setProperty(FLEET_USER_UNDEFINED, String.valueOf(beans.getUserDelegate().isUserRepositoryEmpty()));
}
return "true".equalsIgnoreCase(System.getProperty(FLEET_USER_UNDEFINED)) && databaseAuthenticationEnabled();
}
private boolean databaseAuthenticationEnabled() {
return AuthenticationType.DATABASE == AuthenticationType.valueOf(beans.getProperties().getAuthenticationType());
}
}

View File

@ -17,14 +17,17 @@
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;
import io.linuxserver.fleet.thread.TaskManager;
import io.linuxserver.fleet.web.WebServer;
/**
* <p>
@ -39,9 +42,10 @@ public class FleetBeans {
private final AuthenticationDelegate authenticationDelegate;
private final DockerHubDelegate dockerHubDelegate;
private final SynchronisationDelegate synchronisationDelegate;
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 +58,16 @@ 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() {
@ -94,10 +98,6 @@ public class FleetBeans {
return databaseVersion;
}
public WebServer getWebServer() {
return webServer;
}
public TaskManager getTaskManager() {
return taskManager;
}
@ -105,4 +105,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

@ -20,8 +20,6 @@ package io.linuxserver.fleet.core;
public class Main {
public static void main(String[] args) {
FleetApp app = new FleetApp();
app.run();
FleetApp.instance().run();
}
}

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

@ -240,10 +240,20 @@ public class DefaultSynchronisationState implements SynchronisationState {
}
private void onStart(SynchronisationContext context) {
synchronized (running) {
running.set(true);
}
context.getListeners().forEach(SynchronisationListener::onSynchronisationStart);
}
private void onFinish(SynchronisationContext context) {
synchronized (running) {
running.set(false);
}
context.getListeners().forEach(SynchronisationListener::onSynchronisationFinish);
}

View File

@ -1,116 +0,0 @@
/*
* 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;
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.template.freemarker.FreeMarkerEngine;
import static spark.Spark.*;
public class WebServer {
private boolean started;
public WebServer(int appPort) {
port(appPort);
staticFiles.location("/assets");
staticFiles.expireTime(600);
}
public void start() {
started = true;
path("/admin", configureAuthorisationRoute(""));
path("/admin", configureAuthorisationRoute("/repositories"));
path("/admin", configureAuthorisationRoute("/images"));
path("/admin", configureAuthorisationRoute("/manageImage"));
path("/admin", configureAuthorisationRoute("/manageRepository"));
path("/admin", configureAuthorisationRoute("/forceSync"));
path("/admin", configureAuthorisationRoute("/getImage"));
after("/api/v1/*", (request, response) -> {
response.header("Access-Control-Allow-Origin", "*");
response.header("Access-Control-Allow-Methods", "GET");
response.header("Content-Type", "application/json");
});
after("/admin/getImage", (request, response) -> {
response.header("Content-Type", "application/json");
});
exception(FleetApiException.class, (exception, request, response) -> {
response.body(new JsonTransformer().render(new ApiResponse<>("ERROR", exception.getMessage())));
response.header("Content-Type", "application/json");
response.status(exception.getStatusCode());
});
}
public void addWebSocket(String path, Object object) {
if (started) {
throw new IllegalStateException("Server has already started! Add a web socket before starting");
}
webSocket(path, object);
}
public void addPage(String path, TemplateViewRoute page) {
get(path, page, new FreeMarkerEngine());
}
public void addPostRoute(String path, Route route) {
post(path, route);
}
public void addGetApi(String path, Route route) {
get(path, route, new JsonTransformer());
}
public void addPostApi(String path, Route route) {
post(path, route, new JsonTransformer());
}
private RouteGroup configureAuthorisationRoute(String path) {
return () -> before(path, (request, response) -> {
Session session = request.session(false);
if (null == session)
response.redirect("/admin/login");
else {
AuthenticatedUser user = session.attribute(SessionAttribute.USER);
if (null == user)
response.redirect("/admin/login");
}
});
}
}

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

@ -19,14 +19,15 @@ package io.linuxserver.fleet.web.pages;
import io.linuxserver.fleet.web.SessionAttribute;
import spark.*;
import spark.template.freemarker.FreeMarkerEngine;
import java.util.Map;
public abstract class WebPage implements TemplateViewRoute {
public abstract class WebPage implements Route {
@Override
@SuppressWarnings("unchecked")
public ModelAndView handle(Request request, Response response) {
public Object handle(Request request, Response response) {
ModelAndView modelAndView = handle(request);
@ -34,7 +35,7 @@ public abstract class WebPage implements TemplateViewRoute {
if (null != session && null != session.attribute(SessionAttribute.USER))
((Map<String, Object>) modelAndView.getModel()).put("__AUTHENTICATED_USER", session.attribute(SessionAttribute.USER));
return modelAndView;
return new FreeMarkerEngine().render(modelAndView);
}
protected abstract ModelAndView handle(Request request);

View File

@ -49,7 +49,7 @@ public class LoginRoute implements Route {
AuthenticationResult authResult = authenticationDelegate.authenticate(username, password);
if (!authResult.isAuthenticated()) {
response.redirect("/admin/login?fail=true");
response.redirect("/login?fail=true");
return null;
}

View File

@ -44,7 +44,6 @@ public class ManageImageApi implements Route {
if (null == image) {
response.status(404);
response.header("Content-Type", "application/json");
return new ApiResponse<>("Error", "Image not found.");
}
@ -67,7 +66,6 @@ public class ManageImageApi implements Route {
imageDelegate.saveImage(image);
response.header("Content-Type", "application/json");
return new ApiResponse<>("OK", "Image updated.");
} catch (Exception e) {

View File

@ -44,7 +44,6 @@ public class ManageRepositoryApi implements Route {
if (null == repository) {
response.status(404);
response.header("Content-Type", "application/json");
return new ApiResponse<>("Error", "Repository not found.");
}
@ -64,7 +63,6 @@ public class ManageRepositoryApi implements Route {
repositoryDelegate.saveRepository(repository);
response.header("Content-Type", "application/json");
return new ApiResponse<>("OK", "Repository updated.");
} catch (Exception e) {

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("/setup?passwordMismatch=true");
return null;
}
try {
userDelegate.createNewUser(username, password);
response.redirect("/login");
} catch (SaveException e) {
response.redirect("/setup?createUserError=true");
LOGGER.error("Unable to create new user", e);
}
return null;
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -22,6 +22,14 @@ h2 {
font-size: 30px;
}
.navbar-toggler {
border: 0;
}
.navbar-toggler:focus {
outline: none;
}
.navbar-darkish {
background-color: #cdcdcd;
@ -75,6 +83,14 @@ h2 {
font-size: 16px;
}
.table tbody tr td.image-name span.image-name--repository {
font-weight: normal;
}
.dropdown-menu {
font-size: 0.8rem;
}
code {
background-color: #efefef;
@ -96,7 +112,7 @@ code {
-moz-box-shadow: 0px 0px 5px 0px rgba(82,63,105,0.05);
box-shadow: 0px 0px 5px 0px rgba(82,63,105,0.05);
padding: 10px;
padding: 20px;
border: 1px solid #d4dadf;
font-size: 0.9rem;

View File

@ -43,7 +43,7 @@ var repositoryManager = (function($) {
var syncEnabled = syncSwitch.is(':checked');
var request = {
url: '/admin/manageRepository',
url: '/admin/api/manageRepository',
method: 'POST',
data: {
repositoryId: repositoryId,
@ -62,7 +62,7 @@ var repositoryManager = (function($) {
var maskValue = versionMask.val();
var request = {
url: '/admin/manageRepository',
url: '/admin/api/manageRepository',
method: 'POST',
data: {
repositoryId: repositoryId,
@ -174,7 +174,7 @@ var imageListManager = (function($) {
var buildRequest = function(action, imageId) {
return {
url: '/admin/manageImage',
url: '/admin/api/manageImage',
method: 'POST',
data: {
action: action,
@ -204,7 +204,7 @@ var imageListManager = (function($) {
};
var createStableIcon = function() {
return $('<i class="fas fa-check-circle text-success" title="No issues reported"></i>');
return $('<i class="fas fa-check text-success" title="No issues reported"></i>');
};
var getImageRow = function(item) {
@ -222,7 +222,7 @@ var imageListManager = (function($) {
var getImageMask = function(imageId, callback) {
var request = {
url: '/admin/getImage?imageId=' + imageId,
url: '/admin/api/getImage?imageId=' + imageId,
method: 'GET'
};
@ -277,7 +277,7 @@ var synchronisationManager = (function($) {
var startSynchronisation = function() {
var request = {
url: '/admin/forceSync',
url: '/admin/api/forceSync',
method: 'POST'
};
@ -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

@ -7,6 +7,7 @@
<title>${title}</title>
<link rel="shortcut icon" type="image/png" href="/images/favicon-32x32.png"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<link href="https://fonts.googleapis.com/css?family=Pacifico|Nunito" rel="stylesheet" />
<link rel="stylesheet" type="text/css" href="/css/all.min.css" />
@ -21,7 +22,9 @@
<#if __AUTHENTICATED_USER?has_content>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
<span class="navbar-toggler-icon align-middle">
<i class="fas fa-bars"></i>
</span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
@ -32,7 +35,7 @@
</a>
<div class="dropdown-menu" aria-labelledby="navbarDropdown">
<a class="dropdown-item" href="admin">Manage Repositories</a>
<form action="/admin/logout" method="POST">
<form action="/logout" method="POST">
<button class="dropdown-item">Log Out</button>
</form>
</div>
@ -69,15 +72,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

@ -64,7 +64,7 @@
<#if !image.hidden || __AUTHENTICATED_USER?has_content>
<tr <#if image.hidden>class="hidden-image"</#if> data-image-id="#{image.id}" data-image-name="${image.name}">
<td class="image-name">
<a target="_blank" href="https://hub.docker.com/r/${populatedRepository.repository.name}/${image.name}">${image.name}</a>
<span class="image-name--repository">${populatedRepository.repository.name} /</span> <a target="_blank" href="https://hub.docker.com/r/${populatedRepository.repository.name}/${image.name}">${image.name}</a>
</td>
<td>
<#if image.version?has_content>
@ -79,7 +79,7 @@
<#if image.unstable>
<i class="fas fa-exclamation-triangle text-warning" title="Potentially unstable"></i>
<#else>
<i class="fas fa-check-circle text-success" title="No issues reported"></i>
<i class="fas fa-check text-success" title="No issues reported"></i>
</#if>
</td>

View File

@ -5,22 +5,31 @@
<div class="container container--white mt-3">
<div class="row">
<div class="col-12 p-3">
<h2>Log In</h2>
<div class="col-sm-12 col-md-3"></div>
<div class="col-sm-12 col-md-6">
<form action="/admin/login" method="POST">
<div class="card">
<div class="card-body">
<h2 class="card-title">Log In</h2>
<form action="/login" method="POST">
<div class="form-group">
<label for="username">Username</label>
<input type="text" class="form-control" name="username" id="username" />
<div class="form-group">
<label for="username">Username</label>
<input type="text" class="form-control form-control-sm" name="username" id="username" />
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" class="form-control form-control-sm" name="password" id="password" />
</div>
<div class="form-group text-center">
<button type="submit" class="btn btn-primary">Log In</button>
</div>
</form>
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" class="form-control" name="password" id="password" />
</div>
<button type="submit" class="btn btn-primary">Log In</button>
</form>
</div>
</div>
<div class="col-sm-12 col-md-3"></div>
</div>
</div>

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-sm-12">
<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="/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 form-control-sm" 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 form-control-sm" 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 form-control-sm" 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());
}
}