1
0
Fork 0

Feature/allow access to configured 🔐 in readonly mode (#3698)

* Special method to access configured access levels as opposed to only getting the effective access level
* Add integration test
* Allow grants on collection level
This commit is contained in:
m0ppers 2017-11-17 16:11:22 +01:00 committed by Max Neunhöffer
parent 7b80deb5cc
commit 75e1bf31cd
7 changed files with 405 additions and 69 deletions

View File

@ -130,22 +130,41 @@ RestStatus RestUsersHandler::getRequest(AuthInfo* authInfo) {
} else if (suffixes.size() == 3) {
//_api/user/<user>/database/<dbname>
// return specific database
AuthLevel lvl = authInfo->canUseDatabase(user, suffixes[2]);
bool configured = false;
std::string const& param = _request->value("configured", configured);
if (configured) {
configured = StringUtils::boolean(param);
}
AuthLevel lvl;
if (configured) {
lvl = authInfo->configuredDatabaseAuthLevel(user, suffixes[2]);
} else {
// return effective user rights
lvl = authInfo->canUseDatabase(user, suffixes[2]);
}
VPackBuilder data;
data.add(VPackValue(convertFromAuthLevel(lvl)));
generateOk(ResponseCode::OK, data.slice());
} else if (suffixes.size() == 4) {
bool configured = false;
std::string const& param = _request->value("configured", configured);
if (configured) {
configured = StringUtils::boolean(param);
}
//_api/user/<user>/database/<dbname>/<collection>
AuthLevel lvl =
authInfo->canUseCollection(user, suffixes[2], suffixes[3]);
AuthLevel lvl;
if (configured) {
lvl = authInfo->configuredCollectionAuthLevel(user, suffixes[2], suffixes[3]);
} else {
// return effective user rights
lvl = authInfo->canUseCollection(user, suffixes[2], suffixes[3]);
}
VPackBuilder data;
data.add(VPackValue(convertFromAuthLevel(lvl)));
generateOk(ResponseCode::OK, data.slice());
} else {
generateError(ResponseCode::BAD, TRI_ERROR_BAD_PARAMETER);
}
} else if (suffixes[1] == "config") {
//_api/user/<user>//config
VPackBuilder data = authInfo->getConfigData(user);

View File

@ -58,7 +58,7 @@ class DatabaseManagerThread : public Thread {
}
};
class DatabaseFeature final : public application_features::ApplicationFeature {
class DatabaseFeature : public application_features::ApplicationFeature {
friend class DatabaseManagerThread;
public:

View File

@ -858,30 +858,11 @@ AuthResult AuthInfo::checkPassword(std::string const& username,
return result;
}
AuthLevel AuthInfo::canUseDatabase(std::string const& username,
std::string const& dbname) {
loadFromDB();
AuthLevel level;
{
READ_LOCKER(guard, _authInfoLock);
level = canUseDatabaseInternal(username, dbname, 0);
}
static_assert(AuthLevel::RO < AuthLevel::RW, "ro < rw");
if (level > AuthLevel::RO && !ServerState::writeOpsEnabled()) {
// no write operations allowed on this server at all
LOG_TOPIC(TRACE, Logger::FIXME) << "downgrading user rights";
return AuthLevel::RO;
}
// return actual level
return level;
}
// worker function for canUseDatabase
// worker function for configuredDatabaseAuthLevel
// must only be called with the read-lock on _authInfoLock being held
AuthLevel AuthInfo::canUseDatabaseInternal(std::string const& username,
std::string const& dbname,
size_t depth) const {
AuthLevel AuthInfo::configuredDatabaseAuthLevelInternal(std::string const& username,
std::string const& dbname,
size_t depth) const {
auto it = _authInfo.find(username);
if (it == _authInfo.end()) {
@ -902,7 +883,7 @@ AuthLevel AuthInfo::canUseDatabaseInternal(std::string const& username,
// recurse into function, but only one level deep.
// this allows us to avoid endless recursion without major overhead
if (depth == 0) {
AuthLevel roleLevel = canUseDatabaseInternal(role, dbname, depth + 1);
AuthLevel roleLevel = configuredDatabaseAuthLevelInternal(role, dbname, depth + 1);
if (level == AuthLevel::NONE) {
// use the permission of the role we just found
@ -914,43 +895,35 @@ AuthLevel AuthInfo::canUseDatabaseInternal(std::string const& username,
return level;
}
AuthLevel AuthInfo::canUseCollection(std::string const& username,
std::string const& dbname,
std::string const& coll) {
if (coll.empty()) {
// no collection name given
return AuthLevel::NONE;
}
if (coll[0] >= '0' && coll[0] <= '9') {
// lookup by collection id
// translate numeric collection id into collection name
return canUseCollectionInternal(
username, dbname,
DatabaseFeature::DATABASE->translateCollectionName(dbname, coll));
}
// lookup by collection name
return canUseCollectionInternal(username, dbname, coll);
AuthLevel AuthInfo::configuredDatabaseAuthLevel(std::string const& username,
std::string const& dbname) {
loadFromDB();
READ_LOCKER(guard, _authInfoLock);
return configuredDatabaseAuthLevelInternal(username, dbname, 0);
}
// internal method called by canUseCollection
AuthLevel AuthInfo::canUseDatabase(std::string const& username,
std::string const& dbname) {
AuthLevel level = configuredDatabaseAuthLevel(username, dbname);
static_assert(AuthLevel::RO < AuthLevel::RW, "ro < rw");
if (level > AuthLevel::RO && !ServerState::writeOpsEnabled()) {
return AuthLevel::RO;
}
return level;
}
// internal method called by configuredCollectionAuthLevel
// asserts that collection name is non-empty and already translated
// from collection id to name
AuthLevel AuthInfo::canUseCollectionInternal(std::string const& username,
std::string const& dbname,
std::string const& coll) {
if (coll.empty()) {
// no collection name given
return AuthLevel::NONE;
}
AuthLevel AuthInfo::configuredCollectionAuthLevelInternal(std::string const& username,
std::string const& dbname,
std::string const& coll,
size_t depth) const {
// we must have got a non-empty collection name when we get here
TRI_ASSERT(coll[0] < '0' || coll[0] > '9');
loadFromDB();
READ_LOCKER(guard, _authInfoLock);
auto it = _authInfo.find(username);
if (it == _authInfo.end()) {
return AuthLevel::NONE;
}
@ -961,25 +934,60 @@ AuthLevel AuthInfo::canUseCollectionInternal(std::string const& username,
#ifdef USE_ENTERPRISE
for (auto const& role : entry.roles()) {
if (level == AuthLevel::RW) {
// we already have highest permission
return level;
}
AuthLevel roleLevel = canUseCollection(role, dbname, coll);
// recurse into function, but only one level deep.
// this allows us to avoid endless recursion without major overhead
if (depth == 0) {
AuthLevel roleLevel = configuredCollectionAuthLevelInternal(role, dbname, coll, depth + 1);
if (level == AuthLevel::NONE) {
level = roleLevel;
if (level == AuthLevel::NONE) {
// use the permission of the role we just found
level = roleLevel;
}
}
}
#endif
return level;
}
AuthLevel AuthInfo::configuredCollectionAuthLevel(std::string const& username,
std::string const& dbname,
std::string coll) {
if (coll.empty()) {
// no collection name given
return AuthLevel::NONE;
}
if (coll[0] >= '0' && coll[0] <= '9') {
coll = DatabaseFeature::DATABASE->translateCollectionName(dbname, coll);
}
loadFromDB();
READ_LOCKER(guard, _authInfoLock);
return configuredCollectionAuthLevelInternal(username, dbname, coll, 0);
}
AuthLevel AuthInfo::canUseCollection(std::string const& username,
std::string const& dbname,
std::string const& coll) {
if (coll.empty()) {
// no collection name given
return AuthLevel::NONE;
}
AuthLevel level = configuredCollectionAuthLevel(username, dbname, coll);
static_assert(AuthLevel::RO < AuthLevel::RW, "ro < rw");
if (level > AuthLevel::RO && !ServerState::writeOpsEnabled()) {
LOG_TOPIC(ERR, Logger::FIXME) << "downgrading user rights";
return AuthLevel::RO;
}
return level;
}
// public called from HttpCommTask.cpp and VstCommTask.cpp
// should only lock if required, otherwise we will serialize all
// requests whether we need to or not
@ -1287,3 +1295,9 @@ std::string AuthInfo::generateJwt(VPackBuilder const& payload) {
}
return generateRawJwt(bodyBuilder);
}
void AuthInfo::setAuthInfo(AuthUserEntryMap const& newMap) {
WRITE_LOCKER(writeLocker, _authInfoLock);
_authInfo = newMap;
_outdated = false;
}

View File

@ -68,7 +68,10 @@ class AuthJwtResult : public AuthResult {
class AuthenticationHandler;
typedef std::unordered_map<std::string, AuthUserEntry> AuthUserEntryMap;
class AuthInfo {
public:
explicit AuthInfo(std::unique_ptr<AuthenticationHandler>&&);
~AuthInfo();
@ -112,7 +115,11 @@ class AuthInfo {
AuthResult checkAuthentication(arangodb::rest::AuthenticationMethod authType,
std::string const& secret);
AuthLevel configuredDatabaseAuthLevel(std::string const& username,
std::string const& dbname);
AuthLevel configuredCollectionAuthLevel(std::string const& username,
std::string const& dbname,
std::string coll);
AuthLevel canUseDatabase(std::string const& username,
std::string const& dbname);
AuthLevel canUseCollection(std::string const& username,
@ -124,18 +131,21 @@ class AuthInfo {
std::string generateJwt(VPackBuilder const&);
std::string generateRawJwt(VPackBuilder const&);
void setAuthInfo(AuthUserEntryMap const& userEntryMap);
private:
// worker function for canUseDatabase
// must only be called with the read-lock on _authInfoLock being held
AuthLevel canUseDatabaseInternal(std::string const& username,
AuthLevel configuredDatabaseAuthLevelInternal(std::string const& username,
std::string const& dbname, size_t depth) const;
// internal method called by canUseCollection
// asserts that collection name is non-empty and already translated
// from collection id to name
AuthLevel canUseCollectionInternal(std::string const& username,
std::string const& dbname,
std::string const& coll);
AuthLevel configuredCollectionAuthLevelInternal(std::string const& username,
std::string const& dbname,
std::string const& coll,
size_t depth) const;
void loadFromDB();
bool parseUsers(velocypack::Slice const& slice);
Result storeUserInternal(AuthUserEntry const& user, bool replace);
@ -154,7 +164,7 @@ class AuthInfo {
Mutex _loadFromDBLock;
std::atomic<bool> _outdated;
std::unordered_map<std::string, AuthUserEntry> _authInfo;
AuthUserEntryMap _authInfo;
std::unordered_map<std::string, arangodb::AuthResult> _authBasicCache;
arangodb::basics::LruCache<std::string, arangodb::AuthJwtResult>
_authJwtCache;

View File

@ -0,0 +1,129 @@
/* jshint globalstrict:false, strict:false, maxlen: 5000 */
/* global describe, after, afterEach, before, it */
'use strict';
// //////////////////////////////////////////////////////////////////////////////
// / DISCLAIMER
// /
// / Copyright 2017 ArangoDB 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 Andreas Streichardt
// //////////////////////////////////////////////////////////////////////////////
const expect = require('chai').expect;
const request = require('@arangodb/request');
const users = require('@arangodb/users');
const db = require('@arangodb').db;
describe('Grants', function() {
before(function() {
db._create('grants');
});
afterEach(function() {
users.remove('hans');
let resp = request.put({
url: '/_admin/server/mode',
body: {'mode': 'default'},
json: true,
});
expect(resp.statusCode).to.equal(200);
});
after(function() {
db._drop('grants');
// wait for readonly mode to reset
require('internal').wait(5.0);
});
it('should show the effective rights for a user', function() {
users.save('hans');
users.grantDatabase('hans', '_system', 'rw');
let resp = request.get({
url: '/_api/user/hans/database/_system',
json: true,
});
expect(JSON.parse(resp.body)).to.have.property('result', 'rw');
});
it('should show the effective rights when readonly mode is on', function() {
users.save('hans');
users.grantDatabase('hans', '_system', 'rw');
let resp;
resp = request.put({
url: '/_admin/server/mode',
body: {'mode': 'readonly'},
json: true,
});
resp = request.get({
url: '/_api/user/hans/database/_system',
json: true,
});
expect(JSON.parse(resp.body)).to.have.property('result', 'ro');
});
it('should show the configured rights when readonly mode is on and configured is requested', function() {
users.save('hans');
users.grantDatabase('hans', '_system', 'rw');
let resp;
resp = request.put({
url: '/_admin/server/mode',
body: {'mode': 'readonly'},
json: true,
});
resp = request.get({
url: '/_api/user/hans/database/_system?configured=true',
json: true,
});
expect(JSON.parse(resp.body)).to.have.property('result', 'rw');
});
it('should show the effective rights when readonly mode is on', function() {
users.save('hans');
users.grantDatabase('hans', '_system', 'rw');
users.grantCollection('hans', '_system', 'grants');
let resp;
resp = request.put({
url: '/_admin/server/mode',
body: {'mode': 'readonly'},
json: true,
});
resp = request.get({
url: '/_api/user/hans/database/_system/grants',
json: true,
});
expect(JSON.parse(resp.body)).to.have.property('result', 'ro');
});
it('should show the configured rights when readonly mode is on and configured is requested', function() {
users.save('hans');
users.grantDatabase('hans', '_system', 'rw');
users.grantCollection('hans', '_system', 'grants');
let resp;
resp = request.put({
url: '/_admin/server/mode',
body: {'mode': 'readonly'},
json: true,
});
resp = request.get({
url: '/_api/user/hans/database/_system/grants?configured=true',
json: true,
});
expect(JSON.parse(resp.body)).to.have.property('result', 'rw');
});
});

View File

@ -61,6 +61,7 @@ add_executable(
RocksDBEngine/IndexEstimatorTest.cpp
RocksDBEngine/TypeConversionTest.cpp
SimpleHttpClient/CommunicatorTest.cpp
VocBase/AuthInfoTest.cpp
main.cpp
)

View File

@ -0,0 +1,163 @@
////////////////////////////////////////////////////////////////////////////////
/// @brief test suite for CuckooFilter based index selectivity estimator
///
/// @file
///
/// DISCLAIMER
///
/// Copyright 2017 ArangoDB 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 Andreas Streichardt
/// @author Copyright 2017, ArangoDB GmbH, Cologne, Germany
////////////////////////////////////////////////////////////////////////////////
#include "catch.hpp"
#include "fakeit.hpp"
#include "Aql/QueryRegistry.h"
#include "Cluster/ServerState.h"
#include "RestServer/DatabaseFeature.h"
#include "VocBase/AuthInfo.h"
#include "VocBase/AuthUserEntry.h"
using namespace fakeit;
using namespace arangodb;
using namespace arangodb::aql;
namespace arangodb {
namespace tests {
namespace auth_info_test {
class TestAuthenticationHandler: public AuthenticationHandler {
public:
TestAuthenticationHandler() {}
AuthenticationResult authenticate(std::string const& username,
std::string const& password) {
std::unordered_map<std::string, AuthLevel> permissions {};
std::unordered_set<std::string> roles {};
AuthSource source = AuthSource::COLLECTION;
AuthenticationResult result(permissions, roles, source);
return result;
}
virtual ~TestAuthenticationHandler() {}
};
class TestQueryRegistry: public QueryRegistry {
public:
TestQueryRegistry() {};
virtual ~TestQueryRegistry() {}
};
class TestDatabaseFeature: public DatabaseFeature {
public:
TestDatabaseFeature(application_features::ApplicationServer* server): DatabaseFeature(server) {};
};
TEST_CASE("🥑🔐 AuthInfo", "[authentication]") {
auto authHandler = std::make_unique<TestAuthenticationHandler>();
TestQueryRegistry queryRegistry;
auto state = ServerState::instance();
state->setRole(ServerState::ROLE_SINGLE);
Mock<DatabaseFeature> databaseFeatureMock;
DatabaseFeature &databaseFeature = databaseFeatureMock.get();
DatabaseFeature::DATABASE = &databaseFeature;
AuthInfo authInfo(std::move(authHandler));
authInfo.setQueryRegistry(&queryRegistry);
SECTION("An unknown user will have no access") {
AuthUserEntryMap userEntryMap;
authInfo.setAuthInfo(userEntryMap);
AuthLevel authLevel = authInfo.canUseDatabase("test", "test");
REQUIRE(authLevel == AuthLevel::NONE);
}
SECTION("Granting RW access on database * will grant access to all databases") {
AuthUserEntryMap userEntryMap;
auto testUser = AuthUserEntry::newUser("test", "test", AuthSource::COLLECTION);
testUser.grantDatabase("*", AuthLevel::RW);
userEntryMap.emplace("test", testUser);
authInfo.setAuthInfo(userEntryMap);
AuthLevel authLevel = authInfo.canUseDatabase("test", "test");
REQUIRE(authLevel == AuthLevel::RW);
}
SECTION("Setting ServerState to readonly will make all users effective RO users") {
AuthUserEntryMap userEntryMap;
auto testUser = AuthUserEntry::newUser("test", "test", AuthSource::COLLECTION);
testUser.grantDatabase("*", AuthLevel::RW);
userEntryMap.emplace("test", testUser);
state->setServerMode(ServerState::Mode::READ_ONLY);
authInfo.setAuthInfo(userEntryMap);
AuthLevel authLevel = authInfo.canUseDatabase("test", "test");
REQUIRE(authLevel == AuthLevel::RO);
}
SECTION("In readonly mode the configured access level will still be accessible") {
AuthUserEntryMap userEntryMap;
auto testUser = AuthUserEntry::newUser("test", "test", AuthSource::COLLECTION);
testUser.grantDatabase("*", AuthLevel::RW);
userEntryMap.emplace("test", testUser);
state->setServerMode(ServerState::Mode::READ_ONLY);
authInfo.setAuthInfo(userEntryMap);
AuthLevel authLevel = authInfo.configuredDatabaseAuthLevel("test", "test");
REQUIRE(authLevel == AuthLevel::RW);
}
SECTION("Setting ServerState to readonly will make all users effective RO users (collection level)") {
AuthUserEntryMap userEntryMap;
auto testUser = AuthUserEntry::newUser("test", "test", AuthSource::COLLECTION);
testUser.grantDatabase("*", AuthLevel::RW);
testUser.grantCollection("test", "test", AuthLevel::RW);
userEntryMap.emplace("test", testUser);
state->setServerMode(ServerState::Mode::READ_ONLY);
authInfo.setAuthInfo(userEntryMap);
AuthLevel authLevel = authInfo.canUseCollection("test", "test", "test");
REQUIRE(authLevel == AuthLevel::RO);
}
SECTION("In readonly mode the configured access level will still be accessible (collection level)") {
AuthUserEntryMap userEntryMap;
auto testUser = AuthUserEntry::newUser("test", "test", AuthSource::COLLECTION);
testUser.grantDatabase("*", AuthLevel::RW);
testUser.grantCollection("test", "test", AuthLevel::RW);
userEntryMap.emplace("test", testUser);
state->setServerMode(ServerState::Mode::READ_ONLY);
authInfo.setAuthInfo(userEntryMap);
AuthLevel authLevel = authInfo.configuredCollectionAuthLevel("test", "test", "test");
REQUIRE(authLevel == AuthLevel::RW);
}
state->setServerMode(ServerState::Mode::DEFAULT);
}
}
}
}