package Lemonldap::NG::Manager::Sessions; 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::Session; use Lemonldap::NG::Common::PSGI::Constants; use Lemonldap::NG::Manager::Constants; use feature 'state'; extends 'Lemonldap::NG::Manager::Lib'; has conf => ( is => 'rw', isa => 'HashRef', default => sub { {} } ); our $VERSION = '2.0.0'; ############################# # I. INITIALIZATION METHODS # ############################# use constant defaultRoute => 'sessions.html'; sub addRoutes { my $self = shift; # HTML template $self->addRoute( 'sessions.html', undef, ['GET'] ) # READ ->addRoute( sessions => { ':sessionType' => 'sessions' }, ['GET'] ) # DELETE ->addRoute( sessions => { ':sessionType' => { ':sessionId' => 'delSession' } }, ['DELETE'] ); #TODO: transfer this in Manager.pm ? if ( my $localConf = $self->confAcc->getLocalConf(SESSIONSEXPLORERSECTION) ) { $self->{$_} = $localConf->{$_} foreach ( keys %$localConf ); } my $conf = $self->confAcc->getConf(); # # Return unless configuration is available return 0 unless ($conf); foreach my $type (@sessionTypes) { if ( my $tmp = $self->{ $type . 'Storage' } || $conf->{ $type . 'Storage' } ) { $self->{conf}->{$type}->{module} = $tmp; $self->{conf}->{$type}->{options} = $self->{ $type . 'StorageOptions' } || $conf->{ $type . 'StorageOptions' } || {}; $self->{conf}->{$type}->{kind} = ( $type eq 'global' ? 'SSO' : ucfirst($type) ); } } $self->{ipField} ||= 'ipAddr'; $self->{multiValuesSeparator} ||= '; '; $self->{hiddenAttributes} //= "_password"; } ####################### # II. DISPLAY METHODS # ####################### sub sessions { 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->params(); 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::API->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/^net4\((\w+),\d\)$/$1/; $groupBy =~ s/^_whatToTrace$/$whatToTrace/o or push @fields, $groupBy; } elsif ( my $order = $params->{orderBy} ) { $order =~ s/^net4\((\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; # Check if a '*' is required my $function = 'searchOn'; $function = 'searchOnExpr' if ( grep /\*/, values %filters ); # For now, only one argument can be passed to # Lemonldap::NG::Common::Apache::Session so just the first filter is # used my ($firstFilter) = sort { $a eq '_session_kind' ? 1 : $b eq '_session_kind' ? -1 : $a cmp $b } keys %filters; $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}; foreach my $k ( keys %filters ) { $filters{$k} =~ s/\*/\.\*/g; foreach my $session ( keys %$res ) { if ( $res->{$session}->{$k} ) { delete $res->{$session} unless ( $res->{$session}->{$k} =~ /^$filters{$k}$/ ); print STDERR "Deleted\n"; } } } my $total = ( keys %$res ); # 2.4 Special case doubleIp (users connected from more than 1 IP) if ( $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 += @tmp; $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 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; } # 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 { $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/^net4\((\w+)\)$/$1/; $tmp->{$s} = $res->{$_}->{$s}; } $tmp } keys %$res; while ( my $f = pop @fields ) { if ( $f =~ s/^net4\((\w+)\)$/$1/ ) { @r = sort { my @a = split /\./, $a->{$f}; my @b = split /\./, $b->{$f}; my $cmp = 0; F: for ( my $i = 0 ; $i < 4 ; $i++ ) { if ( $a[$i] != $b[$i] ) { $cmp = $a[$i] <=> $b[$i]; last F; } } $cmp; } @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 delSession { 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 ); my $session = $self->getApacheSession( $mod, $id ); $session->remove; 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 ); my $mod = $self->getMod($req) or return $self->sendError( $req, undef, 400 ); # Try to read session my $apacheSession = $self->getApacheSession( $mod, $id ) or return $self->sendError( $req, undef, 400 ); my %session = %{ $apacheSession->data }; foreach my $k ( keys %session ) { $session{$k} = '**********' if ( $self->{hiddenAttributes} =~ /\b$k\b/ ); $session{$k} = [ split /$self->{multiValuesSeparator}/o, $session{$k} ] if ( $session{$k} =~ /$self->{multiValuesSeparator}/o ); } if ($skey) { return $self->sendJSONresponse( $req, $session{$skey} ); } else { return $self->sendJSONresponse( $req, \%session ); } # TODO: check for utf-8 problems } sub getApacheSession { my ( $self, $mod, $id ) = @_; my $apacheSession = Lemonldap::NG::Common::Session->new( { storageModule => $mod->{module}, storageModuleOptions => $mod->{options}, cacheModule => Lemonldap::NG::Handler::PSGI::API->tsv->{sessionCacheModule}, cacheModuleOptions => Lemonldap::NG::Handler::PSGI::API->tsv->{sessionCacheOptions}, id => $id, kind => $mod->{kind}, } ); if ( $apacheSession->error ) { $self->error( $apacheSession->error ); return undef; } return $apacheSession; } sub getMod { my ( $self, $req ) = @_; my ( $s, $m ); unless ( $s = $req->params('sessionType') ) { $self->error('Session type is required'); return (); } unless ( $m = $self->conf->{$s} ) { $self->error('Unknown (or unconfigured) session type'); return (); } return $m; } 1; __END__ =head1 NAME =encoding utf8 Lemonldap::NG::Manager::Sessions - Sessions explorer component of L. =head1 SYNOPSIS See L. =head1 DESCRIPTION Lemonldap::NG::Manager provides a web interface to manage Lemonldap::NG Web-SSO system. The Perl part of Lemonldap::NG::Manager is the REST server. Web interface is written in Javascript, using AngularJS framework and can be found in `site` directory. The REST API is described in REST-API.md file given in source tree. Lemonldap::NG Manager::Sessions provides the sessions explorer part. =head1 ORGANIZATION Lemonldap::NG Manager::Sessions is the only one module used to explore sessions. The javascript part is in `site/static/js/sessions.js` file. =head1 SEE ALSO L, L =head1 AUTHORS =over =item Clement Oudot, Eclem.oudot@gmail.comE =item François-Xavier Deltombe, Efxdeltombe@gmail.com.E =item Xavier Guimard, Ex.guimard@free.frE =item Thomas Chemineau, Ethomas.chemineau@gmail.comE =back =head1 BUG REPORT Use OW2 system to report bug or ask for features: L =head1 DOWNLOAD Lemonldap::NG is available at L =head1 COPYRIGHT AND LICENSE =over =item Copyright (C) 2015-2016 by Xavier Guimard, Ex.guimard@free.frE =item Copyright (C) 2015-2016 by Clément Oudot, Eclem.oudot@gmail.comE =back This library is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see L. =cut