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