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.83'; our @ISA = qw(CGI Exporter); # Constants use constant { 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, }; # EXPORTER PARAMETERS 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_REDIRECT PE_PP_ACCOUNT_LOCKED PE_PP_PASSWORD_EXPIRED PE_CERTIFICATEREQUIRED PE_ERROR); our %EXPORT_TAGS = ( 'all' => [ @EXPORT, 'import' ], ); our @EXPORT_OK = ( @{ $EXPORT_TAGS{'all'} } ); # 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; $self->{authentication} ||= 'LDAP'; $self->{authentication} =~ s/^ldap/LDAP/; # Authentication module is required and has to be in @ISA my $tmp = 'Lemonldap::NG::Portal::Auth' . $self->{authentication}; $tmp =~ s/\s.*$//; eval "require $tmp"; die($@) if ($@); push @ISA, $tmp; # $self->{authentication} can contains arguments (key1 = scalar_value; # key2 = ...) $tmp = $self->{authentication}; $tmp =~ s/^\w+\s*//; my %h = split( /\s*[=;]\s*/, $tmp) if($tmp); %$self = ( %h, %$self ); $self->authInit(); 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}, shift || $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 ( $self, $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; } sub updateStatus { my ($self) = @_; print $Lemonldap::NG::Handler::Simple::statusPipe ( $self->{user} ? $self->{user} : $ENV{REMOTE_ADDR} ) . " => $ENV{SERVER_NAME}$ENV{SCRIPT_NAME} " . $self->{error} . "\n" if ($Lemonldap::NG::Handler::Simple::statusPipe); } ############################################################### # MAIN subroutine: call all steps until one returns something # # different than PE_OK # ############################################################### # extractFormInfo, setAuthSessionInfo and authenticate must be implemented in # auth modules sub process { my ($self) = @_; $self->{error} = PE_OK; $self->{error} = $self->_subProcess( qw(controlUrlOrigin controlExistingSession extractFormInfo formateParams formateFilter connectLDAP bind search setAuthSessionInfo setSessionInfo setMacros setGroups authenticate store unbind buildCookie log autoRedirect) ); $self->updateStatus; 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} ) { $self->{error} = PE_REDIRECT; 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 ) { $self->{error} = $self->_subProcess(qw(log autoRedirect)); return $self->{error} || PE_DONE; } else { return $r; } } PE_OK; } sub existingSession { my ( $self, $id, $datas ) = @_; 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} = $self->{authFilter} || "(&(uid=" . $self->{user} . ")(objectClass=inetOrgPerson))"; 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 ( $self->_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; } # sub setAuthSessionInfo has to be defined in auth module # 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} } ) { if ( my $tmp = $ENV{$_} ) { $tmp =~ s/[\r\n]/ /gs; $self->{sessionInfo}->{$_} = $tmp; } else { $self->{sessionInfo}->{$_} = join( '; ', $self->{entry}->get_value( $self->{exportedVars}->{$_} ) ) || ""; } } } else { die('Only hash reference are supported now in exportedVars'); } 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; } # 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; push @{$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} ) { $self->updateStatus; 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"; # #
# # # #