
com.arcadedb.server.security.ServerSecurity Maven / Gradle / Ivy
The newest version!
/*
* Copyright © 2021-present Arcade Data Ltd ([email protected])
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* SPDX-FileCopyrightText: 2021-present Arcade Data Ltd ([email protected])
* SPDX-License-Identifier: Apache-2.0
*/
package com.arcadedb.server.security;
import com.arcadedb.ContextConfiguration;
import com.arcadedb.GlobalConfiguration;
import com.arcadedb.database.DatabaseFactory;
import com.arcadedb.database.DatabaseInternal;
import com.arcadedb.log.LogManager;
import com.arcadedb.security.SecurityManager;
import com.arcadedb.serializer.json.JSONException;
import com.arcadedb.serializer.json.JSONObject;
import com.arcadedb.server.ArcadeDBServer;
import com.arcadedb.server.DefaultConsoleReader;
import com.arcadedb.server.ServerException;
import com.arcadedb.server.ServerPlugin;
import com.arcadedb.server.security.credential.CredentialsValidator;
import com.arcadedb.server.security.credential.DefaultCredentialsValidator;
import com.arcadedb.utility.AnsiCode;
import com.arcadedb.utility.LRUCache;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import java.io.*;
import java.nio.file.*;
import java.nio.charset.*;
import java.security.*;
import java.security.spec.*;
import java.util.*;
import java.util.logging.*;
import static com.arcadedb.GlobalConfiguration.SERVER_SECURITY_ALGORITHM;
import static com.arcadedb.GlobalConfiguration.SERVER_SECURITY_RELOAD_EVERY;
import static com.arcadedb.GlobalConfiguration.SERVER_SECURITY_SALT_CACHE_SIZE;
import static com.arcadedb.GlobalConfiguration.SERVER_SECURITY_SALT_ITERATIONS;
public class ServerSecurity implements ServerPlugin, com.arcadedb.security.SecurityManager {
public static final int LATEST_VERSION = 1;
private final ArcadeDBServer server;
private final SecurityUserFileRepository usersRepository;
private final SecurityGroupFileRepository groupRepository;
private final String algorithm;
private final SecretKeyFactory secretKeyFactory;
private final Map saltCache;
private final int saltIteration;
private final Map users = new HashMap<>();
private final int checkConfigReloadEveryMs;
private CredentialsValidator credentialsValidator = new DefaultCredentialsValidator();
private static final Random RANDOM = new SecureRandom();
public static final int SALT_SIZE = 32;
private Timer reloadConfigurationTimer;
public ServerSecurity(final ArcadeDBServer server, final ContextConfiguration configuration, final String configPath) {
this.server = server;
this.algorithm = configuration.getValueAsString(SERVER_SECURITY_ALGORITHM);
this.checkConfigReloadEveryMs = configuration.getValueAsInteger(SERVER_SECURITY_RELOAD_EVERY);
final int cacheSize = configuration.getValueAsInteger(SERVER_SECURITY_SALT_CACHE_SIZE);
if (cacheSize > 0)
saltCache = Collections.synchronizedMap(new LRUCache<>(cacheSize));
else
saltCache = Collections.emptyMap();
saltIteration = configuration.getValueAsInteger(SERVER_SECURITY_SALT_ITERATIONS);
usersRepository = new SecurityUserFileRepository(configPath);
groupRepository = new SecurityGroupFileRepository(configPath, checkConfigReloadEveryMs).onReload((latestConfiguration) -> {
for (final String databaseName : server.getDatabaseNames()) {
updateSchema(server.getDatabase(databaseName));
}
return null;
});
try {
secretKeyFactory = SecretKeyFactory.getInstance(algorithm);
} catch (final NoSuchAlgorithmException e) {
LogManager.instance().log(this, Level.SEVERE, "Security algorithm '%s' not available (error=%s)", e, algorithm);
throw new ServerSecurityException("Security algorithm '" + algorithm + "' not available", e);
}
}
@Override
public void configure(final ArcadeDBServer arcadeDBServer, final ContextConfiguration configuration) {
}
@Override
public void startService() {
// NO ACTION
}
public void loadUsers() {
try {
users.clear();
try {
for (final JSONObject userJson : usersRepository.getUsers()) {
final ServerSecurityUser user = new ServerSecurityUser(server, userJson);
users.put(user.getName(), user);
}
} catch (final JSONException e) {
groupRepository.saveInError(e);
for (final JSONObject userJson : SecurityUserFileRepository.createDefault()) {
final ServerSecurityUser user = new ServerSecurityUser(server, userJson);
users.put(user.getName(), user);
}
}
if (users.isEmpty() || (users.containsKey("root") && users.get("root").getPassword() == null))
askForRootPassword();
final long fileLastModified = usersRepository.getFileLastModified();
if (fileLastModified > -1 && reloadConfigurationTimer == null) {
reloadConfigurationTimer = new Timer();
reloadConfigurationTimer.schedule(new TimerTask() {
@Override
public void run() {
if (usersRepository.isUserFileChanged()) {
LogManager.instance().log(this, Level.INFO, "Reloading user files...");
loadUsers();
}
}
}, checkConfigReloadEveryMs, checkConfigReloadEveryMs);
}
} catch (final IOException e) {
throw new ServerException("Error on starting Security service", e);
}
}
@Override
public void stopService() {
if (reloadConfigurationTimer != null)
reloadConfigurationTimer.cancel();
users.clear();
if (groupRepository != null)
groupRepository.stop();
}
public ServerSecurityUser authenticate(final String userName, final String userPassword, final String databaseName) {
final ServerSecurityUser su = users.get(userName);
if (su == null)
throw new ServerSecurityException("User/Password not valid");
if (!passwordMatch(userPassword, su.getPassword()))
throw new ServerSecurityException("User/Password not valid");
if (databaseName != null) {
final Set allowedDatabases = su.getAuthorizedDatabases();
if (!allowedDatabases.contains(SecurityManager.ANY) && !su.getAuthorizedDatabases().contains(databaseName))
throw new ServerSecurityException("User has not access to database '" + databaseName + "'");
}
return su;
}
/**
* Override the default @{@link CredentialsValidator} implementation (@{@link DefaultCredentialsValidator}) providing a custom one.
*/
public void setCredentialsValidator(final CredentialsValidator credentialsValidator) {
this.credentialsValidator = credentialsValidator;
}
public boolean existsUser(final String userName) {
return users.containsKey(userName);
}
public Set getUsers() {
return users.keySet();
}
public ServerSecurityUser getUser(final String userName) {
return users.get(userName);
}
public ServerSecurityUser createUser(final JSONObject userConfiguration) {
final String name = userConfiguration.getString("name");
if (users.containsKey(name))
throw new SecurityException("User '" + name + "' already exists");
final ServerSecurityUser user = new ServerSecurityUser(server, userConfiguration);
users.put(name, user);
saveUsers();
return user;
}
public boolean dropUser(final String userName) {
if (users.remove(userName) != null) {
saveUsers();
return true;
}
return false;
}
@Override
public void updateSchema(final DatabaseInternal database) {
if (database == null)
return;
for (final ServerSecurityUser user : users.values()) {
final ServerSecurityDatabaseUser databaseUser = user.getDatabaseUser(database);
if (databaseUser != null) {
final JSONObject groupConfiguration = getDatabaseGroupsConfiguration(database.getName());
if (groupConfiguration == null)
continue;
databaseUser.updateFileAccess(database, groupConfiguration);
}
}
}
public String getEncodedHash(final String password, final String salt, final int iterations) {
// Returns only the last part of whole encoded password
final KeySpec keySpec = new PBEKeySpec(password.toCharArray(), salt.getBytes(StandardCharsets.UTF_8), iterations, 256);
final SecretKey secret;
try {
secret = secretKeyFactory.generateSecret(keySpec);
} catch (final InvalidKeySpecException e) {
throw new ServerSecurityException("Error on generating security key", e);
}
final byte[] rawHash = secret.getEncoded();
final byte[] hashBase64 = Base64.getEncoder().encode(rawHash);
return new String(hashBase64, DatabaseFactory.getDefaultCharset());
}
protected String encodePassword(final String password, final String salt) {
return this.encodePassword(password, salt, saltIteration);
}
public String encodePassword(final String userPassword) {
return encodePassword(userPassword, ServerSecurity.generateRandomSalt());
}
public boolean passwordMatch(final String password, final String hashedPassword) {
// hashedPassword consist of: ALGORITHM, ITERATIONS_NUMBER, SALT and
// HASH; parts are joined with dollar character ("$")
final String[] parts = hashedPassword.split("\\$");
if (parts.length != 4)
// wrong hash format
return false;
final int iterations = Integer.parseInt(parts[1]);
final String salt = parts[2];
final String hash = encodePassword(password, salt, iterations);
return hash.equals(hashedPassword);
}
protected static String generateRandomSalt() {
final byte[] salt = new byte[SALT_SIZE];
RANDOM.nextBytes(salt);
return new String(Base64.getEncoder().encode(salt), DatabaseFactory.getDefaultCharset());
}
protected String encodePassword(final String password, final String salt, final int iterations) {
if (!saltCache.isEmpty()) {
final String encoded = saltCache.get(password + "$" + salt + "$" + iterations);
if (encoded != null)
// FOUND CACHED
return encoded;
}
final String hash = getEncodedHash(password, salt, iterations);
final String encoded = String.format("%s$%d$%s$%s", algorithm, iterations, salt, hash);
// CACHE IT
saltCache.put(password + "$" + salt + "$" + iterations, encoded);
return encoded;
}
public List usersToJSON() {
final List jsonl = new ArrayList<>(users.size());
for (final ServerSecurityUser user : users.values())
jsonl.add(user.toJSON());
return jsonl;
}
public JSONObject groupsToJSON() {
final JSONObject json = new JSONObject();
// DATABASES TAKE FROM PREVIOUS CONFIGURATION
json.put("databases", groupRepository.getGroups().getJSONObject("databases"));
json.put("version", LATEST_VERSION);
return json;
}
public void saveUsers() {
try {
usersRepository.save(usersToJSON());
} catch (final IOException e) {
LogManager.instance()
.log(this, Level.SEVERE, "Error on saving security configuration to file '%s'", e, SecurityUserFileRepository.FILE_NAME);
}
}
public void saveGroups() {
try {
groupRepository.save(groupsToJSON());
} catch (final IOException e) {
LogManager.instance()
.log(this, Level.SEVERE, "Error on saving security configuration to file '%s'", e, SecurityGroupFileRepository.FILE_NAME);
}
}
protected void askForRootPassword() throws IOException {
String rootPassword = server != null ?
server.getConfiguration().getValueAsString(GlobalConfiguration.SERVER_ROOT_PASSWORD) :
GlobalConfiguration.SERVER_ROOT_PASSWORD.getValueAsString();
if (rootPassword == null) {
final String rootPasswordPath = server != null ?
server.getConfiguration().getValueAsString(GlobalConfiguration.SERVER_ROOT_PASSWORD_PATH) :
GlobalConfiguration.SERVER_ROOT_PASSWORD_PATH.getValueAsString();
if (rootPasswordPath != null) {
if (Files.isReadable(Paths.get(rootPasswordPath)))
rootPassword = Files.readString(Paths.get(rootPasswordPath));
else
throw new ServerSecurityException("Error reading password file at path '" + rootPasswordPath + "'");
}
}
if (rootPassword == null) {
if (server != null ?
server.getConfiguration().getValueAsBoolean(GlobalConfiguration.HA_K8S) :
GlobalConfiguration.HA_K8S.getValueAsBoolean()) {
// UNDER KUBERNETES IF THE ROOT PASSWORD IS NOT SET (USUALLY WITH A SECRET) THE POD MUST TERMINATE
LogManager.instance().log(this, Level.SEVERE,
"Unable to start a server under Kubernetes if the environment variable `arcadedb.server.rootPassword` is not set");
throw new ServerSecurityException(
"Unable to start a server under Kubernetes if the environment variable `arcadedb.server.rootPassword` is not set");
}
LogManager.instance().flush();
System.err.flush();
System.out.flush();
System.out.println();
System.out.println();
System.out.println(AnsiCode.format("$ANSI{yellow +--------------------------------------------------------------------+}"));
System.out.println(AnsiCode.format("$ANSI{yellow | WARNING: FIRST RUN CONFIGURATION |}"));
System.out.println(AnsiCode.format("$ANSI{yellow +--------------------------------------------------------------------+}"));
System.out.println(AnsiCode.format("$ANSI{yellow | This is the first time the server is running. Please type a |}"));
System.out.println(AnsiCode.format("$ANSI{yellow | password of your choice for the 'root' user or leave it blank |}"));
System.out.println(AnsiCode.format("$ANSI{yellow | to auto-generate it. |}"));
System.out.println(AnsiCode.format("$ANSI{yellow | |}"));
System.out.println(AnsiCode.format("$ANSI{yellow | To avoid this message set the environment variable or JVM |}"));
System.out.println(AnsiCode.format("$ANSI{yellow | setting `arcadedb.server.rootPassword` to the root password to use.|}"));
System.out.println(AnsiCode.format("$ANSI{yellow +--------------------------------------------------------------------+}"));
final DefaultConsoleReader console = new DefaultConsoleReader();
// ASK FOR PASSWORD + CONFIRM
do {
System.out.print(AnsiCode.format("\n$ANSI{yellow Root password [BLANK=auto generate it]: }"));
rootPassword = console.readPassword();
if (rootPassword != null) {
rootPassword = rootPassword.trim();
if (rootPassword.isEmpty())
rootPassword = null;
}
if (rootPassword == null) {
rootPassword = credentialsValidator.generateRandomPassword();
System.out.print(AnsiCode.format(
"Automatic generated password: $ANSI{green " + rootPassword + "}. Please save it in a safe place.\n"));
}
if (rootPassword != null) {
System.out.print(
AnsiCode.format("$ANSI{yellow Please type the root password for confirmation (copy and paste will not work): }"));
String rootConfirmPassword = console.readPassword();
if (rootConfirmPassword != null) {
rootConfirmPassword = rootConfirmPassword.trim();
if (rootConfirmPassword.isEmpty())
rootConfirmPassword = null;
}
if (!rootPassword.equals(rootConfirmPassword)) {
System.out.println(AnsiCode.format(
"$ANSI{red ERROR: Passwords do not match, please reinsert both of them, or press ENTER to auto generate it}"));
try {
Thread.sleep(500);
} catch (final InterruptedException e) {
return;
}
rootPassword = null;
} else
// PASSWORDS MATCH
try {
credentialsValidator.validateCredentials("root", rootPassword);
// PASSWORD IS STRONG ENOUGH
break;
} catch (final ServerSecurityException ex) {
System.out.println(AnsiCode.format(
"$ANSI{red ERROR: Root password does not match the password policies" + (ex.getMessage() != null ?
": " + ex.getMessage() :
"") + "}"));
try {
Thread.sleep(500);
} catch (final InterruptedException e) {
return;
}
rootPassword = null;
}
}
} while (rootPassword == null);
} else
LogManager.instance().log(this, Level.INFO, "Creating root user with the provided password");
credentialsValidator.validateCredentials("root", rootPassword);
final String encodedPassword = encodePassword(rootPassword, ServerSecurity.generateRandomSalt());
if (existsUser("root")) {
getUser("root").setPassword(encodedPassword);
saveUsers();
} else
createUser(new JSONObject().put("name", "root").put("password", encodedPassword));
}
protected JSONObject getDatabaseGroupsConfiguration(final String databaseName) {
final JSONObject groupDatabases = groupRepository.getGroups().getJSONObject("databases");
JSONObject databaseConfiguration = groupDatabases.has(databaseName) ? groupDatabases.getJSONObject(databaseName) : null;
if (databaseConfiguration == null)
// GET DEFAULT (*) DATABASE GROUPS
databaseConfiguration = groupDatabases.has(SecurityManager.ANY) ? groupDatabases.getJSONObject("*") : null;
if (databaseConfiguration == null || !databaseConfiguration.has("groups"))
return null;
return databaseConfiguration.getJSONObject("groups");
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy