e-smith-ldap/root/sbin/e-smith/ldif-fix

420 lines
16 KiB
Perl

#!/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 (<PASSWD>) {
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 (<SHADOW>) {
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 (<GROUP>) {
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 (<SMBDETAIL>) {
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 (<SMBPASSWD>) {
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 (<GROUPMAP>) {
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);
}
}