diff --git a/.gitignore b/.gitignore index 3c3629e647f5ddf82548912e337bea9826b434af..8b59be3fb6bada61f200f856c9f145fba827dab6 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,25 @@ node_modules +e2e-tests/conf/ +lemonldap-ng-common/MYMETA.json +lemonldap-ng-common/MYMETA.yml +lemonldap-ng-common/Makefile +lemonldap-ng-common/blib/ +lemonldap-ng-common/pm_to_blib +lemonldap-ng-handler/MYMETA.json +lemonldap-ng-handler/MYMETA.yml +lemonldap-ng-handler/Makefile +lemonldap-ng-handler/blib/ +lemonldap-ng-handler/pm_to_blib +lemonldap-ng-manager/MYMETA.json +lemonldap-ng-manager/MYMETA.yml +lemonldap-ng-manager/Makefile +lemonldap-ng-manager/blib/ +lemonldap-ng-manager/pm_to_blib +lemonldap-ng-portal/MYMETA.json +lemonldap-ng-portal/MYMETA.yml +lemonldap-ng-portal/Makefile +lemonldap-ng-portal/blib/ +lemonldap-ng-portal/t/ +.gitignore +lemonldap-ng-portal/pm_to_blib +e2e-tests/lemonldap-ng.ini diff --git a/e2e-tests/lemonldap-ng-ldap.ini b/e2e-tests/lemonldap-ng-ldap.ini index 06ec231b2e75662d2bb9d0e467a4d3133a54fc4a..128765aff150fc5361a4b2d46956f1d763dfe16f 100644 --- a/e2e-tests/lemonldap-ng-ldap.ini +++ b/e2e-tests/lemonldap-ng-ldap.ini @@ -32,6 +32,7 @@ useRedirectOnError = 0 [manager] +enabledModules = conf, sessions, notifications, U2F protection = manager staticPrefix = /static languages = fr, en, vi, ar diff --git a/lemonldap-ng-common/lib/Lemonldap/NG/Common/PSGI/Router.pm b/lemonldap-ng-common/lib/Lemonldap/NG/Common/PSGI/Router.pm index 8cd0ffe733cd38897e834905a43e56075d60f2e4..b49a87db0825870c05e78e2f6a168e66443c178f 100644 --- a/lemonldap-ng-common/lib/Lemonldap/NG/Common/PSGI/Router.pm +++ b/lemonldap-ng-common/lib/Lemonldap/NG/Common/PSGI/Router.pm @@ -318,7 +318,7 @@ Examples: $self->addRoute( books => { ':bookId' => 'book' }, ['GET'] ); -booId parameter will be stored in $req->params('bookId'); +bookId parameter will be stored in $req->params('bookId'); =item to manage http://.../books/127/pages/5 with page(), use: diff --git a/lemonldap-ng-common/lib/Lemonldap/NG/Common/Session/REST.pm b/lemonldap-ng-common/lib/Lemonldap/NG/Common/Session/REST.pm index 015619ed3efbb06a7767963467546ea3e67c328a..e156f1398762787814663def36bf2856f4c0069f 100644 --- a/lemonldap-ng-common/lib/Lemonldap/NG/Common/Session/REST.pm +++ b/lemonldap-ng-common/lib/Lemonldap/NG/Common/Session/REST.pm @@ -33,6 +33,8 @@ sub hAttr { $_[0]->{hiddenAttributes} || $_[0]->conf->{hiddenAttributes}; } +### SEE LEMONLDAP::NG::COMMON::SESSION FOR AVAILABLE FUNCTIONS + sub delSession { my ( $self, $req ) = @_; return $self->sendJSONresponse( $req, { result => 1 } ) @@ -50,6 +52,31 @@ sub delSession { return $self->sendJSONresponse( $req, { result => 1 } ); } +sub delU2FKey { + my ( $self, $req ) = @_; + return $self->sendJSONresponse( $req, { result => 1 } ) + if ( $self->{demoMode} ); + 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 ); + + # Try to read session + my $session = $self->getApacheSession( $mod, $id ) + or return $self->sendError( $req, undef, 400 ); + + # Delete U2F key attributs and update session + $session->data->{_u2fKeyHandle} = ''; + $session->data->{_u2fUserKey} = ''; + $session->update( \%{ $session->data } ); + + Lemonldap::NG::Handler::PSGI::Main->localUnlog( $req, $id ); + if ( $session->error ) { + return $self->sendError( $req, $session->error, 200 ); + } + return $self->sendJSONresponse( $req, { result => 1 } ); +} + sub session { my ( $self, $req, $id, $skey ) = @_; my ( %h, $res ); diff --git a/lemonldap-ng-manager/MANIFEST b/lemonldap-ng-manager/MANIFEST index 7a812ee04875d178e3757815df1a459cbaeb61a0..c184cab047447a0e8e508e869236477d4213a5ca 100644 --- a/lemonldap-ng-manager/MANIFEST +++ b/lemonldap-ng-manager/MANIFEST @@ -20,6 +20,7 @@ lib/Lemonldap/NG/Manager/Conf/Tests.pm lib/Lemonldap/NG/Manager/Conf/Zero.pm lib/Lemonldap/NG/Manager/Notifications.pm lib/Lemonldap/NG/Manager/Sessions.pm +lib/Lemonldap/NG/Manager/U2F.pm Makefile.PL MANIFEST This list of files META.yml @@ -33,6 +34,7 @@ site/coffee/llApp.coffee site/coffee/manager.coffee site/coffee/notifications.coffee site/coffee/sessions.coffee +site/coffee/u2f.coffee site/htdocs/manager.fcgi site/htdocs/manager.psgi site/htdocs/static/bwr/angular-animate/angular-animate.js @@ -146,6 +148,8 @@ site/htdocs/static/js/notifications.js site/htdocs/static/js/notifications.min.js site/htdocs/static/js/sessions.js site/htdocs/static/js/sessions.min.js +site/htdocs/static/js/u2f.js +site/htdocs/static/js/u2f.min.js site/htdocs/static/languages/ar.json site/htdocs/static/languages/en.json site/htdocs/static/languages/fr.json @@ -175,6 +179,7 @@ site/templates/notifications.tpl site/templates/scripts.tpl site/templates/sessions.tpl site/templates/tree.tpl +site/templates/u2f.tpl t/02-HTML-template.t t/03-HTML-forms.t t/05-rest-api.t diff --git a/lemonldap-ng-manager/lib/Lemonldap/NG/Manager.pm b/lemonldap-ng-manager/lib/Lemonldap/NG/Manager.pm index 22c6114bb335527b53c35507ce176593de8355f7..4189624beef885e9b89ce5f10ebf786c3eea8a82 100644 --- a/lemonldap-ng-manager/lib/Lemonldap/NG/Manager.pm +++ b/lemonldap-ng-manager/lib/Lemonldap/NG/Manager.pm @@ -89,8 +89,9 @@ sub init { $self->defaultRoute( $working[0]->defaultRoute ); +# Find out more glyphicones at https://www.w3schools.com/icons/bootstrap_icons_glyphicons.asp my $linksIcons = - { 'conf' => 'cog', 'sessions' => 'duplicate', 'notifications' => 'bell' }; + { 'conf' => 'cog', 'sessions' => 'duplicate', 'notifications' => 'bell', 'U2F' => 'wrench' }; $self->links( [] ); for ( my $i = 0 ; $i < @links ; $i++ ) { diff --git a/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/U2F.pm b/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/U2F.pm new file mode 100644 index 0000000000000000000000000000000000000000..73cb736c50bff4999220c34cd9eedbefc2e84b51 --- /dev/null +++ b/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/U2F.pm @@ -0,0 +1,394 @@ +package Lemonldap::NG::Manager::U2F; + +use 5.10.0; +use utf8; +use strict; +use Mouse; + +use Lemonldap::NG::Common::Session; +use Lemonldap::NG::Common::Conf::Constants; +use Lemonldap::NG::Common::PSGI::Constants; +use Lemonldap::NG::Common::Conf::ReConstants; +use Lemonldap::NG::Common::IPv6; + +use feature 'state'; + +extends 'Lemonldap::NG::Common::Conf::AccessLib', + 'Lemonldap::NG::Common::Session::REST'; + +our $VERSION = '2.0.0'; + +############################# +# I. INITIALIZATION METHODS # +############################# + +use constant defaultRoute => 'u2f.html#/persistent'; + +sub addRoutes { + my ( $self, $conf ) = @_; + + # Remote Procedure Call are defined in Lemonldap::NG::Common::Session::REST + # HTML template + $self->addRoute( 'u2f.html', undef, ['GET'] ) + + # READ + ->addRoute( + u2f => { ':sessionType' => 'sessions' }, + ['GET'] + ) + + # DELETE U2F KEY ATTRIBUTS + ->addRoute( + u2f => { ':sessionType' => { ':sessionId' => 'delU2FKey' } }, + ['DELETE'] + ); + + # UPDATE U2F KEY ATTRIBUTS + # ->addRoute( + # u2f => { ':sessionType' => { ':sessionId' => { ':updateSession' } }, + # ['PUT'] + #); + + $self->setTypes($conf); + + $self->{ipField} ||= 'ipAddr'; + $self->{multiValuesSeparator} ||= '; '; + $self->{hiddenAttributes} //= "_password"; +} + +####################### +# II. DISPLAY METHODS # +####################### + +sub u2f { + my ( $self, $req, $session, $skey ) = @_; + + # Case 1: only one session is required + if ($session) { + return $self->session( $req, $session, $skey ); + } + + my $mod = $self->getMod($req) + or return $self->sendError( $req, undef, 400 ); + my $params = $req->parameters(); + my $type = delete $params->{sessionType}; + $type = $type eq 'global' ? 'SSO' : ucfirst($type); + + my $res; + + # Case 2: list of sessions + + my $whatToTrace = Lemonldap::NG::Handler::PSGI::Main->tsv->{whatToTrace}; + + # 2.1 Get fields to require + my @fields = ( '_httpSessionType', $self->{ipField}, $whatToTrace ); + if ( my $groupBy = $params->{groupBy} ) { + $groupBy =~ s/^substr\((\w+)(?:,\d+(?:,\d+)?)?\)$/$1/ + or $groupBy =~ s/^net(?:4|6|)\(([\w:]+),\d+(?:,\d+)?\)$/$1/; + $groupBy =~ s/^_whatToTrace$/$whatToTrace/o + or push @fields, $groupBy; + } + elsif ( my $order = $params->{orderBy} ) { + $order =~ s/^net(?:4|6|)\(([\w:]+)\)$/$1/; + $order =~ s/^_whatToTrace$/$whatToTrace/o + or push @fields, split( /, /, $order ); + } + else { + push @fields, '_utime'; + } + + # 2.2 Restrict query if possible: search for filters (any query arg that is + # not a keyword) + my $moduleOptions = $mod->{options}; + $moduleOptions->{backend} = $mod->{module}; + my %filters = map { + my $s = $_; + $s =~ s/\b_whatToTrace\b/$whatToTrace/o; + /^(?:(?:group|order)By|doubleIp)$/ + ? () + : ( $s => $params->{$_} ); + } keys %$params; + $filters{_session_kind} = $type; + push @fields, keys(%filters); + { + my %seen; + @fields = grep { !$seen{$_}++ } @fields; + } + + # For now, only one argument can be passed to + # Lemonldap::NG::Common::Apache::Session so just the first filter is + # used + my ($firstFilter) = sort { + $filters{$a} =~ m#^[\w:]+/\d+\*?$# ? 1 + : $filters{$b} =~ m#^[\w:]+/\d+\*?$# ? -1 + : $a eq '_session_kind' ? 1 + : $b eq '_session_kind' ? -1 + : $a cmp $b + } keys %filters; + + # Check if a '*' is required + my $function = 'searchOn'; + $function = 'searchOnExpr' + if ( grep { /\*/ and not m#^[\w:]+/\d+\*?$# } + ( $filters{$firstFilter} ) ); + $self->logger->debug( + "First filter: $firstFilter = $filters{$firstFilter} ($function)"); + + $res = + Lemonldap::NG::Common::Apache::Session->$function( $moduleOptions, + $firstFilter, $filters{$firstFilter}, @fields ); + + return $self->sendJSONresponse( + $req, + { + result => 1, + count => 0, + total => 0, + values => [] + } + ) unless ( $res and %$res ); + + delete $filters{$firstFilter} + unless ( grep { /\*/ and not m#^[\w:]+/\d+\*?$# } + ( $filters{$firstFilter} ) ); + foreach my $k ( keys %filters ) { + $self->logger->debug("Removing unless $k =~ /^$filters{$k}\$/"); + if ( $filters{$k} =~ m#^([\w:]+)/(\d+)\*?$# ) { + my ( $net, $bits ) = ( $1, $2 ); + foreach my $session ( keys %$res ) { + delete $res->{$session} + unless ( net6( $res->{$session}->{$k}, $bits ) eq $net ); + } + } + else { + $filters{$k} =~ s/\./\\./g; + $filters{$k} =~ s/\*/\.\*/g; + foreach my $session ( keys %$res ) { + if ( $res->{$session}->{$k} ) { + delete $res->{$session} + unless ( $res->{$session}->{$k} =~ /^$filters{$k}$/ ); + } + } + } + } + + my $total = ( keys %$res ); + + # 2.4 Special case doubleIp (users connected from more than 1 IP) + if ( defined $params->{doubleIp} ) { + my %r; + + # 2.4.1 Store user IP addresses in %r + foreach my $id ( keys %$res ) { + my $entry = $res->{$id}; + next if ( $entry->{_httpSessionType} ); + $r{ $entry->{$whatToTrace} }->{ $entry->{ $self->{ipField} } }++; + } + + # 2.4.2 Store sessions owned by users that has more than one IP address in $r + my $r; + $total = 0; + foreach my $k ( keys %$res ) { + my @tmp = keys %{ $r{ $res->{$k}->{$whatToTrace} } }; + if ( @tmp > 1 ) { + $total += 1; + $res->{$k}->{_sessionId} = $k; + push @{ $r->{ $res->{$k}->{$whatToTrace} } }, $res->{$k}; + } + } + + # 2.4.3 Store these session in an array. Array elements are : + # { + # uid => whatToTraceFieldValue, + # sessions => [ + # { session => , date => <_utime> }, + # { session => , date => <_utime> }, + # ] + # } + $res = []; + foreach my $uid ( sort keys %$r ) { + push @$res, { + value => $uid, + count => scalar( @{ $r->{$uid} } ), + sessions => [ + map { + { + session => $_->{_sessionId}, + date => $_->{_utime} + } + } @{ $r->{$uid} } + ] + }; + } + } + + # 2.4 Order and group by + # $res will become an array ref here (except for doubleIp, already done below). + + # If "groupBy" is asked, elements will be like: + # { uid => 'foo.bar', count => 3 } + elsif ( my $group = $req->params('groupBy') ) { + my $r; + $group =~ s/\b_whatToTrace\b/$whatToTrace/o; + + # Substrings + if ( $group =~ /^substr\((\w+)(?:,(\d+)(?:,(\d+))?)?\)$/ ) { + my ( $field, $length, $start ) = ( $1, $2, $3 ); + $start ||= 0; + $length = 1 if ( $length < 1 ); + foreach my $k ( keys %$res ) { + $r->{ substr $res->{$k}->{$field}, $start, $length }++ + if ( $res->{$k}->{$field} ); + } + $group = $field; + } + + # Subnets IPv4 + elsif ( $group =~ /^net4\((\w+),(\d)\)$/ ) { + my $field = $1; + my $nb = $2 - 1; + foreach my $k ( keys %$res ) { + if ( $res->{$k}->{$field} =~ /^((((\d+)\.\d+)\.\d+)\.\d+)$/ ) { + my @d = ( $4, $3, $2, $1 ); + $r->{ $d[$nb] }++; + } + } + $group = $field; + } + + # Subnets IPv6 + elsif ( $group =~ /^net6\(([\w:]+),(\d)\)$/ ) { + my $field = $1; + my $bits = $2; + foreach my $k ( keys %$res ) { + $r->{ net6( $res->{$k}->{$field}, $bits ) . "/$bits" }++ + if ( isIPv6( $res->{$k}->{$field} ) ); + } + } + + # Both IPv4 and IPv6 + elsif ( $group =~ /^net\(([\w:]+),(\d+),(\d+)\)$/ ) { + my $field = $1; + my $bits = $2; + my $nb = $3 - 1; + foreach my $k ( keys %$res ) { + if ( isIPv6( $res->{$k}->{$field} ) ) { + $r->{ net6( $res->{$k}->{$field}, $bits ) . "/$bits" }++; + } + elsif ( $res->{$k}->{$field} =~ /^((((\d+)\.\d+)\.\d+)\.\d+)$/ ) + { + my @d = ( $4, $3, $2, $1 ); + $r->{ $d[$nb] }++; + } + } + } + + # Simple field groupBy query + elsif ( $group =~ /^\w+$/ ) { + eval { + foreach my $k ( keys %$res ) { + $r->{ $res->{$k}->{$group} }++; + } + }; + return $self->sendError( + $req, +qq{Use of an uninitialized attribute "$group" to group sessions}, + 400 + ) if ($@); + } + else { + return $self->sendError( $req, 'Syntax error in groupBy', 400 ); + } + + # Build result + $res = [ + sort { + my @a = ( $a->{value} =~ /^(\d+)(?:\.(\d+))*$/ ); + my @b = ( $b->{value} =~ /^(\d+)(?:\.(\d+))*$/ ); + ( @a and @b ) + ? ( $a[0] <=> $b[0] + or $a[1] <=> $b[1] + or $a[2] <=> $b[2] + or $a[3] <=> $b[3] ) + : $a->{value} cmp $b->{value} + } + map { { value => $_, count => $r->{$_} } } keys %$r + ]; + } + + # Else if "orderBy" is asked, $res elements will be like: + # { uid => 'foo.bar', session => } + elsif ( my $f = $req->params('orderBy') ) { + my @fields = split /,/, $f; + my @r = map { + my $tmp = { session => $_ }; + foreach my $f (@fields) { + my $s = $f; + $s =~ s/^net(?:4|6|)\(([\w:]+)\)$/$1/; + $tmp->{$s} = $res->{$_}->{$s}; + } + $tmp + } keys %$res; + while ( my $f = pop @fields ) { + if ( $f =~ s/^net4\((\w+)\)$/$1/ ) { + @r = sort { cmpIPv4( $a->{$f}, $b->{$f} ); } @r; + } + elsif ( $f =~ s/^net6\(([:\w]+)\)$/$1/ ) { + @r = sort { expand6( $a->{$f} ) cmp expand6( $b->{$f} ); } @r; + } + elsif ( $f =~ s/^net\(([:\w]+)\)$/$1/ ) { + @r = sort { + my $ip1 = $a->{$f}; + my $ip2 = $b->{$f}; + isIPv6($ip1) + ? ( + isIPv6($ip2) + ? expand6($ip1) cmp expand6($ip2) + : -1 + ) + : isIPv6($ip2) ? 1 + : cmpIPv4( $ip1, $ip2 ); + } @r; + } + else { + @r = sort { $a->{$f} cmp $b->{$f} } @r; + } + } + $res = [@r]; + } + + # Else, $res elements will be like: + # { session => , date => } + else { + $res = [ + sort { $a->{date} <=> $b->{date} } + map { { session => $_, date => $res->{$_}->{_utime} } } + keys %$res + ]; + } + + return $self->sendJSONresponse( + $req, + { + result => 1, + count => scalar(@$res), + total => $total, + values => $res + } + ); +} + +sub cmpIPv4 { + my @a = split /\./, $_[0]; + my @b = split /\./, $_[1]; + my $cmp = 0; + F: for ( my $i = 0 ; $i < 4 ; $i++ ) { + if ( $a[$i] != $b[$i] ) { + $cmp = $a[$i] <=> $b[$i]; + last F; + } + } + $cmp; +} + +1; diff --git a/lemonldap-ng-manager/site/coffee/u2f.coffee b/lemonldap-ng-manager/site/coffee/u2f.coffee new file mode 100644 index 0000000000000000000000000000000000000000..e19d9fa603d88222d8955100b7106eb5c3214e89 --- /dev/null +++ b/lemonldap-ng-manager/site/coffee/u2f.coffee @@ -0,0 +1,376 @@ +### +# Session explorer +### + +# 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: [ + (t,v) -> + "groupBy=substr(#{t},1)" + (t,v) -> + "#{t}=#{v}*&groupBy=#{t}" + (t,v) -> + "#{t}=#{v}" + ] + ipAddr: [ + (t,v) -> + "groupBy=net(#{t},16,1)" + (t,v) -> + v = v + '.' unless v.match /:/ + "#{t}=#{v}*&groupBy=net(#{t},32,2)" + (t,v) -> + v = v + '.' unless v.match /:/ + "#{t}=#{v}*&groupBy=net(#{t},48,3)" + (t,v) -> + v = v + '.' unless v.match /:/ + "#{t}=#{v}*&groupBy=net(#{t},128,4)" + (t,v) -> + "#{t}=#{v}&groupBy=_whatToTrace" + (t,v,q) -> + q.replace(/\&groupBy.*$/, '') + "&_whatToTrace=#{v}" + ] + _startTime: [ + (t,v) -> + "groupBy=substr(#{t},8)" + (t,v) -> + "#{t}=#{v}*&groupBy=substr(#{t},10)" + (t,v) -> + "#{t}=#{v}*&groupBy=substr(#{t},11)" + (t,v) -> + "#{t}=#{v}*&groupBy=substr(#{t},12)" + (t,v) -> + "#{t}=#{v}*&groupBy=_whatToTrace" + (t,v,q) -> + console.log t + console.log v + console.log q + q.replace(/\&groupBy.*$/, '') + "&_whatToTrace=#{v}" + ] + doubleIp: [ + (t,v) -> + t + (t,v) -> + "_whatToTrace=#{v}&groupBy=ipAddr" + (t,v,q) -> + q.replace(/\&groupBy.*$/, '') + "&ipAddr=#{v}" + ] + +overScheme = + _whatToTrace: (t,v,level,over) -> + if level == 1 + "#{t}=#{v}*&groupBy=substr(#{t},#{(level+over+1)})" + else + null + ipAddr: (t,v,level,over) -> + if level > 0 and level < 4 + "#{t}=#{v}*&groupBy=net(#{t},#{16*level+4*(over+1)},2)" + else + 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'] + saml: ['_idp', '_idpConfKey', '_samlToken', '_lassoSessionDump', '_lassoIdentityDump'] + groups: ['groups', 'hGroups'] + ldap: ['dn'] + BrowserID: ['_browserIdAnswer', '_browserIdAnswerRaw'] + OpenIDConnect: ['_oidc_id_token', '_oidc_OP', '_oidc_access_token'] + +# Menu entries +menu = + session: [ + title: 'deleteU2FKey' + icon: 'trash' + ] + home: [] + +### +# AngularJS application +### +llapp = angular.module 'llngSessionsExplorer', ['ui.tree', 'ui.bootstrap', 'llApp'] + +# Main controller +llapp.controller 'SessionsExplorerCtrl', ['$scope', '$translator', '$location', '$q', '$http', ($scope, $translator, $location, $q, $http) -> + $scope.links = links + $scope.menulinks = menulinks + $scope.staticPrefix = staticPrefix + $scope.scriptname = scriptname + $scope.formPrefix = formPrefix + $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 = (node) -> + $translator.translateField node, 'title' + sessionType = 'global' + + # Handle menu items + $scope.menuClick = (button) -> + if button.popup + window.open button.popup + else + button.action = button.title unless button.action + switch typeof button.action + when 'function' + button.action $scope.currentNode, $scope + when 'string' + $scope[button.action]() + else + console.log typeof button.action + $scope.showM = false + + # SESSION MANAGEMENT + + # Delete U2F key attributs + $scope.deleteU2FKey = -> + $scope.waiting = true + $http['delete']("#{scriptname}u2f/#{sessionType}/#{$scope.currentSession.id}").then (response) -> + $scope.currentSession = null + #$scope.currentScope.remove() + $scope.waiting = false + , (resp) -> + $scope.currentSession = null + #$scope.currentScope.remove() + $scope.waiting = false + $scope.showT = true + + # Open node + $scope.stoggle = (scope) -> + node = scope.$modelValue + if node.nodes.length == 0 + $scope.updateTree node.value, node.nodes, node.level, node.over, node.query, node.count + scope.toggle() + + # Display selected session + $scope.displaySession = (scope) -> + + # Private functions + + # Session preparation + transformSession = (session) -> + _stToStr = (s) -> + s + _insert = (re, title) -> + tmp = [] + reg = new RegExp(re) + for key,value of session + if key.match(reg) and value + tmp.push + title: key + value: value + delete session[key] + if tmp.length > 0 + res.push + title: title + nodes: tmp + time = session._utime + id = session._session_id + + # 1. Replace values if needed + for key, value of session + unless value + delete session[key] + else + if typeof session == 'string' and 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] = _stToStr value + res = [] + + # 2. Push session keys in result, grouped by categories + for category, attrs of categories + subres = [] + for attr in attrs + if session[attr] + subres.push + title: attr + value: session[attr] + 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 + for l in session._loginHistory.successLogin + tmp.push + t: l._utime + title: $scope.localeDate l._utime + value: "Success (IP #{l.ipAddr})" + if session._loginHistory.failedLogin + for l in session._loginHistory.failedLogin + tmp.push + t: l._utime + title: $scope.localeDate l._utime + value: "#{l.error} (IP #{l.ipAddr})" + delete session._loginHistory + tmp.sort (a,b) -> + a.t - b.t + res.push + title: '__loginHistory__' + nodes: tmp + + # 5. Other keys (attributes and macros) + tmp = [] + for key, value of session + tmp.push + title: key + value: value + tmp.sort (a,b) -> + if a.title > b.title then 1 + else if a.title < b.title then -1 + else 0 + + res.push + title: '__attributesAndMacros__' + nodes: tmp + return { + _utime: time + id: id + nodes: res + } + + $scope.currentScope = scope + sessionId = scope.$modelValue.session + $http.get("#{scriptname}sessions/#{sessionType}/#{sessionId}").then (response) -> + $scope.currentSession = transformSession response.data + $scope.showT = false + + $scope.localeDate = (s) -> + d = new Date(s * 1000) + return d.toLocaleString() + + # Function to change interface language + $scope.getLanguage = (lang) -> + $scope.lang = lang + $scope.form = 'white' + $scope.init() + $scope.showM = false + + # URI local path management + pathEvent = (event, next, current) -> + n = next.match /#\/(\w+)/ + sessionType = 'global' + if n == null + $scope.type = '_whatToTrace' + else if n[1].match /^(persistent)$/ + sessionType = RegExp.$1 + $scope.type = '_session_uid' + else + $scope.type = n[1] + $scope.init() + + $scope.$on '$locationChangeSuccess', pathEvent + + # Function to update tree: download value of opened subkey + autoId = 0 + $scope.updateTree = (value, node, level, over, currentQuery, count) -> + $scope.waiting = true + + # Query scheme selection: + + # - if defined above + scheme = if schemes[$scope.type] + schemes[$scope.type] + + # - _updateTime must be displayed as startDate + else if $scope.type == '_updateTime' + schemes._startTime + + # - default to _whatToTrace scheme + else + 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 and 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 (response) -> + data = response.data + if data.result + for n in data.values + 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$/ + n.title = n.value + # 12 digits -> 12:34 + .replace(/^(\d{8})(\d{2})(\d{2})$/,'$2:$3') + # 11 digits -> 12:30 + .replace(/^(\d{8})(\d{2})(\d)$/,'$2:$30') + # 10 digits -> 12h + .replace(/^(\d{8})(\d{2})$/,'$2h') + # 8 digits -> 2016-03-15 + .replace(/^(\d{4})(\d{2})(\d{2})/,'$1-$2-$3') + node.push n + $scope.total = data.total if value == '' + $scope.waiting = false + , (resp) -> + $scope.waiting = false + + # Intialization function + # Simply set $scope.waiting to false during $translator and tree root + # initialization + $scope.init = -> + $scope.waiting = true + $scope.data = [] + $q.all [ + $translator.init $scope.lang + $scope.updateTree '', $scope.data, 0, 0 + ] + .then -> + $scope.waiting = false + , (resp) -> + $scope.waiting = false + + # Query scheme initialization + # Default to '_whatToTrace' + c = $location.path().match /^\/(\w+)/ + $scope.type = if c then c[1] else '_whatToTrace' +] diff --git a/lemonldap-ng-manager/site/htdocs/static/js/sessions.js b/lemonldap-ng-manager/site/htdocs/static/js/sessions.js index b1d8b493eb12d2841c09d75335b139827ca06956..ccc91c4673f7aba22b960b84669ee4d4820d0aee 100644 --- a/lemonldap-ng-manager/site/htdocs/static/js/sessions.js +++ b/lemonldap-ng-manager/site/htdocs/static/js/sessions.js @@ -1,4 +1,4 @@ -// Generated by CoffeeScript 1.10.0 +// Generated by CoffeeScript 1.9.3 /* * Session explorer diff --git a/lemonldap-ng-manager/site/htdocs/static/js/u2f.js b/lemonldap-ng-manager/site/htdocs/static/js/u2f.js new file mode 100644 index 0000000000000000000000000000000000000000..09c041abfa15273e863adbd7d416859bcff5d6e6 --- /dev/null +++ b/lemonldap-ng-manager/site/htdocs/static/js/u2f.js @@ -0,0 +1,409 @@ +// Generated by CoffeeScript 1.9.3 + +/* + * Session explorer + */ + +(function() { + var categories, hiddenAttributes, llapp, max, menu, overScheme, schemes; + + max = 25; + + schemes = { + _whatToTrace: [ + function(t, v) { + return "groupBy=substr(" + t + ",1)"; + }, 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); + } + ] + }; + + overScheme = { + _whatToTrace: function(t, v, level, over) { + if (level === 1) { + return t + "=" + v + "*&groupBy=substr(" + t + "," + (level + over + 1) + ")"; + } else { + return null; + } + }, + ipAddr: function(t, v, level, over) { + if (level > 0 && level < 4) { + return t + "=" + v + "*&groupBy=net(" + t + "," + (16 * level + 4 * (over + 1)) + ",2)"; + } else { + return null; + } + } + }; + + hiddenAttributes = '_password'; + + 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'], + saml: ['_idp', '_idpConfKey', '_samlToken', '_lassoSessionDump', '_lassoIdentityDump'], + groups: ['groups', 'hGroups'], + ldap: ['dn'], + BrowserID: ['_browserIdAnswer', '_browserIdAnswerRaw'], + OpenIDConnect: ['_oidc_id_token', '_oidc_OP', '_oidc_access_token'] + }; + + menu = { + session: [ + { + title: 'deleteU2FKey', + icon: 'trash' + } + ], + home: [] + }; + + + /* + * AngularJS application + */ + + llapp = angular.module('llngSessionsExplorer', ['ui.tree', 'ui.bootstrap', 'llApp']); + + 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.availableLanguages = availableLanguages; + $scope.waiting = true; + $scope.showM = false; + $scope.showT = true; + $scope.data = []; + $scope.currentScope = null; + $scope.currentSession = null; + $scope.menu = menu; + $scope.translateP = $translator.translateP; + $scope.translate = $translator.translate; + $scope.translateTitle = function(node) { + return $translator.translateField(node, 'title'); + }; + sessionType = 'global'; + $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; + }; + $scope.deleteU2FKey = function() { + $scope.waiting = true; + $http['delete'](scriptname + "u2f/" + sessionType + "/" + $scope.currentSession.id).then(function(response) { + $scope.currentSession = null; + return $scope.waiting = false; + }, function(resp) { + $scope.currentSession = null; + return $scope.waiting = false; + }); + return $scope.showT = true; + }; + $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(); + }; + $scope.displaySession = function(scope) { + var sessionId, transformSession; + transformSession = function(session) { + var _insert, _stToStr, attr, attrs, category, i, id, j, k, key, l, len, len1, len2, ref, ref1, res, subres, time, tmp, value; + _stToStr = function(s) { + return s; + }; + _insert = function(re, title) { + var key, reg, tmp, value; + tmp = []; + reg = new RegExp(re); + for (key in session) { + value = session[key]; + if (key.match(reg) && value) { + tmp.push({ + title: key, + value: value + }); + delete session[key]; + } + } + if (tmp.length > 0) { + return res.push({ + title: title, + nodes: tmp + }); + } + }; + time = session._utime; + id = session._session_id; + 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] = _stToStr(value); + } + } + } + } + res = []; + for (category in categories) { + attrs = categories[category]; + subres = []; + for (i = 0, len = attrs.length; i < len; i++) { + attr = attrs[i]; + if (session[attr]) { + subres.push({ + title: attr, + value: session[attr] + }); + delete session[attr]; + } + } + if (subres.length > 0) { + res.push({ + title: "__" + category + "__", + nodes: subres + }); + } + } + _insert('^openid', 'OpenID'); + _insert('^notification_(.+)', '__notificationsDone__'); + if (session._loginHistory) { + tmp = []; + if (session._loginHistory.successLogin) { + ref = session._loginHistory.successLogin; + for (j = 0, len1 = ref.length; j < len1; j++) { + l = ref[j]; + tmp.push({ + t: l._utime, + title: $scope.localeDate(l._utime), + value: "Success (IP " + l.ipAddr + ")" + }); + } + } + if (session._loginHistory.failedLogin) { + ref1 = session._loginHistory.failedLogin; + for (k = 0, len2 = ref1.length; k < len2; k++) { + l = ref1[k]; + tmp.push({ + t: l._utime, + title: $scope.localeDate(l._utime), + value: l.error + " (IP " + l.ipAddr + ")" + }); + } + } + delete session._loginHistory; + tmp.sort(function(a, b) { + return a.t - b.t; + }); + res.push({ + title: '__loginHistory__', + nodes: tmp + }); + } + 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; + } + }); + res.push({ + title: '__attributesAndMacros__', + nodes: tmp + }); + return { + _utime: time, + id: id, + nodes: res + }; + }; + $scope.currentScope = scope; + sessionId = scope.$modelValue.session; + $http.get(scriptname + "sessions/" + sessionType + "/" + sessionId).then(function(response) { + return $scope.currentSession = transformSession(response.data); + }); + return $scope.showT = false; + }; + $scope.localeDate = function(s) { + var d; + d = new Date(s * 1000); + return d.toLocaleString(); + }; + $scope.getLanguage = function(lang) { + $scope.lang = lang; + $scope.form = 'white'; + $scope.init(); + return $scope.showM = false; + }; + 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)$/)) { + sessionType = RegExp.$1; + $scope.type = '_session_uid'; + } else { + $scope.type = n[1]; + } + return $scope.init(); + }; + $scope.$on('$locationChangeSuccess', pathEvent); + autoId = 0; + $scope.updateTree = function(value, node, level, over, currentQuery, count) { + var query, scheme, tmp; + $scope.waiting = true; + scheme = schemes[$scope.type] ? schemes[$scope.type] : $scope.type === '_updateTime' ? schemes._startTime : schemes._whatToTrace; + query = scheme[level]($scope.type, value, currentQuery); + 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; + } + return $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; + if ($scope.type.match(/^(?:start|update)Time$/)) { + n.title = n.value.replace(/^(\d{8})(\d{2})(\d{2})$/, '$2:$3').replace(/^(\d{8})(\d{2})(\d)$/, '$2:$30').replace(/^(\d{8})(\d{2})$/, '$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; + }); + }; + $scope.init = function() { + $scope.waiting = true; + $scope.data = []; + return $q.all([$translator.init($scope.lang), $scope.updateTree('', $scope.data, 0, 0)]).then(function() { + return $scope.waiting = false; + }, function(resp) { + return $scope.waiting = false; + }); + }; + c = $location.path().match(/^\/(\w+)/); + return $scope.type = c ? c[1] : '_whatToTrace'; + } + ]); + +}).call(this); diff --git a/lemonldap-ng-manager/site/htdocs/static/js/u2f.min.js b/lemonldap-ng-manager/site/htdocs/static/js/u2f.min.js new file mode 100644 index 0000000000000000000000000000000000000000..84ed3d7dd11d5f5023b193ca9d517c4050af7ad4 --- /dev/null +++ b/lemonldap-ng-manager/site/htdocs/static/js/u2f.min.js @@ -0,0 +1 @@ +(function(){var c,e,d,b,g,f,a;b=25;a={_whatToTrace:[function(i,h){return"groupBy=substr("+i+",1)"},function(i,h){return i+"="+h+"*&groupBy="+i},function(i,h){return i+"="+h}],ipAddr:[function(i,h){return"groupBy=net("+i+",16,1)"},function(i,h){if(!h.match(/:/)){h=h+"."}return i+"="+h+"*&groupBy=net("+i+",32,2)"},function(i,h){if(!h.match(/:/)){h=h+"."}return i+"="+h+"*&groupBy=net("+i+",48,3)"},function(i,h){if(!h.match(/:/)){h=h+"."}return i+"="+h+"*&groupBy=net("+i+",128,4)"},function(i,h){return i+"="+h+"&groupBy=_whatToTrace"},function(i,h,j){return j.replace(/\&groupBy.*$/,"")+("&_whatToTrace="+h)}],_startTime:[function(i,h){return"groupBy=substr("+i+",8)"},function(i,h){return i+"="+h+"*&groupBy=substr("+i+",10)"},function(i,h){return i+"="+h+"*&groupBy=substr("+i+",11)"},function(i,h){return i+"="+h+"*&groupBy=substr("+i+",12)"},function(i,h){return i+"="+h+"*&groupBy=_whatToTrace"},function(i,h,j){console.log(i);console.log(h);console.log(j);return j.replace(/\&groupBy.*$/,"")+("&_whatToTrace="+h)}],doubleIp:[function(i,h){return i},function(i,h){return"_whatToTrace="+h+"&groupBy=ipAddr"},function(i,h,j){return j.replace(/\&groupBy.*$/,"")+("&ipAddr="+h)}]};f={_whatToTrace:function(i,h,k,j){if(k===1){return i+"="+h+"*&groupBy=substr("+i+","+(k+j+1)+")"}else{return null}},ipAddr:function(i,h,k,j){if(k>0&&k<4){return i+"="+h+"*&groupBy=net("+i+","+(16*k+4*(j+1))+",2)"}else{return null}}};e="_password";c={dateTitle:["_utime","_startTime","_updateTime","_lastAuthnUTime","_lastSeen"],connectionTitle:["ipAddr","_timezone","_url"],authenticationTitle:["_session_id","_user","_password","authenticationLevel"],modulesTitle:["_auth","_userDB","_passwordDB","_issuerDB","_authChoice","_authMulti","_userDBMulti"],saml:["_idp","_idpConfKey","_samlToken","_lassoSessionDump","_lassoIdentityDump"],groups:["groups","hGroups"],ldap:["dn"],BrowserID:["_browserIdAnswer","_browserIdAnswerRaw"],OpenIDConnect:["_oidc_id_token","_oidc_OP","_oidc_access_token"]};g={session:[{title:"deleteU2FKey",icon:"trash"}],home:[]};d=angular.module("llngSessionsExplorer",["ui.tree","ui.bootstrap","llApp"]);d.controller("SessionsExplorerCtrl",["$scope","$translator","$location","$q","$http",function(p,h,i,j,m){var n,l,k,o;p.links=links;p.menulinks=menulinks;p.staticPrefix=staticPrefix;p.scriptname=scriptname;p.formPrefix=formPrefix;p.availableLanguages=availableLanguages;p.waiting=true;p.showM=false;p.showT=true;p.data=[];p.currentScope=null;p.currentSession=null;p.menu=g;p.translateP=h.translateP;p.translate=h.translate;p.translateTitle=function(q){return h.translateField(q,"title")};o="global";p.menuClick=function(q){if(q.popup){window.open(q.popup)}else{if(!q.action){q.action=q.title}switch(typeof q.action){case"function":q.action(p.currentNode,p);break;case"string":p[q.action]();break;default:console.log(typeof q.action)}}return p.showM=false};p.deleteU2FKey=function(){p.waiting=true;m["delete"](scriptname+"u2f/"+o+"/"+p.currentSession.id).then(function(q){p.currentSession=null;return p.waiting=false},function(q){p.currentSession=null;return p.waiting=false});return p.showT=true};p.stoggle=function(q){var r;r=q.$modelValue;if(r.nodes.length===0){p.updateTree(r.value,r.nodes,r.level,r.over,r.query,r.count)}return q.toggle()};p.displaySession=function(r){var s,q;q=function(t){var y,A,E,C,G,J,B,I,H,O,F,K,x,w,u,z,N,M,v,L,D;A=function(P){return P};y=function(S,U){var Q,R,P,T;P=[];R=new RegExp(S);for(Q in t){T=t[Q];if(Q.match(R)&&T){P.push({title:Q,value:T});delete t[Q]}}if(P.length>0){return N.push({title:U,nodes:P})}};v=t._utime;B=t._session_id;for(O in t){D=t[O];if(!D){delete t[O]}else{if(typeof t==="string"&&D.match(/; /)){t[O]=D.split("; ")}if(typeof t[O]!=="object"){if(e.match(new RegExp("\b"+O+"\b"))){t[O]="********"}else{if(O.match(/^(_utime|_lastAuthnUTime|_lastSeen|notification)$/)){t[O]=p.localeDate(D)}else{if(O.match(/^(_startTime|_updateTime)$/)){t[O]=A(D)}}}}}}N=[];for(G in c){C=c[G];M=[];for(J=0,K=C.length;J0){N.push({title:"__"+G+"__",nodes:M})}}y("^openid","OpenID");y("^notification_(.+)","__notificationsDone__");if(t._loginHistory){L=[];if(t._loginHistory.successLogin){u=t._loginHistory.successLogin;for(I=0,x=u.length;IP.title){return 1}else{if(Q.titleb&&f[p.type]){if(t=f[p.type](p.type,y,q,v,x)){v++;w=t;q=q-1}else{v=0}}else{v=0}return m.get(scriptname+"sessions/"+o+"?"+w).then(function(A){var D,B,z,E,C;D=A.data;if(D.result){C=D.values;for(B=0,z=C.length;B + + LemonLDAP::NG sessions explorer + + + + + + +
+
+ + + + + + +
+ + + + + + + + + + + +