2020-05-11 19:38:23 +02:00
|
|
|
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;
|
2020-08-17 18:18:52 +02:00
|
|
|
use Lemonldap::NG::Common::Util qw/getPSessionID genId2F/;
|
2020-05-11 19:38:23 +02:00
|
|
|
|
2020-09-04 17:59:00 +02:00
|
|
|
our $VERSION = '2.0.9';
|
2020-05-11 19:38:23 +02:00
|
|
|
|
|
|
|
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 {
|
2020-12-21 21:30:01 +01:00
|
|
|
die "Invalid --where option : " . $self->opts->{where};
|
2020-05-11 19:38:23 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
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";
|
2020-08-17 18:13:11 +02:00
|
|
|
$id = getPSessionID($id);
|
2020-05-11 19:38:23 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
# 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;
|
2020-12-21 21:27:08 +01:00
|
|
|
my @sessions;
|
|
|
|
|
|
|
|
# Run search if a where option was provided
|
|
|
|
if ( $self->opts->{where} ) {
|
|
|
|
my $res = $self->_search();
|
|
|
|
@sessions = keys %{$res};
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
@sessions = @_;
|
|
|
|
}
|
2020-05-11 19:38:23 +02:00
|
|
|
|
|
|
|
my @result;
|
2020-12-21 21:27:08 +01:00
|
|
|
for my $id (@sessions) {
|
2020-05-11 19:38:23 +02:00
|
|
|
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 ) = @_;
|
2020-08-17 18:13:11 +02:00
|
|
|
my $psession_id = getPSessionID($uid);
|
2020-05-11 19:38:23 +02:00
|
|
|
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',
|
2020-08-17 18:18:52 +02:00
|
|
|
sub { genId2F( $_[0] ) } );
|
2020-05-11 19:38:23 +02:00
|
|
|
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',
|
2020-08-17 18:18:52 +02:00
|
|
|
sub { genId2F( $_[0] ) }, @ids );
|
2020-05-11 19:38:23 +02:00
|
|
|
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;
|