package Lemonldap::NG::Manager::2ndFA; use 5.10.0; use utf8; use strict; use Mouse; use MIME::Base64 qw(encode_base64 decode_base64); use Lemonldap::NG::Common::Session; use Lemonldap::NG::Common::Conf::Constants; use Lemonldap::NG::Common::PSGI::Constants; use Lemonldap::NG::Common::Conf::ReConstants; use feature 'state'; extends 'Lemonldap::NG::Manager::Plugin', 'Lemonldap::NG::Common::Conf::AccessLib', 'Lemonldap::NG::Common::Session::REST'; our $VERSION = '2.1.0'; ############################# # I. INITIALIZATION METHODS # ############################# use constant defaultRoute => '2ndfa.html'; sub init { my ( $self, $conf ) = @_; # Remote Procedure are defined in Lemonldap::NG::Common::Session::REST # HTML template $self->addRoute( '2ndfa.html', undef, ['GET'] ) ->addRoute( sfa => { ':sessionType' => 'sfa' }, ['GET'] ) # DELETE 2FA DEVICE ->addRoute( sfa => { ':sessionType' => { ':sessionId' => 'del2F' } }, ['DELETE'] ); $self->setTypes($conf); $self->{multiValuesSeparator} ||= '; '; $self->{hiddenAttributes} //= "_password"; $self->{TOTPCheck} = $self->{U2FCheck} = $self->{UBKCheck} = '1'; return 1; } ################### # II. 2FA METHODS # ################### sub del2F { my ( $self, $req, $session, $skey ) = @_; my $mod = $self->getMod($req) or return $self->sendError( $req, 'Bad mode', 400 ); my $params = $req->parameters(); my $type = $params->{type} or return $self->sendError( $req, 'Missing "type" parameter', 400 ); my $epoch = $params->{epoch} or return $self->sendError( $req, 'Missing "epoch" parameter', 400 ); if ( $type =~ /\b(?:U2F|TOTP|UBK)\b/ ) { $self->logger->debug( "Call procedure delete2F with type=$type and epoch=$epoch"); return $self->delete2F( $req, $session, $skey ); } else { return $self->sendError( $req, 'Bad value "type" parameter', 400 ); } } ######################## # III. DISPLAY METHODS # ######################## sub sfa { 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, 'Bad mode', 400 ); my $params = $req->parameters(); my $type = delete $params->{sessionType}; $type = 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', $whatToTrace, '_2fDevices' ); if ( my $groupBy = $params->{groupBy} ) { $groupBy =~ s/^substr\((\w+)(?:,\d+(?:,\d+)?)?\)$/$1/; $groupBy =~ s/^_whatToTrace$/$whatToTrace/o or push @fields, $groupBy; } 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}; # Select 2FA sessions to display foreach (qw(U2F TOTP UBK)) { $self->{ $_ . 'Check' } = delete $params->{ $_ . 'Check' } if ( defined $params->{ $_ . 'Check' } ); } my %filters = map { my $s = $_; $s =~ s/\b_whatToTrace\b/$whatToTrace/o; /^groupBy$/ ? () : ( $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}\$/"); $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}$/ ); } } } # Remove sessions without at least one 2F device(s) $self->logger->debug( "Removing sessions without at least one 2F device(s)..."); foreach my $session ( keys %$res ) { delete $res->{$session} unless ( defined $res->{$session}->{_2fDevices} and $res->{$session}->{_2fDevices} =~ /"type":\s*"(?:U2F|TOTP|UBK)"/s ); } # Filter 2FA sessions if needed $self->logger->debug("Filtering 2F sessions..."); my $all = ( keys %$res ); foreach (qw(U2F TOTP UBK)) { if ( $self->{ $_ . 'Check' } eq '2' ) { foreach my $session ( keys %$res ) { delete $res->{$session} unless ( defined $res->{$session}->{_2fDevices} and $res->{$session}->{_2fDevices} =~ /"type":\s*"$_"/s ); } $self->logger->debug( "Removing sessions unless a $_ device is registered"); } } my $total = ( keys %$res ); $self->logger->debug("2FA session(s) left : $total / $all"); if ( 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; } # 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, $res elements will be like: # { session => , userId => <_session_uid> } else { $res = [ sort { $a->{date} <=> $b->{date} } map { { session => $_, userId => $res->{$_}->{_session_uid} } } keys %$res ]; } return $self->sendJSONresponse( $req, { result => 1, count => scalar(@$res), total => $total, values => $res } ); } 1;