##@file # Base package for Lemonldap::NG portal ##@class Lemonldap::NG::Portal::Simple # Base class for Lemonldap::NG portal package Lemonldap::NG::Portal::Simple; use strict; use warnings; use Exporter 'import'; use warnings; use MIME::Base64; use Lemonldap::NG::Common::CGI; use CGI::Cookie; use POSIX qw(strftime); use Lemonldap::NG::Portal::_i18n; #inherits use Lemonldap::NG::Common::Captcha; use Lemonldap::NG::Common::Session; use Lemonldap::NG::Common::Apache::Session ; #link protected session Apache::Session object use Lemonldap::NG::Common::Safe; #link protected safe Safe object use Lemonldap::NG::Common::Safelib; use Digest::MD5; # Special comments for doxygen #inherits Lemonldap::NG::Portal::_SOAP #inherits Lemonldap::NG::Portal::AuthApache; #inherits Lemonldap::NG::Portal::AuthAD; #inherits Lemonldap::NG::Portal::AuthCAS; #inherits Lemonldap::NG::Portal::AuthChoice; #inherits Lemonldap::NG::Portal::AuthDBI; #inherits Lemonldap::NG::Portal::AuthFacebook; #inherits Lemonldap::NG::Portal::AuthGoogle; #inherits Lemonldap::NG::Portal::AuthLDAP; #inherits Lemonldap::NG::Portal::AuthMulti; #inherits Lemonldap::NG::Portal::AuthNull; #inherits Lemonldap::NG::Portal::AuthOpenID; #inherits Lemonldap::NG::Portal::AuthProxy; #inherits Lemonldap::NG::Portal::AuthRadius; #inherits Lemonldap::NG::Portal::AuthRemote; #inherits Lemonldap::NG::Portal::AuthSAML; #inherits Lemonldap::NG::Portal::AuthSSL; #inherits Lemonldap::NG::Portal::AuthTwitter; #inherits Lemonldap::NG::Portal::Display; #inherits Lemonldap::NG::Portal::IssuerDBCAS #inherits Lemonldap::NG::Portal::IssuerDBNull #inherits Lemonldap::NG::Portal::IssuerDBOpenID #inherits Lemonldap::NG::Portal::IssuerDBSAML #inherits Lemonldap::NG::Portal::Menu #link Lemonldap::NG::Common::Notification protected notification #inherits Lemonldap::NG::Portal::PasswordDBChoice; #inherits Lemonldap::NG::Portal::PasswordDBDBI; #inherits Lemonldap::NG::Portal::PasswordDBLDAP; #inherits Lemonldap::NG::Portal::PasswordDBNull; #inherits Lemonldap::NG::Portal::UserDBAD; #inherits Lemonldap::NG::Portal::UserDBChoice; #inherits Lemonldap::NG::Portal::UserDBDBI; #inherits Lemonldap::NG::Portal::UserDBFacebook; #inherits Lemonldap::NG::Portal::UserDBGoogle; #inherits Lemonldap::NG::Portal::UserDBLDAP; #inherits Lemonldap::NG::Portal::UserDBMulti; #inherits Lemonldap::NG::Portal::UserDBNull; #inherits Lemonldap::NG::Portal::UserDBOpenID; #inherits Lemonldap::NG::Portal::UserDBProxy; #inherits Lemonldap::NG::Portal::UserDBRemote; #inherits Lemonldap::NG::Portal::UserDBSAML; #inherits Lemonldap::NG::Portal::PasswordDBDBI #inherits Lemonldap::NG::Portal::PasswordDBLDAP #inherits Apache::Session #link Lemonldap::NG::Common::Apache::Session::SOAP protected globalStorage our $VERSION = '1.9.0'; use base qw(Lemonldap::NG::Common::CGI Exporter); our @ISA; # Constants use constant { # Portal errors # Developers warning, do not use PE_INFO, it's reserved to autoRedirect. # If you want to send an information, use $self->info('text'). PE_IMG_NOK => -5, PE_IMG_OK => -4, PE_INFO => -3, PE_REDIRECT => -2, PE_DONE => -1, PE_OK => 0, PE_SESSIONEXPIRED => 1, PE_FORMEMPTY => 2, PE_WRONGMANAGERACCOUNT => 3, PE_USERNOTFOUND => 4, PE_BADCREDENTIALS => 5, PE_LDAPCONNECTFAILED => 6, PE_LDAPERROR => 7, PE_APACHESESSIONERROR => 8, PE_FIRSTACCESS => 9, PE_BADCERTIFICATE => 10, PE_PP_ACCOUNT_LOCKED => 21, PE_PP_PASSWORD_EXPIRED => 22, PE_CERTIFICATEREQUIRED => 23, PE_ERROR => 24, PE_PP_CHANGE_AFTER_RESET => 25, PE_PP_PASSWORD_MOD_NOT_ALLOWED => 26, PE_PP_MUST_SUPPLY_OLD_PASSWORD => 27, PE_PP_INSUFFICIENT_PASSWORD_QUALITY => 28, PE_PP_PASSWORD_TOO_SHORT => 29, PE_PP_PASSWORD_TOO_YOUNG => 30, PE_PP_PASSWORD_IN_HISTORY => 31, PE_PP_GRACE => 32, PE_PP_EXP_WARNING => 33, PE_PASSWORD_MISMATCH => 34, PE_PASSWORD_OK => 35, PE_NOTIFICATION => 36, PE_BADURL => 37, PE_NOSCHEME => 38, PE_BADOLDPASSWORD => 39, PE_MALFORMEDUSER => 40, PE_SESSIONNOTGRANTED => 41, PE_CONFIRM => 42, PE_MAILFORMEMPTY => 43, PE_BADMAILTOKEN => 44, PE_MAILERROR => 45, PE_MAILOK => 46, PE_LOGOUT_OK => 47, PE_SAML_ERROR => 48, PE_SAML_LOAD_SERVICE_ERROR => 49, PE_SAML_LOAD_IDP_ERROR => 50, PE_SAML_SSO_ERROR => 51, PE_SAML_UNKNOWN_ENTITY => 52, PE_SAML_DESTINATION_ERROR => 53, PE_SAML_CONDITIONS_ERROR => 54, PE_SAML_IDPSSOINITIATED_NOTALLOWED => 55, PE_SAML_SLO_ERROR => 56, PE_SAML_SIGNATURE_ERROR => 57, PE_SAML_ART_ERROR => 58, PE_SAML_SESSION_ERROR => 59, PE_SAML_LOAD_SP_ERROR => 60, PE_SAML_ATTR_ERROR => 61, PE_OPENID_EMPTY => 62, PE_OPENID_BADID => 63, PE_MISSINGREQATTR => 64, PE_BADPARTNER => 65, PE_MAILCONFIRMATION_ALREADY_SENT => 66, PE_PASSWORDFORMEMPTY => 67, PE_CAS_SERVICE_NOT_ALLOWED => 68, PE_MAILFIRSTACCESS => 69, PE_MAILNOTFOUND => 70, PE_PASSWORDFIRSTACCESS => 71, PE_MAILCONFIRMOK => 72, PE_RADIUSCONNECTFAILED => 73, PE_MUST_SUPPLY_OLD_PASSWORD => 74, PE_FORBIDDENIP => 75, PE_CAPTCHAERROR => 76, PE_CAPTCHAEMPTY => 77, PE_REGISTERFIRSTACCESS => 78, PE_REGISTERFORMEMPTY => 79, PE_REGISTERALREADYEXISTS => 80, # Portal messages PM_USER => 0, PM_DATE => 1, PM_IP => 2, PM_SESSIONS_DELETED => 3, PM_OTHER_SESSIONS => 4, PM_REMOVE_OTHER_SESSIONS => 5, PM_PP_GRACE => 6, PM_PP_EXP_WARNING => 7, PM_SAML_IDPSELECT => 8, PM_SAML_IDPCHOOSEN => 9, PM_REMEMBERCHOICE => 10, PM_SAML_SPLOGOUT => 11, PM_REDIRECTION => 12, PM_BACKTOSP => 13, PM_BACKTOCASURL => 14, PM_LOGOUT => 15, PM_OPENID_EXCHANGE => 16, PM_CDC_WRITER => 17, PM_OPENID_RPNS => 18, # OpenID "requested parameter is not set" PM_OPENID_PA => 19, # "OpenID policy available at" PM_OPENID_AP => 20, # OpenID "Asked parameter" PM_ERROR_MSG => 21, PM_LAST_LOGINS => 22, PM_LAST_FAILED_LOGINS => 23, PM_OIDC_CONSENT => 24, PM_OIDC_SCOPE_OPENID => 25, PM_OIDC_SCOPE_PROFILE => 26, PM_OIDC_SCOPE_EMAIL => 27, PM_OIDC_SCOPE_ADDRESS => 28, PM_OIDC_SCOPE_PHONE => 29, PM_OIDC_SCOPE_OTHER => 30, PM_OIDC_CONFIRM_LOGOUT => 31, }; # EXPORTER PARAMETERS our @EXPORT = qw( PE_IMG_NOK PE_IMG_OK PE_INFO PE_REDIRECT PE_DONE PE_OK PE_SESSIONEXPIRED PE_FORMEMPTY PE_WRONGMANAGERACCOUNT PE_USERNOTFOUND PE_BADCREDENTIALS PE_LDAPCONNECTFAILED PE_LDAPERROR PE_APACHESESSIONERROR PE_FIRSTACCESS PE_BADCERTIFICATE PE_PP_ACCOUNT_LOCKED PE_PP_PASSWORD_EXPIRED PE_CERTIFICATEREQUIRED PE_ERROR PE_PP_CHANGE_AFTER_RESET PE_PP_PASSWORD_MOD_NOT_ALLOWED PE_PP_MUST_SUPPLY_OLD_PASSWORD PE_PP_INSUFFICIENT_PASSWORD_QUALITY PE_PP_PASSWORD_TOO_SHORT PE_PP_PASSWORD_TOO_YOUNG PE_PP_PASSWORD_IN_HISTORY PE_PP_GRACE PE_PP_EXP_WARNING PE_PASSWORD_MISMATCH PE_PASSWORD_OK PE_NOTIFICATION PE_BADURL PE_NOSCHEME PE_BADOLDPASSWORD PE_MALFORMEDUSER PE_SESSIONNOTGRANTED PE_CONFIRM PE_MAILFORMEMPTY PE_BADMAILTOKEN PE_MAILERROR PE_MAILOK PE_LOGOUT_OK PE_SAML_ERROR PE_SAML_LOAD_SERVICE_ERROR PE_SAML_LOAD_IDP_ERROR PE_SAML_SSO_ERROR PE_SAML_UNKNOWN_ENTITY PE_SAML_DESTINATION_ERROR PE_SAML_CONDITIONS_ERROR PE_SAML_IDPSSOINITIATED_NOTALLOWED PE_SAML_SLO_ERROR PE_SAML_SIGNATURE_ERROR PE_SAML_ART_ERROR PE_SAML_SESSION_ERROR PE_SAML_LOAD_SP_ERROR PE_SAML_ATTR_ERROR PE_OPENID_EMPTY PE_OPENID_BADID PE_MISSINGREQATTR PE_BADPARTNER PE_MAILCONFIRMATION_ALREADY_SENT PE_PASSWORDFORMEMPTY PE_CAS_SERVICE_NOT_ALLOWED PE_MAILFIRSTACCESS PE_MAILNOTFOUND PE_PASSWORDFIRSTACCESS PE_MAILCONFIRMOK PE_MUST_SUPPLY_OLD_PASSWORD PE_FORBIDDENIP PE_CAPTCHAERROR PE_CAPTCHAEMPTY PE_REGISTERFIRSTACCESS PE_REGISTERFORMEMPTY PE_REGISTERALREADYEXISTS PM_USER PM_DATE PM_IP PM_SESSIONS_DELETED PM_OTHER_SESSIONS PM_REMOVE_OTHER_SESSIONS PM_PP_GRACE PM_PP_EXP_WARNING PM_SAML_IDPSELECT PM_SAML_IDPCHOOSEN PM_REMEMBERCHOICE PM_SAML_SPLOGOUT PM_REDIRECTION PM_BACKTOSP PM_BACKTOCASURL PM_LOGOUT PM_OPENID_EXCHANGE PM_CDC_WRITER PM_OPENID_RPNS PM_OPENID_PA PM_OPENID_AP PM_ERROR_MSG PM_LAST_LOGINS PM_LAST_FAILED_LOGINS PM_OIDC_CONSENT PM_OIDC_SCOPE_OPENID PM_OIDC_SCOPE_PROFILE PM_OIDC_SCOPE_EMAIL PM_OIDC_SCOPE_ADDRESS PM_OIDC_SCOPE_PHONE PM_OIDC_SCOPE_OTHER PM_OIDC_CONFIRM_LOGOUT ); our %EXPORT_TAGS = ( 'all' => [ @EXPORT, 'import' ], ); our @EXPORT_OK = ( @{ $EXPORT_TAGS{'all'} } ); # Share secure jail between threads our $safe; BEGIN { eval { require threads::shared; threads::shared::share($safe); }; } ##@cmethod Lemonldap::NG::Portal::Simple new(hashRef args) # Class constructor. #@param args hash reference #@return Lemonldap::NG::Portal::Simple object sub new { @ISA = qw(Lemonldap::NG::Common::CGI Exporter); binmode( STDOUT, ":utf8" ); my $class = shift; return $class if ( ref($class) ); my $self = $class->SUPER::new() or return undef; # Reinit _url $self->{_url} = ''; # Get global configuration $self->getConf(@_) or $self->abort( "Configuration error", "Unable to get configuration: $Lemonldap::NG::Common::Conf::msg" ); $self->{multiValuesSeparator} ||= ';'; # Test mandatory elements # 1. Sessions backend $self->abort( "Configuration error", "You've to indicate a an Apache::Session storage module !" ) unless ( $self->{globalStorage} ); # Use global storage for all backends by default # Persistent $self->{persistentStorage} ||= $self->{globalStorage}; if ( !$self->{persistentStorageOptions} or !%{ $self->{persistentStorageOptions} } ) { $self->{persistentStorageOptions} = $self->{globalStorageOptions}; } # SAML $self->{samlStorage} ||= $self->{globalStorage}; if ( !$self->{samlStorageOptions} or !%{ $self->{samlStorageOptions} } ) { $self->{samlStorageOptions} = $self->{globalStorageOptions}; } # CAS $self->{casStorage} ||= $self->{globalStorage}; if ( !$self->{casStorageOptions} or !%{ $self->{casStorageOptions} } ) { $self->{casStorageOptions} = $self->{globalStorageOptions}; } # Captcha $self->{captchaStorage} ||= $self->{globalStorage}; if ( !$self->{captchaStorageOptions} or !%{ $self->{captchaStorageOptions} } ) { $self->{captchaStorageOptions} = $self->{globalStorageOptions}; } # OpenIDConnect $self->{oidcStorage} ||= $self->{globalStorage}; if ( !$self->{oidcStorageOptions} or !%{ $self->{oidcStorageOptions} } ) { $self->{oidcStorageOptions} = $self->{globalStorageOptions}; } # 2. Domain $self->abort( "Configuration error", "You've to indicate a domain for cookies" ) unless ( $self->{domain} ); $self->{domain} =~ s/^([^\.])/.$1/; # Load Display and Menu functions $self->loadModule('Lemonldap::NG::Portal::Menu'); $self->loadModule('Lemonldap::NG::Portal::Display'); # Rules to allow redirection $self->{mustRedirect} = defined $ENV{REQUEST_METHOD} ? ( $ENV{REQUEST_METHOD} eq "POST" and not $self->param('newpassword') ) : $self->param('logout') ? 1 : 0; # Push authentication/userDB/passwordDB modules in @ISA foreach my $type (qw(authentication userDB passwordDB registerDB)) { my $module_name = 'Lemonldap::NG::Portal::'; my $db_type = $type; my $db_name = $self->{$db_type} or $self->abort("'$db_type' is not set"); # Adapt module type to real module name $db_type =~ s/authentication/Auth/; $db_type =~ s/userDB/UserDB/; $db_type =~ s/passwordDB/PasswordDB/; $db_type =~ s/registerDB/RegisterDB/; # Full module name $module_name .= $db_type . $db_name; # Remove white spaces $module_name =~ s/\s.*$//; # Try to load module $self->abort( "Configuration error", "Unable to load $module_name" ) unless $self->loadModule($module_name); # $self->{authentication} and $self->{userDB} can contains arguments # (key1 = scalar_value; key2 = ...) unless ( $db_name =~ /^Multi/ ) { $db_name =~ s/^\w+\s*//; my %h = split( /\s*[=;]\s*/, $db_name ) if ($db_name); %$self = ( %h, %$self ); } } # Check issuerDB path to load the correct issuerDB module foreach my $issuerDBtype (qw(SAML OpenID CAS OpenIDConnect)) { my $module_name = 'Lemonldap::NG::Portal::IssuerDB' . $issuerDBtype; $self->lmLog( "[IssuerDB activation] Try issuerDB module $issuerDBtype", 'debug' ); # Check activation flag my $activation = $self->{ "issuerDB" . $issuerDBtype . "Activation" } ||= "0"; unless ($activation) { $self->lmLog( "[IssuerDB activation] Activation flag set to off, trying next", 'debug' ); next; } # Check the path my $path = $self->{ "issuerDB" . $issuerDBtype . "Path" }; if ( defined $path ) { $self->lmLog( "[IssuerDB activation] Found path $path", 'debug' ); # Get current path my $url_path = $self->url( -absolute => 1 ); $url_path =~ s#^//#/#; $self->lmLog( "[IssuerDB activation] Path of current request is $url_path", 'debug' ); # Match regular expression if ( $url_path =~ m#$path# ) { $self->abort( "Configuration error", "Unable to load $module_name" ) unless $self->loadModule($module_name); # Remember loaded module $self->{_activeIssuerDB} = $issuerDBtype; $self->lmLog( "[IssuerDB activation] IssuerDB module $issuerDBtype loaded", 'debug' ); last; } else { $self->lmLog( "[IssuerDB activation] Path do not match, trying next", 'debug' ); next; } } else { $self->lmLog( "[IssuerDB activation] No path defined", 'debug' ); next; } } # Load default issuerDB module if none was choosed unless ( $self->{_activeIssuerDB} ) { # Manage old configuration format my $db_type = $self->{'issuerDB'} || 'Null'; my $module_name = 'Lemonldap::NG::Portal::IssuerDB' . $db_type; $self->abort( "Configuration error", "Unable to load $module_name" ) unless $self->loadModule($module_name); # Remember loaded module $self->{_activeIssuerDB} = $db_type; $self->lmLog( "[IssuerDB activation] IssuerDB module $db_type loaded", 'debug' ); } # Notifications if ( $self->{notification} ) { require Lemonldap::NG::Common::Notification; my $tmp; # Use configuration options if ( $self->{notificationStorage} ) { $tmp->{type} = $self->{notificationStorage}; foreach ( keys %{ $self->{notificationStorageOptions} } ) { $tmp->{$_} = $self->{notificationStorageOptions}->{$_}; } } # Else use the configuration backend else { (%$tmp) = ( %{ $self->{lmConf} } ); $self->abort( "notificationStorage not defined", "This parameter is required to use notification system" ) unless ( ref($tmp) ); # Get the type $tmp->{type} =~ s/.*:://; $tmp->{type} =~ s/(CDBI|RDBI)/DBI/; # CDBI/RDBI are DBI # If type not File, DBI or LDAP, abort $self->abort("Only File, DBI or LDAP supported for Notifications") unless $tmp->{type} =~ /^(File|DBI|LDAP)$/; # Force table name $tmp->{table} = 'notifications'; } $tmp->{p} = $self; $self->{notifObject} = Lemonldap::NG::Common::Notification->new($tmp); $self->abort($Lemonldap::NG::Common::Notification::msg) unless ( $self->{notifObject} ); } # SOAP if ( $self->{Soap} or $self->{soap} ) { $self->loadModule('Lemonldap::NG::Portal::_SOAP'); if ( $self->{notification} and $ENV{PATH_INFO} ) { $self->{CustomSOAPServices} ||= {}; $self->{CustomSOAPServices}->{'/notification'} = { f => 'newNotification deleteNotification', o => $self->{notifObject} }; } $self->startSoapServices(); } # Trusted domains $self->{trustedDomains} ||= ""; $self->{trustedDomains} = "*" if ( $self->{trustedDomains} =~ /(^|\s)\*(\s|$)/ ); if ( $self->{trustedDomains} and $self->{trustedDomains} ne "*" ) { $self->{trustedDomains} =~ s#(^|\s+)\.#${1}[^/]+.#g; $self->{trustedDomains} = '(' . join( '|', split( /\s+/, $self->{trustedDomains} ) ) . ')'; $self->{trustedDomains} =~ s/\./\\./g; } return $self; } ##@method boolean loadModule(string module, boolean ignoreError) # Load a module into portal namespace # @param module module name # @param ignoreError set to 1 if error should not appear in logs # @return boolean sub loadModule { my ( $self, $module, $ignoreError ) = @_; return 1 unless $module; # Load module test eval "require $module"; if ($@) { $self->lmLog( "$module load error: $@", 'error' ) unless $ignoreError; return 0; } # Push module in @ISA push @ISA, $module; $self->lmLog( "Module $module loaded", 'debug' ); return 1; } ##@method protected boolean getConf(hashRef args) # Copy all parameters in caller object. #@param args hash-ref #@return True sub getConf { my ($self) = shift; my %args; if ( ref( $_[0] ) ) { %args = %{ $_[0] }; } else { %args = @_; } %$self = ( %$self, %args ); 1; } ## @method protected void setHiddenFormValue(string fieldname, string value, string prefix, boolean base64) # Add element into $self->{portalHiddenFormValues}, those values could be # used to hide values into HTML form. # @param fieldname The field name which will contain the correponding value # @param value The associated value # @param prefix Prefix of the field key # @param base64 Encode value in base64 # @return nothing sub setHiddenFormValue { my ( $self, $key, $val, $prefix, $base64 ) = @_; # Default values $prefix = "lmhidden_" unless defined $prefix; $base64 = 1 unless defined $base64; # Store value if ($val) { $key = $prefix . $key; $val = encode_base64($val) if $base64; $self->{portalHiddenFormValues}->{$key} = $val; $self->lmLog( "Store $val in hidden key $key", 'debug' ); } } ## @method public void getHiddenFormValue(string fieldname, string prefix, boolean base64) # Get value into $self->{portalHiddenFormValues}. # @param fieldname The existing field name which contains a value # @param prefix Prefix of the field key # @param base64 Decode value from base64 # @return string The associated value sub getHiddenFormValue { my ( $self, $key, $prefix, $base64 ) = @_; # Default values $prefix = "lmhidden_" unless defined $prefix; $base64 = 1 unless defined $base64; $key = $prefix . $key; # Get value if ( my $val = $self->param($key) ) { $val = decode_base64($val) if $base64; return $val; $self->lmLog( "Hidden value $val found for key $key", 'debug' ); } # No value found return undef; } ## @method protected void clearHiddenFormValue(arrayref keys) # Clear values form stored hidden fields # Delete all keys if no keys provided # @param keys Array reference of keys # @return nothing sub clearHiddenFormValue { my ( $self, $keys ) = @_; unless ( defined $keys ) { delete $self->{portalHiddenFormValues}; $self->lmLog( "Delete all hidden values", 'debug' ); } else { foreach (@$keys) { delete $self->{portalHiddenFormValues}->{$_}; $self->lmLog( "Delete hidden value for key $_", 'debug' ); } } return; } ##@method public string buildHiddenForm() # Return an HTML representation of hidden values. # @return HTML code sub buildHiddenForm { my $self = shift; my @keys = keys %{ $self->{portalHiddenFormValues} }; my $val = ''; foreach (@keys) { # Check XSS attacks next if $self->checkXSSAttack( $_, $self->{portalHiddenFormValues}->{$_} ); # Build hidden input HTML code $val .= qq{'; } return $val; } ## @method void initCaptcha(void) # init captcha module and generate captcha # @return nothing sub initCaptcha { my $self = shift; # Create new captcha my $captcha = Lemonldap::NG::Common::Captcha->new( { storageModule => $self->{captchaStorage}, storageModuleOptions => $self->{captchaStorageOptions}, size => $self->{captcha_size}, } ); $self->{captcha_secret} = $captcha->code; $self->{captcha_code} = $captcha->md5; $self->{captcha_img} = $self->{portal} . "?displayCaptcha=" . $captcha->md5; $self->lmLog( "Captcha code generated: " . $self->{captcha_code}, 'debug' ); return; } ## @method int checkCaptcha(code, ccode) # Check captcha auth # @param code that user enter in the form # @param captcha code generated by Authen::Captcha # @return a constant sub checkCaptcha { my ( $self, $code, $ccode ) = @_; # Get captcha object my $captcha = Lemonldap::NG::Common::Captcha->new( { storageModule => $self->{captchaStorage}, storageModuleOptions => $self->{captchaStorageOptions}, md5 => $ccode, size => $self->{captcha_size}, } ); # Check code if ( $captcha && $captcha->code ) { if ( $code eq $captcha->code ) { $self->lmLog( "Code $code match captcha $ccode", 'debug' ); return 1; } return -2; } return 0; } ## @method int removeCaptcha(ccode) # Remove captcha session # @param captcha code generated by Authen::Captcha # @return a constant sub removeCaptcha { my ( $self, $ccode ) = @_; # Get captcha object my $captcha = Lemonldap::NG::Common::Captcha->new( { storageModule => $self->{captchaStorage}, storageModuleOptions => $self->{captchaStorageOptions}, md5 => $ccode, size => $self->{captcha_size}, } ); # Remove captcha session (will not be used anymore) if ( $captcha->removeSession ) { $self->lmLog( "Captcha session $ccode removed", 'debug' ); return 0; } else { $self->lmLog( "Unable to remove captcha session $ccode", 'warn' ); return 1; } } ## @method boolean isTrustedUrl(string url) # Check if an URL's domain name is declared in LL::NG config or is declared as trusted domain # @param url Parameter url # @param value Parameter value # @return 1 if url can be trusted, 0 else sub isTrustedUrl { my ( $self, $url ) = @_; return $url =~ m#^https?://$self->{reVHosts}(:\d+)?/#o || $self->{trustedDomains} eq "*" || $self->{trustedDomains} && $url =~ m#^https?://$self->{trustedDomains}(:\d+)?/#o; } ## @method boolean checkXSSAttack(string name, string value) # Check value to detect XSS attack # @param name Parameter name # @param value Parameter value # @return 1 if attack detected, 0 else sub checkXSSAttack { my ( $self, $name, $value ) = @_; # Empty values are not bad return 0 unless $value; # Test value if ( $value =~ m/(?:\0|<|'|"|`|\%(?:00|25|3C|22|27|2C))/ ) { $self->lmLog( "XSS attack detected (param: $name | value: $value)", "warn" ); return $self->{checkXSS}; } return 0; } =begin WSDL _IN lang $string Language _IN code $int Error code _RETURN $string Error string =end WSDL =cut ##@method string msg(int code) # calls Portal/_i18n.pm to display message in the client's language. #@param $code message code #@return message sub msg { my $self = shift; my $code = shift; return &Lemonldap::NG::Portal::_i18n::msg( $code, $self->{lang} ); } ##@method string error(int code) # calls Portal/_i18n.pm to display error in the client's language. #@param $code optional error code #@return error message sub error { my $self = shift; my $code = shift || $self->{error}; if ( my $lang = shift ) { # only for SOAP error requests $self->{lang} = $self->extract_lang($lang); } my $msg; # Check for customized message foreach ( @{ $self->{lang} } ) { if ( $self->{ "error_" . $_ . "_" . $code } ) { $msg = $self->{ "error_" . $_ . "_" . $code }; last; } } $msg ||= $self->{ "error_" . $code }; # Use customized message or built-in message if ( defined $msg ) { # Manage UTF-8 utf8::decode($msg); $self->lmLog( "Use customized message $msg for error $code", 'debug' ); } else { $msg = &Lemonldap::NG::Portal::_i18n::error( $code, $self->{lang} ); } # Return message # Manage SOAP return $msg; } ##@method string error_type(int code) # error_type tells if error is positive, warning or negative # @param $code Lemonldap::NG error code # @return "positive", "warning" or "negative" sub error_type { my $self = shift; my $code = shift || $self->{error}; # Positive errors return "positive" if ( scalar( grep { /^$code$/ } ( PE_REDIRECT, PE_DONE, PE_OK, PE_PASSWORD_OK, PE_MAILOK, PE_LOGOUT_OK, PE_MAILFIRSTACCESS, PE_PASSWORDFIRSTACCESS, PE_MAILCONFIRMOK, PE_REGISTERFIRSTACCESS, ) ) ); # Warning errors return "warning" if ( scalar( grep { /^$code$/ } ( PE_INFO, PE_SESSIONEXPIRED, PE_FORMEMPTY, PE_FIRSTACCESS, PE_PP_GRACE, PE_PP_EXP_WARNING, PE_NOTIFICATION, PE_BADURL, PE_CONFIRM, PE_MAILFORMEMPTY, PE_MAILCONFIRMATION_ALREADY_SENT, PE_PASSWORDFORMEMPTY, PE_CAPTCHAEMPTY, PE_REGISTERFORMEMPTY, ) ) ); # Negative errors (default) return "negative"; } ##@method void header() # Overload CGI::header() to add Lemonldap::NG cookie. sub header { my $self = shift; unshift @_, '-type' unless ($#_); if ( $self->{cookie} ) { $self->SUPER::header( @_, -cookie => $self->{cookie} ); } else { $self->SUPER::header(@_); } } ##@method void redirect() # Overload CGI::redirect() to add Lemonldap::NG cookie. sub redirect { my $self = shift; if ( $self->{cookie} ) { $self->SUPER::redirect( @_, -cookie => $self->{cookie} ); } else { $self->SUPER::redirect(@_); } } ## @method protected hashref getApacheSession(string id, boolean noInfo, boolean $force) # Try to recover the session corresponding to id and return session datas. # If $id is set to undef or if $force is true, return a new session. # @param id session reference # @param noInfo do not set Apache REMOTE_USER # @param force Force session creation if it does not exist # return Lemonldap::NG::Common::Session object sub getApacheSession { my ( $self, $id, $noInfo, $force ) = @_; my $apacheSession = Lemonldap::NG::Common::Session->new( { storageModule => $self->{globalStorage}, storageModuleOptions => $self->{globalStorageOptions}, cacheModule => $self->{localSessionStorage}, cacheModuleOptions => $self->{localSessionStorageOptions}, id => $id, force => $force, kind => "SSO", } ); if ( $apacheSession->error ) { $self->lmLog( $apacheSession->error, 'debug' ); return; } if ( $id and !$force and !$apacheSession->data ) { $self->lmLog( "Session $id not found", 'debug' ); return; } unless ($noInfo) { $self->setApacheUser( $apacheSession->data->{ $self->{whatToTrace} } ) if ($id); $self->{id} = $apacheSession->id; } return $apacheSession; } ## @method protected hashref getPersistentSession(string uid) # Try to recover the persistent session corresponding to uid and return session datas. # @param uid main user identifier (whatToTrace) # return Lemonldap::NG::Common::Session object sub getPersistentSession { my ( $self, $uid ) = @_; return unless defined $uid; # Compute persistent identifier my $pid = $self->_md5hash($uid); my $persistentSession = Lemonldap::NG::Common::Session->new( { storageModule => $self->{persistentStorage}, storageModuleOptions => $self->{persistentStorageOptions}, cacheModule => $self->{localSessionStorage}, cacheModuleOptions => $self->{localSessionStorageOptions}, id => $pid, force => 1, kind => "Persistent", } ); if ( $persistentSession->error ) { $self->lmLog( $persistentSession->error, 'debug' ); } # Set _session_uid if not already present unless ( defined $persistentSession->data->{_session_uid} ) { $persistentSession->update( { '_session_uid' => $uid } ); } return $persistentSession; } ## @method protected string _md5hash(string s) # Return md5(s) # @param $s String to hash # @return hashed value sub _md5hash { my ( $self, $s ) = @_; return substr( Digest::MD5::md5_hex($s), 0, 32 ); } ## @method void updatePersistentSession(hashRef infos, string uid, string id) # Update persistent session. # Call updateSession() and store %$infos in a persistent session. # Note that if the session does not exists, it will be created. # @param infos hash reference of information to update # @param uid optional Unhashed persistent session ID # @param id optional SSO session ID # @return nothing sub updatePersistentSession { my ( $self, $infos, $uid, $id ) = @_; # Return if no infos to update return () unless ( ref $infos eq 'HASH' and %$infos ); # Update current session $self->updateSession( $infos, $id ); $uid ||= $self->{sessionInfo}->{ $self->{whatToTrace} }; return () unless ($uid); my $persistentSession = $self->getPersistentSession($uid); $persistentSession->update($infos); if ( $persistentSession->error ) { $self->lmLog( "Cannot update persistent session " . $self->_md5hash($uid), 'error' ); $self->lmLog( $persistentSession->error, 'error' ); } } ## @method void updateSession(hashRef infos, string id) # Update session stored. # If no id is given, try to get it from cookie. # If the session is available, update datas with $info. # Note that outdated session data may remain some time on # server local cache, if there are several LL::NG servers. # @param infos hash reference of information to update # @param id Session ID # @return nothing sub updateSession { my ( $self, $infos, $id ) = @_; # Return if no infos to update return () unless ( ref $infos eq 'HASH' and %$infos ); # Recover session ID unless given $id ||= $self->{id}; unless ($id) { my %cookies = fetch CGI::Cookie; $id ||= $cookies{ $self->{cookieName} }->value if ( defined $cookies{ $self->{cookieName} } ); } if ($id) { # Update sessionInfo data ## sessionInfo updated if $id defined : quite strange !! ## See http://jira.ow2.org/browse/LEMONLDAP-430 foreach ( keys %$infos ) { $self->lmLog( "Update sessionInfo $_ with " . $infos->{$_}, 'debug' ); $self->{sessionInfo}->{$_} = $infos->{$_}; } # Update session in global storage if ( my $apacheSession = $self->getApacheSession( $id, 1 ) ) { # Store updateTime $infos->{updateTime} = strftime( "%Y%m%d%H%M%S", localtime() ); # Store/update session values $apacheSession->update($infos); if ( $apacheSession->error ) { $self->lmLog( "Cannot update session $id", 'error' ); $self->lmLog( $apacheSession->error, 'error' ); } } } } ## @method void addSessionValue(string key, string value, string id) # Add a value into session key if not already present # @param key Session key # @param value Value to add # @param id optional Session identifier sub addSessionValue { my ( $self, $key, $value, $id ) = @_; # Mandatory parameters return () unless defined $key; return () unless defined $value; # Get current key value my $old_value = $self->{sessionInfo}->{$key}; # Split old values if ( defined $old_value ) { my @old_values = split /\Q$self->{multiValuesSeparator}\E/, $old_value; # Do nothing if value already exists foreach (@old_values) { return () if ( $_ eq $value ); } # Add separator $old_value .= $self->{multiValuesSeparator}; } else { $old_value = ""; } # Store new value my $new_value = $old_value . $value; $self->updateSession( { $key => $new_value }, $id ); # Return return (); } ## @method string getFirstValue(string value) # Get the first value of a multivaluated session value # @param value the complete value # @return first value sub getFirstValue { my ( $self, $value ) = @_; my @values = split /\Q$self->{multiValuesSeparator}\E/, $value; return $values[0]; } ##@method protected int _subProcess(array @subs) # Execute methods until an error is returned. # If $self->{$sub} exists, launch it, else launch $self->$sub #@param @subs array list of subroutines #@return Lemonldap::NG::Portal error sub _subProcess { my $self = shift; my @subs = @_; my $err = undef; foreach my $sub (@subs) { last if ( $err = $self->_sub($sub) ); } return $err; } ##@method protected void updateStatus() # Inform status mechanism module. # If an handler is launched on the same server with "status=>1", inform the # status module with the result (portal error). sub updateStatus { my $self = shift; print $Lemonldap::NG::Handler::Simple::statusPipe ( $self->{user} ? $self->{user} : $self->ipAddr ) . " => $ENV{SERVER_NAME}$ENV{SCRIPT_NAME} " . $self->{error} . "\n" if ($Lemonldap::NG::Handler::Simple::statusPipe); } ##@method protected string notification() #@return Notification stored by checkNotification() sub notification { my $self = shift; return $self->{_notification}; } ##@method protected string get_url() # Return url parameter # @return url parameter if good, nothing else. sub get_url { my $self = shift; return $self->{_url}; } ##@method protected string get_user() # Return user parameter # @return user parameter if good, nothing else. sub get_user { my $self = shift; return undef unless $self->{user}; unless ( $self->{user} =~ /$self->{userControl}/o ) { $self->lmLog( "Value " . $self->{user} . " does not match userControl regexp: " . $self->{userControl}, 'warn' ); return undef; } return $self->{user}; } ## @method string get_module(string type) # Return current used module # @param type auth/user/password/issuer # @return module name sub get_module { my ( $self, $type ) = @_; if ( $type =~ /auth/i ) { if ( defined $self->{_multi}->{stack}->[0] ) { return $self->{_multi}->{stack}->[0]->[0]->{s}; } if ( defined $self->{_choice}->{modules} ) { return $self->{_choice}->{modules}->[0]->{n}; } else { return $self->{authentication}; } } if ( $type =~ /user/i ) { if ( defined $self->{_multi}->{stack}->[1] ) { return $self->{_multi}->{stack}->[1]->[0]->{s}; } if ( defined $self->{_choice}->{modules} ) { return $self->{_choice}->{modules}->[1]->{n}; } else { return $self->{userDB}; } } if ( $type =~ /password/i ) { if ( defined $self->{_choice}->{modules} ) { return $self->{_choice}->{modules}->[2]->{n}; } else { return $self->{passwordDB}; } } if ( $type =~ /issuer/i ) { return $self->{_activeIssuerDB}; } return; } ##@method private Safe safe() # Provide the security jail. #@return Safe object sub safe { my $self = shift; # Test if safe already exists if ($safe) { # Refresh the portal object inside it $safe->{p} = $self; # Refresh environment variables $safe->share_from( 'main', ['%ENV'] ); return $safe; } # Else create it $safe = Lemonldap::NG::Common::Safe->new($self); # Get custom functions my @t = $self->{customFunctions} ? split( /\s+/, $self->{customFunctions} ) : (); foreach (@t) { my $sub = $_; unless (/::/) { $sub = ref($self) . "::$_"; } else { s/^.*:://; } next if ( $self->can($_) ); eval "sub $_ { return $sub( '$self->{portal}', \@_ ); }"; $self->lmLog( $@, 'error' ) if ($@); } # Share %ENV $safe->share_from( 'main', ['%ENV'] ); # Share Safelib $safe->share_from( 'Lemonldap::NG::Common::Safelib', $Lemonldap::NG::Common::Safelib::functions ); # Share custom functions and &encode_base64 $safe->share( '&encode_base64', @t ); return $safe; } ##@method private boolean _deleteSession(Lemonldap::NG::Common::Session session, boolean preserveCookie) # Delete an existing session. If "securedCookie" is set to 2, the http session # will also be removed. # @param h tied Apache::Session object # @param preserveCookie do not delete cookie # @return True if session has been deleted sub _deleteSession { my ( $self, $session, $preserveCookie ) = @_; # Invalidate http cookie and session, if set if ( $self->{securedCookie} >= 2 ) { # Try to find a linked http session (securedCookie == 2) if ( my $id2 = $session->data->{_httpSession} ) { if ( my $session2 = $self->getApacheSession( $id2, 1 ) ) { $session2->remove; if ( $session2->error ) { $self->lmLog( "Unable to remove linked session $id2", 'debug' ); $self->lmLog( $session2->error, 'debug' ); } } } # Create an obsolete cookie to remove it push @{ $self->{cookie} }, $self->cookie( -name => $self->{cookieName} . 'http', -value => 0, -domain => $self->{domain}, -path => "/", -secure => 0, -expires => '-1d', @_, ) unless ($preserveCookie); } $session->remove; # Create an obsolete cookie to remove it push @{ $self->{cookie} }, $self->cookie( -name => $self->{cookieName}, -value => 0, -domain => $self->{domain}, -path => "/", -secure => 0, -expires => '-1d', @_, ) unless ($preserveCookie); # Log my $user = $self->{sessionInfo}->{ $self->{whatToTrace} }; $self->_sub( 'userNotice', "User $user has been disconnected" ) if $user; return $session->error ? 0 : 1; } ##@method private void _dump(void* variable) # Dump variable in debug mode # @param $variable # @return void sub _dump { my $self = shift; my $variable = shift; require Data::Dumper; $Data::Dumper::Indent = 0; $self->lmLog( "Dump: " . Data::Dumper::Dumper($variable), 'debug' ); return; } ##@method protected string info(string t) # Get or set info to display to the user. # @param $t optional text to store # @return HTML text to display sub info { my ( $self, $t ) = @_; $self->{_info} .= $t if ( defined $t ); return $self->{_info}; } ##@method protected string loginInfo(string t) # Get or set info to display to the user on login screen # @param $t optional text to store # @return HTML text to display sub loginInfo { my ( $self, $t ) = @_; $self->{_loginInfo} .= $t if ( defined $t ); return $self->{_loginInfo}; } ##@method public void printImage(string file, string type) # Print image to STDOUT # @param $file The path to the file to print # @param $type The content-type to use (ie: image/png) # @return void sub printImage { my ( $self, $file, $type ) = @_; binmode STDOUT; unless ( open( IMAGE, '<', $file ) ) { $self->lmLog( "Could not display image '$file'", 'error' ); return; } print $self->header( $type . '; charset=utf-8; content-length=' . ( stat($file) )[10] ); my $buffer = ""; while ( read( IMAGE, $buffer, 4096 ) ) { print $buffer; } close(IMAGE); } sub stamp { my $self = shift; return $self->{cipher} ? $self->{cipher}->encrypt( time() ) : 1; } ## @method string convertSec(int sec) # Convert seconds to hours, minutes, seconds # @param $sec number of seconds # @return a formated time sub convertSec { my ( $self, $sec ) = @_; my ( $day, $hrs, $min ) = ( 0, 0, 0 ); # Calculate the minutes if ( $sec > 60 ) { $min = $sec / 60, $sec %= 60; $min = int($min); } # Calculate the hours if ( $min > 60 ) { $hrs = $min / 60, $min %= 60; $hrs = int($hrs); } # Calculate the days if ( $hrs > 24 ) { $day = $hrs / 24, $hrs %= 24; $day = int($day); } # Return the date return ( $day, $hrs, $min, $sec ); } ## @method string getSkin() # Return skin name # @return skin name sub getSkin { my ($self) = @_; my $skin = $self->{portalSkin}; # Fill sessionInfo to eval rule if empty (unauthenticated user) $self->{sessionInfo}->{_url} ||= $self->{urldc}; $self->{sessionInfo}->{ipAddr} ||= $self->ipAddr; # Load specific skin from skinRules if ( $self->{portalSkinRules} ) { foreach my $skinRule ( sort keys %{ $self->{portalSkinRules} } ) { if ( $self->safe->reval($skinRule) ) { $skin = $self->{portalSkinRules}->{$skinRule}; $self->lmLog( "Skin $skin selected from skin rule", 'debug' ); } } } # Check skin GET/POST parameter my $skinParam = $self->param('skin'); if ( defined $skinParam && !$self->checkXSSAttack( 'skin', $skinParam ) ) { $skin = $skinParam; $self->lmLog( "Skin $skin selected from GET/POST parameter", 'debug' ); } return $skin; } ############################################################### # MAIN subroutine: call all steps until one returns something # # different than PE_OK # ############################################################### ##@method boolean process() # Main method calling functions issued from: # - itself: # - controlUrlOrigin # - checkNotifBack # - controlExistingSession # - setMacros # - setLocalGroups # - setPersistentSessionInfo # - removeOther # - grantSession # - store # - buildCookie # - checkNotification # - autoRedirect # - updateStatus # - authentication module: # - authInit # - extractFormInfo # - setAuthSessionInfo # - authenticate # - authFinish # - userDB module: # - userDBInit # - getUser # - setSessionInfo # - setGroups # - passwordDB module: # - passwordDBInit # - modifyPassword # - issuerDB module: # - issuerDBInit # - issuerForUnAuthUser # - issuerForAuthUser # - MailReset: # - sendPasswordMail # #@return 1 if all is OK, 0 if session isn't created or a notification has to be done sub process { my ($self) = @_; $self->{error} = PE_OK; $self->{error} = $self->_subProcess( qw(controlUrlOrigin checkNotifBack controlExistingSession issuerDBInit authInit issuerForUnAuthUser extractFormInfo userDBInit getUser setAuthSessionInfo passwordDBInit modifyPassword setSessionInfo setMacros setGroups setPersistentSessionInfo setLocalGroups sendPasswordMail authenticate authFinish userDBFinish passwordDBFinish grantSession removeOther store buildCookie checkNotification issuerForAuthUser autoRedirect) ); $self->updateStatus; return ( ( $self->{error} > 0 ) ? 0 : 1 ); } ##@apmethod int controlUrlOrigin() # If the user was redirected here, loads 'url' parameter. # Check also confirm parameter. #@return Lemonldap::NG::Portal constant sub controlUrlOrigin { my $self = shift; if ( my $c = $self->param('confirm') ) { # Replace confirm stamp by 1 or -1 $c =~ s/^(-?)(.*)$/${1}1/; # Decrypt confirm stamp if cipher available # and confirm not already decrypted if ( $self->{cipher} and $2 ne "1" ) { my $time = time() - $self->{cipher}->decrypt($2); if ( $time < 600 ) { $self->lmLog( "Confirm parameter accepted $c", 'debug' ); $self->param( 'confirm', $c ); } else { $self->lmLog( 'Confirmation to old, refused', 'notice' ); $self->param( 'confirm', 0 ); } } } $self->{_url} ||= ''; if ( my $url = $self->param('url') ) { # REJECT NON BASE64 URL except for CAS IssuerDB if ( $self->get_module('issuer') ne "CAS" ) { if ( $url =~ m#[^A-Za-z0-9\+/=]# ) { $self->lmLog( "Value must be in BASE64 (param: url | value: $url)", "warn" ); return PE_BADURL; } $self->{urldc} = decode_base64($url); $self->{urldc} =~ s/[\r\n]//sg; } else { $self->{urldc} = $url; } # For logout request, test if Referer comes from an authorizated site my $tmp = ( $self->param('logout') ? $ENV{HTTP_REFERER} : $self->{urldc} ); # XSS attack if ( $self->checkXSSAttack( $self->param('logout') ? 'HTTP Referer' : 'urldc', $self->{urldc} ) ) { delete $self->{urldc}; return PE_BADURL; } # Non protected hosts if ( $tmp and !$self->isTrustedUrl($tmp) ) { $self->lmLog( "URL contains a non protected host (param: " . ( $self->param('logout') ? 'HTTP Referer' : 'urldc' ) . " | value: $tmp)", "warn" ); delete $self->{urldc}; return PE_BADURL; } $self->{_url} = $url; } PE_OK; } ##@apmethod int checkNotifBack() # Checks if a message has been notified to the connected user. # Call Lemonldap::NG::Common::Notification::checkNotification() #@return Lemonldap::NG::Portal error code sub checkNotifBack { my $self = shift; if ( $self->{notification} and grep( /^reference/, $self->param() ) ) { $self->lmLog( "User was on a notification step", 'debug' ); unless ( $self->{notifObject}->checkNotification($self) ) { $self->lmLog( "All notifications have not been accepted, display them again", 'debug' ); $self->{_notification} = $self->{notifObject}->getNotification($self); return PE_NOTIFICATION; } else { $self->lmLog( "All notifications have been accepted, follow the authentication process", 'debug' ); $self->{error} = $self->_subProcess( qw(issuerDBInit authInit issuerForAuthUser authFinish autoRedirect) ); return $self->{error} || PE_DONE; } } PE_OK; } ##@apmethod int controlExistingSession(string id) # Control existing sessions. # To overload to control what to do with existing sessions. # what to do with existing sessions ? # - nothing: user is authenticated and process returns true (default) # - delete and create a new session (not implemented) # - re-authentication (set portalForceAuthn to 1) #@param $id optional value of the session-id else cookies are examinated. #@return Lemonldap::NG::Portal constant sub controlExistingSession { my ( $self, $id ) = @_; my %cookies; %cookies = fetch CGI::Cookie unless ($id); # Special request "display captcha" if ( $self->param("displayCaptcha") ) { my $captcha = Lemonldap::NG::Common::Captcha->new( { storageModule => $self->{captchaStorage}, storageModuleOptions => $self->{captchaStorageOptions}, md5 => $self->param("displayCaptcha"), size => $self->{captcha_size}, } ); if ( $captcha && $captcha->image ) { binmode STDOUT; print $self->header( 'image/png' . '; charset=utf-8; content-length=' . length( $captcha->image ) ); print $captcha->image; } $self->quit(); } # Test if Lemonldap::NG cookie is available if ( $id or ( $cookies{ $self->{cookieName} } and $id = $cookies{ $self->{cookieName} }->value ) ) { my $apacheSession = $self->getApacheSession($id); if ($apacheSession) { %{ $self->{sessionInfo} } = %{ $apacheSession->data }; # Logout if required if ( $self->param('logout') ) { # Delete session unless ( $self->_deleteSession($apacheSession) ) { $self->lmLog( "Unable to delete session $id", 'error' ); $self->lmLog( $apacheSession->error, 'error' ); return PE_ERROR; } else { $self->lmLog( "Session $id deleted from global storage", 'debug' ); } # Call issuerDB logout on each used issuerDBmodule my $issuerDBList = $self->{sessionInfo}->{_issuerDB}; if ( defined $issuerDBList ) { foreach my $issuerDBtype ( split( /\Q$self->{multiValuesSeparator}\E/, $issuerDBList ) ) { my $module_name = 'Lemonldap::NG::Portal::IssuerDB' . $issuerDBtype; $self->lmLog( "Process logout for issuerDB module $issuerDBtype", 'debug' ); # Load current IssuerDB module unless ( $self->loadModule($module_name) ) { $self->lmLog( "Unable to load $module_name", 'error' ); next; } $self->{error} = $self->_subProcess( $module_name . "::issuerDBInit", $module_name . '::issuerLogout' ); } } # Call logout for the module used to authenticate $self->lmLog( "Process logout for authentication module " . $self->{sessionInfo}->{_auth}, 'debug' ); if ( $self->{sessionInfo}->{'_auth'} ne $self->get_module('auth') ) { my $module_name = 'Lemonldap::NG::Portal::Auth' . $self->{sessionInfo}->{_auth}; unless ( $self->loadModule($module_name) ) { $self->lmLog( "Unable to load $module_name", 'error' ); } else { eval { $self->{error} = $self->_subProcess( $module_name . "::authInit", $module_name . "::authLogout" ); }; } } else { eval { $self->{error} = $self->_subProcess( 'authInit', 'authLogout' ); }; } if ($@) { $self->lmLog( "Error when calling authentication logout: $@", 'debug' ); } return $self->{error} if $self->{error} > 0; # Collect logout services and build hidden iFrames if ( $self->{logoutServices} and %{ $self->{logoutServices} } ) { $self->lmLog( "Create iFrames to forward logout to services", 'debug' ); $self->info( "
" . $self->msg(PM_USER) . " | " if ($displayUser); $tmp .= "" . $self->msg(PM_DATE) . " | "; $tmp .= "" . $self->msg(PM_IP) . " | "; $tmp .= "" . $self->{sessionDataToRemember}->{$_} . " | " foreach ( keys %{ $self->{sessionDataToRemember} } ); $tmp .= '' . $self->msg(PM_ERROR_MSG) . ' | ' if ($displayError); $tmp .= '
---|---|---|---|---|
$session->{user} | " if ($displayUser); $tmp .= ""; $tmp .= " | $session->{ipAddr} | "; $tmp .= "" . ( $session->{$_} || "" ) . " | " foreach ( keys %{ $self->{sessionDataToRemember} } ); $tmp .= "$session->{error} | " if ($displayError); $tmp .= "
" . $self->msg(PM_REMOVE_OTHER_SESSIONS) . "
"; } ##@apmethod int grantSession() # Check grantSessionRule to allow session creation. #@return Lemonldap::NG::Portal constant sub grantSession { my ($self) = @_; if ( defined $self->{grantSessionRule} ) { # Eval grantSessionRule # Kept for backward compatibility with LL::NG 1.1.2 and previous my $grantSessionRule = $self->{grantSessionRule}; unless ( $self->safe->reval($grantSessionRule) ) { $self->lmLog( "User " . $self->{user} . " was not granted to open session", 'error' ); $self->registerLogin(PE_SESSIONNOTGRANTED); return PE_SESSIONNOTGRANTED; } } # Eval grantSessionRules sorted by comments sub sortByComment { my $A = ( $a =~ /^.*?##(.*)$/ )[0]; my $B = ( $b =~ /^.*?##(.*)$/ )[0]; return !$A ? 1 : !$B ? -1 : $A cmp $B; } foreach ( sort sortByComment keys %{ $self->{grantSessionRules} } ) { $self->lmLog( "Grant session condition \"$_\" checked", "debug" ); unless ( $self->safe->reval($_) ) { $self->lmLog( "User " . $self->{user} . " was not granted to open session", 'error' ); my $msg = $self->safe->reval( $self->{grantSessionRules}->{$_} ); $msg = $self->{grantSessionRules}->{$_} if ($@); $self->{ "error_" . PE_SESSIONNOTGRANTED } = $msg if ($msg); $self->registerLogin(PE_SESSIONNOTGRANTED); return PE_SESSIONNOTGRANTED; } } my $user = $self->{sessionInfo}->{ $self->{whatToTrace} }; $self->_sub( 'userNotice', "Session granted for $user" ) if ($user); $self->registerLogin(PE_OK); return PE_OK; } ##@apmethod int store() # Store user's datas in sessions database. # Now, the user is known, authenticated and session variable are evaluated. # It's time to store his parameters with Apache::Session::* module #@return Lemonldap::NG::Portal constant sub store { my ($self) = @_; # Now, user is authenticated => inform Apache $self->setApacheUser( $self->{sessionInfo}->{ $self->{whatToTrace} } ); # Create second session for unsecure cookie if ( $self->{securedCookie} == 2 ) { my $session2 = $self->getApacheSession( undef, 1 ); my %infos = %{ $self->{sessionInfo} }; $infos{_httpSessionType} = 1; $session2->update( \%infos ); $self->{sessionInfo}->{_httpSession} = $session2->id; } # Main session my $session = $self->getApacheSession( $self->{id}, 0, $self->{force} ); return PE_APACHESESSIONERROR unless ($session); # Compute unsecure cookie value if needed if ( $self->{securedCookie} == 3 ) { $self->{sessionInfo}->{_httpSession} = $self->{cipher}->encryptHex( $self->{id}, "http" ); } # Fill session my $infos = {}; foreach my $k ( keys %{ $self->{sessionInfo} } ) { next unless defined $self->{sessionInfo}->{$k}; my $displayValue = $self->{sessionInfo}->{$k}; if ( $self->{hiddenAttributes} =~ /\b$k\b/ ) { $displayValue = '****'; } $self->lmLog( "Store $displayValue in session key $k", 'debug' ); $self->_dump($displayValue) if ref($displayValue); $infos->{$k} = $self->{sessionInfo}->{$k}; } $session->update($infos); PE_OK; } ## @apmethod int authFinish # Call authFinish method from authentication module # @return Lemonldap::NG::Portal constant sub authFinish { my $self = shift; # Remove captcha session if ( $self->{captcha_check_code} ) { $self->removeCaptcha( $self->{captcha_check_code} ); } eval { $self->{error} = $self->SUPER::authFinish; }; if ($@) { $self->lmLog( "Optional authFinish method not defined in current authentication module: $@", 'debug' ); return PE_OK; } return $self->{error}; } ## @apmethod int userDBFinish # Call userDBFinish method from userDB module # @return Lemonldap::NG::Portal constant sub userDBFinish { my $self = shift; eval { $self->{error} = $self->SUPER::userDBFinish; }; if ($@) { $self->lmLog( "Optional userDBFinish method not defined in current userDB module: $@", 'debug' ); return PE_OK; } return $self->{error}; } ## @apmethod int passwordDBFinish # Call passwordDBFinish method from passwordDB module # @return Lemonldap::NG::Portal constant sub passwordDBFinish { my $self = shift; eval { $self->{error} = $self->SUPER::passwordDBFinish; }; if ($@) { $self->lmLog( "Optional passwordDBFinish method not defined in current passwordDB module: $@", 'debug' ); return PE_OK; } return $self->{error}; } ##@apmethod int buildCookie() # Build the Lemonldap::NG cookie. #@return Lemonldap::NG::Portal constant sub buildCookie { my $self = shift; push @{ $self->{cookie} }, $self->cookie( -name => $self->{cookieName}, -value => $self->{id}, -domain => $self->{domain}, -path => "/", -secure => $self->{securedCookie}, -httponly => $self->{httpOnly}, -expires => $self->{cookieExpiration}, @_, ); if ( $self->{securedCookie} >= 2 ) { push @{ $self->{cookie} }, $self->cookie( -name => $self->{cookieName} . "http", -value => $self->{sessionInfo}->{_httpSession}, -domain => $self->{domain}, -path => "/", -secure => 0, -httponly => $self->{httpOnly}, -expires => $self->{cookieExpiration}, @_, ); } PE_OK; } ##@apmethod int checkNotification() # Check if messages has to be notified. # Call Lemonldap::NG::Common::Notification::getNotification(). #@return Lemonldap::NG::Portal constant sub checkNotification { my $self = shift; if ( $self->{notification} and $self->{_notification} ||= $self->{notifObject}->getNotification($self) ) { return PE_NOTIFICATION; } return PE_OK; } ## @apmethod int issuerForAuthUser() # Check IssuerDB activation rule # Register used module in user session # @return Lemonldap::NG::Portal constant sub issuerForAuthUser { my $self = shift; # User information my $user = $self->{sessionInfo}->{ $self->{whatToTrace} } || 'unknown'; # Get active module my $issuerDBtype = $self->get_module('issuer'); # Eval activation rule my $rule = $self->{ 'issuerDB' . $issuerDBtype . 'Rule' }; if ( defined $rule ) { $self->lmLog( "Applying rule: $rule", 'debug' ); unless ( $self->safe->reval($rule) ) { $self->lmLog( "User $user was not allowed to use IssuerDB $issuerDBtype", 'warn' ); return PE_OK; } } else { $self->lmLog( "No rule found for IssuerDB $issuerDBtype", 'debug' ); } $self->lmLog( "User $user allowed to use IssuerDB $issuerDBtype", 'debug' ); # Register IssuerDB module in session $self->addSessionValue( '_issuerDB', $issuerDBtype, $self->{id} ); # Update session activity unless for Null IssuerDB $self->updateSession( { '_lastSeen' => time() } ) if ( $self->{timeoutActivity} && $issuerDBtype ne 'Null' ); # Call IssuerDB module method return $self->SUPER::issuerForAuthUser(); } ##@apmethod int autoRedirect() # If the user was redirected to the portal, we will now redirect him # to the requested URL. #@return Lemonldap::NG::Portal constant sub autoRedirect { my $self = shift; $self->clearHiddenFormValue(); # Default redirection URL $self->{urldc} ||= $self->{portal} if ( $self->{mustRedirect} or $self->info() ); # Display info before redirecting if ( $self->info() ) { $self->{infoFormMethod} = $self->param('method') || "get"; $self->clearHiddenFormValue(); my ($query_string) = ( $self->{urldc} =~ /.+?\?(.+)/ ); if ($query_string) { $self->lmLog( "Transform query string $query_string into hidden form values", 'debug' ); my $query = CGI->new($query_string); my $formFields = $query->Vars; foreach ( keys %$formFields ) { $self->setHiddenFormValue( $_, $formFields->{$_}, "", 0 ); } } return PE_INFO; } # Redirection should be made if # - urldc defined if ( $self->{urldc} ) { # Cross-domain mechanism if ( $self->{cda} and $self->{id} and $self->{urldc} !~ m#^https?://[^/]*$self->{domain}(:\d+)?/#oi and $self->isTrustedUrl( $self->{urldc} ) ) { my $ssl = $self->{urldc} =~ /^https/; $self->lmLog( 'CDA request', 'debug' ); $self->{urldc} .= ( $self->{urldc} =~ /\?/ ? '&' : '?' ) . ( ( $self->{securedCookie} < 2 or $ssl ) ? $self->{cookieName} . "=" . $self->{id} : $self->{cookieName} . "http=" . $self->{sessionInfo}->{_httpSession} ); } $self->updateStatus; if ( $self->safe->reval( $self->{jsRedirect} ) ) { $self->{redirectFormMethod} = "get"; return PE_REDIRECT; } else { print $self->redirect( -status => '303 See Other', -location => $self->{urldc}, ); $self->quit(); } } PE_OK; } ## @method void returnSOAPMessage() # Print SOAP message # @return void sub returnSOAPMessage { my $self = shift; # Quit if no SOAP message $self->quit() unless $self->{SOAPMessage}; # Print HTTP header and SOAP message binmode( STDOUT, ":bytes" ); print $self->header( -type => 'application/xml' ); print $self->{SOAPMessage}; # Exit $self->quit(); } ## @method void autoPost() # Transfer POST data with auto submit # @return void sub autoPost { my $self = shift; # Get URL and Form fields $self->{urldc} = $self->{postUrl}; my $formFields = $self->{postFields}; $self->clearHiddenFormValue(); foreach ( keys %$formFields ) { $self->setHiddenFormValue( $_, $formFields->{$_}, "", 0 ); } # Display info before redirecting if ( $self->info() ) { $self->{infoFormMethod} = $self->param('method') || "post"; return PE_INFO; } $self->{redirectFormMethod} = "post"; return PE_REDIRECT; } ## @method HASHREF getCustomTemplateParameters() # Find custom templates parameters # @return Custom parameters sub getCustomTemplateParameters { my $self = shift; my $customTplParams = {}; foreach ( keys %$self ) { next unless ( $_ =~ /^tpl_(.+)$/ ); my $tplParam = $1; my $tplValue = $self->{ "tpl_" . $tplParam }; $self->lmLog( "Set custom template parameter $tplParam with $tplValue", 'debug' ); $customTplParams->{$tplParam} = $tplValue; } return $customTplParams; } 1; __END__ =head1 NAME =encoding utf8 Lemonldap::NG::Portal::Simple - Base module for building Lemonldap::NG compatible portals =head1 SYNOPSIS use Lemonldap::NG::Portal::Simple; my $portal = new Lemonldap::NG::Portal::Simple( domain => 'example.com', globalStorage => 'Apache::Session::MySQL', globalStorageOptions => { DataSource => 'dbi:mysql:database=dbname;host=127.0.0.1', UserName => 'db_user', Password => 'db_password', TableName => 'sessions', LockDataSource => 'dbi:mysql:database=dbname;host=127.0.0.1', LockUserName => 'db_user', LockPassword => 'db_password', }, ldapServer => 'ldap.domaine.com,ldap-backup.domaine.com', securedCookie => 1, exportedVars => { uid => 'uid', cn => 'cn', mail => 'mail', appli => 'appli', }, # Activate SOAP service Soap => 1 ); if($portal->process()) { # Write here the menu with CGI methods. This page is displayed ONLY IF # the user was not redirected here. print $portal->header('text/html; charset=utf-8'); # DON'T FORGET THIS (see L