//////////////////////////////////////////////////////////////////////////////// /// DISCLAIMER /// /// Copyright 2014-2018 ArangoDB GmbH, Cologne, Germany /// Copyright 2004-2014 triAGENS GmbH, Cologne, Germany /// /// 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. /// /// Copyright holder is ArangoDB GmbH, Cologne, Germany /// /// @author /// @author Dr. Frank Celler //////////////////////////////////////////////////////////////////////////////// #include "Auth/User.h" #include "Basics/ReadLocker.h" #include "Basics/ScopeGuard.h" #include "Basics/StaticStrings.h" #include "Basics/StringUtils.h" #include "Basics/VelocyPackHelper.h" #include "Basics/WriteLocker.h" #include "Basics/system-functions.h" #include "Basics/tri-strings.h" #include "Basics/tryEmplaceHelper.h" #include "Cluster/ServerState.h" #include "GeneralServer/GeneralServerFeature.h" #include "Logger/LogMacros.h" #include "Logger/Logger.h" #include "Logger/LoggerStream.h" #include "Random/UniformCharacter.h" #include "RestServer/DatabaseFeature.h" #include "Ssl/SslInterface.h" #include "Transaction/Helpers.h" #include "VocBase/LogicalCollection.h" #include "VocBase/Methods/Collections.h" #include "VocBase/Methods/Databases.h" #include #include #include using namespace arangodb; using namespace arangodb::basics; using namespace arangodb::rest; // private hash function static int HexHashFromData(std::string const& hashMethod, std::string const& str, std::string& outHash) { char* crypted = nullptr; size_t cryptedLength; try { if (hashMethod == "sha1") { arangodb::rest::SslInterface::sslSHA1(str.data(), str.size(), crypted, cryptedLength); } else if (hashMethod == "sha512") { arangodb::rest::SslInterface::sslSHA512(str.data(), str.size(), crypted, cryptedLength); } else if (hashMethod == "sha384") { arangodb::rest::SslInterface::sslSHA384(str.data(), str.size(), crypted, cryptedLength); } else if (hashMethod == "sha256") { arangodb::rest::SslInterface::sslSHA256(str.data(), str.size(), crypted, cryptedLength); } else if (hashMethod == "sha224") { arangodb::rest::SslInterface::sslSHA224(str.data(), str.size(), crypted, cryptedLength); } else if (hashMethod == "md5") { // WFT?!!! arangodb::rest::SslInterface::sslMD5(str.data(), str.size(), crypted, cryptedLength); } else { // invalid algorithm... LOG_TOPIC("3c13c", DEBUG, arangodb::Logger::AUTHENTICATION) << "invalid algorithm for hexHashFromData: " << hashMethod; return TRI_ERROR_BAD_PARAMETER; } } catch (...) { // SslInterface::ssl....() allocates strings with new, which might throw // exceptions return TRI_ERROR_FAILED; } TRI_DEFER(delete[] crypted); if (crypted == nullptr || cryptedLength == 0) { return TRI_ERROR_OUT_OF_MEMORY; } outHash = basics::StringUtils::encodeHex(crypted, cryptedLength); return TRI_ERROR_NO_ERROR; } static void AddSource(VPackBuilder& builder, auth::Source source) { switch (source) { case auth::Source::Local: // used to be collection builder.add("source", VPackValue("LOCAL")); break; case auth::Source::LDAP: builder.add("source", VPackValue("LDAP")); break; default: TRI_ASSERT(false); } } static void AddAuthLevel(VPackBuilder& builder, auth::Level lvl) { if (lvl == auth::Level::RW) { builder.add("read", VPackValue(true)); builder.add("write", VPackValue(true)); } else if (lvl == auth::Level::RO) { builder.add("read", VPackValue(true)); builder.add("write", VPackValue(false)); } else if (lvl == auth::Level::NONE) { builder.add("read", VPackValue(false)); builder.add("write", VPackValue(false)); } else if (lvl == auth::Level::UNDEFINED) { builder.add("undefined", VPackValue(true)); } } static auth::Level AuthLevelFromSlice(VPackSlice const& slice) { TRI_ASSERT(slice.isObject()); VPackSlice v = slice.get("write"); if (v.isBool() && v.isTrue()) { return auth::Level::RW; } v = slice.get("read"); if (v.isBool() && v.isTrue()) { return auth::Level::RO; } v = slice.get("undefined"); if (v.isBool() && v.isTrue()) { return auth::Level::UNDEFINED; } return auth::Level::NONE; } // ============= static ================== auth::User auth::User::newUser(std::string const& user, std::string const& password, auth::Source source) { auth::User entry("", 0); entry._active = true; entry._source = source; entry._username = user; entry._passwordMethod = "sha256"; std::string salt = UniformCharacter(8, "0123456789abcdef").random(); std::string hash; int res = HexHashFromData("sha256", salt + password, hash); if (res != TRI_ERROR_NO_ERROR) { THROW_ARANGO_EXCEPTION_MESSAGE(res, "Could not calculate hex-hash from data"); } entry._passwordSalt = salt; entry._passwordHash = hash; // build authentication entry return entry; } void auth::User::fromDocumentDatabases(auth::User& entry, VPackSlice const& databasesSlice, VPackSlice const& userSlice) { for (auto const& obj : VPackObjectIterator(databasesSlice)) { std::string const dbName = obj.key.copyString(); if (obj.value.isObject()) { auth::Level databaseAuth = auth::Level::NONE; auto const permissionsSlice = obj.value.get("permissions"); if (permissionsSlice.isObject()) { databaseAuth = AuthLevelFromSlice(permissionsSlice); } try { entry.grantDatabase(dbName, databaseAuth); } catch (arangodb::basics::Exception const& e) { LOG_TOPIC("a01a9", DEBUG, Logger::AUTHENTICATION) << e.message(); } VPackSlice collectionsSlice = obj.value.get("collections"); if (collectionsSlice.isObject()) { for (auto const& collection : VPackObjectIterator(collectionsSlice)) { std::string const cName = collection.key.copyString(); auto const collPerSlice = collection.value.get("permissions"); if (collPerSlice.isObject()) { try { entry.grantCollection(dbName, cName, AuthLevelFromSlice(collPerSlice)); } catch (arangodb::basics::Exception const& e) { LOG_TOPIC("181fa", DEBUG, Logger::AUTHENTICATION) << e.message(); } } } } } else { LOG_TOPIC("c4dd7", DEBUG, arangodb::Logger::CONFIG) << "updating deprecated access rights struct for user '" << userSlice.copyString() << "'"; VPackValueLength length; char const* value = obj.value.getString(length); if (TRI_CaseEqualString(value, "rw", 2)) { entry.grantDatabase(dbName, auth::Level::RW); entry.grantCollection(dbName, "*", auth::Level::RW); } else if (TRI_CaseEqualString(value, "ro", 2)) { entry.grantDatabase(dbName, auth::Level::RO); entry.grantCollection(dbName, "*", auth::Level::RO); } } } } auth::User auth::User::fromDocument(VPackSlice const& slice) { if (slice.isNone() || !slice.isObject()) { THROW_ARANGO_EXCEPTION(TRI_ERROR_BAD_PARAMETER); } VPackSlice const keySlice = transaction::helpers::extractKeyFromDocument(slice); if (!keySlice.isString()) { THROW_ARANGO_EXCEPTION_MESSAGE(TRI_ERROR_BAD_PARAMETER, "cannot extract _key"); } TRI_voc_rid_t rev = transaction::helpers::extractRevFromDocument(slice); if (rev == 0) { THROW_ARANGO_EXCEPTION_MESSAGE(TRI_ERROR_BAD_PARAMETER, "cannot extract _rev"); } // extract "user" attribute VPackSlice const userSlice = slice.get("user"); if (!userSlice.isString()) { THROW_ARANGO_EXCEPTION_MESSAGE(TRI_ERROR_BAD_PARAMETER, "cannot extract username"); } VPackSlice const authDataSlice = slice.get("authData"); if (!authDataSlice.isObject()) { THROW_ARANGO_EXCEPTION_MESSAGE(TRI_ERROR_BAD_PARAMETER, "cannot extract authData"); } VPackSlice const simpleSlice = authDataSlice.get("simple"); if (!simpleSlice.isObject()) { LOG_TOPIC("e159f", DEBUG, arangodb::Logger::AUTHENTICATION) << "cannot extract simple"; return auth::User("", 0); } VPackSlice const methodSlice = simpleSlice.get("method"); VPackSlice const saltSlice = simpleSlice.get("salt"); VPackSlice const hashSlice = simpleSlice.get("hash"); if (!methodSlice.isString() || !saltSlice.isString() || !hashSlice.isString()) { LOG_TOPIC("09122", DEBUG, arangodb::Logger::AUTHENTICATION) << "cannot extract password internals"; return auth::User("", 0); } // extract "active" attribute VPackSlice const activeSlice = authDataSlice.get("active"); if (!activeSlice.isBoolean()) { LOG_TOPIC("857e0", DEBUG, arangodb::Logger::AUTHENTICATION) << "cannot extract active flag"; return auth::User("", 0); } auth::User entry(keySlice.copyString(), rev); entry._active = activeSlice.getBool(); entry._source = auth::Source::Local; entry._username = userSlice.copyString(); entry._passwordMethod = methodSlice.copyString(); entry._passwordSalt = saltSlice.copyString(); entry._passwordHash = hashSlice.copyString(); // extract "databases" attribute VPackSlice const databasesSlice = slice.get("databases"); if (databasesSlice.isObject()) { fromDocumentDatabases(entry, databasesSlice, userSlice); } VPackSlice userDataSlice = slice.get("userData"); if (userDataSlice.isObject() && !userDataSlice.isEmptyObject()) { entry._userData.clear(); entry._userData.add(userDataSlice); } VPackSlice userConfigSlice = slice.get("configData"); if (userConfigSlice.isObject() && !userConfigSlice.isEmptyObject()) { entry._configData.clear(); entry._configData.add(userConfigSlice); } // ensure the root user always has the right to change permissions if (entry._username == "root") { entry.grantDatabase(StaticStrings::SystemDatabase, auth::Level::RW); entry.grantCollection(StaticStrings::SystemDatabase, "*", auth::Level::RW); } // build authentication entry return entry; } // ===================== Constructor ======================= auth::User::User(std::string&& key, TRI_voc_rid_t rid) : _key(std::move(key)), _rev(rid), _loaded(TRI_microtime()) {} // ======================= Methods ========================== void auth::User::touch() { _loaded = TRI_microtime(); } bool auth::User::checkPassword(std::string const& password) const { std::string hash; int res = HexHashFromData(_passwordMethod, _passwordSalt + password, hash); if (res != TRI_ERROR_NO_ERROR) { THROW_ARANGO_EXCEPTION_MESSAGE(res, "Could not calculate hex-hash from input"); } return _passwordHash == hash; } void auth::User::updatePassword(std::string const& password) { std::string hash; int res = HexHashFromData(_passwordMethod, _passwordSalt + password, hash); if (res != TRI_ERROR_NO_ERROR) { THROW_ARANGO_EXCEPTION_MESSAGE(res, "Could not calculate hex-hash from input"); } _passwordHash = hash; } VPackBuilder auth::User::toVPackBuilder() const { TRI_ASSERT(!_username.empty()); VPackBuilder builder; { VPackObjectBuilder o(&builder, true); if (!_key.empty()) { builder.add(StaticStrings::KeyString, VPackValue(_key)); } if (_rev > 0) { builder.add(StaticStrings::RevString, VPackValue(TRI_RidToString(_rev))); } builder.add("user", VPackValue(_username)); AddSource(builder, _source); // authData sub-object { VPackObjectBuilder o2(&builder, "authData", true); builder.add("active", VPackValue(_active)); if (_source == auth::Source::Local) { VPackObjectBuilder o3(&builder, "simple", true); builder.add("hash", VPackValue(_passwordHash)); builder.add("salt", VPackValue(_passwordSalt)); builder.add("method", VPackValue(_passwordMethod)); } } { // databases sub-object VPackObjectBuilder o2(&builder, "databases", true); for (auto const& dbCtxPair : _dbAccess) { VPackObjectBuilder o3(&builder, dbCtxPair.first, true); // permissions { VPackObjectBuilder o4(&builder, "permissions", true); auth::Level lvl = dbCtxPair.second._databaseAuthLevel; AddAuthLevel(builder, lvl); } // collections { VPackObjectBuilder o5(&builder, "collections", true); for (auto const& colAccessPair : dbCtxPair.second._collectionAccess) { VPackObjectBuilder o6(&builder, colAccessPair.first, true); VPackObjectBuilder o7(&builder, "permissions", true); AddAuthLevel(builder, colAccessPair.second); } } } } if (!_userData.isEmpty() && _userData.isClosed() && _userData.slice().isObject()) { builder.add("userData", _userData.slice()); } if (!_configData.isEmpty() && _configData.isClosed() && _configData.slice().isObject()) { builder.add("configData", _configData.slice()); } } return builder; } void auth::User::grantDatabase(std::string const& dbname, auth::Level level) { if (dbname.empty() || level == auth::Level::UNDEFINED) { THROW_ARANGO_EXCEPTION_MESSAGE(TRI_ERROR_BAD_PARAMETER, "Cannot set rights for empty db name"); } if (_username == "root" && dbname == StaticStrings::SystemDatabase && level != auth::Level::RW) { THROW_ARANGO_EXCEPTION_MESSAGE( TRI_ERROR_FORBIDDEN, "Cannot lower access level of 'root' to _system"); } LOG_TOPIC("b9d75", DEBUG, Logger::AUTHENTICATION) << _username << ": Granting " << auth::convertFromAuthLevel(level) << " on " << dbname; auto it = _dbAccess.find(dbname); if (it != _dbAccess.end()) { it->second._databaseAuthLevel = level; } else { // grantDatabase is not supposed to change any rights on the // collection level code which relies on the old behavior // will need to be adjusted _dbAccess.try_emplace(dbname, DBAuthContext(level, CollLevelMap())); } } /// Removes the entry, returns true if entry existed bool auth::User::removeDatabase(std::string const& dbname) { if (dbname.empty()) { THROW_ARANGO_EXCEPTION_MESSAGE(TRI_ERROR_BAD_PARAMETER, "Cannot remove rights for empty db name"); } if (_username == "root" && dbname == StaticStrings::SystemDatabase) { THROW_ARANGO_EXCEPTION_MESSAGE( TRI_ERROR_FORBIDDEN, "Cannot remove access level of 'root' to _system"); } LOG_TOPIC("f1382", DEBUG, Logger::AUTHENTICATION) << _username << ": Removing grant on " << dbname; return _dbAccess.erase(dbname) > 0; } void auth::User::grantCollection(std::string const& dbname, std::string const& cname, auth::Level const level) { if (dbname.empty() || cname.empty() || level == auth::Level::UNDEFINED) { THROW_ARANGO_EXCEPTION_MESSAGE( TRI_ERROR_BAD_PARAMETER, "Cannot set rights for empty db / collection name"); } else if (cname[0] == '_') { THROW_ARANGO_EXCEPTION_MESSAGE(TRI_ERROR_BAD_PARAMETER, "Cannot set rights for system collections"); } else if (_username == "root" && dbname == StaticStrings::SystemDatabase && cname == "*" && level != auth::Level::RW) { THROW_ARANGO_EXCEPTION_MESSAGE(TRI_ERROR_FORBIDDEN, "Cannot lower access level of 'root' to " " a system collection"); } else if (dbname == "*" && cname != "*") { THROW_ARANGO_EXCEPTION_MESSAGE(TRI_ERROR_BAD_PARAMETER, "Invalid database / collection pair"); } LOG_TOPIC("d333a", DEBUG, Logger::AUTHENTICATION) << _username << ": Granting " << auth::convertFromAuthLevel(level) << " on " << dbname << "/" << cname; auto[it, emplaced] = _dbAccess.try_emplace( dbname, arangodb::lazyConstruct([&]{ // do not overwrite wildcard access to a database, by granting more // specific rights to a collection in a specific db auth::Level lvl = auth::Level::UNDEFINED; return DBAuthContext(lvl, CollLevelMap({{cname, level}})); }) ); if (!emplaced) { it->second._collectionAccess[cname] = level; } } /// Removes the collection right, returns true if entry existed bool auth::User::removeCollection(std::string const& dbname, std::string const& cname) { if (dbname.empty() || cname.empty()) { THROW_ARANGO_EXCEPTION_MESSAGE( TRI_ERROR_BAD_PARAMETER, "Cannot set rights for empty db / collection name"); } if (_username == "root" && dbname == StaticStrings::SystemDatabase && (cname == "*")) { THROW_ARANGO_EXCEPTION_MESSAGE(TRI_ERROR_FORBIDDEN, "Cannot lower access level of 'root' to " " a collection in _system"); } LOG_TOPIC("78e62", DEBUG, Logger::AUTHENTICATION) << _username << ": Removing grant on " << dbname << "/" << cname; auto const& it = _dbAccess.find(dbname); if (it != _dbAccess.end()) { return it->second._collectionAccess.erase(cname) > 0; } return false; } // Resolve the access level for this database. auth::Level auth::User::configuredDBAuthLevel(std::string const& dbname) const { auto it = _dbAccess.find(dbname); if (it != _dbAccess.end()) { // found specific grant return it->second._databaseAuthLevel; } return auth::Level::UNDEFINED; } // Resolve rights for the specified collection. auth::Level auth::User::configuredCollectionAuthLevel(std::string const& dbname, std::string const& cname) const { auto it = _dbAccess.find(dbname); if (it != _dbAccess.end()) { // Second try to find a specific grant CollLevelMap::const_iterator pair = it->second._collectionAccess.find(cname); if (pair != it->second._collectionAccess.end()) { return pair->second; // found specific collection grant } } return auth::Level::UNDEFINED; } auth::Level auth::User::databaseAuthLevel(std::string const& dbname) const { auth::Level lvl = configuredDBAuthLevel(dbname); if (lvl == auth::Level::UNDEFINED && dbname != "*") { // take best from wildcard or _system auto it = _dbAccess.find("*"); if (it != _dbAccess.end()) { lvl = std::max(it->second._databaseAuthLevel, lvl); } if (dbname != StaticStrings::SystemDatabase) { it = _dbAccess.find(StaticStrings::SystemDatabase); if (it != _dbAccess.end()) { lvl = std::max(it->second._databaseAuthLevel, lvl); } } } return std::max(lvl, auth::Level::NONE); } /// Find the access level for a collection. Will automatically try to fall back auth::Level auth::User::collectionAuthLevel(std::string const& dbname, std::string const& cname) const { if (cname.empty() || (dbname == "*" && cname != "*")) { return auth::Level::NONE; // invalid collection names } // we must have got a non-empty collection name when we get here TRI_ASSERT(cname[0] < '0' || cname[0] > '9'); bool isSystem = cname[0] == '_'; if (isSystem) { // disallow access to _system/_users for everyone if (dbname == TRI_VOC_SYSTEM_DATABASE && cname == TRI_COL_NAME_USERS) { return auth::Level::NONE; } else if (cname == "_queues") { return auth::Level::RO; } else if (cname == "_frontend") { return auth::Level::RW; } return databaseAuthLevel(dbname); } auth::Level lvl = auth::Level::NONE; if (dbname != "*") { // skip special rules for wildcard auto it = _dbAccess.find(dbname); if (it != _dbAccess.end()) { // Second try to find a specific grant CollLevelMap::const_iterator pair = it->second._collectionAccess.find(cname); if (pair != it->second._collectionAccess.end()) { return pair->second; // found specific collection grant } else if (cname == "*") { // skip special rules for wildcard return auth::Level::NONE; } // Fallback step 1. lvl = it->second._databaseAuthLevel; pair = it->second._collectionAccess.find("*"); if (pair != it->second._collectionAccess.end()) { // found wildcard collection grant, take better default lvl = std::max(pair->second, lvl); } } if (dbname != StaticStrings::SystemDatabase) { // Fallback step 3. look into _system it = _dbAccess.find(StaticStrings::SystemDatabase); if (it != _dbAccess.end()) { lvl = std::max(it->second._databaseAuthLevel, lvl); } } } // Fallback step 2. is to look into the "*" database auto it = _dbAccess.find("*"); if (it != _dbAccess.end()) { lvl = std::max(it->second._databaseAuthLevel, lvl); if (!isSystem) { CollLevelMap::const_iterator pair = it->second._collectionAccess.find("*"); if (pair != it->second._collectionAccess.end()) { // found wildcard collection grant, take better default lvl = std::max(pair->second, lvl); } } // nothing found } return lvl; }