From 1672732dcdb2c3d5e0a96c1af24a28ff8f19c9bb Mon Sep 17 00:00:00 2001 From: Maxime Besson Date: Tue, 28 Mar 2023 15:36:08 +0200 Subject: [PATCH 1/5] Add functions to test CIDR ranges (#2903) --- .../lib/Lemonldap/NG/Common/Safelib.pm | 14 +++++++++++++- .../lib/Lemonldap/NG/Handler/Main/Reload.pm | 3 +++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/lemonldap-ng-common/lib/Lemonldap/NG/Common/Safelib.pm b/lemonldap-ng-common/lib/Lemonldap/NG/Common/Safelib.pm index e635f11ee5..1b1481cde7 100644 --- a/lemonldap-ng-common/lib/Lemonldap/NG/Common/Safelib.pm +++ b/lemonldap-ng-common/lib/Lemonldap/NG/Common/Safelib.pm @@ -10,6 +10,7 @@ use Encode; use MIME::Base64; use Lemonldap::NG::Common::IPv6; use JSON::XS; +use Net::CIDR; use Date::Parse; our $VERSION = '2.0.15'; @@ -18,9 +19,20 @@ our $VERSION = '2.0.15'; # Note that only functions, not methods, can be written here our $functions = [ - qw(&checkLogonHours &date &dateToTime &checkDate &basic &unicode2iso &unicode2isoSafe &iso2unicode &iso2unicodeSafe &groupMatch &isInNet6 &varIsInUri &has2f_internal) + qw(&checkLogonHours &date &dateToTime &checkDate &basic &unicode2iso &unicode2isoSafe &iso2unicode &iso2unicodeSafe &groupMatch &isInNet6 &varIsInUri &has2f_internal &ipInSubnet) ]; +## @function boolean ipInSubnet(string ip, string network, ... ) +# Function to check if an IP is part of a network +# @param $ip IP address to test +# @param $network Network in CIDR notation. +# You can call the function with multiple networks +# @return 1 true, 0 else +sub ipInSubnet { + my ( $ip, @networks ) = @_; + return Net::CIDR::cidrlookup( $ip, @networks ); +} + ## @function boolean checkLogonHours(string logon_hours, string syntax, string time_correction, boolean default_access) # Function to check logon hours # @param $logon_hours string representing allowed logon hours (GMT) diff --git a/lemonldap-ng-handler/lib/Lemonldap/NG/Handler/Main/Reload.pm b/lemonldap-ng-handler/lib/Lemonldap/NG/Handler/Main/Reload.pm index 246bf547a3..5c67cf8118 100644 --- a/lemonldap-ng-handler/lib/Lemonldap/NG/Handler/Main/Reload.pm +++ b/lemonldap-ng-handler/lib/Lemonldap/NG/Handler/Main/Reload.pm @@ -665,6 +665,9 @@ sub substitute { # handle has2f $expr =~ s/\bhas2f\(([^),]*)\)/has2f_internal(\$s,$1)/g; + # handle inSubnet + $expr =~ s/\binSubnet\(([^)]*)\)/ipInSubnet(\$r->{env}->{REMOTE_ADDR},$1)/g; + return $expr; } -- GitLab From 3677924cf803f1191f82bbb2cf703d03762b1cb3 Mon Sep 17 00:00:00 2001 From: Maxime Besson Date: Tue, 28 Mar 2023 15:36:30 +0200 Subject: [PATCH 2/5] Add new dependency (#2903) --- debian/control | 2 ++ lemonldap-ng-common/Makefile.PL | 1 + rpm/lemonldap-ng.spec | 1 + 3 files changed, 4 insertions(+) diff --git a/debian/control b/debian/control index 0759b4eeb7..fc539ce60f 100644 --- a/debian/control +++ b/debian/control @@ -41,6 +41,7 @@ Build-Depends-Indep: fonts-urw-base35 | gsfonts , libmime-tools-perl , libmouse-perl, libnet-cidr-lite-perl , + libnet-cidr-perl , libnet-ldap-perl , libio-socket-timeout-perl , libnet-openid-consumer-perl , @@ -233,6 +234,7 @@ Depends: ${misc:Depends}, libhtml-template-perl, libjson-perl, libjson-xs-perl, + libnet-cidr-perl, libmouse-perl, libclass-xsaccessor-perl, libplack-perl, diff --git a/lemonldap-ng-common/Makefile.PL b/lemonldap-ng-common/Makefile.PL index ac58a00bbe..d1fdd1ea93 100644 --- a/lemonldap-ng-common/Makefile.PL +++ b/lemonldap-ng-common/Makefile.PL @@ -84,6 +84,7 @@ WriteMakefile( 'JSON' => 0, 'JSON::XS' => 0, 'Mouse' => 0, + 'Net::CIDR' => 0, 'Plack' => 0, 'URI' => 0, 'LWP::UserAgent' => 0, diff --git a/rpm/lemonldap-ng.spec b/rpm/lemonldap-ng.spec index b49f0b21fb..d91b0393cc 100644 --- a/rpm/lemonldap-ng.spec +++ b/rpm/lemonldap-ng.spec @@ -138,6 +138,7 @@ BuildRequires: perl(MIME::Base64) BuildRequires: perl(MIME::Entity) BuildRequires: perl(mod_perl2) BuildRequires: perl(Mouse) +BuildRequires: perl(Net::CIDR) BuildRequires: perl(Net::Facebook::Oauth2) BuildRequires: perl(Net::LDAP) BuildRequires: perl(Net::LDAP::Extension::SetPassword) -- GitLab From 75f25061a52e985308428c8d6b619dd03b20b758 Mon Sep 17 00:00:00 2001 From: Maxime Besson Date: Tue, 28 Mar 2023 15:36:43 +0200 Subject: [PATCH 3/5] Documentation for #2903 --- doc/sources/admin/extendedfunctions.rst | 40 +++++++++++++++++++++++++ doc/sources/admin/rules_examples.rst | 11 +++++++ 2 files changed, 51 insertions(+) diff --git a/doc/sources/admin/extendedfunctions.rst b/doc/sources/admin/extendedfunctions.rst index c932d62865..90a4942e20 100644 --- a/doc/sources/admin/extendedfunctions.rst +++ b/doc/sources/admin/extendedfunctions.rst @@ -34,6 +34,8 @@ Inside this jail, you can access to: * groupMatch_ * has2f_ (|new| in version 2.0.10) * inGroup_ (|new| in version 2.0.8) + * inSubnet_ (|new| in version 2.17) + * ipInSubnet_ (|new| in version 2.17) * isInNet6_ * iso2unicode_ * iso2unicodeSafe_ (|new| in version 2.0.15) @@ -306,6 +308,25 @@ Usage example: The function returns 1 if the user belongs to the given group, and 0 if they don't. +inSubnet +~~~~~~~~ + +.. versionadded:: 2.17 + +This function lets you test if the request's IP address (``REMOTE_ADDR`` +environment variable) belongs to a certain subnet. You can give multiple subnets. + +Usage example: + +:: + + inSubnet('127.0.0.0/8') + + inSubnet('10.0.0.0/8', '192.168.0.0/16') + +The function returns 1 if the user's IP is in provided subnets, and 0 if +it is not. + isInNet6 ~~~~~~~~ @@ -316,6 +337,25 @@ IP address is local*: isInNet6($ipAddr, 'fe80::/10') +ipInSubnet +~~~~~~~~~~ + +.. versionadded:: 2.17 + +This function lets you test if the provided IP address belongs to a certain +subnet. You can give multiple subnets. + +Usage example: + +:: + + ipInSubnet($ENV{REMOTE_ADDR}, '127.0.0.0/8') + + ipInSubnet($ENV{REMOTE_ADDR}, '10.0.0.0/8', '192.168.0.0/16') + +The function returns 1 if provided IP is in one of the subnets, and 0 if +it is not. + iso2unicode ~~~~~~~~~~~ diff --git a/doc/sources/admin/rules_examples.rst b/doc/sources/admin/rules_examples.rst index a820e8b5c1..b25864d985 100644 --- a/doc/sources/admin/rules_examples.rst +++ b/doc/sources/admin/rules_examples.rst @@ -68,6 +68,13 @@ attribute you see there can be used in a rule! $authenticationLevel >= 3 +- Filtering on IP subnet + +:: + + # since 2.17 + inSubnet('192.168.0.0/16') + - Filtering on Authentication method :: @@ -100,8 +107,12 @@ Using environment variables :: + # Before 2.17 $env->{REMOTE_ADDR} =~ /^10\./ + # Since 2.17 + inSubnet('10.0.0.0/8') + - Comparing requested URI :: -- GitLab From 6f0a85f5feeb978365369d8e249df9e755b3fc7f Mon Sep 17 00:00:00 2001 From: Maxime Besson Date: Tue, 28 Mar 2023 15:36:54 +0200 Subject: [PATCH 4/5] Unit tests for #2903 --- .../t/12-Lemonldap-NG-Handler-Jail.t | 23 +++++++++++++------ .../t/13-Lemonldap-NG-Handler-Fake-Safe.t | 23 +++++++++++++------ 2 files changed, 32 insertions(+), 14 deletions(-) diff --git a/lemonldap-ng-handler/t/12-Lemonldap-NG-Handler-Jail.t b/lemonldap-ng-handler/t/12-Lemonldap-NG-Handler-Jail.t index 31468e6bb2..c3bc747fcc 100644 --- a/lemonldap-ng-handler/t/12-Lemonldap-NG-Handler-Jail.t +++ b/lemonldap-ng-handler/t/12-Lemonldap-NG-Handler-Jail.t @@ -6,7 +6,7 @@ # change 'tests => 1' to 'tests => last_test_to_print'; use strict; -use Test::More tests => 22; +use Test::More tests => 26; require 't/test.pm'; BEGIN { use_ok('Lemonldap::NG::Handler::Main::Jail') } @@ -76,8 +76,7 @@ ok( 'checkDate extended function is defined' ); is( - $code->( - { + $code->( { _2fDevices => "[{\"name\":\"MyTOTP\",\"_secret\":\"g5fsxwf4d34biemlojsbbvhgtskrssos\",\"epoch\":1602173208,\"type\":\"TOTP\"}]" }, @@ -87,8 +86,7 @@ is( "Function works" ); is( - $code->( - { + $code->( { _2fDevices => "[{\"name\":\"MyTOTP\",\"_secret\":\"g5fsxwf4d34biemlojsbbvhgtskrssos\",\"epoch\":1602173208,\"type\":\"TOTP\"}]" }, @@ -98,8 +96,7 @@ is( "Function works" ); is( - $code->( - { + $code->( { _2fDevices => "[{\"name\":\"MyTOTP\",\"_secret\":\"g5fsxwf4d34biemlojsbbvhgtskrssos\",\"epoch\":1602173208,\"type\":\"TOTP\"}]" }, @@ -108,6 +105,18 @@ is( "Function works" ); +$sub = "sub { return(ipInSubnet(\@_)) }"; +$code = $jail->jail_reval($sub); +ok( + ( defined($code) and ref($code) eq 'CODE' ), + 'ipInSubnet extended function is defined' +); +is( $code->( "127.0.0.1", "127.0.0.0/8" ), 1, "ipInSubnet works as expected" ); +is( $code->( "192.168.0.1", "127.0.0.0/8" ), 0, + "ipInSubnet works as expected" ); +is( $code->( "192.168.0.1", "127.0.0.0/8", "192.168.0.0/16" ), + 1, "ipInSubnet works as expected" ); + $sub = "sub { return()"; $code = $jail->jail_reval($sub); ok( ( not defined($code) ), 'Syntax error yields undef result' ); diff --git a/lemonldap-ng-handler/t/13-Lemonldap-NG-Handler-Fake-Safe.t b/lemonldap-ng-handler/t/13-Lemonldap-NG-Handler-Fake-Safe.t index a02573aab7..944983b07d 100644 --- a/lemonldap-ng-handler/t/13-Lemonldap-NG-Handler-Fake-Safe.t +++ b/lemonldap-ng-handler/t/13-Lemonldap-NG-Handler-Fake-Safe.t @@ -5,7 +5,7 @@ # change 'tests => 1' to 'tests => last_test_to_print'; -use Test::More tests => 16; +use Test::More tests => 20; require 't/test.pm'; BEGIN { use_ok('Lemonldap::NG::Handler::Main::Jail') } @@ -69,8 +69,7 @@ ok( 'checkDate extended function is defined' ); is( - $has2f->( - { + $has2f->( { _2fDevices => "[{\"name\":\"MyTOTP\",\"_secret\":\"g5fsxwf4d34biemlojsbbvhgtskrssos\",\"epoch\":1602173208,\"type\":\"TOTP\"}]" }, @@ -80,8 +79,7 @@ is( "Function works" ); is( - $has2f->( - { + $has2f->( { _2fDevices => "[{\"name\":\"MyTOTP\",\"_secret\":\"g5fsxwf4d34biemlojsbbvhgtskrssos\",\"epoch\":1602173208,\"type\":\"TOTP\"}]" }, @@ -90,8 +88,7 @@ is( "Function works" ); is( - $has2f->( - { + $has2f->( { _2fDevices => "[{\"name\":\"MyTOTP\",\"_secret\":\"g5fsxwf4d34biemlojsbbvhgtskrssos\",\"epoch\":1602173208,\"type\":\"TOTP\"}]" }, @@ -101,6 +98,18 @@ is( "Function works" ); +$sub = "sub { return(ipInSubnet(\@_)) }"; +$code = $jail->jail_reval($sub); +ok( + ( defined($code) and ref($code) eq 'CODE' ), + 'ipInSubnet extended function is defined' +); +is( $code->( "127.0.0.1", "127.0.0.0/8" ), 1, "ipInSubnet works as expected" ); +is( $code->( "192.168.0.1", "127.0.0.0/8" ), 0, + "ipInSubnet works as expected" ); +is( $code->( "192.168.0.1", "127.0.0.0/8", "192.168.0.0/16" ), + 1, "ipInSubnet works as expected" ); + $sub = "sub { return()"; $code = $jail->jail_reval($sub); ok( ( not defined($code) ), 'Syntax error yields undef result' ); -- GitLab From ed3a28a7cdd83b94ac65f71b519ce7f08ed6ff1e Mon Sep 17 00:00:00 2001 From: Maxime Besson Date: Tue, 28 Mar 2023 16:08:41 +0200 Subject: [PATCH 5/5] Portal unit tests for rule syntax --- lemonldap-ng-portal/t/01-BuildRule.t | 81 ++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 lemonldap-ng-portal/t/01-BuildRule.t diff --git a/lemonldap-ng-portal/t/01-BuildRule.t b/lemonldap-ng-portal/t/01-BuildRule.t new file mode 100644 index 0000000000..9b80b59497 --- /dev/null +++ b/lemonldap-ng-portal/t/01-BuildRule.t @@ -0,0 +1,81 @@ +use warnings; +use Test::More; +use strict; +use Lemonldap::NG::Portal::Main::Request; + +require 't/test-lib.pm'; + +my $app = LLNG::Manager::Test->new( { + ini => { + logLevel => 'error', + useSafeJail => 1, + } + } +)->p; + +my %base_req = ( + REQUEST_URI => '/', + REMOTE_ADDR => '192.168.2.3', + PATH_INFO => '/' +); + +# For each rule, define a series of tests structured like this: +# [ a, b, c ] +# where: +# a : a series of environment variables to be set in the $req object +# b : a hash of sessionInfo +# c : expected rule result + +my @rulestotest = ( { + rule => "inGroup('toto')", + tests => [ + [ {}, { hGroups => { "titi" => 1 } }, 0 ], + [ {}, { hGroups => { "toto" => 1 } }, 1 ], + ] + }, + { + rule => "inSubnet('127.0.0.0/8')", + tests => [ [ {}, {}, 0 ], [ { REMOTE_ADDR => '127.0.0.2' }, {}, 1 ], ] + }, + { + rule => "inSubnet('127.0.0.0/8', '192.168.0.0/16')", + tests => [ + [ {}, {}, 1 ], + [ { REMOTE_ADDR => '127.0.0.2' }, {}, 1 ], + [ { REMOTE_ADDR => '10.0.0.1' }, {}, 0 ], + ] + }, + { + rule => "ipInSubnet(\$ipAddr, '127.0.0.0/8', '192.168.0.0/16')", + tests => [ + [ {}, { ipAddr => "192.168.2.3" }, 1 ], + [ {}, { ipAddr => "127.8.7.6" }, 1 ], + [ {}, { ipAddr => "10.0.1.2" }, 0 ], + ] + }, +); + +{ + no warnings; + $Data::Dumper::Indent = 0; + $Data::Dumper::Terse = 1; +} + +for my $rule (@rulestotest) { + my $rule_text = $rule->{rule}; + my $sub = $app->buildRule($rule_text); + for my $test ( @{ $rule->{tests} } ) { + my ( $req_param, $session_info, $output ) = @{$test}; + my $req = Lemonldap::NG::Portal::Main::Request->new( + { %base_req, %$req_param } ); + is( + $sub->( $req, $session_info ), + $output, + "Rule $rule_text on input " + . Dumper( [ $req_param, $session_info ] ) + . " returned $output" + ); + } +} + +done_testing(); -- GitLab