//////////////////////////////////////////////////////////////////////////////// /// 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 Simon Grätzer //////////////////////////////////////////////////////////////////////////////// #include "RestUsersHandler.h" #include "Basics/VelocyPackHelper.h" #include "GeneralServer/AuthenticationFeature.h" #include "Rest/HttpRequest.h" #include "Rest/Version.h" #include "RestServer/DatabaseFeature.h" #include "Utils/CollectionNameResolver.h" #include "Utils/ExecContext.h" #include "VocBase/LogicalCollection.h" #include "VocBase/Methods/Collections.h" #include "VocBase/vocbase.h" #include #include #include namespace { //////////////////////////////////////////////////////////////////////////////// /// @return a collection exists in database or a wildcard was specified //////////////////////////////////////////////////////////////////////////////// arangodb::Result existsCollection(std::string const& database, std::string const& collection) { auto* databaseFeature = arangodb::application_features::ApplicationServer::lookupFeature( "Database"); if (!databaseFeature) { return arangodb::Result(TRI_ERROR_INTERNAL, "failure to find feature 'Database'"); } static const std::string wildcard("*"); if (wildcard == database) { return arangodb::Result(); // wildcard always matches } auto* vocbase = databaseFeature->lookupDatabase(database); if (!vocbase) { return arangodb::Result(TRI_ERROR_ARANGO_DATABASE_NOT_FOUND); } if (wildcard == collection) { return arangodb::Result(); // wildcard always matches } return !arangodb::CollectionNameResolver(*vocbase).getCollection(collection) ? arangodb::Result(TRI_ERROR_ARANGO_DATA_SOURCE_NOT_FOUND) : arangodb::Result(); } } // namespace using namespace arangodb; using namespace arangodb::basics; using namespace arangodb::rest; RestUsersHandler::RestUsersHandler(GeneralRequest* request, GeneralResponse* response) : RestBaseHandler(request, response) {} RestStatus RestUsersHandler::execute() { RequestType const type = _request->requestType(); AuthenticationFeature* af = AuthenticationFeature::instance(); if (af == nullptr || af->userManager() == nullptr) { // nullptr may happens during shutdown, or on Agency generateError(ResponseCode::BAD, TRI_ERROR_NOT_IMPLEMENTED); return RestStatus::DONE; } switch (type) { case RequestType::GET: return getRequest(af->userManager()); case RequestType::POST: return postRequest(af->userManager()); case RequestType::PUT: return putRequest(af->userManager()); case RequestType::PATCH: return patchRequest(af->userManager()); case RequestType::DELETE_REQ: return deleteRequest(af->userManager()); default: generateError(ResponseCode::BAD, TRI_ERROR_HTTP_METHOD_NOT_ALLOWED); return RestStatus::DONE; } } bool RestUsersHandler::isAdminUser() const { if (!ExecContext::isAuthEnabled()) { return true; } else if (ExecContext::CURRENT != nullptr) { return ExecContext::CURRENT->isAdminUser(); } return false; } bool RestUsersHandler::canAccessUser(std::string const& user) const { if (_request->authenticated() && user == _request->user()) { return true; } return isAdminUser(); } /// helper to generate a compliant response for individual user requests void RestUsersHandler::generateUserResult(rest::ResponseCode code, VPackBuilder const& doc) { VPackBuilder b; b(VPackValue(VPackValueType::Object))(StaticStrings::Error, VPackValue(false))(StaticStrings::Code, VPackValue((int)code))(); b = VPackCollection::merge(doc.slice(), b.slice(), false); generateResult(code, b.slice()); } RestStatus RestUsersHandler::getRequest(auth::UserManager* um) { std::vector suffixes = _request->decodedSuffixes(); if (suffixes.empty()) { if (isAdminUser()) { VPackBuilder users = um->allUsers(); generateOk(ResponseCode::OK, users.slice()); } else { generateError(ResponseCode::FORBIDDEN, TRI_ERROR_HTTP_FORBIDDEN); } } else if (suffixes.size() == 1) { std::string const& user = suffixes[0]; if (canAccessUser(user)) { VPackBuilder doc = um->serializeUser(user); generateUserResult(ResponseCode::OK, doc); } else { generateError(ResponseCode::FORBIDDEN, TRI_ERROR_HTTP_FORBIDDEN); } } else { std::string const& user = suffixes[0]; if (!canAccessUser(user)) { generateError(ResponseCode::FORBIDDEN, TRI_ERROR_HTTP_FORBIDDEN); return RestStatus::DONE; } if (suffixes[1] == "database") { if (suffixes.size() == 2) { //_api/user//database?full= bool full = false; std::string const& param = _request->value("full", full); if (full) { full = StringUtils::boolean(param); } generateDatabaseResult(um, user, full); } else if (suffixes.size() == 3) { //_api/user//database/ // return specific database bool configured = _request->parsedValue("configured", false); auth::Level lvl = um->databaseAuthLevel(user, suffixes[2], configured); VPackBuilder data; data.add(VPackValue(convertFromAuthLevel(lvl))); generateOk(ResponseCode::OK, data.slice()); } else if (suffixes.size() == 4) { bool configured = _request->parsedValue("configured", false); //_api/user//database// auth::Level lvl = um->collectionAuthLevel(user, suffixes[2], suffixes[3], configured); 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//config (only used by WebUI) Result r; r = um->accessUser(user, [&](auth::User const& u) { VPackSlice resp = u.configData(); if (suffixes.size() == 3 && resp.isObject()) { resp = resp.get(suffixes[2]); } generateOk(ResponseCode::OK, resp.isNone() ? VPackSlice::nullSlice() : resp); return TRI_ERROR_NO_ERROR; }); if (r.fail()) { generateError(r); } } else { generateError(ResponseCode::BAD, TRI_ERROR_BAD_PARAMETER); } } return RestStatus::DONE; } /// generate response for /_api/user/database?full=true/false void RestUsersHandler::generateDatabaseResult(auth::UserManager* um, std::string const& username, bool full) { // return list of databases VPackBuilder data; data.openObject(); Result res = um->accessUser(username, [&](auth::User const& user) { DatabaseFeature::DATABASE->enumerateDatabases([&](TRI_vocbase_t& vocbase) -> void { if (full) { auto lvl = user.configuredDBAuthLevel(vocbase.name()); std::string str = convertFromAuthLevel(lvl); velocypack::ObjectBuilder b(&data, vocbase.name(), true); data.add("permission", VPackValue(str)); velocypack::ObjectBuilder b2(&data, "collections", true); methods::Collections::enumerate(&vocbase, [&](std::shared_ptr const& c) -> void { TRI_ASSERT(c); lvl = user.configuredCollectionAuthLevel(vocbase.name(), c->name()); data.add(c->name(), velocypack::Value(convertFromAuthLevel(lvl))); }); lvl = user.configuredCollectionAuthLevel(vocbase.name(), "*"); data.add("*", velocypack::Value(convertFromAuthLevel(lvl))); } else { // hide db's without access auto lvl = user.databaseAuthLevel(vocbase.name()); if (lvl >= auth::Level::RO) { data.add(vocbase.name(), VPackValue(convertFromAuthLevel(lvl))); } } }); if (full) { auth::Level lvl = user.databaseAuthLevel("*"); data("*", VPackValue(VPackValueType::Object))("permission", VPackValue(convertFromAuthLevel(lvl)))(); } return TRI_ERROR_NO_ERROR; }); data.close(); if (res.ok()) { generateOk(ResponseCode::OK, data.slice()); } else { generateError(res); } } /// helper to create(0), replace(1), update(2) a user static Result StoreUser(auth::UserManager* um, int mode, std::string const& user, VPackSlice json) { std::string passwd; bool active = true; VPackSlice extra; if (json.isObject()) { VPackSlice s = json.get("passwd"); passwd = s.isString() ? s.copyString() : ""; s = json.get("active"); active = s.isBool() ? s.getBool() : true; extra = json.get("extra"); } Result r; if (mode == 0 || mode == 1) { r = um->storeUser(mode == 1, user, passwd, active, extra); } else if (mode == 2) { r = um->updateUser(user, [&](auth::User& entry) { if (json.isObject()) { if (json.get("passwd").isString()) { entry.updatePassword(passwd); } if (json.get("active").isBool()) { entry.setActive(active); } } if (extra.isObject() && !extra.isEmptyObject()) { entry.setUserData(VPackBuilder(extra)); } return TRI_ERROR_NO_ERROR; }); } return r; } RestStatus RestUsersHandler::postRequest(auth::UserManager* um) { std::vector suffixes = _request->decodedSuffixes(); bool parseSuccess = false; VPackSlice const body = this->parseVPackBody(parseSuccess); if (!parseSuccess || !body.isObject()) { generateResult(rest::ResponseCode::OK, VPackSlice()); return RestStatus::DONE; } if (suffixes.empty()) { // create a new user if (isAdminUser()) { VPackSlice s = body.get("user"); std::string user = s.isString() ? s.copyString() : ""; // create user Result r = StoreUser(um, 0, user, body); if (r.ok()) { VPackBuilder doc = um->serializeUser(user); generateUserResult(ResponseCode::CREATED, doc); } else { generateError(r); } } else { generateError(rest::ResponseCode::FORBIDDEN, TRI_ERROR_FORBIDDEN); } } else if (suffixes.size() == 1) { // validate username / password std::string const& user = suffixes[0]; std::string password; VPackSlice s = body.get("passwd"); if (s.isString()) { password = s.copyString(); } if (um->checkPassword(user, password)) { generateOk(rest::ResponseCode::OK, VPackSlice::trueSlice()); } else { generateError(rest::ResponseCode::NOT_FOUND, TRI_ERROR_USER_NOT_FOUND); } } else { generateError(rest::ResponseCode::BAD, TRI_ERROR_BAD_PARAMETER); } return RestStatus::DONE; } RestStatus RestUsersHandler::putRequest(auth::UserManager* um) { std::vector suffixes = _request->decodedSuffixes(); bool parseSuccess = false; VPackSlice const body = this->parseVPackBody(parseSuccess); if (!parseSuccess) { generateError(rest::ResponseCode::BAD, TRI_ERROR_BAD_PARAMETER); return RestStatus::DONE; } if (suffixes.size() == 1) { // replace existing user std::string const& user = suffixes[0]; if (!canAccessUser(user)) { generateError(rest::ResponseCode::FORBIDDEN, TRI_ERROR_FORBIDDEN); return RestStatus::DONE; } Result r = StoreUser(um, 1, user, body); if (r.ok()) { VPackBuilder doc = um->serializeUser(user); generateUserResult(ResponseCode::OK, doc); } else { generateError(r); } } else if (suffixes.size() == 3 || suffixes.size() == 4) { // update database / collection permissions std::string const& name = suffixes[0]; if (suffixes[1] == "database") { // update a user's permissions std::string const& db = suffixes[2]; std::string coll = suffixes.size() == 4 ? suffixes[3] : ""; if (!isAdminUser()) { generateError(rest::ResponseCode::FORBIDDEN, TRI_ERROR_FORBIDDEN); return RestStatus::DONE; } // validate that the collection is present if (suffixes.size() > 3) { auto res = existsCollection(db, coll); if (!res.ok()) { generateError(res); return RestStatus::DONE; } } if (!body.isObject() || !body.get("grant").isString()) { generateError(rest::ResponseCode::BAD, TRI_ERROR_BAD_PARAMETER); return RestStatus::DONE; } VPackSlice grant = body.get("grant"); auth::Level lvl = auth::convertToAuthLevel(grant); // contains response in case of success VPackBuilder b; b(VPackValue(VPackValueType::Object)); Result r = um->updateUser(name, [&](auth::User& entry) { if (coll.empty()) { entry.grantDatabase(db, lvl); b(db, VPackValue(convertFromAuthLevel(lvl)))(); } else { entry.grantCollection(db, coll, lvl); b(db + "/" + coll, VPackValue(convertFromAuthLevel(lvl)))(); } return TRI_ERROR_NO_ERROR; }); if (r.ok()) { generateUserResult(ResponseCode::OK, b); } else { generateError(r); } } else if (suffixes[1] == "config") { // update internal config data, used in the admin dashboard if (!canAccessUser(name)) { generateError(rest::ResponseCode::FORBIDDEN, TRI_ERROR_FORBIDDEN); return RestStatus::DONE; } Result res; if (!body.isNone()) { std::string const& key = suffixes[2]; // The API expects: { value : } // If we get sth else than the above it is translated // to a remove of the config option. res = um->updateUser(name, [&](auth::User& u) { VPackSlice newVal = body; VPackSlice oldConf = u.userData(); if (!newVal.isObject() || !newVal.hasKey("value")) { if (oldConf.isObject() && oldConf.hasKey(key)) { u.setUserData( VPackCollection::remove(oldConf, std::unordered_set{key})); } // Nothing to do. We do not have a config yet. } else { // We need to merge the new key into the config VPackBuilder b; b(VPackValue(VPackValueType::Object))(key, newVal.get("value"))(); if (oldConf.isObject() && !oldConf.isEmptyObject()) { // merge value in u.setUserData(VPackCollection::merge(oldConf, b.slice(), false)); } else { u.setUserData(std::move(b)); } } return TRI_ERROR_NO_ERROR; }); } if (res.ok()) { resetResponse(ResponseCode::OK); } else { generateError(res); } } } else { generateError(rest::ResponseCode::BAD, TRI_ERROR_BAD_PARAMETER); } return RestStatus::DONE; } RestStatus RestUsersHandler::patchRequest(auth::UserManager* um) { std::vector const& suffixes = _request->decodedSuffixes(); bool parseSuccess = false; VPackSlice const body = this->parseVPackBody(parseSuccess); if (!parseSuccess) { generateError(rest::ResponseCode::BAD, TRI_ERROR_BAD_PARAMETER); return RestStatus::DONE; } if (suffixes.size() == 1) { std::string const& user = suffixes[0]; if (canAccessUser(user)) { // update a user Result r = StoreUser(um, 2, user, body); if (r.ok()) { VPackBuilder doc = um->serializeUser(user); generateUserResult(ResponseCode::OK, doc); } else { generateError(r); } } else { generateError(rest::ResponseCode::FORBIDDEN, TRI_ERROR_FORBIDDEN); } } else { generateError(rest::ResponseCode::BAD, TRI_ERROR_BAD_PARAMETER); } return RestStatus::DONE; } RestStatus RestUsersHandler::deleteRequest(auth::UserManager* um) { std::vector suffixes = _request->decodedSuffixes(); if (suffixes.size() == 1) { if (!isAdminUser()) { generateError(rest::ResponseCode::FORBIDDEN, TRI_ERROR_FORBIDDEN); return RestStatus::DONE; } std::string const& user = suffixes[0]; Result r = um->removeUser(user); if (r.ok()) { VPackBuilder b; b(VPackValue(VPackValueType::Object))(StaticStrings::Error, VPackValue(false))(StaticStrings::Code, VPackValue(202))(); generateResult(ResponseCode::ACCEPTED, b.slice()); } else { generateError(r); } } else if (suffixes.size() == 2) { std::string const& user = suffixes[0]; if (suffixes[1] == "config" && canAccessUser(user)) { Result r = um->updateUser(user, [&](auth::User& u) { u.setConfigData(VPackBuilder()); return TRI_ERROR_NO_ERROR; }); if (r.ok()) { resetResponse(ResponseCode::OK); } else { generateError(r); } } else { generateError(rest::ResponseCode::BAD, TRI_ERROR_BAD_PARAMETER); } } else if (suffixes.size() == 3 || suffixes.size() == 4) { std::string const& user = suffixes[0]; if (suffixes[1] == "database") { // revoke a user's permissions std::string const& db = suffixes[2]; std::string coll = suffixes.size() == 4 ? suffixes[3] : ""; if (!isAdminUser()) { generateError(rest::ResponseCode::FORBIDDEN, TRI_ERROR_FORBIDDEN); return RestStatus::DONE; } // validate that the collection is present if (suffixes.size() > 3) { auto res = existsCollection(db, coll); if (!res.ok()) { generateError(res); return RestStatus::DONE; } } Result r = um->updateUser(user, [&](auth::User& entry) { if (coll.empty()) { entry.removeDatabase(db); } else { entry.removeCollection(db, coll); } return TRI_ERROR_NO_ERROR; }); if (r.ok()) { VPackBuilder b; b(VPackValue(VPackValueType::Object))(StaticStrings::Error, VPackValue(false))(StaticStrings::Code, VPackValue(202))(); generateResult(ResponseCode::ACCEPTED, b.slice()); } else { generateError(r); } } else if (suffixes[1] == "config") { // remove internal config data, used in the WebUI if (canAccessUser(user)) { std::string const& key = suffixes[2]; Result r = um->updateUser(user, [&](auth::User& u) { VPackBuilder b; b(VPackValue(VPackValueType::Object))(key, VPackSlice::nullSlice())(); if (!u.configData().isNone()) { u.setConfigData(VPackCollection::merge(u.configData(), b.slice(), false, true)); } return TRI_ERROR_NO_ERROR; }); if (r.ok()) { resetResponse(ResponseCode::OK); } else { generateError(r); } } else { generateError(rest::ResponseCode::FORBIDDEN, TRI_ERROR_FORBIDDEN); } } } else { generateError(rest::ResponseCode::BAD, TRI_ERROR_BAD_PARAMETER); } return RestStatus::DONE; }