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 }