Append an option to normalize headers & Improve unit tests + doc (#2604)

This commit is contained in:
Christophe Maudoux 2021-09-01 21:37:41 +02:00
parent a431e0521a
commit eab4d4a31e
26 changed files with 99 additions and 31 deletions

View File

@ -11,15 +11,18 @@ Just enable it in the manager (section “plugins”).
- **Parameters**:
- **Activation**: Enable / Disable this plugin
- **Download file**: Allow users to download DevOps file from a remote server by
providing an URL (By example: http://myapp.example.com:8080). Plugin will
try to retrieve remote file by sending a request (i.e.
http://myapp.example.com:8080/rules.json)
- **Download file**: Allow users to download DevOps file from a
remote server by providing an URL
(By example: http://myapp.example.com:8080). Plugin will
try to retrieve remote file by sending a request
(i.e. http://myapp.example.com:8080/rules.json)
- **Display normalized headers**: Display headers as they are sent
Usage
-----
When enabled, ``/checkdevops`` URL path is handled by this plugin.
Then, you can paste a file to test your rules and headers.
Then, you can paste a file to test your rules and headers or
provide an URL to download the ``rules.json`` file.
Example
~~~~~~~
@ -48,7 +51,7 @@ access rules and headers:
By example: ``$groups =~ /\bdevops\b/``
.. attention::
.. danger::
Be careful to not display secret attributes.

View File

@ -143,6 +143,7 @@
"locationRules": {
"auth.example.com" : {
"(?#checkUser)^/checkuser": "$uid eq \"dwho\"",
"(?#checkDevOps)^/checkdevops": "$uid eq \"dwho\"",
"(?#errors)^/lmerror/": "accept",
"default" : "accept"
},

View File

@ -5,7 +5,7 @@ use strict;
use Exporter 'import';
use base qw(Exporter);
our $VERSION = '2.0.13';
our $VERSION = '2.0.14';
# CONSTANTS
@ -31,7 +31,7 @@ use constant DEFAULTCONFBACKENDOPTIONS => (
);
our $hashParameters = qr/^(?:(?:l(?:o(?:ca(?:lSessionStorageOption|tionRule)|goutService)|dapExportedVar|wp(?:Ssl)?Opt)|(?:(?:d(?:emo|bi)|webID)ExportedVa|exported(?:Heade|Va)|issuerDBGetParamete)r|f(?:indUser(?:Exclud|Search)ingAttribute|acebookExportedVar)|re(?:moteGlobalStorageOption|st2f(?:Verify|Init)Arg|loadUrl)|g(?:r(?:antSessionRule|oup)|lobalStorageOption)|n(?:otificationStorageOption|ginxCustomHandler)|macro)s|o(?:idc(?:S(?:ervice(?:DynamicRegistrationEx(?:portedVar|traClaim)s|MetaDataAuthnContext)|torageOptions)|RPMetaData(?:(?:Option(?:sExtraClaim)?|ExportedVar|ScopeRule|Macro)s|Node)|OPMetaData(?:(?:ExportedVar|Option)s|J(?:SON|WKS)|Node))|penIdExportedVars)|c(?:as(?:A(?:ppMetaData(?:(?:ExportedVar|Option|Macro)s|Node)|ttributes)|S(?:rvMetaData(?:(?:ExportedVar|Option)s|Node)|torageOptions))|(?:ustom(?:Plugins|Add)Param|heckUserHiddenHeader|ombModule)s)|s(?:aml(?:S(?:PMetaData(?:(?:ExportedAttribute|Option|Macro)s|Node|XML)|torageOptions)|IDPMetaData(?:(?:ExportedAttribute|Option)s|Node|XML))|essionDataToRemember|laveExportedVars|fExtra)|a(?:(?:daptativeAuthenticationLevelR|ut(?:hChoiceMod|oSigninR))ules|pplicationList)|p(?:ersistentStorageOptions|o(?:rtalSkinRules|st))|v(?:hostOptions|irtualHost)|S(?:MTPTLSOpts|SLVarIf))$/;
our $arrayParameters = qr/^mySessionAuthorizedRWKeys$/;
our $boolKeys = qr/^(?:s(?:aml(?:IDP(?:MetaDataOptions(?:(?:Check(?:S[LS]OMessageSignatur|Audienc|Tim)|IsPassiv)e|A(?:llow(?:LoginFromIDP|ProxiedAuthn)|daptSessionUtime)|Force(?:Authn|UTF8)|StoreSAMLToken|RelayStateURL)|SSODescriptorWantAuthnRequestsSigned)|S(?:P(?:MetaDataOptions(?:(?:CheckS[LS]OMessageSignatur|OneTimeUs)e|EnableIDPInitiatedURL|ForceUTF8)|SSODescriptor(?:WantAssertion|AuthnRequest)sSigned)|erviceUseCertificateInResponse)|DiscoveryProtocol(?:Activation|IsPassive)|CommonDomainCookieActivation|UseQueryStringSpecific|MetadataForceUTF8)|f(?:RemovedUseNotif|OnlyUpgrade)|kip(?:Upgrade|Renew)Confirmation|oap(?:Session|Config)Server|t(?:ayConnecte|orePasswor)d|laveDisplayLogo|howLanguages|slByAjax)|o(?:idc(?:RPMetaDataOptions(?:A(?:llow(?:(?:ClientCredentials|Password)Grant|Offline)|ccessToken(?:Claims|JWT))|Re(?:freshToken|quirePKCE)|LogoutSessionRequired|IDTokenForceClaims|BypassConsent|Public)|ServiceAllow(?:(?:AuthorizationCode|Implicit|Hybrid)Flow|DynamicRegistration|OnlyDeclaredScopes)|OPMetaDataOptions(?:(?:CheckJWTSignatur|UseNonc)e|StoreIDToken))|ldNotifFormat)|p(?:ortal(?:Display(?:Re(?:freshMyRights|setPassword|gister)|CertificateResetByMail|GeneratePassword|PasswordPolicy)|E(?:rrorOn(?:ExpiredSession|MailNotFound)|nablePasswordDisplay)|(?:CheckLogin|Statu)s|OpenLinkInNewWindow|ForceAuthn|AntiFrame)|roxyUseSoap)|c(?:a(?:sS(?:rvMetaDataOptions(?:Gateway|Renew)|trictMatching)|ptcha_(?:register|login|mail)_enabled)|o(?:ntextSwitching(?:Allowed2fModifications|StopWithLogout)|mpactConf|rsEnabled)|heck(?:DevOps(?:Download)?|State|User|XSS)|rowdsec|da)|l(?:dap(?:(?:G(?:roup(?:DecodeSearchedValu|Recursiv)|etUserBeforePasswordChang)|UsePasswordResetAttribut)e|(?:AllowResetExpired|Set)Password|ChangePasswordAsUser|PpolicyControl|ITDS)|oginHistoryEnabled)|no(?:tif(?:ication(?:Server(?:(?:POS|GE)T|DELETE)?|sExplorer)?|y(?:Deleted|Other))|AjaxHook)|i(?:ssuerDB(?:OpenID(?:Connect)?|SAML|CAS|Get)Activation|mpersonationSkipEmptyValues)|u(?:se(?:RedirectOn(?:Forbidden|Error)|SafeJail)|2fUserCanRemoveKey|pgradeSession)|re(?:st(?:(?:Password|Session|Config|Auth)Server|ExportSecretKeys)|freshSessions)|br(?:uteForceProtection(?:IncrementalTempo)?|owsersDontStorePassword)|d(?:is(?:ablePersistentStorage|playSessionId)|biDynamicHashEnabled)|(?:mai(?:lOnPasswordChang|ntenanc)|vhostMaintenanc)e|to(?:tp2fUserCanRemoveKey|kenUseGlobalStorage)|g(?:roupsBeforeMacros|lobalLogoutTimer)|a(?:voidAssignment|ctiveTimer)|h(?:ideOldPassword|ttpOnly)|yubikey2fUserCanRemoveKey|krb(?:RemoveDomain|ByJs)|(?:wsdlServ|findUs)er)$/;
our $boolKeys = qr/^(?:s(?:aml(?:IDP(?:MetaDataOptions(?:(?:Check(?:S[LS]OMessageSignatur|Audienc|Tim)|IsPassiv)e|A(?:llow(?:LoginFromIDP|ProxiedAuthn)|daptSessionUtime)|Force(?:Authn|UTF8)|StoreSAMLToken|RelayStateURL)|SSODescriptorWantAuthnRequestsSigned)|S(?:P(?:MetaDataOptions(?:(?:CheckS[LS]OMessageSignatur|OneTimeUs)e|EnableIDPInitiatedURL|ForceUTF8)|SSODescriptor(?:WantAssertion|AuthnRequest)sSigned)|erviceUseCertificateInResponse)|DiscoveryProtocol(?:Activation|IsPassive)|CommonDomainCookieActivation|UseQueryStringSpecific|MetadataForceUTF8)|f(?:RemovedUseNotif|OnlyUpgrade)|kip(?:Upgrade|Renew)Confirmation|oap(?:Session|Config)Server|t(?:ayConnecte|orePasswor)d|laveDisplayLogo|howLanguages|slByAjax)|o(?:idc(?:RPMetaDataOptions(?:A(?:llow(?:(?:ClientCredentials|Password)Grant|Offline)|ccessToken(?:Claims|JWT))|Re(?:freshToken|quirePKCE)|LogoutSessionRequired|IDTokenForceClaims|BypassConsent|Public)|ServiceAllow(?:(?:AuthorizationCode|Implicit|Hybrid)Flow|DynamicRegistration|OnlyDeclaredScopes)|OPMetaDataOptions(?:(?:CheckJWTSignatur|UseNonc)e|StoreIDToken))|ldNotifFormat)|c(?:a(?:sS(?:rvMetaDataOptions(?:Gateway|Renew)|trictMatching)|ptcha_(?:register|login|mail)_enabled)|o(?:ntextSwitching(?:Allowed2fModifications|StopWithLogout)|mpactConf|rsEnabled)|heck(?:DevOps(?:D(?:isplayNormalizedHeaders|ownload))?|State|User|XSS)|rowdsec|da)|p(?:ortal(?:Display(?:Re(?:freshMyRights|setPassword|gister)|CertificateResetByMail|GeneratePassword|PasswordPolicy)|E(?:rrorOn(?:ExpiredSession|MailNotFound)|nablePasswordDisplay)|(?:CheckLogin|Statu)s|OpenLinkInNewWindow|ForceAuthn|AntiFrame)|roxyUseSoap)|l(?:dap(?:(?:G(?:roup(?:DecodeSearchedValu|Recursiv)|etUserBeforePasswordChang)|UsePasswordResetAttribut)e|(?:AllowResetExpired|Set)Password|ChangePasswordAsUser|PpolicyControl|ITDS)|oginHistoryEnabled)|no(?:tif(?:ication(?:Server(?:(?:POS|GE)T|DELETE)?|sExplorer)?|y(?:Deleted|Other))|AjaxHook)|i(?:ssuerDB(?:OpenID(?:Connect)?|SAML|CAS|Get)Activation|mpersonationSkipEmptyValues)|u(?:se(?:RedirectOn(?:Forbidden|Error)|SafeJail)|2fUserCanRemoveKey|pgradeSession)|re(?:st(?:(?:Password|Session|Config|Auth)Server|ExportSecretKeys)|freshSessions)|br(?:uteForceProtection(?:IncrementalTempo)?|owsersDontStorePassword)|d(?:is(?:ablePersistentStorage|playSessionId)|biDynamicHashEnabled)|(?:mai(?:lOnPasswordChang|ntenanc)|vhostMaintenanc)e|to(?:tp2fUserCanRemoveKey|kenUseGlobalStorage)|g(?:roupsBeforeMacros|lobalLogoutTimer)|a(?:voidAssignment|ctiveTimer)|h(?:ideOldPassword|ttpOnly)|yubikey2fUserCanRemoveKey|krb(?:RemoveDomain|ByJs)|(?:wsdlServ|findUs)er)$/;
our @sessionTypes = ( 'remoteGlobal', 'global', 'localSession', 'persistent', 'saml', 'oidc', 'cas' );

View File

@ -1,7 +1,7 @@
# This file is generated by Lemonldap::NG::Manager::Build. Don't modify it by hand
package Lemonldap::NG::Common::Conf::DefaultValues;
our $VERSION = '2.0.13';
our $VERSION = '2.0.14';
sub defaultValues {
return {
@ -35,6 +35,7 @@ sub defaultValues {
'certificateResetByMailURL' =>
'http://auth.example.com/certificateReset',
'certificateResetByMailValidityDelay' => 0,
'checkDevOpsDisplayNormalizedHeaders' => 1,
'checkDevOpsDownload' => 1,
'checkTime' => 600,
'checkUserDisplayComputedSession' => 1,

View File

@ -5,7 +5,7 @@ use strict;
use Exporter 'import';
use base qw(Exporter);
our $VERSION = '2.0.13';
our $VERSION = '2.0.14';
our %EXPORT_TAGS = ( 'all' => [qw($simpleHashKeys $doubleHashKeys $specialNodeKeys $casAppMetaDataNodeKeys $casSrvMetaDataNodeKeys $oidcOPMetaDataNodeKeys $oidcRPMetaDataNodeKeys $samlIDPMetaDataNodeKeys $samlSPMetaDataNodeKeys $virtualHostKeys $specialNodeHash $authParameters $issuerParameters $samlServiceParameters $oidcServiceParameters $casServiceParameters)] );
our @EXPORT_OK = ( @{ $EXPORT_TAGS{'all'} } );

View File

@ -4,7 +4,7 @@ package Lemonldap::NG::Handler::Lib::StatusConstants;
use strict;
use Exporter 'import';
our $VERSION = '2.0.13';
our $VERSION = '2.0.14';
sub portalConsts {
return {

View File

@ -1,7 +1,7 @@
# This file is generated by Lemonldap::NG::Manager::Build. Don't modify it by hand
package Lemonldap::NG::Manager::Attributes;
our $VERSION = '2.0.13';
our $VERSION = '2.0.14';
sub perlExpr {
my ( $val, $conf ) = @_;
@ -870,6 +870,10 @@ qr/(?:(?:https?):\/\/(?:(?:(?:(?:(?:(?:[a-zA-Z0-9][-a-zA-Z0-9]*)?[a-zA-Z0-9])[.]
'default' => 0,
'type' => 'bool'
},
'checkDevOpsDisplayNormalizedHeaders' => {
'default' => 1,
'type' => 'bool'
},
'checkDevOpsDownload' => {
'default' => 1,
'type' => 'bool'

View File

@ -6,7 +6,7 @@
package Lemonldap::NG::Manager::Build::Attributes;
our $VERSION = '2.0.13';
our $VERSION = '2.0.14';
use strict;
use Regexp::Common qw/URI/;
@ -476,6 +476,12 @@ sub attributes {
documentation => 'Enable check DevOps download field',
flags => 'p',
},
checkDevOpsDisplayNormalizedHeaders => {
default => 1,
type => 'bool',
documentation => 'Display normalized headers',
flags => 'p',
},
checkUser => {
default => 0,
type => 'bool',

View File

@ -17,7 +17,7 @@
package Lemonldap::NG::Manager::Build::Tree;
our $VERSION = '2.0.13';
our $VERSION = '2.0.14';
# TODO: Missing:
# * activeTimer
@ -806,7 +806,11 @@ sub tree {
title => 'devOpsCheck',
help => 'checkdevops.html',
form => 'simpleInputContainer',
nodes => [ 'checkDevOps', 'checkDevOpsDownload' ],
nodes => [
'checkDevOps',
'checkDevOpsDownload',
'checkDevOpsDisplayNormalizedHeaders'
],
},
{
title => 'impersonation',

View File

@ -181,6 +181,7 @@
"cfgVersion":"عملية ضبط الإصدارات",
"checkDevOps":"تفعيل",
"checkDevOpsDownload":"Download file",
"checkDevOpsDisplayNormalizedHeaders":"Display normalized headers",
"checkState":"تفعيل",
"checkStateSecret":"سر مشترك",
"checkUser":"تفعيل",

View File

@ -181,6 +181,7 @@
"cfgVersion":"Configuration version",
"checkDevOps":"Activation",
"checkDevOpsDownload":"Download file",
"checkDevOpsDisplayNormalizedHeaders":"Display normalized headers",
"checkState":"Activation",
"checkStateSecret":"Shared secret",
"checkUser":"Activation",

View File

@ -181,6 +181,7 @@
"cfgVersion":"Configuration version",
"checkDevOps":"Activation",
"checkDevOpsDownload":"Download file",
"checkDevOpsDisplayNormalizedHeaders":"Display normalized headers",
"checkState":"Activation",
"checkStateSecret":"Shared secret",
"checkUser":"Activation",

View File

@ -181,6 +181,7 @@
"cfgVersion":"Configuration version",
"checkDevOps":"Activación",
"checkDevOpsDownload":"Download file",
"checkDevOpsDisplayNormalizedHeaders":"Display normalized headers",
"checkState":"Activación",
"checkStateSecret":"Secreto compartido",
"checkUser":"Activación",

View File

@ -181,6 +181,7 @@
"cfgVersion":"Version de la configuration",
"checkDevOps":"Activation",
"checkDevOpsDownload":"Télécharger un fichier",
"checkDevOpsDisplayNormalizedHeaders":"Afficher les entêtes normalisés",
"checkState":"Activation",
"checkStateSecret":"Secret partagé",
"checkUser":"Activation",

View File

@ -181,6 +181,7 @@
"cfgVersion":"Versione configurazione",
"checkDevOps":"Activation",
"checkDevOpsDownload":"Download file",
"checkDevOpsDisplayNormalizedHeaders":"Display normalized headers",
"checkState":"Attivazione",
"checkStateSecret":"Segreto condiviso",
"checkUser":"Attivazione",

View File

@ -181,6 +181,7 @@
"cfgVersion":"Wersja konfiguracji",
"checkDevOps":"Aktywacja",
"checkDevOpsDownload":"Download file",
"checkDevOpsDisplayNormalizedHeaders":"Display normalized headers",
"checkState":"Aktywacja",
"checkStateSecret":"Współdzielony sekret",
"checkUser":"Aktywacja",

View File

@ -181,6 +181,7 @@
"cfgVersion":"Yapılandırma sürümü",
"checkDevOps":"Aktivasyon",
"checkDevOpsDownload":"Dosyayı indir",
"checkDevOpsDisplayNormalizedHeaders":"Display normalized headers",
"checkState":"Aktivasyon",
"checkStateSecret":"Paylaşılan sır",
"checkUser":"Aktivasyon",

View File

@ -181,6 +181,7 @@
"cfgVersion":"Phiên bản cấu hình",
"checkDevOps":"Activation",
"checkDevOpsDownload":"Download file",
"checkDevOpsDisplayNormalizedHeaders":"Display normalized headers",
"checkState":"Kích hoạt",
"checkStateSecret":"Chia sẻ bí mật",
"checkUser":"Kích hoạt",

View File

@ -181,6 +181,7 @@
"cfgVersion":"配置信息",
"checkDevOps":"Activation",
"checkDevOpsDownload":"Download file",
"checkDevOpsDisplayNormalizedHeaders":"Display normalized headers",
"checkState":"激活",
"checkStateSecret":"Shared secret",
"checkUser":"激活",

View File

@ -181,6 +181,7 @@
"cfgVersion":"設定版本",
"checkDevOps":"啟用",
"checkDevOpsDownload":"Download file",
"checkDevOpsDisplayNormalizedHeaders":"Display normalized headers",
"checkState":"啟用",
"checkStateSecret":"已分享的祕密",
"checkUser":"啟用",

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -4,7 +4,7 @@ package Lemonldap::NG::Portal::Main::Constants;
use strict;
use Exporter 'import';
our $VERSION = '2.0.13';
our $VERSION = '2.0.14';
use constant HANDLER => 'Lemonldap::NG::Handler::PSGI::Main';
use constant URIRE =>

View File

@ -13,9 +13,10 @@ use Lemonldap::NG::Portal::Main::Constants qw(
PE_TOKENEXPIRED
PE_FILENOTFOUND
PE_BAD_DEVOPS_FILE
PE_REGISTERFORMEMPTY
);
our $VERSION = '2.0.12';
our $VERSION = '2.0.14';
extends qw(
Lemonldap::NG::Portal::Main::Plugin
@ -122,6 +123,9 @@ sub run {
if $msg;
}
$msg = 'PE' . PE_REGISTERFORMEMPTY
unless ( $req->param('url') || $req->param('checkDevOpsFile') );
# Check URL if allowed and exists
if ( $self->conf->{checkDevOpsDownload} and $url = $req->param('url') ) {
undef $url if $self->p->checkXSSAttack( 'CheckDevOps URL', $url );
@ -169,7 +173,7 @@ sub run {
unless ( $json || $msg ) {
$json = eval {
from_json( $req->param('checkDevOpsFile'), { allow_nonref => 1 } );
};
} if $req->param('checkDevOpsFile');
if ($@) {
# Prepare form params
@ -202,6 +206,20 @@ sub run {
# Compile headers
$handler->headersInit( undef, { $vhost => $json->{headers} } );
$headers = $handler->checkHeaders( $req, $req->userData );
# Normalize headers name if required
if ( $self->conf->{checkDevOpsDisplayNormalizedHeaders} ) {
$self->logger->debug("Normalize headers...");
@$headers = map {
; # Prevent compilation error with old Perl versions
no strict 'refs';
{
key => &{ $handler . '::cgiName' }( $_->{key} ),
value => $_->{value}
}
} @$headers;
}
my $headers_list = join ', ', map { "$_->{key}:$_->{value}" } @$headers;
$self->logger->debug("CheckDevOps compiled headers: $headers_list");

View File

@ -17,7 +17,7 @@ my $file = '{
"default": "accept"
},
"headers": {
"User": "$uid",
"Auth-User": "$uid",
"Mail": "$mail",
"Name": "$cn",
"UA": "$UA"
@ -96,10 +96,10 @@ m%<pre><textarea id="checkDevOpsFile" name="checkDevOpsFile" class="form-control
ok( $res->[2]->[0] =~ m%<b><span trspan="headers">HEADERS</span></b>%,
'HEADERS' )
or explain( $res->[2]->[0], 'HEADERS' );
ok( $res->[2]->[0] =~ m%Name: Doctor Who<br/>%, 'Hearder Name found' )
ok( $res->[2]->[0] =~ m%HTTP_NAME: Doctor Who<br/>%, 'Normalized hearder Name found' )
or explain( $res->[2]->[0], 'Hearder Name' );
ok( $res->[2]->[0] =~ m%User: dwho<br/>%, 'Hearder User found' )
or explain( $res->[2]->[0], 'Hearder User' );
ok( $res->[2]->[0] =~ m%HTTP_AUTH_USER: dwho<br/>%, 'Normalized hearder Auth-User found' )
or explain( $res->[2]->[0], 'Hearder Auth-User' );
# Rules
ok( $res->[2]->[0] =~ m%<b><span trspan="rules">RULES</span></b>%, 'RULES' )
@ -123,6 +123,25 @@ count(13);
( $host, $url, $query ) =
expectForm( $res, undef, '/checkdevops', 'checkDevOpsFile' );
# Empty form
# ----------
ok(
$res = $client->_post(
'/checkdevops',
IO::String->new($query),
cookie => "lemonldap=$id",
length => length($query),
),
'POST empty checkdevops form'
);
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->{MSG} eq 'PE79', 'PE79' )
or print STDERR Dumper($res);
count(4);
# Fail to download file
# ---------------------
$query = 'url=http://testfail.example.com';

View File

@ -33,13 +33,14 @@ my $bad_file = '{
}';
my $client = LLNG::Manager::Test->new( {
ini => {
logLevel => 'error',
authentication => 'Demo',
userDB => 'Same',
requireToken => 1,
checkDevOps => 1,
checkDevOpsDownload => 0,
hiddenAttributes => 'mail UA'
logLevel => 'error',
authentication => 'Demo',
userDB => 'Same',
requireToken => 1,
checkDevOps => 1,
checkDevOpsDownload => 0,
checkDevOpsDisplayNormalizedHeaders => 0,
hiddenAttributes => 'mail UA'
}
}
);