package Lemonldap::NG::Portal::Simple; use strict; use warnings; use Exporter 'import'; use Net::LDAP; use warnings; use MIME::Base64; use CGI; use CGI::Cookie; require POSIX; use Lemonldap::NG::Portal::_i18n; our $VERSION = '0.78'; our @ISA = qw(CGI Exporter); # Constants sub PE_DONE { -1 } sub PE_OK { 0 } sub PE_SESSIONEXPIRED { 1 } sub PE_FORMEMPTY { 2 } sub PE_WRONGMANAGERACCOUNT { 3 } sub PE_USERNOTFOUND { 4 } sub PE_BADCREDENTIALS { 5 } sub PE_LDAPCONNECTFAILED { 6 } sub PE_LDAPERROR { 7 } sub PE_APACHESESSIONERROR { 8 } sub PE_FIRSTACCESS { 9 } sub PE_BADCERTIFICATE { 10 } sub PE_PP_ACCOUNT_LOCKED { 21 } sub PE_PP_PASSWORD_EXPIRED { 22 } # EXPORTER PARAMETERS our %EXPORT_TAGS = ( 'all' => [ qw( 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 import ) ], 'constants' => [ qw( 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 ) ], ); our @EXPORT_OK = ( @{ $EXPORT_TAGS{'all'} } ); our @EXPORT = qw( 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 import ); # CONSTRUCTOR sub new { my $class = shift; my $self = $class->SUPER::new(); $self->getConf(@_) or die "Unable to get configuration"; die("You've to indicate a an Apache::Session storage module !") unless ( $self->{globalStorage} ); eval "require " . $self->{globalStorage}; die( "Module " . $self->{globalStorage} . " not found in \@INC" ) if ($@); die("You've to indicate a domain for cookies") unless ( $self->{domain} ); $self->{domain} =~ s/^([^\.])/.$1/; $self->{ldapServer} ||= 'localhost'; $self->{ldapPort} ||= 389; $self->{securedCookie} ||= 0; $self->{cookieName} ||= "lemonldap"; $self->{ldapPpolicyControl} ||= 0; if ( $self->{authentication} and $self->{authentication} ne "ldap" ) { # $Lemonldap::NG::Portal::AuthSSL::OVERRIDE does not overload $self # variables: if the administrator has defined a sub, we respect it my $tmp = 'require Lemonldap::NG::Portal::Auth' . $self->{authentication} . '; $tmp = $Lemonldap::NG::Portal::Auth' . $self->{authentication} . '::OVERRIDE;'; eval $tmp; die($@) if($@); %$self = ( %$tmp, %$self ); } return $self; } # getConf basic, copy all parameters in $self. Overloaded in SharedConf.pm sub getConf { my ($self) = shift; my %args; if ( ref( $_[0] ) ) { %args = %{ $_[0] }; } else { %args = @_; } %$self = ( %$self, %args ); 1; } # error calls i18n.pm to dysplay error in the wanted language sub error { my $self = shift; return &Lemonldap::NG::Portal::_i18n::error( $self->{error}, $ENV{HTTP_ACCEPT_LANGUAGE} ); } # Private sub used to bind to LDAP server both with Lemonldap::NG account and user # credentials if LDAP authentication is used sub _bind { my ( $ldap, $dn, $password ) = @_; my $mesg; if ( $dn and $password ) { # named bind $mesg = $ldap->bind( $dn, password => $password ); } else { # anonymous bind $mesg = $ldap->bind(); } if ( $mesg->code() != 0 ) { return 0; } return 1; } # CGI.pm overload to add Lemonldap::NG cookie sub header { my $self = shift; if ( $self->{cookie} ) { $self->SUPER::header( @_, -cookie => $self->{cookie} ); } else { $self->SUPER::header(@_); } } # CGI.pm overload to add Lemonldap::NG cookie sub redirect { my $self = shift; if ( $self->{cookie} ) { $self->SUPER::redirect( @_, -cookie => $self->{cookie} ); } else { $self->SUPER::redirect(@_); } } # Externalise functions execution sub _subProcess { my $self = shift; my @subs = @_; my $err = undef; foreach my $sub (@subs) { if ( $self->{$sub} ) { last if ( $err = &{ $self->{$sub} }($self) ); } else { last if ( $err = $self->$sub ); } } return $err; } ############################################################### # MAIN subroutine: call all steps until one returns something # # different than PE_OK # ############################################################### sub process { my ($self) = @_; $self->{error} = PE_OK; $self->{error} = $self->_subProcess( qw(controlUrlOrigin controlExistingSession extractFormInfo formateParams formateFilter connectLDAP bind search setSessionInfo setMacros setGroups authenticate store unbind buildCookie log autoRedirect) ); return ( ( $self->{error} > 0 ) ? 0 : 1 ); } # 1. If the user was redirected here, we have to load 'url' parameter sub controlUrlOrigin { my $self = shift; if ( $self->param('url') ) { $self->{urldc} = decode_base64( $self->param('url') ); } PE_OK; } # 2. Control existing sessions # what to do with existing sessions ? # - delete and create a new session (default) # - re-authentication (actual scheme) # - nothing: user is authenticated and process # returns true sub controlExistingSession { my $self = shift; my %cookies = fetch CGI::Cookie; # Test if Lemonldap::NG cookie is available if ( $cookies{$self->{cookieName}} and my $id = $cookies{$self->{cookieName}}->value ) { my %h; # Trying to recover session from global session storage eval { tie %h, $self->{globalStorage}, $id, $self->{globalStorageOptions}; }; if ( $@ or not tied(%h) ) { # Session not available (expired ?) print STDERR "Session $id isn't yet available ($ENV{REMOTE_ADDR})\n"; return PE_OK; } # Logout if required if($self->param('logout')) { # Delete session in global storage tied(%h)->delete; # Delete cookie $self->{id} = ""; $self->buildCookie(); if( $self->{urldc} ) { if( $self->{autoRedirect} ) { &{ $self->{autoRedirect} }($self); } else { $self->autoRedirect(); } } return PE_FIRSTACCESS; } $self->{id} = $id; # A session has been find => calling &existingSession my($r, $datas); %$datas = %h; untie(%h); if ( $self->{existingSession} ) { $r = &{ $self->{existingSession} }($self, $id, $datas) } else { $r = $self->existingSession($id, $datas); } if ( $r == PE_DONE) { for my $sub qw(log autoRedirect) { if ( $self->{$sub} ) { last if ( $self->{error} = &{ $self->{$sub} }($self) ); } else { last if ( $self->{error} = $self->$sub ); } } return $self->{error} || PE_DONE; } else { return $r; } } PE_OK; } sub existingSession { my ($self, $id, $datas) = @_; PE_OK; } # 3. In ldap authentication scheme, we load here user and password from HTML # form sub extractFormInfo { my $self = shift; return PE_FIRSTACCESS unless ( $self->param('user') ); return PE_FORMEMPTY unless ( length( $self->{'user'} = $self->param('user') ) > 0 && length( $self->{'password'} = $self->param('password') ) > 0 ); PE_OK; } # Unused. You can overload if you have to modify user and password before # authentication sub formateParams() { PE_OK; } # 4. By default, the user is searched in the LDAP server with its UID. To use # it with Active Directory, overload it to use CN instead of UID. sub formateFilter { my $self = shift; $self->{filter} = "(&(uid=" . $self->{user} . ")(objectClass=person))"; PE_OK; } # 5. First LDAP connexion used to find user DN with the filter defined before. sub connectLDAP { my $self = shift; return PE_OK if ( $self->{ldap} ); my $useTls = 0; my $tlsParam; foreach my $server ( split /[\s,]+/, $self->{ldapServer} ) { if ( $server =~ m{^ldap\+tls://([^/]+)/?\??(.*)$} ) { $useTls = 1; $server = $1; $tlsParam = $2 || ""; } else { $useTls = 0; } last if $self->{ldap} = Net::LDAP->new( $server, port => $self->{ldapPort}, onerror => undef, ); } return PE_LDAPCONNECTFAILED unless ( $self->{ldap} ); if ($useTls) { my %h = split( /[&=]/, $tlsParam ); $h{cafile} = $self->{caFile} if( $self->{caFile} ); $h{capath} = $self->{caPath} if( $self->{caPath} ); my $mesg = $self->{ldap}->start_tls(%h); $mesg->code && return PE_LDAPCONNECTFAILED; } PE_OK; } # 6. LDAP bind with Lemonldap::NG account or anonymous unless defined sub bind { my $self = shift; $self->connectLDAP unless ( $self->{ldap} ); return PE_WRONGMANAGERACCOUNT unless ( &_bind( $self->{ldap}, $self->{managerDn}, $self->{managerPassword} ) ); PE_OK; } # 7. Search the DN sub search { my $self = shift; my $mesg = $self->{ldap}->search( base => $self->{ldapBase}, scope => 'sub', filter => $self->{filter}, ); if ( $mesg->code() != 0 ) { print STDERR $mesg->error . "\n"; return PE_LDAPERROR; } return PE_USERNOTFOUND unless ( $self->{entry} = $mesg->entry(0) ); $self->{dn} = $self->{entry}->dn(); PE_OK; } # 8. Load all parameters included in exportedVars parameter. # Multi-value parameters are loaded in a single string with # '; ' separator sub setSessionInfo { my ($self) = @_; $self->{sessionInfo}->{dn} = $self->{dn}; $self->{sessionInfo}->{startTime} = &POSIX::strftime("%Y%m%d%H%M%S",localtime()); unless ( $self->{exportedVars} ) { foreach (qw(uid cn mail)) { $self->{sessionInfo}->{$_} = join( '; ', $self->{entry}->get_value($_) ) || ""; } } elsif ( ref( $self->{exportedVars} ) eq 'HASH' ) { foreach ( keys %{ $self->{exportedVars} } ) { $self->{sessionInfo}->{$_} = join( '; ', $self->{entry}->get_value( $self->{exportedVars}->{$_} ) ) || ""; } } else { foreach ( @{ $self->{exportedVars} } ) { $self->{sessionInfo}->{$_} = join( '; ', $self->{entry}->get_value($_) ) || ""; } } PE_OK; } # 9. Unused here, but overloaded in SharedConf.pm sub setMacros { PE_OK; } # 10. Unused here, but overloaded in SharedConf.pm sub setGroups { PE_OK; } # 11. Now, LDAP will not be used by Lemonldap::NG except for LDAP # authentication scheme sub unbind { my $self = shift; $self->{ldap}->unbind if $self->{ldap}; delete $self->{ldap}; PE_OK; } # 12. Default authentication: LDAP bind with user credentials sub authenticate { my $self = shift; $self->unbind(); my $err; return $err unless ( ( $err = $self->connectLDAP ) == PE_OK ); # Check if we use Ppolicy control if ( $self->{ldapPpolicyControl} ) { # require Perl module eval 'require Net::LDAP::Control::PasswordPolicy'; die( 'Module Net::LDAP::Control::PasswordPolicy not found in @INC' ) if ($@); eval 'use Net::LDAP::Constant qw( LDAP_CONTROL_PASSWORDPOLICY LDAP_PP_ACCOUNT_LOCKED LDAP_PP_PASSWORD_EXPIRED );'; no strict 'subs'; # Create Control object my $pp = Net::LDAP::Control::PasswordPolicy->new; # Bind with user credentials my $mesg = $self->{ldap}->bind( $self->{dn}, password => $self->{password}, control => [ $pp ] ); # Get bind response return PE_OK if ( $mesg->code == 0 ); # Get server control response my ( $resp ) = $mesg->control( LDAP_CONTROL_PASSWORDPOLICY ); if ( defined $resp ) { my $pp_error = $resp->error; if ( $pp_error ) { return PE_PP_ACCOUNT_LOCKED if ( $pp_error == LDAP_PP_ACCOUNT_LOCKED ); return PE_PP_PASSWORD_EXPIRED if ( $pp_error == LDAP_PP_PASSWORD_EXPIRED ); } else { return PE_BADCREDENTIALS; } } else { return PE_LDAPERROR; } } else { return PE_BADCREDENTIALS unless ( &_bind( $self->{ldap}, $self->{dn}, $self->{password} ) ); } PE_OK; } # 13. Now, the user is authenticated. It's time to store his parameters with # Apache::Session::* module sub store { my ($self) = @_; my %h; eval { tie %h, $self->{globalStorage}, undef, $self->{globalStorageOptions}; }; if ( $@ ) { print STDERR "$@\n"; return PE_APACHESESSIONERROR; } $self->{id} = $h{_session_id}; $h{$_} = $self->{sessionInfo}->{$_} foreach ( keys %{ $self->{sessionInfo} } ); $h{_utime} = time(); untie %h; PE_OK; } # 14. If all is done, we build the Lemonldap::NG cookie sub buildCookie { my $self = shift; $self->{cookie} = $self->cookie( -name => $self->{cookieName}, -value => $self->{id}, -domain => $self->{domain}, -path => "/", -secure => $self->{securedCookie}, @_, ); PE_OK; } # 15. By default, nothing is logged. Users actions are logged on applications. # It's easy to override this in the contructor : # my $portal = new Lemonldap::NG::Portal ( { # ... # log => sub {use Sys::Syslog; syslog; # openlog("Portal $$", 'ndelay', 'auth'); # syslog('notice', 'User '.$self->{user}.' is authenticated'); # }, # ... # } ); sub log { PE_OK; } # 16. If the user was redirected to the portal, we will now redirect him # to the requested URL sub autoRedirect { my $self = shift; if ( my $u = $self->{urldc} ) { print $self->SUPER::redirect( -uri => $u, -cookie => $self->{cookie}, -status => '302 Moved Temporary' ); # Remove this lines if your browsers does not support redirections # print << "EOF"; # #
# # # #