1
0
Fork 0
arangodb/arangod/Auth/User.cpp

612 lines
21 KiB
C++

////////////////////////////////////////////////////////////////////////////////
/// 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/StaticStrings.h"
#include "Basics/StringUtils.h"
#include "Basics/VelocyPackHelper.h"
#include "Basics/WriteLocker.h"
#include "Basics/tri-strings.h"
#include "Basics/ScopeGuard.h"
#include "Cluster/ServerState.h"
#include "GeneralServer/GeneralServerFeature.h"
#include "Logger/Logger.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 <velocypack/Iterator.h>
#include <velocypack/StringRef.h>
#include <velocypack/velocypack-aliases.h>
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.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 = _dbAccess.find(dbname);
if (it != _dbAccess.end()) {
it->second._collectionAccess[cname] = level;
} else {
// 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;
_dbAccess.emplace(dbname, DBAuthContext(lvl, CollLevelMap({{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(!isdigit(cname[0]));
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;
}