From 2700ea82d95e01155f643f6bb3b8e41153d802bf Mon Sep 17 00:00:00 2001 From: Daniel Berteaud Date: Wed, 10 Jul 2019 08:30:36 +0200 Subject: [PATCH] Import WIP script ldap_sync.pl Able to sync users and their info. Still need to work for alias and groups -> distribution list --- scripts/sync_ldap.pl | 270 ++++++++++++++++++++++++++++++++++++++++++++++++++ scripts/sync_ldap.yml | 46 +++++++++ 2 files changed, 316 insertions(+) create mode 100644 scripts/sync_ldap.pl create mode 100644 scripts/sync_ldap.yml diff --git a/scripts/sync_ldap.pl b/scripts/sync_ldap.pl new file mode 100644 index 0000000..7a90c79 --- /dev/null +++ b/scripts/sync_ldap.pl @@ -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; +} diff --git a/scripts/sync_ldap.yml b/scripts/sync_ldap.yml new file mode 100644 index 0000000..c6fb169 --- /dev/null +++ b/scripts/sync_ldap.yml @@ -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