fusiondirectory-setup 66.1 KB
Newer Older
1
#!/usr/bin/perl
2

3
4
5
6
7
8
9
########################################################################
#
#  fusiondirectory-setup
#
#  Manage fusiondirectory installs from the command line
#
#  This code is part of FusionDirectory (http://www.fusiondirectory.org/)
10
#  Copyright (C) 2011-2017  FusionDirectory
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#
#  This program is free software; you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation; either version 2 of the License, or
#  (at your option) any later version.
#
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program; if not, write to the Free Software
#  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.
#
########################################################################

28
29
use strict;
use warnings;
30
use 5.008;
31

32
33
# used to manage files
use Path::Class;
34

35
36
# used for checking config dirs rights (make the translation for lstat output)
use Fcntl ':mode';
37

38
39
40
41
42
43
# used to handle ldap connections
use Net::LDAP;

# used to base64 encode
use MIME::Base64;

44
# used to generate {SSHA} password (for LDAP)
45
use Digest::SHA;
46
use Crypt::CBC;
47

48
# used to uncompress tar.gz
49
50
use Archive::Extract;

51
# used to copy files
52
use File::Copy::Recursive qw(rcopy);
53

54
55
56
#XML parser
use XML::Twig;

57
58
59
# To hide password input
use Term::ReadKey;

60
61
use Data::Dumper;

62
# fd's directory and class.cache file's path declaration
63
my %vars = (
64
65
66
 fd_home          => "/var/www/fusiondirectory",
 fd_cache         => "/var/cache/fusiondirectory",
 fd_config_dir    => "/etc/fusiondirectory",
67
 fd_smarty_dir    => "/usr/share/php/smarty3",
68
 fd_spool_dir     => "/var/spool/fusiondirectory",
69
 ldap_conf        => "/etc/ldap/ldap.conf",
70
71
72
73
 config_file      => "fusiondirectory.conf",
 secrets_file     => "fusiondirectory.secrets",
 locale_dir       => "locale",
 class_cache      => "class.cache",
74
 locale_cache_dir => "locale",
75
76
77
 tmp_dir          => "tmp",
 fai_log_dir      => "fai",
 template_dir     => "template"
78
79
);

80
my ($fd_config,$fd_secrets,$locale_dir,$class_cache,$locale_cache_dir,$tmp_dir,$fai_log_dir,$template_dir);
81
82

my (@root_config_dirs,@apache_config_dirs,@config_dirs);
83

84
my @plugin_types = qw(addons admin personal);
85
86
my $yes_flag = 0;

87
88
89
my %classes_hash_result = ();
my %i18n_hash_result = ();

90
91
92
93
my $configrdn   = "cn=config,ou=fusiondirectory";
my $userrdn     = "ou=people";
my $aclrolerdn  = "ou=aclroles";
my $grouprdn    = "ou=groups";
94
95
my $systemrdn   = "ou=systems";
my $dnsrdn      = "ou=dns";
96
my $dhcprdn     = "ou=dhcp";
97
my $workstationrdn  = "ou=workstations,ou=systems";
98

99
#################################################################################################################################################
100

101
102
# ask a question send as parameter, and return true if the answer is "yes"
sub ask_yn_question {
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
  return 1 if ($yes_flag);
  my ($question) = @_;
  print ( "$question [Yes/No]?\n" );

  while ( my $input = <STDIN> ) {
    # remove the \n at the end of $input
    chomp $input;

    # if user's answer is "yes"
    if ( lc($input) eq "yes" || lc($input) eq "y") {
      return 1;
    # else if he answer "no"
    } elsif ( lc($input) eq "no" || lc($input) eq "n") {
      return 0;
    }
  }
}

# function that ask for an user input and do some checks
sub ask_user_input {
123
 my ($thing_to_ask, $default_answer, $hide_input) = @_;
124
 my $answer;
125

126
127
128
129
 if (defined $default_answer) {
   $thing_to_ask .= " [$default_answer]";
 }
 print $thing_to_ask.":\n";
130

131
132
133
134
 if (defined $hide_input && $hide_input) {
   ReadMode('noecho');
 }

135
 do
136
 {
137
138
139
140
141
142
   if ($answer = <STDIN>) {
     chomp $answer;
     $answer =~ s/^\s+|\s+$//g;
   } else {
     $answer = "";
   }
143
 } while (($answer eq "") && (not defined $default_answer));
144

145
146
 ReadMode('restore');

147
148
149
 if ($answer eq "") {
   return $default_answer;
 }
150
 return $answer;
151
}
152

153
# Die on all LDAP error except for «No such object»
154
155
sub die_on_ldap_errors
{
156
  my ($mesg) = @_;
157
158
159
160
161
  if (($mesg->code != 0) && ($mesg->code != 32)) {
    die $mesg->error;
  }
}

162
163
164
165
166
167
{
  my $indice = 0;
  sub find_free_role_dn {
    my ($ldap,$base,$prefix) = @_;
    my ($cn,$dn,$mesg);
    do {
168
      $cn = $prefix.'-'.$indice;
169
      $dn = "cn=$cn,$aclrolerdn,$base";
170
171
172
173
174
175
      $indice++;
      $mesg = $ldap->search(
        base    => "$dn",
        scope   => 'base',
        filter  => '(objectClass=*)'
      );
176
      die_on_ldap_errors($mesg);
177
178
179
180
181
182
183
184
185
186
187
188
    } while ($mesg->count);
    return $cn;
  }
}

sub create_role {
  my ($ldap,$base,$cn,$acl) = @_;
  my %role = (
    'cn'              => "$cn",
    'objectclass'     => [ 'top', 'gosaRole' ],
    'gosaAclTemplate' => "0:$acl"
  );
189

190
191
  if (!branch_exists($ldap, "$aclrolerdn,$base")) {
    create_branch($ldap, $base, $aclrolerdn);
192
  }
193

194
  my $role_dn = "cn=$cn,$aclrolerdn,$base";
195
196
197
198
199
200
201
202
  # Add the administator role object
  my @options = %role;
  my $role_add = $ldap->add( $role_dn, attr => \@options );
  # send a warning if the ldap's admin's add didn't gone well
  $role_add->code && die "\n! failed to add LDAP's $role_dn entry - ".$role_add->error_name.": ".$role_add->error_text;
  return $role_dn;
}

203
204
205
206
207
208
209
210
###################################################### Password encryption #########################################################################

sub cred_encrypt {
  my ($input, $password) = @_;
  my $cipher = Crypt::CBC->new(
                -key    =>  $password,
                -cipher => 'Rijndael',
                -salt   => 1,
211
                -header => 'salt',
212
213
214
215
              ) || die "Couldn't create CBC object";
  return $cipher->encrypt_hex($input);
}

216
217
218
219
220
221
222
223
224
225
226
sub cred_decrypt {
  my ($input, $password) = @_;
  my $cipher = Crypt::CBC->new(
                -key    =>  $password,
                -cipher => 'Rijndael',
                -salt   => 1,
                -header => 'salt',
              ) || die "Couldn't create CBC object";
  return $cipher->decrypt_hex($input);
}

227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
sub get_random_string {
  my ($size) = @_;
  $size = 32 if !$size;
  my @chars = ("A".."Z", "a".."z", '.', '/', 0..9);
  my $string;
  $string .= $chars[rand @chars] for 1..$size;
  return $string;
}

sub encrypt_passwords {
  if (!-e $fd_config) {
    die "Cannot find a valid configuration file ($fd_config)!\n";
  }
  if (-e $fd_secrets) {
    die "There's already a file '$fd_secrets'. Cannot convert your existing fusiondirectory.conf - aborted\n";
  }
  print "Starting password encryption\n";
  print "* generating random master key\n";
  my $master_key = get_random_string();
  print "* creating '$fd_secrets'\n";
  my $fp_file = file($fd_secrets);
  my $fp = $fp_file->openw() or die "! Unable to open '$fd_secrets' in write mode\n";
249
  $fp->print("RequestHeader set FDKEY $master_key\n");
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
  $fp->close or die "! Can't close '$fd_secrets'\n";
  chmod 0600, $fd_secrets or die "! Unable to change '$fd_secrets' rights\n";
  my $root_uid = getpwnam("root");
  my $root_gid = getgrnam("root");
  chown $root_uid,$root_gid,$fd_secrets or die "! Unable to change '$fd_secrets' owner\n";

  # Move original fusiondirectory.conf out of the way and make it unreadable for the web user
  print "* creating backup in '$fd_config.orig'\n";
  rcopy($fd_config, "$fd_config.orig");
  chmod 0600, "$fd_config.orig" or die "! Unable to change '$fd_config.orig' rights\n";
  chown $root_uid,$root_gid,"$fd_config.orig" or die "! Unable to change '$fd_config.orig' owner\n";

  print "* loading '$fd_config'\n";
  my $twig = XML::Twig->new();    # create the twig
  $twig->parsefile($fd_config); # build it
  # Locate all passwords inside the fusiondirectory.conf
  my @locs = $twig->root->first_child('main')->children('location');
  foreach my $loc (@locs) {
    my $ref = $loc->first_child('referral');
    print "* encrypting FusionDirectory password for: ".$ref->{'att'}->{'adminDn'}."\n";
    $ref->set_att('adminPassword' => cred_encrypt($ref->{'att'}->{'adminPassword'}, $master_key));
  }

  # Save
  print "* saving modified '$fd_config'\n";
  $twig->print_to_file($fd_config, pretty_print => 'indented') or die "Cannot write modified $fd_config - aborted\n";
  print "OK\n\n";

  # Print reminder
  print "
Please adapt your http fusiondirectory location declaration to include the newly
281
created $fd_secrets.
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302

Example:

Alias /fusiondirectory /usr/share/fusiondirectory/html

<Location /fusiondirectory>
  php_admin_flag engine on
  php_admin_flag register_globals off
  php_admin_flag allow_call_time_pass_reference off
  php_admin_flag expose_php off
  php_admin_flag zend.ze1_compatibility_mode off
  php_admin_flag register_long_arrays off
  php_admin_value upload_tmp_dir /var/spool/fusiondirectory/
  php_admin_value session.cookie_lifetime 0
  include /etc/fusiondirectory/fusiondirectory.secrets
</Location>


Please reload your httpd configuration after you've modified anything.\n";
}

303
####################################################### class.cache update #########################################################################
304
305
306
307
308

# function that scan recursivly a directory to find .inc and . php
# then return a hash with class => path to the class file
sub get_classes {

309
    my ($path) = @_;
310

311
    # if this function has been called without a parameter
312
    die ("! function get_classes called without parameter\n") if ( !defined($path) );
313
314

    # create a "dir" object with the path
315
    my $dir = dir ($path) or die ("! Can't open $path\n");
316

317
318
319
320
321
    my $contrib_dir = dir($vars{fd_home},"contrib");
    if ("$dir" eq "$contrib_dir") {
        return;
    }

322
323
324
325
326
327
    # create an array with the content of $dir
    my @dir_files = $dir->children;

    foreach my $file (@dir_files) {
        # recursive call if $file is a directory
        if ( -d $file ) {
328
            get_classes($file);
329
330
331
332
            next;
        }

        # only process if $file is a .inc or a .php file
333
        if ( ( $file =~ /.*\.inc$/ ) && ( $file !~ /.*smarty.*/ ) ) {
334
335
          # put the entire content of the file pointed by $file in $data
          my @lines = $file->slurp;
336

337
          # modifing $file, to contains relative path, not complete one
338
          $file =~ s/^$vars{fd_home}//;
339
340

          foreach my $line ( @lines ) {
341
342
            # remove \n from the end of each line
            chomp $line;
343
344
345
346
347
348

            # only process for lines beginning with "class", and extracting the 2nd word (the class name)
            if ( $line =~ /^class\s*(\w+).*/ ) {
              # adding the values (class name and file path) to the hash
              $classes_hash_result{$1} = $file;
            }
349
          }
350
351
        }
    }
352
    return %classes_hash_result;
353
354
355
356
357
}

# call get_classes and create /var/cache/fusiondirectory/class.cache
sub rescan_classes {

358
  # hash that will contain the result of the "get_classes" function
359
  my %get_classes_result = get_classes ($vars{fd_home});
360

361
362
  # create a "file" object with the $class_cache path
  my $file_class = file ($class_cache);
363

364
  # create the handler (write mode) for the file previoulsy created
365
  my $fhw = $file_class->openw() or die ("! Unable to open $class_cache in write mode\n");
366

367
368
  # first lines of class.cache
  $fhw->print ("<?php\n\t\$class_mapping= array(\n");
369

370
371
372
373
  # for each $key/$value, writting a new line to $class_cache
  while ( my($key,$value) = each %get_classes_result ) {
    $fhw->print ("\t\t\"$key\" => \"$value\",\n");
  }
374

375
376
  # last line of classe.cache
  $fhw->print ("\t);\n?>");
377

378
  $fhw->close or die ("! Can't close $class_cache\n");
379
380
381
382
383
384
}

###################################################### Internalisation's update ####################################################################################

# function that create .mo files with .po for each language
sub get_i18n {
385

386
    my ($path) = @_;
387

388
    # if this function has been called without a parameter
389
    die ("! function get_i18n called without parameter" ) if ( !defined($path) );
390
391

    # create a "dir" object
392
    my $dir = dir ($path) or die ("! Can't open $path\n");
393
394
395
396
397

    # create an array with the content of $dir
    my @dir_files = $dir->children;

    foreach my $file (@dir_files) {
398
399
400
401
402
403
      # recursive call if $file is a directory
      if (-d $file) {
        %i18n_hash_result = get_i18n ($file);
        next;
      }

404
405
      # if the file's directory is ???/language/fusiondirectory.po
      if ($file =~ qr{^.*/(\w+)/fusiondirectory.po$}) {
406
407
408
        # push the file's path in the language (fr/en/es/it...) array (wich is inside the hash pointed by $ref_result
        push @{$i18n_hash_result{$1}}, $file;
      }
409
    }
410
  return %i18n_hash_result;
411
412
}

413
414
415
# call get_i18n with the FusionDirectory's locales's directory and the hash that will contain the result in parameter
sub rescan_i18n {

416
417
  # hash that will contain the result of the "get_i18n" function
  my %get_i18n_result = get_i18n ($locale_dir);
418

419
  while ( my ($lang, $files) = each %get_i18n_result ) {
420

421
    # directory wich will contain the .mo file for each language
422
    my $lang_cache_dir = dir ("$locale_cache_dir/$lang/LC_MESSAGES");
423

424
    # if $lang_cache_dir doesn't already exists, creating it
425
    if ( !-d $lang_cache_dir ) {
426
      $lang_cache_dir->mkpath or die ("! Can't create $locale_cache_dir/$lang/LC_MESSAGES");
427
    }
428

429
430
431
    # glue .po files's names
    my $po_files = join(" ", @{$files});
    chomp $po_files;
432

433
    # merging .po files
434
    system ( "msgcat --use-first ".$po_files.">".$lang_cache_dir."/fusiondirectory.po" ) and die ("! Unable to merge .po files for $lang with msgcat, is it already installed?\n");
435

436
    # compiling .po files in .mo files
437
    system ( "msgfmt -o $lang_cache_dir/fusiondirectory.mo $lang_cache_dir/fusiondirectory.po && rm $lang_cache_dir/fusiondirectory.po" ) and die ("! Unable to compile .mo files with msgfmt, is it already installed?\n");
438
  }
439
440
}

441
442
############################################################# Directories checking ###################################################################################

443
#get the apache user group name
444
445
sub get_apache_group {
  my $apache_group = "";
446
447
448

  # try to identify the running distribution, if it's not debian or rehat like, script ask for user input
  if (-e "/etc/debian_version") {
449
    $apache_group = "www-data";
450
  } elsif ((-e "/etc/redhat-release") || (-e "/etc/mageia-release")) {
451
452
453
    $apache_group = "apache";
  } elsif (-e "/etc/SuSE-release") {
    $apache_group = "www";
454
455
  } elsif (-e "/etc/arch-release") {
    $apache_group = "http";
456
  } else {
457
458
    print ("! Looks like you are not a Debian, Suse, Redhat or Mageia, I don't know your distribution !\n");
    $apache_group = ask_user_input ("What is your apache group?");
459
  }
460
  return $apache_group;
461
}
462

463
464
465
466
467
#check the rights of a directory or file, creates missing directory if needed
sub check_rights {
  my ($dir,$user,$group,$rights,$create) = @_;
  my $user_uid = getpwnam ( $user );
  my $group_gid = getgrnam ( $group );
468

469
470
471
472
473
474
  # if the current dir exists
  if (-e $dir) {
    print("$dir exists…\n");
    # retrieve dir's informations
    my @lstat = lstat ($dir);

475
    # extract the owner and the group of the directory
476
477
478
479
480
    my $dir_owner = getpwuid ( $lstat[4] );
    my $dir_group = getgrgid ( $lstat[5] );

    # extract the dir's rights
    my $dir_rights = S_IMODE( $lstat[2] );
481

482
483
484
485
486
    if ( ($dir_owner ne $user) || ($dir_group ne $group) || ($dir_rights ne $rights) ) {
      if ( ask_yn_question ("$dir is not set properly, do you want to fix it ?: ") ) {
        chown ($user_uid,$group_gid,$dir) or die ("! Unable to change $dir owner\n") if ( ($dir_owner ne $user) || ($dir_group ne $group) );
        chmod ( $rights, $dir ) or die ("! Unable to change $dir rights\n") if ($dir_rights ne $rights);
      } else {
487
        print ("Skipping...\n");
488
489
490
491
492
      }
    } else {
      print("Rights on $dir are correct\n");
    }
  } elsif ($create) {
493

494
    if ( ask_yn_question("Directory $dir doesn't exists, do you want to create it ?: ") ) {
495
      my $conf_dir = dir ($dir);
496

497
498
499
500
      # create the directory, and change the rights
      $conf_dir->mkpath (0,$rights);
      chmod ($rights, $dir);
      chown ($user_uid,$group_gid,$dir) or die ("Unable to change $dir rights\n");
501

502
    } else {
503
      print ( "Skipping...\n" );
504
505
506
507
508
509
510
511
512
    }
  } else {
    return 0;
  }
  return 1;
}

# function that check FusionDirectory's directories
sub check_directories {
513
  my $apache_group = get_apache_group();
514

515
  # for each config directory
516
  foreach my $dir (@config_dirs) {
517
518
519

      # if $dir is one of the dirs that remains to root
      if ( grep (/.*$dir.*/, @root_config_dirs) ) {
520
        check_rights($dir,"root","root",oct(755),1);
521

522
      # else if $dir is one of the dirs that remains to apache's user group, and the dir's owner is not root or the group is not the apache's user group, modifying owner
523
      } elsif ( grep ( /.*$dir.*/, @apache_config_dirs) ) {
524
        check_rights($dir,"root",$apache_group,oct(770),1);
525
526
      }
  }
527
528
}

529
530
# function that check FusionDirectory's config file
sub check_config {
531
  my $apache_group = get_apache_group();
532

533
  # check config file
534
  check_rights($fd_config,"root",$apache_group,oct(640),0) or die 'The config file does not exists!';
535
536
}

537
538
############################################################# Change install directories #################################################################################

539
sub write_vars {
540
541
  my $filecontent = <<eof;
<?php
542
require_once('variables_common.inc');
543

544
545
546
/*! \\file
 * Define common locations and variables
 * Generated by fusiondirectory-setup */
547

548
549
550
if (!defined("CONFIG_DIR")) {
  define ("CONFIG_DIR", "$vars{fd_config_dir}/"); /* FusionDirectory etc path */
}
551

552
553
554
555
556
557
/* Allow setting the config file in the apache configuration
    e.g.  SetEnv CONFIG_FILE fusiondirectory.conf 1.0
 */
if (!defined("CONFIG_FILE")) {
  define ("CONFIG_FILE", "$vars{config_file}"); /* FusionDirectory filename */
}
558

559
560
561
/* Path for smarty3 libraries */
define("SMARTY", "$vars{fd_smarty_dir}");

562
563
/* Smarty compile dir */
define ("SPOOL_DIR", "$vars{fd_spool_dir}/"); /* FusionDirectory spool directory */
564

565
566
/* Global cache dir */
define ("CACHE_DIR", "$vars{fd_cache}/"); /* FusionDirectory var directory */
567

568
569
/* Global locale cache dir */
define ("LOCALE_DIR", "$locale_cache_dir/"); /* FusionDirectory locale directory */
570

571
572
/* Global tmp dir */
define ("TEMP_DIR", "$tmp_dir/"); /* FusionDirectory tmp directory */
573

574
575
/* Directory containing the configuration template */
define ("CONFIG_TEMPLATE_DIR", "$template_dir/"); /* FusionDirectory template directory */
576

577
578
/* Directory containing the fai logs */
define ("FAI_LOG_DIR", "$fai_log_dir/"); /* FusionDirectory fai directory */
579

580
581
/* Directory containing the supann files
define ("SUPANN_DIR", "$vars{fd_config_dir}/supann/"); /* FusionDirectory supann template directory */
582

583
584
/* name of the class.cache file */
define("CLASS_CACHE", "$vars{class_cache}"); /* name of the class cache */
585
586
587
588
589
590
591
592
?>
eof

  my $variables_path = "$vars{fd_home}/include/variables.inc";
  my $variables_file = file ($variables_path);
  my $vars_file = $variables_file->openw() or die ("! Unable to open $variables_path in write mode\n");
  $vars_file->print($filecontent);
  $vars_file->close or die ("! Can't close $variables_file\n");
593
594
}

595
############################################################# LDAP conformity check #################################################################################
596

597
598
599
# function that add the FusionDirectory's admin account
# return nothing is it a problem?
sub add_ldap_admin {
600
  my ($base, $ldap, $admindns, $people_entries, $roles) = @_;
601

602
603
604
605
  # Get the configuration to know which attribute must be used in the dn
  my $mesg = $ldap->search(
    base => "$base",
    filter => "(&(objectClass=fusionDirectoryConf)(cn=fusiondirectory))",
606
    attrs => ['fdAccountPrimaryAttribute', 'fdForcePasswordDefaultHash', 'fdPasswordDefaultHash']
607
  );
608
  $mesg->code && die $mesg->error;
609
610
611
612
613
614
615
616
617
  my $attr;
  if ($mesg->count <= 0) {
    print "Could not find configuration object, using default value\n";
    $attr = 'uid';
  } elsif (($mesg->entries)[0]->exists('fdAccountPrimaryAttribute')) {
    $attr = ($mesg->entries)[0]->get_value('fdAccountPrimaryAttribute');
  } else {
    $attr = 'uid';
  }
618
619
620
621
  if ($mesg->count > 0) {
    if (($mesg->entries)[0]->exists('fdForcePasswordDefaultHash') && ($mesg->entries)[0]->exists('fdPasswordDefaultHash')) {
      if ((($mesg->entries)[0]->get_value('fdForcePasswordDefaultHash') eq 'TRUE') &&
          (($mesg->entries)[0]->get_value('fdPasswordDefaultHash') ne 'ssha')) {
Côme Bernigaud's avatar
Côme Bernigaud committed
622
        warn "Warning: Administator password will be hashed with ssha instead of forced default ".($mesg->entries)[0]->get_value('fdPasswordDefaultHash')."\n";
623
624
625
      }
    }
  }
626
627
628
629
630
631
632

  my $fd_admin_uid = ask_user_input ("Please enter a login for FusionDirectory's admin", "fd-admin");
  # Does this user exists?
  my $dn = "";
  foreach my $entry (@$people_entries) {
    my $mesg = $ldap->search(
      base => "$entry",
633
      filter => "(&(objectClass=inetOrgPerson)(uid=$fd_admin_uid))",
634
635
      attrs => ['uid']
    );
636
    $mesg->code && die $mesg->error;
637
638
639
640
641
    if ($mesg->count) {
      print "User $fd_admin_uid already existing, adding admin acl to it\n";
      $dn = ($mesg->entries)[0]->dn;
      last;
    }
642
643
  }

644
  if ($dn eq "") {
645
646
    my $fd_admin_pwd = ask_user_input ("Please enter FusionDirectory's admin password", undef, 1);
    my $fd_admin_pwd_confirm = ask_user_input ("Please enter it again", undef, 1);
647
648
649
650
651
652
653

    # while the confirmation password is not the same than the first one
    while ( ($fd_admin_pwd_confirm ne $fd_admin_pwd) && ($fd_admin_pwd_confirm ne "quit" ) ) {
      $fd_admin_pwd_confirm = ask_user_input ("! Inputs don't match, try again or type 'quit' to end this function");
    }
    return -1 if ($fd_admin_pwd_confirm eq "quit");

654
    my $ctx = Digest::SHA->new(1);
655
656
657
658
    my $salt = get_random_string(8);
    $ctx->add($fd_admin_pwd);
    $ctx->add($salt);
    my $hashedPasswd = '{SSHA}'.encode_base64($ctx->digest.$salt, '');
659
660
661
662
663
    my %obj = (
      'cn'  =>  'System Administrator',
      'sn'  =>  'Administrator',
      'uid' =>  $fd_admin_uid,
      'givenname'     =>  'System',
664
      'objectclass'   =>  [ 'top', 'person', 'organizationalPerson', 'inetOrgPerson' ],
665
      'userPassword'  =>  $hashedPasswd
666
667
668
669
670
    );
    if (not defined $obj{$attr}) {
      print "Error : invalid account primary attribute $attr, using uid\n";
      $attr = 'uid';
    }
671
    $dn = "$attr=".$obj{$attr}.",$userrdn,$base";
672
673
674
675
676
677
678

    # Add the administator user object
    my @options = %obj;
    my $admin_add = $ldap->add( $dn, attr => \@options );
    # send a warning if the ldap's admin's add didn't gone well
    $admin_add->code && die "\n! failed to add LDAP's $dn entry - ".$admin_add->error_name.": ".$admin_add->error_text;
  }
679

680
681
682
  # Create admin role if not existing
  my $role;
  if (scalar @$roles == 0) {
683
    my $role_dn = create_role($ldap,$base,'admin','all;cmdrw');
684
685
686
687
688
689
    $role = encode_base64($role_dn, '');
  } else {
    $role = shift(@$roles);
  }

  # Add the assignment that make him an administrator
690
  my $acls = $ldap->search (
691
692
693
694
    base    => "$base",
    scope   => 'base',
    filter  => "(objectClass=*)",
    attrs   => ['objectClass', 'gosaAclEntry']
695
  );
696
  $acls->code && die "\n! failed to search acls in '$base' - ".$acls->error_name.": ".$acls->error_text;
697
  ($acls->count == 0) && die "\n! failed to search acls in '$base' - base not found";
698
  my $oclass = ($acls->entries)[0]->get_value("objectClass", asref => 1);
699
  # Add admin acl
700
  my $newacl = ["0:subtree:$role:".encode_base64($dn, '')];
701
702
703
  if (not (grep $_ eq 'gosaAcl', @$oclass)) {
    push (@$oclass, 'gosaAcl');
  } else {
704
    my $acl = ($acls->entries)[0]->get_value("gosaAclEntry", asref => 1);
705
706
707
    my $i = 1;
    if (defined $acl) {
      foreach my $line (@$acl) {
708
709
710
711
        # Reorder existing non-admin acls
        $line =~ s/^\d+:/$i:/;
        push (@$newacl, $line);
        $i++;
712
713
714
715
716
717
718
719
720
721
      }
    }
  }
  my $result = $ldap->modify (
    $base,
    replace => {
      'objectClass'   => $oclass,
      'gosaAclEntry'  => $newacl
    }
  );
722
  $result->code && warn "\n! failed to add ACL for admin on '$base' - ".$result->error_name.": ".$result->error_text;
723
724
}

725
726
# function that initiate the ldap connexion, and bind as the ldap's admin
sub get_ldap_connexion {
727
728
729
730
731
  my %hash_result = ();
  my $bind_dn = "";
  my $bind_pwd = "";
  my $uri = "";
  my $base = "";
732
  my $tls = 0;
733
734
735

  # read ldap's server's info from /etc/fusiondirectory/fusiondirectory.conf
  if (-e $fd_config) {
736
    my $twig = XML::Twig->new();    # create the twig
737
    $twig->safe_parsefile($fd_config) or die("There is an error in $fd_config XML code: ".(split /\n/, $@)[1]."\n");
738
739
740
741
742
    my @locs = $twig->root->first_child('main')->children('location');
    my %locations = ();
    foreach my $loc (@locs) {
      my $ref = $loc->first_child('referral');
      $locations{$loc->{'att'}->{'name'}} = {
743
        'tls'       => 0,
744
745
746
747
        'uri'       => $ref->{'att'}->{'URI'},
        'bind_dn'   => $ref->{'att'}->{'adminDn'},
        'bind_pwd'  => $ref->{'att'}->{'adminPassword'}
      };
748
749
750
      if (defined $loc->{'att'}->{'ldapTLS'} and $loc->{'att'}->{'ldapTLS'} =~ m/true/i) {
        $locations{$loc->{'att'}->{'name'}}->{'tls'} = 1
      }
751
752
    }

753
754
755
756
757
758
759
760
761
762
    my ($location) = keys(%locations);
    if (scalar(keys(%locations)) > 1) {
      my $question = "There are several locations in your config file, which one should be used : (".join(',',keys(%locations)).")";
      my $answer;
      do {
        $answer = ask_user_input ($question, $location);
      } while (not exists($locations{$answer}));
      $location = $answer;
    }

763
764
765
766
767
768
    if ($locations{$location}->{'uri'} =~ qr|^(.*)/([^/]+)$|) {
      $uri  = $1;
      $base = $2;
    } else {
      die '"'.$locations{$location}->{'uri'}.'" does not contain any base!';
    }
769
770
    $bind_dn  = $locations{$location}->{'bind_dn'};
    $bind_pwd = $locations{$location}->{'bind_pwd'};
771
    $tls      = $locations{$location}->{'tls'};
772

773
774
  # if can't find fusiondirectory.conf
  } else {
775

776
    if ( ask_yn_question ("Can't find fusiondirectory.conf, do you want to specify LDAP's information yourself ?: ") ) {
777
778
      $uri = ask_user_input ("LDAP server's URI");
      $base = ask_user_input ("Search base");
779
780
      $hash_result{base} = $base;

781
      $bind_dn = ask_user_input ("Bind DN");
782
      $bind_pwd = ask_user_input("Bind password", undef, 1);
783
784
785
786
    } else {
      return;
    }
  }
787

788
789
790
791
  # ldap connection
  my $ldap = Net::LDAP->new ($uri) or die ("! Can't contact LDAP server $uri\n");

  $hash_result{ldap} = $ldap;
792
  $hash_result{base} = $base;
793
794

  # bind to the LDAP server
795
  if (-e $fd_secrets) {
796
    open(my $secrets, q{<}, $fd_secrets) || die ("Could not open $fd_secrets");
797
    my $key = "";
798
    while(<$secrets>) {
799
      if ($_ =~ m/RequestHeader set FDKEY ([^ \n]+)\n/) {
800
801
802
803
        $key = $1;
        last;
      }
    }
804
    close($secrets);
805
806
    $bind_pwd = cred_decrypt($bind_pwd, $key);
  }
807
808
809

  if ($tls) {
    # Read LDAP config file
810
    open (my $ldapconf, q{<}, $vars{ldap_conf}) or die ("! Failed to open ldap config file '$vars{ldap_conf}': $!\n");
811
812
813
814
815

    my %tls_options = (
      'REQCERT'   => 'require',
      'CERT'      => '',
      'KEY'       => '',
816
817
      'CACERTDIR' => '',
      'CACERT'    => '',
818
819
    );
    # Scan LDAP config
820
    while (<$ldapconf>) {
821
822
      /^\s*(#|$)/ && next;
      chomp;
823
      if (m/^TLS_(REQCERT|CERT|KEY|CACERTDIR|CACERT)\s+(.*)\s*$/i) {
824
        $tls_options{uc $1} = $2;
825
826
      }
    }
827
    close($ldapconf);
828
829
830
831
832

    $ldap->start_tls(
      verify      => $tls_options{'REQCERT'},
      clientcert  => $tls_options{'CERT'},
      clientkey   => $tls_options{'KEY'},
833
834
      capath      => $tls_options{'CACERTDIR'},
      cafile      => $tls_options{'CACERT'}
835
836
837
    );
  }

838
839
840
841
842
843
  my $bind = $ldap->bind ($bind_dn, password => $bind_pwd);

  # send a warning if the bind didn't gone well
  $bind->code && die ("! Failed to bind to LDAP server: ", $bind->error."\n");

  return %hash_result;
844
845
}

846
847
# function that check if there is an admin
sub check_admin {
848
  my ($base, $ldap, $people_entries) = @_;
849
850

  # search for FusionDirectory's admin account
851
852
853

  # search for admin role
  my $admin_roles = $ldap->search (
854
    base => "$base",
855
856
    filter => "(&(objectClass=gosaRole)(gosaAclTemplate=*:all;cmdrw))",
    attrs => ['gosaAclTemplate']
857
  );
858
  $admin_roles->code && die $admin_roles->error;
859
  my @dns = ();
860
861
862
863
864
865
866
867
868
869
870
871
872
  my @roles = ();
  my $count = 0;
  while (my $entry = $admin_roles->shift_entry) {
    my $role_dn64 = encode_base64($entry->dn, '');
    push @roles, $role_dn64;
    print ("Role ".$entry->dn." is an admin ACL role\n");
    # Search for base-wide assignments
    my $assignments = $ldap->search (
      base    => "$base",
      scope   => 'base',
      filter  => "(&(objectClass=gosaAcl)(gosaAclEntry=*:subtree:$role_dn64:*))",
      attrs   => ['gosaAclEntry']
    );
873
    $assignments->code && die $assignments->error;
874
875
    while (my $assignment = $assignments->shift_entry) {
      my $acl = $assignment->get_value("gosaAclEntry", asref => 1);
876
      foreach my $line (@$acl) {
877
        if ($line =~ m/^.:subtree:\Q$role_dn64\E/) {
878
          my @parts = split(':',$line,4);
879
880
881
882
883
884
885
          my @members = split(",",$parts[3]);
          foreach my $member (@members) {
            # Is this an existing user?
            my $dn = decode_base64($member);
            my $member_node = $ldap->search(
              base    => $dn,
              scope   => 'base',
886
              filter  => "(objectClass=inetOrgPerson)"
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
            );
            if ($member_node->count == 1) {
              print ("$dn is a valid admin\n");
              return;
            }
            # Is this a group?
            $member_node = $ldap->search(
              base    => $dn,
              scope   => 'base',
              filter  => "(objectClass=posixGroup)",
              attrs   => ['memberUid']
            );
            if ($member_node->count == 1) {
              # Find group members
              my $member_entry  = $member_node->shift_entry;
              my $memberUids    = $member_entry->get_value("memberUid", asref => 1);
903
              my $filter = '(&(objectClass=inetOrgPerson)(|(uid='.join(')(uid=', @$memberUids).')))';
904
905
906
907
              my $group_members = $ldap->search(
                base    => $base,
                filter  => $filter,
              );
908
              $group_members->code && die $group_members->error;
909
910
911
912
913
914
              if (my $group_member_entry = $group_members->shift_entry) {
                print ($group_member_entry->dn." is a valid admin\n");
                return;
              }
            } else {
              push @dns, $dn;
915
916
917
918
919
            }
          }
        }
      }
    }
920
921
922
923
    $count++;
  }
  if ($count < 1) {
    print ("! There is no admin ACL role\n");
924
  }
925
  foreach my $dn (@dns) {
926
927
928
    print ("! $dn is supposed to be admin but does not exists\n");
  }
  if (ask_yn_question("No valid admin account found, do you want to create it ?")) {
929
    return add_ldap_admin($base, $ldap, \@dns, $people_entries, \@roles);
930
931
932
  }
}

933
934
sub create_branch {
  my ($ldap, $base, $ou) = @_;
935
936
  $ou =~ m/^ou=([^,]*),?$/ or die "Can’t create branch of unknown type $ou\n";
  my $branch_add = $ldap->add( "$ou,$base",
937
    attr => [
938
      'ou'  => $1,
939
940
941
942
      'objectClass' =>  'organizationalUnit'
      ]
  );

943
  $branch_add->code && die "! failed to add LDAP's $ou,$base branch: ".$branch_add->error."\n";
944
945
946
947
948
949
950
}

sub branch_exists {
  my ($ldap, $branch) = @_;

  # search for branch
  my $branch_mesg = $ldap->search (base => $branch, filter => '(objectClass=*)', scope => 'base');
951
952
953
  if ($branch_mesg->code == 32) {
    return 0;
  }
954
955
956
957
958
959
  $branch_mesg->code && die $branch_mesg->error;

  my @entries = $branch_mesg->entries;
  return (defined ($entries[0]));
}

960
961
# function that check LDAP configuration
sub check_ldap {
962
  read_ldap_config();
963

964
965
966
967
968
969
970
971
972
  # initiate the LDAP connexion
  my %hash_ldap_param = get_ldap_connexion();

  # LDAP's connection's parameters
  my $base = $hash_ldap_param{base};
  my $ldap = $hash_ldap_param{ldap};

  my $admin_add = "";

973
  # Collect existing people branches (even if main one may not exists);
974
  my $people = $ldap->search (base => $base, filter => $userrdn);
975
  $people->code && die $people->error;
976
977
  my @people_entries = $people->entries;
  @people_entries = map {$_->dn} @people_entries;
978

979
980
  # if people branch exists
  if ( branch_exists($ldap, "$userrdn,$base") ) {
981
    check_admin($base, $ldap, \@people_entries);
982
983
984

  # if ou=people doesn't exists
  } else {
985
    print ( "! $userrdn,$base not found in your LDAP directory\n" );
986
987
988

    # if user's answer is "yes", creating ou=people branch
    if ( ask_yn_question("Do you want to create it ?: ") ) {
989
990
      create_branch($ldap, $base, $userrdn);
      push @people_entries, "$userrdn,$base";
991
      check_admin($base, $ldap, \@people_entries);
992
    } else {
993
      print ("Skipping...\n");
994
995
996
    }
  }

997
998
999
  # if groups branch does not exist
  if (!branch_exists($ldap, "$grouprdn,$base")) {
    print ("! $grouprdn,$base not found in your LDAP directory\n");
1000

For faster browsing, not all history is shown. View entire blame