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