8000 Follow-up for APM-79 by jsteemann · Pull Request #14584 · arangodb/arangodb · GitHub
[go: up one dir, main page]

Skip to content

Follow-up for APM-79 #14584

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Aug 4, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
devel
-----

* Automatically extend web UI sessions while they are still active.
The web UI can now call a backend route to renew its JWT, so there will not
be any rude logouts in the middle of an active session.

Active web UI sessions (here: sessions with user activity within the last
90 minutes) will automatically renew their JWT if they get close to the
JWT expiry date.

* Reduce memory usage for in-memory revision trees. Previously, a revision
tree instance for a non-empty collection/shard was using 4 MB of memory
when uncompressed. Trees that were unused for a while were compressed on
Expand Down
1 change: 1 addition & 0 deletions arangod/Auth/TokenCache.h
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ class TokenCache {
bool authenticated() const { return _authenticated; }
void authenticated(bool value) { _authenticated = value; }
void setExpiry(double expiry) { _expiry = expiry; }
double expiry() const noexcept { return _expiry; }
bool expired() const { return _expiry != 0 && _expiry < TRI_microtime(); }
std::vector<std::string> const& allowedPaths() const { return _allowedPaths; }

Expand Down
4 changes: 3 additions & 1 deletion arangod/Auth/UserManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,9 @@ static bool inline IsRole(std::string const& name) {

#ifndef USE_ENTERPRISE
auth::UserManager::UserManager(application_features::ApplicationServer& server)
: _server(server), _globalVersion(1), _internalVersion(0) {}
: _server(server),
_globalVersion(1),
_internalVersion(0) {}
#else
auth::UserManager::UserManager(application_features::ApplicationServer& server)
: _server(server),
Expand Down
1 change: 0 additions & 1 deletion arangod/Auth/UserManager.h
Original file line number Diff line number Diff line change
Expand Up @@ -184,4 +184,3 @@ class UserManager {
};
} // namespace auth
} // namespace arangodb

16 changes: 8 additions & 8 deletions arangod/GeneralServer/AuthenticationFeature.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -191,22 +191,22 @@ void AuthenticationFeature::prepare() {
if (ServerState::isSingleServer(role) || ServerState::isCoordinator(role)) {
#if USE_ENTERPRISE
if (server().getFeature<LdapFeature>().isEnabled()) {
_userManager.reset(
new auth::UserManager(server(), std::make_unique<LdapAuthenticationHandler>(
server().getFeature<LdapFeature>())));
} else {
_userManager.reset(new auth::UserManager(server()));
_userManager = std::make_unique<auth::UserManager>(
server(), std::make_unique<LdapAuthenticationHandler>(server().getFeature<LdapFeature>()));
}
#else
_userManager.reset(new auth::UserManager(server()));
#endif
if (_userManager == nullptr) {
_userManager = std::make_unique<auth::UserManager>(server());
}

TRI_ASSERT(_userManager != nullptr);
} else {
LOG_TOPIC("713c0", DEBUG, Logger::AUTHENTICATION)
<< "Not creating user manager";
}

TRI_ASSERT(_authCache == nullptr);
_authCache.reset(new auth::TokenCache(_userManager.get(), _authenticationTimeout));
_authCache = std::make_unique<auth::TokenCache>(_userManager.get(), _authenticationTimeout);

if (_jwtSecretProgramOption.empty()) {
LOG_TOPIC("43396", INFO, Logger::AUTHENTICATION)
Expand Down
5 changes: 3 additions & 2 deletions arangod/GeneralServer/CommTask.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -737,13 +737,13 @@ auth::TokenCache::Entry CommTask::checkAuthHeader(GeneralRequest& req) {
return auth::TokenCache::Entry::Superuser();
}

std::string::size_type methodPos = authStr.find_first_of(' ');
std::string::size_type methodPos = authStr.find(' ');
if (methodPos == std::string::npos) {
events::UnknownAuthenticationMethod(req);
return auth::TokenCache::Entry::Unauthenticated();
}

// skip over authentication method
// skip over authentication method and following whitespace
char const* auth = authStr.c_str() + methodPos;
while (*auth == ' ') {
++auth;
Expand All @@ -770,6 +770,7 @@ auth::TokenCache::Entry CommTask::checkAuthHeader(GeneralRequest& req) {

auto authToken = this->_auth->tokenCache().checkAuthentication(authMethod, auth);
req.setAuthenticated(authToken.authenticated());
req.setTokenExpiry(authToken.expiry());
req.setUser(authToken.username()); // do copy here, so that we do not invalidate the member
if (authToken.authenticated()) {
events::Authenticated(req, authMethod);
Expand Down
41 changes: 35 additions & 6 deletions arangod/RestHandler/RestAuthHandler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,40 @@ RestStatus RestAuthHandler::execute() {
generateError(rest::ResponseCode::METHOD_NOT_ALLOWED, TRI_ERROR_HTTP_METHOD_NOT_ALLOWED);
return RestStatus::DONE;
}

auth::UserManager* um = AuthenticationFeature::instance()->userManager();
8000 if (um == nullptr) {
std::string msg = "This server does not support users";
LOG_TOPIC("2e7d4", WARN, Logger::AUTHENTICATION) << msg;
generateError(rest::ResponseCode::UNAUTHORIZED, TRI_ERROR_HTTP_UNAUTHORIZED, msg);
return RestStatus::DONE;
}

auto const& suffixes = _request->suffixes();

if (suffixes.size() == 1 && suffixes[0] == "renew") {
// JWT token renew request
if (!_request->authenticated() ||
_request->user().empty() ||
_request->authenticationMethod() != arangodb::rest::AuthenticationMethod::JWT) {
generateError(rest::ResponseCode::NOT_FOUND, TRI_ERROR_USER_NOT_FOUND);
} else {
VPackBuilder resultBuilder;
{
VPackObjectBuilder b(&resultBuilder);
// only return a new token if the current token is about to expire
if (_request->tokenExpiry() > 0.0 &&
_request->tokenExpiry() - TRI_microtime() < 150.0) {
resultBuilder.add("jwt", VPackValue(generateJwt(_request->user())));
}
// otherwise we will send an empty body back. callers must handle
// this case!
}

generateDocument(resultBuilder.slice(), true, &VPackOptions::Defaults);
}
return RestStatus::DONE;
}

bool parseSuccess = false;
VPackSlice slice = this->parseVPackBody(parseSuccess);
Expand Down Expand Up @@ -84,12 +118,7 @@ RestStatus RestAuthHandler::execute() {
}
});

auth::UserManager* um = AuthenticationFeature::instance()->userManager();
if (um == nullptr) {
std::string msg = "This server does not support users";
LOG_TOPIC("2e7d4", ERR, Logger::AUTHENTICATION) << msg;
generateError(rest::ResponseCode::UNAUTHORIZED, TRI_ERROR_HTTP_UNAUTHORIZED, msg);
} else if (um->checkPassword(username, password)) {
if (um->checkPassword(username, password)) {
VPackBuilder resultBuilder;
{
VPackObjectBuilder b(&resultBuilder);
Expand Down
9 changes: 8 additions & 1 deletion arangod/V8Server/v8-vocbase.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2272,13 +2272,20 @@ void TRI_InitV8VocBridge(v8::Isolate* isolate, v8::Handle<v8::Context> context,
vocbase.server().getFeature<ClusterFeature>().maxNumberOfShards()), v8::PropertyAttribute(v8::ReadOnly | v8::DontEnum))
.FromMaybe(false); // ignore result

// max number of shards
// force one shard collections?
context->Global()
->DefineOwnProperty(TRI_IGETC,
TRI_V8_ASCII_STRING(isolate, "FORCE_ONE_SHARD"),
v8::Boolean::New(isolate,
vocbase.server().getFeature<ClusterFeature>().forceOneShard()), v8::PropertyAttribute(v8::ReadOnly | v8::DontEnum))
.FromMaybe(false); // ignore result

// session timeout (used by web UI)
context->Global()
->DefineOwnProperty(TRI_IGETC,
TRI_V8_ASCII_STRING(isolate, "SESSION_TIMEOUT"),
v8::Number::New(isolate, static_cast<double>(AuthenticationFeature::instance()->sessionTimeout())), v8::PropertyAttribute(v8::ReadOnly | v8::DontEnum))
.FromMaybe(false); // ignore result

// a thread-global variable that will contain the AQL module.
// do not remove this, otherwise AQL queries will break
Expand Down
1 change: 1 addition & 0 deletions js/apps/system/_admin/aardvark/APP/aardvark.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ router.get('/config.js', function (req, res) {
defaultReplicationFactor: internal.defaultReplicationFactor,
maxNumberOfShards: internal.maxNumberOfShards,
forceOneShard: internal.forceOneShard,
sessionTimeout: internal.sessionTimeout,
showMaintenanceStatus: true
})}`
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/* jshint unused: false */
/* global Noty, Blob, window, Joi, sigma, $, tippy, document, _, arangoHelper, frontendConfig, arangoHelper, sessionStorage, localStorage, XMLHttpRequest */
/* global Noty, Blob, window, atob, Joi, sigma, $, tippy, document, _, arangoHelper, frontendConfig, sessionStorage, localStorage, XMLHttpRequest */

(function () {
'use strict';
Expand Down Expand Up @@ -124,6 +124,65 @@
});
},

lastActivity: function () {
// return timestamp of last activity (only seconds part)
return sessionStorage.getItem('lastActivity') || 0;
},

noteActivity: function () {
// note timestamp of last activity (only seconds part)
sessionStorage.setItem('lastActivity', Date.now() / 1000);
},

renewJwt: function (callback) {
if (!window.atob) {
return;
}
var self = this;
var currentUser = self.getCurrentJwtUsername();
if (currentUser === undefined || currentUser === "") {
return;
}

$.ajax({
cache: false,
type: 'POST',
url: self.databaseUrl('/_open/auth/renew'),
data: JSON.stringify({ username: currentUser }),
contentType: 'application/json',
processData: false,
success: function (data) {
var updated = false;
if (data.jwt) {
try {
var jwtParts = data.jwt.split('.');
if (!jwtParts[1]) {
throw "invalid token!";
8000 }
var payload = JSON.parse(atob(jwtParts[1]));
if (payload.preferred_username === currentUser) {
self.setCurrentJwt(data.jwt, currentUser);
updated = true;
}
} catch (err) {
}
}
if (updated) {
// success
callback();
}
},
error: function (data) {
// this function is triggered by a non-interactive
// background task. if it fails for whatever reason,
// we don't report this error.
// the worst thing that can happen is that the JWT
// is not renewed and thus the user eventually gets
// logged out
}
});
},

getCoordinatorShortName: function (id) {
var shortName;
if (window.clusterHealth) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,32 +19,12 @@ window.Users = Backbone.Model.extend({
},

url: function () {
if (this.isNew()) {
return arangoHelper.databaseUrl('/_api/user');
}
if (this.get('user') !== '') {
if (!this.isNew() && this.get('user') !== '') {
return arangoHelper.databaseUrl('/_api/user/' + encodeURIComponent(this.get('user')));
}
return arangoHelper.databaseUrl('/_api/user');
},

checkPassword: function (passwd, callback) {
$.ajax({
cache: false,
type: 'POST',
url: arangoHelper.databaseUrl('/_api/user/' + encodeURIComponent(this.get('user'))),
data: JSON.stringify({ passwd: passwd }),
contentType: 'application/json',
processData: false,
success: function (data) {
callback(false, data);
},
error: function (data) {
callback(true, data);
}
});
},


setPassword: function (passwd) {
$.ajax({
cache: false,
Expand Down
View file Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
window.App.handleResize();
});

// create only this one global event listener
// create only the following global event listeners
$(document).click(function (e) {
e.stopPropagation();

Expand All @@ -41,14 +41,21 @@
$('.subBarDropdown').hide();
}
}

// also note that the web UI was actively used
arangoHelper.noteActivity();
});

$('body').on('keyup', function (e) {
// hide modal dialogs when pressing ESC
if (e.keyCode === 27) {
if (window.modalView) {
window.modalView.hide();
}
}

// also note that the web UI was actively used
arangoHelper.noteActivity();
});
}
}());
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@
timer: 15000,
lap: 0,
timerFunction: null,
// last time of JWT renewal call, in seconds.
// this is initialized to the current time so we don't
// fire off a renewal request at the very beginning.
lastTokenRenewal: Date.now() / 1000,

events: {
'click .footer-center p': 'showShortcutModal'
Expand Down Expand Up @@ -43,6 +47,39 @@
});
}
}, 1000);

// track an activity once when we initialize this view
arangoHelper.noteActivity();

window.setInterval(function () {
if (self.isOffline) {
// only try to renew token if we are still online
return;
}

var now = Date.now() / 1000;
var lastActivity = arangoHelper.lastActivity();

if (lastActivity > 0 && (now - lastActivity) > 90 * 60) {
// don't make an attempt to renew the token if last
// user activity is longer than 90 minutes ago
return;
}

// to save some superfluous HTTP requests to the server,
// try to renew only if session time is x% or more over
var frac = (frontendConfig.sessionTimeout >= 1800) ? 0.95 : 0.8;
if (now - self.lastTokenRenewal < frontendConfig.sessionTimeout * frac) {
return;
}

arangoHelper.renewJwt(function() {
// successful renewal of token. now store last renewal time so
// that we later only renew if the session is again about to
// expire
self.lastTokenRenewal = now;
});
}, 15 * 1000);
},

template: templateEngine.createTemplate('footerView.ejs'),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -296,18 +296,6 @@
}
},

/*
breadcrumb: function (name) {

if (window.location.hash.split('/')[0] !== '#collection') {
$('#subNavigationBar .breadcrumb').html(
'<a class="activeBread" href="#' + name + '">' + name + '</a>'
)
}

},
*/

selectMenuItem: function (menuItem, noMenuEntry) {
if (menuItem === undefined) {
menuItem = window.location.hash.split('/')[0];
Expand Down
Loading
0