diff --git a/Changes.md b/Changes.md index 01a3544..b09283e 100644 --- a/Changes.md +++ b/Changes.md @@ -4,6 +4,7 @@ * Fix SAML regression * Fix Captcha rule bug +* Add admin global logout ## v2.20.0-3 _(2024-10-25)_ * Add ReCaptcha v3 diff --git a/full/Dockerfile b/full/Dockerfile index 95ad4ae..6de4aac 100644 --- a/full/Dockerfile +++ b/full/Dockerfile @@ -24,6 +24,7 @@ RUN \ echo patch ignorepollers.patch && patch -p1 < ignorepollers.patch && \ echo patch fixedLogout.patch && patch -p1 < fixedLogout.patch && \ echo patch matrix-token-exchange.patch && patch -p1 < matrix-token-exchange.patch && \ + echo patch globalLogout.patch && patch -p1 < globalLogout.patch && \ rm -f *.patch && \ LLNG_DEFAULTCONFFILE=/etc/lemonldap-ng/lemonldap-ng.ini \ perl -MLemonldap::NG::Manager::Build -e 'Lemonldap::NG::Manager::Build->run( \ diff --git a/full/globalLogout.patch b/full/globalLogout.patch new file mode 100644 index 0000000..7437112 --- /dev/null +++ b/full/globalLogout.patch @@ -0,0 +1,77 @@ +diff --git a/usr/share/perl5/Lemonldap/NG/Manager/Sessions.pm b/usr/share/perl5/Lemonldap/NG/Manager/Sessions.pm +index edf6a5f19..89c7571d2 100644 +--- a/usr/share/perl5/Lemonldap/NG/Manager/Sessions.pm ++++ b/usr/share/perl5/Lemonldap/NG/Manager/Sessions.pm +@@ -40,6 +40,13 @@ sub init { + ['DELETE'] + ) + ++ # DELETE ALL SESSIONS FOR A USER ++ ->addRoute( ++ sessions => ++ { glogout => { ':sessionType' => { ':sessionId' => 'userLogout' } } }, ++ ['POST'] ++ ) ++ + # DELETE OIDC CONSENT + ->addRoute( + sessions => { +@@ -60,6 +67,58 @@ sub init { + return 1; + } + ++# ++# User logout ++# ++ ++sub userLogout { ++ my ( $self, $req ) = @_; ++ ++ my $mod = $self->getMod($req) ++ or return $self->sendError( $req, undef, 400 ); ++ my $id = $req->params('sessionId') ++ or return $self->sendError( $req, 'sessionId is missing', 400 ); ++ my $session = $self->getApacheSession( $mod, $id ); ++ ++ my $uidKey = Lemonldap::NG::Handler::Main->tsv->{whatToTrace}; ++ my $uid = $session->data->{$uidKey}; ++ ++ my $count = 0; ++ foreach my $storage (qw(oidcStorage sessionStorage)) { ++ my $storageModule = ++ Lemonldap::NG::Handler::Main->tsv->{"${storage}Module"}; ++ if ( defined $storageModule ) { ++ next if ( $storageModule eq "Apache::Session::Memcached" ); ++ my $opts = Lemonldap::NG::Handler::Main->tsv->{"${storage}Options"}; ++ $opts->{backend} = $storageModule; ++ my $sessions = ++ Lemonldap::NG::Common::Apache::Session->searchOn( $opts, $uidKey, ++ $uid ); ++ my @keys; ++ if ( $sessions and %$sessions ) { ++ @keys = keys %$sessions; ++ foreach my $sid (@keys) { ++ my $session = Lemonldap::NG::Common::Session->new( ++ storageModule => $storageModule, ++ storageModuleOptions => $opts, ++ cacheModule => Lemonldap::NG::Handler::Main->tsv ++ ->{sessionCacheModule}, ++ cacheModuleOptions => Lemonldap::NG::Handler::Main->tsv ++ ->{sessionCacheOptions}, ++ id => $sid, ++ ); ++ if ( $session->data ) { ++ $session->remove; ++ $count++; ++ } ++ } ++ } ++ } ++ } ++ Lemonldap::NG::Handler::PSGI::Main->localUnlog( $req, $id ); ++ return $self->sendJSONresponse( $req, { result => 1, count => $count } ); ++} ++ + ####################### + # II. CONSENT METHODS # + ####################### diff --git a/full/install/usr/share/lemonldap-ng/manager/htdocs/static/js/sessions.js b/full/install/usr/share/lemonldap-ng/manager/htdocs/static/js/sessions.js new file mode 100644 index 0000000..7cb8cb9 --- /dev/null +++ b/full/install/usr/share/lemonldap-ng/manager/htdocs/static/js/sessions.js @@ -0,0 +1,863 @@ +// Generated by CoffeeScript 2.7.0 +(function() { + /* + * Sessions explorer + */ + /* + * AngularJS application + */ + var categories, hiddenAttributes, llapp, max, menu, overScheme, schemes; + + // Max number of session to display (see overScheme) + max = 25; + + // Queries to do each type of display: each array item corresponds to the depth + // of opened nodes in the tree + schemes = { + _whatToTrace: [ + // First level: display 1 letter + function(t, + v) { + return `groupBy=substr(${t},1)`; + }, + // Second level (if no overScheme), display usernames + function(t, + v) { + return `${t}=${v}*&groupBy=${t}`; + }, + function(t, + v) { + return `${t}=${v}`; + } + ], + ipAddr: [ + function(t, + v) { + return `groupBy=net(${t},16,1)`; + }, + function(t, + v) { + if (!v.match(/:/)) { + v = v + '.'; + } + return `${t}=${v}*&groupBy=net(${t},32,2)`; + }, + function(t, + v) { + if (!v.match(/:/)) { + v = v + '.'; + } + return `${t}=${v}*&groupBy=net(${t},48,3)`; + }, + function(t, + v) { + if (!v.match(/:/)) { + v = v + '.'; + } + return `${t}=${v}*&groupBy=net(${t},128,4)`; + }, + function(t, + v) { + return `${t}=${v}&groupBy=_whatToTrace`; + }, + function(t, + v, + q) { + return q.replace(/\&groupBy.*$/, + '') + `&_whatToTrace=${v}`; + } + ], + _startTime: [ + function(t, + v) { + return `groupBy=substr(${t},8)`; + }, + function(t, + v) { + return `${t}=${v}*&groupBy=substr(${t},10)`; + }, + function(t, + v) { + return `${t}=${v}*&groupBy=substr(${t},11)`; + }, + function(t, + v) { + return `${t}=${v}*&groupBy=substr(${t},12)`; + }, + function(t, + v) { + return `${t}=${v}*&groupBy=_whatToTrace`; + }, + function(t, + v, + q) { + console.log(t); + console.log(v); + console.log(q); + return q.replace(/\&groupBy.*$/, + '') + `&_whatToTrace=${v}`; + } + ], + doubleIp: [ + function(t, + v) { + return t; + }, + function(t, + v) { + return `_whatToTrace=${v}&groupBy=ipAddr`; + }, + function(t, + v, + q) { + return q.replace(/\&groupBy.*$/, + '') + `&ipAddr=${v}`; + } + ], + _session_uid: [ + // First level: display 1 letter + function(t, + v) { + return `groupBy=substr(${t},1)`; + }, + // Second level (if no overScheme), display usernames + function(t, + v) { + return `${t}=${v}*&groupBy=${t}`; + }, + function(t, + v) { + return `${t}=${v}`; + } + ] + }; + + // When number of children nodes exceeds "max" value and if "overScheme." + // is available and does not return "null", a level is added. See + // "$scope.updateTree" method + overScheme = { + _whatToTrace: function(t, v, level, over) { + // "v.length > over" avoids a loop if one user opened more than "max" + // sessions + console.log('overScheme => level', level, 'over', over); + if (level === 1 && v.length > over) { + return `${t}=${v}*&groupBy=substr(${t},${level + over + 1})`; + } else { + return null; + } + }, + // Note: IPv4 only + ipAddr: function(t, v, level, over) { + console.log('overScheme => level', level, 'over', over); + if (level > 0 && level < 4 && !v.match(/^\d+\.\d/) && over < 2) { + return `${t}=${v}*&groupBy=net(${t},${16 * level + 4 * (over + 1)},${1 + level + over})`; + } else { + return null; + } + }, + _startTime: function(t, v, level, over) { + console.log('overScheme => level', level, 'over', over); + if (level > 3) { + return `${t}=${v}*&groupBy=substr(${t},${10 + level + over})`; + } else { + return null; + } + }, + _session_uid: function(t, v, level, over) { + console.log('overScheme => level', level, 'over', over); + if (level === 1 && v.length > over) { + return `${t}=${v}*&groupBy=substr(${t},${level + over + 1})`; + } else { + return null; + } + } + }; + + hiddenAttributes = '_password'; + + // Attributes to group in session display + categories = { + dateTitle: ['_utime', '_startTime', '_updateTime', '_lastAuthnUTime', '_lastSeen'], + connectionTitle: ['ipAddr', '_timezone', '_url'], + authenticationTitle: ['_session_id', '_user', '_password', 'authenticationLevel'], + modulesTitle: ['_auth', '_userDB', '_passwordDB', '_issuerDB', '_authChoice', '_authMulti', '_userDBMulti', '_2f'], + saml: ['_idp', '_idpConfKey', '_samlToken', '_lassoSessionDump', '_lassoIdentityDump'], + groups: ['groups', 'hGroups'], + ldap: ['dn'], + OpenIDConnect: ['_oidc_id_token', '_oidc_OP', '_oidc_access_token', '_oidc_refresh_token', '_oidc_access_token_eol', '_oidcConnectedRP', '_oidcConnectedRPIDs'], + sfaTitle: ['_2fDevices'], + oidcConsents: ['_oidcConsents'] + }; + + // Menu entries + menu = { + session: [ + { + title: 'deleteSession', + icon: 'trash' + }, + { + title: 'globalLogout', + icon: 'trash' + } + ], + home: [] + }; + + llapp = angular.module('llngSessionsExplorer', ['ui.tree', 'ui.bootstrap', 'llApp']); + + // Main controller + llapp.controller('SessionsExplorerCtrl', [ + '$scope', + '$translator', + '$location', + '$q', + '$http', + function($scope, + $translator, + $location, + $q, + $http) { + var autoId, + c, + pathEvent, + sessionType; + $scope.links = links; + $scope.menulinks = menulinks; + $scope.staticPrefix = staticPrefix; + $scope.scriptname = scriptname; + $scope.formPrefix = formPrefix; + $scope.impPrefix = impPrefix; + $scope.sessionTTL = sessionTTL; + $scope.availableLanguages = availableLanguages; + $scope.waiting = true; + $scope.showM = false; + $scope.showT = true; + $scope.data = []; + $scope.currentScope = null; + $scope.currentSession = null; + $scope.menu = menu; + // Import translations functions + $scope.translateP = $translator.translateP; + $scope.translate = $translator.translate; + $scope.translateTitle = function(node) { + return $translator.translateField(node, + 'title'); + }; + sessionType = 'global'; + // Handle menu items + $scope.menuClick = function(button) { + if (button.popup) { + window.open(button.popup); + } else { + if (!button.action) { + button.action = button.title; + } + switch (typeof button.action) { + case 'function': + button.action($scope.currentNode, + $scope); + break; + case 'string': + $scope[button.action](); + break; + default: + console.log(typeof button.action); + } + } + return $scope.showM = false; + }; + // SESSION MANAGEMENT + + // Delete RP Consent + $scope.deleteOIDCConsent = function(rp, + epoch) { + var items; + items = document.querySelectorAll(`.data-${epoch}`); + $scope.waiting = true; + $http['delete'](`${scriptname}sessions/OIDCConsent/${sessionType}/${$scope.currentSession.id}?rp=${rp}&epoch=${epoch}`).then(function(response) { + var e, + i, + len, + results; + $scope.waiting = false; + results = []; + for (i = 0, len = items.length; i < len; i++) { + e = items[i]; + results.push(e.remove()); + } + return results; + }, + function(resp) { + return $scope.waiting = false; + }); + return $scope.showT = false; + }; + // Delete + $scope.deleteSession = function() { + $scope.waiting = true; + return $http['delete'](`${scriptname}sessions/${sessionType}/${$scope.currentSession.id}`).then(function(response) { + $scope.currentSession = null; + $scope.currentScope.remove(); + return $scope.waiting = false; + }, + function(resp) { + return $scope.waiting = false; + }); + }; + $scope.globalLogout = function() { + $scope.waiting = true; + return $http['post'](`${scriptname}sessions/glogout/${sessionType}/${$scope.currentSession.id}`).then(function(response) { + $scope.currentSession = null; + $scope.currentScope.remove(); + return $scope.waiting = false; + }, + function(resp) { + return $scope.waiting = false; + }); + }; + // Open node + $scope.stoggle = function(scope) { + var node; + node = scope.$modelValue; + if (node.nodes.length === 0) { + $scope.updateTree(node.value, + node.nodes, + node.level, + node.over, + node.query, + node.count); + } + return scope.toggle(); + }; + // Display selected session + $scope.displaySession = function(scope) { + var sessionId, + transformSession; + // Private functions + + // Session preparation + transformSession = function(session) { + var _insert, + array, + attr, + attrs, + category, + cv, + element, + epoch, + i, + j, + k, + key, + l, + len, + len1, + len2, + len3, + len4, + len5, + m, + name, + o, + oidcConsent, + p, + real, + ref, + ref1, + res, + sfDevice, + spoof, + subres, + tab, + time, + title, + tmp, + value; + _insert = function(re, + title) { + var cv, + i, + key, + len, + reg, + tab, + tmp, + val, + value, + vk; + tmp = []; + reg = new RegExp(re); + cv = ""; + for (key in session) { + value = session[key]; + if (key.match(reg) && value) { + cv += `${value}:${key},`; + delete session[key]; + } + } + if (cv) { + cv = cv.replace(/,$/, + ''); + tab = cv.split(','); + tab.sort(); + tab.reverse(); + for (i = 0, len = tab.length; i < len; i++) { + val = tab[i]; + vk = val.split(':'); + tmp.push({ + title: vk[1], + value: $scope.localeDate(vk[0]) + }); + } + return res.push({ + title: title, + nodes: tmp + }); + } + }; + time = session._utime; +// 1. Replace values if needed + for (key in session) { + value = session[key]; + if (!value) { + delete session[key]; + } else { + if (typeof session === 'string' && value.match(/; /)) { + session[key] = value.split('; '); + } + if (typeof session[key] !== 'object') { + if (hiddenAttributes.match(new RegExp('\b' + key + '\b'))) { + session[key] = '********'; + } else if (key.match(/^(_utime|_lastAuthnUTime|_lastSeen|notification)$/)) { + session[key] = $scope.localeDate(value); + } else if (key.match(/^(_startTime|_updateTime)$/)) { + session[key] = $scope.strToLocaleDate(value); + } + } + } + } + res = []; +// 2. Push session keys in result, grouped by categories + for (category in categories) { + attrs = categories[category]; + subres = []; + for (i = 0, len = attrs.length; i < len; i++) { + attr = attrs[i]; + if (session[attr]) { + if (attr === "_2fDevices" && session[attr]) { + array = JSON.parse(session[attr]); + if (array.length > 0) { + subres.push({ + title: "type", + value: "name", + epoch: "date", + td: "0" + }); + for (j = 0, len1 = array.length; j < len1; j++) { + sfDevice = array[j]; + for (key in sfDevice) { + value = sfDevice[key]; + if (key === 'type') { + title = value; + } + if (key === 'name') { + name = value; + } + if (key === 'epoch') { + epoch = value; + } + } + subres.push({ + title: title, + value: name, + epoch: epoch, + td: "1" + }); + } + } + delete session[attr]; + } else if (session[attr].toString().match(/"rp":\s*"[\w-]+"/)) { + subres.push({ + title: "RP", + value: "scope", + epoch: "date", + td: "0" + }); + array = JSON.parse(session[attr]); + for (k = 0, len2 = array.length; k < len2; k++) { + oidcConsent = array[k]; + for (key in oidcConsent) { + value = oidcConsent[key]; + if (key === 'rp') { + title = value; + } + if (key === 'scope') { + name = value; + } + if (key === 'epoch') { + epoch = value; + } + } + subres.push({ + title: title, + value: name, + epoch: epoch, + td: "2" + }); + } + delete session[attr]; + } else if (session[attr].toString().match(/\w+/)) { + subres.push({ + title: attr, + value: session[attr], + epoch: '' + }); + delete session[attr]; + } else { + delete session[attr]; + } + } else { + delete session[attr]; + } + } + if (subres.length > 0) { + res.push({ + title: `__${category}__`, + nodes: subres + }); + } + } + // 3. Add OpenID and notifications already notified + _insert('^openid', + 'OpenID'); + _insert('^notification_(.+)', + '__notificationsDone__'); + // 4. Add session history if exists + if (session._loginHistory) { + tmp = []; + if (session._loginHistory.successLogin) { + ref = session._loginHistory.successLogin; + for (m = 0, len3 = ref.length; m < len3; m++) { + l = ref[m]; + // History custom values + cv = ""; + for (key in l) { + value = l[key]; + if (!key.match(/^(_utime|ipAddr|error)$/)) { + cv += `, ${key} : ${value}`; + } + } + tab = cv.split(', '); + tab.sort(); + cv = tab.join(', '); + tmp.push({ + t: l._utime, + title: $scope.localeDate(l._utime), + value: `Success (IP ${l.ipAddr})` + cv + }); + } + } + if (session._loginHistory.failedLogin) { + ref1 = session._loginHistory.failedLogin; + for (o = 0, len4 = ref1.length; o < len4; o++) { + l = ref1[o]; + // History custom values + cv = ""; + for (key in l) { + value = l[key]; + if (!key.match(/^(_utime|ipAddr|error)$/)) { + cv += `, ${key} : ${value}`; + } + } + tab = cv.split(', '); + tab.sort(); + cv = tab.join(', '); + tmp.push({ + t: l._utime, + title: $scope.localeDate(l._utime), + value: `Error ${l.error} (IP ${l.ipAddr})` + cv + }); + } + } + delete session._loginHistory; + tmp.sort(function(a, + b) { + return b.t - a.t; + }); + res.push({ + title: '__loginHistory__', + nodes: tmp + }); + } + // 5. Other keys (attributes and macros) + tmp = []; + for (key in session) { + value = session[key]; + tmp.push({ + title: key, + value: value + }); + } + tmp.sort(function(a, + b) { + if (a.title > b.title) { + return 1; + } else if (a.title < b.title) { + return -1; + } else { + return 0; + } + }); + // Sort by real and spoofed attributes + real = []; + spoof = []; + for (p = 0, len5 = tmp.length; p < len5; p++) { + element = tmp[p]; + if (element.title.match(new RegExp('^' + $scope.impPrefix + '.+$'))) { + console.log(element, + '-> real attribute'); + real.push(element); + } else { + //console.log element, '-> spoofed attribute' + spoof.push(element); + } + } + tmp = spoof.concat(real); + res.push({ + title: '__attributesAndMacros__', + nodes: tmp + }); + return { + _utime: time, + nodes: res + }; + }; + $scope.currentScope = scope; + sessionId = scope.$modelValue.session; + $http.get(`${scriptname}sessions/${sessionType}/${sessionId}`).then(function(response) { + $scope.currentSession = transformSession(response.data); + return $scope.currentSession.id = sessionId; + }); + return $scope.showT = false; + }; + $scope.localeDate = function(s) { + var d; + d = new Date(s * 1000); + return d.toLocaleString(); + }; + $scope.isValid = function(epoch, + type) { + var isValid, + now, + path; + path = $location.path(); + now = Date.now() / 1000; + console.log("Path", + path); + console.log("Session epoch", + epoch); + console.log("Current date", + now); + console.log("Session TTL", + sessionTTL); + isValid = now - epoch < sessionTTL || $location.path().match(/^\/persistent/); + if (type === 'msg') { + console.log("Return msg"); + if (isValid) { + return "info"; + } else { + return "warning"; + } + } else if (type === 'style') { + console.log("Return style"); + if (isValid) { + return {}; + } else { + return { + 'color': '#627990', + 'font-style': 'italic' + }; + } + } else { + console.log("Return isValid"); + return isValid; + } + }; + $scope.strToLocaleDate = function(s) { + var arrayDate, + d; + arrayDate = s.match(/^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})$/); + if (!arrayDate.length) { + return s; + } + d = new Date(`${arrayDate[1]}-${arrayDate[2]}-${arrayDate[3]}T${arrayDate[4]}:${arrayDate[5]}:${arrayDate[6]}`); + return d.toLocaleString(); + }; + // Function to change interface language + $scope.getLanguage = function(lang) { + $scope.lang = lang; + $scope.form = 'white'; + $scope.init(); + return $scope.showM = false; + }; + // URI local path management + pathEvent = function(event, + next, + current) { + var n; + n = next.match(/#!?\/(\w+)/); + sessionType = 'global'; + if (n === null) { + $scope.type = '_whatToTrace'; + } else if (n[1].match(/^(persistent|offline)$/)) { + sessionType = RegExp.$1; + $scope.type = '_session_uid'; + } else { + $scope.type = n[1]; + } + return $scope.init(); + }; + $scope.$on('$locationChangeSuccess', + pathEvent); + // Function to update tree: download value of opened subkey + autoId = 0; + $scope.updateTree = function(value, + node, + level, + over, + currentQuery, + count) { + var query, + scheme, + tmp; + $scope.waiting = true; + // Query scheme selection: + + // - if defined above + // - _updateTime must be displayed as startTime + // - default to _whatToTrace scheme + scheme = schemes[$scope.type] ? schemes[$scope.type] : $scope.type === '_updateTime' ? schemes._startTime : schemes._whatToTrace; + // Build query using schemes + query = scheme[level]($scope.type, + value, + currentQuery); + // If number of session exceeds "max" and overScheme exists, call it + if (count > max && overScheme[$scope.type]) { + if (tmp = overScheme[$scope.type]($scope.type, + value, + level, + over, + currentQuery)) { + over++; + query = tmp; + level = level - 1; + } else { + over = 0; + } + } else { + over = 0; + } + // Launch HTTP query + $http.get(`${scriptname}sessions/${sessionType}?${query}`).then(function(response) { + var data, + i, + len, + n, + ref; + data = response.data; + if (data.result) { + ref = data.values; + for (i = 0, len = ref.length; i < len; i++) { + n = ref[i]; + autoId++; + n.id = `node${autoId}`; + if (level < scheme.length - 1) { + n.nodes = []; + n.level = level + 1; + n.query = query; + n.over = over; + // Date display in tree + if ($scope.type.match(/^(?:start|update)Time$/)) { + // 12 digits -> 12:34 + n.title = n.value.replace(/^(\d{8})(\d{2})(\d{2})$/, + // 11 digits -> 12:30 + '$2:$3').replace(/^(\d{8})(\d{2})(\d)$/, + // 10 digits -> 12h + '$2:$30').replace(/^(\d{8})(\d{2})$/, + // 8 digits -> 2016-03-15 + '$2h').replace(/^(\d{4})(\d{2})(\d{2})/, + '$1-$2-$3'); + } + } + node.push(n); + } + if (value === '') { + $scope.total = data.total; + } + } + return $scope.waiting = false; + }, + function(resp) { + return $scope.waiting = false; + }); + // Highlight current selection + console.log("Selection", + sessionType); + $scope.navssoStyle = { + color: '#777' + }; + $scope.offlineStyle = { + color: '#777' + }; + $scope.persistentStyle = { + color: '#777' + }; + if (sessionType === 'global') { + $scope.navssoStyle = { + color: '#333' + }; + } + if (sessionType === 'offline') { + $scope.offlineStyle = { + color: '#333' + }; + } + if (sessionType === 'persistent') { + return $scope.persistentStyle = { + color: '#333' + }; + } + }; + // Intialization function + // Simply set $scope.waiting to false during $translator and tree root + // initialization + $scope.init = function() { + $scope.waiting = true; + $scope.data = []; + $scope.currentScope = null; + $scope.currentSession = null; + $q.all([$translator.init($scope.lang), + $scope.updateTree('', + $scope.data, + 0, + 0)]).then(function() { + return $scope.waiting = false; + }, + function(resp) { + return $scope.waiting = false; + }); + // Colorized link + $scope.activeModule = "sessions"; + return $scope.myStyle = { + color: '#ffb84d' + }; + }; + // Query scheme initialization + // Default to '_whatToTrace' + c = $location.path().match(/^\/(\w+)/); + return $scope.type = c ? c[1] : '_whatToTrace'; + } + ]); + +}).call(this); diff --git a/full/install/usr/share/lemonldap-ng/manager/htdocs/static/js/sessions.min.js b/full/install/usr/share/lemonldap-ng/manager/htdocs/static/js/sessions.min.js new file mode 100644 index 0000000..9808bb3 --- /dev/null +++ b/full/install/usr/share/lemonldap-ng/manager/htdocs/static/js/sessions.min.js @@ -0,0 +1 @@ +!function(){var g={_whatToTrace:[function(e,t){return`groupBy=substr(${e},1)`},function(e,t){return e+`=${t}*&groupBy=`+e},function(e,t){return e+"="+t}],ipAddr:[function(e,t){return`groupBy=net(${e},16,1)`},function(e,t){return t.match(/:/)||(t+="."),e+`=${t}*&groupBy=net(${e},32,2)`},function(e,t){return t.match(/:/)||(t+="."),e+`=${t}*&groupBy=net(${e},48,3)`},function(e,t){return t.match(/:/)||(t+="."),e+`=${t}*&groupBy=net(${e},128,4)`},function(e,t){return e+`=${t}&groupBy=_whatToTrace`},function(e,t,n){return n.replace(/\&groupBy.*$/,"")+"&_whatToTrace="+t}],_startTime:[function(e,t){return`groupBy=substr(${e},8)`},function(e,t){return e+`=${t}*&groupBy=substr(${e},10)`},function(e,t){return e+`=${t}*&groupBy=substr(${e},11)`},function(e,t){return e+`=${t}*&groupBy=substr(${e},12)`},function(e,t){return e+`=${t}*&groupBy=_whatToTrace`},function(e,t,n){return console.log(e),console.log(t),console.log(n),n.replace(/\&groupBy.*$/,"")+"&_whatToTrace="+t}],doubleIp:[function(e,t){return e},function(e,t){return`_whatToTrace=${t}&groupBy=ipAddr`},function(e,t,n){return n.replace(/\&groupBy.*$/,"")+"&ipAddr="+t}],_session_uid:[function(e,t){return`groupBy=substr(${e},1)`},function(e,t){return e+`=${t}*&groupBy=`+e},function(e,t){return e+"="+t}]},f={_whatToTrace:function(e,t,n,o){return console.log("overScheme => level",n,"over",o),1===n&&t.length>o?e+`=${t}*&groupBy=substr(${e},${n+o+1})`:null},ipAddr:function(e,t,n,o){return console.log("overScheme => level",n,"over",o),0 level",n,"over",o),3 level",n,"over",o),1===n&&t.length>o?e+`=${t}*&groupBy=substr(${e},${n+o+1})`:null}},O={dateTitle:["_utime","_startTime","_updateTime","_lastAuthnUTime","_lastSeen"],connectionTitle:["ipAddr","_timezone","_url"],authenticationTitle:["_session_id","_user","_password","authenticationLevel"],modulesTitle:["_auth","_userDB","_passwordDB","_issuerDB","_authChoice","_authMulti","_userDBMulti","_2f"],saml:["_idp","_idpConfKey","_samlToken","_lassoSessionDump","_lassoIdentityDump"],groups:["groups","hGroups"],ldap:["dn"],OpenIDConnect:["_oidc_id_token","_oidc_OP","_oidc_access_token","_oidc_refresh_token","_oidc_access_token_eol","_oidcConnectedRP","_oidcConnectedRPIDs"],sfaTitle:["_2fDevices"],oidcConsents:["_oidcConsents"]},i={session:[{title:"deleteSession",icon:"trash"},{title:"globalLogout",icon:"trash"}],home:[]};angular.module("llngSessionsExplorer",["ui.tree","ui.bootstrap","llApp"]).controller("SessionsExplorerCtrl",["$scope","$translator","$location","$q","$http",function(M,t,r,e,o){var p,n,d;return M.links=links,M.menulinks=menulinks,M.staticPrefix=staticPrefix,M.scriptname=scriptname,M.formPrefix=formPrefix,M.impPrefix=impPrefix,M.sessionTTL=sessionTTL,M.availableLanguages=availableLanguages,M.waiting=!0,M.showM=!1,M.showT=!0,M.data=[],M.currentScope=null,M.currentSession=null,M.menu=i,M.translateP=t.translateP,M.translate=t.translate,M.translateTitle=function(e){return t.translateField(e,"title")},d="global",M.menuClick=function(e){if(e.popup)window.open(e.popup);else switch(e.action||(e.action=e.title),typeof e.action){case"function":e.action(M.currentNode,M);break;case"string":M[e.action]();break;default:console.log(typeof e.action)}return M.showM=!1},M.deleteOIDCConsent=function(e,t){var i=document.querySelectorAll(".data-"+t);return M.waiting=!0,o.delete(`${scriptname}sessions/OIDCConsent/${d}/${M.currentSession.id}?rp=${e}&epoch=`+t).then(function(e){var t,n,o,r;for(M.waiting=!1,r=[],n=0,o=i.length;nt.title?1:e.title real attribute"),b):C).push(i);return R=C.concat(b),L.push({title:"__attributesAndMacros__",nodes:R}),{_utime:H,nodes:L}};return M.currentScope=e,t=e.$modelValue.session,o.get(scriptname+`sessions/${d}/`+t).then(function(e){return M.currentSession=n(e.data),M.currentSession.id=t}),M.showT=!1},M.localeDate=function(e){return new Date(1e3*e).toLocaleString()},M.isValid=function(e,t){var n=r.path(),o=Date.now()/1e3;return console.log("Path",n),console.log("Session epoch",e),console.log("Current date",o),console.log("Session TTL",sessionTTL),n=o-erun( \ diff --git a/manager/globalLogout.patch b/manager/globalLogout.patch new file mode 100644 index 0000000..7437112 --- /dev/null +++ b/manager/globalLogout.patch @@ -0,0 +1,77 @@ +diff --git a/usr/share/perl5/Lemonldap/NG/Manager/Sessions.pm b/usr/share/perl5/Lemonldap/NG/Manager/Sessions.pm +index edf6a5f19..89c7571d2 100644 +--- a/usr/share/perl5/Lemonldap/NG/Manager/Sessions.pm ++++ b/usr/share/perl5/Lemonldap/NG/Manager/Sessions.pm +@@ -40,6 +40,13 @@ sub init { + ['DELETE'] + ) + ++ # DELETE ALL SESSIONS FOR A USER ++ ->addRoute( ++ sessions => ++ { glogout => { ':sessionType' => { ':sessionId' => 'userLogout' } } }, ++ ['POST'] ++ ) ++ + # DELETE OIDC CONSENT + ->addRoute( + sessions => { +@@ -60,6 +67,58 @@ sub init { + return 1; + } + ++# ++# User logout ++# ++ ++sub userLogout { ++ my ( $self, $req ) = @_; ++ ++ my $mod = $self->getMod($req) ++ or return $self->sendError( $req, undef, 400 ); ++ my $id = $req->params('sessionId') ++ or return $self->sendError( $req, 'sessionId is missing', 400 ); ++ my $session = $self->getApacheSession( $mod, $id ); ++ ++ my $uidKey = Lemonldap::NG::Handler::Main->tsv->{whatToTrace}; ++ my $uid = $session->data->{$uidKey}; ++ ++ my $count = 0; ++ foreach my $storage (qw(oidcStorage sessionStorage)) { ++ my $storageModule = ++ Lemonldap::NG::Handler::Main->tsv->{"${storage}Module"}; ++ if ( defined $storageModule ) { ++ next if ( $storageModule eq "Apache::Session::Memcached" ); ++ my $opts = Lemonldap::NG::Handler::Main->tsv->{"${storage}Options"}; ++ $opts->{backend} = $storageModule; ++ my $sessions = ++ Lemonldap::NG::Common::Apache::Session->searchOn( $opts, $uidKey, ++ $uid ); ++ my @keys; ++ if ( $sessions and %$sessions ) { ++ @keys = keys %$sessions; ++ foreach my $sid (@keys) { ++ my $session = Lemonldap::NG::Common::Session->new( ++ storageModule => $storageModule, ++ storageModuleOptions => $opts, ++ cacheModule => Lemonldap::NG::Handler::Main->tsv ++ ->{sessionCacheModule}, ++ cacheModuleOptions => Lemonldap::NG::Handler::Main->tsv ++ ->{sessionCacheOptions}, ++ id => $sid, ++ ); ++ if ( $session->data ) { ++ $session->remove; ++ $count++; ++ } ++ } ++ } ++ } ++ } ++ Lemonldap::NG::Handler::PSGI::Main->localUnlog( $req, $id ); ++ return $self->sendJSONresponse( $req, { result => 1, count => $count } ); ++} ++ + ####################### + # II. CONSENT METHODS # + ####################### diff --git a/manager/install/usr/share/lemonldap-ng/manager/htdocs/static/js/sessions.js b/manager/install/usr/share/lemonldap-ng/manager/htdocs/static/js/sessions.js new file mode 100644 index 0000000..7cb8cb9 --- /dev/null +++ b/manager/install/usr/share/lemonldap-ng/manager/htdocs/static/js/sessions.js @@ -0,0 +1,863 @@ +// Generated by CoffeeScript 2.7.0 +(function() { + /* + * Sessions explorer + */ + /* + * AngularJS application + */ + var categories, hiddenAttributes, llapp, max, menu, overScheme, schemes; + + // Max number of session to display (see overScheme) + max = 25; + + // Queries to do each type of display: each array item corresponds to the depth + // of opened nodes in the tree + schemes = { + _whatToTrace: [ + // First level: display 1 letter + function(t, + v) { + return `groupBy=substr(${t},1)`; + }, + // Second level (if no overScheme), display usernames + function(t, + v) { + return `${t}=${v}*&groupBy=${t}`; + }, + function(t, + v) { + return `${t}=${v}`; + } + ], + ipAddr: [ + function(t, + v) { + return `groupBy=net(${t},16,1)`; + }, + function(t, + v) { + if (!v.match(/:/)) { + v = v + '.'; + } + return `${t}=${v}*&groupBy=net(${t},32,2)`; + }, + function(t, + v) { + if (!v.match(/:/)) { + v = v + '.'; + } + return `${t}=${v}*&groupBy=net(${t},48,3)`; + }, + function(t, + v) { + if (!v.match(/:/)) { + v = v + '.'; + } + return `${t}=${v}*&groupBy=net(${t},128,4)`; + }, + function(t, + v) { + return `${t}=${v}&groupBy=_whatToTrace`; + }, + function(t, + v, + q) { + return q.replace(/\&groupBy.*$/, + '') + `&_whatToTrace=${v}`; + } + ], + _startTime: [ + function(t, + v) { + return `groupBy=substr(${t},8)`; + }, + function(t, + v) { + return `${t}=${v}*&groupBy=substr(${t},10)`; + }, + function(t, + v) { + return `${t}=${v}*&groupBy=substr(${t},11)`; + }, + function(t, + v) { + return `${t}=${v}*&groupBy=substr(${t},12)`; + }, + function(t, + v) { + return `${t}=${v}*&groupBy=_whatToTrace`; + }, + function(t, + v, + q) { + console.log(t); + console.log(v); + console.log(q); + return q.replace(/\&groupBy.*$/, + '') + `&_whatToTrace=${v}`; + } + ], + doubleIp: [ + function(t, + v) { + return t; + }, + function(t, + v) { + return `_whatToTrace=${v}&groupBy=ipAddr`; + }, + function(t, + v, + q) { + return q.replace(/\&groupBy.*$/, + '') + `&ipAddr=${v}`; + } + ], + _session_uid: [ + // First level: display 1 letter + function(t, + v) { + return `groupBy=substr(${t},1)`; + }, + // Second level (if no overScheme), display usernames + function(t, + v) { + return `${t}=${v}*&groupBy=${t}`; + }, + function(t, + v) { + return `${t}=${v}`; + } + ] + }; + + // When number of children nodes exceeds "max" value and if "overScheme." + // is available and does not return "null", a level is added. See + // "$scope.updateTree" method + overScheme = { + _whatToTrace: function(t, v, level, over) { + // "v.length > over" avoids a loop if one user opened more than "max" + // sessions + console.log('overScheme => level', level, 'over', over); + if (level === 1 && v.length > over) { + return `${t}=${v}*&groupBy=substr(${t},${level + over + 1})`; + } else { + return null; + } + }, + // Note: IPv4 only + ipAddr: function(t, v, level, over) { + console.log('overScheme => level', level, 'over', over); + if (level > 0 && level < 4 && !v.match(/^\d+\.\d/) && over < 2) { + return `${t}=${v}*&groupBy=net(${t},${16 * level + 4 * (over + 1)},${1 + level + over})`; + } else { + return null; + } + }, + _startTime: function(t, v, level, over) { + console.log('overScheme => level', level, 'over', over); + if (level > 3) { + return `${t}=${v}*&groupBy=substr(${t},${10 + level + over})`; + } else { + return null; + } + }, + _session_uid: function(t, v, level, over) { + console.log('overScheme => level', level, 'over', over); + if (level === 1 && v.length > over) { + return `${t}=${v}*&groupBy=substr(${t},${level + over + 1})`; + } else { + return null; + } + } + }; + + hiddenAttributes = '_password'; + + // Attributes to group in session display + categories = { + dateTitle: ['_utime', '_startTime', '_updateTime', '_lastAuthnUTime', '_lastSeen'], + connectionTitle: ['ipAddr', '_timezone', '_url'], + authenticationTitle: ['_session_id', '_user', '_password', 'authenticationLevel'], + modulesTitle: ['_auth', '_userDB', '_passwordDB', '_issuerDB', '_authChoice', '_authMulti', '_userDBMulti', '_2f'], + saml: ['_idp', '_idpConfKey', '_samlToken', '_lassoSessionDump', '_lassoIdentityDump'], + groups: ['groups', 'hGroups'], + ldap: ['dn'], + OpenIDConnect: ['_oidc_id_token', '_oidc_OP', '_oidc_access_token', '_oidc_refresh_token', '_oidc_access_token_eol', '_oidcConnectedRP', '_oidcConnectedRPIDs'], + sfaTitle: ['_2fDevices'], + oidcConsents: ['_oidcConsents'] + }; + + // Menu entries + menu = { + session: [ + { + title: 'deleteSession', + icon: 'trash' + }, + { + title: 'globalLogout', + icon: 'trash' + } + ], + home: [] + }; + + llapp = angular.module('llngSessionsExplorer', ['ui.tree', 'ui.bootstrap', 'llApp']); + + // Main controller + llapp.controller('SessionsExplorerCtrl', [ + '$scope', + '$translator', + '$location', + '$q', + '$http', + function($scope, + $translator, + $location, + $q, + $http) { + var autoId, + c, + pathEvent, + sessionType; + $scope.links = links; + $scope.menulinks = menulinks; + $scope.staticPrefix = staticPrefix; + $scope.scriptname = scriptname; + $scope.formPrefix = formPrefix; + $scope.impPrefix = impPrefix; + $scope.sessionTTL = sessionTTL; + $scope.availableLanguages = availableLanguages; + $scope.waiting = true; + $scope.showM = false; + $scope.showT = true; + $scope.data = []; + $scope.currentScope = null; + $scope.currentSession = null; + $scope.menu = menu; + // Import translations functions + $scope.translateP = $translator.translateP; + $scope.translate = $translator.translate; + $scope.translateTitle = function(node) { + return $translator.translateField(node, + 'title'); + }; + sessionType = 'global'; + // Handle menu items + $scope.menuClick = function(button) { + if (button.popup) { + window.open(button.popup); + } else { + if (!button.action) { + button.action = button.title; + } + switch (typeof button.action) { + case 'function': + button.action($scope.currentNode, + $scope); + break; + case 'string': + $scope[button.action](); + break; + default: + console.log(typeof button.action); + } + } + return $scope.showM = false; + }; + // SESSION MANAGEMENT + + // Delete RP Consent + $scope.deleteOIDCConsent = function(rp, + epoch) { + var items; + items = document.querySelectorAll(`.data-${epoch}`); + $scope.waiting = true; + $http['delete'](`${scriptname}sessions/OIDCConsent/${sessionType}/${$scope.currentSession.id}?rp=${rp}&epoch=${epoch}`).then(function(response) { + var e, + i, + len, + results; + $scope.waiting = false; + results = []; + for (i = 0, len = items.length; i < len; i++) { + e = items[i]; + results.push(e.remove()); + } + return results; + }, + function(resp) { + return $scope.waiting = false; + }); + return $scope.showT = false; + }; + // Delete + $scope.deleteSession = function() { + $scope.waiting = true; + return $http['delete'](`${scriptname}sessions/${sessionType}/${$scope.currentSession.id}`).then(function(response) { + $scope.currentSession = null; + $scope.currentScope.remove(); + return $scope.waiting = false; + }, + function(resp) { + return $scope.waiting = false; + }); + }; + $scope.globalLogout = function() { + $scope.waiting = true; + return $http['post'](`${scriptname}sessions/glogout/${sessionType}/${$scope.currentSession.id}`).then(function(response) { + $scope.currentSession = null; + $scope.currentScope.remove(); + return $scope.waiting = false; + }, + function(resp) { + return $scope.waiting = false; + }); + }; + // Open node + $scope.stoggle = function(scope) { + var node; + node = scope.$modelValue; + if (node.nodes.length === 0) { + $scope.updateTree(node.value, + node.nodes, + node.level, + node.over, + node.query, + node.count); + } + return scope.toggle(); + }; + // Display selected session + $scope.displaySession = function(scope) { + var sessionId, + transformSession; + // Private functions + + // Session preparation + transformSession = function(session) { + var _insert, + array, + attr, + attrs, + category, + cv, + element, + epoch, + i, + j, + k, + key, + l, + len, + len1, + len2, + len3, + len4, + len5, + m, + name, + o, + oidcConsent, + p, + real, + ref, + ref1, + res, + sfDevice, + spoof, + subres, + tab, + time, + title, + tmp, + value; + _insert = function(re, + title) { + var cv, + i, + key, + len, + reg, + tab, + tmp, + val, + value, + vk; + tmp = []; + reg = new RegExp(re); + cv = ""; + for (key in session) { + value = session[key]; + if (key.match(reg) && value) { + cv += `${value}:${key},`; + delete session[key]; + } + } + if (cv) { + cv = cv.replace(/,$/, + ''); + tab = cv.split(','); + tab.sort(); + tab.reverse(); + for (i = 0, len = tab.length; i < len; i++) { + val = tab[i]; + vk = val.split(':'); + tmp.push({ + title: vk[1], + value: $scope.localeDate(vk[0]) + }); + } + return res.push({ + title: title, + nodes: tmp + }); + } + }; + time = session._utime; +// 1. Replace values if needed + for (key in session) { + value = session[key]; + if (!value) { + delete session[key]; + } else { + if (typeof session === 'string' && value.match(/; /)) { + session[key] = value.split('; '); + } + if (typeof session[key] !== 'object') { + if (hiddenAttributes.match(new RegExp('\b' + key + '\b'))) { + session[key] = '********'; + } else if (key.match(/^(_utime|_lastAuthnUTime|_lastSeen|notification)$/)) { + session[key] = $scope.localeDate(value); + } else if (key.match(/^(_startTime|_updateTime)$/)) { + session[key] = $scope.strToLocaleDate(value); + } + } + } + } + res = []; +// 2. Push session keys in result, grouped by categories + for (category in categories) { + attrs = categories[category]; + subres = []; + for (i = 0, len = attrs.length; i < len; i++) { + attr = attrs[i]; + if (session[attr]) { + if (attr === "_2fDevices" && session[attr]) { + array = JSON.parse(session[attr]); + if (array.length > 0) { + subres.push({ + title: "type", + value: "name", + epoch: "date", + td: "0" + }); + for (j = 0, len1 = array.length; j < len1; j++) { + sfDevice = array[j]; + for (key in sfDevice) { + value = sfDevice[key]; + if (key === 'type') { + title = value; + } + if (key === 'name') { + name = value; + } + if (key === 'epoch') { + epoch = value; + } + } + subres.push({ + title: title, + value: name, + epoch: epoch, + td: "1" + }); + } + } + delete session[attr]; + } else if (session[attr].toString().match(/"rp":\s*"[\w-]+"/)) { + subres.push({ + title: "RP", + value: "scope", + epoch: "date", + td: "0" + }); + array = JSON.parse(session[attr]); + for (k = 0, len2 = array.length; k < len2; k++) { + oidcConsent = array[k]; + for (key in oidcConsent) { + value = oidcConsent[key]; + if (key === 'rp') { + title = value; + } + if (key === 'scope') { + name = value; + } + if (key === 'epoch') { + epoch = value; + } + } + subres.push({ + title: title, + value: name, + epoch: epoch, + td: "2" + }); + } + delete session[attr]; + } else if (session[attr].toString().match(/\w+/)) { + subres.push({ + title: attr, + value: session[attr], + epoch: '' + }); + delete session[attr]; + } else { + delete session[attr]; + } + } else { + delete session[attr]; + } + } + if (subres.length > 0) { + res.push({ + title: `__${category}__`, + nodes: subres + }); + } + } + // 3. Add OpenID and notifications already notified + _insert('^openid', + 'OpenID'); + _insert('^notification_(.+)', + '__notificationsDone__'); + // 4. Add session history if exists + if (session._loginHistory) { + tmp = []; + if (session._loginHistory.successLogin) { + ref = session._loginHistory.successLogin; + for (m = 0, len3 = ref.length; m < len3; m++) { + l = ref[m]; + // History custom values + cv = ""; + for (key in l) { + value = l[key]; + if (!key.match(/^(_utime|ipAddr|error)$/)) { + cv += `, ${key} : ${value}`; + } + } + tab = cv.split(', '); + tab.sort(); + cv = tab.join(', '); + tmp.push({ + t: l._utime, + title: $scope.localeDate(l._utime), + value: `Success (IP ${l.ipAddr})` + cv + }); + } + } + if (session._loginHistory.failedLogin) { + ref1 = session._loginHistory.failedLogin; + for (o = 0, len4 = ref1.length; o < len4; o++) { + l = ref1[o]; + // History custom values + cv = ""; + for (key in l) { + value = l[key]; + if (!key.match(/^(_utime|ipAddr|error)$/)) { + cv += `, ${key} : ${value}`; + } + } + tab = cv.split(', '); + tab.sort(); + cv = tab.join(', '); + tmp.push({ + t: l._utime, + title: $scope.localeDate(l._utime), + value: `Error ${l.error} (IP ${l.ipAddr})` + cv + }); + } + } + delete session._loginHistory; + tmp.sort(function(a, + b) { + return b.t - a.t; + }); + res.push({ + title: '__loginHistory__', + nodes: tmp + }); + } + // 5. Other keys (attributes and macros) + tmp = []; + for (key in session) { + value = session[key]; + tmp.push({ + title: key, + value: value + }); + } + tmp.sort(function(a, + b) { + if (a.title > b.title) { + return 1; + } else if (a.title < b.title) { + return -1; + } else { + return 0; + } + }); + // Sort by real and spoofed attributes + real = []; + spoof = []; + for (p = 0, len5 = tmp.length; p < len5; p++) { + element = tmp[p]; + if (element.title.match(new RegExp('^' + $scope.impPrefix + '.+$'))) { + console.log(element, + '-> real attribute'); + real.push(element); + } else { + //console.log element, '-> spoofed attribute' + spoof.push(element); + } + } + tmp = spoof.concat(real); + res.push({ + title: '__attributesAndMacros__', + nodes: tmp + }); + return { + _utime: time, + nodes: res + }; + }; + $scope.currentScope = scope; + sessionId = scope.$modelValue.session; + $http.get(`${scriptname}sessions/${sessionType}/${sessionId}`).then(function(response) { + $scope.currentSession = transformSession(response.data); + return $scope.currentSession.id = sessionId; + }); + return $scope.showT = false; + }; + $scope.localeDate = function(s) { + var d; + d = new Date(s * 1000); + return d.toLocaleString(); + }; + $scope.isValid = function(epoch, + type) { + var isValid, + now, + path; + path = $location.path(); + now = Date.now() / 1000; + console.log("Path", + path); + console.log("Session epoch", + epoch); + console.log("Current date", + now); + console.log("Session TTL", + sessionTTL); + isValid = now - epoch < sessionTTL || $location.path().match(/^\/persistent/); + if (type === 'msg') { + console.log("Return msg"); + if (isValid) { + return "info"; + } else { + return "warning"; + } + } else if (type === 'style') { + console.log("Return style"); + if (isValid) { + return {}; + } else { + return { + 'color': '#627990', + 'font-style': 'italic' + }; + } + } else { + console.log("Return isValid"); + return isValid; + } + }; + $scope.strToLocaleDate = function(s) { + var arrayDate, + d; + arrayDate = s.match(/^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})$/); + if (!arrayDate.length) { + return s; + } + d = new Date(`${arrayDate[1]}-${arrayDate[2]}-${arrayDate[3]}T${arrayDate[4]}:${arrayDate[5]}:${arrayDate[6]}`); + return d.toLocaleString(); + }; + // Function to change interface language + $scope.getLanguage = function(lang) { + $scope.lang = lang; + $scope.form = 'white'; + $scope.init(); + return $scope.showM = false; + }; + // URI local path management + pathEvent = function(event, + next, + current) { + var n; + n = next.match(/#!?\/(\w+)/); + sessionType = 'global'; + if (n === null) { + $scope.type = '_whatToTrace'; + } else if (n[1].match(/^(persistent|offline)$/)) { + sessionType = RegExp.$1; + $scope.type = '_session_uid'; + } else { + $scope.type = n[1]; + } + return $scope.init(); + }; + $scope.$on('$locationChangeSuccess', + pathEvent); + // Function to update tree: download value of opened subkey + autoId = 0; + $scope.updateTree = function(value, + node, + level, + over, + currentQuery, + count) { + var query, + scheme, + tmp; + $scope.waiting = true; + // Query scheme selection: + + // - if defined above + // - _updateTime must be displayed as startTime + // - default to _whatToTrace scheme + scheme = schemes[$scope.type] ? schemes[$scope.type] : $scope.type === '_updateTime' ? schemes._startTime : schemes._whatToTrace; + // Build query using schemes + query = scheme[level]($scope.type, + value, + currentQuery); + // If number of session exceeds "max" and overScheme exists, call it + if (count > max && overScheme[$scope.type]) { + if (tmp = overScheme[$scope.type]($scope.type, + value, + level, + over, + currentQuery)) { + over++; + query = tmp; + level = level - 1; + } else { + over = 0; + } + } else { + over = 0; + } + // Launch HTTP query + $http.get(`${scriptname}sessions/${sessionType}?${query}`).then(function(response) { + var data, + i, + len, + n, + ref; + data = response.data; + if (data.result) { + ref = data.values; + for (i = 0, len = ref.length; i < len; i++) { + n = ref[i]; + autoId++; + n.id = `node${autoId}`; + if (level < scheme.length - 1) { + n.nodes = []; + n.level = level + 1; + n.query = query; + n.over = over; + // Date display in tree + if ($scope.type.match(/^(?:start|update)Time$/)) { + // 12 digits -> 12:34 + n.title = n.value.replace(/^(\d{8})(\d{2})(\d{2})$/, + // 11 digits -> 12:30 + '$2:$3').replace(/^(\d{8})(\d{2})(\d)$/, + // 10 digits -> 12h + '$2:$30').replace(/^(\d{8})(\d{2})$/, + // 8 digits -> 2016-03-15 + '$2h').replace(/^(\d{4})(\d{2})(\d{2})/, + '$1-$2-$3'); + } + } + node.push(n); + } + if (value === '') { + $scope.total = data.total; + } + } + return $scope.waiting = false; + }, + function(resp) { + return $scope.waiting = false; + }); + // Highlight current selection + console.log("Selection", + sessionType); + $scope.navssoStyle = { + color: '#777' + }; + $scope.offlineStyle = { + color: '#777' + }; + $scope.persistentStyle = { + color: '#777' + }; + if (sessionType === 'global') { + $scope.navssoStyle = { + color: '#333' + }; + } + if (sessionType === 'offline') { + $scope.offlineStyle = { + color: '#333' + }; + } + if (sessionType === 'persistent') { + return $scope.persistentStyle = { + color: '#333' + }; + } + }; + // Intialization function + // Simply set $scope.waiting to false during $translator and tree root + // initialization + $scope.init = function() { + $scope.waiting = true; + $scope.data = []; + $scope.currentScope = null; + $scope.currentSession = null; + $q.all([$translator.init($scope.lang), + $scope.updateTree('', + $scope.data, + 0, + 0)]).then(function() { + return $scope.waiting = false; + }, + function(resp) { + return $scope.waiting = false; + }); + // Colorized link + $scope.activeModule = "sessions"; + return $scope.myStyle = { + color: '#ffb84d' + }; + }; + // Query scheme initialization + // Default to '_whatToTrace' + c = $location.path().match(/^\/(\w+)/); + return $scope.type = c ? c[1] : '_whatToTrace'; + } + ]); + +}).call(this); diff --git a/manager/install/usr/share/lemonldap-ng/manager/htdocs/static/js/sessions.min.js b/manager/install/usr/share/lemonldap-ng/manager/htdocs/static/js/sessions.min.js new file mode 100644 index 0000000..9808bb3 --- /dev/null +++ b/manager/install/usr/share/lemonldap-ng/manager/htdocs/static/js/sessions.min.js @@ -0,0 +1 @@ +!function(){var g={_whatToTrace:[function(e,t){return`groupBy=substr(${e},1)`},function(e,t){return e+`=${t}*&groupBy=`+e},function(e,t){return e+"="+t}],ipAddr:[function(e,t){return`groupBy=net(${e},16,1)`},function(e,t){return t.match(/:/)||(t+="."),e+`=${t}*&groupBy=net(${e},32,2)`},function(e,t){return t.match(/:/)||(t+="."),e+`=${t}*&groupBy=net(${e},48,3)`},function(e,t){return t.match(/:/)||(t+="."),e+`=${t}*&groupBy=net(${e},128,4)`},function(e,t){return e+`=${t}&groupBy=_whatToTrace`},function(e,t,n){return n.replace(/\&groupBy.*$/,"")+"&_whatToTrace="+t}],_startTime:[function(e,t){return`groupBy=substr(${e},8)`},function(e,t){return e+`=${t}*&groupBy=substr(${e},10)`},function(e,t){return e+`=${t}*&groupBy=substr(${e},11)`},function(e,t){return e+`=${t}*&groupBy=substr(${e},12)`},function(e,t){return e+`=${t}*&groupBy=_whatToTrace`},function(e,t,n){return console.log(e),console.log(t),console.log(n),n.replace(/\&groupBy.*$/,"")+"&_whatToTrace="+t}],doubleIp:[function(e,t){return e},function(e,t){return`_whatToTrace=${t}&groupBy=ipAddr`},function(e,t,n){return n.replace(/\&groupBy.*$/,"")+"&ipAddr="+t}],_session_uid:[function(e,t){return`groupBy=substr(${e},1)`},function(e,t){return e+`=${t}*&groupBy=`+e},function(e,t){return e+"="+t}]},f={_whatToTrace:function(e,t,n,o){return console.log("overScheme => level",n,"over",o),1===n&&t.length>o?e+`=${t}*&groupBy=substr(${e},${n+o+1})`:null},ipAddr:function(e,t,n,o){return console.log("overScheme => level",n,"over",o),0 level",n,"over",o),3 level",n,"over",o),1===n&&t.length>o?e+`=${t}*&groupBy=substr(${e},${n+o+1})`:null}},O={dateTitle:["_utime","_startTime","_updateTime","_lastAuthnUTime","_lastSeen"],connectionTitle:["ipAddr","_timezone","_url"],authenticationTitle:["_session_id","_user","_password","authenticationLevel"],modulesTitle:["_auth","_userDB","_passwordDB","_issuerDB","_authChoice","_authMulti","_userDBMulti","_2f"],saml:["_idp","_idpConfKey","_samlToken","_lassoSessionDump","_lassoIdentityDump"],groups:["groups","hGroups"],ldap:["dn"],OpenIDConnect:["_oidc_id_token","_oidc_OP","_oidc_access_token","_oidc_refresh_token","_oidc_access_token_eol","_oidcConnectedRP","_oidcConnectedRPIDs"],sfaTitle:["_2fDevices"],oidcConsents:["_oidcConsents"]},i={session:[{title:"deleteSession",icon:"trash"},{title:"globalLogout",icon:"trash"}],home:[]};angular.module("llngSessionsExplorer",["ui.tree","ui.bootstrap","llApp"]).controller("SessionsExplorerCtrl",["$scope","$translator","$location","$q","$http",function(M,t,r,e,o){var p,n,d;return M.links=links,M.menulinks=menulinks,M.staticPrefix=staticPrefix,M.scriptname=scriptname,M.formPrefix=formPrefix,M.impPrefix=impPrefix,M.sessionTTL=sessionTTL,M.availableLanguages=availableLanguages,M.waiting=!0,M.showM=!1,M.showT=!0,M.data=[],M.currentScope=null,M.currentSession=null,M.menu=i,M.translateP=t.translateP,M.translate=t.translate,M.translateTitle=function(e){return t.translateField(e,"title")},d="global",M.menuClick=function(e){if(e.popup)window.open(e.popup);else switch(e.action||(e.action=e.title),typeof e.action){case"function":e.action(M.currentNode,M);break;case"string":M[e.action]();break;default:console.log(typeof e.action)}return M.showM=!1},M.deleteOIDCConsent=function(e,t){var i=document.querySelectorAll(".data-"+t);return M.waiting=!0,o.delete(`${scriptname}sessions/OIDCConsent/${d}/${M.currentSession.id}?rp=${e}&epoch=`+t).then(function(e){var t,n,o,r;for(M.waiting=!1,r=[],n=0,o=i.length;nt.title?1:e.title real attribute"),b):C).push(i);return R=C.concat(b),L.push({title:"__attributesAndMacros__",nodes:R}),{_utime:H,nodes:L}};return M.currentScope=e,t=e.$modelValue.session,o.get(scriptname+`sessions/${d}/`+t).then(function(e){return M.currentSession=n(e.data),M.currentSession.id=t}),M.showT=!1},M.localeDate=function(e){return new Date(1e3*e).toLocaleString()},M.isValid=function(e,t){var n=r.path(),o=Date.now()/1e3;return console.log("Path",n),console.log("Session epoch",e),console.log("Current date",o),console.log("Session TTL",sessionTTL),n=o-e