diff --git a/lemonldap-ng-common/lib/Lemonldap/NG/Common/CliSessions.pm b/lemonldap-ng-common/lib/Lemonldap/NG/Common/CliSessions.pm index 8765ea351..aae98e5f7 100644 --- a/lemonldap-ng-common/lib/Lemonldap/NG/Common/CliSessions.pm +++ b/lemonldap-ng-common/lib/Lemonldap/NG/Common/CliSessions.pm @@ -84,7 +84,6 @@ sub _search { } return $res; - } sub search { @@ -264,12 +263,86 @@ sub _del_psession_special { if ($deleted) { $data->{$specialKeyName} = to_json( [@new] ); } + + # TODO should this be in the if??? $psession->update($data); } +sub _migrateu2f_device { + my ( $self, $device ) = @_; + + my $credential_id = $device->{_keyHandle}; + my $_userKey = $device->{_userKey}; + + eval { require Authen::WebAuthn }; + if ($@) { + die "Missing Authen::WebAuthn dependency: $@"; + } + + my $credential_pubkey = + Authen::WebAuthn::convert_raw_ecc_to_cose($_userKey); + + return { + type => "WebAuthn", + name => "$device->{name}", + _credentialId => "$credential_id", + _credentialPublicKey => "$credential_pubkey", + _signCount => 0, + epoch => "$device->{epoch}", + }; +} + +sub _migrateu2f { + my $self = shift; + my $target = shift; + + my $psession = $self->_get_psession($target); + my $data = $psession->data; + my $migrated = 0; + + my $_2fDevices = $data->{_2fDevices} || "[]"; + $_2fDevices = from_json($_2fDevices); + + die "Expecting JSON array in _2fDevices" + unless ref($_2fDevices) eq "ARRAY"; + + my @new_2fDevices = @{$_2fDevices}; + my @u2f_devices = grep { $_->{type} eq "U2F" } @{$_2fDevices}; + + my %migrated_devices; + for my $u2f_device (@u2f_devices) { + my $migrated_device = $self->_migrateu2f_device($u2f_device); + $migrated_devices{ $migrated_device->{_credentialId} } = + $migrated_device; + } + + for my $migrated_device ( keys %migrated_devices ) { + + # If credentialId is not already present + unless ( + grep { + $_->{type} eq "WebAuthn" + and $_->{_credentialId} eq $migrated_device + } @new_2fDevices + ) + { + push @new_2fDevices, $migrated_devices{$migrated_device}; + $migrated = 1; + } + } + + if ($migrated) { + $data->{_2fDevices} = to_json( [@new_2fDevices] ); + $psession->update($data); + } + +} + sub consents_get { - my $self = shift; - my $target = shift; + my $self = shift; + my $target = shift; + return 0 unless $target; + my $o = $self->stdout; my $consents = $self->_get_psession_special( $target, '_oidcConsents', sub { $_[0]->{rp} } ); @@ -278,8 +351,10 @@ sub consents_get { } sub secondfactors_get { - my $self = shift; - my $target = shift; + my $self = shift; + my $target = shift; + return 0 unless $target; + my $o = $self->stdout; my $consents = $self->_get_psession_special( $target, '_2fDevices', sub { genId2F( $_[0] ) } ); @@ -290,8 +365,11 @@ sub secondfactors_get { sub consents_delete { my $self = shift; my $target = shift; - my @ids = @_; - return unless @ids; + return 0 unless $target; + + my @ids = @_; + return 0 unless @ids; + $self->_del_psession_special( $target, '_oidcConsents', sub { $_[0]->{rp} }, @ids ); return 0; @@ -300,23 +378,66 @@ sub consents_delete { sub secondfactors_delete { my $self = shift; my $target = shift; - my @ids = @_; - return unless @ids; + return 0 unless $target; + + my @ids = @_; + return 0 unless @ids; $self->_del_psession_special( $target, '_2fDevices', sub { genId2F( $_[0] ) }, @ids ); return 0; } +sub _get_psession_targets { + my ( $self, @args ) = @_; + + if ( $self->opts->{where} or $self->opts->{all} ) { + $self->opts->{persistent} = 1; + + if ( $self->opts->{all} ) { + delete $self->opts->{where}; + } + + my $res = $self->_search(); + return ( map { $res->{$_}->{_session_uid} } keys %{$res} ); + } + else { + return @args; + } +} + sub secondfactors_delType { - my $self = shift; - my $target = shift; - my @types = @_; - return unless @types; - $self->_del_psession_special( $target, '_2fDevices', sub { $_[0]->{type} }, - @types ); + my $self = shift; + + my $target; + unless ( $self->opts->{where} or $self->opts->{all} ) { + $target = shift; + } + + my @types = @_; + return 0 unless @types; + + my @targets = $self->_get_psession_targets($target); + for my $target (@targets) { + $self->_del_psession_special( $target, '_2fDevices', + sub { $_[0]->{type} }, @types ); + } + return 0; } +sub secondfactors_migrateu2f { + my ( $self, @ids ) = @_; + my $result = 0; + my @sessions = $self->_get_psession_targets(@ids); + + for my $id (@sessions) { + if ( !$self->_migrateu2f($id) ) { + $result = 1; + } + } + return $result; +} + sub setKey { my $self = shift; my $id = shift; @@ -380,8 +501,8 @@ sub run { # Subcommands and target elsif ( $action =~ /^(?:secondfactors|consents)$/ ) { my $subcommand = shift; - unless ( $subcommand and @_ ) { - die "Missing subcommand and target for $action"; + unless ($subcommand) { + die "Missing subcommand $action"; } my $func = "${action}_${subcommand}"; if ( $self->can($func) ) { diff --git a/lemonldap-ng-common/scripts/lemonldap-ng-sessions b/lemonldap-ng-common/scripts/lemonldap-ng-sessions index 186a65eaf..d68e21060 100755 --- a/lemonldap-ng-common/scripts/lemonldap-ng-sessions +++ b/lemonldap-ng-common/scripts/lemonldap-ng-sessions @@ -22,6 +22,7 @@ GetOptions( 'help|h' => \$help, 'select|s=s@' => \$opts->{select}, 'where|w=s' => \$opts->{where}, + 'all|a' => \$opts->{all}, 'backend|b=s' => \$opts->{backend}, 'persistent|p' => \$opts->{persistent}, 'id-only|i' => \$opts->{idonly}, @@ -80,7 +81,7 @@ if ( $action eq "setKey" ) { } if ( $action eq "secondfactors" ) { - unless ( @ARGV >= 2 ) { + unless ( @ARGV >= 1 ) { pod2usage( -exitval => 1, -verbose => 99, @@ -238,8 +239,10 @@ Commands: delete [ ...] delete second factors for a user. The ID must match one of the IDs returned by the "show" command. - delType [ ...] + delType [|--all] [ ...] delete all second factors of a given type for a user + migrateu2f [|--all] + migrate U2F device registrations to WebAuthn device registrations =head2 Consents