mirror of
https://github.com/valitydev/salt.git
synced 2024-11-07 08:58:59 +00:00
Add authentication against Active Directory
This commit is contained in:
parent
8977ed20c7
commit
7421042dd2
@ -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
|
||||
|
||||
|
@ -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):
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user