mirror of https://gitee.com/bigwinds/arangodb
added roles to users (#3354)
This commit is contained in:
parent
206063c8fe
commit
bd5e84a12f
|
@ -1,6 +1,13 @@
|
|||
devel
|
||||
-----
|
||||
|
||||
* usernames must not start with `:role:`, added new options:
|
||||
--server.authentication-timeout
|
||||
--ldap.roles-attribute-name
|
||||
--ldap.roles-transformation
|
||||
--ldap.roles-search
|
||||
--ldap.superuser-role
|
||||
|
||||
* performance improvements for full collection scans and a few other operations
|
||||
in MMFiles engine
|
||||
|
||||
|
|
|
@ -221,12 +221,18 @@ The default value is *false*.
|
|||
**Note**: this option is only available on platforms that support UNIX
|
||||
domain sockets.
|
||||
|
||||
|
||||
### Enable/disable authentication for system API requests only
|
||||
|
||||
@startDocuBlock serverAuthenticateSystemOnly
|
||||
|
||||
|
||||
### Enable authentication cache timeout
|
||||
|
||||
`--server.authentication-timeout value`
|
||||
|
||||
Sets the cache timeout to *value* (in seconds). This is only necessary
|
||||
if you use an external authentication system like LDAP.
|
||||
|
||||
### Enable/disable replication applier
|
||||
|
||||
`--database.replication-applier flag`
|
||||
|
|
|
@ -7,6 +7,8 @@ PostgreSQL, or other database systems.
|
|||
User management is possible in the [web interface](../WebInterface/Users.md)
|
||||
and in [arangosh](InArangosh.md) while logged on to the *\_system* database.
|
||||
|
||||
Note that the only usernames *must* not start with `:role:`.
|
||||
|
||||
Actions and Access Levels
|
||||
-------------------------
|
||||
|
||||
|
|
|
@ -61,15 +61,20 @@ rm -f $GENPATH
|
|||
ln -s `pwd` $GENPATH
|
||||
cd $GENPATH
|
||||
|
||||
if test -z "$CMAKE_BUILD_TYPE"; then
|
||||
CMAKE_BUILD_TYPE=RelWithDebInfo
|
||||
fi
|
||||
|
||||
echo "CONCURRENY: $concurrency"
|
||||
echo "HOST: `hostname`"
|
||||
echo "PWD: `pwd`"
|
||||
echo "BUILD_TYPE: $CMAKE_BUILD_TYPE"
|
||||
|
||||
function configureBuild {
|
||||
echo "`date +%T` configuring..."
|
||||
CXXFLAGS=-fno-omit-frame-pointer \
|
||||
cmake \
|
||||
-DCMAKE_BUILD_TYPE=RelWithDebInfo \
|
||||
-DCMAKE_BUILD_TYPE=$CMAKE_BUILD_TYPE \
|
||||
-DCMAKE_INSTALL_PREFIX=/ \
|
||||
-DUSE_CATCH_TESTS=On \
|
||||
-DDEBUG_SYNC_REPLICATION=On \
|
||||
|
|
|
@ -79,6 +79,10 @@ void AuthenticationFeature::collectOptions(
|
|||
"enable or disable authentication for ALL client requests",
|
||||
new BooleanParameter(&_active));
|
||||
|
||||
options->addOption("--server.authentication-timeout",
|
||||
"timeout for the authentication cache (0 = undefinitely)",
|
||||
new DoubleParameter(&_authenticationTimeout));
|
||||
|
||||
options->addOption(
|
||||
"--server.authentication-system-only",
|
||||
"use HTTP authentication only for requests to /_api and /_admin",
|
||||
|
|
|
@ -50,6 +50,7 @@ class AuthenticationFeature final
|
|||
AuthInfo* _authInfo;
|
||||
bool _authenticationUnixSockets;
|
||||
bool _authenticationSystemOnly;
|
||||
double _authenticationTimeout;
|
||||
|
||||
std::string _jwtSecretProgramOption;
|
||||
bool _active;
|
||||
|
@ -59,10 +60,12 @@ class AuthenticationFeature final
|
|||
bool authenticationSystemOnly() const { return _authenticationSystemOnly; }
|
||||
std::string jwtSecret() { return authInfo()->jwtSecret(); }
|
||||
std::string generateNewJwtSecret();
|
||||
bool hasUserdefinedJwt() { return !_jwtSecretProgramOption.empty(); }
|
||||
bool hasUserdefinedJwt() const { return !_jwtSecretProgramOption.empty(); }
|
||||
void setJwtSecret(std::string const& jwtSecret) {
|
||||
authInfo()->setJwtSecret(jwtSecret);
|
||||
}
|
||||
double authenticationTimeout() const { return _authenticationTimeout; }
|
||||
|
||||
AuthInfo* authInfo();
|
||||
AuthLevel canUseDatabase(std::string const& username,
|
||||
std::string const& dbname);
|
||||
|
|
|
@ -43,19 +43,28 @@ class AuthenticationResult : public arangodb::Result {
|
|||
|
||||
AuthenticationResult(int errorNumber, AuthSource const& source)
|
||||
: Result(errorNumber), _authSource(source) {}
|
||||
|
||||
AuthenticationResult(
|
||||
std::unordered_map<std::string, std::string> const& permissions,
|
||||
AuthSource const& source)
|
||||
: Result(0), _authSource(source), _permissions(permissions) {}
|
||||
std::unordered_set<std::string> roles, AuthSource const& source)
|
||||
: Result(0),
|
||||
_authSource(source),
|
||||
_permissions(permissions),
|
||||
_roles(roles) {}
|
||||
|
||||
public:
|
||||
AuthSource source() const { return _authSource; }
|
||||
|
||||
std::unordered_map<std::string, std::string> permissions() const {
|
||||
return _permissions;
|
||||
}
|
||||
|
||||
std::unordered_set<std::string> roles() const { return _roles; }
|
||||
|
||||
protected:
|
||||
AuthSource _authSource;
|
||||
std::unordered_map<std::string, std::string> _permissions;
|
||||
std::unordered_set<std::string> _roles;
|
||||
};
|
||||
|
||||
class AuthenticationHandler {
|
||||
|
|
|
@ -103,6 +103,7 @@ bool AuthInfo::parseUsers(VPackSlice const& slice) {
|
|||
<< s.get("user").copyString();
|
||||
continue;
|
||||
}
|
||||
|
||||
AuthUserEntry auth = AuthUserEntry::fromDocument(s);
|
||||
|
||||
// we also need to insert inactive users into the cache here
|
||||
|
@ -121,7 +122,7 @@ static std::shared_ptr<VPackBuilder> QueryAllUsers(
|
|||
LOG_TOPIC(DEBUG, arangodb::Logger::FIXME) << "system database is unknown";
|
||||
THROW_ARANGO_EXCEPTION(TRI_ERROR_INTERNAL);
|
||||
}
|
||||
|
||||
|
||||
// we cannot set this execution context, otherwise the transaction
|
||||
// will ask us again for permissions and we get a deadlock
|
||||
ExecContext* oldExe = ExecContext::CURRENT;
|
||||
|
@ -145,7 +146,7 @@ static std::shared_ptr<VPackBuilder> QueryAllUsers(
|
|||
}
|
||||
THROW_ARANGO_EXCEPTION_MESSAGE(queryResult.code,
|
||||
"Error executing user query");
|
||||
//return std::shared_ptr<VPackBuilder>();
|
||||
// return std::shared_ptr<VPackBuilder>();
|
||||
}
|
||||
|
||||
VPackSlice usersSlice = queryResult.result->slice();
|
||||
|
@ -163,6 +164,7 @@ static std::shared_ptr<VPackBuilder> QueryAllUsers(
|
|||
static VPackBuilder QueryUser(aql::QueryRegistry* queryRegistry,
|
||||
std::string const& user) {
|
||||
TRI_vocbase_t* vocbase = DatabaseFeature::DATABASE->systemDatabase();
|
||||
|
||||
if (vocbase == nullptr) {
|
||||
THROW_ARANGO_EXCEPTION_MESSAGE(TRI_ERROR_FAILED, "_system db is unknown");
|
||||
}
|
||||
|
@ -172,7 +174,7 @@ static VPackBuilder QueryUser(aql::QueryRegistry* queryRegistry,
|
|||
ExecContext* oldExe = ExecContext::CURRENT;
|
||||
ExecContext::CURRENT = nullptr;
|
||||
TRI_DEFER(ExecContext::CURRENT = oldExe);
|
||||
|
||||
|
||||
std::string const queryStr("FOR u IN _users FILTER u.user == @name RETURN u");
|
||||
auto emptyBuilder = std::make_shared<VPackBuilder>();
|
||||
|
||||
|
@ -191,22 +193,27 @@ static VPackBuilder QueryUser(aql::QueryRegistry* queryRegistry,
|
|||
(queryResult.code == TRI_ERROR_QUERY_KILLED)) {
|
||||
THROW_ARANGO_EXCEPTION(TRI_ERROR_REQUEST_CANCELED);
|
||||
}
|
||||
|
||||
THROW_ARANGO_EXCEPTION_MESSAGE(queryResult.code,
|
||||
"Error executing user query");
|
||||
}
|
||||
|
||||
VPackSlice usersSlice = queryResult.result->slice();
|
||||
|
||||
if (usersSlice.isNone() || !usersSlice.isArray()) {
|
||||
THROW_ARANGO_EXCEPTION(TRI_ERROR_OUT_OF_MEMORY);
|
||||
}
|
||||
|
||||
if (usersSlice.length() == 0) {
|
||||
THROW_ARANGO_EXCEPTION(TRI_ERROR_USER_NOT_FOUND);
|
||||
}
|
||||
|
||||
VPackSlice doc = usersSlice.at(0);
|
||||
|
||||
if (doc.isExternal()) {
|
||||
doc = doc.resolveExternals();
|
||||
}
|
||||
|
||||
return VPackBuilder(doc);
|
||||
}
|
||||
|
||||
|
@ -227,12 +234,15 @@ static void ConvertLegacyFormat(VPackSlice doc, VPackBuilder& result) {
|
|||
void AuthInfo::loadFromDB() {
|
||||
TRI_ASSERT(_queryRegistry != nullptr);
|
||||
TRI_ASSERT(ServerState::instance()->isSingleServerOrCoordinator());
|
||||
|
||||
auto role = ServerState::instance()->getRole();
|
||||
|
||||
if (role != ServerState::ROLE_SINGLE &&
|
||||
role != ServerState::ROLE_COORDINATOR) {
|
||||
_outdated = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_outdated) {
|
||||
return;
|
||||
}
|
||||
|
@ -245,20 +255,24 @@ void AuthInfo::loadFromDB() {
|
|||
}
|
||||
|
||||
WRITE_LOCKER(writeLocker, _authInfoLock);
|
||||
|
||||
try {
|
||||
std::shared_ptr<VPackBuilder> builder = QueryAllUsers(_queryRegistry);
|
||||
_authInfo.clear();
|
||||
_authBasicCache.clear();
|
||||
|
||||
if (builder) {
|
||||
VPackSlice usersSlice = builder->slice();
|
||||
|
||||
if (usersSlice.length() != 0) {
|
||||
parseUsers(usersSlice);
|
||||
}
|
||||
}
|
||||
_outdated = _authInfo.empty() == true;
|
||||
|
||||
_outdated = (_authInfo.empty() == true);
|
||||
} catch (...) {
|
||||
LOG_TOPIC(WARN, Logger::AUTHENTICATION)
|
||||
<< "Exception when loading users from db";
|
||||
<< "Exception when loading users from db";
|
||||
_outdated = true;
|
||||
}
|
||||
}
|
||||
|
@ -266,7 +280,7 @@ void AuthInfo::loadFromDB() {
|
|||
// only call from the boostrap feature, must be sure to be the only one
|
||||
void AuthInfo::createRootUser() {
|
||||
loadFromDB();
|
||||
|
||||
|
||||
MUTEX_LOCKER(locker, _loadFromDBLock);
|
||||
WRITE_LOCKER(writeLocker, _authInfoLock);
|
||||
auto it = _authInfo.find("root");
|
||||
|
@ -275,7 +289,7 @@ void AuthInfo::createRootUser() {
|
|||
return;
|
||||
}
|
||||
TRI_ASSERT(_authInfo.empty());
|
||||
|
||||
|
||||
try {
|
||||
// Attention:
|
||||
// the root user needs to have a specific rights grant
|
||||
|
@ -283,6 +297,7 @@ void AuthInfo::createRootUser() {
|
|||
auto initDatabaseFeature =
|
||||
application_features::ApplicationServer::getFeature<
|
||||
InitDatabaseFeature>("InitDatabase");
|
||||
|
||||
TRI_ASSERT(initDatabaseFeature != nullptr);
|
||||
|
||||
AuthUserEntry entry = AuthUserEntry::newUser(
|
||||
|
@ -308,7 +323,7 @@ Result AuthInfo::storeUserInternal(AuthUserEntry const& entry, bool replace) {
|
|||
if (vocbase == nullptr) {
|
||||
return Result(TRI_ERROR_INTERNAL);
|
||||
}
|
||||
|
||||
|
||||
// we cannot set this execution context, otherwise the transaction
|
||||
// will ask us again for permissions and we get a deadlock
|
||||
ExecContext* oldExe = ExecContext::CURRENT;
|
||||
|
@ -355,7 +370,9 @@ Result AuthInfo::storeUserInternal(AuthUserEntry const& entry, bool replace) {
|
|||
return res;
|
||||
}
|
||||
|
||||
// ================= public ==================
|
||||
// -----------------------------------------------------------------------------
|
||||
// -- SECTION -- public
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
VPackBuilder AuthInfo::allUsers() {
|
||||
// will query db directly, no need for _authInfoLock
|
||||
|
@ -385,16 +402,20 @@ void AuthInfo::reloadAllUsers() {
|
|||
// tell other coordinators to reload as well
|
||||
AgencyComm agency;
|
||||
int maxTries = 10;
|
||||
|
||||
while (maxTries-- > 0) {
|
||||
AgencyCommResult commRes = agency.getValues("Sync/UserVersion");
|
||||
|
||||
if (!commRes.successful()) {
|
||||
// Error in communication, note that value not found is not an error
|
||||
LOG_TOPIC(TRACE, Logger::AUTHENTICATION)
|
||||
<< "AuthInfo: no agency communication";
|
||||
break;
|
||||
}
|
||||
|
||||
VPackSlice oldVal = commRes.slice()[0].get(
|
||||
{AgencyCommManager::path(), "Sync", "UserVersion"});
|
||||
|
||||
if (!oldVal.isInteger()) {
|
||||
LOG_TOPIC(ERR, Logger::AUTHENTICATION)
|
||||
<< "Sync/UserVersion is not a number";
|
||||
|
@ -406,10 +427,12 @@ void AuthInfo::reloadAllUsers() {
|
|||
commRes =
|
||||
agency.casValue("Sync/UserVersion", oldVal, newVal.slice(), 0.0,
|
||||
AgencyCommManager::CONNECTION_OPTIONS._requestTimeout);
|
||||
|
||||
if (commRes.successful()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
LOG_TOPIC(WARN, Logger::AUTHENTICATION)
|
||||
<< "Sync/UserVersion could not be updated";
|
||||
}
|
||||
|
@ -419,6 +442,7 @@ Result AuthInfo::storeUser(bool replace, std::string const& user,
|
|||
if (user.empty()) {
|
||||
return TRI_ERROR_USER_INVALID_NAME;
|
||||
}
|
||||
|
||||
loadFromDB();
|
||||
|
||||
WRITE_LOCKER(writeGuard, _authInfoLock);
|
||||
|
@ -438,9 +462,11 @@ Result AuthInfo::storeUser(bool replace, std::string const& user,
|
|||
}
|
||||
|
||||
Result r = storeUserInternal(entry, replace);
|
||||
|
||||
if (r.ok()) {
|
||||
reloadAllUsers();
|
||||
}
|
||||
|
||||
return r;
|
||||
}
|
||||
|
||||
|
@ -450,7 +476,7 @@ static Result UpdateUser(VPackSlice const& user) {
|
|||
if (vocbase == nullptr) {
|
||||
return Result(TRI_ERROR_INTERNAL);
|
||||
}
|
||||
|
||||
|
||||
// we cannot set this execution context, otherwise the transaction
|
||||
// will ask us again for permissions and we get a deadlock
|
||||
ExecContext* oldExe = ExecContext::CURRENT;
|
||||
|
@ -474,7 +500,8 @@ static Result UpdateUser(VPackSlice const& user) {
|
|||
Result AuthInfo::enumerateUsers(
|
||||
std::function<void(AuthUserEntry&)> const& func) {
|
||||
loadFromDB();
|
||||
// we require an consisten view on the user object
|
||||
|
||||
// we require an consistent view on the user object
|
||||
{
|
||||
WRITE_LOCKER(guard, _authInfoLock);
|
||||
for (auto& it : _authInfo) {
|
||||
|
@ -486,10 +513,12 @@ Result AuthInfo::enumerateUsers(
|
|||
return r;
|
||||
}
|
||||
}
|
||||
|
||||
// must also clear the basic cache here because the secret may be
|
||||
// invalid now if the password was changed
|
||||
_authBasicCache.clear();
|
||||
}
|
||||
|
||||
// we need to reload data after the next callback
|
||||
reloadAllUsers();
|
||||
return TRI_ERROR_NO_ERROR;
|
||||
|
@ -500,20 +529,25 @@ Result AuthInfo::updateUser(std::string const& user,
|
|||
if (user.empty()) {
|
||||
return TRI_ERROR_USER_NOT_FOUND;
|
||||
}
|
||||
|
||||
loadFromDB();
|
||||
|
||||
Result r;
|
||||
{ // we require an consisten view on the user object
|
||||
WRITE_LOCKER(guard, _authInfoLock);
|
||||
auto it = _authInfo.find(user);
|
||||
|
||||
if (it == _authInfo.end()) {
|
||||
return Result(TRI_ERROR_USER_NOT_FOUND);
|
||||
}
|
||||
|
||||
TRI_ASSERT(!it->second.key().empty());
|
||||
func(it->second);
|
||||
VPackBuilder data = it->second.toVPackBuilder();
|
||||
r = UpdateUser(data.slice());
|
||||
// must also clear the basic cache here because the secret may be
|
||||
// invalid now if the password was changed
|
||||
|
||||
_authBasicCache.clear();
|
||||
}
|
||||
|
||||
|
@ -552,7 +586,7 @@ static Result RemoveUserInternal(AuthUserEntry const& entry) {
|
|||
if (vocbase == nullptr) {
|
||||
return Result(TRI_ERROR_INTERNAL);
|
||||
}
|
||||
|
||||
|
||||
// we cannot set this execution context, otherwise the transaction
|
||||
// will ask us again for permissions and we get a deadlock
|
||||
ExecContext* oldExe = ExecContext::CURRENT;
|
||||
|
@ -613,19 +647,24 @@ Result AuthInfo::removeAllUsers() {
|
|||
Result res;
|
||||
{
|
||||
WRITE_LOCKER(guard, _authInfoLock);
|
||||
|
||||
for (auto const& pair : _authInfo) {
|
||||
res = RemoveUserInternal(pair.second);
|
||||
|
||||
if (!res.ok()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
{ // do not get into race conditions with loadFromDB
|
||||
|
||||
// do not get into race conditions with loadFromDB
|
||||
{
|
||||
MUTEX_LOCKER(locker, _loadFromDBLock);
|
||||
_authInfo.clear();
|
||||
_authBasicCache.clear();
|
||||
_outdated = true;
|
||||
}
|
||||
}
|
||||
|
||||
reloadAllUsers();
|
||||
return res;
|
||||
}
|
||||
|
@ -633,6 +672,7 @@ Result AuthInfo::removeAllUsers() {
|
|||
VPackBuilder AuthInfo::getConfigData(std::string const& username) {
|
||||
loadFromDB();
|
||||
VPackBuilder bb = QueryUser(_queryRegistry, username);
|
||||
|
||||
return bb.isEmpty() ? bb : VPackBuilder(bb.slice().get("configData"));
|
||||
}
|
||||
|
||||
|
@ -684,16 +724,23 @@ Result AuthInfo::setUserData(std::string const& user,
|
|||
|
||||
AuthResult AuthInfo::checkPassword(std::string const& username,
|
||||
std::string const& password) {
|
||||
AuthResult result(username);
|
||||
|
||||
if (StringUtils::isPrefix(username, ":role:")) {
|
||||
return result;
|
||||
}
|
||||
|
||||
loadFromDB();
|
||||
|
||||
READ_LOCKER(readLocker, _authInfoLock);
|
||||
AuthResult result(username);
|
||||
|
||||
auto it = _authInfo.find(username);
|
||||
|
||||
if (it == _authInfo.end() || (it->second.source() == AuthSource::LDAP)) {
|
||||
TRI_ASSERT(_authenticationHandler);
|
||||
AuthenticationResult authResult =
|
||||
_authenticationHandler->authenticate(username, password);
|
||||
|
||||
if (!authResult.ok()) {
|
||||
return result;
|
||||
}
|
||||
|
@ -702,27 +749,33 @@ AuthResult AuthInfo::checkPassword(std::string const& username,
|
|||
if (authResult.source() == AuthSource::LDAP) {
|
||||
AuthUserEntry entry =
|
||||
AuthUserEntry::newUser(username, password, AuthSource::LDAP);
|
||||
entry.setRoles(authResult.roles());
|
||||
|
||||
// upgrade read-lock to a write-lock
|
||||
readLocker.unlock();
|
||||
WRITE_LOCKER(writeLocker, _authInfoLock);
|
||||
|
||||
it = _authInfo.find(username);
|
||||
|
||||
if (it != _authInfo.end()) {
|
||||
it->second = entry;
|
||||
} else {
|
||||
auto r = _authInfo.emplace(username, entry);
|
||||
|
||||
if (!r.second) {
|
||||
return result;
|
||||
}
|
||||
|
||||
it = r.first;
|
||||
}
|
||||
AuthUserEntry const& auth = it->second;
|
||||
|
||||
if (auth.isActive()) {
|
||||
result._authorized = auth.checkPassword(password);
|
||||
}
|
||||
|
||||
return result;
|
||||
} // AuthSource::LDAP
|
||||
}
|
||||
}
|
||||
|
||||
if (it != _authInfo.end()) {
|
||||
|
@ -734,16 +787,34 @@ AuthResult AuthInfo::checkPassword(std::string const& username,
|
|||
return result;
|
||||
}
|
||||
|
||||
// public
|
||||
#include <iostream>
|
||||
|
||||
AuthLevel AuthInfo::canUseDatabase(std::string const& username,
|
||||
std::string const& dbname) {
|
||||
loadFromDB();
|
||||
READ_LOCKER(guard, _authInfoLock);
|
||||
auto it = _authInfo.find(username);
|
||||
if (it != _authInfo.end()) {
|
||||
return it->second.databaseAuthLevel(dbname);
|
||||
|
||||
if (it == _authInfo.end()) {
|
||||
return AuthLevel::NONE;
|
||||
}
|
||||
return AuthLevel::NONE;
|
||||
|
||||
auto const& entry = it->second;
|
||||
AuthLevel level = entry.databaseAuthLevel(dbname);
|
||||
|
||||
for (auto const& role : entry.roles()) {
|
||||
if (level == AuthLevel::RW) {
|
||||
return level;
|
||||
}
|
||||
|
||||
AuthLevel roleLevel = canUseDatabase(role, dbname);
|
||||
|
||||
if (level == AuthLevel::NONE) {
|
||||
level = roleLevel;
|
||||
}
|
||||
}
|
||||
|
||||
return level;
|
||||
}
|
||||
|
||||
AuthLevel AuthInfo::canUseCollection(std::string const& username,
|
||||
|
@ -752,10 +823,27 @@ AuthLevel AuthInfo::canUseCollection(std::string const& username,
|
|||
loadFromDB();
|
||||
READ_LOCKER(guard, _authInfoLock);
|
||||
auto it = _authInfo.find(username);
|
||||
if (it != _authInfo.end()) {
|
||||
return it->second.collectionAuthLevel(dbname, coll);
|
||||
|
||||
if (it == _authInfo.end()) {
|
||||
return AuthLevel::NONE;
|
||||
}
|
||||
return AuthLevel::NONE;
|
||||
|
||||
auto const& entry = it->second;
|
||||
AuthLevel level = entry.collectionAuthLevel(dbname, coll);
|
||||
|
||||
for (auto const& role : entry.roles()) {
|
||||
if (level == AuthLevel::RW) {
|
||||
return level;
|
||||
}
|
||||
|
||||
AuthLevel roleLevel = canUseCollection(role, dbname, coll);
|
||||
|
||||
if (level == AuthLevel::NONE) {
|
||||
level = roleLevel;
|
||||
}
|
||||
}
|
||||
|
||||
return level;
|
||||
}
|
||||
|
||||
// public called from HttpCommTask.cpp and VstCommTask.cpp
|
||||
|
@ -778,6 +866,7 @@ AuthResult AuthInfo::checkAuthentication(AuthenticationMethod authType,
|
|||
// private
|
||||
AuthResult AuthInfo::checkAuthenticationBasic(std::string const& secret) {
|
||||
auto role = ServerState::instance()->getRole();
|
||||
|
||||
if (role != ServerState::ROLE_SINGLE &&
|
||||
role != ServerState::ROLE_COORDINATOR) {
|
||||
return AuthResult();
|
||||
|
@ -786,13 +875,15 @@ AuthResult AuthInfo::checkAuthenticationBasic(std::string const& secret) {
|
|||
{
|
||||
READ_LOCKER(guard, _authInfoLock);
|
||||
auto const& it = _authBasicCache.find(secret);
|
||||
if (it != _authBasicCache.end()) {
|
||||
|
||||
if (it != _authBasicCache.end() && !it->second.expired()) {
|
||||
return it->second;
|
||||
}
|
||||
}
|
||||
|
||||
std::string const up = StringUtils::decodeBase64(secret);
|
||||
std::string::size_type n = up.find(':', 0);
|
||||
|
||||
if (n == std::string::npos || n == 0 || n + 1 > up.size()) {
|
||||
LOG_TOPIC(TRACE, arangodb::Logger::AUTHENTICATION)
|
||||
<< "invalid authentication data found, cannot extract "
|
||||
|
@ -804,6 +895,13 @@ AuthResult AuthInfo::checkAuthenticationBasic(std::string const& secret) {
|
|||
std::string password = up.substr(n + 1);
|
||||
|
||||
AuthResult result = checkPassword(username, password);
|
||||
|
||||
double timeout = AuthenticationFeature::INSTANCE->authenticationTimeout();
|
||||
|
||||
if (0 < timeout) {
|
||||
result.setExpiry(TRI_microtime() + timeout);
|
||||
}
|
||||
|
||||
{
|
||||
WRITE_LOCKER(guard, _authInfoLock);
|
||||
|
||||
|
|
|
@ -47,11 +47,15 @@ class AuthResult {
|
|||
AuthResult() : _authorized(false) {}
|
||||
|
||||
explicit AuthResult(std::string const& username)
|
||||
: _username(username), _authorized(false) {}
|
||||
: _username(username), _authorized(false), _expiry(0) {}
|
||||
|
||||
void setExpiry(double expiry) { _expiry = expiry; }
|
||||
bool expired() { return _expiry != 0 && _expiry < TRI_microtime(); }
|
||||
|
||||
public:
|
||||
std::string _username;
|
||||
/// User exists and password was checked
|
||||
bool _authorized;
|
||||
bool _authorized; // User exists and password was checked
|
||||
double _expiry;
|
||||
};
|
||||
|
||||
class AuthJwtResult : public AuthResult {
|
||||
|
@ -79,7 +83,7 @@ class AuthInfo {
|
|||
|
||||
/// Trigger eventual reload, user facing API call
|
||||
void reloadAllUsers();
|
||||
|
||||
|
||||
/// Create the root user with a default password, will fail if the user
|
||||
/// already exists. Only ever call if you can guarantee to be in charge
|
||||
void createRootUser();
|
||||
|
@ -92,7 +96,7 @@ class AuthInfo {
|
|||
Result updateUser(std::string const& username,
|
||||
std::function<void(AuthUserEntry&)> const&);
|
||||
Result accessUser(std::string const& username,
|
||||
std::function<void(AuthUserEntry const&)> const&);
|
||||
std::function<void(AuthUserEntry const&)> const&);
|
||||
velocypack::Builder serializeUser(std::string const& user);
|
||||
Result removeUser(std::string const& user);
|
||||
Result removeAllUsers();
|
||||
|
|
|
@ -168,6 +168,73 @@ AuthUserEntry AuthUserEntry::newUser(std::string const& user,
|
|||
return entry;
|
||||
}
|
||||
|
||||
void AuthUserEntry::fromDocumentRoles(AuthUserEntry& entry,
|
||||
VPackSlice const& rolesSlice) {
|
||||
for (auto const& it : VPackArrayIterator(rolesSlice)) {
|
||||
if (it.isString()) {
|
||||
std::string const role = it.copyString();
|
||||
|
||||
entry._roles.insert(role);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AuthUserEntry::fromDocumentDatabases(AuthUserEntry& entry,
|
||||
VPackSlice const& databasesSlice,
|
||||
VPackSlice const& userSlice) {
|
||||
for (auto const& obj : VPackObjectIterator(databasesSlice)) {
|
||||
std::string const dbName = obj.key.copyString();
|
||||
|
||||
if (obj.value.isObject()) {
|
||||
AuthLevel databaseAuth = AuthLevel::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(DEBUG, Logger::AUTHORIZATION) << 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 permissionsSlice = collection.value.get("permissions");
|
||||
|
||||
if (permissionsSlice.isObject()) {
|
||||
try {
|
||||
entry.grantCollection(dbName, cName,
|
||||
AuthLevelFromSlice(permissionsSlice));
|
||||
} catch (arangodb::basics::Exception const& e) {
|
||||
LOG_TOPIC(DEBUG, Logger::AUTHORIZATION) << e.message();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
LOG_TOPIC(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, AuthLevel::RW);
|
||||
entry.grantCollection(dbName, "*", AuthLevel::RW);
|
||||
} else if (TRI_CaseEqualString(value, "ro", 2)) {
|
||||
entry.grantDatabase(dbName, AuthLevel::RO);
|
||||
entry.grantCollection(dbName, "*", AuthLevel::RO);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AuthUserEntry AuthUserEntry::fromDocument(VPackSlice const& slice) {
|
||||
if (slice.isNone() || !slice.isObject()) {
|
||||
THROW_ARANGO_EXCEPTION(TRI_ERROR_BAD_PARAMETER);
|
||||
|
@ -175,6 +242,7 @@ AuthUserEntry AuthUserEntry::fromDocument(VPackSlice const& slice) {
|
|||
|
||||
VPackSlice const keySlice =
|
||||
transaction::helpers::extractKeyFromDocument(slice);
|
||||
|
||||
if (!keySlice.isString()) {
|
||||
THROW_ARANGO_EXCEPTION_MESSAGE(TRI_ERROR_BAD_PARAMETER,
|
||||
"cannot extract key");
|
||||
|
@ -182,6 +250,7 @@ AuthUserEntry AuthUserEntry::fromDocument(VPackSlice const& slice) {
|
|||
|
||||
// extract "user" attribute
|
||||
VPackSlice const userSlice = slice.get("user");
|
||||
|
||||
if (!userSlice.isString()) {
|
||||
THROW_ARANGO_EXCEPTION_MESSAGE(TRI_ERROR_BAD_PARAMETER,
|
||||
"cannot extract username");
|
||||
|
@ -195,6 +264,7 @@ AuthUserEntry AuthUserEntry::fromDocument(VPackSlice const& slice) {
|
|||
}
|
||||
|
||||
VPackSlice const simpleSlice = authDataSlice.get("simple");
|
||||
|
||||
if (!simpleSlice.isObject()) {
|
||||
LOG_TOPIC(DEBUG, arangodb::Logger::FIXME) << "cannot extract simple";
|
||||
return AuthUserEntry();
|
||||
|
@ -213,6 +283,7 @@ AuthUserEntry AuthUserEntry::fromDocument(VPackSlice const& slice) {
|
|||
|
||||
// extract "active" attribute
|
||||
VPackSlice const activeSlice = authDataSlice.get("active");
|
||||
|
||||
if (!activeSlice.isBoolean()) {
|
||||
LOG_TOPIC(DEBUG, arangodb::Logger::FIXME) << "cannot extract active flag";
|
||||
return AuthUserEntry();
|
||||
|
@ -229,56 +300,17 @@ AuthUserEntry AuthUserEntry::fromDocument(VPackSlice const& slice) {
|
|||
|
||||
// extract "databases" attribute
|
||||
VPackSlice const databasesSlice = slice.get("databases");
|
||||
|
||||
if (databasesSlice.isObject()) {
|
||||
for (auto const& obj : VPackObjectIterator(databasesSlice)) {
|
||||
std::string const dbName = obj.key.copyString();
|
||||
fromDocumentDatabases(entry, databasesSlice, userSlice);
|
||||
}
|
||||
|
||||
if (obj.value.isObject()) {
|
||||
AuthLevel databaseAuth = AuthLevel::NONE;
|
||||
// extract "roles" attribute
|
||||
VPackSlice const rolesSlice = slice.get("roles");
|
||||
|
||||
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(DEBUG, Logger::AUTHORIZATION) << 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 permissionsSlice = collection.value.get("permissions");
|
||||
if (permissionsSlice.isObject()) {
|
||||
try {
|
||||
entry.grantCollection(dbName, cName,
|
||||
AuthLevelFromSlice(permissionsSlice));
|
||||
} catch(arangodb::basics::Exception const& e) {
|
||||
LOG_TOPIC(DEBUG, Logger::AUTHORIZATION) << e.message();
|
||||
}
|
||||
} // if
|
||||
} // for
|
||||
} // if
|
||||
|
||||
} else {
|
||||
LOG_TOPIC(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, AuthLevel::RW);
|
||||
entry.grantCollection(dbName, "*", AuthLevel::RW);
|
||||
} else if (TRI_CaseEqualString(value, "ro", 2)) {
|
||||
entry.grantDatabase(dbName, AuthLevel::RO);
|
||||
entry.grantCollection(dbName, "*", AuthLevel::RO);
|
||||
}
|
||||
}
|
||||
} // for
|
||||
} // if
|
||||
if (rolesSlice.isArray()) {
|
||||
fromDocumentRoles(entry, rolesSlice);
|
||||
}
|
||||
|
||||
// ensure the root user always has the right to change permissions
|
||||
if (entry._username == "root") {
|
||||
|
@ -317,13 +349,16 @@ VPackBuilder AuthUserEntry::toVPackBuilder() const {
|
|||
|
||||
VPackBuilder builder;
|
||||
VPackObjectBuilder o(&builder, true);
|
||||
|
||||
if (!_key.empty()) {
|
||||
builder.add(StaticStrings::KeyString, VPackValue(_key));
|
||||
}
|
||||
|
||||
builder.add("user", VPackValue(_username));
|
||||
AddSource(builder, _source);
|
||||
|
||||
{ // authData sub-object
|
||||
// authData sub-object
|
||||
{
|
||||
VPackObjectBuilder o2(&builder, "authData", true);
|
||||
builder.add("active", VPackValue(_active));
|
||||
if (_source == AuthSource::COLLECTION) {
|
||||
|
@ -333,17 +368,23 @@ VPackBuilder AuthUserEntry::toVPackBuilder() const {
|
|||
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
|
||||
|
||||
// permissions
|
||||
{
|
||||
VPackObjectBuilder o4(&builder, "permissions", true);
|
||||
AuthLevel lvl = dbCtxPair.second._databaseAuthLevel;
|
||||
AddAuthLevel(builder, lvl);
|
||||
}
|
||||
{ // collections
|
||||
|
||||
// collections
|
||||
{
|
||||
VPackObjectBuilder o4(&builder, "collections", true);
|
||||
|
||||
for (auto const& colAccessPair : dbCtxPair.second._collectionAccess) {
|
||||
VPackObjectBuilder o4(&builder, colAccessPair.first, true);
|
||||
VPackObjectBuilder o5(&builder, "permissions", true);
|
||||
|
@ -361,6 +402,7 @@ void AuthUserEntry::grantDatabase(std::string const& dbname, AuthLevel level) {
|
|||
THROW_ARANGO_EXCEPTION_MESSAGE(TRI_ERROR_BAD_PARAMETER,
|
||||
"Cannot set rights for empty db name");
|
||||
}
|
||||
|
||||
if (_username == "root" && dbname == StaticStrings::SystemDatabase &&
|
||||
level != AuthLevel::RW) {
|
||||
THROW_ARANGO_EXCEPTION_MESSAGE(
|
||||
|
@ -368,12 +410,14 @@ void AuthUserEntry::grantDatabase(std::string const& dbname, AuthLevel level) {
|
|||
}
|
||||
|
||||
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 behaviour will need to be adjusted
|
||||
// grantDatabase is not supposed to change any rights on the
|
||||
// collection level code which relies on the old behaviour
|
||||
// will need to be adjusted
|
||||
|
||||
_dbAccess.emplace(
|
||||
dbname,
|
||||
DBAuthContext(level, std::unordered_map<std::string, AuthLevel>()));
|
||||
|
@ -396,13 +440,14 @@ void AuthUserEntry::grantCollection(std::string const& dbname,
|
|||
std::string const& coll,
|
||||
AuthLevel const level) {
|
||||
if (dbname.empty() || coll.empty()) {
|
||||
THROW_ARANGO_EXCEPTION_MESSAGE(TRI_ERROR_BAD_PARAMETER,
|
||||
THROW_ARANGO_EXCEPTION_MESSAGE(
|
||||
TRI_ERROR_BAD_PARAMETER,
|
||||
"Cannot set rights for empty db / collection name");
|
||||
} else if (coll[0] == '_') {
|
||||
THROW_ARANGO_EXCEPTION_MESSAGE(TRI_ERROR_BAD_PARAMETER,
|
||||
"Cannot set rights for system collections");
|
||||
"Cannot set rights for system collections");
|
||||
} else if (_username == "root" && dbname == StaticStrings::SystemDatabase &&
|
||||
coll == "*" && level != AuthLevel::RW) {
|
||||
coll == "*" && level != AuthLevel::RW) {
|
||||
THROW_ARANGO_EXCEPTION_MESSAGE(TRI_ERROR_FORBIDDEN,
|
||||
"Cannot lower access level of 'root' to "
|
||||
" a system collection");
|
||||
|
@ -418,9 +463,10 @@ void AuthUserEntry::grantCollection(std::string const& dbname,
|
|||
if (it != _dbAccess.end()) {
|
||||
dbLevel = it->second._databaseAuthLevel;
|
||||
}
|
||||
_dbAccess.emplace(dbname, DBAuthContext(
|
||||
dbLevel, std::unordered_map<std::string, AuthLevel>(
|
||||
{{coll, level}})));
|
||||
_dbAccess.emplace(
|
||||
dbname,
|
||||
DBAuthContext(dbLevel, std::unordered_map<std::string, AuthLevel>(
|
||||
{{coll, level}})));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -517,7 +563,7 @@ bool AuthUserEntry::hasSpecificCollection(
|
|||
AuthLevel AuthUserEntry::DBAuthContext::collectionAuthLevel(
|
||||
std::string const& collectionName, bool& notFound) const {
|
||||
std::unordered_map<std::string, AuthLevel>::const_iterator pair =
|
||||
_collectionAccess.find(collectionName);
|
||||
_collectionAccess.find(collectionName);
|
||||
if (pair != _collectionAccess.end()) {
|
||||
return pair->second;
|
||||
}
|
||||
|
|
|
@ -32,11 +32,11 @@
|
|||
|
||||
namespace arangodb {
|
||||
|
||||
/// This class represents a 'user' entry. It contains structures
|
||||
/// to store the access levels for databases and collections.
|
||||
/// The user object must be serialized via `toVPackBuilder()` and
|
||||
/// written to the _users collection after modifying it.
|
||||
///
|
||||
// This class represents a 'user' entry. It contains structures to
|
||||
// store the access levels for databases and collections. The user
|
||||
// object must be serialized via `toVPackBuilder()` and written to the
|
||||
// _users collection after modifying it.
|
||||
|
||||
class AuthUserEntry {
|
||||
friend class AuthInfo;
|
||||
|
||||
|
@ -45,6 +45,12 @@ class AuthUserEntry {
|
|||
AuthSource source);
|
||||
static AuthUserEntry fromDocument(velocypack::Slice const&);
|
||||
|
||||
private:
|
||||
static void fromDocumentRoles(AuthUserEntry&, velocypack::Slice const&);
|
||||
static void fromDocumentDatabases(AuthUserEntry&,
|
||||
velocypack::Slice const& databases,
|
||||
velocypack::Slice const& user);
|
||||
|
||||
public:
|
||||
std::string const& key() const { return _key; }
|
||||
std::string const& username() const { return _username; }
|
||||
|
@ -61,25 +67,32 @@ class AuthUserEntry {
|
|||
|
||||
void setActive(bool active) { _active = active; }
|
||||
|
||||
/// grant specific access rights for db. The default "*"
|
||||
/// is also a valid database name
|
||||
std::unordered_set<std::string> roles () const { return _roles; }
|
||||
void setRoles(std::unordered_set<std::string> roles) { _roles = roles; }
|
||||
|
||||
// grant specific access rights for db. The default "*" is also a
|
||||
// valid database name
|
||||
void grantDatabase(std::string const& dbname, AuthLevel level);
|
||||
/// Removes the entry.
|
||||
|
||||
// Removes the entry.
|
||||
void removeDatabase(std::string const& dbname);
|
||||
/// Grant collection rights, "*" is a valid parameter for dbname and
|
||||
/// collection.
|
||||
/// The combination of "*"/"*" is automatically used for the root user
|
||||
|
||||
// Grant collection rights, "*" is a valid parameter for dbname and
|
||||
// collection. The combination of "*"/"*" is automatically used for
|
||||
// the root
|
||||
void grantCollection(std::string const& dbname, std::string const& collection,
|
||||
AuthLevel level);
|
||||
|
||||
void removeCollection(std::string const& dbname,
|
||||
std::string const& collection);
|
||||
|
||||
/// Resolve the access level for this database. Might fall back to
|
||||
/// the special '*' entry if the specific database is not found
|
||||
// Resolve the access level for this database. Might fall back to
|
||||
// the special '*' entry if the specific database is not found
|
||||
AuthLevel databaseAuthLevel(std::string const& dbname) const;
|
||||
/// Resolve rights for the specified collection. Falls back to the
|
||||
/// special '*' entry if either the database or collection is not
|
||||
/// found.
|
||||
|
||||
// Resolve rights for the specified collection. Falls back to the
|
||||
// special '*' entry if either the database or collection is not
|
||||
// found.
|
||||
AuthLevel collectionAuthLevel(std::string const& dbname,
|
||||
std::string const& collectionName) const;
|
||||
|
||||
|
@ -94,12 +107,13 @@ class AuthUserEntry {
|
|||
DBAuthContext(AuthLevel dbLvl,
|
||||
std::unordered_map<std::string, AuthLevel> const& coll)
|
||||
: _databaseAuthLevel(dbLvl), _collectionAccess(coll) {}
|
||||
|
||||
|
||||
DBAuthContext(AuthLevel dbLvl,
|
||||
std::unordered_map<std::string, AuthLevel>&& coll)
|
||||
: _databaseAuthLevel(dbLvl), _collectionAccess(std::move(coll)) {}
|
||||
|
||||
AuthLevel collectionAuthLevel(std::string const& collectionName, bool& notFound) const;
|
||||
AuthLevel collectionAuthLevel(std::string const& collectionName,
|
||||
bool& notFound) const;
|
||||
|
||||
public:
|
||||
AuthLevel _databaseAuthLevel;
|
||||
|
@ -116,6 +130,7 @@ class AuthUserEntry {
|
|||
std::string _passwordSalt;
|
||||
std::string _passwordHash;
|
||||
std::unordered_map<std::string, DBAuthContext> _dbAccess;
|
||||
std::unordered_set<std::string> _roles;
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -28,6 +28,7 @@
|
|||
|
||||
#ifdef USE_ENTERPRISE
|
||||
#include "Enterprise/Audit/AuditFeature.h"
|
||||
#include "Enterprise/Ldap/LdapFeature.h"
|
||||
#endif
|
||||
|
||||
using namespace arangodb;
|
||||
|
@ -73,6 +74,8 @@ LogTopic Logger::V8("v8", LogLevel::WARN);
|
|||
LogTopic Logger::VIEWS("views", LogLevel::FATAL);
|
||||
|
||||
#ifdef USE_ENTERPRISE
|
||||
LogTopic LdapFeature::LDAP("ldap", LogLevel::INFO);
|
||||
|
||||
LogTopic AuditFeature::AUDIT_AUTHENTICATION("audit-authentication", LogLevel::INFO);
|
||||
LogTopic AuditFeature::AUDIT_DATABASE("audit-database", LogLevel::INFO);
|
||||
LogTopic AuditFeature::AUDIT_COLLECTION("audit-collection", LogLevel::INFO);
|
||||
|
|
Loading…
Reference in New Issue