package Lemonldap::NG::Common::CliSessions; use strict; use Mouse; use JSON; use Lemonldap::NG::Common::Conf; use Lemonldap::NG::Common::Logger::Std; use Lemonldap::NG::Common::Apache::Session; use Lemonldap::NG::Common::Session; use Lemonldap::NG::Common::Util qw/getPSessionID genId2F/; our $VERSION = '2.1.0'; has opts => ( is => 'rw' ); has stdout => ( is => 'ro', default => *STDOUT, ); has stderr => ( is => 'ro', default => *STDERR, ); has conf => ( is => 'ro', default => sub { my $res = Lemonldap::NG::Common::Conf->new( { ( ref $_[0] && $_[0]->{iniFile} ? ( confFile => $_[0]->{iniFile} ) : () ) } ); die $Lemonldap::NG::Common::Conf::msg unless ($res); return $res->getConf(); }, ); sub _to_json { my $self = shift; my $obj = shift; return to_json( $obj, { pretty => 1, canonical => 1 } ); } sub _search { my ($self) = shift; my $backendStorage = ( lc( $self->opts->{backend} || 'global' ) ) . "Storage"; # Handle --persistent if ( $self->opts->{persistent} ) { $backendStorage = "persistentStorage"; } $backendStorage = "globalStorage" unless $self->conf->{$backendStorage}; my $args = $self->conf->{"${backendStorage}Options"}; $args->{backend} = $self->conf->{$backendStorage}; my @fields = @{ $self->opts->{select} || [] }; if ( $self->opts->{idonly} ) { @fields = ('_session_id'); } my $res; if ( $self->opts->{where} ) { # TODO fix regexp? if ( $self->opts->{where} =~ /^(\w+)\s*=\s*(.*)/ ) { my ( $selectField, $value ) = ( $1, $2 ); $res = Lemonldap::NG::Common::Apache::Session->searchOn( $args, $selectField, $value, @fields ); } else { die "Invalid --where option : " . $self->opts->{where}; } } else { $res = Lemonldap::NG::Common::Apache::Session->get_key_from_all_sessions( $args, ( @fields ? [@fields] : () ) ); } return $res; } sub search { my ($self) = shift; my $res = $self->_search(); my $o = $self->stdout; if ( $self->opts->{idonly} ) { print $o map { $res->{$_}->{_session_id} . "\n" } keys %{$res}; } else { print $o $self->_to_json( [ values %{$res} ] ); } return 0; } # Returns the session object, so we can modify it sub _get_one_session { my ( $self, $id, $backend ) = @_; # Lookup backend storage from CLI options my $backendStorage = ( lc( $self->opts->{backend} || 'global' ) ) . "Storage"; # allow argument to overwrite the backend if ($backend) { $backendStorage = $backend . "Storage"; } # Handle --persistent elsif ( $self->opts->{persistent} ) { $backendStorage = "persistentStorage"; $id = getPSessionID($id); } # In any case, fall back to global storage if we couldn't find the backend $backendStorage = "globalStorage" unless $self->conf->{$backendStorage}; my $as = Lemonldap::NG::Common::Session->new( { storageModule => $self->conf->{$backendStorage}, storageModuleOptions => $self->conf->{"${backendStorage}Options"}, id => $id, } ); if ( $as->error ) { my $e = $self->stderr; print $e $as->error; return undef; } else { return $as; } } # Returns only session data sub _get_one_data { my ( $self, $id ) = @_; my $as = $self->_get_one_session($id); if ($as) { my $new; # Filter if ( ( ref( $self->opts->{select} ) eq 'ARRAY' ) and ( scalar @{ $self->opts->{select} } > 0 ) ) { for ( @{ $self->opts->{select} } ) { $new->{$_} = $as->data->{$_} if defined $as->data->{$_}; } } else { $new = $as->data; } return $new; } return undef; } sub delete { my ($self) = shift; my $result = 0; my @sessions; # Run search if a where option was provided if ( $self->opts->{where} ) { my $res = $self->_search(); @sessions = keys %{$res}; } else { @sessions = @_; } my @result; for my $id (@sessions) { my $as = $self->_get_one_session($id); if ($as) { unless ( $as->remove ) { my $e = $self->stderr; print $e $as->error; $result = 1; } } } return $result; } sub get { my $self = shift; my $o = $self->stdout; my @result; for my $id (@_) { my $new = $self->_get_one_data($id); push @result, $new if $new; } print $o $self->_to_json( \@result ); return 0; } sub _get_psession { my ( $self, $uid ) = @_; my $psession_id = getPSessionID($uid); my $res = $self->_get_one_session( $psession_id, 'persistent' ); die "Could not get psession for user $uid" unless $res; return $res; } sub _get_psession_data { my ( $self, $uid ) = @_; my $ps = $self->_get_psession($uid); return $ps->data; } # This method takes a special psession key (oidcConsents, 2fDevices..) # and returns the expected JSON object # idBuilder is a sub that gets applied to every object in the array, yielding the key of # this object in the resulting hash sub _get_psession_special { my ( $self, $target, $keyName, $idBuilder ) = @_; my $psession = $self->_get_psession_data($target); my $res = {}; my $special = $psession->{$keyName} || "[]"; $special = from_json($special); die "Expecting JSON array in $keyName" unless ref($special) eq "ARRAY"; for my $item ( @{$special} ) { my $id = $idBuilder->($item); $res->{$id} = $item; } return $res; } # This method deletes all matching items from an array psession key (oidcConsents, 2fDevices..) # keyBuilder is a sub that gets applied to every object in the array, yielding the value to be # compared against sub _del_psession_special { my ( $self, $target, $specialKeyName, $itemKeyBuilder, @todelete ) = @_; my $psession = $self->_get_psession($target); my $data = $psession->data; my $deleted = 0; my $special = $data->{$specialKeyName} || "[]"; $special = from_json($special); die "Expecting JSON array in $specialKeyName" unless ref($special) eq "ARRAY"; my @new; for my $item ( @{$special} ) { my $id = $itemKeyBuilder->($item); if ( $id and grep { $_ eq $id } @todelete ) { $deleted = $deleted + 1; } else { push @new, $item; } } if ($deleted) { $data->{$specialKeyName} = to_json( [@new] ); } $psession->update($data); } sub consents_get { my $self = shift; my $target = shift; my $o = $self->stdout; my $consents = $self->_get_psession_special( $target, '_oidcConsents', sub { $_[0]->{rp} } ); print $o $self->_to_json($consents); return 0; } sub secondfactors_get { my $self = shift; my $target = shift; my $o = $self->stdout; my $consents = $self->_get_psession_special( $target, '_2fDevices', sub { genId2F( $_[0] ) } ); print $o $self->_to_json($consents); return 0; } sub consents_delete { my $self = shift; my $target = shift; my @ids = @_; return unless @ids; $self->_del_psession_special( $target, '_oidcConsents', sub { $_[0]->{rp} }, @ids ); return 0; } sub secondfactors_delete { my $self = shift; my $target = shift; my @ids = @_; return unless @ids; $self->_del_psession_special( $target, '_2fDevices', sub { genId2F( $_[0] ) }, @ids ); return 0; } sub secondfactors_delType { my $self = shift; my $target = shift; my @types = @_; return unless @types; $self->_del_psession_special( $target, '_2fDevices', sub { $_[0]->{type} }, @types ); return 0; } sub setKey { my $self = shift; my $id = shift; die "Unever number of arguments" unless ( @_ % 2 ) == 0; my %newvalues = (@_); die "Session ID and new values not provided" unless $id and %newvalues; my $as = $self->_get_one_session($id); die unless $as; my $data = $as->data; for ( keys %newvalues ) { $data->{$_} = $newvalues{$_}; } $as->update($data); return 0; } sub delKey { my $self = shift; my $id = shift; my @todelete = @_; die "Session ID and key names not provided" unless $id and @todelete; my $as = $self->_get_one_session($id); die unless $as; my $data = $as->data; for (@todelete) { # Weird, I know. But that's how # Lemonldap::NG::Common::Session::update works $data->{$_} = undef; } $as->update($data); return 0; } sub run { my $self = shift; my $action = shift; my $opts = shift; unless ( ref $self ) { $self = $self->new; } unless ($action) { die 'nothing to do, aborting'; } $self->opts($opts); # Simple commands if ( $action =~ /^(?:get|search|delete|setKey|delKey)$/ ) { return $self->$action(@_); } # Subcommands and target elsif ( $action =~ /^(?:secondfactors|consents)$/ ) { my $subcommand = shift; unless ( $subcommand and @_ ) { die "Missing subcommand and target for $action"; } my $func = "${action}_${subcommand}"; if ( $self->can($func) ) { return $self->$func(@_); } else { die "Unknown subcommand $subcommand for action $action"; } } else { die "unknown action $action. Only get or search are allowed"; } } 1;