Project

General

Profile

1
#!/usr/bin/perl -w
2
#
3
#  '$RCSfile$'
4
#  Copyright: 2001 Regents of the University of California 
5
#
6
#   '$Author: tao $'
7
#     '$Date: 2013-09-05 16:58:07 -0700 (Thu, 05 Sep 2013) $'
8
# '$Revision: 8166 $' 
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 Captcha::reCAPTCHA; # for protection against spams
42
use Cwd 'abs_path';
43

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

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

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

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

    
76
my $metacatUrl = $contextUrl . "/metacat";
77
my $cgiPrefix = "/" . $properties->getProperty('application.context') . "/cgi-bin";
78
my $styleSkinsPath = $contextUrl . "/style/skins";
79
my $styleCommonPath = $contextUrl . "/style/common";
80

    
81
my @errorMessages;
82
my $error = 0;
83

    
84
# Import all of the HTML form fields as variables
85
import_names('FORM');
86

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

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

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

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

    
125
# XXX HACK: this is a temporary fix to pull out the UCNRS password property from the
126
#           NRS skin instead of metacat.properties. The intent is to prevent editing
127
#           of our core properties file, which is manipulated purely through the web.
128
#           Once organizations are editable, this section should be removed as should
129
#           the properties within nrs/nrs.properties.
130
my $nrsProperties = new Config::Properties();
131
my $nrsProps = "$skinsDir/nrs/nrs.properties";
132
unless (open (NRS_PROPERTIES, $nrsProps)) {
133
    print "Content-type: text/html\n\n";
134
    print "Unable to locate skin properties at $nrsProps.  Is this path correct?";
135
    exit(0);
136
}
137
$nrsProperties->load(*NRS_PROPERTIES);
138

    
139
my $nrsConfig = $nrsProperties->splitToTree(qr/\./, 'registry.config');
140

    
141
# XXX END HACK
142

    
143

    
144
my $searchBase;
145
my $ldapUsername;
146
my $ldapPassword;
147
# TODO: when should we use surl instead? Is there a setting promoting one over the other?
148
# TODO: the default tree for accounts should be exposed somewhere, defaulting to unaffiliated
149
my $ldapurl = $properties->getProperty('auth.url');
150

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

    
154
# Get the CGI input variables
155
my $query = new CGI;
156
my $debug = 1;
157

    
158
#--------------------------------------------------------------------------80c->
159
# Set up the Template Toolkit to read html form templates
160

    
161
# templates hash, imported from ldap.templates tree in metacat.properties
162
my $templates = $properties->splitToTree(qr/\./, 'ldap.templates');
163
$$templates{'header'} = $skinProperties->getProperty("registry.templates.header");
164
$$templates{'footer'} = $skinProperties->getProperty("registry.templates.footer");
165

    
166
# set some configuration options for the template object
167
my $ttConfig = {
168
             INCLUDE_PATH => $templatesDir,
169
             INTERPOLATE  => 0,
170
             POST_CHOMP   => 1,
171
             DEBUG        => 1, 
172
             };
173

    
174
# create an instance of the template
175
my $template = Template->new($ttConfig) || handleGeneralServerFailure($Template::ERROR);
176

    
177
# custom LDAP properties hash
178
my $ldapCustom = $properties->splitToTree(qr/\./, 'ldap');
179

    
180
my $orgProps = $properties->splitToTree(qr/\./, 'organization');
181
my $orgNames = $properties->splitToTree(qr/\./, 'organization.name');
182
# pull out properties available e.g. 'name', 'base'
183
my @orgData = keys(%$orgProps);
184

    
185
my @orgList;
186
while (my ($oKey, $oVal) = each(%$orgNames)) {
187
    push(@orgList, $oKey);
188
}
189

    
190
my $authBase = $properties->getProperty("auth.base");
191
my $ldapConfig;
192
foreach my $o (@orgList) {
193
    foreach my $d (@orgData) {
194
        $ldapConfig->{$o}{$d} = $properties->getProperty("organization.$d.$o");
195
    }
196

    
197
    # XXX hack, remove after 1.9
198
    if ($o eq 'UCNRS') {
199
        $ldapConfig->{'UCNRS'}{'base'} = $nrsConfig->{'base'};
200
        $ldapConfig->{'UCNRS'}{'user'} = $nrsConfig->{'username'};
201
        $ldapConfig->{'UCNRS'}{'password'} = $nrsConfig->{'password'};
202
    }
203

    
204
    # set default base
205
    if (!$ldapConfig->{$o}{'base'}) {
206
        $ldapConfig->{$o}{'base'} = $authBase;
207
    }
208

    
209
    # include filter information. By default, our filters are 'o=$name', e.g. 'o=NAPIER'
210
    # these can be overridden by specifying them in metacat.properties. Non-default configs
211
    # such as UCNRS must specify all LDAP properties.
212
    if ($ldapConfig->{$o}{'base'} eq $authBase) {
213
        my $filter = "o=$o";
214
        if (!$ldapConfig->{$o}{'org'}) {
215
            $ldapConfig->{$o}{'org'} = $filter;
216
        }
217
        if (!$ldapConfig->{$o}{'filter'}) {
218
            $ldapConfig->{$o}{'filter'} = $filter;
219
        }
220
        # also include DN, which is just org + base
221
        if ($ldapConfig->{$o}{'org'}) {
222
            $ldapConfig->{$o}{'dn'} = $ldapConfig->{$o}{'org'} . "," . $ldapConfig->{$o}{'base'};
223
        }
224
    } else {
225
        $ldapConfig->{$o}{'dn'} = $ldapConfig->{$o}{'base'};
226
    }
227
    
228
    # set LDAP administrator user account
229
    if (!$ldapConfig->{$o}{'user'}) {
230
        $ldapConfig->{$o}{'user'} = $ldapConfig->{'unaffiliated'}{'user'};
231
    }
232
    # check for a fully qualified LDAP name. If it doesn't exist, append base.
233
    my @userParts = split(',', $ldapConfig->{$o}{'user'});
234
    if (scalar(@userParts) == 1) {
235
        $ldapConfig->{$o}{'user'} = $ldapConfig->{$o}{'user'} . "," . $ldapConfig->{$o}{'base'};
236
    }
237

    
238
    if (!$ldapConfig->{$o}{'password'}) {
239
        $ldapConfig->{$o}{'password'} = $ldapConfig->{'unaffiliated'}{'password'};
240
    }
241
}
242

    
243
#--------------------------------------------------------------------------80c->
244
# Define the main program logic that calls subroutines to do the work
245
#--------------------------------------------------------------------------80c->
246

    
247
# The processing step we are handling
248
my $stage = $query->param('stage') || $templates->{'stage'};
249

    
250
my $cfg = $query->param('cfg');
251
debug("started with stage $stage, cfg $cfg");
252

    
253
# define the possible stages
254
my %stages = (
255
              'initregister'      => \&handleInitRegister,
256
              'register'          => \&handleRegister,
257
              'registerconfirmed' => \&handleRegisterConfirmed,
258
              'simplesearch'      => \&handleSimpleSearch,
259
              'initaddentry'      => \&handleInitAddEntry,
260
              'addentry'          => \&handleAddEntry,
261
              'initmodifyentry'   => \&handleInitModifyEntry,
262
              'modifyentry'       => \&handleModifyEntry,
263
              'changepass'        => \&handleChangePassword,
264
              'initchangepass'    => \&handleInitialChangePassword,
265
              'resetpass'         => \&handleResetPassword,
266
              'initresetpass'     => \&handleInitialResetPassword,
267
             );
268

    
269
# call the appropriate routine based on the stage
270
if ( $stages{$stage} ) {
271
  $stages{$stage}->();
272
} else {
273
  &handleResponseMessage();
274
}
275

    
276
#--------------------------------------------------------------------------80c->
277
# Define the subroutines to do the work
278
#--------------------------------------------------------------------------80c->
279

    
280
sub fullTemplate {
281
    my $templateList = shift;
282
    my $templateVars = setVars(shift);
283
    my $c = Captcha::reCAPTCHA->new;
284
    my $captcha = 'captcha';
285
    #my $error=null;
286
    my $use_ssl= 1;
287
    #my $options=null;
288
    $templateVars->{$captcha} = $c->get_html('6LcUD-cSAAAAANmwhTdCgmcieFk2IEhYGILR93gz',undef, $use_ssl, undef);
289
    $template->process( $templates->{'header'}, $templateVars );
290
    foreach my $tmpl (@{$templateList}) {
291
        $template->process( $templates->{$tmpl}, $templateVars );
292
    }
293
    $template->process( $templates->{'footer'}, $templateVars );
294
}
295

    
296
#
297
# create the initial registration form 
298
#
299
sub handleInitRegister {
300
  my $vars = shift;
301
  print "Content-type: text/html\n\n";
302
  # process the template files:
303
  fullTemplate(['register'], {stage => "register"}); 
304
  exit();
305
}
306

    
307
#
308
# process input from the register stage, which occurs when
309
# a user submits form data to create a new account
310
#
311
sub handleRegister {
312
    
313
    print "Content-type: text/html\n\n";
314
    
315
    
316
    my $allParams = { 'givenName' => $query->param('givenName'), 
317
                      'sn' => $query->param('sn'),
318
                      'o' => $query->param('o'), 
319
                      'mail' => $query->param('mail'), 
320
                      'uid' => $query->param('uid'), 
321
                      'userPassword' => $query->param('userPassword'), 
322
                      'userPassword2' => $query->param('userPassword2'), 
323
                      'title' => $query->param('title'), 
324
                      'telephoneNumber' => $query->param('telephoneNumber') };
325
    
326
    # Check the recaptcha
327
    my $c = Captcha::reCAPTCHA->new;
328
    my $challenge = $query->param('recaptcha_challenge_field');
329
    my $response = $query->param('recaptcha_response_field');
330
    # Verify submission
331
    my $result = $c->check_answer(
332
        "private google key", $ENV{'REMOTE_ADDR'},
333
        $challenge, $response
334
    );
335

    
336
    if ( $result->{is_valid} ) {
337
        #print "Yes!";
338
        #exit();
339
    }
340
    else {
341
        my $errorMessage = "The verification code is wrong. Please input again.";
342
        fullTemplate(['register'], { stage => "register",
343
                                     allParams => $allParams,
344
                                     errorMessage => $errorMessage });
345
        exit();
346
    }
347
    
348
    
349
    # Check that all required fields are provided and not null
350
    my @requiredParams = ( 'givenName', 'sn', 'o', 'mail', 
351
                           'uid', 'userPassword', 'userPassword2');
352
    if (! paramsAreValid(@requiredParams)) {
353
        my $errorMessage = "Required information is missing. " .
354
            "Please fill in all required fields and resubmit the form.";
355
        fullTemplate(['register'], { stage => "register",
356
                                     allParams => $allParams,
357
                                     errorMessage => $errorMessage });
358
        exit();
359
    } else {
360
        my $o = $query->param('o');    
361
        $searchBase = $ldapConfig->{$o}{'base'};  
362
    }
363

    
364
    # Search LDAP for matching entries that already exist
365
    # Some forms use a single text search box, whereas others search per
366
    # attribute.
367
    my $filter;
368
    if ($query->param('searchField')) {
369

    
370
      $filter = "(|" . 
371
                "(uid=" . $query->param('searchField') . ") " .
372
                "(mail=" . $query->param('searchField') . ")" .
373
                "(&(sn=" . $query->param('searchField') . ") " . 
374
                "(givenName=" . $query->param('searchField') . "))" . 
375
                ")";
376
    } else {
377
      $filter = "(|" . 
378
                "(uid=" . $query->param('uid') . ") " .
379
                "(mail=" . $query->param('mail') . ")" .
380
                "(&(sn=" . $query->param('sn') . ") " . 
381
                "(givenName=" . $query->param('givenName') . "))" . 
382
                ")";
383
    }
384

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

    
388
    # If entries match, send back a request to confirm new-user creation
389
    if ($found) {
390
      fullTemplate( ['registerMatch', 'register'], { stage => "registerconfirmed",
391
                                                     allParams => $allParams,
392
                                                     foundAccounts => $found });
393
    # Otherwise, create a new user in the LDAP directory
394
    } else {
395
        createAccount($allParams);
396
    }
397

    
398
    exit();
399
}
400

    
401
#
402
# process input from the registerconfirmed stage, which occurs when
403
# a user chooses to create an account despite similarities to other
404
# existing accounts
405
#
406
sub handleRegisterConfirmed {
407
  
408
    my $allParams = { 'givenName' => $query->param('givenName'), 
409
                      'sn' => $query->param('sn'),
410
                      'o' => 'unaffiliated', # only accept unaffiliated registration
411
                      'mail' => $query->param('mail'), 
412
                      'uid' => $query->param('uid'), 
413
                      'userPassword' => $query->param('userPassword'), 
414
                      'userPassword2' => $query->param('userPassword2'), 
415
                      'title' => $query->param('title'), 
416
                      'telephoneNumber' => $query->param('telephoneNumber') };
417
    print "Content-type: text/html\n\n";
418
    createAccount($allParams);
419
    exit();
420
}
421

    
422
#
423
# change a user's password upon request
424
#
425
sub handleChangePassword {
426

    
427
    print "Content-type: text/html\n\n";
428

    
429
    my $allParams = { 'test' => "1", };
430
    if ($query->param('uid')) {
431
        $$allParams{'uid'} = $query->param('uid');
432
    }
433
    if ($query->param('o')) {
434
        $$allParams{'o'} = $query->param('o');
435
        my $o = $query->param('o');
436
        
437
        $searchBase = $ldapConfig->{$o}{'base'};
438
    }
439

    
440

    
441
    # Check that all required fields are provided and not null
442
    my @requiredParams = ( 'uid', 'o', 'oldpass', 
443
                           'userPassword', 'userPassword2');
444
    if (! paramsAreValid(@requiredParams)) {
445
        my $errorMessage = "Required information is missing. " .
446
            "Please fill in all required fields and submit the form.";
447
        fullTemplate( ['changePass'], { stage => "changepass",
448
                                        allParams => $allParams,
449
                                        errorMessage => $errorMessage });
450
        exit();
451
    }
452

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

    
456
        my $o = $query->param('o');
457
        $searchBase = $ldapConfig->{$o}{'base'};
458
        $ldapUsername = $ldapConfig->{$o}{'user'};
459
        $ldapPassword = $ldapConfig->{$o}{'password'};
460

    
461
        my $dn = "uid=" . $query->param('uid') . "," . $ldapConfig->{$o}{'dn'};;
462
        if ($query->param('o') =~ "LTER") {
463
            fullTemplate( ['registerLter'] );
464
        } else {
465
            my $errorMessage = changePassword(
466
                    $dn, $query->param('userPassword'), 
467
                    $dn, $query->param('oldpass'), $query->param('o'));
468
            if ($errorMessage) {
469
                fullTemplate( ['changePass'], { stage => "changepass",
470
                                                allParams => $allParams,
471
                                                errorMessage => $errorMessage });
472
                exit();
473
            } else {
474
                fullTemplate( ['changePassSuccess'], { stage => "changepass",
475
                                                       allParams => $allParams });
476
                exit();
477
            }
478
        }
479
    } else {
480
        my $errorMessage = "The passwords do not match. Try again.";
481
        fullTemplate( ['changePass'], { stage => "changepass",
482
                                        allParams => $allParams,
483
                                        errorMessage => $errorMessage });
484
        exit();
485
    }
486
}
487

    
488
#
489
# change a user's password upon request - no input params
490
# only display chagepass template without any error
491
#
492
sub handleInitialChangePassword {
493
    print "Content-type: text/html\n\n";
494

    
495
    my $allParams = { 'test' => "1", };
496
    my $errorMessage = "";
497
    fullTemplate( ['changePass'], { stage => "changepass",
498
                                    errorMessage => $errorMessage });
499
    exit();
500
}
501

    
502
#
503
# reset a user's password upon request
504
#
505
sub handleResetPassword {
506

    
507
    print "Content-type: text/html\n\n";
508

    
509
    my $allParams = { 'test' => "1", };
510
    if ($query->param('uid')) {
511
        $$allParams{'uid'} = $query->param('uid');
512
    }
513
    if ($query->param('o')) {
514
        $$allParams{'o'} = $query->param('o');
515
        my $o = $query->param('o');
516
        
517
        $searchBase = $ldapConfig->{$o}{'base'};
518
        $ldapUsername = $ldapConfig->{$o}{'user'};
519
        $ldapPassword = $ldapConfig->{$o}{'password'};
520
    }
521

    
522
    # Check that all required fields are provided and not null
523
    my @requiredParams = ( 'uid', 'o' );
524
    if (! paramsAreValid(@requiredParams)) {
525
        my $errorMessage = "Required information is missing. " .
526
            "Please fill in all required fields and submit the form.";
527
        fullTemplate( ['resetPass'],  { stage => "resetpass",
528
                                        allParams => $allParams,
529
                                        errorMessage => $errorMessage });
530
        exit();
531
    }
532

    
533
    # We have all of the info we need, so try to change the password
534
    my $o = $query->param('o');
535
    my $dn = "uid=" . $query->param('uid') . "," . $ldapConfig->{$o}{'dn'};
536
    debug("handleResetPassword: dn: $dn");
537
    if ($query->param('o') =~ "LTER") {
538
        fullTemplate( ['registerLter'] );
539
        exit();
540
    } else {
541
        my $errorMessage = "";
542
        my $recipient;
543
        my $userPass;
544
        my $entry = getLdapEntry($ldapurl, $searchBase, 
545
                $query->param('uid'), $query->param('o'));
546

    
547
        if ($entry) {
548
            $recipient = $entry->get_value('mail');
549
            $userPass = getRandomPassword();
550
            $errorMessage = changePassword($dn, $userPass, $ldapUsername, $ldapPassword, $query->param('o'));
551
        } else {
552
            $errorMessage = "User not found in database.  Please try again.";
553
        }
554

    
555
        if ($errorMessage) {
556
            fullTemplate( ['resetPass'], { stage => "resetpass",
557
                                           allParams => $allParams,
558
                                           errorMessage => $errorMessage });
559
            exit();
560
        } else {
561
            my $errorMessage = sendPasswordNotification($query->param('uid'),
562
                    $query->param('o'), $userPass, $recipient, $cfg);
563
            fullTemplate( ['resetPassSuccess'], { stage => "resetpass",
564
                                                  allParams => $allParams,
565
                                                  errorMessage => $errorMessage });
566
            exit();
567
        }
568
    }
569
}
570

    
571
#
572
# reset a user's password upon request- no initial params
573
# only display resetpass template without any error
574
#
575
sub handleInitialResetPassword {
576
    print "Content-type: text/html\n\n";
577
    my $errorMessage = "";
578
    fullTemplate( ['resetPass'], { stage => "resetpass",
579
                                   errorMessage => $errorMessage });
580
    exit();
581
}
582

    
583
#
584
# Construct a random string to use for a newly reset password
585
#
586
sub getRandomPassword {
587
    my $length = shift;
588
    if (!$length) {
589
        $length = 8;
590
    }
591
    my $newPass = "";
592

    
593
    my @chars = ( "A" .. "Z", "a" .. "z", 0 .. 9, qw(! @ $ ^) );
594
    $newPass = join("", @chars[ map { rand @chars } ( 1 .. $length ) ]);
595
    return $newPass;
596
}
597

    
598
#
599
# Change a password to a new value, binding as the provided user
600
#
601
sub changePassword {
602
    my $userDN = shift;
603
    my $userPass = shift;
604
    my $bindDN = shift;
605
    my $bindPass = shift;
606
    my $o = shift;
607

    
608
    my $searchBase = $ldapConfig->{$o}{'base'};
609

    
610
    my $errorMessage = 0;
611
    my $ldap;
612

    
613
    #if main ldap server is down, a html file containing warning message will be returned
614
    $ldap = Net::LDAP->new($ldapurl, timeout => $timeout) or handleLDAPBindFailure($ldapurl);
615
    
616
    if ($ldap) {
617
        #$ldap->start_tls( verify => 'require',
618
                      #cafile => '/usr/share/ssl/ldapcerts/cacert.pem');
619
        $ldap->start_tls( verify => 'none');
620
        debug("changePassword: attempting to bind to $bindDN");
621
        my $bindresult = $ldap->bind( version => 3, dn => $bindDN, 
622
                                  password => $bindPass );
623
        if ($bindresult->code) {
624
            $errorMessage = "Failed to log in. Are you sure your connection credentails are " .
625
                            "correct? Please correct and try again...";
626
            return $errorMessage;
627
        }
628

    
629
    	# Find the user here and change their entry
630
    	my $newpass = createSeededPassHash($userPass);
631
    	my $modifications = { userPassword => $newpass };
632
      debug("changePass: setting password for $userDN to $newpass");
633
    	my $result = $ldap->modify( $userDN, replace => { %$modifications });
634
    
635
    	if ($result->code()) {
636
            debug("changePass: error changing password: " . $result->error);
637
        	$errorMessage = "There was an error changing the password:" .
638
                           "<br />\n" . $result->error;
639
    	} 
640
    	$ldap->unbind;   # take down session
641
    }
642

    
643
    return $errorMessage;
644
}
645

    
646
#
647
# generate a Seeded SHA1 hash of a plaintext password
648
#
649
sub createSeededPassHash {
650
    my $secret = shift;
651

    
652
    my $salt = "";
653
    for (my $i=0; $i < 4; $i++) {
654
        $salt .= int(rand(10));
655
    }
656

    
657
    my $ctx = Digest::SHA1->new;
658
    $ctx->add($secret);
659
    $ctx->add($salt);
660
    my $hashedPasswd = '{SSHA}' . encode_base64($ctx->digest . $salt ,'');
661

    
662
    return $hashedPasswd;
663
}
664

    
665
#
666
# Look up an ldap entry for a user
667
#
668
sub getLdapEntry {
669
    my $ldapurl = shift;
670
    my $base = shift;
671
    my $username = shift;
672
    my $org = shift;
673

    
674
    my $entry = "";
675
    my $mesg;
676
    my $ldap;
677
    debug("ldap server: $ldapurl");
678

    
679
    #if main ldap server is down, a html file containing warning message will be returned
680
    $ldap = Net::LDAP->new($ldapurl, timeout => $timeout) or handleLDAPBindFailure($ldapurl);
681
    
682
    if ($ldap) {
683
    	$ldap->start_tls( verify => 'none');
684
    	my $bindresult = $ldap->bind;
685
    	if ($bindresult->code) {
686
        	return $entry;
687
    	}
688

    
689
    	if($ldapConfig->{$org}{'filter'}){
690
            debug("getLdapEntry: filter set, searching for base=$base, " .
691
                  "(&(uid=$username)($ldapConfig->{$org}{'filter'})");
692
        	$mesg = $ldap->search ( base   => $base,
693
                filter => "(&(uid=$username)($ldapConfig->{$org}{'filter'}))");
694
    	} else {
695
            debug("getLdapEntry: no filter, searching for $base, (uid=$username)");
696
        	$mesg = $ldap->search ( base   => $base, filter => "(uid=$username)");
697
    	}
698
    
699
    	if ($mesg->count > 0) {
700
        	$entry = $mesg->pop_entry;
701
        	$ldap->unbind;   # take down session
702
    	} else {
703
        	$ldap->unbind;   # take down session
704
        	# Follow references by recursive call to self
705
        	my @references = $mesg->references();
706
        	for (my $i = 0; $i <= $#references; $i++) {
707
            	my $uri = URI->new($references[$i]);
708
            	my $host = $uri->host();
709
            	my $path = $uri->path();
710
            	$path =~ s/^\///;
711
            	$entry = &getLdapEntry($host, $path, $username, $org);
712
            	if ($entry) {
713
                    debug("getLdapEntry: recursion found $host, $path, $username, $org");
714
                	return $entry;
715
            	}
716
        	}
717
    	}
718
    }
719
    return $entry;
720
}
721

    
722
# 
723
# send an email message notifying the user of the pw change
724
#
725
sub sendPasswordNotification {
726
    my $username = shift;
727
    my $org = shift;
728
    my $newPass = shift;
729
    my $recipient = shift;
730
    my $cfg = shift;
731

    
732
    my $errorMessage = "";
733
    if ($recipient) {
734
        my $mailhost = $properties->getProperty('email.mailhost');
735
        my $sender =  $properties->getProperty('email.sender');
736
        # Send the email message to them
737
        my $smtp = Net::SMTP->new($mailhost);
738
        $smtp->mail($sender);
739
        $smtp->to($recipient);
740

    
741
        my $message = <<"        ENDOFMESSAGE";
742
        To: $recipient
743
        From: $sender
744
        Subject: KNB Password Reset
745
        
746
        Somebody (hopefully you) requested that your KNB password be reset.  
747
        This is generally done when somebody forgets their password.  Your 
748
        password can be changed by visiting the following URL:
749

    
750
        $contextUrl/cgi-bin/ldapweb.cgi?stage=changepass&cfg=$cfg
751

    
752
            Username: $username
753
        Organization: $org
754
        New Password: $newPass
755

    
756
        Thanks,
757
            The KNB Development Team
758
    
759
        ENDOFMESSAGE
760
        $message =~ s/^[ \t\r\f]+//gm;
761
    
762
        $smtp->data($message);
763
        $smtp->quit;
764
    } else {
765
        $errorMessage = "Failed to send password because I " .
766
                        "couldn't find a valid email address.";
767
    }
768
    return $errorMessage;
769
}
770

    
771
#
772
# search the LDAP directory to see if a similar account already exists
773
#
774
sub findExistingAccounts {
775
    my $ldapurl = shift;
776
    my $base = shift;
777
    my $filter = shift;
778
    my $attref = shift;
779
    my $ldap;
780
    my $mesg;
781

    
782
    my $foundAccounts = 0;
783

    
784
    #if main ldap server is down, a html file containing warning message will be returned
785
    debug("findExistingAccounts: connecting to $ldapurl, $timeout");
786
    $ldap = Net::LDAP->new($ldapurl, timeout => $timeout) or handleLDAPBindFailure($ldapurl);
787
    if ($ldap) {
788
    	$ldap->start_tls( verify => 'none');
789
    	$ldap->bind( version => 3, anonymous => 1);
790
		$mesg = $ldap->search (
791
			base   => $base,
792
			filter => $filter,
793
			attrs => @$attref,
794
		);
795

    
796
	    if ($mesg->count() > 0) {
797
			$foundAccounts = "";
798
			my $entry;
799
			foreach $entry ($mesg->all_entries) { 
800
                # a fix to ignore 'ou=Account' properties which are not usable accounts within Metacat.
801
                # this could be done directly with filters on the LDAP connection, instead.
802
                if ($entry->dn !~ /ou=Account/) {
803
                    $foundAccounts .= "<p>\n<b><u>Account:</u> ";
804
                    $foundAccounts .= $entry->dn();
805
                    $foundAccounts .= "</b><br />\n";
806
                    foreach my $attribute ($entry->attributes()) {
807
                        my $value = $entry->get_value($attribute);
808
                        $foundAccounts .= "$attribute: ";
809
                        $foundAccounts .= $value;
810
                        $foundAccounts .= "<br />\n";
811
                    }
812
                    $foundAccounts .= "</p>\n";
813
                }
814
			}
815
        }
816
    	$ldap->unbind;   # take down session
817

    
818
    	# Follow references
819
    	my @references = $mesg->references();
820
    	for (my $i = 0; $i <= $#references; $i++) {
821
        	my $uri = URI->new($references[$i]);
822
        	my $host = $uri->host();
823
        	my $path = $uri->path();
824
        	$path =~ s/^\///;
825
        	my $refFound = &findExistingAccounts($host, $path, $filter, $attref);
826
        	if ($refFound) {
827
            	$foundAccounts .= $refFound;
828
        	}
829
    	}
830
    }
831

    
832
    #print "<p>Checking referrals...</p>\n";
833
    #my @referrals = $mesg->referrals();
834
    #print "<p>Referrals count: ", scalar(@referrals), "</p>\n";
835
    #for (my $i = 0; $i <= $#referrals; $i++) {
836
        #print "<p>Referral: ", $referrals[$i], "</p>\n";
837
    #}
838

    
839
    return $foundAccounts;
840
}
841

    
842
#
843
# Validate that we have the proper set of input parameters
844
#
845
sub paramsAreValid {
846
    my @pnames = @_;
847

    
848
    my $allValid = 1;
849
    foreach my $parameter (@pnames) {
850
        if (!defined($query->param($parameter)) || 
851
            ! $query->param($parameter) ||
852
            $query->param($parameter) =~ /^\s+$/) {
853
            $allValid = 0;
854
        }
855
    }
856

    
857
    return $allValid;
858
}
859

    
860
#
861
# Bind to LDAP and create a new account using the information provided
862
# by the user
863
#
864
sub createAccount {
865
    my $allParams = shift;
866

    
867
    if ($query->param('o') =~ "LTER") {
868
        fullTemplate( ['registerLter'] );
869
    } else {
870

    
871
        # Be sure the passwords match
872
        if ($query->param('userPassword') !~ $query->param('userPassword2')) {
873
            my $errorMessage = "The passwords do not match. Try again.";
874
            fullTemplate( ['registerFailed', 'register'], { stage => "register",
875
                                                            allParams => $allParams,
876
                                                            errorMessage => $errorMessage });
877
            exit();
878
        }
879

    
880
        my $o = $query->param('o');
881

    
882
        my $searchBase = $ldapConfig->{$o}{'base'};
883
        my $dnBase = $ldapConfig->{$o}{'dn'};
884
        my $ldapUsername = $ldapConfig->{$o}{'user'};
885
        my $ldapPassword = $ldapConfig->{$o}{'password'};
886
        debug("LDAP connection to $ldapurl...");    
887
        #if main ldap server is down, a html file containing warning message will be returned
888
        my $ldap = Net::LDAP->new($ldapurl, timeout => $timeout) or handleLDAPBindFailure($ldapurl);
889
        
890
        if ($ldap) {
891
        	$ldap->start_tls( verify => 'none');
892
        	debug("Attempting to bind to LDAP server with dn = $ldapUsername, pwd = $ldapPassword");
893
        	$ldap->bind( version => 3, dn => $ldapUsername, password => $ldapPassword );
894
        
895
        	my $dn = 'uid=' . $query->param('uid') . ',' . $dnBase;
896
        	debug("Inserting new entry for: $dn");
897

    
898
        	# Create a hashed version of the password
899
        	my $shapass = createSeededPassHash($query->param('userPassword'));
900

    
901
        	# Do the insertion
902
        	my $additions = [ 
903
                'uid'   => $query->param('uid'),
904
                'o'   => $query->param('o'),
905
                'cn'   => join(" ", $query->param('givenName'), 
906
                                    $query->param('sn')),
907
                'sn'   => $query->param('sn'),
908
                'givenName'   => $query->param('givenName'),
909
                'mail' => $query->param('mail'),
910
                'userPassword' => $shapass,
911
                'objectclass' => ['top', 'person', 'organizationalPerson', 
912
                                'inetOrgPerson', 'uidObject' ]
913
            	];
914
        	if (defined($query->param('telephoneNumber')) && 
915
            	$query->param('telephoneNumber') &&
916
            	! $query->param('telephoneNumber') =~ /^\s+$/) {
917
            	$$additions[$#$additions + 1] = 'telephoneNumber';
918
            	$$additions[$#$additions + 1] = $query->param('telephoneNumber');
919
        	}
920
        	if (defined($query->param('title')) && 
921
            	$query->param('title') &&
922
            	! $query->param('title') =~ /^\s+$/) {
923
            	$$additions[$#$additions + 1] = 'title';
924
            	$$additions[$#$additions + 1] = $query->param('title');
925
        	}
926
        	my $result = $ldap->add ( 'dn' => $dn, 'attr' => [ @$additions ]);
927
    
928
        	if ($result->code()) {
929
            	fullTemplate( ['registerFailed', 'register'], { stage => "register",
930
                                                            allParams => $allParams,
931
                                                            errorMessage => $result->error });
932
            	# TODO SCW was included as separate errors, test this
933
           	 	#$templateVars    = setVars({ stage => "register",
934
           	 	#                     allParams => $allParams });
935
            	#$template->process( $templates->{'register'}, $templateVars);
936
        	} else {
937
            	fullTemplate( ['success'] );
938
        	}
939

    
940
        	$ldap->unbind;   # take down session
941
        }
942
    }
943
}
944

    
945
sub handleResponseMessage {
946

    
947
  print "Content-type: text/html\n\n";
948
  my $errorMessage = "You provided invalid input to the script. " .
949
                     "Try again please.";
950
  fullTemplate( [], { stage => $templates->{'stage'},
951
                      errorMessage => $errorMessage });
952
  exit();
953
}
954

    
955
#
956
# perform a simple search against the LDAP database using 
957
# a small subset of attributes of each dn and return it
958
# as a table to the calling browser.
959
#
960
sub handleSimpleSearch {
961

    
962
    my $o = $query->param('o');
963

    
964
    my $ldapurl = $ldapConfig->{$o}{'url'};
965
    my $searchBase = $ldapConfig->{$o}{'base'};
966

    
967
    print "Content-type: text/html\n\n";
968

    
969
    my $allParams = { 
970
                      'cn' => $query->param('cn'),
971
                      'sn' => $query->param('sn'),
972
                      'gn' => $query->param('gn'),
973
                      'o'  => $query->param('o'),
974
                      'facsimiletelephonenumber' 
975
                      => $query->param('facsimiletelephonenumber'),
976
                      'mail' => $query->param('cmail'),
977
                      'telephonenumber' => $query->param('telephonenumber'),
978
                      'title' => $query->param('title'),
979
                      'uid' => $query->param('uid'),
980
                      'ou' => $query->param('ou'),
981
                    };
982

    
983
    # Search LDAP for matching entries that already exist
984
    my $filter = "(" . 
985
                 $query->param('searchField') . "=" .
986
                 "*" .
987
                 $query->param('searchValue') .
988
                 "*" .
989
                 ")";
990

    
991
    my @attrs = [ 'sn', 
992
                  'gn', 
993
                  'cn', 
994
                  'o', 
995
                  'facsimiletelephonenumber', 
996
                  'mail', 
997
                  'telephoneNumber', 
998
                  'title', 
999
                  'uid', 
1000
                  'labeledURI', 
1001
                  'ou' ];
1002

    
1003
    my $found = searchDirectory($ldapurl, $searchBase, $filter, \@attrs);
1004

    
1005
    # Send back the search results
1006
    if ($found) {
1007
      fullTemplate( ('searchResults'), { stage => "searchresults",
1008
                                         allParams => $allParams,
1009
                                         foundAccounts => $found });
1010
    } else {
1011
      $found = "No entries matched your criteria.  Please try again\n";
1012

    
1013
      fullTemplate( ('searchResults'), { stage => "searchresults",
1014
                                         allParams => $allParams,
1015
                                         foundAccounts => $found });
1016
    }
1017

    
1018
    exit();
1019
}
1020

    
1021
#
1022
# search the LDAP directory to see if a similar account already exists
1023
#
1024
sub searchDirectory {
1025
    my $ldapurl = shift;
1026
    my $base = shift;
1027
    my $filter = shift;
1028
    my $attref = shift;
1029

    
1030
	my $mesg;
1031
    my $foundAccounts = 0;
1032
    
1033
    #if ldap server is down, a html file containing warning message will be returned
1034
    my $ldap = Net::LDAP->new($ldapurl, timeout => $timeout) or handleLDAPBindFailure($ldapurl);
1035
    
1036
    if ($ldap) {
1037
    	$ldap->start_tls( verify => 'none');
1038
    	$ldap->bind( version => 3, anonymous => 1);
1039
    	my $mesg = $ldap->search (
1040
        	base   => $base,
1041
        	filter => $filter,
1042
        	attrs => @$attref,
1043
    	);
1044

    
1045
    	if ($mesg->count() > 0) {
1046
        	$foundAccounts = "";
1047
        	my $entry;
1048
        	foreach $entry ($mesg->sorted(['sn'])) {
1049
          		$foundAccounts .= "<tr>\n<td class=\"main\">\n";
1050
          		$foundAccounts .= "<a href=\"" unless 
1051
                    (!$entry->get_value('labeledURI'));
1052
         		 $foundAccounts .= $entry->get_value('labeledURI') unless
1053
                    (!$entry->get_value('labeledURI'));
1054
          		$foundAccounts .= "\">\n" unless 
1055
                    (!$entry->get_value('labeledURI'));
1056
          		$foundAccounts .= $entry->get_value('givenName');
1057
          		$foundAccounts .= "</a>\n" unless 
1058
                    (!$entry->get_value('labeledURI'));
1059
          		$foundAccounts .= "\n</td>\n<td class=\"main\">\n";
1060
          		$foundAccounts .= "<a href=\"" unless 
1061
                    (!$entry->get_value('labeledURI'));
1062
          		$foundAccounts .= $entry->get_value('labeledURI') unless
1063
                    (!$entry->get_value('labeledURI'));
1064
          		$foundAccounts .= "\">\n" unless 
1065
                    (!$entry->get_value('labeledURI'));
1066
          		$foundAccounts .= $entry->get_value('sn');
1067
          		$foundAccounts .= "</a>\n";
1068
          		$foundAccounts .= "\n</td>\n<td class=\"main\">\n";
1069
          		$foundAccounts .= $entry->get_value('mail');
1070
          		$foundAccounts .= "\n</td>\n<td class=\"main\">\n";
1071
          		$foundAccounts .= $entry->get_value('telephonenumber');
1072
          		$foundAccounts .= "\n</td>\n<td class=\"main\">\n";
1073
          		$foundAccounts .= $entry->get_value('title');
1074
          		$foundAccounts .= "\n</td>\n<td class=\"main\">\n";
1075
          		$foundAccounts .= $entry->get_value('ou');
1076
          		$foundAccounts .= "\n</td>\n";
1077
          		$foundAccounts .= "</tr>\n";
1078
        	}
1079
    	}
1080
    	$ldap->unbind;   # take down session
1081
    }
1082
    return $foundAccounts;
1083
}
1084

    
1085
sub debug {
1086
    my $msg = shift;
1087
    
1088
    if ($debug) {
1089
        print STDERR "LDAPweb: $msg\n";
1090
    }
1091
}
1092

    
1093
sub handleLDAPBindFailure {
1094
    my $ldapAttemptUrl = shift;
1095
    my $primaryLdap =  $properties->getProperty('auth.url');
1096

    
1097
    if ($ldapAttemptUrl eq  $primaryLdap) {
1098
        handleGeneralServerFailure("The main LDAP server $ldapurl is down!");
1099
    } else {
1100
        debug("attempted to bind to nonresponsive LDAP server $ldapAttemptUrl, skipped.");
1101
    }
1102
}
1103

    
1104
sub handleGeneralServerFailure {
1105
    my $errorMessage = shift;
1106
    fullTemplate( ['mainServerFailure'], { errorMessage => $errorMessage });
1107
    exit(0);   
1108
   }
1109
    
1110
sub setVars {
1111
    my $paramVars = shift;
1112
    # initialize default parameters 
1113
    my $templateVars = { cfg => $cfg,
1114
                         styleSkinsPath => $contextUrl . "/style/skins",
1115
                         styleCommonPath => $contextUrl . "/style/common",
1116
                         contextUrl => $contextUrl,
1117
                         cgiPrefix => $cgiPrefix,
1118
                         orgList => \@orgList,
1119
                         config  => $config,
1120
    };
1121
    
1122
    # append customized params
1123
    while (my ($k, $v) = each (%$paramVars)) {
1124
        $templateVars->{$k} = $v;
1125
    }
1126
    
1127
    return $templateVars;
1128
} 
(10-10/14)