mirror of https://gitee.com/bigwinds/arangodb
WIP
This commit is contained in:
commit
22b454800d
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
Binary file not shown.
|
@ -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>
|
Binary file not shown.
|
@ -45,6 +45,14 @@
|
|||
};
|
||||
|
||||
window.arangoHelper = {
|
||||
getCurrentJwt: function() {
|
||||
return localStorage.getItem("jwt");
|
||||
},
|
||||
|
||||
setCurrentJwt: function(jwt) {
|
||||
localStorage.setItem("jwt", jwt);
|
||||
},
|
||||
|
||||
lastNotificationMessage: null,
|
||||
|
||||
CollectionTypes: {},
|
||||
|
|
|
@ -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("");
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
/*eslint camelcase:false */
|
||||
/*eslint camelcase: 0 */
|
||||
'use strict';
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*jshint -W083 */
|
||||
/*eslint no-loop-func: false */
|
||||
/*eslint no-loop-func: 0 */
|
||||
/*global describe, it, beforeEach, afterEach */
|
||||
'use strict';
|
||||
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue