Import WIP script ldap_sync.pl

Able to sync users and their info. Still need to work for alias and groups -> distribution list
This commit is contained in:
Daniel Berteaud 2019-07-10 08:30:36 +02:00
parent ec97b322ef
commit 2700ea82d9
2 changed files with 316 additions and 0 deletions

270
scripts/sync_ldap.pl Normal file
View File

@ -0,0 +1,270 @@
#!/usr/local/bin/perl -w
use lib '/opt/zimbra/common/lib/perl5';
use Zimbra::LDAP;
use Zimbra::ZmClient;
use Net::LDAP;
use YAML::Tiny;
use Getopt::Long;
use Data::UUID;
use utf8;
use Data::Dumper;
my $conf = {};
my $opt = {
config => '/opt/zimbra/conf/ldap_sync.yml'
};
GetOptions (
'c|config=s' => \$opt->{config},
);
# Check if the config file exists, and if so, parse it
# and load it in $conf
if ( -e $opt->{config} ) {
print "Reading config file " . $opt->{config} . "\n";
my $yaml = YAML::Tiny->read( $opt->{config} )
or die "Config file " . $opt->{config} . " is invalid\n";
if ( not $yaml->[0] ) {
die "Config file " . $opt->{config} . " is invalid\n";
}
$conf = $yaml->[0];
} else {
# If the config file doesn't exist, just die
die "Config file " . $opt->{config} . " doesn't exist\n";
}
my $zim_ldap = Zimbra::LDAP->new();
my $uuid = Data::UUID->new();
my $exit = 0;
my $res;
DOMAIN: foreach my $domain ( keys $conf ) {
print "Checking domain $domain\n";
# Search in Zimbra LDAP if the required domain exists
$res = $zim_ldap->ldap->search(
filter => "(&(objectClass=zimbraDomain)(zimbraDomainName=$domain)(!(zimbraDomainAliasTargetId=*)))"
);
if ( $res->code ) {
print "Couldn't lookup zimbra domains : " . $res->error . "\n";
$exit = 255;
}
# We must have exactly 1 result
if ( scalar $res->entries == 0 ) {
if ( yaml_bool($conf->{$domain}->{zimbra}->{create_if_missing}) ) {
print "Creating domain $domain";
ZmClient::sendZmprovRequest( "createDomain $domain " . build_domain_attrs($conf->{$domain}) );
} else {
print "Domain $domain doesn't exist, you must create it first\n";
$exit = 255;
}
} elsif ( scalar $res->entries gt 1 ) {
die "Found several domains matching, something is wrong, please check your settings\n";
}
# Get LDAP entry representing the domain
my $domain_entry = ($res->entries)[0];
# Check if auth is set to ad or ldap
if ( not $domain_entry->exists('zimbraAuthMech') or $domain_entry->get_value('zimbraAuthMech') !~ m/^ad|ldap$/) {
if ( yaml_bool($conf->{$domain}->{zimbra}->{setup_ldap_auth}) ) {
ZmClient::sendZmprovRequest( "modifyDomain $domain " . build_domain_attrs( $conf->{$domain} ) );
} else {
die "Domain " . $conf->{$domain}->{zimbra}->{domain} . " must be configured for LDAP or AD external authentication first\n";
}
}
print "Trying to connect to " . join( ' or ', @{ $conf->{$domain}->{ldap}->{servers} } ) . "\n";
my $ext_ldap = Net::LDAP->new( [ @{ $conf->{$domain}->{ldap}->{servers} } ] );
if ( not $ext_ldap ) {
print "Error while connecting to LDAP : $@\n";
$exit = 255;
next DOMAIN;
}
print "Connection succeeded\n";
if ( yaml_bool( $conf->{$domain}->{ldap}->{start_tls} ) ) {
print "Trying to switch to a secured connection using StartTLS\n";
$res = $ext_ldap->start_tls( verify => 'require' );
if ( $res->code ) {
print "StartTLS failed : " . $res->error . "\n";
$exit = 255;
next DOMAIN;
}
print "StartTLS succeeded\n";
}
if ( defined $conf->{$domain}->{ldap}->{bind_dn} and defined $conf->{$domain}->{ldap}->{bind_pass} ) {
print "Trying to bind as " . $conf->{$domain}->{ldap}->{bind_dn} . "\n";
$ext_ldap->bind(
$conf->{$domain}->{ldap}->{bind_dn},
password => $conf->{$domain}->{ldap}->{bind_pass}
);
if ( $res->code ) {
print "StartTLS failed : " . $res->error . "\n";
$exit = 255;
next DOMAIN;
}
print "Bind succeeded\n";
}
print "Searching for potential users in " . $conf->{$domain}->{users}->{base} . " matching filter " . $conf->{$domain}->{users}->{filter} . "\n";
my $ext_user_search = $ext_ldap->search(
base => $conf->{$domain}->{users}->{base},
filter => $conf->{$domain}->{users}->{filter},
attrs => [ keys $conf->{$domain}->{users}->{attr_map}, ( $conf->{$domain}->{users}->{key} ) ]
);
if ( $ext_user_search->code ) {
print "Search failed : " . $ext_user_search->error . "\n";
$exit = 255;
next DOMAIN;
}
print "Found " . scalar $ext_user_search->entries . " users in external LDAP\n";
print "Searching for users in Zimbra\n";
my $zim_user_search = $zim_ldap->ldap->search(
base => 'ou=people,' . $domain_entry->dn,
filter => '(&(objectClass=zimbraAccount)(!(|' .
'(mail=' . $zim_ldap->global->get_value('zimbraSpamIsSpamAccount') . ')' .
'(mail=' . $zim_ldap->global->get_value('zimbraSpamIsNotSpamAccount') . ')' .
'(mail=' . $zim_ldap->global->get_value('zimbraAmavisQuarantineAccount') . ')' .
'(uid=galsync*)(uid=admin))))',
attrs => [ ( map { $conf->{$domain}->{users}->{attr_map}->{$_} } keys $conf->{$domain}->{users}->{attr_map} ), ( 'uid', 'zimbraAccountStatus', 'zimbraAuthLdapExternalDn' ) ]
);
if ( $zim_user_search->code ) {
print "Search failed : " . $zim_user_search->error . "\n";
$exit = 255;
next DOMAIN;
}
print "Found " . scalar $zim_user_search->entries . " users in Zimbra\n";
print "Now comparing the accounts\n";
my $ext_users = ldap2hashref( $ext_user_search, $conf->{$domain}->{users}->{key} );
my $zim_users = ldap2hashref( $zim_user_search, 'uid' );
# First loop : Check users which exist in external LDAP but not in Zimbra
# or which exist in both but need to be updated
foreach my $user ( keys $ext_users ) {
if ( defined $zim_users->{$user} ) {
# User exists in Zimbra, lets check its attribute are up to date
my $attrs = '';
foreach my $attr ( keys $conf->{$domain}->{users}->{attr_map} ) {
if ( not defined $ext_users->{$user}->{$attr} and not defined $ext_users->{$user}->{$conf->{$domain}->{users}->{attr_map}->{$attr}} ) {
# Attr does not exist in external LDAP and in Zimbra, not need to continue
next;
}
if ( $conf->{$domain}->{users}->{attr_map}->{$attr} ne 'sn' and not defined $ext_users->{$user}->{$attr} ) {
# If the attribute doesn't exist in external LDAP, we must remove it from Zimbra.
# Except for sn which is mandatory
$attrs .= '-' . $conf->{$domain}->{users}->{attr_map}->{$attr} . " '" . $zim_users->{$user}->{$conf->{$domain}->{users}->{attr_map}->{$attr}} . "' ";
} elsif (
( $conf->{$domain}->{users}->{attr_map}->{$attr} ne 'sn' and
$ext_users->{$user}->{$attr} ne ( $zim_users->{$user}->{$conf->{$domain}->{users}->{attr_map}->{$attr}} || '' )
) ||
$conf->{$domain}->{users}->{attr_map}->{$attr} eq 'sn' and
defined $ext_users->{$user}->{$attr} and
$ext_users->{$user}->{$attr} ne ( $zim_users->{$user}->{$conf->{$domain}->{users}->{attr_map}->{$attr}} || '' )
) {
my $value = $ext_users->{$user}->{$attr};
$value =~ s/'/\\'/g;
utf8::encode($value);
$attrs .= $conf->{$domain}->{users}->{attr_map}->{$attr} . " '" . $value . "' ";
print $ext_users->{$user}->{$attr} . " vs " . $zim_users->{$user}->{$conf->{$domain}->{users}->{attr_map}->{$attr}} . "\n";
}
}
if ( not defined $zim_users->{$user}->{zimbraAuthLdapExternalDn} or $zim_users->{$user}->{zimbraAuthLdapExternalDn} ne $ext_users->{$user}->{dn} ) {
my $value = $ext_users->{$user}->{dn};
utf8::encode($value);
$attrs .= " zimbraAuthLdapExternalDn '$value'";
}
if ( $attrs ne '' ) {
# Some attribute must change, lets update Zimbra
print "User $user has changed in external LDAP, updating it\n";
print "Sending zmprov modifyAccount $user\@$domain $attrs\n";
ZmClient::sendZmprovRequest( "modifyAccount $user\@$domain $attrs" );
}
} else {
# User exists in external LDAP but not in Zimbra. We must create it
print "User $user found in external LDAP but not in Zimbra. Will be created\n";
my $attrs = '';
foreach my $attr ( keys $conf->{$domain}->{users}->{attr_map} ) {
next if (not defined $ext_users->{$user}->{$attr} or $ext_users->{$user}->{$attr} eq '');
$attrs .= ' ' . $conf->{$domain}->{users}->{attr_map}->{$attr} . ' ' . $ext_users->{$user}->{$attr};
}
my $pass = $uuid->create_str;
print "Sending zmprov createAccount $user\@$domain $pass $attrs\n";
ZmClient::sendZmprovRequest( "createAccount $user\@$domain $pass $attrs" );
}
}
# Now, we loop through the ZImbra user to check if they should be locked (if they don't exist in external LDAP anymore)
foreach my $user ( keys $zim_users ) {
if ( not defined $ext_users->{$user} and defined $zim_users->{$user}->{zimbraAccountStatus} and $zim_users->{$user}->{zimbraAccountStatus} =~ m/^active|lockout$/ ) {
print "User $user doesn't exist in external LDAP anymore, locking it in Zimbra\n";
print "Sending zmprov modifyAccount $user\@$domain zimbraAccountStatus locked\n";
ZmClient::sendZmprovRequest( "modifyAccount $user\@$domain zimbraAccountStatus locked" );
}
}
}
# zmprov breaks terminal (no echo to your input after execution)
# fix it with a tset
system('tset');
sub ldap2hashref {
my $search = shift;
my $key = shift;
my $return = {};
foreach my $entry ( $search->entries ) {
$return->{lc $entry->get_value($key)}->{dn} = $entry->dn;
foreach my $attr ( $entry->attributes ) {
$return->{lc $entry->get_value($key)}->{$attr} = $entry->get_value($attr) if ($attr ne $key);
}
}
return $return;
}
# Check YAML bool
sub yaml_bool {
my $bool = shift;
if ( $bool =~ m/^y|yes|true|1|on$/i ) {
return 1;
} else {
return 0;
}
}
sub build_domain_attrs {
my $domain_conf = shift;
my $attrs = "zimbraAuthMech " . $domain_conf->{ldap}->{type};
$attrs .= " zimbraAuthMechAdmin " . $domain_conf->{ldap}->{type};
if ( defined $domain_conf->{ldap}->{bind_dn} and defined $domain_conf->{ldap}->{bind_pass} ) {
my $pass = $domain_conf->{ldap}->{bind_pass};
$pass =~ s/'/\\'/g;
$attrs .= " zimbraAuthLdapSearchBindDn '" . $domain_conf->{ldap}->{bind_dn} . "' zimbraAuthLdapSearchBindPassword '" . $pass . "'";
}
if ( defined $domain_conf->{users}->{filter} ) {
$attrs = " zimbraAuthLdapSearchFilter '(&(" . $domain_conf->{users}->{key} . "=%u)(" . $domain_conf->{users}->{filter} . ")'";
}
$attrs .= " zimbraAuthLdapURL " . join( ' +zimbraAuthLdapURL', $domain_conf->{ldap}->{servers} );
if ( defined $domain_conf->{ldap}->{start_tls} and yaml_bool($domain_conf->{ldap}->{start_tls}) ) {
$attrs .= " zimbraAuthLdapStartTlsEnabled TRUE";
}
return $attrs;
}

46
scripts/sync_ldap.yml Normal file
View File

@ -0,0 +1,46 @@
---
#.or:mple
# ldap:
# servers:
# - ldap://ldap1.example.org:389
# - ldap://ldap2.example.org:389
# start_tls: True
# bind_dn: CN=Zimbra,OU=Apps,DC=example,DC=org
# bind_pass: 'S3cr3t.P@ssPHr4z'
# type: ad # can be ad or ldap
#
# users:
# base: OU=People,DC=example,DC=org
# filter: '(&(objectClass=user)(memberOf:1.2.840.113556.1.4.1941:=CN=Role_Mail,OU=Roles,DC=example,DC=org)(mail=*))'
# key: sAMAccountName
# alias_attr: otherMailbox
# attr_map:
# displayName: displayName
# description: description
# cn: cn
# sn: sn
# givenName: givenName
# telephoneNumber: telephoneNumber
# homePhone: homePhone
# mobile: mobile
# streetAddress: street
# l: l
# st: st
# co: co
# title: title
# company: company
#
# groups:
# base: OU=Groups,DC=example,DC=org
# filter: (&(objectClass=group)(mail=*))
# key: cn
# members_attr: member
# members_as_dn: True
# attr_map:
# displayName: displayName
# description: description
#
# zimbra:
# create_if_missing: False
# setup_ldap_auth: False