Add authentication against Active Directory

This commit is contained in:
C. R. Oldham 2015-03-25 12:49:35 -06:00
parent 8977ed20c7
commit 7421042dd2
3 changed files with 137 additions and 28 deletions

View File

@ -98,14 +98,15 @@ User authentication does not need to be entered again until the token expires.
Token expiration time can be set in the Salt master config file.
LDAP
----
LDAP and Active Directory
-------------------------
Salt supports both user and group authentication for LDAP.
Salt supports both user and group authentication for LDAP (and Active Directory
accessed via its LDAP interface)
LDAP configuration happens in the Salt master configuration file.
Server configuration values:
Server configuration values and their defaults:
.. code-block:: yaml
@ -113,6 +114,17 @@ Server configuration values:
auth.ldap.port: 389
auth.ldap.tls: False
auth.ldap.scope: 2
auth.ldap.uri: ''
auth.ldap.tls: False
auth.ldap.no_verify: False
auth.ldap.anonymous: False
auth.ldap.groupou: 'Groups'
auth.ldap.groupclass: 'posixGroup'
auth.ldap.accountattributename: 'memberUid'
# These are only for Active Directory
auth.ldap.activedirectory: False
auth.ldap.persontype: 'person'
Salt also needs to know which Base DN to search for users and groups and
the DN to bind to:
@ -128,21 +140,51 @@ To bind to a DN, a password is required
auth.ldap.bindpw: mypassword
Salt uses a filter to find the DN associated with a user. Salt substitutes
the ``{{ username }}`` value for the username when querying LDAP.
Salt uses a filter to find the DN associated with a user. Salt
substitutes the ``{{ username }}`` value for the username when querying LDAP
.. code-block:: yaml
auth.ldap.filter: uid={{ username }}
If group support for LDAP is desired, one can specify an OU that contains group
data. This is prepended to the basedn to create a search path
For OpenLDAP, to determine group membership, one can specify an OU that contains
group data. This is prepended to the basedn to create a search path. Then
the results are filtered against ``auth.ldap.groupclass``, default
``posixGroup``, and the account's 'name' attribute, ``memberUid`` by default.
.. code-block:: yaml
auth.ldap.groupou: Groups
Once configured, LDAP permissions can be assigned to users and groups.
Active Directory handles group membership differently, and does not utilize the
``groupou`` configuration variable. AD needs the following options in
the master config:
.. code-block:: yaml
auth.ldap.activedirectory: True
auth.ldap.filter: sAMAccountName={{username}}
auth.ldap.accountattributename: sAMAccountName
auth.ldap.groupclass: group
auth.ldap.persontype: person
To determine group membership in AD, the username and password that is entered
when LDAP is requested as the eAuth mechanism on the command line is used to
bind to AD's LDAP interface. If this fails, then it doesn't matter what groups
the user belongs to, he or she is denied access. Next, the distinguishedName
of the user is looked up with the following LDAP search:
(&(<value of auth.ldap.accountattributename>={{username}})
(objectClass=<value of auth.ldap.persontype>)
)
This should return a distinguishedName that we can use to filter for group
membership. Then the following LDAP quey is executed:
(&(member=<distinguishedName from search above>)
(objectClass=<value of auth.ldap.groupclass>)
)
.. code-block:: yaml

View File

@ -92,8 +92,8 @@ class LoadAuth(object):
return self.auth[fstr](*fcall['args'], **fcall['kwargs'])
else:
return self.auth[fstr](*fcall['args'])
except Exception:
err = 'Authentication module threw an exception. Exception not logged.'
except Exception as e:
log.debug('Authentication module threw {0}'.format(e))
return False
def time_auth(self, load):

View File

@ -32,7 +32,11 @@ __defopts__ = {'auth.ldap.uri': '',
'auth.ldap.no_verify': False,
'auth.ldap.anonymous': False,
'auth.ldap.scope': 2,
'auth.ldap.groupou': 'Groups'
'auth.ldap.groupou': 'Groups',
'auth.ldap.accountattributename': 'memberUid',
'auth.ldap.persontype': 'person',
'auth.ldap.groupclass': 'posixGroup',
'auth.ldap.activedirectory': False,
}
@ -69,7 +73,7 @@ class _LDAPConnection(object):
'''
def __init__(self, uri, server, port, tls, no_verify, binddn, bindpw,
anonymous):
anonymous, accountattributename, activedirectory):
'''
Bind to an LDAP directory using passed credentials.
'''
@ -117,8 +121,8 @@ def _bind(username, password):
connargs = {}
# config params (auth.ldap.*)
params = {
'mandatory': ['uri', 'server', 'port', 'tls', 'no_verify', 'anonymous'],
'additional': ['binddn', 'bindpw', 'filter'],
'mandatory': ['uri', 'server', 'port', 'tls', 'no_verify', 'anonymous', 'accountattributename', 'activedirectory'],
'additional': ['binddn', 'bindpw', 'filter', 'groupclass'],
}
paramvalues = {}
@ -172,8 +176,25 @@ def _bind(username, password):
log.warn('Unable to find user {0}'.format(username))
return False
elif len(result) > 1:
log.warn('Found multiple results for user {0}'.format(username))
return False
# Active Directory returns something odd. Though we do not
# chase referrals (ldap.set_option(ldap.OPT_REFERRALS, 0) above)
# it still appears to return several entries for other potential
# sources for a match. All these sources have None for the
# CN (ldap array return items are tuples: (cn, ldap entry))
# But the actual CNs are at the front of the list.
# So with some list comprehension magic, extract the first tuple
# entry from all the results, create a list from those,
# and count the ones that are not None. If that total is more than one
# we need to error out because the ldap filter isn't narrow enough.
cns = [tup[0] for tup in result]
total_not_none = sum(1 for c in cns if c is not None)
if total_not_none > 1:
log.warn('Found multiple results for user {0}'.format(username))
return False
elif total_not_none == 0:
log.warn('Unable to find CN matching user {0}'.format(username))
return False
connargs['binddn'] = result[0][0]
if paramvalues['binddn'] and not paramvalues['bindpw']:
connargs['binddn'] = paramvalues['binddn']
@ -203,10 +224,12 @@ def auth(username, password):
'''
Simple LDAP auth
'''
if _bind(username, password):
log.debug('LDAP authentication successful')
return True
else:
log.debug('LDAP authentication FAILED')
return False
@ -214,20 +237,64 @@ def groups(username, **kwargs):
'''
Authenticate against an LDAP group
Uses groupou and basedn specified in group to filter
group search
Behavior is highly dependent on if Active Directory is in use.
AD handles group membership very differently than OpenLDAP.
See the :ref:`External Authentication <eauth>` documentation for a thorough
discussion of available parameters for customizing the search.
OpenLDAP allows you to search for all groups in the directory
and returns members of those groups. Then we check against
the username entered.
'''
group_list = []
bind = _bind(username, kwargs['password'])
if bind:
search_results = bind.search_s('ou={0},{1}'.format(_config('groupou'), _config('basedn')),
ldap.SCOPE_SUBTREE,
'(&(memberUid={0})(objectClass=posixGroup))'.format(username),
['memberUid', 'cn'])
log.debug('ldap bind to determine group membership succeeded! ------------------------')
if _config('activedirectory'):
try:
get_user_dn_search = '(&({0}={1})(objectClass={2}))'.format(_config('accountattributename'),
username,
_config('persontype'))
user_dn_results = bind.search_s(_config('basedn'),
ldap.SCOPE_SUBTREE,
get_user_dn_search, ['distinguishedName'])
except Exception as e:
log.debug('Exception thrown while looking up user DN in AD: {0}'.format(e))
return group_list
if not user_dn_results:
log.warn('Could not get distinguished name for user {0}'.format(username))
return group_list
# LDAP results are always tuples. First entry in the tuple is the DN
dn = user_dn_results[0][0]
ldap_search_string = '(&(member={0})(objectClass={1}))'.format(dn, _config('groupclass'))
try:
search_results = bind.search_s(_config('basedn'),
ldap.SCOPE_SUBTREE,
ldap_search_string,
[_config('accountattributename'), 'cn'])
except Exception as e:
log.debug('Exception thrown while retrieving group membership in AD: {0}'.format(e))
return group_list
for _, entry in search_results:
if 'cn' in entry:
group_list.append(entry['cn'][0])
log.debug('User {0} is a member of groups: {1}'.format(username, group_list))
else:
search_results = bind.search_s('ou={0},{1}'.format(_config('groupou'), _config('basedn')),
ldap.SCOPE_SUBTREE,
'(&({0}={1})(objectClass={2}))'.format(_config('accountattributename'),
username, _config('groupclass')),
[_config('accountattributename'), 'cn'])
for _, entry in search_results:
if username in entry[_config('accountattributename')]:
group_list.append(entry['cn'][0])
log.debug('User {0} is a member of groups: {1}'.format(username, group_list))
else:
return False
for _, entry in search_results:
if username in entry['memberUid']:
group_list.append(entry['cn'][0])
log.debug('User {0} is a member of groups: {1}'.format(username, group_list))
log.debug('ldap bind for groups failed! ------------------------')
return group_list
return group_list