/* * RESTHeart - the Web API for MongoDB * Copyright (C) SoftInstigate Srl * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero 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 Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.restheart.security.impl; import com.mongodb.MongoClient; import com.mongodb.client.FindIterable; import com.mongodb.client.MongoCollection; import com.mongodb.client.MongoDatabase; import static com.mongodb.client.model.Filters.eq; import io.undertow.security.idm.Account; import io.undertow.security.idm.Credential; import io.undertow.security.idm.IdentityManager; import io.undertow.security.idm.PasswordCredential; import java.io.FileNotFoundException; import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.function.Consumer; import org.bson.BsonDocument; import org.bson.BsonValue; import org.bson.conversions.Bson; import org.mindrot.jbcrypt.BCrypt; import org.restheart.cache.Cache; import org.restheart.cache.CacheFactory; import org.restheart.cache.LoadingCache; import org.restheart.db.MongoDBClientSingleton; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * * @author Andrea Di Cesare {@literal <andrea@softinstigate.com>} */ public final class DbIdentityManager extends AbstractSimpleSecurityManager implements IdentityManager { private static final Logger LOGGER = LoggerFactory.getLogger(DbIdentityManager.class); private MongoCollection<BsonDocument> mongoColl; private String db; private String coll; private String propertyNameId = "_id"; private String propertyNamePassword = "password"; private String propertyNameRoles = "roles"; private Boolean bcryptHashedPassword = false; private Boolean createUser = false; private BsonDocument createUserDocument = null; private Boolean cacheEnabled = false; private Long cacheSize = 1_000l; // 1000 entries private Long cacheTTL = 60 * 1_000l; // 1 minute private Cache.EXPIRE_POLICY cacheExpirePolicy = Cache.EXPIRE_POLICY.AFTER_WRITE; private LoadingCache<String, SimpleAccount> cache = null; /** * * @param arguments * @throws java.io.FileNotFoundException */ public DbIdentityManager(Map<String, Object> arguments) throws FileNotFoundException { init(arguments, "dbim"); if (this.cacheEnabled) { this.cache = CacheFactory.createLocalLoadingCache( this.cacheSize, this.cacheExpirePolicy, this.cacheTTL, (String key) -> { return this.findAccount(key); }); } MongoClient mongoClient = MongoDBClientSingleton .getInstance().getClient(); MongoDatabase mongoDb = mongoClient.getDatabase(this.db); this.mongoColl = mongoDb.getCollection(coll, BsonDocument.class); if (this.createUser) { LOGGER.trace("create user option enabled"); if (this.mongoColl.count() < 1) { this.mongoColl.insertOne(this.createUserDocument); LOGGER.info("no user found. created default user with _id {}", this.createUserDocument.get("_id")); } else { LOGGER.trace("not creating default user since users exist"); } } } @Override Consumer<? super Map<String, Object>> consumeConfiguration() { return ci -> { Object _db = ci.get("db"); Object _coll = ci.get("coll"); Object _cacheEnabled = ci.get("cache-enabled"); Object _cacheSize = ci.get("cache-size"); Object _cacheTTL = ci.get("cache-ttl"); Object _cacheExpirePolicy = ci.get("cache-expire-policy"); Object _bcryptHashedPassword = ci.get("bcrypt-hashed-password"); Object _createUser = ci.get("create-user"); Object _createUserDocument = ci.get("create-user-document"); Object _propertyNameId = ci.get("prop-name-id"); Object _propertyNamePassword = ci.get("prop-name-password"); Object _propertyNameRoles = ci.get("prop-name-roles"); if (_db == null || !(_db instanceof String)) { throw new IllegalArgumentException( "wrong configuration file format. " + "missing db property"); } if (_coll == null || !(_coll instanceof String)) { throw new IllegalArgumentException( "wrong configuration file format. " + "missing coll property"); } if (_cacheEnabled != null && !(_cacheEnabled instanceof Boolean)) { throw new IllegalArgumentException( "wrong configuration file format. " + "cache-enabled must be a boolean"); } if (_cacheSize != null && !(_cacheSize instanceof Long || _cacheSize instanceof Integer)) { throw new IllegalArgumentException( "wrong configuration file format. " + "cache-size must be a number"); } if (_cacheTTL != null && !(_cacheTTL instanceof Long || _cacheTTL instanceof Integer)) { throw new IllegalArgumentException( "wrong configuration file format. " + "cache-ttl must be a number (of milliseconds)"); } if (_cacheExpirePolicy != null && !(_cacheExpirePolicy instanceof String)) { throw new IllegalArgumentException( "wrong configuration file format. " + "cache-expire-policy valid values are " + Arrays.toString(Cache.EXPIRE_POLICY.values())); } if (_bcryptHashedPassword != null && !(_bcryptHashedPassword instanceof Boolean)) { throw new IllegalArgumentException( "wrong configuration file format. " + "bcrypt-hashed-password must be a boolean"); } if (_createUser != null && !(_createUser instanceof Boolean)) { throw new IllegalArgumentException( "wrong configuration file format. " + "create-user must be a boolean"); } if (_createUserDocument != null && !(_createUserDocument instanceof String)) { throw new IllegalArgumentException( "wrong configuration file format. " + "create-user-document must be a json document"); } if (_propertyNameId != null && !(_propertyNameId instanceof String)) { throw new IllegalArgumentException( "wrong configuration file format. " + "prop-name-id must be a string"); } if (_propertyNamePassword != null && !(_propertyNamePassword instanceof String)) { throw new IllegalArgumentException( "wrong configuration file format. " + "prop-name-password must be a string"); } if (_propertyNameRoles != null && !(_propertyNameRoles instanceof String)) { throw new IllegalArgumentException( "wrong configuration file format. " + "prop-name-roles must be a string"); } this.db = (String) _db; this.coll = (String) _coll; if (_cacheEnabled != null) { this.cacheEnabled = (Boolean) _cacheEnabled; } if (_cacheSize != null) { if (_cacheSize instanceof Integer) { this.cacheSize = ((Integer) _cacheSize).longValue(); } else { this.cacheSize = (Long) _cacheSize; } } if (_cacheTTL != null) { if (_cacheTTL instanceof Integer) { this.cacheTTL = ((Integer) _cacheTTL).longValue(); } else { this.cacheTTL = (Long) _cacheTTL; } } if (_cacheExpirePolicy != null) { try { this.cacheExpirePolicy = Cache.EXPIRE_POLICY .valueOf((String) _cacheExpirePolicy); } catch (IllegalArgumentException iae) { throw new IllegalArgumentException( "wrong configuration file format. " + "cache-expire-policy valid values are " + Arrays.toString(Cache.EXPIRE_POLICY.values())); } } if (_bcryptHashedPassword != null) { this.bcryptHashedPassword = (Boolean) _bcryptHashedPassword; } if (_createUser != null) { this.createUser = (Boolean) _createUser; } if (this.createUser && _createUserDocument != null) { try { this.createUserDocument = BsonDocument.parse((String) _createUserDocument); } catch (Exception ex) { throw new IllegalArgumentException( "wrong configuration file format. " + "create-user-document must be a json document", ex); } } if (_propertyNameId != null) { this.propertyNameId = (String) _propertyNameId; } if (_propertyNamePassword != null) { this.propertyNamePassword = (String) _propertyNamePassword; } if (_propertyNameRoles != null) { this.propertyNameRoles = (String) _propertyNameRoles; } }; } @Override public Account verify(Account account) { return account; } @Override public Account verify(String id, Credential credential) { final SimpleAccount account = getAccount(id); if (account != null && verifyCredential(account, credential)) { updateAuthTokenCache(account); return account; } else { return null; } } @Override public Account verify(Credential credential) { return null; } /** * if client authenticates with the DbIdentityManager passing the real * credentials update the account in the auth-token cache, otherwise the * client authenticating with the auth-token will not see roles updates * until the cache expires (by default TTL is 15 minutes after last request) * * @param account */ private void updateAuthTokenCache(SimpleAccount account) { Cache<String, SimpleAccount> authTokenCache = AuthTokenIdentityManager.getInstance().getCachedAccounts(); String id = account.getPrincipal().getName(); Optional<SimpleAccount> _authTokenAccount = authTokenCache.get(id); if (_authTokenAccount != null && _authTokenAccount.isPresent()) { SimpleAccount authTokenAccount = _authTokenAccount.get(); SimpleAccount updatedAuthTokenAccount = new SimpleAccount( id, authTokenAccount.getCredentials().getPassword(), account.getRoles()); authTokenCache.put(id, updatedAuthTokenAccount); LOGGER.debug("***** updated auth token cache"); } } private boolean verifyCredential(Account account, Credential credential) { String id = account.getPrincipal().getName(); SimpleAccount ourAccount = getAccount(id); if (ourAccount == null) { return false; } if (credential instanceof PasswordCredential && account instanceof SimpleAccount) { char[] password = ((PasswordCredential) credential).getPassword(); char[] expected = ourAccount.getCredentials().getPassword(); return checkPassword( this.bcryptHashedPassword, password, expected); } return false; } static boolean checkPassword(boolean hashed, char[] password, char[] expected) { if (hashed) { try { return BCrypt.checkpw( new String(password), new String(expected)); } catch (Throwable t) { return false; } } else { return Arrays.equals(password, expected); } } private SimpleAccount getAccount(String id) { if (cache == null) { return findAccount(id); } else { Optional<SimpleAccount> _account = cache.getLoading(id); if (_account != null && _account.isPresent()) { return _account.get(); } else { return null; } } } private SimpleAccount findAccount(String id) { Bson query = eq(this.propertyNameId, id); FindIterable<BsonDocument> result = mongoColl .find(query) .limit(1); if (result == null || !result.iterator().hasNext()) { LOGGER.debug("no account found with id: {}", id); return null; } BsonDocument _account = result.iterator().next(); if (!_account.containsKey(this.propertyNamePassword)) { LOGGER.error("account with id: {} does not have password {}", id, this.propertyNamePassword); return null; } BsonValue _password = _account.get(this.propertyNamePassword); if (_password == null || !_password.isString()) { LOGGER.debug( "account with id: {} " + "has an invalid password (not string): {}", id, _password); return null; } String password = _password.asString().getValue(); if (!_account.containsKey(this.propertyNameRoles)) { LOGGER.error("account with id: {} does not have {} property", id, this.propertyNameRoles); return null; } BsonValue _roles = _account.get(this.propertyNameRoles); if (_roles == null || !_roles.isArray()) { LOGGER.debug( "account with id: {} has an invalid roles (not array): {}", id, _roles); return null; } Set<String> roles = new HashSet<>(); List<BsonValue> __roles = _roles.asArray().getValues(); __roles.forEach(el -> { if (el != null && el.isString()) { roles.add(el.asString().getValue()); } else { LOGGER.debug( "account with _d: {} " + "has a not string role: {} ; ignoring it", id, el); } }); return new SimpleAccount(id, password.toCharArray(), roles); } }