From e477a1cef89688cc7789185d2854b166dd4af28f Mon Sep 17 00:00:00 2001 From: Christophe Maudoux Date: Sat, 13 Feb 2021 23:30:35 +0100 Subject: [PATCH] Append unit test & Doc (#2458) --- doc/sources/admin/checkdevops.rst | 46 +++ doc/sources/admin/index_plugins.rst | 1 + doc/sources/admin/start.rst | 51 ++-- lemonldap-ng-portal/MANIFEST | 3 + .../NG/Portal/Plugins/CheckDevOps.pm | 6 +- lemonldap-ng-portal/t/56-CheckDevOps.t | 271 ++++++++++++++++++ .../t/67-CheckUser-with-token.t | 2 +- lemonldap-ng-portal/t/test-lib.pm | 6 + 8 files changed, 359 insertions(+), 27 deletions(-) create mode 100644 doc/sources/admin/checkdevops.rst create mode 100644 lemonldap-ng-portal/t/56-CheckDevOps.t diff --git a/doc/sources/admin/checkdevops.rst b/doc/sources/admin/checkdevops.rst new file mode 100644 index 000000000..20d499f7e --- /dev/null +++ b/doc/sources/admin/checkdevops.rst @@ -0,0 +1,46 @@ +Check DevOps plugin +=================== + +This plugin can be used to check the :doc:`DevOps` file. + +Configuration +------------- +Just enable it in the manager (section “plugins”). + +Usage +----- +When enabled, ``/checkdevops`` URL path is handled by this plugin. +Then, you can paste a file to test your rules and headers. + +Example +~~~~~~~ +DevOps handler requires a rules.json file to define +access rules and headers: + +.. code-block:: json + + { + "rules": { + "^/admin": "$uid eq 'admin'", + "default": "accept" + }, + "headers": { + "Auth-User": "$uid" + } + } + +.. note:: + + This plugin displays ALL user session attributes except + the hidden ones. + + You have to restrict access to specific users like DevOps teams + by setting an access rule like other VirtualHosts. + + By example: ``$groups =~ /\bdevops\b/`` + +.. attention:: + + Be careful to not display secret attributes. + + checkDevOps plugin uses hidden attributes option. \ No newline at end of file diff --git a/doc/sources/admin/index_plugins.rst b/doc/sources/admin/index_plugins.rst index 81d37f0f2..b0447a538 100644 --- a/doc/sources/admin/index_plugins.rst +++ b/doc/sources/admin/index_plugins.rst @@ -8,6 +8,7 @@ Plugins autosignin bruteforceprotection cda + checkdevops checkstate checkuser viewer diff --git a/doc/sources/admin/start.rst b/doc/sources/admin/start.rst index 968ed10e6..e6219ab2b 100644 --- a/doc/sources/admin/start.rst +++ b/doc/sources/admin/start.rst @@ -271,24 +271,25 @@ Name Description :doc:`Auto Signin` |new| Auto Signin Addon :doc:`Brute Force protection` |new| User must wait to log in after some failed login attempts :doc:`CDA` Cross Domain Authentication +:doc:`Check DevOps` [5]_ |new| Check DevOps handler file plugin :doc:`Check state` |new| Check state plugin (test page) -:doc:`Check user` [5]_ |new| Check access rights, transmitted headers and session attibutes for a specific user and URL +:doc:`Check user` [6]_ |new| Check access rights, transmitted headers and session attibutes for a specific user and URL :doc:`Configuration viewer` |new| Edit WebSSO configuration in Read Only mode -:doc:`Context switching` [6]_\ |new| Switch context other users +:doc:`Context switching` [7]_\ |new| Switch context other users :doc:`Custom` Write a custom plugin -:doc:`Decrypt value` [7]_\ |image35| Decrypt ciphered values -:doc:`Display login history` +:doc:`Decrypt value` [8]_\ |image35| Decrypt ciphered values +:doc:`Display login history` Display Success/Fails logins :doc:`Force Authentication` Force authentication to access to Portal -:doc:`Global Logout` [8]_ Suggest to close all opened sessions at logout +:doc:`Global Logout` [9]_ Suggest to close all opened sessions at logout :doc:`Grant Sessions` Rules to apply before allowing a user to open a session -:doc:`Impersonation` [9]_\ |new| Allow users to use another identity -:doc:`Find user` [10]_\ |new| Search for user account -:doc:`Notifications system` +:doc:`Impersonation` [10]_\ |new| Allow users to use another identity +:doc:`Find user` [11]_\ |new| Search for user account +:doc:`Notifications system` DIsplay a message during log in process :doc:`Portal Status` Experimental portal status page :doc:`Public pages` Enable public pages system -:doc:`Refresh session API` [11]_ Plugin that provides an API to refresh a user session -:doc:`Reset password by mail` -:doc:`Reset certificate by mail` [12]_\ |image37| Allow users to reset their certificate +:doc:`Refresh session API` [12]_ Plugin that provides an API to refresh a user session +:doc:`Reset password by mail` Send a mail to reset its password +:doc:`Reset certificate by mail` [13]_\ |image37| Allow users to reset their certificate :doc:`REST services` |new| REST server for :doc:`Proxy` :doc:`SOAP services` |deprecated| SOAP server for :doc:`Proxy` :doc:`Stay connected` |new| Enable persistent connection on same browser @@ -306,12 +307,12 @@ Handlers are software control agents to be installed on your web servers ==================================================================== ========== ============================================================= =========================================== ================================================================================== =============================================== ====================================================================================================================== Handler type Apache LLNG FastCGI/uWSGI server (Nginx, or :doc:`SSOaaS`) `Plack servers `__ Node.js ( `express apps `__\ or :doc:`SSOaaS`) :doc:`Self protected apps` Comment ==================================================================== ========== ============================================================= =========================================== ================================================================================== =============================================== ====================================================================================================================== -Main *(default handler)* ✔ ✔ ✔ :doc:`Partial` ** [13]_ ** ✔ +Main *(default handler)* ✔ ✔ ✔ :doc:`Partial` ** [14]_ ** ✔ :doc:`AuthBasic` ✔ ✔ ✔ ✔ Designed for some server-to-server applications :doc:`CDA` ✔ ✔ ✔ ✔ For Cross Domain Authentication :doc:`DevOps` (:doc:`SSOaaS`) |new| ✔ ✔ ✔ ✔ Allows application developers to define their own rules and headers inside their applications :doc:`DevOpsST` (:doc:`SSOaaS`) |new| ✔ ✔ ✔ ✔ Enables both :doc:`DevOps` and :doc:`Service Token` -:doc:`OAuth2` [14]_\ |new| ✔ ✔ ✔ ✔ Uses OpenID Connect/OAuth2 access token to check authentication and authorization, can be used to protect Web Services +:doc:`OAuth2` [15]_\ |new| ✔ ✔ ✔ ✔ Uses OpenID Connect/OAuth2 access token to check authentication and authorization, can be used to protect Web Services :doc:`Secure Token` ✔ ✔ ✔ Designed to secure exchanges between a LLNG reverse-proxy and a remote app :doc:`Service Token` |new| *(Server-to-Server)* ✔ ✔ ✔ ✔ ✔ Designed to permit underlying requests *(API-Based Infrastructure)* :doc:`Zimbra PreAuth` ✔ ✔ ✔ @@ -562,45 +563,49 @@ by your language code): :doc:`Radius second factor` is available with LLNG ≥ 2.0.6 .. [4] + :doc:`Check DevOps file plugin` are available with LLNG ≥ + 2.0.12 + +.. [5] :doc:`Additional second factors` are available with LLNG ≥ 2.0.6 -.. [5] +.. [6] :doc:`Check user plugin` is available with LLNG ≥ 2.0.3 -.. [6] +.. [7] :doc:`Context switching plugin` is available with LLNG ≥ 2.0.6 -.. [7] +.. [8] :doc:`Decrypt value plugin` is available with LLNG ≥ 2.0.7 -.. [8] +.. [9] :doc:`Global Logout plugin` is available with LLNG ≥ 2.0.7 -.. [9] +.. [10] :doc:`Impersonation plugin` is available with LLNG ≥ 2.0.3 -.. [10] +.. [11] :doc:`Find user plugin` is available with LLNG ≥ 2.0.11 -.. [11] +.. [12] :doc:`Refresh session API plugin` is available with LLNG ≥ 2.0.7 -.. [12] +.. [13] :doc:`Reset certificate by mail plugin` is available with LLNG ≥ 2.0.7 -.. [13] +.. [14] :doc:`Node.js handler` has not yet reached the same level of functionalities -.. [14] +.. [15] :doc:`OAuth2 Handler` is available with LLNG ≥ 2.0.4 .. |image0| image:: /icons/kthememgr.png diff --git a/lemonldap-ng-portal/MANIFEST b/lemonldap-ng-portal/MANIFEST index b048eb3f0..7aa2524b2 100644 --- a/lemonldap-ng-portal/MANIFEST +++ b/lemonldap-ng-portal/MANIFEST @@ -107,6 +107,7 @@ lib/Lemonldap/NG/Portal/Plugins/AutoSignin.pm lib/Lemonldap/NG/Portal/Plugins/BruteForceProtection.pm lib/Lemonldap/NG/Portal/Plugins/CDA.pm lib/Lemonldap/NG/Portal/Plugins/CertificateResetByMail.pm +lib/Lemonldap/NG/Portal/Plugins/CheckDevOps.pm lib/Lemonldap/NG/Portal/Plugins/CheckState.pm lib/Lemonldap/NG/Portal/Plugins/CheckUser.pm lib/Lemonldap/NG/Portal/Plugins/ContextSwitching.pm @@ -389,6 +390,7 @@ site/templates/bootstrap/2fregisters.tpl site/templates/bootstrap/captcha.tpl site/templates/bootstrap/casBack2Url.tpl site/templates/bootstrap/certificateReset.tpl +site/templates/bootstrap/checkdevops.tpl site/templates/bootstrap/checklogins.tpl site/templates/bootstrap/checkuser.tpl site/templates/bootstrap/confirm.tpl @@ -632,6 +634,7 @@ t/43-MailPasswordReset.t t/44-CertificateResetByMail-Demo.t t/44-CertificateResetByMail-LDAP.t t/50-IssuerGet.t +t/56-CheckDevOps.t t/57-GlobalLogout-with-Double-cookies-Single-session.t t/57-GlobalLogout-with-Double-cookies.t t/57-GlobalLogout-without-Timer.t diff --git a/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Plugins/CheckDevOps.pm b/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Plugins/CheckDevOps.pm index add2a2d3f..0303d5613 100644 --- a/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Plugins/CheckDevOps.pm +++ b/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Plugins/CheckDevOps.pm @@ -33,8 +33,8 @@ has ott => ( sub init { my ($self) = @_; - $self->addAuthRoute( checkdevops => 'run', ['POST'] ) - ->addAuthRoute( checkdevops => 'display', ['GET'] ); + $self->addAuthRoute( checkdevops => 'run', ['POST'] ) + ->addAuthRouteWithRedirect( checkdevops => 'display', ['GET'] ); return 1; } @@ -148,7 +148,7 @@ sub run { ? 'allowed' : 'forbidden' } - } keys %{ $json->{rules} }; + } sort keys %{ $json->{rules} }; my $rules_list = join ', ', map { "$_->{uri}:$_->{access}" } @$rules; $self->logger->debug("CheckDevOps compiled rules: $rules_list"); diff --git a/lemonldap-ng-portal/t/56-CheckDevOps.t b/lemonldap-ng-portal/t/56-CheckDevOps.t new file mode 100644 index 000000000..7ee75c44a --- /dev/null +++ b/lemonldap-ng-portal/t/56-CheckDevOps.t @@ -0,0 +1,271 @@ +use Test::More; +use strict; +use IO::String; +use JSON; + +BEGIN { + require 't/test-lib.pm'; +} + +my $res; +my $file = '{ + "rules": { + "^/deny": "deny", + "^/testno": "$uid ne qq{dwho}", + "^/testyes": "$uid eq qq{dwho}", + "default": "accept" + }, + "headers": { + "User": "$uid", + "Mail": "$mail", + "Name": "$cn", + "UA": "$UA" + } +}'; +my $bad_file = '{ + "rules": { + "^/testno": "$uid ne qq{dwho}" + "default": "accept" + }, + "headers": { + "User": "$uid", + } +}'; + +my $client = LLNG::Manager::Test->new( { + ini => { + logLevel => 'error', + authentication => 'Demo', + userDB => 'Same', + requireToken => 1, + checkDevOps => 1, + hiddenAttributes => 'mail UA' + } + } +); + +## Try to authenticate +ok( $res = $client->_get( '/', accept => 'text/html' ), 'Get Menu', ); +count(1); +my ( $host, $url, $query ) = + expectForm( $res, '#', undef, 'user', 'password', 'token' ); + +$query =~ s/user=/user=dwho/; +$query =~ s/password=/password=dwho/; +ok( + $res = $client->_post( + '/', + IO::String->new($query), + length => length($query), + accept => 'text/html', + ), + 'Auth query' +); +count(1); + +my $id = expectCookie($res); +expectRedirection( $res, 'http://auth.example.com/' ); + +# CheckDevOps form +# ---------------- +ok( + $res = $client->_get( + '/checkdevops', + cookie => "lemonldap=$id", + accept => 'text/html' + ), + 'CheckDevOps form', +); +( $host, $url, $query ) = + expectForm( $res, undef, '/checkdevops', 'checkDevOpsFile', 'token' ); +ok( $res->[2]->[0] =~ m%%, + 'Found trspan="checkDevOps"' ) + or explain( $res->[2]->[0], 'trspan="checkDevOps"' ); +count(2); + +# POST without token +# ------------------ +ok( + $res = $client->_post( + '/checkdevops', + IO::String->new(''), + cookie => "lemonldap=$id", + length => 0, + accept => 'text/html' + ), + 'POST checkdevops without token' +); +ok( $res->[2]->[0] =~ m%[2]->[0], 'trspan="PE81"' ); +count(2); +( $host, $url, $query ) = + expectForm( $res, undef, '/checkdevops', 'checkDevOpsFile', 'token' ); + +# POST bad file +# ------------- +$query .= "&checkDevOpsFile=$bad_file"; +ok( + $res = $client->_post( + '/checkdevops', + IO::String->new($query), + cookie => "lemonldap=$id", + length => length($query), + accept => 'text/html' + ), + 'POST checkdevops with bad file' +); +ok( $res->[2]->[0] =~ m%%, + 'Found PE_BAD_DEVOPS_FILE' ) + or explain( $res->[2]->[0], 'trspan="PE104"' ); +count(2); +( $host, $url, $query ) = + expectForm( $res, undef, '/checkdevops', 'checkDevOpsFile', 'token' ); + +# POST file +# --------- +$query .= "&checkDevOpsFile=$file"; +ok( + $res = $client->_post( + '/checkdevops', + IO::String->new($query), + cookie => "lemonldap=$id", + length => length($query), + accept => 'text/html' + ), + 'POST checkdevops with file' +); + +# Headers +ok( $res->[2]->[0] =~ m%HEADERS%, + 'HEADERS' ) + or explain( $res->[2]->[0], 'HEADERS' ); +ok( $res->[2]->[0] =~ m%Name: Doctor Who
%, 'Hearder Name found' ) + or explain( $res->[2]->[0], 'Hearder Name' ); +ok( $res->[2]->[0] =~ m%User: dwho
%, 'Hearder User found' ) + or explain( $res->[2]->[0], 'Hearder User' ); + +# Rules +ok( $res->[2]->[0] =~ m%RULES%, 'RULES' ) + or explain( $res->[2]->[0], 'RULES' ); +ok( $res->[2]->[0] =~ m%\^/testno: %, 'testno' ) + or explain( $res->[2]->[0], 'testno' ); +ok( $res->[2]->[0] =~ m%default: %, 'default' ) + or explain( $res->[2]->[0], 'default' ); +ok( $res->[2]->[0] =~ m%\^/testyes: %, 'testyes' ) + or explain( $res->[2]->[0], 'testyes' ); +ok( $res->[2]->[0] =~ m%\^/deny: %, 'deny' ) + or explain( $res->[2]->[0], 'deny' ); +ok( $res->[2]->[0] =~ m%\$uid eq qq{dwho}"%, 'file' ) + or explain( $res->[2]->[0], 'file' ); +count(10); +( $host, $url, $query ) = + expectForm( $res, undef, '/checkdevops', 'checkDevOpsFile', 'token' ); + +# POST file (json) +# ---------------- +$query .= "&checkDevOpsFile=$file"; +ok( + $res = $client->_post( + '/checkdevops', + IO::String->new($query), + cookie => "lemonldap=$id", + length => length($query), + ), + 'POST checkdevops with file' +); +ok( $res = eval { from_json( $res->[2]->[0] ) }, 'Response is JSON' ) + or print STDERR "$@\n" . Dumper($res); +ok( $res->{ALERTE} eq 'alert-info', 'alert-info found' ) + or print STDERR Dumper($res); +ok( $res->{FILE} =~ /headers/, 'headers found' ) + or print STDERR Dumper($res); +ok( $res->{FILE} =~ /rules/, 'rules found' ) + or print STDERR Dumper($res); +ok( $res->{FILE} =~ /"\$uid ne qq{dwho}"/, 'rule found' ) + or print STDERR Dumper($res); +count(6); + +# POST bad file (json) +# -------------------- +ok( + $res = $client->_get( + '/checkdevops', + cookie => "lemonldap=$id", + accept => 'text/html' + ), + 'CheckDevOps form', +); +( $host, $url, $query ) = + expectForm( $res, undef, '/checkdevops', 'checkDevOpsFile', 'token' ); + +$query .= "&checkDevOpsFile=$bad_file"; +ok( + $res = $client->_post( + '/checkdevops', + IO::String->new($query), + cookie => "lemonldap=$id", + length => length($query), + ), + 'POST checkdevops with file' +); +ok( $res = eval { from_json( $res->[2]->[0] ) }, 'Response is JSON' ) + or print STDERR "$@\n" . Dumper($res); +ok( $res->{ALERTE} eq 'alert-danger', 'alert-danger found' ) + or print STDERR Dumper($res); +ok( $res->{FILE} eq '', 'No file found' ) + or print STDERR Dumper($res); +ok( $res->{MSG} eq 'PE104', 'PE104 found' ) + or print STDERR Dumper($res); +ok( $res->{TOKEN} =~ /^\d{10}_\d+$/, 'Token found' ) + or print STDERR Dumper($res); +count(7); + +# POST with an expired token (json) +# --------------------------------- +ok( + $res = $client->_post( + '/checkdevops', + IO::String->new($query), + cookie => "lemonldap=$id", + length => length($query), + ), + 'POST checkdevops without token' +); +ok( $res = eval { from_json( $res->[2]->[0] ) }, 'Response is JSON' ) + or print STDERR "$@\n" . Dumper($res); +ok( $res->{ALERTE} eq 'alert-warning', 'alert-warning found' ) + or print STDERR Dumper($res); +ok( $res->{TOKEN} =~ /^\d{10}_\d+$/, 'Token found' ) + or print STDERR Dumper($res); +ok( $res->{FILE} eq '', 'No file found' ) + or print STDERR Dumper($res); +ok( $res->{MSG} eq 'PE82', 'PE82 found' ) + or print STDERR Dumper($res); +count(6); + +# POST without token (json) +# ------------------------- +ok( + $res = $client->_post( + '/checkdevops', + IO::String->new(''), + cookie => "lemonldap=$id", + length => 0, + ), + 'POST checkdevops without token' +); +ok( $res = eval { from_json( $res->[2]->[0] ) }, 'Response is JSON' ) + or print STDERR "$@\n" . Dumper($res); +ok( $res->{ALERTE} eq 'alert-warning', 'alert-warning found' ) + or print STDERR Dumper($res); +ok( $res->{TOKEN} =~ /^\d{10}_\d+$/, 'Token found' ) + or print STDERR Dumper($res); +ok( $res->{MSG} eq 'PE81', 'PE81 found' ) + or print STDERR Dumper($res); +count(5); + +$client->logout($id); + +clean_sessions(); + +done_testing( count() ); diff --git a/lemonldap-ng-portal/t/67-CheckUser-with-token.t b/lemonldap-ng-portal/t/67-CheckUser-with-token.t index 1b178b672..9be3fb44b 100644 --- a/lemonldap-ng-portal/t/67-CheckUser-with-token.t +++ b/lemonldap-ng-portal/t/67-CheckUser-with-token.t @@ -51,7 +51,7 @@ my $id = expectCookie($res); expectRedirection( $res, 'http://auth.example.com/' ); # CheckUser form -# ------------------------ +# -------------- ok( $res = $client->_get( '/checkuser', diff --git a/lemonldap-ng-portal/t/test-lib.pm b/lemonldap-ng-portal/t/test-lib.pm index 193a8902c..027bf21ba 100644 --- a/lemonldap-ng-portal/t/test-lib.pm +++ b/lemonldap-ng-portal/t/test-lib.pm @@ -309,6 +309,12 @@ m@]+(?:value="([^"]*?)")?#gs, %fields ); + # Add textarea + %fields = ( + $res->[2]->[0] =~ + m#]+(?:value="([^"]*?)")?#gs, + %fields + ); my $query = buildForm( \%fields ); foreach my $f (@requiredFields) { ok( exists $fields{$f}, qq{ Field "$f" is defined} );