1
0
Fork 0
This commit is contained in:
Frank Celler 2016-06-01 23:03:00 +02:00
commit 22b454800d
37 changed files with 1066 additions and 531 deletions

View File

@ -209,6 +209,7 @@ add_executable(${BIN_ARANGOD}
Replication/InitialSyncer.cpp
Replication/Syncer.cpp
RestHandler/RestAdminLogHandler.cpp
RestHandler/RestAuthHandler.cpp
RestHandler/RestBaseHandler.cpp
RestHandler/RestBatchHandler.cpp
RestHandler/RestCursorHandler.cpp

View File

@ -39,12 +39,15 @@
#include "Random/RandomGenerator.h"
#include "Rest/HttpRequest.h"
#include "Rest/HttpResponse.h"
#include "RestServer/RestServerFeature.h"
#include "SimpleHttpClient/GeneralClientConnection.h"
#include "SimpleHttpClient/SimpleHttpClient.h"
#include "SimpleHttpClient/SimpleHttpResult.h"
using namespace arangodb;
using namespace arangodb::application_features;
using namespace basics::StringUtils;
static void addEmptyVPackObject(std::string const& name, VPackBuilder& builder) {
builder.add(VPackValue(name));
@ -518,7 +521,7 @@ bool AgencyComm::initialize() {
/// @brief will try to initialize a new agency
//////////////////////////////////////////////////////////////////////////////
bool AgencyComm::tryInitializeStructure() {
bool AgencyComm::tryInitializeStructure(std::string const& jwtSecret) {
VPackBuilder builder;
try {
VPackObjectBuilder b(&builder);
@ -614,6 +617,10 @@ bool AgencyComm::tryInitializeStructure() {
addEmptyVPackObject("DBServers", builder);
}
builder.add("InitDone", VPackValue(true));
builder.add("Secret", VPackValue(encodeHex(jwtSecret)));
} catch (std::exception const& e) {
LOG_TOPIC(ERR, Logger::STARTUP) << "Couldn't create initializing structure " << e.what();
return false;
} catch (...) {
LOG_TOPIC(ERR, Logger::STARTUP) << "Couldn't create initializing structure";
return false;
@ -668,13 +675,16 @@ bool AgencyComm::shouldInitializeStructure() {
bool AgencyComm::ensureStructureInitialized() {
LOG_TOPIC(TRACE, Logger::STARTUP) << "Checking if agency is initialized";
RestServerFeature* restServer =
application_features::ApplicationServer::getFeature<RestServerFeature>("RestServer");
while (true) {
while (shouldInitializeStructure()) {
LOG_TOPIC(TRACE, Logger::STARTUP)
<< "Agency is fresh. Needs initial structure.";
// mop: we initialized it .. great success
if (tryInitializeStructure()) {
if (tryInitializeStructure(restServer->jwtSecret())) {
LOG_TOPIC(TRACE, Logger::STARTUP) << "Successfully initialized agency";
break;
}
@ -693,7 +703,7 @@ bool AgencyComm::ensureStructureInitialized() {
if (value.isBoolean() && value.getBoolean()) {
// expecting a value of "true"
LOG_TOPIC(TRACE, Logger::STARTUP) << "Found an initialized agency";
return true;
break;
}
}
@ -702,6 +712,18 @@ bool AgencyComm::ensureStructureInitialized() {
sleep(1);
} // next attempt
AgencyCommResult secretResult = getValues("Secret");
VPackSlice secretValue = secretResult.slice()[0].get(std::vector<std::string>(
{prefix(), "Secret"}));
if (!secretValue.isString()) {
LOG(ERR) << "Couldn't find secret in agency!";
return false;
}
restServer->setJwtSecret(decodeHex(secretValue.copyString()));
return true;
}
////////////////////////////////////////////////////////////////////////////////

View File

@ -764,13 +764,7 @@ class AgencyComm {
/// @brief will try to initialize a new agency
//////////////////////////////////////////////////////////////////////////////
bool tryInitializeStructure();
//////////////////////////////////////////////////////////////////////////////
/// @brief initialize key in etcd
//////////////////////////////////////////////////////////////////////////////
bool initFromVPackSlice(std::string key, arangodb::velocypack::Slice s);
bool tryInitializeStructure(std::string const& jwtSecret);
//////////////////////////////////////////////////////////////////////////////
/// @brief checks if we are responsible for initializing the agency

View File

@ -557,6 +557,11 @@ bool HttpCommTask::processRead() {
// not authenticated
else {
HttpResponse response(GeneralResponse::ResponseCode::UNAUTHORIZED);
std::string realm =
"Bearer token_type=\"JWT\", realm=\"ArangoDB\"";
response.setHeaderNC(StaticStrings::WwwAuthenticate, std::move(realm));
clearRequest();
handleResponse(&response);
}

View File

@ -0,0 +1,128 @@
////////////////////////////////////////////////////////////////////////////////
/// DISCLAIMER
///
/// Copyright 2014-2016 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 Andreas Streichardt
////////////////////////////////////////////////////////////////////////////////
#include "RestAuthHandler.h"
#include <velocypack/Builder.h>
#include <velocypack/velocypack-aliases.h>
#include "Basics/StringUtils.h"
#include "Logger/Logger.h"
#include "Rest/HttpRequest.h"
#include "RestServer/RestServerFeature.h"
#include "Ssl/SslInterface.h"
#include "VocBase/AuthInfo.h"
using namespace arangodb;
using namespace arangodb::basics;
using namespace arangodb::rest;
RestAuthHandler::RestAuthHandler(HttpRequest* request, std::string const* jwtSecret)
: RestVocbaseBaseHandler(request), _jwtSecret(*jwtSecret), _validFor(60 * 60 * 24 * 30) {}
bool RestAuthHandler::isDirect() const { return false; }
std::string RestAuthHandler::generateJwt(std::string const& username, std::string const& password) {
VPackBuilder headerBuilder;
{
VPackObjectBuilder h(&headerBuilder);
headerBuilder.add("alg", VPackValue("HS256"));
headerBuilder.add("typ", VPackValue("JWT"));
}
std::chrono::seconds exp = std::chrono::duration_cast<std::chrono::seconds>(
std::chrono::system_clock::now().time_since_epoch()
) + _validFor;
VPackBuilder bodyBuilder;
{
VPackObjectBuilder p(&bodyBuilder);
bodyBuilder.add("preferred_username", VPackValue(username));
bodyBuilder.add("iss", VPackValue("arangodb"));
bodyBuilder.add("exp", VPackValue(exp.count()));
}
std::string fullMessage(StringUtils::encodeBase64(headerBuilder.toJson()) + "." + StringUtils::encodeBase64(bodyBuilder.toJson()));
std::string signature = sslHMAC(_jwtSecret.c_str(), _jwtSecret.length(), fullMessage.c_str(), fullMessage.length(), SslInterface::Algorithm::ALGORITHM_SHA256);
return fullMessage + "." + StringUtils::encodeBase64U(signature);
}
HttpHandler::status_t RestAuthHandler::execute() {
auto const type = _request->requestType();
if (type != GeneralRequest::RequestType::POST) {
generateError(GeneralResponse::ResponseCode::METHOD_NOT_ALLOWED,
TRI_ERROR_HTTP_METHOD_NOT_ALLOWED);
return status_t(HANDLER_DONE);
}
VPackOptions options = VPackOptions::Defaults;
options.checkAttributeUniqueness = true;
bool parseSuccess;
std::shared_ptr<VPackBuilder> parsedBody =
parseVelocyPackBody(&options, parseSuccess);
if (!parseSuccess) {
return badRequest();
}
VPackSlice slice = parsedBody->slice();
if (!slice.isObject()) {
return badRequest();
}
VPackSlice usernameSlice = slice.get("username");
VPackSlice passwordSlice = slice.get("password");
if (!usernameSlice.isString() || !passwordSlice.isString()) {
return badRequest();
}
std::string const username = usernameSlice.copyString();
std::string const password = passwordSlice.copyString();
AuthResult auth = RestServerFeature::AUTH_INFO.checkPassword(username, password);
if (auth._authorized) {
VPackBuilder resultBuilder;
{
VPackObjectBuilder b(&resultBuilder);
std::string jwt = generateJwt(username, password);
resultBuilder.add("jwt", VPackValue(jwt));
resultBuilder.add("must_change_password", VPackValue(auth._mustChange));
}
generateDocument(resultBuilder.slice(), true, &VPackOptions::Defaults);
return status_t(HANDLER_DONE);
} else {
// mop: rfc 2616 10.4.2 (if credentials wrong 401)
generateError(GeneralResponse::ResponseCode::UNAUTHORIZED, TRI_ERROR_HTTP_UNAUTHORIZED,
"Wrong credentials");
return status_t(HANDLER_DONE);
}
}
HttpHandler::status_t RestAuthHandler::badRequest() {
generateError(GeneralResponse::ResponseCode::BAD, TRI_ERROR_HTTP_BAD_PARAMETER,
"invalid JSON");
return status_t(HANDLER_DONE);
}

View File

@ -0,0 +1,60 @@
////////////////////////////////////////////////////////////////////////////////
/// DISCLAIMER
///
/// Copyright 2014-2016 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 Andreas Streichardt
////////////////////////////////////////////////////////////////////////////////
#ifndef ARANGOD_REST_HANDLER_REST_AUTH_HANDLER_H
#define ARANGOD_REST_HANDLER_REST_AUTH_HANDLER_H 1
#include "Basics/Common.h"
#include "RestHandler/RestVocbaseBaseHandler.h"
#include <chrono>
namespace arangodb {
////////////////////////////////////////////////////////////////////////////////
/// @brief auth handler
////////////////////////////////////////////////////////////////////////////////
class RestAuthHandler : public RestVocbaseBaseHandler {
public:
RestAuthHandler(HttpRequest*, std::string const* jwtSecret);
std::string generateJwt(std::string const&, std::string const&);
public:
bool isDirect() const override;
//////////////////////////////////////////////////////////////////////////////
/// @brief returns the log files (inheritDoc)
//////////////////////////////////////////////////////////////////////////////
status_t execute() override;
private:
std::string _jwtSecret;
std::chrono::seconds _validFor;
status_t badRequest();
};
}
#endif

View File

@ -41,6 +41,7 @@
#include "ProgramOptions/Section.h"
#include "Rest/Version.h"
#include "RestHandler/RestAdminLogHandler.h"
#include "RestHandler/RestAuthHandler.h"
#include "RestHandler/RestBatchHandler.h"
#include "RestHandler/RestCursorHandler.h"
#include "RestHandler/RestDebugHandler.h"
@ -87,6 +88,7 @@ RestServerFeature::RestServerFeature(
_authenticationUnixSockets(true),
_authenticationSystemOnly(false),
_proxyCheck(true),
_jwtSecret(""),
_handlerFactory(nullptr),
_jobManager(nullptr) {
setOptional(true);
@ -123,12 +125,16 @@ void RestServerFeature::collectOptions(
"--server.authentication-system-only",
"use HTTP authentication only for requests to /_api and /_admin",
new BooleanParameter(&_authenticationSystemOnly));
#ifdef ARANGODB_HAVE_DOMAIN_SOCKETS
options->addOption("--server.authentication-unix-sockets",
"authentication for requests via UNIX domain sockets",
new BooleanParameter(&_authenticationUnixSockets));
#endif
options->addOption("--server.jwt-secret",
"secret to use when doing jwt authentication",
new StringParameter(&_jwtSecret));
options->addSection("http", "HttpServer features");
@ -190,6 +196,15 @@ void RestServerFeature::validateOptions(std::shared_ptr<ProgramOptions>) {
}),
_accessControlAllowOrigins.end());
}
if (!_jwtSecret.empty()) {
if (_jwtSecret.length() > RestServerFeature::_maxSecretLength) {
LOG(ERR) << "Given JWT secret too long. Max length is " << RestServerFeature::_maxSecretLength;
FATAL_ERROR_EXIT();
}
} else {
generateNewJwtSecret();
}
}
static TRI_vocbase_t* LookupDatabaseFromRequest(HttpRequest* request,
@ -215,6 +230,7 @@ static TRI_vocbase_t* LookupDatabaseFromRequest(HttpRequest* request,
}
static bool SetRequestContext(HttpRequest* request, void* data) {
TRI_ASSERT(RestServerFeature::RESTSERVER != nullptr);
TRI_server_t* server = static_cast<TRI_server_t*>(data);
TRI_vocbase_t* vocbase = LookupDatabaseFromRequest(request, server);
@ -229,17 +245,31 @@ static bool SetRequestContext(HttpRequest* request, void* data) {
return false;
}
VocbaseContext* ctx = new arangodb::VocbaseContext(request, vocbase);
VocbaseContext* ctx = new arangodb::VocbaseContext(request, vocbase, RestServerFeature::getJwtSecret());
request->setRequestContext(ctx, true);
// the "true" means the request is the owner of the context
return true;
}
void RestServerFeature::prepare() { HttpHandlerFactory::setMaintenance(true); }
void RestServerFeature::generateNewJwtSecret() {
_jwtSecret = "";
std::random_device rd;
std::mt19937 rng(rd());
std::uniform_int_distribution<int> distribution(0,255);
for (size_t i=0;i<RestServerFeature::_maxSecretLength;i++) {
_jwtSecret += distribution(rng);
}
}
void RestServerFeature::prepare() {
HttpHandlerFactory::setMaintenance(true);
}
void RestServerFeature::start() {
RESTSERVER = this;
_jobManager.reset(new AsyncJobManager(ClusterCommRestCallback));
_httpOptions._vocbase = DatabaseFeature::DATABASE->vocbase();
@ -495,6 +525,10 @@ void RestServerFeature::defineHandlers() {
_handlerFactory->addPrefixHandler(
"/_admin/shutdown",
RestHandlerCreator<arangodb::RestShutdownHandler>::createNoData);
_handlerFactory->addPrefixHandler(
"/_open/auth",
RestHandlerCreator<arangodb::RestAuthHandler>::createData<std::string const*>, &_jwtSecret);
// ...........................................................................
// /_admin

View File

@ -58,9 +58,15 @@ class RestServerFeature final
return RESTSERVER->trustedProxies();
}
static std::string getJwtSecret() {
TRI_ASSERT(RESTSERVER != nullptr);
return RESTSERVER->jwtSecret();
}
private:
static RestServerFeature* RESTSERVER;
static const size_t _maxSecretLength = 64;
public:
explicit RestServerFeature(application_features::ApplicationServer*);
@ -77,9 +83,12 @@ class RestServerFeature final
bool _authentication;
bool _authenticationUnixSockets;
bool _authenticationSystemOnly;
bool _proxyCheck;
std::vector<std::string> _trustedProxies;
std::vector<std::string> _accessControlAllowOrigins;
std::string _jwtSecret;
public:
bool authentication() const { return _authentication; }
@ -87,6 +96,9 @@ class RestServerFeature final
bool authenticationSystemOnly() const { return _authenticationSystemOnly; }
bool proxyCheck() const { return _proxyCheck; }
std::vector<std::string> trustedProxies() const { return _trustedProxies; }
std::string jwtSecret() const { return _jwtSecret; }
void generateNewJwtSecret();
void setJwtSecret(std::string const& jwtSecret) { _jwtSecret = jwtSecret; }
private:
void buildServers();

View File

@ -23,12 +23,20 @@
#include "VocbaseContext.h"
#include <chrono>
#include <velocypack/Builder.h>
#include <velocypack/Exception.h>
#include <velocypack/Parser.h>
#include <velocypack/velocypack-aliases.h>
#include "Basics/MutexLocker.h"
#include "Basics/tri-strings.h"
#include "Cluster/ServerState.h"
#include "Endpoint/ConnectionInfo.h"
#include "Logger/Logger.h"
#include "RestServer/RestServerFeature.h"
#include "Ssl/SslInterface.h"
#include "VocBase/AuthInfo.h"
#include "VocBase/server.h"
#include "VocBase/vocbase.h"
@ -40,8 +48,14 @@ using namespace arangodb::rest;
double VocbaseContext::ServerSessionTtl =
60.0 * 60.0 * 24 * 60; // 2 month session timeout
VocbaseContext::VocbaseContext(HttpRequest* request, TRI_vocbase_t* vocbase)
: RequestContext(request), _vocbase(vocbase) {}
VocbaseContext::VocbaseContext(HttpRequest* request,
TRI_vocbase_t* vocbase, std::string const& jwtSecret)
: RequestContext(request), _vocbase(vocbase), _jwtSecret(jwtSecret) {
TRI_ASSERT(_server != nullptr);
TRI_ASSERT(_vocbase != nullptr);
}
VocbaseContext::~VocbaseContext() { TRI_ReleaseVocBase(_vocbase); }
////////////////////////////////////////////////////////////////////////////////
/// @brief whether or not to use special cluster authentication
@ -76,7 +90,21 @@ GeneralResponse::ResponseCode VocbaseContext::authenticate() {
// no authentication required at all
return GeneralResponse::ResponseCode::OK;
}
std::string const& path = _request->requestPath();
// mop: inside authenticateRequest() _request->user will be populated
GeneralResponse::ResponseCode result = authenticateRequest();
if (result == GeneralResponse::ResponseCode::UNAUTHORIZED || result == GeneralResponse::ResponseCode::FORBIDDEN) {
if (StringUtils::isPrefix(path, "/_open/") ||
StringUtils::isPrefix(path, "/_admin/aardvark/") || path == "/") {
// mop: these paths are always callable...they will be able to check req.user when it could be validated
result = GeneralResponse::ResponseCode::OK;
}
}
return result;
}
GeneralResponse::ResponseCode VocbaseContext::authenticateRequest() {
#ifdef ARANGODB_HAVE_DOMAIN_SOCKETS
// check if we need to run authentication for this type of
// endpoint
@ -106,23 +134,46 @@ GeneralResponse::ResponseCode VocbaseContext::authenticate() {
}
}
if (StringUtils::isPrefix(path, "/_open/") ||
StringUtils::isPrefix(path, "/_admin/aardvark/") || path == "/") {
return GeneralResponse::ResponseCode::OK;
}
// .............................................................................
// authentication required
// .............................................................................
bool found;
std::string const& auth =
std::string const& authStr =
_request->header(StaticStrings::Authorization, found);
if (!found) {
return GeneralResponse::ResponseCode::UNAUTHORIZED;
}
size_t methodPos = authStr.find_first_of(' ');
if (methodPos == std::string::npos) {
return GeneralResponse::ResponseCode::UNAUTHORIZED;
}
// skip over authentication method
char const* auth = authStr.c_str() + methodPos;
while (*auth == ' ') {
++auth;
}
LOG(DEBUG) << "Authorization header: " << authStr;
if (TRI_CaseEqualString(authStr.c_str(), "basic ", 6)) {
return basicAuthentication(auth);
} else if (TRI_CaseEqualString(authStr.c_str(), "bearer ", 7)) {
return jwtAuthentication(std::string(auth));
} else {
// mop: hmmm is 403 the correct status code? or 401? or 400? :S
return GeneralResponse::ResponseCode::UNAUTHORIZED;
}
}
////////////////////////////////////////////////////////////////////////////////
/// @brief checks the authentication via basic
////////////////////////////////////////////////////////////////////////////////
GeneralResponse::ResponseCode VocbaseContext::basicAuthentication(const char* auth) {
if (useClusterAuthentication()) {
std::string const expected = ServerState::instance()->getAuthentication();
@ -130,13 +181,12 @@ GeneralResponse::ResponseCode VocbaseContext::authenticate() {
return GeneralResponse::ResponseCode::UNAUTHORIZED;
}
// TODO should support other authentications (currently only "basic ")
std::string const up = StringUtils::decodeBase64(auth.substr(6));
std::string const up = StringUtils::decodeBase64(auth);
std::string::size_type n = up.find(':', 0);
if (n == std::string::npos || n == 0 || n + 1 > up.size()) {
LOG(TRACE) << "invalid authentication data found, cannot extract "
"username/password";
"username/password";
return GeneralResponse::ResponseCode::BAD;
}
@ -145,9 +195,9 @@ GeneralResponse::ResponseCode VocbaseContext::authenticate() {
return GeneralResponse::ResponseCode::OK;
}
AuthResult result =
RestServerFeature::AUTH_INFO.checkAuthentication(auth, _vocbase->_name);
RestServerFeature::AUTH_INFO.checkAuthentication(AuthInfo::AuthType::BASIC, auth);
if (!result._authorized) {
return GeneralResponse::ResponseCode::UNAUTHORIZED;
@ -158,8 +208,8 @@ GeneralResponse::ResponseCode VocbaseContext::authenticate() {
if (result._mustChange) {
if ((_request->requestType() == GeneralRequest::RequestType::PUT ||
_request->requestType() == GeneralRequest::RequestType::PATCH) &&
StringUtils::isPrefix(_request->requestPath(), "/_api/user/")) {
_request->requestType() == GeneralRequest::RequestType::PATCH) &&
StringUtils::isPrefix(_request->requestPath(), "/_api/user/")) {
return GeneralResponse::ResponseCode::OK;
}
@ -168,3 +218,142 @@ GeneralResponse::ResponseCode VocbaseContext::authenticate() {
return GeneralResponse::ResponseCode::OK;
}
////////////////////////////////////////////////////////////////////////////////
/// @brief checks the authentication via jwt
////////////////////////////////////////////////////////////////////////////////
GeneralResponse::ResponseCode VocbaseContext::jwtAuthentication(std::string const& auth) {
std::vector<std::string> const parts = StringUtils::split(auth, '.');
if (parts.size() != 3) {
return GeneralResponse::ResponseCode::UNAUTHORIZED;
}
std::string const& header = parts[0];
std::string const& body = parts[1];
std::string const& signature = parts[2];
std::string const message = header + "." + body;
if (!validateJwtHeader(header)) {
LOG(DEBUG) << "Couldn't validate jwt header " << header;
return GeneralResponse::ResponseCode::UNAUTHORIZED;
}
std::string username;
if (!validateJwtBody(body, &username)) {
LOG(DEBUG) << "Couldn't validate jwt body " << body;
return GeneralResponse::ResponseCode::UNAUTHORIZED;
}
if (!validateJwtHMAC256Signature(message, signature)) {
LOG(DEBUG) << "Couldn't validate jwt signature " << signature;
return GeneralResponse::ResponseCode::UNAUTHORIZED;
}
_request->setUser(username);
return GeneralResponse::ResponseCode::OK;
}
std::shared_ptr<VPackBuilder> VocbaseContext::parseJson(std::string const& str, std::string const& hint) {
std::shared_ptr<VPackBuilder> result;
VPackParser parser;
try {
parser.parse(str);
result = parser.steal();
} catch (std::bad_alloc const&) {
LOG(ERR) << "Out of memory parsing " << hint << "!";
} catch (VPackException const& ex) {
LOG(DEBUG) << "Couldn't parse " << hint << ": " << ex.what();
} catch (...) {
LOG(ERR) << "Got unknown exception trying to parse " << hint;
}
return result;
}
bool VocbaseContext::validateJwtHeader(std::string const& header) {
std::shared_ptr<VPackBuilder> headerBuilder = parseJson(StringUtils::decodeBase64(header), "jwt header");
if (headerBuilder.get() == nullptr) {
return false;
}
VPackSlice const headerSlice = headerBuilder->slice();
if (!headerSlice.isObject()) {
return false;
}
VPackSlice const algSlice = headerSlice.get("alg");
VPackSlice const typSlice = headerSlice.get("typ");
if (!algSlice.isString()) {
return false;
}
if (!typSlice.isString()) {
return false;
}
if (algSlice.copyString() != "HS256") {
return false;
}
std::string typ = typSlice.copyString();
if (typ != "JWT") {
return false;
}
return true;
}
bool VocbaseContext::validateJwtBody(std::string const& body, std::string* username) {
std::shared_ptr<VPackBuilder> bodyBuilder = parseJson(StringUtils::decodeBase64(body), "jwt body");
if (bodyBuilder.get() == nullptr) {
return false;
}
VPackSlice const bodySlice = bodyBuilder->slice();
if (!bodySlice.isObject()) {
return false;
}
VPackSlice const issSlice = bodySlice.get("iss");
if (!issSlice.isString()) {
return false;
}
if (issSlice.copyString() != "arangodb") {
return false;
}
VPackSlice const usernameSlice = bodySlice.get("preferred_username");
if (!usernameSlice.isString()) {
return false;
}
*username = usernameSlice.copyString();
// mop: optional exp (cluster currently uses non expiring jwts)
if (bodySlice.hasKey("exp")) {
VPackSlice const expSlice = bodySlice.get("exp");
if (!expSlice.isNumber()) {
return false;
}
std::chrono::system_clock::time_point expires(std::chrono::seconds(expSlice.getNumber<uint64_t>()));
std::chrono::system_clock::time_point now = std::chrono::system_clock::now();
if (now >= expires) {
return false;
}
}
return true;
}
bool VocbaseContext::validateJwtHMAC256Signature(std::string const& message, std::string const& signature) {
std::string decodedSignature = StringUtils::decodeBase64U(signature);
return verifyHMAC(_jwtSecret.c_str(), _jwtSecret.length(), message.c_str(), message.length(), decodedSignature.c_str(), decodedSignature.length(), SslInterface::Algorithm::ALGORITHM_SHA256);
}

View File

@ -24,6 +24,9 @@
#ifndef ARANGOD_REST_SERVER_VOCBASE_CONTEXT_H
#define ARANGOD_REST_SERVER_VOCBASE_CONTEXT_H 1
#include <velocypack/Builder.h>
#include <velocypack/velocypack-aliases.h>
#include "Basics/Common.h"
#include "Rest/HttpRequest.h"
#include "Rest/HttpResponse.h"
@ -35,7 +38,11 @@ struct TRI_vocbase_t;
namespace arangodb {
class VocbaseContext : public arangodb::RequestContext {
public:
VocbaseContext(HttpRequest*, TRI_vocbase_t*);
static double ServerSessionTtl;
public:
VocbaseContext(HttpRequest*, TRI_vocbase_t*, std::string const&);
~VocbaseContext();
public:
TRI_vocbase_t* vocbase() const { return _vocbase; }
@ -46,11 +53,35 @@ class VocbaseContext : public arangodb::RequestContext {
private:
bool useClusterAuthentication() const;
public:
static double ServerSessionTtl;
private:
//////////////////////////////////////////////////////////////////////////////
/// @brief checks the authentication (basic)
//////////////////////////////////////////////////////////////////////////////
GeneralResponse::ResponseCode basicAuthentication(const char*);
//////////////////////////////////////////////////////////////////////////////
/// @brief checks the authentication (jwt)
//////////////////////////////////////////////////////////////////////////////
GeneralResponse::ResponseCode jwtAuthentication(std::string const&);
std::shared_ptr<VPackBuilder> parseJson(std::string const&, std::string const&);
bool validateJwtHeader(std::string const&);
bool validateJwtBody(std::string const&, std::string*);
bool validateJwtHMAC256Signature(std::string const&, std::string const&);
private:
//////////////////////////////////////////////////////////////////////////////
/// @brief checks the authentication header and sets user if successful
//////////////////////////////////////////////////////////////////////////////
GeneralResponse::ResponseCode authenticateRequest();
private:
TRI_vocbase_t* _vocbase;
std::string const _jwtSecret;
};
}

View File

@ -31,8 +31,10 @@
#include "Basics/ReadLocker.h"
#include "Basics/VelocyPackHelper.h"
#include "Basics/WriteLocker.h"
#include "Basics/tri-strings.h"
#include "Logger/Logger.h"
#include "RestServer/DatabaseFeature.h"
#include "Ssl/SslInterface.h"
#include "Utils/SingleCollectionTransaction.h"
#include "Utils/StandaloneTransactionContext.h"
#include "VocBase/MasterPointer.h"
@ -94,139 +96,18 @@ static AuthEntry CreateAuthEntry(VPackSlice const& slice) {
bool mustChange =
VelocyPackHelper::getBooleanValue(slice, "changePassword", false);
std::cout
<< "user: " << userSlice.copyString() << "\n"
<< "method: " << methodSlice.copyString() << "\n"
<< "salt: " << saltSlice.copyString() << "\n"
<< "hash: " << hashSlice.copyString() << "\n"
<< "active: " << active << "\n"
<< "must change: " << mustChange << "\n";
return AuthEntry(userSlice.copyString(), methodSlice.copyString(),
saltSlice.copyString(), hashSlice.copyString(), active,
mustChange);
}
AuthLevel AuthEntry::canUseDatabase(std::string const& dbname) const {
return AuthLevel::NONE;
}
void AuthInfo::clear() {
_authInfo.clear();
_authCache.clear();
}
bool AuthInfo::reload() {
insertInitial();
TRI_vocbase_t* vocbase = DatabaseFeature::DATABASE->vocbase();
if (vocbase == nullptr) {
LOG(DEBUG) << "system database is unknown, cannot load authentication "
<< "and authorization information";
return false;
}
LOG(DEBUG) << "starting to load authentication and authorization information";
WRITE_LOCKER(writeLocker, _authInfoLock);
SingleCollectionTransaction trx(StandaloneTransactionContext::Create(vocbase),
TRI_COL_NAME_USERS, TRI_TRANSACTION_READ);
int res = trx.begin();
if (res != TRI_ERROR_NO_ERROR) {
return false;
}
OperationResult users =
trx.all(TRI_COL_NAME_USERS, 0, UINT64_MAX, OperationOptions());
trx.finish(users.code);
if (users.failed()) {
LOG(ERR) << "cannot read users from _users collection";
return false;
}
auto usersSlice = users.slice();
if (!usersSlice.isArray()) {
LOG(ERR) << "cannot read users from _users collection";
return false;
}
clear();
if (usersSlice.length() == 0) {
insertInitial();
} else {
for (VPackSlice const& userSlice : VPackArrayIterator(usersSlice)) {
AuthEntry auth = CreateAuthEntry(userSlice.resolveExternal());
if (auth.isActive()) {
_authInfo[auth.username()] = auth;
}
};
}
return true;
}
std::string AuthInfo::checkCache(std::string const& authorizationField,
bool* mustChange) {
READ_LOCKER(readLocker, _authInfoLock);
auto const& it = _authCache.find(authorizationField);
if (it != _authCache.end()) {
AuthCache const& cached = it->second;
#warning expires
*mustChange = cached.mustChange();
return cached.username();
}
// sorry, not found
return "";
}
bool AuthInfo::canUseDatabase(std::string const& username,
char const* databaseName) {
#warning TODO
#if 0
READ_LOCKER(readLocker, _authInfoLock);
AuthEntry const& entry = findUser(username);
if (!entry.isActive()) {
return false;
}
return entry._databases.find(databaseName) != entry.databases.end();
#endif
return true;
}
AuthResult AuthInfo::checkAuthentication(std::string const& authorizationField,
char const* databaseName) {
return AuthResult();
}
bool AuthInfo::populate(VPackSlice const& slice) {
TRI_ASSERT(slice.isArray());
WRITE_LOCKER(writeLocker, _authInfoLock);
clear();
for (VPackSlice const& authSlice : VPackArrayIterator(slice)) {
AuthEntry auth = CreateAuthEntry(authSlice);
if (auth.isActive()) {
_authInfo.emplace(auth.username(), auth);
}
}
return true;
_authBasicCache.clear();
}
void AuthInfo::insertInitial() {
@ -274,42 +155,199 @@ void AuthInfo::insertInitial() {
}
}
#if 0
bool AuthInfo::populate(VPackSlice const& slice) {
TRI_ASSERT(slice.isArray());
// no entry found in cache, decode the basic auth info and look it up
std::string const up = StringUtils::decodeBase64(auth);
std::string::size_type n = up.find(':', 0);
WRITE_LOCKER(writeLocker, _authInfoLock);
if (n == std::string::npos || n == 0 || n + 1 > up.size()) {
LOG(TRACE) << "invalid authentication data found, cannot extract "
"username/password";
return GeneralResponse::ResponseCode::BAD;
clear();
for (VPackSlice const& authSlice : VPackArrayIterator(slice)) {
AuthEntry auth = CreateAuthEntry(authSlice.resolveExternal());
if (auth.isActive()) {
_authInfo.emplace(auth.username(), auth);
}
username = up.substr(0, n);
LOG(TRACE) << "checking authentication for user '" << username << "'";
////////////////////////////////////////////////////////////////////////////////
/// @brief check if a user can see a database
/// note: "seeing" here does not necessarily mean the user can access the db.
/// it only means there is a user account (with whatever password) present
/// in the database
////////////////////////////////////////////////////////////////////////////////
static bool CanUseDatabase(TRI_vocbase_t* vocbase, char const* username) {
if (!vocbase->_settings.requireAuthentication) {
// authentication is turned off
return true;
}
if (strlen(username) == 0) {
// will happen if username is "" (when converting it from a null value)
// this will happen if authentication is turned off
return true;
}
return TRI_ExistsAuthenticationAuthInfo(vocbase, username);
return true;
}
#endif
bool AuthInfo::reload() {
insertInitial();
TRI_vocbase_t* vocbase = DatabaseFeature::DATABASE->vocbase();
if (vocbase == nullptr) {
LOG(DEBUG) << "system database is unknown, cannot load authentication "
<< "and authorization information";
return false;
}
LOG(DEBUG) << "starting to load authentication and authorization information";
SingleCollectionTransaction trx(StandaloneTransactionContext::Create(vocbase),
TRI_COL_NAME_USERS, TRI_TRANSACTION_READ);
int res = trx.begin();
if (res != TRI_ERROR_NO_ERROR) {
return false;
}
OperationResult users =
trx.all(TRI_COL_NAME_USERS, 0, UINT64_MAX, OperationOptions());
trx.finish(users.code);
if (users.failed()) {
LOG(ERR) << "cannot read users from _users collection";
return false;
}
auto usersSlice = users.slice();
if (!usersSlice.isArray()) {
LOG(ERR) << "cannot read users from _users collection";
return false;
}
if (usersSlice.length() == 0) {
insertInitial();
} else {
populate(usersSlice);
}
return true;
}
AuthResult AuthInfo::checkPassword(std::string const& username,
std::string const& password) {
AuthResult result;
// look up username
READ_LOCKER(readLocker, _authInfoLock);
auto it = _authInfo.find(username);
if (it == _authInfo.end()) {
return result;
}
AuthEntry const& auth = it->second;
if (!auth.isActive()) {
return result;
}
result._username = username;
result._mustChange = auth.mustChange();
std::string salted = auth.passwordSalt() + password;
size_t len = salted.size();
std::string const& passwordMethod = auth.passwordMethod();
// default value is false
char* crypted = nullptr;
size_t cryptedLength;
try {
if (passwordMethod == "sha1") {
arangodb::rest::SslInterface::sslSHA1(salted.c_str(), len, crypted,
cryptedLength);
} else if (passwordMethod == "sha512") {
arangodb::rest::SslInterface::sslSHA512(salted.c_str(), len, crypted,
cryptedLength);
} else if (passwordMethod == "sha384") {
arangodb::rest::SslInterface::sslSHA384(salted.c_str(), len, crypted,
cryptedLength);
} else if (passwordMethod == "sha256") {
arangodb::rest::SslInterface::sslSHA256(salted.c_str(), len, crypted,
cryptedLength);
} else if (passwordMethod == "sha224") {
arangodb::rest::SslInterface::sslSHA224(salted.c_str(), len, crypted,
cryptedLength);
} else if (passwordMethod == "md5") {
arangodb::rest::SslInterface::sslMD5(salted.c_str(), len, crypted,
cryptedLength);
} else {
// invalid algorithm...
}
} catch (...) {
// SslInterface::ssl....() allocate strings with new, which might throw
// exceptions
}
if (crypted != nullptr) {
if (0 < cryptedLength) {
size_t hexLen;
char* hex = TRI_EncodeHexString(crypted, cryptedLength, &hexLen);
if (hex != nullptr) {
result._authorized = auth.checkPasswordHash(hex);
TRI_FreeString(TRI_CORE_MEM_ZONE, hex);
}
}
delete[] crypted;
}
return result;
}
AuthLevel AuthInfo::canUseDatabase(std::string const& username, std::string const& dbname) {
auto const& it = _authInfo.find(username);
if (it == _authInfo.end()) {
return AuthLevel::NONE;
}
AuthEntry const& entry = it->second;
return entry.canUseDatabase(dbname);
}
AuthResult AuthInfo::checkAuthentication(AuthType authType, std::string const& secret) {
switch (authType) {
case AuthType::BASIC:
return checkAuthenticationBasic(secret);
case AuthType::JWT:
return checkAuthenticationJWT(secret);
}
return AuthResult();
}
AuthResult AuthInfo::checkAuthenticationBasic(std::string const& secret) {
auto const& it = _authBasicCache.find(secret);
if (it != _authBasicCache.end()) {
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(TRACE) << "invalid authentication data found, cannot extract "
"username/password";
return AuthResult();
}
std::string username = up.substr(0, n);
std::string password = up.substr(n + 1);
AuthResult result = checkPassword(username, password);
if (result._authorized) {
_authBasicCache.emplace(secret, result);
}
return result;
}
AuthResult AuthInfo::checkAuthenticationJWT(std::string const& secret) {
return AuthResult();
}

View File

@ -33,13 +33,10 @@ namespace velocypack {
class Slice;
}
class AuthResult {
public:
std::string _username;
bool _authorized;
bool _mustChange;
enum class AuthLevel {
NONE, RO, RW
};
class AuthEntry {
public:
AuthEntry() : _active(false), _mustChange(false) {}
@ -66,6 +63,8 @@ class AuthEntry {
return _passwordHash == hash;
}
AuthLevel canUseDatabase(std::string const& dbname) const;
private:
std::string _username;
std::string _passwordMethod;
@ -75,48 +74,44 @@ class AuthEntry {
bool _mustChange;
};
class AuthCache {
class AuthResult {
public:
AuthCache(std::string const& authorizationField, AuthEntry const& authEntry,
double expires)
: _authorizationField(authorizationField),
_username(authEntry.username()),
_mustChange(authEntry.mustChange()),
_expires(expires) {}
public:
std::string const& username() const { return _username; }
bool mustChange() const { return _mustChange; }
private:
std::string const _authorizationField;
std::string const _username;
bool const _mustChange;
double const _expires;
std::string _username;
bool _authorized;
bool _mustChange;
};
class AuthInfo {
public:
bool canUseDatabase(std::string const& username, char const* databaseName);
AuthResult checkAuthentication(std::string const& authorizationField,
char const* databaseName);
enum class AuthType {
BASIC, JWT
};
public:
bool reload();
AuthResult checkPassword(std::string const& username,
std::string const& password);
AuthResult checkAuthentication(AuthType authType,
std::string const& secret);
AuthLevel canUseDatabase(std::string const& username,
std::string const& dbname);
private:
void clear();
bool populate(velocypack::Slice const& slice);
void insertInitial();
bool populate(velocypack::Slice const& slice);
std::string checkCache(std::string const& authorizationField,
bool* mustChange);
AuthResult checkAuthenticationBasic(std::string const& secret);
AuthResult checkAuthenticationJWT(std::string const& secret);
private:
std::unordered_map<std::string, arangodb::AuthEntry> _authInfo;
std::unordered_map<std::string, arangodb::AuthCache> _authCache;
basics::ReadWriteLock _authInfoLock;
std::unordered_map<std::string, arangodb::AuthEntry> _authInfo;
std::unordered_map<std::string, arangodb::AuthResult> _authBasicCache;
};
}

View File

@ -1,162 +0,0 @@
////////////////////////////////////////////////////////////////////////////////
/// DISCLAIMER
///
/// Copyright 2014-2016 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 Dr. Frank Celler
////////////////////////////////////////////////////////////////////////////////
#include "auth.h"
#include "Basics/ReadLocker.h"
#include "Basics/VelocyPackHelper.h"
#include "Basics/WriteLocker.h"
#include "Basics/hashes.h"
#include "Basics/tri-strings.h"
#include "Logger/Logger.h"
#include "Ssl/SslInterface.h"
#include "Utils/SingleCollectionTransaction.h"
#include "Utils/StandaloneTransactionContext.h"
#include "VocBase/collection.h"
#include "VocBase/document-collection.h"
#include "VocBase/vocbase.h"
////////////////////////////////////////////////////////////////////////////////
/// @brief checks the authentication
////////////////////////////////////////////////////////////////////////////////
bool TRI_CheckAuthenticationAuthInfo(TRI_vocbase_t* vocbase, char const* hash,
char const* username, char const* password,
bool* mustChange) {
TRI_ASSERT(vocbase != nullptr);
bool res = false;
VocbaseAuthInfo* auth = nullptr;
{
// look up username
READ_LOCKER(readLocker, vocbase->_authInfoLock);
auto it = vocbase->_authInfo.find(username);
if (it == vocbase->_authInfo.end()) {
return false;
}
// We do not take responsiblity for the data
auth = it->second;
if (auth == nullptr || !auth->isActive()) {
return false;
}
*mustChange = auth->mustChange();
size_t const n = strlen(auth->passwordSalt());
size_t const p = strlen(password);
char* salted = static_cast<char*>(
TRI_Allocate(TRI_UNKNOWN_MEM_ZONE, n + p + 1, false));
if (salted == nullptr) {
return false;
}
memcpy(salted, auth->passwordSalt(), n);
memcpy(salted + n, password, p);
salted[n + p] = '\0';
// default value is false
char* crypted = nullptr;
size_t cryptedLength;
char const* passwordMethod = auth->passwordMethod();
TRI_ASSERT(passwordMethod != nullptr);
try {
if (strcmp(passwordMethod, "sha1") == 0) {
arangodb::rest::SslInterface::sslSHA1(salted, n + p, crypted,
cryptedLength);
} else if (strcmp(passwordMethod, "sha512") == 0) {
arangodb::rest::SslInterface::sslSHA512(salted, n + p, crypted,
cryptedLength);
} else if (strcmp(passwordMethod, "sha384") == 0) {
arangodb::rest::SslInterface::sslSHA384(salted, n + p, crypted,
cryptedLength);
} else if (strcmp(passwordMethod, "sha256") == 0) {
arangodb::rest::SslInterface::sslSHA256(salted, n + p, crypted,
cryptedLength);
} else if (strcmp(passwordMethod, "sha224") == 0) {
arangodb::rest::SslInterface::sslSHA224(salted, n + p, crypted,
cryptedLength);
} else if (strcmp(passwordMethod, "md5") == 0) {
arangodb::rest::SslInterface::sslMD5(salted, n + p, crypted,
cryptedLength);
} else {
// invalid algorithm...
res = false;
}
} catch (...) {
// SslInterface::ssl....() allocate strings with new, which might throw
// exceptions
// if we get one, we can ignore it because res is set to false anyway
}
if (crypted != nullptr) {
TRI_ASSERT(cryptedLength > 0);
size_t hexLen;
char* hex = TRI_EncodeHexString(crypted, cryptedLength, &hexLen);
if (hex != nullptr) {
res = auth->isEqualPasswordHash(hex);
TRI_FreeString(TRI_CORE_MEM_ZONE, hex);
}
delete[] crypted;
}
TRI_FreeString(TRI_UNKNOWN_MEM_ZONE, salted);
}
if (res && hash != nullptr) {
// insert item into the cache
auto cached = std::make_unique<VocbaseAuthCache>();
cached->_hash = std::string(hash);
cached->_username = std::string(username);
cached->_mustChange = auth->mustChange();
if (cached->_hash.empty() || cached->_username.empty()) {
return res;
}
WRITE_LOCKER(writeLocker, vocbase->_authInfoLock);
auto it = vocbase->_authCache.find(cached->_hash);
if (it != vocbase->_authCache.end()) {
delete (*it).second;
(*it).second = nullptr;
}
vocbase->_authCache[cached->_hash] = cached.get();
cached.release();
}
return res;
}

View File

@ -1991,8 +1991,9 @@ int TRI_GetUserDatabasesServer(TRI_server_t* server, char const* username,
char const* dbName = p.second->_name;
TRI_ASSERT(dbName != nullptr);
if (!RestServerFeature::AUTH_INFO.canUseDatabase(username, dbName)) {
// user cannot see database
auto level = RestServerFeature::AUTH_INFO.canUseDatabase(username, dbName);
if (level == AuthLevel::NONE) {
continue;
}

View File

@ -33,7 +33,6 @@ const errors = require('@arangodb').errors;
const joinPath = require('path').posix.join;
const notifications = require('@arangodb/configuration').notifications;
const examples = require('@arangodb/graph-examples/example-graph');
const systemStorage = require('@arangodb/foxx/sessions/storages/_system');
const createRouter = require('@arangodb/foxx/router');
const users = require('@arangodb/users');
const cluster = require('@arangodb/cluster');
@ -42,7 +41,6 @@ const ERROR_USER_NOT_FOUND = errors.ERROR_USER_NOT_FOUND.code;
const API_DOCS = require(module.context.fileName('api-docs.json'));
API_DOCS.basePath = `/_db/${encodeURIComponent(db._name())}`;
const sessions = systemStorage();
const router = createRouter();
module.exports = router;
@ -89,7 +87,7 @@ router.get('/config.js', function(req, res) {
});
router.get('/whoAmI', function(req, res) {
res.json({user: req.session.uid || null});
res.json({user: req.user || null});
})
.summary('Return the current user')
.description(dd`
@ -98,63 +96,12 @@ router.get('/whoAmI', function(req, res) {
`);
router.post('/logout', function (req, res) {
sessions.clear(req.session);
delete req.session;
res.json({success: true});
})
.summary('Log out')
.description(dd`
Destroys the current session and revokes any authentication.
`);
router.post('/login', function (req, res) {
const currentDb = db._name();
/*
const actualDb = req.body.database;
if (actualDb !== currentDb) {
res.redirect(307, joinPath(
'/_db',
encodeURIComponent(actualDb),
module.context.mount,
'/login'
));
return;
}
*/
const user = req.body.username;
const valid = users.isValid(user, req.body.password);
if (!valid) {
res.throw('unauthorized', 'Bad username or password');
}
sessions.setUser(req.session, user);
sessions.save(req.session);
res.json({user});
})
.body({
username: joi.string().required(),
password: joi.string().required().allow('')
//database: joi.string().default(db._name())
}, 'Login credentials.')
.error('unauthorized', 'Invalid credentials.')
.summary('Log in')
.description(dd`
Authenticates the user for the active session with a username and password.
Creates a new session if none exists.
`);
const authRouter = createRouter();
router.use(authRouter);
authRouter.use((req, res, next) => {
if (global.AUTHENTICATION_ENABLED()) {
if (!req.session.uid) {
if (!req.user) {
res.throw('unauthorized');
}
}
@ -233,12 +180,11 @@ authRouter.post('/query/upload/:user', function(req, res) {
let user;
try {
user = users.document(req.session.uid);
user = users.document(req.user);
} catch (e) {
if (!e.isArangoError || e.errorNum !== ERROR_USER_NOT_FOUND) {
throw e;
}
sessions.setUser(req.session);
res.throw('not found');
}
@ -276,12 +222,11 @@ authRouter.get('/query/download/:user', function(req, res) {
let user;
try {
user = users.document(req.session.uid);
user = users.document(req.user);
} catch (e) {
if (!e.isArangoError || e.errorNum !== ERROR_USER_NOT_FOUND) {
throw e;
}
sessions.setUser(req.session);
res.throw('not found');
}

View File

@ -34,7 +34,7 @@ module.exports = router;
router.use((req, res, next) => {
if (global.AUTHENTICATION_ENABLED()) {
if (!req.session.uid) {
if (!req.user) {
res.throw('unauthorized');
}
}

View File

@ -42,7 +42,7 @@ module.exports = router;
router.use((req, res, next) => {
if (global.AUTHENTICATION_ENABLED()) {
if (!req.session.uid) {
if (!req.user) {
res.throw('unauthorized');
}
}

File diff suppressed because one or more lines are too long

View File

@ -3122,4 +3122,4 @@ var cutByResolution = function (str) {
</div>
<div id="workMonitorContent" class="innerContent">
</div></script></head><body><nav class="navbar" style="display: none"><div class="primary"><div class="navlogo"><a class="logo big" href="#"><img class="arangodbLogo" src="img/arangodb_logo_big.png"></a> <a class="logo small" href="#"><img class="arangodbLogo" src="img/arangodb_logo_small.png"></a> <a class="version"><span>VERSION:</span><span id="currentVersion"></span></a></div><div class="statmenu" id="statisticBar"></div><div class="navmenu" id="navigationBar"></div></div></nav><div id="modalPlaceholder"></div><div class="bodyWrapper" style="display: none"><div class="centralRow"><div id="navbar2" class="navbarWrapper secondary"><div class="subnavmenu" id="subNavigationBar"></div></div><div class="resizecontainer contentWrapper"><div id="loadingScreen" class="loadingScreen" style="display: none"><i class="fa fa-circle-o-notch fa-spin fa-3x fa-fw margin-bottom"></i> <span class="sr-only">Loading...</span></div><div id="content" class="centralContent"></div><footer class="footer"><div id="footerBar"></div></footer></div></div></div><div id="progressPlaceholder" style="display:none"></div><div id="spotlightPlaceholder" style="display:none"></div><div id="offlinePlaceholder" style="display:none"><div class="offline-div"><div class="pure-u"><div class="pure-u-1-4"></div><div class="pure-u-1-2 offline-window"><div class="offline-header"><h3>You have been disconnected from the server</h3></div><div class="offline-body"><p>The connection to the server has been lost. The server may be under heavy load.</p><p>Trying to reconnect in <span id="offlineSeconds">10</span> seconds.</p><p class="animation_state"><span><button class="button-success">Reconnect now</button></span></p></div></div><div class="pure-u-1-4"></div></div></div></div><div class="arangoFrame" style=""><div class="outerDiv"><div class="innerDiv"></div></div></div><script src="libs.js?version=1464698657958"></script><script src="app.js?version=1464698657958"></script></body></html>
</div></script></head><body><nav class="navbar" style="display: none"><div class="primary"><div class="navlogo"><a class="logo big" href="#"><img class="arangodbLogo" src="img/arangodb_logo_big.png"></a> <a class="logo small" href="#"><img class="arangodbLogo" src="img/arangodb_logo_small.png"></a> <a class="version"><span>VERSION:</span><span id="currentVersion"></span></a></div><div class="statmenu" id="statisticBar"></div><div class="navmenu" id="navigationBar"></div></div></nav><div id="modalPlaceholder"></div><div class="bodyWrapper" style="display: none"><div class="centralRow"><div id="navbar2" class="navbarWrapper secondary"><div class="subnavmenu" id="subNavigationBar"></div></div><div class="resizecontainer contentWrapper"><div id="loadingScreen" class="loadingScreen" style="display: none"><i class="fa fa-circle-o-notch fa-spin fa-3x fa-fw margin-bottom"></i> <span class="sr-only">Loading...</span></div><div id="content" class="centralContent"></div><footer class="footer"><div id="footerBar"></div></footer></div></div></div><div id="progressPlaceholder" style="display:none"></div><div id="spotlightPlaceholder" style="display:none"></div><div id="offlinePlaceholder" style="display:none"><div class="offline-div"><div class="pure-u"><div class="pure-u-1-4"></div><div class="pure-u-1-2 offline-window"><div class="offline-header"><h3>You have been disconnected from the server</h3></div><div class="offline-body"><p>The connection to the server has been lost. The server may be under heavy load.</p><p>Trying to reconnect in <span id="offlineSeconds">10</span> seconds.</p><p class="animation_state"><span><button class="button-success">Reconnect now</button></span></p></div></div><div class="pure-u-1-4"></div></div></div></div><div class="arangoFrame" style=""><div class="outerDiv"><div class="innerDiv"></div></div></div><script src="libs.js?version=1464784832891"></script><script src="app.js?version=1464784832891"></script></body></html>

View File

@ -45,6 +45,14 @@
};
window.arangoHelper = {
getCurrentJwt: function() {
return localStorage.getItem("jwt");
},
setCurrentJwt: function(jwt) {
localStorage.setItem("jwt", jwt);
},
lastNotificationMessage: null,
CollectionTypes: {},

View File

@ -34,7 +34,8 @@ window.ArangoUsers = Backbone.Collection.extend({
login: function (username, password, callback) {
var self = this;
$.ajax("login", {
$.ajax({
url: arangoHelper.databaseUrl("/_open/auth"),
method: "POST",
data: JSON.stringify({
username: username,
@ -43,11 +44,24 @@ window.ArangoUsers = Backbone.Collection.extend({
dataType: "json"
}).success(
function (data) {
self.activeUser = data.user;
arangoHelper.setCurrentJwt(data.jwt);
var jwtParts = data.jwt.split('.');
if (!jwtParts[1]) {
throw new Error("Invalid JWT");
}
if (!window.atob) {
throw new Error("base64 support missing in browser");
}
var payload = JSON.parse(atob(jwtParts[1]));
self.activeUser = payload.preferred_username;
callback(false, self.activeUser);
}
).error(
function () {
arangoHelper.setCurrentJwt(null);
self.activeUser = null;
callback(true, null);
}
@ -59,7 +73,7 @@ window.ArangoUsers = Backbone.Collection.extend({
},
logout: function () {
$.ajax("logout", {method:"POST"});
arangoHelper.setCurrentJwt(null);
this.activeUser = null;
this.reset();
window.App.navigate("");

View File

@ -5,6 +5,13 @@
"use strict";
// We have to start the app only in production mode, not in test mode
if (!window.hasOwnProperty("TEST_BUILD")) {
$(document).ajaxSend(function(event, jqxhr, settings) {
var currentJwt = window.arangoHelper.getCurrentJwt();
if (currentJwt) {
jqxhr.setRequestHeader("Authorization", "bearer " + currentJwt);
}
});
$(document).ready(function() {
window.App = new window.Router();
Backbone.history.start();

View File

@ -396,7 +396,7 @@ function computeStatisticsLong (attrs, clusterId) {
router.use((req, res, next) => {
if (global.AUTHENTICATION_ENABLED()) {
if (!req.session.uid) {
if (!req.user) {
throw new httperr.Unauthorized();
}
}

View File

@ -2247,7 +2247,8 @@ testFuncs.authentication = function(options) {
print(CYAN + "Authentication tests..." + RESET);
let instanceInfo = startInstance("tcp", options, {
"server.authentication": "true"
"server.authentication": "true",
"server.jwt-secret": "haxxmann",
}, "authentication");
if (instanceInfo === false) {

View File

@ -31,12 +31,7 @@ var internal = require("internal");
var arangodb = require("@arangodb");
var arangosh = require("@arangodb/arangosh");
////////////////////////////////////////////////////////////////////////////////
/// @brief creates a new user
////////////////////////////////////////////////////////////////////////////////
// creates a new user
exports.save = function (user, passwd, active, extra, changePassword) {
var db = internal.db;
@ -63,10 +58,7 @@ exports.save = function (user, passwd, active, extra, changePassword) {
return arangosh.checkRequestResult(requestResult);
};
////////////////////////////////////////////////////////////////////////////////
/// @brief replaces an existing user
////////////////////////////////////////////////////////////////////////////////
// replaces an existing user
exports.replace = function (user, passwd, active, extra, changePassword) {
var db = internal.db;
@ -82,10 +74,7 @@ exports.replace = function (user, passwd, active, extra, changePassword) {
return arangosh.checkRequestResult(requestResult);
};
////////////////////////////////////////////////////////////////////////////////
/// @brief updates an existing user
////////////////////////////////////////////////////////////////////////////////
// updates an existing user
exports.update = function (user, passwd, active, extra, changePassword) {
var db = internal.db;
@ -112,10 +101,7 @@ exports.update = function (user, passwd, active, extra, changePassword) {
return arangosh.checkRequestResult(requestResult);
};
////////////////////////////////////////////////////////////////////////////////
/// @brief deletes an existing user
////////////////////////////////////////////////////////////////////////////////
// deletes an existing user
exports.remove = function (user) {
var db = internal.db;
@ -125,10 +111,7 @@ exports.remove = function (user) {
arangosh.checkRequestResult(requestResult);
};
////////////////////////////////////////////////////////////////////////////////
/// @brief gets an existing user
////////////////////////////////////////////////////////////////////////////////
// gets an existing user
exports.document = function (user) {
var db = internal.db;
@ -138,10 +121,7 @@ exports.document = function (user) {
return arangosh.checkRequestResult(requestResult);
};
////////////////////////////////////////////////////////////////////////////////
/// @brief checks whether a combination of username / password is valid.
////////////////////////////////////////////////////////////////////////////////
// checks whether a combination of username / password is valid.
exports.isValid = function (user, password) {
var db = internal.db;
@ -161,10 +141,7 @@ exports.isValid = function (user, password) {
return requestResult.result;
};
////////////////////////////////////////////////////////////////////////////////
/// @brief gets all existing users
////////////////////////////////////////////////////////////////////////////////
// gets all existing users
exports.all = function () {
var db = internal.db;
@ -174,10 +151,7 @@ exports.all = function () {
return arangosh.checkRequestResult(requestResult).result;
};
////////////////////////////////////////////////////////////////////////////////
/// @brief reloads the user authentication data
////////////////////////////////////////////////////////////////////////////////
// reloads the user authentication data
exports.reload = function () {
var db = internal.db;

View File

@ -32,7 +32,10 @@ var jsunity = require("jsunity");
var arango = require("@arangodb").arango;
var db = require("internal").db;
var users = require("@arangodb/users");
var request = require('@arangodb/request');
var crypto = require('@arangodb/crypto');
var expect = require('expect.js');
var print = require('internal').print;
////////////////////////////////////////////////////////////////////////////////
/// @brief test suite
@ -40,6 +43,12 @@ var users = require("@arangodb/users");
function AuthSuite () {
'use strict';
var baseUrl = function () {
return arango.getEndpoint().replace(/^tcp:/, 'http:').replace(/^ssl:/, 'https:');
}
const jwtSecret = 'haxxmann';
return {
////////////////////////////////////////////////////////////////////////////////
@ -82,18 +91,21 @@ function AuthSuite () {
assertTrue(db._collections().length > 0);
// double check with wrong passwords
let isBroken;
isBroken = true;
try {
arango.reconnect(arango.getEndpoint(), db._name(), "hackers@arangodb.com", "foobar2");
fail();
}
catch (err1) {
isBroken = false;
}
isBroken = true;
try {
arango.reconnect(arango.getEndpoint(), db._name(), "hackers@arangodb.com", "");
fail();
}
catch (err2) {
isBroken = false;
}
},
@ -111,11 +123,13 @@ function AuthSuite () {
assertTrue(db._collections().length > 0);
// double check with wrong password
let isBroken;
isBroken = true;
try {
arango.reconnect(arango.getEndpoint(), db._name(), "hackers@arangodb.com", "foobar");
fail();
}
catch (err1) {
isBroken = false;
}
},
@ -133,25 +147,41 @@ function AuthSuite () {
assertTrue(db._collections().length > 0);
// double check with wrong passwords
let isBroken;
isBroken = true;
try {
arango.reconnect(arango.getEndpoint(), db._name(), "hackers@arangodb.com", "Foobar");
fail();
console.error("HASSMANN HIHI");
assertTrue(db._collections().length > 0);
}
catch (err1) {
console.error("HASSMANN");
isBroken = false;
}
if (isBroken) {
throw new Error("Wurst");
}
isBroken = true;
try {
arango.reconnect(arango.getEndpoint(), db._name(), "hackers@arangodb.com", "foobar");
fail();
}
catch (err2) {
isBroken = false;
}
try {
arango.reconnect(arango.getEndpoint(), db._name(), "hackers@arangodb.com", "FOOBAR");
if (isBroken) {
fail();
}
isBroken = true;
try {
arango.reconnect(arango.getEndpoint(), db._name(), "hackers@arangodb.com", "FOOBAR");
}
catch (err3) {
isBroken = false;
}
if (isBroken) {
fail();
}
},
@ -169,25 +199,38 @@ function AuthSuite () {
assertTrue(db._collections().length > 0);
// double check with wrong passwords
let isBroken;
isBroken = true;
try {
arango.reconnect(arango.getEndpoint(), db._name(), "hackers@arangodb.com", "fuxx");
fail();
}
catch (err1) {
isBroken = false;
}
if (isBroken) {
fail();
}
isBroken = true;
try {
arango.reconnect(arango.getEndpoint(), db._name(), "hackers@arangodb.com", "bar");
fail();
}
catch (err2) {
isBroken = false;
}
try {
arango.reconnect(arango.getEndpoint(), db._name(), "hackers@arangodb.com", "");
if (isBroken) {
fail();
}
isBroken = true;
try {
arango.reconnect(arango.getEndpoint(), db._name(), "hackers@arangodb.com", "");
}
catch (err3) {
isBroken = false;
}
if (isBroken) {
fail();
}
},
@ -205,28 +248,215 @@ function AuthSuite () {
assertTrue(db._collections().length > 0);
// double check with wrong passwords
let isBroken;
isBroken = true;
try {
arango.reconnect(arango.getEndpoint(), db._name(), "hackers@arangodb.com", "foobar");
fail();
}
catch (err1) {
isBroken = false;
}
if (isBroken) {
fail();
}
isBroken = true;
try {
arango.reconnect(arango.getEndpoint(), db._name(), "hackers@arangodb.com", "\\abc'def: x-a");
fail();
}
catch (err2) {
isBroken = false;
}
try {
arango.reconnect(arango.getEndpoint(), db._name(), "hackers@arangodb.com", "");
if (isBroken) {
fail();
}
catch (err3) {
}
}
isBroken = true;
try {
arango.reconnect(arango.getEndpoint(), db._name(), "hackers@arangodb.com", "");
}
catch (err3) {
isBroken = false;
}
if (isBroken) {
fail();
}
},
testAuthOpen: function() {
var res = request(baseUrl() + "/_open/auth");
expect(res).to.be.a(request.Response);
// mop: GET is an unsupported method, but it is skipping auth
expect(res).to.have.property('statusCode', 405);
},
testAuth: function() {
var res = request.post({
url: baseUrl() + "/_open/auth",
body: JSON.stringify({"username": "root", "password": ""})
});
expect(res).to.be.a(request.Response);
expect(res).to.have.property('statusCode', 200);
expect(res.body).to.be.an('string');
var obj = JSON.parse(res.body);
expect(obj).to.have.property('jwt');
expect(obj).to.have.property('must_change_password');
expect(obj.jwt).to.be.a('string');
expect(obj.jwt.split('.').length).to.be(3);
expect(obj.must_change_password).to.be.a('boolean');
},
testAuthNewUser: function() {
users.save("hackers@arangodb.com", "foobar");
users.reload();
var res = request.post({
url: baseUrl() + "/_open/auth",
body: JSON.stringify({"username": "hackers@arangodb.com", "password": "foobar"})
});
expect(res).to.be.a(request.Response);
expect(res).to.have.property('statusCode', 200);
expect(res.body).to.be.an('string');
var obj = JSON.parse(res.body);
expect(obj).to.have.property('jwt');
expect(obj).to.have.property('must_change_password');
expect(obj.jwt).to.be.a('string');
expect(obj.jwt.split('.').length).to.be(3);
expect(obj.must_change_password).to.be.a('boolean');
},
testAuthNewWrongPassword: function() {
users.save("hackers@arangodb.com", "foobarJAJA");
users.reload();
var res = request.post({
url: baseUrl() + "/_open/auth",
body: JSON.stringify({"username": "hackers@arangodb.com", "password": "foobar"})
});
expect(res).to.be.a(request.Response);
expect(res).to.have.property('statusCode', 401);
},
testAuthNoPassword: function() {
var res = request.post({
url: baseUrl() + "/_open/auth",
body: JSON.stringify({"username": "hackers@arangodb.com", "passwordaa": "foobar"}),
});
expect(res).to.be.a(request.Response);
expect(res).to.have.property('statusCode', 400);
},
testAuthNoUsername: function() {
var res = request.post({
url: baseUrl() + "/_open/auth",
body: JSON.stringify({"usern": "hackers@arangodb.com", "password": "foobar"}),
});
expect(res).to.be.a(request.Response);
expect(res).to.have.property('statusCode', 400);
},
testAuthRequired: function() {
var res = request.get(baseUrl() + "/_api/version");
expect(res).to.be.a(request.Response);
expect(res).to.have.property('statusCode', 401);
},
testFullAuthWorkflow: function() {
var res = request.post({
url: baseUrl() + "/_open/auth",
body: JSON.stringify({"username": "root", "password": ""}),
});
var jwt = JSON.parse(res.body).jwt;
var res = request.get({
url: baseUrl() + "/_api/version",
auth: {
bearer: jwt,
}
});
expect(res).to.be.a(request.Response);
expect(res).to.have.property('statusCode', 200);
},
testViaJS: function() {
var jwt = crypto.jwtEncode(jwtSecret, {"iss": "arangodb", "exp": Math.floor(Date.now() / 1000) + 3600}, 'HS256');
var res = request.get({
url: baseUrl() + "/_api/version",
auth: {
bearer: jwt,
}
})
expect(res).to.be.a(request.Response);
expect(res).to.have.property('statusCode', 200);
},
testNoneAlgDisabled: function() {
var jwt = (new Buffer(JSON.stringify({"typ": "JWT","alg": "none"})).toString('base64')) + "." + (new Buffer(JSON.stringify({"iss": "arangodb"})).toString('base64'));
// not supported
var res = request.get({
url: baseUrl() + "/_api/version",
auth: {
bearer: jwt,
}
})
expect(res).to.be.a(request.Response);
expect(res).to.have.property('statusCode', 401);
},
testIssRequired: function() {
var jwt = crypto.jwtEncode(jwtSecret, {"exp": Math.floor(Date.now() / 1000) + 3600 }, 'HS256');
// not supported
var res = request.get({
url: baseUrl() + "/_api/version",
auth: {
bearer: jwt,
}
})
expect(res).to.be.a(request.Response);
expect(res).to.have.property('statusCode', 401);
},
testIssArangodb: function() {
var jwt = crypto.jwtEncode(jwtSecret, {"iss": "arangodbaaa", "exp": Math.floor(Date.now() / 1000) + 3600 }, 'HS256');
// not supported
var res = request.get({
url: baseUrl() + "/_api/version",
auth: {
bearer: jwt,
}
})
expect(res).to.be.a(request.Response);
expect(res).to.have.property('statusCode', 401);
},
testExpOptional: function() {
var jwt = crypto.jwtEncode(jwtSecret, {"iss": "arangodb" }, 'HS256');
// not supported
var res = request.get({
url: baseUrl() + "/_api/version",
auth: {
bearer: jwt,
}
})
expect(res).to.be.a(request.Response);
expect(res).to.have.property('statusCode', 200);
},
testExp: function() {
var jwt = crypto.jwtEncode(jwtSecret, {"iss": "arangodbaaa", "exp": Math.floor(Date.now() / 1000) - 1000 }, 'HS256');
// not supported
var res = request.get({
url: baseUrl() + "/_api/version",
auth: {
bearer: jwt,
}
})
expect(res).to.be.a(request.Response);
expect(res).to.have.property('statusCode', 401);
},
};
}

View File

@ -1,5 +1,5 @@
/*jshint expr: true */
/*eslint no-unused-expressions: false */
/*eslint no-unused-expressions: 0 */
/*global describe, it, beforeEach, afterEach */
'use strict';
const internal = require('internal');

View File

@ -1,4 +1,4 @@
/*eslint camelcase:false */
/*eslint camelcase: 0 */
'use strict';
////////////////////////////////////////////////////////////////////////////////

View File

@ -35,6 +35,7 @@ const crypto = require('@arangodb/crypto');
module.exports = class SyntheticRequest {
constructor(req, context) {
this.user = req.user;
this._url = parseUrl(req.url);
this._raw = req;
this.context = context;

View File

@ -146,6 +146,7 @@ exports.save = function(username, password, active, userData, changePassword) {
const data = {
user: username,
databases: {},
configData: {},
userData: userData || {},
authData: {
simple: hashPassword(password),
@ -196,6 +197,7 @@ exports.replace = function(username, password, active, userData, changePassword)
const data = {
user: username,
databases: user.databases,
configData: user.configData,
userData: userData || {},
authData: {
simple: hashPassword(password),
@ -457,6 +459,9 @@ exports.grantDatabase = function(username, database, type) {
users.update(user, { databases: databases });
// not exports.reload() as this is an abstract method...
require("@arangodb/users").reload();
return databases;
};
@ -483,6 +488,9 @@ exports.revokeDatabase = function(username, database) {
users.update(user, { databases: databases }, false, false);
// not exports.reload() as this is an abstract method...
require("@arangodb/users").reload();
delete databases[database];
return databases;
};

View File

@ -1,5 +1,5 @@
/*jshint -W083 */
/*eslint no-loop-func: false */
/*eslint no-loop-func: 0 */
/*global describe, it, beforeEach, afterEach */
'use strict';

View File

@ -30,7 +30,6 @@ std::string const StaticStrings::Binary("binary");
std::string const StaticStrings::Empty("");
std::string const StaticStrings::N1800("1800");
// system attribute names
std::string const StaticStrings::IdString("_id");
std::string const StaticStrings::KeyString("_key");
@ -75,7 +74,7 @@ std::string const StaticStrings::Origin("origin");
std::string const StaticStrings::Queue("x-arango-queue");
std::string const StaticStrings::Server("server");
std::string const StaticStrings::StartThread("x-arango-start-thread");
std::string const StaticStrings::WwwAuthenticate("www-authenticate");
// mime types
std::string const StaticStrings::MimeTypeJson("application/json; charset=utf-8");

View File

@ -80,6 +80,7 @@ class StaticStrings {
static std::string const Queue;
static std::string const Server;
static std::string const StartThread;
static std::string const WwwAuthenticate;
// mime types
static std::string const MimeTypeJson;

View File

@ -256,8 +256,7 @@ std::string sslHMAC(char const* key, size_t keyLength, char const* message,
HMAC(evp_md, key, (int)keyLength, (const unsigned char*)message, messageLen,
md, &md_len);
// return value as hex
std::string result = StringUtils::encodeHex(std::string((char*)md, md_len));
std::string result = std::string((char*)md, md_len);
TRI_SystemFree(md);
return result;

View File

@ -3154,8 +3154,8 @@ static void JS_HMAC(v8::FunctionCallbackInfo<v8::Value> const& args) {
}
}
std::string result = SslInterface::sslHMAC(
key.c_str(), key.size(), message.c_str(), message.size(), al);
std::string result = StringUtils::encodeHex(SslInterface::sslHMAC(
key.c_str(), key.size(), message.c_str(), message.size(), al));
TRI_V8_RETURN_STD_STRING(result);
TRI_V8_TRY_CATCH_END
}