package automately.core.data;
import automately.core.data.comparators.JsonFieldComparator;
import automately.core.data.predicates.JsonQueryPredicate;
import automately.core.data.predicates.KeyStartsWithPredicate;
import com.hazelcast.core.ICountDownLatch;
import com.hazelcast.core.IMap;
import com.hazelcast.query.*;
import io.jsync.Handler;
import io.jsync.app.ClusterApp;
import io.jsync.app.core.Cluster;
import io.jsync.json.JsonObject;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import java.math.BigInteger;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.spec.InvalidKeySpecException;
import java.util.*;
import java.util.concurrent.TimeUnit;
import static io.jsync.utils.CryptoUtils.calculateHmacSHA1;
import static java.util.UUID.randomUUID;
/**
* UserData is a utility that can be used to access data in the Cluster
* and do certain operations.
*/
public class UserData {
/**
* Begin static code initialization.
*/
static {
if (ClusterApp.activeInstance() == null) {
throw new Error("Could not initialize JobUtil because the JCluster instance does not seem to be active.");
}
}
private static Cluster cluster = ClusterApp.activeInstance().cluster();
private static IMap<String, User> users = cluster.data().persistentMap("users");
private static IMap<String, Meta> userMeta = cluster.data().persistentMap("users.meta");
private static IMap<String, JsonObject> userKeys = cluster.data().persistentMap("users.keys");
private static IMap<String, JsonObject> userKeySalts = cluster.data().persistentMap("users.keys.salts");
private static IMap<String, Job> jobs = cluster.data().persistentMap("jobs");
/**
* End static code initialization.
*/
private UserData() {
}
private static String generateStrongHash(String data, int iterations, byte[] salt) {
char[] chars = data.toCharArray();
PBEKeySpec spec = new PBEKeySpec(chars, salt, iterations, 64 * 8);
SecretKeyFactory skf = null;
try {
skf = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
try {
return toHex(skf.generateSecret(spec).getEncoded());
} catch (InvalidKeySpecException e) {
throw new RuntimeException(e);
}
}
private static byte[] getSalt(){
try {
SecureRandom sr = SecureRandom.getInstance("SHA1PRNG");
byte[] salt = new byte[16];
sr.nextBytes(salt);
return salt;
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
private static String toHex(byte[] array) {
BigInteger bi = new BigInteger(1, array);
String hex = bi.toString(16);
int paddingLength = (array.length * 2) - hex.length();
if (paddingLength > 0) {
return String.format("%0" + paddingLength + "d", 0) + hex;
} else {
return hex;
}
}
public static void createUser(String username, String password, Handler<Map.Entry<User, String>> successHandler){
createUser(username, password, true, false, successHandler);
}
public static void createUser(String username, String password, boolean enabled, Handler<Map.Entry<User, String>> successHandler){
createUser(username, password, enabled, false, successHandler);
}
/**
* createUser allows you to quickly create a new User and store it in the Cluster.
* it calls setUserPassword and setUserMetaDefaults.
*
* @param username the username you want to use
* @param password the password you want to use
* @param creationHandler a handler that will return the newly created user and it's private key
*/
public static void createUser(String username, String password, boolean enabled, boolean admin, Handler<Map.Entry<User, String>> creationHandler) {
if (getUserByUsername(username) == null) {
User user = new User();
user.username = username.trim();
user.admin = admin;
user.enabled = enabled;
// This is the users default key
users.set(user.token(), user);
setUserPassword(user, password);
setMetaDefaults(user);
generateUserKey(user, true, "Default Key", null,
eventKeyObj -> creationHandler.handle(new AbstractMap.SimpleEntry<>(user, eventKeyObj.getString("key"))));
}
}
/**
* deleteUser allows you to delete a User from the Cluster
*
* @param user the user you want to delete from the Cluster
* @param purgeAll true if you want to delete all data associated with the user such as files, jobs, etc
* @return returns true if the deletion of the User was successful
*/
public static boolean deleteUser(User user, boolean purgeAll) {
if (getUserByToken(user.token()) != null) {
if (purgeAll) {
//Deletes all users files userMeta and databus and jobs
// First we must delete the user's meta information
Predicate p = Predicates.equal("userToken", user.token());
// Delete Meta
Collection<String> metaKeys = userMeta.keySet(p);
for (String key : metaKeys) {
userMeta.remove(key);
}
// We must delete all user information
// Delete DataBus stuff
IMap dataBus = cluster.data().persistentMap("dataBus");
String baseIdentifier = "internal.private_..." + user.token() + "_job_dataBus_internal_";
Collection<String> dataBusKeys = dataBus.keySet(new KeyStartsWithPredicate(baseIdentifier));
for (String key : dataBusKeys) {
dataBus.remove(key);
}
IMap files = cluster.data().persistentMap("files");
// Delete Files
Collection<String> fileKeys = files.keySet(p);
for (String key : fileKeys) {
files.remove(key);
}
// Delete Files
Collection<String> jobKeys = jobs.keySet(p);
for (String key : jobKeys) {
// Send a message to the cluster to tell the job to stop it's execution
// Sent over the cluster because all jobs aren't ran locally
cluster.eventBus().publish("job.server." + key + ".execution", "stop");
cluster.eventBus().publish("job.server." + key + ".execution", "kill");
jobs.remove(key);
}
// Remove all reserved http hosts
IMap<String, JsonObject> reservedHostData = cluster.data().persistentMap("http.clustered.hosts.reserved.data");
reservedHostData.keySet(new JsonQueryPredicate(
new JsonObject().putString("user", user.token()))).forEach(reservedHostData::remove);
}
users.remove(user.token());
}
return !users.containsKey(user.token());
}
/**
* getUser is used to retrieve a User from the Cluster.
* This just calls getUserByToken.
*
* @param token the token for the User you want to retrieve
* @return returns the User or null if the User does not exist
*/
public static User getUser(String token) {
return getUserByToken(token);
}
/**
* getUserByToken is used to retrieve a User from the Cluster
* via it's token.
*
* @param token the token of the User you want to retrieve
* @return returns the User or null if the User does not exist
*/
public static User getUserByToken(String token) {
return users.get(token);
}
/**
* getUserByUsername is used to retrieve a User from the Cluster
* via it's username.
*
* @param username the username of the User you want to retrieve
* @return returns the User or null if the User does not exist
*/
public static User getUserByUsername(String username) {
EntryObject e = new PredicateBuilder().getEntryObject();
// We check the value to be equal to true to ensure that this friendship is active.
// Then we check to see if the parentToken was the postUser and the key is the currently
// logged in user
// Or we check if the parentToken is equal to the current user and the username is equal to the post user.
//Predicate p = e.get("value").equal(true).and()
Predicate predicate = e.get("username").equal(username);
for (User user : users.values(predicate)) {
if (user.username.equals(username))
return user;
}
// If we have a value more than 0 we know that the friendship is active.
return null;
}
/**
* setUserPassword is used to set a User's password.
*
* @param user the User you wish to set the password for
* @param password the new password for the User
*/
public static void setUserPassword(User user, String password) {
if (getUserByToken(user.token()) != null) {
byte[] kindasaltydata = getSalt();
String securePassword = generateStrongHash(password, 1000, kindasaltydata);
setMeta(user, "password", securePassword);
setMeta(user, "kindasaltydata", kindasaltydata);
}
}
/**
* validateUserPassword is used to easily validate a User's password.
*
* @param user the User you wish to validate the password for
* @param password the password you are trying to validate
* @return returns true if it was successful
*/
public static boolean validateUserPassword(User user, String password) {
try {
Meta curPassword = getMeta(user, "password");
if(curPassword != null){
String current = (String) curPassword.value;
Meta saltyMeta = getMeta(user, "kindasaltydata");
if(saltyMeta != null){
byte[] saltybytes = saltyMeta.toJson().getBinary("value");
String hashedPassword = generateStrongHash(password, 1000, saltybytes);
return hashedPassword.equals(current);
} else {
String oldHashedPassword = calculateHmacSHA1(password, user.username);
if(oldHashedPassword.equals(current)){
setUserPassword(user, password);
return true;
}
}
}
} catch (Exception e){
e.printStackTrace();
}
return false;
}
/**
* getUserKeys retrieves a Collection<JsonObject> for every single key belonging
* to the specified User.
*
* @param user the User you wish to retrieve the key objects for
* @return returns a Colllection<Jsonobject> for the User
*/
public static Collection<JsonObject> getUserKeys(User user) {
Predicate predicate = new JsonQueryPredicate(new JsonObject().putString("user", user.token()));
return userKeys.values(predicate);
}
/**
* getUserKeys retrieves a Collection<JsonObject> for every single key belonging
* to the specified User.
*
* @param user the User you wish to retrieve the keys's for
* @param page the page index starting from 0 you are looking for
* @return returns a Colllection<JsonObject> for the User
*/
public static Collection<JsonObject> getUserKeys(User user, int page) {
return getUserKeys(user, page, 10);
}
/**
* getUserKeys retrieves a Collection<JsonObject> for every single key belonging
* to the specified User.
*
* @param user the User you wish to retrieve the keys's for
* @param page the page index starting from 0 you are looking for
* @param count the number of results you are looking for
* @return returns a Colllection<JsonObject> for the User
*/
public static Collection<JsonObject> getUserKeys(User user, int page, int count) {
Predicate predicate = new JsonQueryPredicate(new JsonObject().putString("user", user.token()));
if (page < 0) {
page = 0;
}
if (count < 0) {
count = 10;
}
// Max Count is always 100
if (count > 100) count = 100;
// This predicate uses the previous one.. and then sorts the posts by date...
// IMPORTANT apparently can't lambda
PagingPredicate pagingPredicate = new PagingPredicate(predicate, new JsonFieldComparator("updated", true), count);
Collection<JsonObject> values = userKeys.values(pagingPredicate);
if (count > pagingPredicate.getPage()) {
while (page > pagingPredicate.getPage()) {
pagingPredicate.nextPage();
}
values = userKeys.values(pagingPredicate);
}
return values;
}
public static boolean validateUserKey(User user, String key){
return validateUserKey(user, key, false);
}
public static boolean validateUserKey(User user, String key, boolean requireDefault) {
try {
// Let's attempt to validate the key based on the user files
Collection<JsonObject> keys = userKeys.values(new JsonQueryPredicate(new JsonObject().putString("user", user.token())));
for (JsonObject keyObj : keys) {
if(keyObj.getBoolean("revoked", false) || (requireDefault && !keyObj.getBoolean("default", false))){
continue;
}
String keyData = keyObj.getString("key");
String keyRef = calculateHmacSHA1(keyData + user.token(), keyData);
JsonObject saltyData = userKeySalts.getOrDefault(keyRef, new JsonObject());
byte[] prettysaltydata = saltyData.getBinary("prettysaltydata", new byte[0]);
int prettierdata = saltyData.getInteger("prettierdata", 1000);
String newKey = generateStrongHash(key, prettierdata, prettysaltydata);
if(newKey.equals(keyData)){
return true;
}
}
} catch (Exception e){
e.printStackTrace();
}
return false;
}
public static JsonObject getUserKey(User user, String keyId) {
// Let's attempt to validate the key based on the user files
Iterator<JsonObject> keys = userKeys.values(new JsonQueryPredicate(new JsonObject().
putString("user", user.token()).putString("id", keyId))).iterator();
if(keys.hasNext()){
return keys.next();
}
return null;
}
public static void generateUserKey(User user, Handler<JsonObject> keyHandler){
generateUserKey(user, false, null, null, keyHandler);
}
public static void generateUserKey(User user, String nickname, String description, Handler<JsonObject> keyHandler){
generateUserKey(user, false, nickname, description, keyHandler);
}
public static void generateUserKey(User user, boolean defaultKey, String nickname, String description, Handler<JsonObject> keyHandler) {
if(user == null){
throw new IllegalArgumentException(new NullPointerException("user cannot be null!"));
}
if(keyHandler == null){
throw new IllegalArgumentException(new NullPointerException("keyHandler cannot be null!"));
}
if(nickname == null){
nickname = "";
}
if(description == null){
description = "Created on " + (new Date()).toString();
}
String userKey = calculateHmacSHA1(randomUUID().toString(), randomUUID().toString());
byte[] prettysaltydata = getSalt();
int prettierdata = new Double(Math.random() * ( 2000 - 1000 )).intValue();
// Let's generate a new key
String newKey = generateStrongHash(userKey, prettierdata, prettysaltydata);
String keyRef = calculateHmacSHA1(newKey + user.token(), newKey);
JsonObject keyData = new JsonObject();
keyData.putString("user", user.token());
keyData.putString("key", newKey);
keyData.putNumber("created", (new Date()).getTime());
keyData.putNumber("updated", (new Date()).getTime());
keyData.putString("nickname", nickname);
keyData.putString("description", description);
keyData.putBoolean("revoked", false);
keyData.putString("id", keyRef);
// This will always be the default key if there are no keys
keyData.putBoolean("default", defaultKey);
JsonObject saltyData = new JsonObject();
saltyData.putBinary("prettysaltydata", prettysaltydata);
saltyData.putNumber("prettierdata", prettierdata);
userKeys.set(newKey, keyData);
userKeySalts.set(keyRef, saltyData);
if(defaultKey){
// Let's revoke any old default keys
Collection<Map.Entry<String, JsonObject>> oldDefaultKeys = userKeys.entrySet(new JsonQueryPredicate(new JsonObject().putString("user", user.token()).putBoolean("default", true)));
for (Map.Entry<String, JsonObject> entry : oldDefaultKeys) {
JsonObject oldDefaultKey = entry.getValue();
String oldKeyRef = calculateHmacSHA1(entry.getKey() + user.token(), entry.getKey());
if(oldKeyRef.equals(keyRef)){
continue;
}
oldDefaultKey.putBoolean("default", false);
oldDefaultKey.putNumber("updated", (new Date()).getTime());
oldDefaultKey.putString("key", "revoked");
oldDefaultKey.putBoolean("revoked", true);
oldDefaultKey.putString("id", oldKeyRef);
userKeys.put(entry.getKey(), oldDefaultKey);
userKeySalts.remove(oldKeyRef);
}
}
keyHandler.handle(keyData.putString("key", userKey));
}
public static JsonObject updateUserKey(User user, String key, String nickname, String description){
// Let's attempt to validate the key based on the user files
Collection<JsonObject> keys = userKeys.values(new JsonQueryPredicate(new JsonObject().putString("user", user.token())));
for (JsonObject keyObj : keys) {
if(keyObj.getBoolean("revoked", false)){
continue;
}
String keyData = keyObj.getString("key");
String keyRef = calculateHmacSHA1(keyData + user.token(), keyData);
JsonObject saltyData = userKeySalts.getOrDefault(keyRef, new JsonObject());
byte[] prettysaltydata = saltyData.getBinary("prettysaltydata", new byte[0]);
int prettierdata = saltyData.getInteger("prettierdata", 1000);
String newKey = generateStrongHash(key, prettierdata, prettysaltydata);
if(newKey.equals(keyData)){
if(nickname == null){
nickname = "";
}
Date updated = new Date();
if(description == null){
description = "Updated on " + updated.toString();
}
// We have found the key! so we can update it
keyObj.putString("nickname", nickname.trim());
keyObj.putString("description", description.trim());
keyObj.putNumber("updated", updated.getTime());
userKeys.put(keyData, keyObj);
return keyObj;
}
}
return null;
}
public static JsonObject revokeUserKey(User user, String keyId) {
// Let's attempt to validate the key based on the user files
Iterator<JsonObject> keys = userKeys.values(new JsonQueryPredicate(new JsonObject().
putString("user", user.token()).putString("id", keyId))).iterator();
if(keys.hasNext()){
JsonObject keyObj = keys.next();
// We cannot revoke a default key
if(keyObj.getBoolean("default", false)){
throw new RuntimeException("Cannot revoke a default key. Try generating a new default key.");
}
if(keyObj.getBoolean("revoked", false)){
return keyObj; // key is already revoked
}
keyObj.putBoolean("revoked", true);
keyObj.putNumber("updated", (new Date()).getTime());
String hashedKey = keyObj.getString("key");
keyObj.putString("key", "revoked");
userKeys.put(hashedKey, keyObj);
userKeySalts.remove(keyId);
return keyObj;
}
return null;
}
/**
* getMeta is used to retrieve a Collection of Meta objects from the Cluster.
*
* @param key the key you wish to search for
* @param value the value of the key you are searching for
* @return returns a Collection<Meta> with the values matching the key and value
*/
public static Collection<Meta> getMeta(String key, String value) {
EntryObject e = new PredicateBuilder().getEntryObject();
Predicate p = e.get("key").equal(key.trim()).and(e.get("value").equal(value));
return userMeta.values(p);
}
/**
* getMeta is used to retrieve a Collection of Meta objects from the Cluster.
*
* @param key the key you wish to search for
* @return returns a Collection<Meta> with the values matching the key
*/
public static Collection<Meta> getMeta(String key) {
EntryObject e = new PredicateBuilder().getEntryObject();
Predicate p = e.get("key").equal(key.trim());
return userMeta.values(p);
}
/**
* getUserMeta is used to retrieve a Collection of Meta objects for a User from the Cluster.
*
* @param user the User of the Meta objects
* @return returns a Collection<Meta> with the values matching the User
*/
public static Collection<Meta> getMeta(User user) {
EntryObject e = new PredicateBuilder().getEntryObject();
Predicate p = e.get("userToken").equal(user.token());
return userMeta.values(p);
}
/**
* delteUserMeta is used to delete a Meta object for a User from the Cluster.
*
* @param user the User you are deleting the Meta object for.
* @param key the key for the Meta object
*/
public static void deleteUserMeta(User user, String key) {
EntryObject e = new PredicateBuilder().getEntryObject();
Predicate p = e.get("userToken").equal(user.token()).and(e.get("key").equal(key));
Collection<Meta> values = userMeta.values(p);
for (Meta meta : values) {
userMeta.remove(meta.token());
}
}
/**
* getUserMeta is used to retrieve a Meta object for a User matching
* the given key.
*
* @param user the User you are trying to find the Meta object for
* @param key the key you are attempting to match
* @return returns a Meta object otherwise null if it does not exist
*/
public static Meta getMeta(User user, String key) {
EntryObject e = new PredicateBuilder().getEntryObject();
Predicate p = e.get("userToken").equal(user.token()).and(e.get("key").equal(key));
Collection<Meta> values = userMeta.values(p);
Iterator<Meta> iterator = values.iterator();
Meta metaValue = null;
if (iterator.hasNext()) {
metaValue = iterator.next();
}
// Remove copies
while (iterator.hasNext()) {
userMeta.remove(iterator.next().token());
}
return metaValue;
}
/**
* setUserMeta is used to store a specific key and value as a Meta object for
* the specified User.
*
* @param user the User you wish to store the Meta object for
* @param key the key for the Meta object
* @param value the value for the Meta object
*/
public static void setMeta(User user, String key, Object value) {
Meta meta = new Meta();
meta.key = key;
meta.value = value;
setMeta(user, meta);
}
/**
* setUserMEta allows you to store a Meta object for the specified User.
*
* @param user the User you are storing the Meta object for
* @param meta the Meta object you wish to store
*/
public static void setMeta(User user, Meta meta) {
Meta existingMeta = getMeta(user, meta.key);
if (existingMeta != null) {
existingMeta.value = meta.value;
existingMeta.userToken = user.token();
userMeta.set(existingMeta.token(), existingMeta);
} else {
meta.userToken = user.token();
userMeta.set(meta.token(), meta);
}
}
/**
* containsUserMeta is used to check if there is a certain key stored
* under the specified User's Meta data.
*
* @param user the User you wish to check
* @param key the key you are searching for
* @return returns true if it contains the key you are looing for
*/
public static boolean containsMeta(User user, String key) {
EntryObject e = new PredicateBuilder().getEntryObject();
Predicate p = e.get("userToken").equal(user.token()).and(e.get("key").equal(key));
Collection<Meta> values = userMeta.values(p);
return values.size() > 0;
}
/**
* setUserMetaDefaults is a useful method that allows you to set all the
* default values for a User such as quotas,
*
* @param user
*/
public static void setMetaDefaults(User user) {
if (!containsMeta(user, "max_service_jobs")) {
// This is the number of jobs we can run as a service.
setMeta(user, new Meta("max_service_jobs", 25));
}
if (!containsMeta(user, "max_jobs")) {
// max_service_jobs is affected by this.
setMeta(user, new Meta("max_jobs", 250));
}
}
// TODO add getUserKeys
}