Able to sync users and their info. Still need to work for alias and groups -> distribution listmaster
parent
ec97b322ef
commit
2700ea82d9
2 changed files with 316 additions and 0 deletions
@ -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; |
||||
} |
@ -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 |
Loading…
Reference in new issue