420 lines
16 KiB
Perl
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);
|
|
}
|
|
}
|