More advanced ldap_sync script

including alias and groups -> dl
This commit is contained in:
Daniel Berteaud 2019-07-16 18:59:44 +02:00
parent 2700ea82d9
commit 665f663846
5 changed files with 1013 additions and 316 deletions

127
ldap_sync/README.md Normal file
View File

@ -0,0 +1,127 @@
# LDAP synchronisation
This script brings a complete synchronization of user accounts and groups from an external LDAP server.
Zimbra (OSE) supports autoprovisioning, but this feature only takes care of user accounts creation. Several other scripts can be found, nut noone of them implemented what I need, so I wrote this one.
The goals are :
* Do not only provision accounts, but update them if needed (eg : name changed)
* Support AD, OpenLDAP, or any custom LDAP schema
* Synchronize LDAP groups into distribution lists in Zimbra, preserving memberships
* Lock Zimbra accounts when the corresponding LDAP accounts are removed (or not matching the filter anymore)
* Handle email alias defined in LDAP, and translate them into aliases in Zimbra
* Allow objects (aliase, distribution list) to be created directly in Zimbra. Objects coming from LDAP are synchronized, including alias previously defined in LDAP which aren't anymore are removed from Zimbra. But aliases defined directly in Zimbra won't be touched. Same is true for distribution lists. So you can mix LDAP defined and Zimbra defined configuration
## Configuration
The configuration is stored in a single file in YAML format. The script will look for a config at /opt/zimbra/conf/ldap_sync.yml or trhe one specified in the --config argument.
The config has two main section :
* general : settings which affects all domains, mainly to configure email notification in case of error
* domains : list of domain to sync, and the settings for each of them
The general section looks like
```
general:
notify:
from: zimbra@example.org
to: admin@acme-corp.biz
```
Foreach each domain you defined, you can configure 4 sections :
* ldap : defined where and how to connect to the external LDAP server
* users : define how to search for users in the external LDAP, and which attributes will be mapped to which one in Zimbra
* groups : define how to search for groups in external LDAP, and which attributes will be mapped to which one in Zimbra
* zimbra : define some settings about how the script should behave for this domain (eg, should it create the domain if missing, should it autoconfigure external authentication etc.)
In most case, the configuration can be minimal, as defaults values are provided. You just have to set the LDAP server, bind DN and password (if applicable), and the schema. The schema can be ad, rfc2307 or rfc2307bis. If one of these schema is specified, adapted defaults values will be used
Here are some examples of domains definition :
```
domains:
# A simple example, against and AD style directory
# Note that this example has no groups definition, and so,
# groups won't be synchronized to distribution lists
acme-corp.biz:
ldap:
servers:
- ldap://dc1.acme-corp.biz:389
- ldap://dc2.acme-corp.biz:389
start_tls: True
bind_dn: CN=Zimbra,CN=Users,DC=acme-corp,DC=biz
bind_pass: 'Sup3rS3cret.P@ssPhr4se'
schema: ad
users:
base: OU=CN=Users,DC=acme-corp,DC=biz
# Another simple example, against OpenLDAP using rfc2307bis schema
corp2.com:
ldap:
servers:
- ldap://ldap.corp2.com
schema: rfc2307bis
users:
base: ou=people,dc=corp2,dc=com
filter: '(memberOf=cn=mail_users,ou=Groups,dc=corp2,dc=com)'
groups:
base: ou=groups,dc=corp2,dc=com
# A more complete example, which shows all the available settings
corp3.net:
ldap:
servers:
- ldap://ldap1.corp3.net:389
- ldap://ldap3.corp3.net:389
start_tls: True
bind_dn: CN=Zimbra,OU=Apps,DC=corp3,DC=net
bind_pass: 'p@ssw0rd'
schema: ad
type: ad
users:
base: OU=People,DC=corp3,DC=net
filter: '(&(objectClass=user)(memberOf:1.2.840.113556.1.4.1941:=CN=Role_Mail,OU=Roles,DC=corp3,DC=net)(mail=*))'
key: sAMAccountName
mail_attr: mail
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=corp3,DC=net
filter: (objectClass=group)
key: cn
members_attr: member
members_as_dn: True
mail_attr: mail
alias_attr: null
attr_map:
displayName: displayName
description: description
zimbra:
create_if_missing: False
setup_ldap_auth: True
```
## Command line
Once a configuration file is ready, the script can be called with the following command line arguments :
* --config : path to the config file (defaults to /opt/zimbra/conf/ldap_sync.yml)
* --quiet : will not print anything except errors
* --verbose : prints aditional info during the sync

874
ldap_sync/ldap_sync.pl Normal file
View File

@ -0,0 +1,874 @@
#!/usr/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 String::ShellQuote qw(shell_quote);
use Array::Diff;
use Hash::Merge::Simple qw(merge);
use Text::Unidecode;
use Email::MIME;
use Email::Sender::Simple qw(sendmail);
use Email::Sender::Transport::Sendmail;
use utf8;
use Data::Dumper;
# This is needed for Email::Sender::Simple
# See https://rt.cpan.org/Public/Bug/Display.html?id=76533
$SIG{CHLD} = sub { wait };
# Init an empty conf
my $conf = {};
# Defaults for command line flags
my $opt = {
'config=s' => '/opt/zimbra/conf/ldap_sync.yml',
'dry' => 0,
'quiet' => 0,
'verbose' => 0
};
# Read some options from the command line
GetOptions (
'config=s' => \$opt->{config},
'dry-run' => \$opt->{dry},
'quiet' => \$opt->{quiet},
'verbose' => \$opt->{verbose}
);
if ( $opt->{verbose} and $opt->{quiet} ) {
print "You cannot use quiet and debug at the same time\n";
usage();
exit 255;
}
# Message set in zimbraNotes to identify objects synced from external LDAP
my $sync_from_ldap = "Synced from external LDAP directory. Do not edit this field";
# Check if the config file exists, and if so, parse it
# and load it in $conf
if ( -e $opt->{config} ) {
log_verbose( "Reading config file " . $opt->{config} );
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 $err = '';
DOMAIN: foreach my $domain ( keys $conf->{domains} ) {
log_verbose( "Start to process domain $domain" );
# Get default config for this domain and merge it with what we have in the config file
$conf->{domains}->{$domain} = get_default_conf( $conf->{domains}->{$domain} );
# Search in Zimbra LDAP if the required domain exists
my $zim_domain_search = $zim_ldap->ldap->search(
filter => "(&(objectClass=zimbraDomain)(zimbraDomainName=$domain)(!(zimbraDomainAliasTargetId=*)))",
attrs => [
'zimbraDomainName',
'zimbraDomainType',
'zimbraId',
'zimbraAuthMechAdmin',
'zimbraAuthMech',
'zimbraAuthLdapSearchBindDn',
'zimbraAuthLdapSearchBindPassword',
'zimbraAuthLdapSearchFilter'
]
);
if ( $zim_domain_search->code ) {
handle_error( $domain, 'Zimbra domain lookup', $zim_domain_search->error );
next DOMAIN
}
# We must have exactly 1 result
if ( scalar $zim_domain_search->entries == 0 ) {
if ( yaml_bool($conf->{domains}->{$domain}->{zimbra}->{create_if_missing}) ) {
log_info( "Creating domain $domain" );
ZmClient::sendZmprovRequest( "createDomain $domain " . build_domain_attrs($conf->{domains}->{$domain}) );
} else {
handle_error( $domain, 'Zimbra domain lookup', "Domain $domain doesn't exist in Zimbra");
next DOMAIN;
}
} elsif ( scalar $zim_domain_search->entries gt 1 ) {
handle_error( $domain, 'Zimbra domain lookup', "Found several matches for domain $domain" );
next DOMAIN;
}
# Get LDAP entry representing the domain
my $domain_entry = ldap2hashref( $zim_domain_search, 'zimbraDomainName' )->{$domain};
# Check if auth is set to ad or ldap
if ( not defined $domain_entry->{zimbraAuthMech} or $domain_entry->{zimbraAuthMech} !~ m/^ad|ldap$/i) {
if ( yaml_bool( $conf->{domains}->{$domain}->{zimbra}->{setup_ldap_auth} ) ) {
send_zmprov_cmd( "modifyDomain $domain " . build_domain_attrs( $conf->{domains}->{$domain} ) );
} else {
handle_error( $domain, 'Domain external auth check', "domain $domain must be configured for LDAP or AD authentication first" );
next DOMAIN;
}
}
# Now lookup for domain aliases defined in Zimbra
my $zim_domain_alias_search = $zim_ldap->ldap->search(
filter => "(&(objectClass=zimbraDomain)(zimbraDomainAliasTargetId=" . $domain_entry->{zimbraId} . "))"
);
if ( $zim_domain_alias_search->code ) {
handle_error( $domain, 'Zimbra domain alias lookup', $zim_domain_alias_search->error );
next DOMAIN;
}
$domain_entry->{zimbraDomainAliases} = [];
foreach my $alias ( $zim_domain_alias_search->entries ) {
push @{ $domain_entry->{zimbraDomainAliases} }, $alias->get_value('zimbraDomainName');
}
log_verbose( "Trying to connect to " . join( ' or ', @{ $conf->{domains}->{$domain}->{ldap}->{servers} } ) );
my $ext_ldap = Net::LDAP->new( [ @{ $conf->{domains}->{$domain}->{ldap}->{servers} } ] );
if ( not $ext_ldap ) {
handle_error( $domain, 'External LDAP connection', $@ );
next DOMAIN;
}
log_verbose( "Connection succeeded" );
if ( yaml_bool( $conf->{domains}->{$domain}->{ldap}->{start_tls} ) ) {
log_verbose( "Trying to switch to a secured connection using StartTLS" );
my $tls = $ext_ldap->start_tls( verify => 'require' );
if ( $tls->code ) {
handle_error( $domain, 'External LDAP StartTLS', $tls->error );
next DOMAIN;
}
log_verbose( "StartTLS succeeded" );
}
if ( defined $conf->{domains}->{$domain}->{ldap}->{bind_dn} and defined $conf->{domains}->{$domain}->{ldap}->{bind_pass} ) {
log_verbose( "Trying to bind as " . $conf->{domains}->{$domain}->{ldap}->{bind_dn} );
my $bind = $ext_ldap->bind(
$conf->{domains}->{$domain}->{ldap}->{bind_dn},
password => $conf->{domains}->{$domain}->{ldap}->{bind_pass}
);
if ( $bind->code ) {
handle_error( $domain, 'External LDAP bind', $bind->error );
next DOMAIN;
}
log_verbose( "Bind succeeded" );
}
my $zim_aliases = {};
foreach my $domain_alias ( $domain, @{ $domain_entry->{zimbraDomainAliases} }) {
log_verbose( "Searching for aliases in Zimbra for domain alias $domain_alias" );
my $zim_aliases_search = $zim_ldap->ldap->search (
base => 'ou=people,' . domain2dn( $domain_alias ),
filter => '(objectClass=zimbraAlias)',
attrs => [
'zimbraAliasTargetId',
'uid'
]
);
if ( $zim_aliases_search->code ) {
handle_error( $domain, 'Zimbra user and distribution lists alias lookup', $zim_aliases_search->error );
next DOMAIN;
}
$zim_aliases->{$domain_alias} = ldap2hashref( $zim_aliases_search, 'uid' );
}
log_verbose( "Searching for potential users in " . $conf->{domains}->{$domain}->{users}->{base} . " matching filter " . $conf->{domains}->{$domain}->{users}->{filter} );
# List of attributes to fetch from LDAP
# First, we want all the attributes which are mapped to Zimbra fields
my $fetch_attrs = [ keys $conf->{domains}->{$domain}->{users}->{attr_map} ];
# We also want the object key
push $fetch_attrs, $conf->{domains}->{$domain}->{users}->{key};
# If defined in the config, we need to get attribute containing email and aliases
foreach ( qw( alias_attr mail_attr ) ) {
next if ( not $conf->{domains}->{$domain}->{users}->{$_} );
push $fetch_attrs, $conf->{domains}->{$domain}->{users}->{$_};
}
my $ext_user_search = $ext_ldap->search(
base => $conf->{domains}->{$domain}->{users}->{base},
filter => $conf->{domains}->{$domain}->{users}->{filter},
attrs => $fetch_attrs
);
if ( $ext_user_search->code ) {
handle_error( $domain, 'External LDAP user lookup', $ext_user_search->error );
next DOMAIN;
}
log_verbose( "Found " . scalar $ext_user_search->entries . " users in external LDAP" );
log_verbose( "Searching for users in Zimbra" );
# Search for Zimbra users, but exclude known system accounts
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->{domains}->{$domain}->{users}->{attr_map}->{$_} } keys $conf->{domains}->{$domain}->{users}->{attr_map} ),
( 'uid', 'zimbraAccountStatus', 'zimbraAuthLdapExternalDn', 'zimbraMailAlias', 'mail', 'zimbraNotes' )
]
);
if ( $zim_user_search->code ) {
handle_error( $domain, 'Zimbra users lookup', $zim_user_search->error );
next DOMAIN;
}
log_verbose( "Found " . scalar $zim_user_search->entries . " users in Zimbra" );
log_verbose( "Comparing the accounts" );
my $ext_users = ldap2hashref(
$ext_user_search,
$conf->{domains}->{$domain}->{users}->{key},
( $conf->{domains}->{$domain}->{users}->{mail_attr}, $conf->{domains}->{$domain}->{users}->{alias_attr} )
);
my $zim_users = ldap2hashref(
$zim_user_search,
'uid',
('mail')
);
# 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 ) {
my $attrs = '';
if ( defined $zim_users->{$user} ) {
# User exists in Zimbra, lets check its attribute are up to date
foreach my $attr ( keys $conf->{domains}->{$domain}->{users}->{attr_map} ) {
if ( not defined $ext_users->{$user}->{$attr} and not defined $zim_users->{$user}->{$conf->{domains}->{$domain}->{users}->{attr_map}->{$attr}} ) {
# Attr does not exist in external LDAP and in Zimbra, no need to continue comparing them
next;
}
if ( $conf->{domains}->{$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 in Zimbra
$attrs .= '-' . $conf->{domains}->{$domain}->{users}->{attr_map}->{$attr} . " '" . $zim_users->{$user}->{$conf->{domains}->{$domain}->{users}->{attr_map}->{$attr}} . "' ";
} elsif (
( $conf->{domains}->{$domain}->{users}->{attr_map}->{$attr} ne 'sn' and
$ext_users->{$user}->{$attr} ne ( $zim_users->{$user}->{$conf->{domains}->{$domain}->{users}->{attr_map}->{$attr}} || '' )
) ||
$conf->{domains}->{$domain}->{users}->{attr_map}->{$attr} eq 'sn' and
defined $ext_users->{$user}->{$attr} and
$ext_users->{$user}->{$attr} ne ( $zim_users->{$user}->{$conf->{domains}->{$domain}->{users}->{attr_map}->{$attr}} || '' )
) {
$attrs .= " " . $conf->{domains}->{$domain}->{users}->{attr_map}->{$attr} . " " . zim_attr_value( $ext_users->{$user}->{$attr} );
log_verbose( "Attribute $attr for user $user changed from " .
$zim_users->{$user}->{$conf->{domains}->{$domain}->{users}->{attr_map}->{$attr}} .
" to " .
$ext_users->{$user}->{$attr}
);
}
}
if (
not defined $zim_users->{$user}->{zimbraAuthLdapExternalDn} or
$zim_users->{$user}->{zimbraAuthLdapExternalDn} ne $ext_users->{$user}->{dn}
) {
$attrs .= " zimbraAuthLdapExternalDn " . zim_attr_value( $ext_users->{$user}->{dn} );
}
if ( $attrs ne '' ) {
# Some attribute must change, we need to update Zimbra
log_verbose( "User $user has changed in external LDAP, updating it" );
send_zmprov_cmd( "modifyAccount $user\@$domain $attrs" );
}
} else {
# User exists in external LDAP but not in Zimbra. We must create it
log_verbose( "User $user found in external LDAP but not in Zimbra. Will be created" );
foreach my $attr ( keys $conf->{domains}->{$domain}->{users}->{attr_map} ) {
next if (not defined $ext_users->{$user}->{$attr} or $ext_users->{$user}->{$attr} eq '');
$attrs .= ' ' . $conf->{domains}->{$domain}->{users}->{attr_map}->{$attr} . " " . zim_attr_value( $ext_users->{$user}->{$attr} );
}
# The password won't be used because Zimbra is set to use external LDAP/AD auth
# But better to set it to a random value
my $pass = $uuid->create_str;
send_zmprov_cmd( "createAccount $user\@$domain $pass $attrs" );
}
my @ext_aliases = ();
foreach my $mail_attr ( qw( mail_attr alias_attr ) ) {
next if (
not defined $conf->{domains}->{$domain}->{users}->{$mail_attr} or
not defined $ext_users->{$user}->{$conf->{domains}->{$domain}->{users}->{$mail_attr}}
);
push @ext_aliases, @{ $ext_users->{$user}->{$conf->{domains}->{$domain}->{users}->{$mail_attr}} };
}
@ext_aliases = sort @ext_aliases;
foreach my $alias ( @ext_aliases ) {
next if ( not alias_matches_domain( $alias, $domain_entry ) );
next if ( grep { $alias eq $_ } @{ $zim_users->{$user}->{mail} } );
log_verbose( "Creating alias $alias for user $user" );
send_zmprov_cmd( "addAccountAlias $user $alias" );
}
# On each sync, we register the list of LDAP aliases into Zimbra's LDAP in the zimbraNotes attribute
# We can compare if it has changed, and and/remove the aliases accordingly
# This is not very clean, but at least allows the script to be "stateless"
# and only relies on LDAP content on both sides
my $ext_prev_aliases = parse_zimbra_notes( $zim_users->{$user}->{zimbraNotes} || '' )->{LDAP_Aliases};
my @ext_prev_aliases = ( defined $ext_prev_aliases ) ? sort @{ $ext_prev_aliases } : ();
my $alias_diff = Array::Diff->diff( \@ext_prev_aliases, \@ext_aliases );
foreach my $alias ( @{ $alias_diff->deleted } ) {
my ( $al, $dom ) = split /\@/, $alias;
next if ( not defined $zim_aliases->{$dom} or not defined $zim_aliases->{$dom}->{$al} );
log_verbose( "Removing LDAP alias $alias from user $user as it doesn't exist in LDAP anymore" );
send_zmprov_cmd( "removeAccountAlias $user\@$domain $alias" );
}
my $note = $sync_from_ldap . "|LDAP_Aliases=" . join(',', @ext_aliases);
if ( $note ne ($zim_users->{$user}->{zimbraNotes} || '') ) {
send_zmprov_cmd( "modifyAccount $user\@$domain zimbraNotes " . zim_attr_value( $note ) );
}
}
# Second loop : we loop through the Zimbra users to check if they should be locked (if they don't exist in external LDAP anymore)
foreach my $user ( keys $zim_users ) {
# Make sure we only lock accounts if they don't exist anymore in external LDAP
# has the zimbraNotes attribute set, with the expected value, and the account is active
if ( not defined $ext_users->{$user} and
defined $zim_users->{$user}->{zimbraNotes} and
$zim_users->{$user}->{zimbraNotes} =~ m/^$sync_from_ldap/ and
defined $zim_users->{$user}->{zimbraAccountStatus} and
$zim_users->{$user}->{zimbraAccountStatus} =~ m/^active|lockout$/ ) {
log_verbose( "User $user doesn't exist in external LDAP anymore, locking it in Zimbra" );
send_zmprov_cmd( "modifyAccount $user\@$domain zimbraAccountStatus locked" );
}
}
# Now, we try to sync groups in external LDAP into distribution list in Zimbra
if ( defined $conf->{domains}->{$domain}->{groups} ) {
log_verbose( "Searching for potential groups in " .
$conf->{domains}->{$domain}->{groups}->{base} .
" matching filter " .
$conf->{domains}->{$domain}->{groups}->{filter}
);
$fetch_attrs = [ keys $conf->{domains}->{$domain}->{groups}->{attr_map} ];
push $fetch_attrs, $conf->{domains}->{$domain}->{groups}->{key};
push $fetch_attrs, $conf->{domains}->{$domain}->{groups}->{members_attr};
foreach ( qw( mail_attr alias_attr ) ) {
next if ( not defined $conf->{domains}->{$domain}->{groups}->{$_} );
push $fetch_attrs, $conf->{domains}->{$domain}->{groups}->{$_};
}
my $ext_group_search = $ext_ldap->search(
base => $conf->{domains}->{$domain}->{groups}->{base},
filter => $conf->{domains}->{$domain}->{groups}->{filter},
attrs => $fetch_attrs
);
if ( $ext_group_search->code ) {
handle_error( $domain, 'External LDAP groups lookup', $ext_group_search->error );
next DOMAIN;
}
log_verbose( "Found " . scalar $ext_group_search->entries . " groups in external LDAP" );
log_verbose( "Searching for distribution lists in Zimbra" );
# Search in Zimbra for distribution lists so we can compare with the groups in external LDAP
my $zim_dl_search = $zim_ldap->ldap->search(
base => 'ou=people,' . $domain_entry->{dn},
filter => "(objectClass=zimbraDistributionList)",
attrs => [
( map { $conf->{domains}->{$domain}->{groups}->{attr_map}->{$_} } keys $conf->{domains}->{$domain}->{groups}->{attr_map} ),
( 'uid', 'zimbraDistributionListSubscriptionPolicy', 'zimbraDistributionListUnsubscriptionPolicy',
'zimbraMailForwardingAddress', 'zimbraNotes', 'zimbraMailStatus', 'mail' )
]
);
if ( $zim_dl_search->code ) {
handle_error( $domain, 'Zimbra distribution lists lookup', $zim_dl_search->error );
next DOMAIN;
}
log_verbose( "Found " . scalar $zim_dl_search->entries . " distribution list(s) in Zimbra" );
log_verbose( "Comparing groups with distribution lists" );
my $ext_groups = ldap2hashref(
$ext_group_search,
$conf->{domains}->{$domain}->{groups}->{key},
( $conf->{domains}->{$domain}->{groups}->{members_attr},
$conf->{domains}->{$domain}->{groups}->{mail_attr},
$conf->{domains}->{$domain}->{groups}->{alias_attr}
)
);
my $zim_dl = ldap2hashref(
$zim_dl_search,
'uid',
('zimbraMailForwardingAddress', 'mail')
);
# Build a dn2id hashref to lookup users or groups by their DN
my $dn2id = {};
$dn2id->{$ext_users->{$_}->{dn}} = $_ foreach ( keys $ext_users );
$dn2id->{$ext_groups->{$_}->{dn}} = $_ foreach ( keys $ext_groups );
# First loop, check if every group in LDAP exist as a DL in Zimbra
foreach my $group ( keys $ext_groups ) {
if ( defined $zim_dl->{$group} ) {
# A group match an existing DL, we must check its attributes
my $attrs = '';
foreach my $attr ( keys $conf->{domains}->{$domain}->{groups}->{attr_map} ) {
if (
not defined $ext_groups->{$group}->{$attr} and
not defined $zim_dl->{$group}->{$conf->{domains}->{$domain}->{groups}->{attr_map}->{$attr}}
) {
# Attr does not exist in external LDAP and in Zimbra, not need to continue
next;
} elsif ( not defined $ext_groups->{$group}->{$attr} ) {
# Attr doesn't exist in external LDAP, but exists in Zimbra. We must remove it
$attrs = ' -' . $conf->{domains}->{$domain}->{groups}->{attr_map}->{$attr} . " " . zim_attr_value( $zim_dl->{$group}->{$conf->{domains}->{$domain}->{groups}->{attr_map}->{$attr}} );
} elsif ( $ext_groups->{$group}->{$attr} ne $zim_dl->{$group}->{$conf->{domains}->{$domain}->{groups}->{attr_map}->{$attr}} ) {
# Attr exists in both but doesn't match
$attrs .= " " . $conf->{domains}->{$domain}->{groups}->{attr_map}->{$attr} . " " . zim_attr_value( $ext_groups->{$group}->{$attr} );
log_verbose( $ext_groups->{$group}->{$attr} . " vs " . $zim_dl->{$group}->{$conf->{domains}->{$domain}->{groups}->{attr_map}->{$attr}} );
}
}
# Users cannot subscribe or unsubscribe from LDAP group
if (
not defined $zim_dl->{$group}->{zimbraDistributionListSubscriptionPolicy} or
$zim_dl->{$group}->{zimbraDistributionListSubscriptionPolicy} ne 'REJECT'
) {
$attrs .= " zimbraDistributionListSubscriptionPolicy REJECT";
}
if (
not defined $zim_dl->{$group}->{zimbraDistributionListUnsubscriptionPolicy} or
$zim_dl->{$group}->{zimbraDistributionListUnsubscriptionPolicy} ne 'REJECT'
) {
$attrs .= " zimbraDistributionListUnsubscriptionPolicy REJECT";
}
# If the group in LDAP has a mail defined, enable mail delivery in Zimbra. Else, disable it
my $mail_status = ( defined $ext_groups->{$group}->{$conf->{domains}->{$domain}->{groups}->{mail_attr}} ) ? 'enabled' : 'disabled';
if (
not defined $zim_dl->{$group}->{zimbraMailStatus} or
$zim_dl->{$group}->{zimbraMailStatus} ne $mail_status
) {
$attrs .= " zimbraMailStatus $mail_status";
}
if ( $attrs ne '' ) {
# Some attribute must change, lets update Zimbra
log_verbose( "Group $group has changed in external LDAP, updating it" );
send_zmprov_cmd( "modifyDistributionList $group\@$domain $attrs" );
}
} else {
# A new group with no corresponding DL in Zimbra
log_verbose( "Found a new group : $group. Creating it in Zimbra" );
my $attrs = '';
foreach my $attr ( keys $conf->{domains}->{$domain}->{groups}->{attr_map} ) {
next if (not defined $ext_groups->{$group}->{$attr} or $ext_groups->{$group}->{$attr} eq '');
$attrs .= ' ' . $conf->{domains}->{$domain}->{groups}->{attr_map}->{$attr} . " " . zim_attr_value( $ext_groups->{$group}->{$attr} );
}
$attrs .= " zimbraDistributionListUnsubscriptionPolicy REJECT zimbraDistributionListSubscriptionPolicy REJECT";
my $mail_status = ( defined $ext_groups->{$group}->{$conf->{domains}->{$domain}->{groups}->{mail_attr}} ) ? 'enabled' : 'disabled';
$attrs .= " zimbraMailStatus $mail_status";
send_zmprov_cmd( "createDistributionList $group\@$domain $attrs" );
}
# Now that all the needed groups exist as distribution list, we need to handle membership
# For that, we must convert the membership list of the external group to the same format as Zimbra
my @ext_members = ();
if ( not yaml_bool( $conf->{domains}->{$domain}->{groups}->{members_as_dn} ) ) {
foreach my $member ( $ext_groups->{$group}->{$conf->{domains}->{$domain}->{groups}->{members_attr}} ) {
next if ( not defined $ext_users->{$member} and not defined $ext_groups->{$member} );
push @ext_members, $member . '@' . $domain;
}
} else {
foreach my $member ( @{ $ext_groups->{$group}->{$conf->{domains}->{$domain}->{groups}->{members_attr}} } ) {
next if not defined $dn2id->{$member};
push @ext_members, $dn2id->{$member} . '@' . $domain;
}
}
@ext_members = sort @ext_members;
my @zim_members = (defined $zim_dl->{$group}->{zimbraMailForwardingAddress} ) ? sort @{$zim_dl->{$group}->{zimbraMailForwardingAddress}} : ();
my $diff = Array::Diff->diff( \@ext_members, \@zim_members );
if ( scalar @{ $diff->deleted } gt 0 ){
send_zmprov_cmd( "addDistributionListMember $group\@$domain " . join (' ', @{ $diff->deleted } ) );
}
if ( scalar @{ $diff->added } gt 0 ) {
send_zmprov_cmd( "removeDistributionListMember $group\@$domain $_ ") foreach ( @{ $diff->added } );
}
my @ext_aliases = ();
foreach my $mail_attr ( qw( mail_attr alias_attr ) ) {
next if (not defined $conf->{domains}->{$domain}->{groups}->{$mail_attr} or not defined $ext_groups->{$group}->{$conf->{domains}->{$domain}->{groups}->{$mail_attr}} );
push @ext_aliases, @{ $ext_groups->{$group}->{$conf->{domains}->{$domain}->{groups}->{$mail_attr}} };
}
foreach my $alias ( @ext_aliases ) {
next if ( not alias_matches_domain( $alias, $domain_entry ) );
next if ( grep { $alias eq $_ } @{ $zim_dl->{$group}->{mail} } );
log_verbose( "Creating alias $alias for group $group" );
send_zmprov_cmd( "addDistributionListAlias $group\@$domain $alias" );
}
# Now, check the diff between the list of LDAP alias for this users with the previous run
my $ext_prev_aliases = parse_zimbra_notes( $zim_dl->{$group}->{zimbraNotes} || '' )->{LDAP_Aliases};
my @ext_prev_aliases = ( defined $ext_prev_aliases ) ? sort @{ $ext_prev_aliases } : ();
my $alias_diff = Array::Diff->diff( \@ext_prev_aliases, \@ext_aliases );
foreach my $alias ( @{ $alias_diff->deleted } ) {
my ( $al, $dom ) = split /\@/, $alias;
next if ( not defined $zim_aliases->{$dom} or not defined $zim_aliases->{$dom}->{$al} );
log_verbose( "Removing LDAP alias $alias from distribution list $group as it doesn't exist in LDAP anymore" );
send_zmprov_cmd( "removeDistributionListAlias $group\@$domain $alias" );
}
my $note = $sync_from_ldap . "|LDAP_Aliases=" . join(',', @ext_aliases);
if ( $note ne ($zim_dl->{$group}->{zimbraNotes} || '') ) {
send_zmprov_cmd( "modifyDistributionList $group\@$domain zimbraNotes " . zim_attr_value( $note ) );
}
}
# Now, look at all the distribution list which were created from LDAP but doesn't exist anymore in LDAP
foreach my $dl ( keys $zim_dl ) {
next if ( not defined $zim_dl->{$dl}->{zimbraNotes} or $zim_dl->{$dl}->{zimbraNotes} !~ m/^$sync_from_ldap/ );
next if ( defined $ext_groups->{$dl} );
log_verbose( "Group $dl doesn't exist in LDAP anymore, removing the corresponding distribution list" );
send_zmprov_cmd( "deleteDistributionList $dl\@$domain" );
}
}
}
# zmprov breaks terminal (no echo to your input after execution)
# fix it with a tset
system('tset');
# Exit with the global exit code (if at least one domain had an error, it'll be != 0)
exit $exit;
###### Subroutines ######
# Print usage
sub usage {
print <<_EOF;
Usage: $0 [ --config /path/to/config.yml ] [ --dry-run ] [ --quiet ] [ --verbose ]
With:
* -c|--config : designate the path of the config file to use. Default is /opt/zimbra/conf/ldap_sync.yml
* -d|--dry-run : do not change anything, only prints what would be done
* -q|--quiet : No output except if there are errors
* -v|--verbose : extra output (not only if something is to be updated in Zimbra)
_EOF
}
# Print messages only if the verbose flag was given
sub log_verbose {
my $msg = shift;
print $msg . "\n" if ( $opt->{verbose} );
}
# Print info messages unless the quiet flag was given
sub log_info {
my $msg = shift;
print $msg . "\n" if ( not $opt->{quiet} );
}
# Print errors
sub log_error {
my $msg = shift;
print $msg . "\n";
}
# Just a helper to handle error. Will print the error
# send an email to the admin if nedded, and set an exit code
sub handle_error {
my $domain = shift;
my $step = shift;
my $err = shift;
log_error( $err );
if ( defined $conf->{general}->{notify}->{to} ) {
my $mail = Email::MIME->create(
header_str => [
From => $conf->{general}->{notify}->{from},
To => $addr,
Subject => "Zimbra LDAP synchronisation error for domain $domain"
],
attributes => {
charset => 'utf-8',
encoding => 'base64'
},
body_str => "LDAP synchronisation for domain $domain failed at step '$step'. The error was\n$err\n",
);
my $transport = Email::Sender::Transport::Sendmail->new({ sendmail => '/opt/zimbra/common/sbin/sendmail' });
sendmail( $mail, { transport => $transport } );
}
$exit = 255;
}
# ldap2hashref takes three args
# * An LDAP search result
# * The attribute used as the key of objects
# * an optional array of attributes we want as an array, even if there's a single value
# It'll return a hashref. The key will be unaccentuated and lower cased.
sub ldap2hashref {
my ( $search, $key, @want_array ) = @_;
my $return = {};
foreach my $entry ( $search->entries ) {
$return->{unidecode( lc $entry->get_value($key) )}->{dn} = $entry->dn;
foreach my $attr ( $entry->attributes ) {
my @values = $entry->get_value($attr);
$return->{unidecode( lc $entry->get_value($key) )}->{$attr} = ( scalar @values == 1 ) ?
( grep { $attr eq $_ } @want_array ) ? \@values : $values[0] :
\@values;
}
}
return $return;
}
# Check YAML bool, and return either 1 or 0
sub yaml_bool {
my $bool = shift;
if ( $bool =~ m/^y|yes|true|1|on$/i ) {
return 1;
} else {
return 0;
}
}
# Build a string to pass to zmprov to configure a domain
# Takes the domain conf hashref as only arg
sub build_domain_attrs {
my $domain_conf = shift;
my $attrs = "zimbraAuthMech " . zim_attr_value( $domain_conf->{ldap}->{type} );
$attrs .= " zimbraAuthMechAdmin " . zim_attr_value( $domain_conf->{ldap}->{type} );
if ( defined $domain_conf->{ldap}->{bind_dn} and defined $domain_conf->{ldap}->{bind_pass} ) {
$attrs .= " zimbraAuthLdapSearchBindDn " . zim_attr_value( $domain_conf->{ldap}->{bind_dn} );
$attrs .= " zimbraAuthLdapSearchBindPassword " . zim_attr_value( $domain_conf->{ldap}->{bind_pass} );
}
# if ( defined $domain_conf->{users}->{filter} ) {
# $attrs = " zimbraAuthLdapSearchFilter " . zim_attr_value( "(&(|(" . $domain_conf->{users}->{key} . "=%u)(" . $domain_conf->{users}->{key} . "=%n))(" . $domain_conf->{users}->{filter} . ")" );
# }
$attrs .= " zimbraAuthLdapURL " . join( ' +zimbraAuthLdapURL', zim_attr_value( $domain_conf->{ldap}->{servers} ) );
if ( defined $domain_conf->{ldap}->{start_tls} and yaml_bool( $domain_conf->{ldap}->{start_tls} ) ) {
$attrs .= " zimbraAuthLdapStartTlsEnabled TRUE";
} else {
$attrs .= " zimbraAuthLdapStartTlsEnabled FALSE";
}
return $attrs;
}
# Takes a domain name as arg and return its DN in Zimbra LDAP directory
sub domain2dn {
my $domain = shift;
$domain =~ s/\./,dc=/g;
return 'dc=' . $domain;
}
# Prepare a string to be used as arg to zmprov
sub zim_attr_value {
my $value = shift;
utf8::encode($value);
return shell_quote($value);
}
# Take an alias and a domain. Return 1 if the alias is member of this domain (or one of the domain aliases)
sub alias_matches_domain {
my $alias = shift;
my $domain = shift;
return 1 if ( $alias =~ m/\@$domain->{zimbraDomainName}$/ );
foreach my $dom ( @{ $domain->{zimbraDomainAliases} } ) {
return 1 if ( $alias =~ m/\@$dom$/ );
}
return 0;
}
# Send a command to zmprov
sub send_zmprov_cmd {
my $cmd = shift;
log_info( "Sending command zmprov " . $cmd );
if ( not $opt->{dry} ) {
ZmClient::sendZmprovRequest( $cmd );
}
}
# Parse the zimbraNotes field and return the content as a hashref
sub parse_zimbra_notes {
my $notes = shift;
my $return = {};
return $return if ( $notes !~ m/^$sync_from_ldap/ );
$notes =~ s/^$sync_from_ldap//;
foreach my $rec ( split /\|/, $notes ) {
next if not ( $rec =~ m/^([^=\|]+)=(([^,\|]+,?)+)/ );
my ( $key, $values ) = ( $1, $2 );
$return->{$key} = [ split /,/, $values ];
}
return $return;
}
# Set default config values if missing
sub get_default_conf {
my $conf = shift;
my $default = {};
if ( $conf->{ldap}->{schema} eq 'ad' ) {
$defaults = {
ldap => {
type => 'ad',
start_tls => 1
},
users => {
filter => '(&(objectClass=user)(mail=*))',
key => 'sAMAccountName',
mail_attr => 'mail',
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 => {
filter => '(objectClass=group)',
key => 'cn',
members_attr => 'member',
members_as_dn => 1,
mail_attr => 'mail',
alias_attr => 0,
attr_map => {
displayName => 'displayName',
description => 'description'
}
}
};
} elsif ( $conf->{ldap}->{schema} eq 'rfc2307bis' ) {
$defaults = {
ldap => {
type => 'ldap',
start_tls => 1
},
users => {
filter => '(&(objectClass=inetOrgPerson)(mail=*))',
key => 'uid',
mail_attr => 'mail',
alias_attr => 'mail',
attr_map => {
displayName => 'displayName',
description => 'description',
cn => 'cn',
sn => 'sn',
givenName => 'givenName',
telephoneNumber => 'telephoneNumber',
mobile => 'mobile',
streetAddress => 'street',
l => 'l',
street => 'st',
title => 'title',
o => 'company'
}
},
groups => {
filter => '(objectClass=groupOfNames)',
key => 'cn',
members_attr => 'member',
members_as_dn => 1,
mail_attr => 0,
alias_attr => 0,
attr_map => {
displayName => 'displayName',
description => 'description'
}
}
};
} elsif ( $conf->{ldap}->{schema} eq 'rfc2307' ) {
$defaults = {
ldap => {
type => 'ldap',
start_tls => 1
},
users => {
filter => '(&(objectClass=inetOrgPerson)(mail=*))',
key => 'uid',
mail_attr => 'mail',
alias_attr => 'mail',
attr_map => {
displayName => 'displayName',
description => 'description',
cn => 'cn',
sn => 'sn',
givenName => 'givenName',
telephoneNumber => 'telephoneNumber',
mobile => 'mobile',
streetAddress => 'street',
l => 'l',
street => 'st',
title => 'title',
o => 'company'
}
},
groups => {
filter => '(objectClass=posixGroup)',
key => 'cn',
members_attr => 'memberUid',
members_as_dn => 0,
mail_attr => 0,
alias_attr => 0,
attr_map => {
displayName => 'displayName',
description => 'description'
}
}
};
}
return merge $defaults, $conf;
}

12
ldap_sync/ldap_sync.yml Normal file
View File

@ -0,0 +1,12 @@
---
# General settings, which affect all domain you sync
# general:
# notify:
# from: zimbra@example.org
# to: admin@acme-corp.biz
# Now, define the list of domain to sync
# and for each of them, the settings. See README.md for examples
domains: {}

View File

@ -1,270 +0,0 @@
#!/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;
}

View File

@ -1,46 +0,0 @@
---
#.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