Re: How to integrate E.F.A with Active Directory on 3.0.0.9
Posted: 23 Mar 2017 23:51
Hang tight, 3.0.1.9 is around the corner and will have substantial changes to MailWatch that could affect your script (hopefully in a good way).
https://forum.efa-project.org/
Code: Select all
#!/usr/bin/python3
# Stuart.Smith@FosterFuels.com
# v1.1 03/17/2017
#
# TODO:
# add publicDelegates property to user query, parse, and add results to filters
# TODO:
# check for duplicate GUID before insterting into users, if guid is found, rename and update
# attributes rather than insertin the user. Don't forget to add the "old" user address
# as a filter once the user attributes have been updated
# TODO:
# delete users in mailscanner no longer in Active Directory
#
# HISTORY:
# v1.1
# added publicDelegates to search and filters
# moved configuration to variables
# began using 'is list' tests rather than len(value[0])
#
ldap_URI = 'ldap://'
ldap_user = 'cn=,cn=Users,dc=,dc='
ldap_secret = ''
ldap_base_DN = 'DC=,DC='
ldap_user_DN = 'CN=Users'
ldap_user_filter = '(&(objectClass=user)(objectCategory=person)(proxyAddresses=*))'
ldap_group_DN = 'CN=Users'
ldap_group_filter = '(objectClass=group)'
mysql_host = "localhost"
mysql_user = ""
mysql_secret = ""
mysql_db = "mailscanner"
import ldap3
from ldap3 import Server, Connection, Tls
import ssl
import pymysql
class ADData(object):
def __init__(self):
self.groups = {}
self.users = {}
self.gUIDs = {}
class ADUser(object):
def __init__(self):
self.displayName = ''
self.DN = ''
self.filters = []
self.sMTPAddress = ''
self.objectGUID = ''
self.delegates = []
class ADGroup(object):
def __init__(self):
self.displayName = ''
self.DN = ''
self.members = {}
self.objectGUID = ''
self.sMTPaddress = ''
#class to store stuff
aDdata = ADData()
tls_configuration = Tls(validate=ssl.CERT_NONE, version=ssl.PROTOCOL_TLSv1)
server = Server(
ldap_URI,
get_info='ALL', use_ssl=True, tls=tls_configuration)
#connect anonymously first
conn = Connection(server)
connUser = Connection(server)
conn.open()
#print(conn)
#make sure that tls is started
conn.bind()
conn.start_tls()
conn.rebind(user=ldap_user, password=ldap_secret)
#print(conn)
#print(server.info)
# search for users first
conn.search(
ldap_user_DN + ',' + ldap_base_DN,
ldap_user_filter,
attributes=['cn','displayName','sAMAccountName','proxyAddresses','mail','objectGUID','publicDelegates'])
for entry in conn.entries:
user = ADUser()
user.DN = "CN=" + (str(entry['cn'].value) + (ldap_user_DN + ',' + ldap_base_DN))
user.displayName = str(entry['displayName'].value)
user.sAMAccountName = str(entry['sAMAccountName'].value)
user.objectGUID = str(entry['objectGUID'].value)
user.sMTPAddress = str(entry['mail'].value)
#check to see if we havea string value of list value
# this is horrible, but can't use 'is list' on LDAP attributes
if len(entry['proxyAddresses'].value[0]) == 1:
if entry['proxyAddresses'].value.upper().startswith('SMTP:'):
print ("SINGLE: mail: {0} proxyAddress: {1}".format(user.sMTPAddress.upper(),(entry['proxyAddresses'].value[entry['proxyAddresses'].value.index(':')+1:]).upper()))
#interested in this, strip off the SMTP: and add to list
if ((entry['proxyAddresses'].value[entry['proxyAddresses'].value.index(':')+1:]).upper() == user.sMTPAddress.upper()):
print ("\t\t\tfound a match! do NOT add a filter for self");
else:
user.filters.append(entry['proxyAddresses'].value[entry['proxyAddresses'].value.index(':')+1:])
#endif
#endif
else:
values = entry['proxyAddresses'].value
if (values):
#print ("\tFound {0} addresses".format(len(values)))
for i in range(0, len(values)):
#check case insensitive for 'smtp'
# discard values that do not match, we
# aren't interested in X400 or X500
# addresses
#print ("\tChecking {0} for SMTP: match".format(values[i].upper()))
if values[i].upper().startswith('SMTP:'):
print ("LIST: mail: {0} proxyAddress: {1}".format(user.sMTPAddress,values[i][values[i].index(':')+1:]))
if ((values[i][values[i].index(':')+1:]).upper() == user.sMTPAddress.upper()):
print ("\t\t\tfound a match! do NOT add a filter for self");
else:
#interested in this, strip off the SMTP: and add to list
user.filters.append(values[i][values[i].index(':')+1:])
#endif
#endif
#next
#endif
#endif
#print(type(entry['publicDelegates'].value))
if not entry['publicDelegates'].value is None:
if not type(entry['publicDelegates'].value) is list:
user.delegates.append(str(entry['publicDelegates']))
else:
delegates = entry['publicDelegates'].value
for i in range(0, len(delegates)):
user.delegates.append(str(delegates[i]))
#end if
#user.sMTPAddress = user.filters[0]
aDdata.users[user.DN] = user
aDdata.gUIDs[user.objectGUID] = user
#endfor
##print(aDdata.users.keys())
conn.search(
ldap_group_DN + ',' + ldap_base_DN,
ldap_group_filter,
attributes=['displayName','mail','member','objectGUID'])
for entry in conn.entries:
#print(entry)
group = ADGroup()
#dump attributes
## print(entry["displayName"].value)
## print(entry['mail'].value)
if (entry["displayName"].value == None):
continue
group.displayName = str(entry['displayName'].value)
group.sMTPAddress = str(entry['mail'].value)
#print(entry['member'])
#retrieve all the CNs of group members
values = entry['member'].value
#if the group is not empty, iterate all members
if (values):
#check to see if this is a list of string value
if (len(values[0]) == 1):
#single entry
group.members[(str(values[0].value))] = aDdata.users[(str(values[i]))]
else:
for i in range(0,len(values)):
if (values[0] == None):
continue
if str(values[i]) in aDdata.users.keys():
## print (aDdata.users[(str(values[i]))].DN)
group.members[(str(values[i]))] = aDdata.users[(str(values[i]))]
group.members[(str(values[i]))].filters.append(group.sMTPAddress)
#endif
#endif
aDdata.groups[group.displayName] = group
aDdata.gUIDs[group.objectGUID] = group
#endfor
conn.unbind()
db = pymysql.connect(mysql_host,mysql_user,mysql_secret,mysql_db)
cursor = db.cursor()
for k in aDdata.users.keys():
user = aDdata.users[k]
print (user.displayName)
print (user.objectGUID)
print (user.filters)
#post-processing to add delegates to filters now that all the users are in the list
# added here so we only loop through users once
if (not user.delegates is None) and (len(user.delegates) != 0):
print(type(user.delegates))
if not type(user.delegates) is list:
print("{0} has delegate {1}\n".format(user.displayName, user.delegates))
if user.delegates in aDdata.users.keys():
print("\t{0} found for delegate {1}\n".format(aDdata.users[user.delegates].sMTPAddress, user.delegates))
aDdata.users[user.delegates].filters.append(user.sMTPAddress)
else:
print("\tNo address found for delegate {0}\n".format(user.delegates))
#end if
else:
for delegate in user.delegates:
print("{0} has delegate {1}\n".format(user.displayName, delegate))
if delegate in aDdata.users.keys():
print("\t{0} found for delegate {1}\n".format(aDdata.users[delegate].sMTPAddress, delegate))
aDdata.users[delegate].filters.append(user.sMTPAddress)
else:
print("\tNo address found for delegate {0}\n".format(delegate))
#end if
#end for
#end fi
#end if
cursor.execute("BEGIN;")
try:
cursor.execute("""
INSERT INTO users
( username, fullname, type )
VALUES
( '{0}', '{1}', 'U' )
ON DUPLICATE KEY UPDATE
guid = '{2}';
""".format(user.sMTPAddress, user.displayName, user.objectGUID))
#cursor.execute("COMMIT;")
except:
cursor.execute("ROLLBACK;")
raise
else:
cursor.execute("COMMIT;")
cursor.execute("BEGIN;")
try:
cursor.execute("DELETE FROM user_filters WHERE username = '{0}' AND manual = 'N';".format(user.sMTPAddress))
for mailFilter in user.filters:
cursor.execute("INSERT INTO user_filters ( username, filter, active, manual ) VALUES ( '{0}', '{1}', 'Y', 'N' );".format(user.sMTPAddress, mailFilter))
#endfor
cursor.execute("COMMIT;")
except:
cursor.execute("ROLLBACK;")
raise
else:
cursor.execute('COMMIT;')
Code: Select all
yum install python34
yum install python34-setuptools
easy_install-3.4 pip
pip3.4 install ldap3
/usr/bin/pip3.4 install PyMySQL
mysql mailscanner -u root -p
ALTER TABLE users ADD COLUMN guid varchar(36);
ALTER TABLE user_filters ADD COLUMN manual enum('N','Y') default 'Y';
Code: Select all
Traceback (most recent call last):
File "/usr/bin/ldap-userimport-mailscanner-manit", line 99, in <module>
attributes=['cn','displayName','sAMAccountName','proxyAddresses','mail','objectGUID','publicDelegates'])
File "/usr/lib/python3.4/site-packages/ldap3/core/connection.py", line 763, in search
self.server.schema if self.server else None)
File "/usr/lib/python3.4/site-packages/ldap3/operation/search.py", line 363, in search_operation
request['filter'] = compile_filter(parse_filter(search_filter, schema, auto_escape, auto_encode).elements[0]) # parse the searchFilter string and compile it starting from the root node
File "/usr/lib/python3.4/site-packages/ldap3/operation/search.py", line 213, in parse_filter
raise LDAPInvalidFilterError('invalid filter')
ldap3.core.exceptions.LDAPInvalidFilterError: invalid filter
Yikes! Sorry for the delay in response! I've been pulling cable in a new office and lots of construction going on so I've been negligent in checking the forums.r31griffo wrote: ↑09 May 2017 05:50 Very nice script!
I'd like to request a couple of feature changes though (if possible), I'd have a go myself but I'm totally lost with Python.
I have a few distribution lists that would be nice to automatically import, I tried modifying the filter to include all members of a group but the script errored out (I'd imagine groups would have to be handled separately (ie if object is a group):The other thing that comes to mind would be a variable to toggle LDAP vs LDAPS and port.Code: Select all
Traceback (most recent call last): File "/usr/bin/ldap-userimport-mailscanner-manit", line 99, in <module> attributes=['cn','displayName','sAMAccountName','proxyAddresses','mail','objectGUID','publicDelegates']) File "/usr/lib/python3.4/site-packages/ldap3/core/connection.py", line 763, in search self.server.schema if self.server else None) File "/usr/lib/python3.4/site-packages/ldap3/operation/search.py", line 363, in search_operation request['filter'] = compile_filter(parse_filter(search_filter, schema, auto_escape, auto_encode).elements[0]) # parse the searchFilter string and compile it starting from the root node File "/usr/lib/python3.4/site-packages/ldap3/operation/search.py", line 213, in parse_filter raise LDAPInvalidFilterError('invalid filter') ldap3.core.exceptions.LDAPInvalidFilterError: invalid filter
Code: Select all
#!/usr/bin/perl -T -w
# This script will pull all users' SMTP addresses from your Active Directory
# (including primary and secondary email addresses) and list them in the
# format "user@example.com OK" which Postfix uses with relay_recipient_maps.
# Be sure to double-check the path to perl above.
# This requires Net::LDAP to be installed. To install Net::LDAP, at a shell
# type "perl -MCPAN -e shell" and then "install Net::LDAP"
use Net::LDAP;
use Net::LDAP::Control::Paged;
use Net::LDAP::Constant ( "LDAP_CONTROL_PAGED" );
# Enter the path/file for the output
$VALID = "/etc/postfix/ldap_recipients";
open VALID, ">$VALID" or die "CANNOT OPEN $VALID $!";
# Enter the FQDN of your Active Directory domain controllers below
$dc1="ip_ad or fqdn";
$dc2="ip_ad or fqdn";
# Enter the LDAP container for your userbase.
# The syntax is CN=Users,dc=example,dc=com
# This can be found by installing the Windows 2000 Support Tools
# then running ADSI Edit.
# In ADSI Edit, expand the "Domain NC [domaincontroller1.example.com]" &
# you will see, for example, DC=example,DC=com (this is your base).
# The Users Container will be specified in the right pane as
# CN=Users depending on your schema (this is your container).
# You can double-check this by clicking "Properties" of your user
# folder in ADSI Edit and examining the "Path" value, such as:
# LDAP://domaincontroller1.example.com/CN=Users,DC=example,DC=com
# which would be $hqbase="cn=Users,dc=example,dc=com"
# Note: You can also use just $hqbase="dc=example,dc=com"
$hqbase="dc=contoso,dc=com";
# Enter the username & password for a valid user in your Active Directory
# with username in the form cn=username,cn=Users,dc=example,dc=com
# Make sure the user's password does not expire. Note that this user
# does not require any special privileges.
# You can double-check this by clicking "Properties" of your user in
# ADSI Edit and examining the "Path" value, such as:
# LDAP://domaincontroller1.example.com/CN=user,CN=Users,DC=example,DC=com
# which would be $user="cn=user,cn=Users,dc=example,dc=com"
# Note: You can also use the UPN login: "user\@example.com"
$user="userad\@contoso.com";
$passwd="password";
# Connecting to Active Directory domain controllers
$noldapserver=0;
$ldap = Net::LDAP->new($dc1) or
$noldapserver=1;
if ($noldapserver == 1) {
$ldap = Net::LDAP->new($dc2) or
die "Error connecting to specified domain controllers $@ \n";
}
$mesg = $ldap->bind ( dn => $user,
password =>$passwd);
if ( $mesg->code()) {
die ("error:", $mesg->error_text((),"\n"));
}
# How many LDAP query results to grab for each paged round
# Set to under 1000 for Active Directory
$page = Net::LDAP::Control::Paged->new( size => 990 );
@args = ( base => $hqbase,
# Play around with this to grab objects such as Contacts, Public Folders, etc.
# A minimal filter for just users with email would be:
# filter => "(&(sAMAccountName=*)(mail=*))"
filter => "(& (mailnickname=*) (| (&(objectCategory=person)
(objectClass=user)(!(homeMDB=*))(!(msExchHomeServerName=*)))
(&(objectCategory=person)(objectClass=user)(|(homeMDB=*)
(msExchHomeServerName=*)))(&(objectCategory=person)(objectClass=contact))
(objectCategory=group)(objectCategory=publicFolder) ))",
control => [ $page ],
attrs => "proxyAddresses",
);
my $cookie;
while(1) {
# Perform search
my $mesg = $ldap->search( @args );
# Filtering results for proxyAddresses attributes
foreach my $entry ( $mesg->entries ) {
my $name = $entry->get_value( "cn" );
# LDAP Attributes are multi-valued, so we have to print each one.
foreach my $mail ( $entry->get_value( "proxyAddresses" ) ) {
# Test if the Line starts with one of the following lines:
# proxyAddresses: [smtp|SMTP]:
# and also discard this starting string, so that $mail is only the
# address without any other characters...
if ( $mail =~ s/^smtp://igs ) {
print VALID lc($mail)." OK\n";
}
}
}
# Only continue on LDAP_SUCCESS
$mesg->code and last;
# Get cookie from paged control
my($resp) = $mesg->control( LDAP_CONTROL_PAGED ) or last;
$cookie = $resp->cookie or last;
# Set cookie in paged control
$page->cookie($cookie);
}
if ($cookie) {
# We had an abnormal exit, so let the server know we do not want any more
$page->cookie($cookie);
$page->size(0);
$ldap->search( @args );
# Also would be a good idea to die unhappily and inform OP at this point
die("LDAP query unsuccessful");
}
$ldap->unbind;
# Add additional restrictions, users, etc. to the output file below.
#print VALID "user\@domain1.com OK\n";
#print VALID "user\@domain2.com 550 User unknown.\n";
#print VALID "domain3.com 550 User does not exist.\n";
close VALID;
Code: Select all
relay_recipient_maps = hash:/etc/postfix/ldap_recipients
Code: Select all
postmap /etc/postfix/ldap_recipients
Code: Select all
if ((defined('LDAP_SSL') && LDAP_SSL === true)) {
ldap_start_tls($ds) or die(ldap_print_error($ds));
}
Code: Select all
// LDAP settings for authentication
define('USE_LDAP', true);
define('LDAP_SSL', false); // Set to true if using LDAP with SSL encryption.
define('LDAP_HOST', '10.10.50.10');
define('LDAP_PORT', '7389');
define('LDAP_DN', 'dc=mydomain,dc=lan');
define('LDAP_USER', 'uid=rouser,cn=users,dc=mydomain,dc=lan'); // If no email set: cn=admin,dc=example,dc=com
define('LDAP_PASS', '10ngc0nvolut3dpa33w0rd');
define('LDAP_FILTER', 'mailPrimaryAddress=%s');
define('LDAP_PROTOCOL_VERSION', 3);
define('LDAP_EMAIL_FIELD', 'mailPrimaryAddress');
// Ldap field that is used to bind to the ldap server to check the credentials.
// The value of the LDAP_USERNAME_FIELD will be extended by LDAP_BIND_PREFIX and LDAP_BIND_SUFFIX to created the binding username.
define('LDAP_USERNAME_FIELD', 'mailPrimaryAddress');
// define('LDAP_BIND_PREFIX', 'cn=');
// define('LDAP_BIND_SUFFIX', ',' . LDAP_DN);
// Microsoft Active Directory compatibility support for searches from Domain Base DN
define('LDAP_MS_AD_COMPATIBILITY', true);
Code: Select all
/etc/postfix/ldap_maps_domain.cf
[root@efa log]# cat /etc/postfix/ldap_maps_domain.cf
domain = mydomain.com
server_host = 10.10.50.10:7389
search_base = DC=mydomain,DC=lan
bind = yes
bind_dn = uid=rouser,cn=users,dc=mydomain,dc=lan
bind_pw = 10ngc0nvolut3dpa33w0rd
query_filter = (|(mail=%s)(mailPrimaryAddress=%s))
leaf_result_attribute = mail
version = 3
[root@efa log]#