1#!/usr/bin/perl -w 2 3# Created by P.Wieleba@iem.pw.edu.pl in 2004 4 5use strict; 6use Getopt::Std; 7use FindBin; 8use FindBin qw($RealBin); 9use lib "$RealBin/"; 10use smbldap_tools; 11 12# function declaration 13sub migrate_user; 14sub migrate_shadow_user; 15sub get_user_entry; 16sub exist_in_tab; 17sub del_from_tab; 18sub add_to_tab; 19sub read_shadow_file; 20 21# smbldap-migrate-unix-accounts (-? or -h for help) 22# 23# 24 25my %Options; 26 27my $ok = getopts('M:P:S:vn?hd:a', \%Options); 28 29if ( (!$ok) || ($Options{'?'}) || ($Options{'h'}) || (!keys(%Options)) ) { 30 print "Usage: $0 [-PSMvn?hda]\n"; 31 print " -?|-h show this help message\n"; 32 print " -P file import passwd file\n"; 33 print " -S file import shadow file\n"; 34 print " -M file import FreeBSD master.passwd\n"; 35 print " -v displays modified entries to STDOUT\n"; 36 print " -n do everything execpt updating LDAP\n"; 37 print " -d obj_nam delete and add (not just update) existing entry in LDAP\n"; 38 print " -a adds sambaSamAccount objectClass\n"; 39 exit (1); 40} 41 42my $INFILE = undef; 43my %shadowUsers; 44 45if ( $Options{'M'} ) { 46 open($INFILE,$Options{'M'}) or 47 die "I cannot open file: " . $Options{'M'} . "\n"; 48} elsif ( $Options{'P'} ) { 49 open($INFILE,$Options{'P'}) or 50 die "I cannot open file: " . $Options{'P'} . "\n"; 51 # if defined -S option also read shadow file 52 if ( $Options{'S'} ) { 53 %shadowUsers = read_shadow_file($Options{'S'}); 54 (%shadowUsers) or ( close($INFILE) and 55 die "I cannot open file: " . $Options{'S'} . "\n" ); 56 } 57} elsif ( $Options{'S'} ) { 58 open($INFILE,$Options{'S'}) or 59 die "I cannot open file: " . $Options{'S'} . "\n"; 60} 61 62my $ldap_master=connect_ldap_master(); 63 64while ( my $line=<$INFILE> ) { 65 chop($line); 66 next if ( $line =~ /^\s*$/ ); # whitespace 67 next if ( $line =~ /^#/ ); 68 next if ( $line =~ /^\+/ ); 69 my $entry = undef; 70 if ($Options{'M'}) { 71 my($user,$pwd,$uid,$gid,$class,$change,$expire,$gecos,$homedir,$shell) = split(/:/,$line); 72 # if user is not in LDAP new entry will be created 73 $entry = get_user_entry($ldap_master,$user); 74 $entry = migrate_user($entry,$user,$pwd,$uid,$gid,$gecos,$homedir,$shell); 75 # for master.passwd file (nss_ldap) 76 if ($entry) { 77 my @objectClass = $entry->get_value( 'objectClass' ); 78 $entry->replace( 'objectClass' => [add_to_tab(\@objectClass,'shadowAccount')] ); 79 } 80 } elsif ($Options{'P'}) { 81 my($user,$pwd,$uid,$gid,$gecos,$homedir,$shell) = split(/:/,$line); 82 # if user is not in LDAP new entry will be created 83 $entry = get_user_entry($ldap_master,$user); 84 $entry = migrate_user($entry,$user,$pwd,$uid,$gid,$gecos,$homedir,$shell,undef); 85 86 # should I delete next functionality 87 # add shadow entries if also -S defined 88 if ($Options{'S'} and $shadowUsers{$user}) { 89 my($user,$pwd,$lastchg,$min,$max,$warn,$inactive,$expire,$flag) = split(/:/,$shadowUsers{$user}); 90 $entry = migrate_shadow_user($entry,$user,$pwd,$lastchg,$min,$max,$warn,$inactive,$expire,$flag); 91 } 92 } elsif ($Options{'S'}) { 93 my($user,$pwd,$lastchg,$min,$max,$warn,$inactive,$expire,$flag)=split(/:/,$line); 94 # if user is not in LDAP new entry will be created 95 $entry = get_user_entry($ldap_master,$user); 96 $entry = migrate_shadow_user($entry,$user,$pwd,$lastchg,$min,$max,$warn,$inactive,$expire,$flag); 97 } 98 99 if ($entry) { 100 # objectClass $Options{'d'} will be removed 101 # from entry if it exists 102 if ($Options{'d'}) { 103 my @objectClass = $entry->get_value( 'objectClass' ); 104 $entry->replace( 'objectClass' => [del_from_tab(\@objectClass,$Options{'d'})] ); 105 #$entry->delete( 'objectClass' => [ $Options{'d'} ] ); 106 } 107 # if used "-a" and sambaSamAccount doesn't exist. 108 if ( $Options{'a'} and !exist_in_tab([$entry->get_value('objectClass')],'sambaSamAccount') ) { 109 my @objectClass = $entry->get_value( 'objectClass' ); 110 $entry->replace( 'objectclass' => [add_to_tab(\@objectClass,'sambaSamAccount')] ); 111 112 # the below part comes from smbldap-useradd and 113 # maybe it should be replaced by a new subroutine. 114 my $userUidNumber = $entry->get_value('uidNumber'); 115 # as rid we use 2 * uid + 1000 116 my $userRid = 2 * $userUidNumber + 1000; 117 # let's test if this SID already exist 118 my $user_sid = "$config{SID}-$userRid"; 119 my $test_exist_sid = does_sid_exist($user_sid,$config{usersdn}); 120 if ($test_exist_sid->count == 1) { 121 print "User SID already owned by\n"; 122 # there should not exist more than one entry, but ... 123 foreach my $entry ($test_exist_sid->all_entries) { 124 my $dn= $entry->dn; 125 chomp($dn); 126 print "$dn\n"; 127 } 128 } else { 129 $entry->replace( 'sambaSID' => $user_sid ); 130 } 131 } 132 if ($Options{'v'}) { 133 $entry->dump(); 134 } 135 if (!$Options{'n'}) { 136 my $mesg; 137 if ( $Options{'d'} ) { 138 # delete entry from LDAP if it exists 139 $mesg = $ldap_master->search( base => $entry->dn(), 140 scope => 'sub', 141 filter => '(objectClass=*)' 142 ); 143 if ( $mesg->count() == 1 ) { 144 $mesg = $ldap_master->delete($entry->dn()); 145 if ($mesg->is_error()) { 146 print "Error: " . $mesg->error() . "\n"; 147 } 148 $entry->changetype('add'); 149 } 150 } 151 $mesg = $entry->update($ldap_master); 152 if ($mesg->is_error()) { 153 print "Error: " . $mesg->error() . "\n"; 154 } 155 } 156 } 157} 158 159$INFILE and close($INFILE); 160# take down the session 161$ldap_master and $ldap_master->unbind; 162 163# returns updated $entry 164sub migrate_user 165 { 166 my($entry,$user,$pwd,$uid,$gid,$gecos,$homedir,$shell) = @_; 167 my($name,$office,$wphone,$hphone)=split(/,/,$gecos); 168 my($cn); 169 170 # posixAccount MUST ( cn $ uid $ uidNumber $ gidNumber $ homeDirectory ) 171 my @objectClass = $entry->get_value( 'objectClass' ); 172 @objectClass = add_to_tab(\@objectClass,'posixAccount'); 173 @objectClass = add_to_tab(\@objectClass,'inetOrgPerson'); 174 $entry->replace( 'objectClass' => \@objectClass ); 175 176 $entry->replace( 'uid' => $user ); 177 if ($name) { 178 $cn = $name; 179 } else { 180 $cn = $user; 181 } 182 $entry->replace( 'cn' => $cn ); 183 # perhaps I should delete it 184 if ( exist_in_tab(\@objectClass,'inetOrgPerson') ) { 185 # 'sn' is required by person objectClass from core.schema 186 my @tmp = split(/\s+/,$cn); 187 my $sn = $tmp[$#tmp]; 188 $entry->replace( 'sn' => $sn ); 189 # perhaps 'telephoneNumber' 'roomNumber' 'homePhone' 190 # and 'givenName' also should be modified ??????? 191 } 192 ($pwd) and $entry->replace( 'userPassword' => "{crypt}" . $pwd ); 193 ($uid ne "") and $entry->replace( 'uidNumber' => $uid ); 194 ($gid ne "") and $entry->replace( 'gidNumber' => $gid ); 195 ($gecos) and $entry->replace( 'gecos' => $gecos ); 196 ($homedir) and $entry->replace( 'homeDirectory' => $homedir ); 197 ($shell) and $entry->replace( 'loginShell' => $shell ); 198 199 return $entry; 200 } 201 202# returns updated $entry 203sub migrate_shadow_user 204 { 205 my($entry,$user,$pwd,$lastchg,$min,$max,$warn,$inactive,$expire,$flag) = @_; 206 207 # shadowAccount MUST uid 208 my @objectClass = $entry->get_value( 'objectClass' ); 209 # if the entry doesn't exist, it needs structural objectclass 210 (@objectClass) or push(@objectClass,'account'); 211 $entry->replace( 'objectClass' => [add_to_tab(\@objectClass,'shadowAccount')] ); 212 213 $entry->replace( 'uid' => $user ); 214 ($pwd) and $entry->replace( 'userPassword' => "{crypt}" . $pwd ); 215 ($lastchg) and $entry->replace( 'shadowLastChange' => $lastchg ); 216 ($min) and $entry->replace( 'shadowMin' => $min ); 217 ($max) and $entry->replace( 'shadowMax' => $max ); 218 ($warn) and $entry->replace( 'shadowWarning' => $warn ); 219 ($inactive) and $entry->replace( 'shadowInactive' => $inactive ); 220 ($expire) and $entry->replace( 'shadowExpire' => $expire ); 221 ($flag) and $entry->replace( 'shadowFlag' => $flag ); 222 223 return $entry; 224 } 225 226# creates a _new_entry_ if user doesn't exist in ldap 227# else return's ldap user entry 228sub get_user_entry 229 { 230 my($ldap_master,$user) = @_; 231 232 # do not use read_user_entry() 233 my $mesg = $ldap_master->search( base => $config{usersdn}, 234 scope => 'one', 235 filter => "(uid=$user)" 236 ); 237 my $entry; 238 if ( $mesg->count() != 1 ) { 239 $entry = Net::LDAP::Entry->new(); 240 $entry->dn("uid=$user,$config{usersdn}"); 241 } else { 242 $entry = $mesg->entry(0); # ???? 243 } 244 return $entry; 245 } 246 247# Check if a $text element exists in @table 248# eg. exist_in_tab(\@table,$text); 249sub exist_in_tab 250 { 251 my($ref_tab,$text) = @_; 252 my @tab = @$ref_tab; 253 254 foreach my $elem (@tab) { 255 if ( lc($elem) eq lc($text) ) { 256 return 1; 257 } 258 } 259 return 0; 260 } 261 262# Delete $text element from @table 263# eg. del_from_tab(\@table,$text); 264sub del_from_tab 265 { 266 my($ref_tab,$text) = @_; 267 my @tab = @$ref_tab; 268 my @new_tab; 269 270 foreach my $elem (@tab) { 271 if ( lc($elem) ne lc($text) ) { 272 push(@new_tab,$elem); 273 } 274 } 275 return @new_tab; 276 } 277 278# Add $text to tab if it doesn't exist there 279sub add_to_tab 280 { 281 my($ref_tab,$text) = @_; 282 my @tab = @$ref_tab; 283 284 if ( !exist_in_tab(\@tab,$text) ) { 285 push(@tab,$text); 286 } 287 return @tab; 288 } 289 290# reads shadow file entries and places them in a hash 291sub read_shadow_file 292 { 293 my($shadow) = @_; 294 295 my $shadowUser; 296 my %shadowUsers; 297 open(SHADOW,$shadow) or 298 return ; 299 while (my $line=<SHADOW>) { 300 chop($line); 301 next if ( $line =~ /^\s*$/ ); # whitespace 302 next if ( $line =~ /^#/ ); 303 ($shadowUser) = split(/:/, $line); 304 $shadowUsers{$shadowUser} = $line; 305 } 306 close(SHADOW); 307 return %shadowUsers; 308 } 309 310######################################## 311 312=head1 NAME 313 314smbldap-migrate-unix-accounts - Migrate unix accounts to LDAP 315 316=head1 SYNOPSIS 317 318smbldap-migrate-unix-accounts [-P file] [-S file] [-M file] [-n] [-v] 319[-h] [-?] [-d] 320 321=head1 DESCRIPTION 322 323This command processes one file as defined by option and 324creates new or changes existing ldap user entry. 325New attributes are added, and existing are changed. 326None of the existing attributes is deleted. 327 328-P passwd_file 329 Processes passwd_file and uptades LDAP. Creates new ldap user 330 entry or just adds posixAccount objectclass and corresponding 331 attributes to the ldap user entry or just uptades their values. 332 333-S shadow_file 334 Reads shadow_file and uptades LDAP. Creates new ldap user 335 entry or just adds shadowAccount objectclass and corresponding 336 attributes to the ldap user entry or just uptades their values. 337 338-M master.passwd_file 339 Reads master.passwd_file and uptades LDAP. Creates new ldap user 340 entry or just adds shadowAccount and posixAccount objectclass 341 and corresponding attributes to the ldap user entry or just 342 uptades their values. 343 344-h show the help message 345 346-? the same as -h 347 348-v displayes modified entries to STDOUT 349 350-n do everything execpt updating LDAP. It is useful when used 351 with -v switch. 352 353-d objeClass_name 354 In spite of just updating existing user entry, the entry will be 355 deleted from LDAP and a new one will be added. 356 It is essential to use this option if you update users in LDAP 357 and want to change their structural objectClass. 358 Use it in the example schema: 359 There are no users in LDAP, and you migrate samba first. 360 # pdbedit -e ldapsam:ldap://localhost 361 # smbldap-migrate-passwd -P passwd -d 'account' 362 363-a adds sambaSamAccount objectClass and generates sambaSID attribute 364 365=cut 366 367#' 368 369# The End 370 371 372