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:
parent
ec97b322ef
commit
2700ea82d9
|
@ -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