#!/usr/bin/perl -T use strict; use warnings; use Net::LDAP; use Net::LDAP::LDIF; use Date::Parse; use esmith::ConfigDB; use esmith::AccountsDB; use esmith::util; use Getopt::Long qw(:config bundling); $ENV{'PATH'} = '/bin:/usr/bin:/sbin:/usr/sbin'; $ENV{'LANG'} = 'C'; $ENV{'TZ'} = ''; sub dnsort { my %type = ( add => 1, modrdn => 2, moddn => 2, modify => 3, delete => 4); my %attr = ( dc => 1, ou => 2, cn => 3, uid => 4); my ($oa) = ($a->get_value('newrdn') || $a->dn) =~ /^([^=]+)=/; my ($ob) = ($b->get_value('newrdn') || $b->dn) =~ /^([^=]+)=/; my ($ua, $ub) = map { my $tu = $_->get_value('uidnumber'); defined $tu && $tu ne '' ? $tu : -1 } ($a, $b); my ($ga, $gb) = map { my $tg = $_->get_value('gidnumber'); defined $tg && $tg ne '' ? $tg : -1 } ($a, $b); ($attr{$oa} || 9) <=> ($attr{$ob} || 9) || ($type{$a->changetype} || 9) <=> ($type{$b->changetype} || 9) || $ua <=> $ub || $ga <=> $gb || ($a->get_value('newrdn') || $a->dn) cmp ($b->get_value('newrdn') || $b->dn); } my $c = esmith::ConfigDB->open_ro; my $a = esmith::AccountsDB->open_ro; my $auth = $c->get('ldap')->prop('Authentication') || 'disabled'; my $schema = '/etc/openldap/schema/samba.schema'; my $domain = $c->get('DomainName')->value; my $basedn = esmith::util::ldapBase($domain); my $userou = 'ou=Users'; my $groupou = 'ou=Groups'; my $compou = 'ou=Computers'; my ($dc) = split /\./, $domain; my $company = $c->get_prop('ldap', 'defaultCompany') || $domain; my %opt; GetOptions ( \%opt, "diff|d", "update|u", "input|i=s", "output|o=s" ); $opt{input} = '/usr/sbin/slapcat -c 2> /dev/null|' unless $opt{input} && ($opt{input} eq '-' || -f "$opt{input}" || -c "$opt{input}"); $opt{diff} = 1 if $opt{update}; if ( $opt{output} && $opt{output} =~ m{^([-\w/.]+)$}) { $opt{output} = $1; } else { $opt{output} = '-'; } my ($data, $dn); # Top object (base) $data->{$basedn} = { objectclass => [qw/organization dcObject top/], dc => $dc, o => $company, }; # Top containers for users/groups/computers foreach (qw/Users Groups Computers/) { $data->{"ou=$_,$basedn"} = { objectclass => [qw/organizationalUnit top/], ou => $_, }; } # Common accounts needed for SME to work properly $data->{"cn=nobody,$groupou,$basedn"}->{objectclass} = [ qw/posixGroup/ ]; $data->{"uid=www,$userou,$basedn"}->{objectclass} = [ qw/account/ ]; $data->{"cn=www,$groupou,$basedn"} = { objectclass => [ qw/posixGroup/ ], memberuid => [ qw/admin/ ] }; $data->{"cn=rsshusers,$groupou,$basedn"}->{objectclass} = [ qw/posixGroup/ ]; $data->{"cn=shared,$groupou,$basedn"} = { objectclass => [ qw/posixGroup mailboxRelatedObject/ ], mail => "everyone\@$domain", memberuid => [ qw/www/ ] }; # Read in accounts database information foreach my $acct ($a->get('admin'), $a->users, $a->groups, $a->ibays, $a->get_all_by_prop(type => 'machine')) { my $key = $acct->key; my $type = $acct->prop('type'); next if $key eq 'Primary'; $dn = "uid=$key,".($type eq 'machine' ? $compou : $userou).",$basedn"; if ($type =~ /^(?:user|group|machine|ibay)$/ || $key eq 'admin') { if ($type eq 'user' || $key eq 'admin') { # Allow removal of obsolete person objectclass and samba attributes push @{$data->{$dn}->{_delete}->{objectclass}}, 'person'; push @{$data->{$dn}->{objectclass}}, 'inetOrgPerson'; $data->{$dn}->{mail} = "$key\@$domain"; @{$data->{$dn}}{qw/givenname sn telephonenumber o ou l street/} = map { $acct->prop($_) || [] } qw/FirstName LastName Phone Company Dept City Street/; $data->{$dn}->{cn} = $acct->prop('FirstName').' '.$acct->prop('LastName'); } else { push @{$data->{$dn}->{objectclass}}, 'account'; } # users/ibays need to be a member of shared push @{$data->{"cn=shared,$groupou,$basedn"}->{memberuid}}, $key if $type =~ /^(user|ibay)$/ || $key eq 'admin'; # users need to be a member of rsshusers if their shell is /usr/bin/rssh push @{$data->{"cn=rsshusers,$groupou,$basedn"}->{memberuid}}, $key if ($type =~ /^(user)$/ || $key eq 'admin') && (($acct->prop('Shell') || '/usr/bin/rssh') eq '/usr/bin/rssh'); if ($auth ne 'enabled') { # Allow removal of shadow properties push @{$data->{$dn}->{_delete}->{objectclass}}, 'shadowAccount'; $data->{$dn}->{_delete}->{lc($_)} = 1 foreach qw/userPassword shadowLastChange shadowMin shadowMax shadowWarning shadowInactive shadowExpire shadowFlag/; if ( -f "$schema" ) { # If we will be adding samba properties then allow removal push @{$data->{$dn}->{_delete}->{objectclass}}, 'sambaSamAccount'; $data->{$dn}->{_delete}->{lc($_)} = 1 foreach qw/displayName sambaAcctFlags sambaLMPassword sambaNTPassword sambaNTPassword sambaPrimaryGroupSID sambaPwdLastSet sambaSID/; } } } $dn = "cn=$key,$groupou,$basedn"; push @{$data->{$dn}->{objectclass}}, 'posixGroup'; if ($type eq 'group') { # Allways replace memberuid with new set $data->{$dn}->{_delete}->{memberuid} = 1; push @{$data->{$dn}->{objectclass}}, 'mailboxRelatedObject'; $data->{$dn}->{mail} = "$key\@$domain"; $data->{$dn}->{description} = $acct->prop('Description') || []; push @{$data->{$dn}->{memberuid}}, split /,/, ($acct->prop('Members') || ''); # www needs to be a memeber of every group push @{$data->{$dn}->{memberuid}}, 'www'; if ($auth ne 'enabled' && -f "$schema" ) { # If we will be adding samba properties then allow removal push @{$data->{$dn}->{_delete}->{objectclass}}, 'sambaGroupMapping'; $data->{$dn}->{_delete}->{lc($_)} = 1 foreach qw/displayName sambaGroupType sambaSID/; } } elsif ($type eq 'ibay') { $dn = "cn=".$acct->prop('Group').",$groupou,$basedn"; push @{$data->{$dn}->{memberuid}}, $acct->key; } } if ($auth ne 'enabled') { # Read in information from unix (passwd) system open PASSWD, '/etc/passwd'; while () { chomp; my @passwd = split /:/, $_; next unless scalar @passwd == 7; $dn = "uid=$passwd[0],".($passwd[0] =~ /\$$/ ? $compou : $userou).",$basedn"; next unless exists $data->{$dn}; push @{$data->{$dn}->{objectclass}}, 'posixAccount'; @{$data->{$dn}}{qw/cn uid uidnumber gidnumber homedirectory loginshell/} = map { $passwd[$_] ? $passwd[$_] : [] } (4,0,2,3,5,6); } close (PASSWD); # Shadow file defaults (pulled from cpu.conf) my %shadow_def = ( 1 => [], 2 => 11192, 3 => -1, 4 => 99999, 5 => 7, 6 => -1, 7 => -1, 8 => 134538308 ); # Read in information from unix (shadow) system open SHADOW, '/etc/shadow'; while () { chomp; my @shadow = split /:/, $_; next unless scalar @shadow >= 6; $shadow[1] = '!*' if $shadow[1] eq '!!'; $shadow[1] = "{CRYPT}$shadow[1]" unless $shadow[1] =~ /^\{/; $dn = "uid=$shadow[0],".($shadow[0] =~ /\$$/ ? $compou : $userou).",$basedn"; next unless exists $data->{$dn}; push @{$data->{$dn}->{objectclass}}, 'shadowAccount'; @{$data->{$dn}}{ map { lc($_) } qw/userPassword shadowLastChange shadowMin shadowMax shadowWarning shadowInactive shadowExpire shadowFlag/} = map { $shadow[$_] ? $shadow[$_] : $shadow_def{$_} } (1..8); } close (SHADOW); # Read in information from unix (group) system open GROUP, '/etc/group'; while () { chomp; my @group = split /:/, $_; next unless scalar @group >= 3; $group[3] = [ split /,/, ($group[3] || '') ]; $dn = "cn=$group[0],$groupou,$basedn"; next unless exists $data->{$dn}; push @{$data->{$dn}->{objectclass}}, 'posixGroup'; @{$data->{$dn}}{qw/cn gidnumber/} = map { $group[$_] ? $group[$_] : [] } (0,2); push @{$data->{$dn}->{memberuid}}, @{$group[3]}; } close (GROUP); my %smbprop = ( 'User SID' => 'sambasid', 'Account Flags' => 'sambaacctflags', 'Primary Group SID' => 'sambaprimarygroupsid', 'Full Name' => 'displayname', 'Password last set' => 'sambapwdlastset', ); # Read in information from unix (smbpasswd) system if ( -f "$schema" && -x '/usr/bin/pdbedit' ) { $dn = undef; open SMBDETAIL, '/usr/bin/pdbedit -vL 2> /dev/null|'; while () { chomp; $dn = ("uid=$1,".($1 =~ /\$$/ ? $compou : $userou).",$basedn") if m/^Unix username:\s+(\S.*)$/; next unless $dn && exists $data->{$dn}; # Map the samba account properties that we care about $data->{$dn}->{$smbprop{$1}} = ($2 ? str2time($2) : (defined $3 ? $3 : [])) if m/^(.+):\s+(?:(\S.*\d{4} \d{2}:\d{2}:\d{2}.*)|(.*))$/ && exists $smbprop{$1}; } close (SMBDETAIL); open SMBPASSWD, '/usr/bin/pdbedit -wL 2> /dev/null|'; while () { chomp; my @smbpasswd = split /:/, $_; next unless scalar @smbpasswd >= 6; $dn = "uid=$smbpasswd[0],".($smbpasswd[0] =~ /\$$/ ? $compou : $userou).",$basedn"; next unless exists $data->{$dn} && exists $data->{$dn}->{uidnumber} && $data->{$dn}->{uidnumber} eq $smbpasswd[1]; push @{$data->{$dn}->{objectclass}}, 'sambaSamAccount'; @{$data->{$dn}}{qw/sambalmpassword sambantpassword/} = map { $smbpasswd[$_] ? $smbpasswd[$_] : [] } (2,3); } close (SMBPASSWD); } if ( -f "$schema" && -x '/usr/bin/net' ) { open GROUPMAP, '/usr/bin/net groupmap list 2> /dev/null|'; while () { chomp; if (m/^(.+) \((.+)\) -> (.+)$/) { # Skip local machine accounts next if $2 =~ /S-1-5-32-\d+/; $dn = "cn=$3,$groupou,$basedn"; next unless exists $data->{$dn}; push @{$data->{$dn}->{objectclass}}, 'sambaGroupMapping'; @{$data->{$dn}}{qw/displayname sambasid sambagrouptype/} = ($1, $2, 2); } } close (GROUPMAP); } } my @ldif; # Loop through ldap data and update as necessary my $reader = Net::LDAP::LDIF->new( $opt{input}, 'r', onerror => 'undef' ); while( not $reader->eof()) { my $entry = $reader->read_entry() || next; $dn = $entry->dn; # Ensure the basedn is correct $dn = "$1$basedn" if $dn =~ /^((?:(?!dc=)[^,]+,)*)dc=/; # Ensure correct ou is part of user/groups/computers if ($dn =~ /^(uid=([^,\$]+)(\$)?),((?:(?!dc=)[^,]+,)*)dc=/) { if ( defined $3 && $3 eq '$') { $dn = "$1,$compou,$basedn"; } elsif (grep /posixGroup/, @{$entry->get_value('objectclass', asref => 1) || []}) { $dn = "cn=$2,$groupou,$basedn"; # Cleanup attributes that the modrdn will perform $entry->add(cn => $2); $entry->delete(uid => [$2]); } else { $dn = "$1,$userou,$basedn"; } } elsif ($dn =~ /^(cn=[^,]+),((?:(?!dc=)[^,]+,)*)dc=/) { $dn = "$1,$groupou,$basedn" unless $2 =~ /^ou=auto\./; } # Don't process records twice next if $data->{$dn}->{_done}; # Rename existing entry into place if we can if ($dn ne $entry->dn) { my $rdn = Net::LDAP::Entry->new; $rdn->dn($entry->dn); $rdn->changetype('modrdn'); my ($newdn, $newbase) = split /,/, $dn, 2; $rdn->add(newrdn => $newdn, deleteoldrdn => 1, newsuperior => $newbase); push @ldif, $rdn; # Now we can change the entry to new dn $entry->dn($dn); } # Change type to modify so that we can keep track of changes we make $entry->changetype('modify'); # Hack to make upgrades work (add calEntry if calFGUrl attributes exists) if ($entry->exists('calFBURL') && -f "/etc/openldap/schema/rfc2739.schema") { push @{$data->{$dn}->{objectclass}}, 'calEntry'; } my %attributes = (); @attributes{ keys %{$data->{$dn}}, exists $data->{$dn}->{_delete} ? map { lc($_) } keys %{$data->{$dn}->{_delete}} : () } = (); foreach my $attr (sort keys %attributes) { # Skip the pseudo attributes next if $attr =~ /^_/; my @l = @{$entry->get_value($attr, asref => 1) || []}; my @u = exists $data->{$dn}->{$attr} ? (ref $data->{$dn}->{$attr} ? @{$data->{$dn}->{$attr}} : ($data->{$dn}->{$attr})) : (); # Figure out differences between attributes my (@lonly, @uonly, @donly, %lseen, %useen, %dseen) = () x 6; # Unique lists of what is in ldap and what needs to be in ldap @lseen{@l} = (); @useen{@u} = (); # Create list of attributes that aren't in the other @uonly = grep { ! exists $lseen{$_} } keys %useen; @lonly = grep { ! exists $useen{$_} } keys %lseen; # Determine which of the ldap only attributes we need to remove if ((keys %useen == 1 && keys %lseen == 1) || (keys %useen == 0 && exists $data->{$dn}->{$attr})) { # Replacing a single entry or erasing entire entry @donly = @lonly; } elsif ($data->{$dn}->{_delete} && $data->{$dn}->{_delete}->{$attr}) { if (my $ref = ref($data->{$dn}->{_delete}->{$attr})) { # Map hash keys or array elemts to valid values to delete @dseen{$ref eq 'HASH' ? keys %{$data->{$dn}->{_delete}->{$attr}} : @{$data->{$dn}->{_delete}->{$attr}}} = (); @donly = grep { exists $dseen{$_} } @lonly; } else { # Permission to remove all values @donly = @lonly; } } if (@donly && @donly == keys %lseen) { # If we are removing all ldap attributes do a remove or full delete if (@uonly) { $entry->replace($attr => [ @uonly ]); } else { $entry->delete($attr => []); } } else { $entry->delete($attr => [ @donly ]) if @donly; $entry->add($attr => [ @uonly ]) if @uonly; } } $data->{$dn}->{_done} = 1; push @ldif, $entry; } $reader->done(); # Add missing records that didn't exist in ldap yet foreach $dn (grep { ! exists $data->{$_}->{_done} } sort keys %$data) { my $entry = Net::LDAP::Entry->new; $entry->dn($dn); foreach my $attr (sort keys %{$data->{$dn}}) { # Skip the pseudo attributes next if $attr =~ /^_/; my %seen = (); @seen{ref $data->{$dn}->{$attr} ? @{$data->{$dn}->{$attr}} : ($data->{$dn}->{$attr})} = (); $entry->add($attr => [ sort keys %seen ]) if keys %seen != 0; } push @ldif, $entry; } #------------------------------------------------------------ # Update LDAP database entry. #------------------------------------------------------------ my $ldap; if ($opt{update}) { $ldap = Net::LDAP->new('localhost') or die "$@"; $ldap->bind( dn => "cn=root,$basedn", password => esmith::util::LdapPassword() ); } my $writer = Net::LDAP::LDIF->new( $opt{output}, 'w', onerror => 'undef', wrap => 0, sort => 1, change => $opt{diff} ); foreach my $entry (sort dnsort @ldif) { if ($opt{update} && ($entry->changetype ne 'modify' || @{$entry->{changes}}) ) { my $result = $entry->update($ldap); warn "Failure to ",$entry->changetype," ",$entry->dn,": ",$result->error,"\n" if $result->code; } if ($writer->{change} || $entry->changetype !~ /modr?dn/) { $writer->write_entry($entry); } }