Project

General

Profile

1
#!/usr/bin/perl -w
2
#
3
#  '$RCSfile$'
4
#  Copyright: 2001 Regents of the University of California 
5
#
6
#   '$Author: walbridge $'
7
#     '$Date: 2009-03-26 11:09:50 -0700 (Thu, 26 Mar 2009) $'
8
# '$Revision: 4868 $' 
9
# 
10
# This program is free software; you can redistribute it and/or modify
11
# it under the terms of the GNU General Public License as published by
12
# the Free Software Foundation; either version 2 of the License, or
13
# (at your option) any later version.
14
#
15
# This program is distributed in the hope that it will be useful,
16
# but WITHOUT ANY WARRANTY; without even the implied warranty of
17
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18
# GNU General Public License for more details.
19
#
20
# You should have received a copy of the GNU General Public License
21
# along with this program; if not, write to the Free Software
22
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
23
#
24

    
25
#
26
# This is a web-based application for allowing users to register a new
27
# account for Metacat access.  We currently only support LDAP even
28
# though metacat could potentially support other types of directories.
29

    
30
use lib '../WEB-INF/lib';
31
use strict;             # turn on strict syntax checking
32
use Template;           # load the template-toolkit module
33
use CGI qw/:standard :html3/; # load the CGI module 
34
use Net::LDAP;          # load the LDAP net libraries
35
use Net::SMTP;          # load the SMTP net libraries
36
use Digest::SHA1;       # for creating the password hash
37
use MIME::Base64;       # for creating the password hash
38
use URI;                # for parsing URL syntax
39
use Config::Properties; # for parsing Java .properties files
40
use File::Basename;     # for path name parsing
41
use Cwd 'abs_path';
42

    
43
# Global configuration paramters
44
# This entire block (including skin parsing) could be pushed out to a separate .pm file
45
my $cgiUrl = $ENV{'SCRIPT_FILENAME'};
46
my $workingDirectory = dirname($cgiUrl);
47
my $metacatProps = "${workingDirectory}/../WEB-INF/metacat.properties";
48
my $properties = new Config::Properties();
49
unless (open (METACAT_PROPERTIES, $metacatProps)) {
50
    print "Content-type: text/html\n\n";
51
    print "Unable to locate Metacat properties. Working directory is set as " . 
52
        $workingDirectory .", is this correct?";
53
    exit(0);
54
}
55

    
56
$properties->load(*METACAT_PROPERTIES);
57

    
58
# local directory configuration
59
my $skinsDir = "${workingDirectory}/../style/skins";
60
my $templatesDir = abs_path("${workingDirectory}/../style/common/templates");
61
my $tempDir = $properties->getProperty('application.tempDir');
62

    
63
# url configuration
64
my $server = $properties->splitToTree(qr/\./, 'server');
65
my $contextUrl = 'http://' . $properties->getProperty('server.name');
66
if ($properties->getProperty('server.httpPort') ne '80') {
67
        $contextUrl = $contextUrl . ':' . $properties->getProperty('server.httpPort');
68
}
69
$contextUrl = $contextUrl . '/' .  $properties->getProperty('application.context');
70

    
71
my $metacatUrl = $contextUrl . "/metacat";
72
my $cgiPrefix = "/" . $properties->getProperty('application.context') . "/cgi-bin";
73
my $styleSkinsPath = $contextUrl . "/style/skins";
74
my $styleCommonPath = $contextUrl . "/style/common";
75

    
76
my @errorMessages;
77
my $error = 0;
78

    
79
# Import all of the HTML form fields as variables
80
import_names('FORM');
81

    
82
# Must have a config to use Metacat
83
my $skinName = "";
84
if ($FORM::cfg) {
85
    $skinName = $FORM::cfg;
86
} elsif ($ARGV[0]) {
87
    $skinName = $ARGV[0];
88
} else {
89
    debug("No configuration set.");
90
    print "Content-type: text/html\n\n";
91
    print 'LDAPweb Error: The registry requires a skin name to continue.';
92
    exit();
93
}
94

    
95
# Metacat isn't initialized, the registry will fail in strange ways.
96
if (!($metacatUrl)) {
97
    debug("No Metacat.");
98
    print "Content-type: text/html\n\n";
99
    'Registry Error: Metacat is not initialized! Make sure' .
100
        ' MetacatUrl is set correctly in ' .  $skinName . '.cfg';
101
    exit();
102
}
103

    
104
my $skinProperties = new Config::Properties();
105
if (!($skinName)) {
106
    $error = "Application misconfigured.  Please contact the administrator.";
107
    push(@errorMessages, $error);
108
} else {
109
    my $skinProps = "$skinsDir/$skinName/$skinName.properties";
110
    unless (open (SKIN_PROPERTIES, $skinProps)) {
111
        print "Content-type: text/html\n\n";
112
        print "Unable to locate skin properties at $skinProps.  Is this path correct?";
113
        exit(0);
114
    }
115
    $skinProperties->load(*SKIN_PROPERTIES);
116
}
117

    
118
my $config = $skinProperties->splitToTree(qr/\./, 'registry.config');
119

    
120
my $searchBase;
121
my $ldapUsername;
122
my $ldapPassword;
123
# TODO: when should we use surl instead? Is there a setting promoting one over the other?
124
# TODO: the default tree for accounts should be exposed somewhere, defaulting to unaffiliated
125
my $ldapurl = $properties->getProperty('auth.url');
126

    
127
# Java uses miliseconds, Perl expects whole seconds
128
my $timeout = $properties->getProperty('ldap.connectTimeLimit') / 1000;
129

    
130
# Get the CGI input variables
131
my $query = new CGI;
132
my $debug = 1;
133

    
134
#--------------------------------------------------------------------------80c->
135
# Set up the Template Toolkit to read html form templates
136

    
137
# templates hash, imported from ldap.templates tree in metacat.properties
138
my $templates = $properties->splitToTree(qr/\./, 'ldap.templates');
139
$$templates{'header'} = $skinProperties->getProperty("registry.templates.header");
140
$$templates{'footer'} = $skinProperties->getProperty("registry.templates.footer");
141

    
142
# set some configuration options for the template object
143
my $ttConfig = {
144
             INCLUDE_PATH => $templatesDir,
145
             INTERPOLATE  => 0,
146
             POST_CHOMP   => 1,
147
             DEBUG        => 1, 
148
             };
149

    
150
# create an instance of the template
151
my $template = Template->new($ttConfig) || handleGeneralServerFailure($Template::ERROR);
152

    
153
# custom LDAP properties hash
154
my $ldapCustom = $properties->splitToTree(qr/\./, 'ldap');
155

    
156
my $orgProps = $properties->splitToTree(qr/\./, 'organization');
157
my $orgNames = $properties->splitToTree(qr/\./, 'organization.name');
158
# pull out properties available e.g. 'name', 'base'
159
my @orgData = keys(%$orgProps);
160
my @orgList;
161
while (my ($oKey, $oVal) = each(%$orgNames)) {
162
    push(@orgList, $oKey);
163
}
164

    
165
my $authBase = $properties->getProperty("auth.base");
166
my $ldapConfig;
167
foreach my $o (@orgList) {
168
    foreach my $d (@orgData) {
169
        $ldapConfig->{$o}{$d} = $properties->getProperty("organization.$d.$o");
170
    }
171

    
172
    # set default base
173
    if (!$ldapConfig->{$o}{'base'}) {
174
        $ldapConfig->{$o}{'base'} = $authBase;
175
    }
176

    
177
    # include filter information. By default, our filters are 'o=$name', e.g. 'o=NAPIER'
178
    # these can be overridden by specifying them in metacat.properties. Non-default configs
179
    # such as UCNRS must specify all LDAP properties.
180
    if ($ldapConfig->{$o}{'base'} eq $authBase) {
181
        my $filter = "o=$o";
182
        if (!$ldapConfig->{$o}{'org'}) {
183
            $ldapConfig->{$o}{'org'} = $filter;
184
        }
185
        if (!$ldapConfig->{$o}{'filter'}) {
186
            $ldapConfig->{$o}{'filter'} = $filter;
187
        }
188
        # also include DN, which is just org + base
189
        if ($ldapConfig->{$o}{'org'}) {
190
            $ldapConfig->{$o}{'dn'} = $ldapConfig->{$o}{'org'} . "," . $ldapConfig->{$o}{'base'};
191
        }
192
    } else {
193
        $ldapConfig->{$o}{'dn'} = $ldapConfig->{$o}{'base'};
194
    }
195
    
196
    # set LDAP administrator user account
197
    if (!$ldapConfig->{$o}{'user'}) {
198
        $ldapConfig->{$o}{'user'} = $ldapConfig->{'unaffiliated'}{'user'};
199
    }
200
    # check for a fully qualified LDAP name. If it doesn't exist, append base.
201
    my @userParts = split(',', $ldapConfig->{$o}{'user'});
202
    if (scalar(@userParts) == 1) {
203
        $ldapConfig->{$o}{'user'} = $ldapConfig->{$o}{'user'} . "," . $ldapConfig->{$o}{'base'};
204
    }
205

    
206
    if (!$ldapConfig->{$o}{'password'}) {
207
        $ldapConfig->{$o}{'password'} = $ldapConfig->{'unaffiliated'}{'password'};
208
    }
209
}
210

    
211
#--------------------------------------------------------------------------80c->
212
# Define the main program logic that calls subroutines to do the work
213
#--------------------------------------------------------------------------80c->
214

    
215
# The processing step we are handling
216
my $stage = $query->param('stage') || $templates->{'stage'};
217

    
218
my $cfg = $query->param('cfg');
219
debug("started with stage $stage, cfg $cfg");
220

    
221
# define the possible stages
222
my %stages = (
223
              'initregister'      => \&handleInitRegister,
224
              'register'          => \&handleRegister,
225
              'registerconfirmed' => \&handleRegisterConfirmed,
226
              'simplesearch'      => \&handleSimpleSearch,
227
              'initaddentry'      => \&handleInitAddEntry,
228
              'addentry'          => \&handleAddEntry,
229
              'initmodifyentry'   => \&handleInitModifyEntry,
230
              'modifyentry'       => \&handleModifyEntry,
231
              'changepass'        => \&handleChangePassword,
232
              'initchangepass'    => \&handleInitialChangePassword,
233
              'resetpass'         => \&handleResetPassword,
234
              'initresetpass'     => \&handleInitialResetPassword,
235
             );
236

    
237
# call the appropriate routine based on the stage
238
if ( $stages{$stage} ) {
239
  $stages{$stage}->();
240
} else {
241
  &handleResponseMessage();
242
}
243

    
244
#--------------------------------------------------------------------------80c->
245
# Define the subroutines to do the work
246
#--------------------------------------------------------------------------80c->
247

    
248
sub fullTemplate {
249
    my $templateList = shift;
250
    my $templateVars = setVars(shift);
251

    
252
    $template->process( $templates->{'header'}, $templateVars );
253
    foreach my $tmpl (@{$templateList}) {
254
        $template->process( $templates->{$tmpl}, $templateVars );
255
    }
256
    $template->process( $templates->{'footer'}, $templateVars );
257
}
258

    
259
#
260
# create the initial registration form 
261
#
262
sub handleInitRegister {
263
  my $vars = shift;
264

    
265
  print "Content-type: text/html\n\n";
266
  # process the template files:
267
  fullTemplate(['register'], {stage => "register"}); 
268
  exit();
269
}
270

    
271
#
272
# process input from the register stage, which occurs when
273
# a user submits form data to create a new account
274
#
275
sub handleRegister {
276
    
277
    print "Content-type: text/html\n\n";
278

    
279
    my $allParams = { 'givenName' => $query->param('givenName'), 
280
                      'sn' => $query->param('sn'),
281
                      'o' => $query->param('o'), 
282
                      'mail' => $query->param('mail'), 
283
                      'uid' => $query->param('uid'), 
284
                      'userPassword' => $query->param('userPassword'), 
285
                      'userPassword2' => $query->param('userPassword2'), 
286
                      'title' => $query->param('title'), 
287
                      'telephoneNumber' => $query->param('telephoneNumber') };
288
    # Check that all required fields are provided and not null
289
    my @requiredParams = ( 'givenName', 'sn', 'o', 'mail', 
290
                           'uid', 'userPassword', 'userPassword2');
291
    if (! paramsAreValid(@requiredParams)) {
292
        my $errorMessage = "Required information is missing. " .
293
            "Please fill in all required fields and resubmit the form.";
294
        fullTemplate(['register'], { stage => "register",
295
                                     allParams => $allParams,
296
                                     errorMessage => $errorMessage });
297
        exit();
298
    } else {
299
        my $o = $query->param('o');    
300
        $searchBase = $ldapConfig->{$o}{'base'};  
301
    }
302

    
303
    # Search LDAP for matching entries that already exist
304
    # Some forms use a single text search box, whereas others search per
305
    # attribute.
306
    my $filter;
307
    if ($query->param('searchField')) {
308

    
309
      $filter = "(|" . 
310
                "(uid=" . $query->param('searchField') . ") " .
311
                "(mail=" . $query->param('searchField') . ")" .
312
                "(&(sn=" . $query->param('searchField') . ") " . 
313
                "(givenName=" . $query->param('searchField') . "))" . 
314
                ")";
315
    } else {
316
      $filter = "(|" . 
317
                "(uid=" . $query->param('uid') . ") " .
318
                "(mail=" . $query->param('mail') . ")" .
319
                "(&(sn=" . $query->param('sn') . ") " . 
320
                "(givenName=" . $query->param('givenName') . "))" . 
321
                ")";
322
    }
323

    
324
    my @attrs = [ 'uid', 'o', 'cn', 'mail', 'telephoneNumber', 'title' ];
325
    my $found = findExistingAccounts($ldapurl, $searchBase, $filter, \@attrs);
326

    
327
    # If entries match, send back a request to confirm new-user creation
328
    if ($found) {
329
      fullTemplate( ['registerMatch', 'register'], { stage => "registerconfirmed",
330
                                                     allParams => $allParams,
331
                                                     foundAccounts => $found });
332
    # Otherwise, create a new user in the LDAP directory
333
    } else {
334
        createAccount($allParams);
335
    }
336

    
337
    exit();
338
}
339

    
340
#
341
# process input from the registerconfirmed stage, which occurs when
342
# a user chooses to create an account despite similarities to other
343
# existing accounts
344
#
345
sub handleRegisterConfirmed {
346
  
347
    my $allParams = { 'givenName' => $query->param('givenName'), 
348
                      'sn' => $query->param('sn'),
349
                      'o' => 'unaffiliated', # only accept unaffiliated registration
350
                      'mail' => $query->param('mail'), 
351
                      'uid' => $query->param('uid'), 
352
                      'userPassword' => $query->param('userPassword'), 
353
                      'userPassword2' => $query->param('userPassword2'), 
354
                      'title' => $query->param('title'), 
355
                      'telephoneNumber' => $query->param('telephoneNumber') };
356
    print "Content-type: text/html\n\n";
357
    createAccount($allParams);
358
    exit();
359
}
360

    
361
#
362
# change a user's password upon request
363
#
364
sub handleChangePassword {
365

    
366
    print "Content-type: text/html\n\n";
367

    
368
    my $allParams = { 'test' => "1", };
369
    if ($query->param('uid')) {
370
        $$allParams{'uid'} = $query->param('uid');
371
    }
372
    if ($query->param('o')) {
373
        $$allParams{'o'} = $query->param('o');
374
        my $o = $query->param('o');
375
        
376
        $searchBase = $ldapConfig->{$o}{'base'};
377
    }
378

    
379

    
380
    # Check that all required fields are provided and not null
381
    my @requiredParams = ( 'uid', 'o', 'oldpass', 
382
                           'userPassword', 'userPassword2');
383
    if (! paramsAreValid(@requiredParams)) {
384
        my $errorMessage = "Required information is missing. " .
385
            "Please fill in all required fields and submit the form.";
386
        fullTemplate( ['changePass'], { stage => "changepass",
387
                                        allParams => $allParams,
388
                                        errorMessage => $errorMessage });
389
        exit();
390
    }
391

    
392
    # We have all of the info we need, so try to change the password
393
    if ($query->param('userPassword') =~ $query->param('userPassword2')) {
394

    
395
        my $o = $query->param('o');
396
        $searchBase = $ldapConfig->{$o}{'base'};
397
        $ldapUsername = $ldapConfig->{$o}{'user'};
398
        $ldapPassword = $ldapConfig->{$o}{'password'};
399

    
400
        my $dn = "uid=" . $query->param('uid') . "," . $ldapConfig->{$o}{'dn'};;
401
        if ($query->param('o') =~ "LTER") {
402
            fullTemplate( ['registerLter'] );
403
        } else {
404
            my $errorMessage = changePassword(
405
                    $dn, $query->param('userPassword'), 
406
                    $dn, $query->param('oldpass'), $query->param('o'));
407
            if ($errorMessage) {
408
                fullTemplate( ['changePass'], { stage => "changepass",
409
                                                allParams => $allParams,
410
                                                errorMessage => $errorMessage });
411
                exit();
412
            } else {
413
                fullTemplate( ['changePassSuccess'], { stage => "changepass",
414
                                                       allParams => $allParams });
415
                exit();
416
            }
417
        }
418
    } else {
419
        my $errorMessage = "The passwords do not match. Try again.";
420
        fullTemplate( ['changePass'], { stage => "changepass",
421
                                        allParams => $allParams,
422
                                        errorMessage => $errorMessage });
423
        exit();
424
    }
425
}
426

    
427
#
428
# change a user's password upon request - no input params
429
# only display chagepass template without any error
430
#
431
sub handleInitialChangePassword {
432
    print "Content-type: text/html\n\n";
433

    
434
    my $allParams = { 'test' => "1", };
435
    my $errorMessage = "";
436
    fullTemplate( ['changePass'], { stage => "changepass",
437
                                    errorMessage => $errorMessage });
438
    exit();
439
}
440

    
441
#
442
# reset a user's password upon request
443
#
444
sub handleResetPassword {
445

    
446
    print "Content-type: text/html\n\n";
447

    
448
    my $allParams = { 'test' => "1", };
449
    if ($query->param('uid')) {
450
        $$allParams{'uid'} = $query->param('uid');
451
    }
452
    if ($query->param('o')) {
453
        $$allParams{'o'} = $query->param('o');
454
        my $o = $query->param('o');
455
        
456
        $searchBase = $ldapConfig->{$o}{'base'};
457
        $ldapUsername = $ldapConfig->{$o}{'user'};
458
        $ldapPassword = $ldapConfig->{$o}{'password'};
459
    }
460

    
461
    # Check that all required fields are provided and not null
462
    my @requiredParams = ( 'uid', 'o' );
463
    if (! paramsAreValid(@requiredParams)) {
464
        my $errorMessage = "Required information is missing. " .
465
            "Please fill in all required fields and submit the form.";
466
        fullTemplate( ['resetPass'],  { stage => "resetpass",
467
                                        allParams => $allParams,
468
                                        errorMessage => $errorMessage });
469
        exit();
470
    }
471

    
472
    # We have all of the info we need, so try to change the password
473
    my $o = $query->param('o');
474
    my $dn = "uid=" . $query->param('uid') . "," . $ldapConfig->{$o}{'dn'};
475
    debug("handleResetPassword: dn: $dn");
476
    if ($query->param('o') =~ "LTER") {
477
        fullTemplate( ['registerLter'] );
478
        exit();
479
    } else {
480
        my $errorMessage = "";
481
        my $recipient;
482
        my $userPass;
483
        my $entry = getLdapEntry($ldapurl, $searchBase, 
484
                $query->param('uid'), $query->param('o'));
485

    
486
        if ($entry) {
487
            $recipient = $entry->get_value('mail');
488
            $userPass = getRandomPassword();
489
            $errorMessage = changePassword($dn, $userPass, $ldapUsername, $ldapPassword, $query->param('o'));
490
        } else {
491
            $errorMessage = "User not found in database.  Please try again.";
492
        }
493

    
494
        if ($errorMessage) {
495
            fullTemplate( ['resetPass'], { stage => "resetpass",
496
                                           allParams => $allParams,
497
                                           errorMessage => $errorMessage });
498
            exit();
499
        } else {
500
            my $errorMessage = sendPasswordNotification($query->param('uid'),
501
                    $query->param('o'), $userPass, $recipient, $cfg);
502
            fullTemplate( ['resetPassSuccess'], { stage => "resetpass",
503
                                                  allParams => $allParams,
504
                                                  errorMessage => $errorMessage });
505
            exit();
506
        }
507
    }
508
}
509

    
510
#
511
# reset a user's password upon request- no initial params
512
# only display resetpass template without any error
513
#
514
sub handleInitialResetPassword {
515
    print "Content-type: text/html\n\n";
516
    my $errorMessage = "";
517
    fullTemplate( ['resetPass'], { stage => "resetpass",
518
                                   errorMessage => $errorMessage });
519
    exit();
520
}
521

    
522
#
523
# Construct a random string to use for a newly reset password
524
#
525
sub getRandomPassword {
526
    my $length = shift;
527
    if (!$length) {
528
        $length = 8;
529
    }
530
    my $newPass = "";
531

    
532
    my @chars = ( "A" .. "Z", "a" .. "z", 0 .. 9, qw(! @ $ ^) );
533
    $newPass = join("", @chars[ map { rand @chars } ( 1 .. $length ) ]);
534
    return $newPass;
535
}
536

    
537
#
538
# Change a password to a new value, binding as the provided user
539
#
540
sub changePassword {
541
    my $userDN = shift;
542
    my $userPass = shift;
543
    my $bindDN = shift;
544
    my $bindPass = shift;
545
    my $o = shift;
546

    
547
    my $searchBase = $ldapConfig->{$o}{'base'};
548

    
549
    my $errorMessage = 0;
550
    my $ldap;
551

    
552
    #if main ldap server is down, a html file containing warning message will be returned
553
    $ldap = Net::LDAP->new($ldapurl, timeout => $timeout) or handleLDAPBindFailure($ldapurl);
554
    
555
    if ($ldap) {
556
        #$ldap->start_tls( verify => 'require',
557
                      #cafile => '/usr/share/ssl/ldapcerts/cacert.pem');
558
        $ldap->start_tls( verify => 'none');
559
        debug("changePassword: attempting to bind to $bindDN");
560
        my $bindresult = $ldap->bind( version => 3, dn => $bindDN, 
561
                                  password => $bindPass );
562
        if ($bindresult->code) {
563
            $errorMessage = "Failed to log in. Are you sure your connection credentails are " .
564
                            "correct? Please correct and try again...";
565
            return $errorMessage;
566
        }
567

    
568
    	# Find the user here and change their entry
569
    	my $newpass = createSeededPassHash($userPass);
570
    	my $modifications = { userPassword => $newpass };
571
      debug("changePass: setting password for $userDN to $newpass");
572
    	my $result = $ldap->modify( $userDN, replace => { %$modifications });
573
    
574
    	if ($result->code()) {
575
            debug("changePass: error changing password: " . $result->error);
576
        	$errorMessage = "There was an error changing the password:" .
577
                           "<br />\n" . $result->error;
578
    	} 
579
    	$ldap->unbind;   # take down session
580
    }
581

    
582
    return $errorMessage;
583
}
584

    
585
#
586
# generate a Seeded SHA1 hash of a plaintext password
587
#
588
sub createSeededPassHash {
589
    my $secret = shift;
590

    
591
    my $salt = "";
592
    for (my $i=0; $i < 4; $i++) {
593
        $salt .= int(rand(10));
594
    }
595

    
596
    my $ctx = Digest::SHA1->new;
597
    $ctx->add($secret);
598
    $ctx->add($salt);
599
    my $hashedPasswd = '{SSHA}' . encode_base64($ctx->digest . $salt ,'');
600

    
601
    return $hashedPasswd;
602
}
603

    
604
#
605
# Look up an ldap entry for a user
606
#
607
sub getLdapEntry {
608
    my $ldapurl = shift;
609
    my $base = shift;
610
    my $username = shift;
611
    my $org = shift;
612

    
613
    my $entry = "";
614
    my $mesg;
615
    my $ldap;
616
    debug("ldap server: $ldapurl");
617

    
618
    #if main ldap server is down, a html file containing warning message will be returned
619
    $ldap = Net::LDAP->new($ldapurl, timeout => $timeout) or handleLDAPBindFailure($ldapurl);
620
    
621
    if ($ldap) {
622
    	$ldap->start_tls( verify => 'none');
623
    	my $bindresult = $ldap->bind;
624
    	if ($bindresult->code) {
625
        	return $entry;
626
    	}
627

    
628
    	if($ldapConfig->{$org}{'filter'}){
629
            debug("getLdapEntry: filter set, searching for base=$base, " .
630
                  "(&(uid=$username)($ldapConfig->{$org}{'filter'})");
631
        	$mesg = $ldap->search ( base   => $base,
632
                filter => "(&(uid=$username)($ldapConfig->{$org}{'filter'}))");
633
    	} else {
634
            debug("getLdapEntry: no filter, searching for $base, (uid=$username)");
635
        	$mesg = $ldap->search ( base   => $base, filter => "(uid=$username)");
636
    	}
637
    
638
    	if ($mesg->count > 0) {
639
        	$entry = $mesg->pop_entry;
640
        	$ldap->unbind;   # take down session
641
    	} else {
642
        	$ldap->unbind;   # take down session
643
        	# Follow references by recursive call to self
644
        	my @references = $mesg->references();
645
        	for (my $i = 0; $i <= $#references; $i++) {
646
            	my $uri = URI->new($references[$i]);
647
            	my $host = $uri->host();
648
            	my $path = $uri->path();
649
            	$path =~ s/^\///;
650
            	$entry = &getLdapEntry($host, $path, $username, $org);
651
            	if ($entry) {
652
                    debug("getLdapEntry: recursion found $host, $path, $username, $org");
653
                	return $entry;
654
            	}
655
        	}
656
    	}
657
    }
658
    return $entry;
659
}
660

    
661
# 
662
# send an email message notifying the user of the pw change
663
#
664
sub sendPasswordNotification {
665
    my $username = shift;
666
    my $org = shift;
667
    my $newPass = shift;
668
    my $recipient = shift;
669
    my $cfg = shift;
670

    
671
    my $errorMessage = "";
672
    if ($recipient) {
673
        my $mailhost = $properties->getProperty('email.mailhost');
674
        my $sender =  $properties->getProperty('email.sender');
675
        # Send the email message to them
676
        my $smtp = Net::SMTP->new($mailhost);
677
        $smtp->mail($sender);
678
        $smtp->to($recipient);
679

    
680
        my $message = <<"        ENDOFMESSAGE";
681
        To: $recipient
682
        From: $sender
683
        Subject: KNB Password Reset
684
        
685
        Somebody (hopefully you) requested that your KNB password be reset.  
686
        This is generally done when somebody forgets their password.  Your 
687
        password can be changed by visiting the following URL:
688

    
689
        $contextUrl/cgi-bin/ldapweb.cgi?stage=changepass&cfg=$cfg
690

    
691
            Username: $username
692
        Organization: $org
693
        New Password: $newPass
694

    
695
        Thanks,
696
            The KNB Development Team
697
    
698
        ENDOFMESSAGE
699
        $message =~ s/^[ \t\r\f]+//gm;
700
    
701
        $smtp->data($message);
702
        $smtp->quit;
703
    } else {
704
        $errorMessage = "Failed to send password because I " .
705
                        "couldn't find a valid email address.";
706
    }
707
    return $errorMessage;
708
}
709

    
710
#
711
# search the LDAP directory to see if a similar account already exists
712
#
713
sub findExistingAccounts {
714
    my $ldapurl = shift;
715
    my $base = shift;
716
    my $filter = shift;
717
    my $attref = shift;
718
    my $ldap;
719
    my $mesg;
720

    
721
    my $foundAccounts = 0;
722

    
723
    #if main ldap server is down, a html file containing warning message will be returned
724
    debug("findExistingAccounts: connecting to $ldapurl, $timeout");
725
    $ldap = Net::LDAP->new($ldapurl, timeout => $timeout) or handleLDAPBindFailure($ldapurl);
726
    if ($ldap) {
727
    	$ldap->start_tls( verify => 'none');
728
    	$ldap->bind( version => 3, anonymous => 1);
729
		$mesg = $ldap->search (
730
			base   => $base,
731
			filter => $filter,
732
			attrs => @$attref,
733
		);
734

    
735
	    if ($mesg->count() > 0) {
736
			$foundAccounts = "";
737
			my $entry;
738
			foreach $entry ($mesg->all_entries) { 
739
				$foundAccounts .= "<p>\n<b><u>Account:</u> ";
740
				$foundAccounts .= $entry->dn();
741
				$foundAccounts .= "</b><br />\n";
742
				foreach my $attribute ($entry->attributes()) {
743
					$foundAccounts .= "$attribute: ";
744
					$foundAccounts .= $entry->get_value($attribute);
745
					$foundAccounts .= "<br />\n";
746
				}
747
				$foundAccounts .= "</p>\n";
748
			}
749
        }
750
    	$ldap->unbind;   # take down session
751

    
752
    	# Follow references
753
    	my @references = $mesg->references();
754
    	for (my $i = 0; $i <= $#references; $i++) {
755
        	my $uri = URI->new($references[$i]);
756
        	my $host = $uri->host();
757
        	my $path = $uri->path();
758
        	$path =~ s/^\///;
759
        	my $refFound = &findExistingAccounts($host, $path, $filter, $attref);
760
        	if ($refFound) {
761
            	$foundAccounts .= $refFound;
762
        	}
763
    	}
764
    }
765

    
766
    #print "<p>Checking referrals...</p>\n";
767
    #my @referrals = $mesg->referrals();
768
    #print "<p>Referrals count: ", scalar(@referrals), "</p>\n";
769
    #for (my $i = 0; $i <= $#referrals; $i++) {
770
        #print "<p>Referral: ", $referrals[$i], "</p>\n";
771
    #}
772

    
773
    return $foundAccounts;
774
}
775

    
776
#
777
# Validate that we have the proper set of input parameters
778
#
779
sub paramsAreValid {
780
    my @pnames = @_;
781

    
782
    my $allValid = 1;
783
    foreach my $parameter (@pnames) {
784
        if (!defined($query->param($parameter)) || 
785
            ! $query->param($parameter) ||
786
            $query->param($parameter) =~ /^\s+$/) {
787
            $allValid = 0;
788
        }
789
    }
790

    
791
    return $allValid;
792
}
793

    
794
#
795
# Bind to LDAP and create a new account using the information provided
796
# by the user
797
#
798
sub createAccount {
799
    my $allParams = shift;
800

    
801
    if ($query->param('o') =~ "LTER") {
802
        fullTemplate( ['registerLter'] );
803
    } else {
804

    
805
        # Be sure the passwords match
806
        if ($query->param('userPassword') !~ $query->param('userPassword2')) {
807
            my $errorMessage = "The passwords do not match. Try again.";
808
            fullTemplate( ['registerFailed', 'register'], { stage => "register",
809
                                                            allParams => $allParams,
810
                                                            errorMessage => $errorMessage });
811
            exit();
812
        }
813

    
814
        my $o = $query->param('o');
815

    
816
        my $searchBase = $ldapConfig->{$o}{'base'};
817
        my $dnBase = $ldapConfig->{$o}{'dn'};
818
        my $ldapUsername = $ldapConfig->{$o}{'user'};
819
        my $ldapPassword = $ldapConfig->{$o}{'password'};
820
        debug("LDAP connection to $ldapurl...");    
821
        #if main ldap server is down, a html file containing warning message will be returned
822
        my $ldap = Net::LDAP->new($ldapurl, timeout => $timeout) or handleLDAPBindFailure($ldapurl);
823
        
824
        if ($ldap) {
825
        	$ldap->start_tls( verify => 'none');
826
        	debug("Attempting to bind to LDAP server with dn = $ldapUsername, pwd = $ldapPassword");
827
        	$ldap->bind( version => 3, dn => $ldapUsername, password => $ldapPassword );
828
        
829
        	my $dn = 'uid=' . $query->param('uid') . ',' . $dnBase;
830
        	debug("Inserting new entry for: $dn");
831

    
832
        	# Create a hashed version of the password
833
        	my $shapass = createSeededPassHash($query->param('userPassword'));
834

    
835
        	# Do the insertion
836
        	my $additions = [ 
837
                'uid'   => $query->param('uid'),
838
                'o'   => $query->param('o'),
839
                'cn'   => join(" ", $query->param('givenName'), 
840
                                    $query->param('sn')),
841
                'sn'   => $query->param('sn'),
842
                'givenName'   => $query->param('givenName'),
843
                'mail' => $query->param('mail'),
844
                'userPassword' => $shapass,
845
                'objectclass' => ['top', 'person', 'organizationalPerson', 
846
                                'inetOrgPerson', 'uidObject' ]
847
            	];
848
        	if (defined($query->param('telephoneNumber')) && 
849
            	$query->param('telephoneNumber') &&
850
            	! $query->param('telephoneNumber') =~ /^\s+$/) {
851
            	$$additions[$#$additions + 1] = 'telephoneNumber';
852
            	$$additions[$#$additions + 1] = $query->param('telephoneNumber');
853
        	}
854
        	if (defined($query->param('title')) && 
855
            	$query->param('title') &&
856
            	! $query->param('title') =~ /^\s+$/) {
857
            	$$additions[$#$additions + 1] = 'title';
858
            	$$additions[$#$additions + 1] = $query->param('title');
859
        	}
860
        	my $result = $ldap->add ( 'dn' => $dn, 'attr' => [ @$additions ]);
861
    
862
        	if ($result->code()) {
863
            	fullTemplate( ['registerFailed', 'register'], { stage => "register",
864
                                                            allParams => $allParams,
865
                                                            errorMessage => $result->error });
866
            	# TODO SCW was included as separate errors, test this
867
           	 	#$templateVars    = setVars({ stage => "register",
868
           	 	#                     allParams => $allParams });
869
            	#$template->process( $templates->{'register'}, $templateVars);
870
        	} else {
871
            	fullTemplate( ['success'] );
872
        	}
873

    
874
        	$ldap->unbind;   # take down session
875
        }
876
    }
877
}
878

    
879
sub handleResponseMessage {
880

    
881
  print "Content-type: text/html\n\n";
882
  my $errorMessage = "You provided invalid input to the script. " .
883
                     "Try again please.";
884
  fullTemplate( [], { stage => $templates->{'stage'},
885
                      errorMessage => $errorMessage });
886
  exit();
887
}
888

    
889
#
890
# perform a simple search against the LDAP database using 
891
# a small subset of attributes of each dn and return it
892
# as a table to the calling browser.
893
#
894
sub handleSimpleSearch {
895

    
896
    my $o = $query->param('o');
897

    
898
    my $ldapurl = $ldapConfig->{$o}{'url'};
899
    my $searchBase = $ldapConfig->{$o}{'base'};
900

    
901
    print "Content-type: text/html\n\n";
902

    
903
    my $allParams = { 
904
                      'cn' => $query->param('cn'),
905
                      'sn' => $query->param('sn'),
906
                      'gn' => $query->param('gn'),
907
                      'o'  => $query->param('o'),
908
                      'facsimiletelephonenumber' 
909
                      => $query->param('facsimiletelephonenumber'),
910
                      'mail' => $query->param('cmail'),
911
                      'telephonenumber' => $query->param('telephonenumber'),
912
                      'title' => $query->param('title'),
913
                      'uid' => $query->param('uid'),
914
                      'ou' => $query->param('ou'),
915
                    };
916

    
917
    # Search LDAP for matching entries that already exist
918
    my $filter = "(" . 
919
                 $query->param('searchField') . "=" .
920
                 "*" .
921
                 $query->param('searchValue') .
922
                 "*" .
923
                 ")";
924

    
925
    my @attrs = [ 'sn', 
926
                  'gn', 
927
                  'cn', 
928
                  'o', 
929
                  'facsimiletelephonenumber', 
930
                  'mail', 
931
                  'telephoneNumber', 
932
                  'title', 
933
                  'uid', 
934
                  'labeledURI', 
935
                  'ou' ];
936

    
937
    my $found = searchDirectory($ldapurl, $searchBase, $filter, \@attrs);
938

    
939
    # Send back the search results
940
    if ($found) {
941
      fullTemplate( ('searchResults'), { stage => "searchresults",
942
                                         allParams => $allParams,
943
                                         foundAccounts => $found });
944
    } else {
945
      $found = "No entries matched your criteria.  Please try again\n";
946

    
947
      fullTemplate( ('searchResults'), { stage => "searchresults",
948
                                         allParams => $allParams,
949
                                         foundAccounts => $found });
950
    }
951

    
952
    exit();
953
}
954

    
955
#
956
# search the LDAP directory to see if a similar account already exists
957
#
958
sub searchDirectory {
959
    my $ldapurl = shift;
960
    my $base = shift;
961
    my $filter = shift;
962
    my $attref = shift;
963

    
964
	my $mesg;
965
    my $foundAccounts = 0;
966
    
967
    #if ldap server is down, a html file containing warning message will be returned
968
    my $ldap = Net::LDAP->new($ldapurl, timeout => $timeout) or handleLDAPBindFailure($ldapurl);
969
    
970
    if ($ldap) {
971
    	$ldap->start_tls( verify => 'none');
972
    	$ldap->bind( version => 3, anonymous => 1);
973
    	my $mesg = $ldap->search (
974
        	base   => $base,
975
        	filter => $filter,
976
        	attrs => @$attref,
977
    	);
978

    
979
    	if ($mesg->count() > 0) {
980
        	$foundAccounts = "";
981
        	my $entry;
982
        	foreach $entry ($mesg->sorted(['sn'])) {
983
          		$foundAccounts .= "<tr>\n<td class=\"main\">\n";
984
          		$foundAccounts .= "<a href=\"" unless 
985
                    (!$entry->get_value('labeledURI'));
986
         		 $foundAccounts .= $entry->get_value('labeledURI') unless
987
                    (!$entry->get_value('labeledURI'));
988
          		$foundAccounts .= "\">\n" unless 
989
                    (!$entry->get_value('labeledURI'));
990
          		$foundAccounts .= $entry->get_value('givenName');
991
          		$foundAccounts .= "</a>\n" unless 
992
                    (!$entry->get_value('labeledURI'));
993
          		$foundAccounts .= "\n</td>\n<td class=\"main\">\n";
994
          		$foundAccounts .= "<a href=\"" unless 
995
                    (!$entry->get_value('labeledURI'));
996
          		$foundAccounts .= $entry->get_value('labeledURI') unless
997
                    (!$entry->get_value('labeledURI'));
998
          		$foundAccounts .= "\">\n" unless 
999
                    (!$entry->get_value('labeledURI'));
1000
          		$foundAccounts .= $entry->get_value('sn');
1001
          		$foundAccounts .= "</a>\n";
1002
          		$foundAccounts .= "\n</td>\n<td class=\"main\">\n";
1003
          		$foundAccounts .= $entry->get_value('mail');
1004
          		$foundAccounts .= "\n</td>\n<td class=\"main\">\n";
1005
          		$foundAccounts .= $entry->get_value('telephonenumber');
1006
          		$foundAccounts .= "\n</td>\n<td class=\"main\">\n";
1007
          		$foundAccounts .= $entry->get_value('title');
1008
          		$foundAccounts .= "\n</td>\n<td class=\"main\">\n";
1009
          		$foundAccounts .= $entry->get_value('ou');
1010
          		$foundAccounts .= "\n</td>\n";
1011
          		$foundAccounts .= "</tr>\n";
1012
        	}
1013
    	}
1014
    	$ldap->unbind;   # take down session
1015
    }
1016
    return $foundAccounts;
1017
}
1018

    
1019
sub debug {
1020
    my $msg = shift;
1021
    
1022
    if ($debug) {
1023
        print STDERR "LDAPweb: $msg\n";
1024
    }
1025
}
1026

    
1027
sub handleLDAPBindFailure {
1028
    my $ldapAttemptUrl = shift;
1029
    my $primaryLdap =  $properties->getProperty('auth.url');
1030

    
1031
    if ($ldapAttemptUrl eq  $primaryLdap) {
1032
        handleGeneralServerFailure("The main LDAP server $ldapurl is down!");
1033
    } else {
1034
        debug("attempted to bind to nonresponsive LDAP server $ldapAttemptUrl, skipped.");
1035
    }
1036
}
1037

    
1038
sub handleGeneralServerFailure {
1039
    my $errorMessage = shift;
1040
    fullTemplate( ['mainServerFailure'], { errorMessage => $errorMessage });
1041
    exit(0);   
1042
   }
1043
    
1044
sub setVars {
1045
    my $paramVars = shift;
1046
    # initialize default parameters 
1047
    my $templateVars = { cfg => $cfg,
1048
                         styleSkinsPath => $contextUrl . "/style/skins",
1049
                         styleCommonPath => $contextUrl . "/style/common",
1050
                         contextUrl => $contextUrl,
1051
                         cgiPrefix => $cgiPrefix,
1052
                         orgList => \@orgList,
1053
                         config  => $config,
1054
    };
1055
    
1056
    # append customized params
1057
    while (my ($k, $v) = each (%$paramVars)) {
1058
        $templateVars->{$k} = $v;
1059
    }
1060
    
1061
    return $templateVars;
1062
} 
(10-10/14)