mirror of
https://github.com/saltstack/salt.git
synced 2025-04-17 10:10:20 +00:00
Merge pull request #26183 from cro/anonldap2
Fix LDAP configuration issue.
This commit is contained in:
commit
c3814137a3
3 changed files with 94 additions and 31 deletions
|
@ -104,48 +104,87 @@ Token expiration time can be set in the Salt master config file.
|
|||
|
||||
|
||||
LDAP and Active Directory
|
||||
-------------------------
|
||||
=========================
|
||||
|
||||
Salt supports both user and group authentication for LDAP (and Active Directory
|
||||
accessed via its LDAP interface)
|
||||
|
||||
OpenLDAP and similar systems
|
||||
----------------------------
|
||||
|
||||
LDAP configuration happens in the Salt master configuration file.
|
||||
|
||||
Server configuration values and their defaults:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
# Server to auth against
|
||||
auth.ldap.server: localhost
|
||||
|
||||
# Port to connect via
|
||||
auth.ldap.port: 389
|
||||
|
||||
# Use TLS when connecting
|
||||
auth.ldap.tls: False
|
||||
|
||||
# LDAP scope level, almost always 2
|
||||
auth.ldap.scope: 2
|
||||
auth.ldap.uri: ''
|
||||
auth.ldap.tls: False
|
||||
|
||||
# Server specified in URI format
|
||||
auth.ldap.uri: '' # Overrides .ldap.server, .ldap.port, .ldap.tls above
|
||||
|
||||
# Verify server's TLS certificate
|
||||
auth.ldap.no_verify: False
|
||||
|
||||
# Bind to LDAP anonymously to determine group membership
|
||||
# Active Directory does not allow anonymous binds without special configuration
|
||||
auth.ldap.anonymous: False
|
||||
|
||||
# FOR TESTING ONLY, this is a VERY insecure setting.
|
||||
# If this is True, the LDAP bind password will be ignored and
|
||||
# access will be determined by group membership alone with
|
||||
# the group memberships being retrieved via anonymous bind
|
||||
auth.ldap.auth_by_group_membership_only: False
|
||||
|
||||
# Require authenticating user to be part of this Organizational Unit
|
||||
# This can be blank if your LDAP schema does not use this kind of OU
|
||||
auth.ldap.groupou: 'Groups'
|
||||
|
||||
# Object Class for groups. An LDAP search will be done to find all groups of this
|
||||
# class to which the authenticating user belongs.
|
||||
auth.ldap.groupclass: 'posixGroup'
|
||||
|
||||
# Unique ID attribute name for the user
|
||||
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:
|
||||
There are two phases to LDAP authentication. First, Salt authenticates to search for a users's Distinguished Name
|
||||
and group membership. The user it authenticates as in this phase is often a special LDAP system user with
|
||||
read-only access to the LDAP directory. After Salt searches the directory to determine the actual user's DN
|
||||
and groups, it re-authenticates as the user running the Salt commands.
|
||||
|
||||
If you are already aware of the structure of your DNs and permissions in your LDAP store are set such that
|
||||
users can look up their own group memberships, then the first and second users can be the same. To tell Salt this is
|
||||
the case, omit the ``auth.ldap.bindpw`` parameter. You can template the binddn like this:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
auth.ldap.basedn: dc=saltstack,dc=com
|
||||
auth.ldap.binddn: cn=admin,dc=saltstack,dc=com
|
||||
auth.ldap.binddn: uid={{ username }},cn=users,cn=accounts,dc=saltstack,dc=com
|
||||
|
||||
To bind to a DN, a password is required
|
||||
Salt will use the password entered on the salt command line in place of the bindpw.
|
||||
|
||||
To use two separate users, specify the LDAP lookup user in the binddn directive, and include a bindpw like so
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
auth.ldap.binddn: uid=ldaplookup,cn=sysaccounts,cn=etc,dc=saltstack,dc=com
|
||||
auth.ldap.bindpw: mypassword
|
||||
|
||||
Salt uses a filter to find the DN associated with a user. Salt
|
||||
As mentioned before, 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
|
||||
|
@ -161,6 +200,9 @@ the results are filtered against ``auth.ldap.groupclass``, default
|
|||
|
||||
auth.ldap.groupou: Groups
|
||||
|
||||
Active Directory
|
||||
----------------
|
||||
|
||||
Active Directory handles group membership differently, and does not utilize the
|
||||
``groupou`` configuration variable. AD needs the following options in
|
||||
the master config:
|
||||
|
@ -186,7 +228,7 @@ of the user is looked up with the following LDAP search:
|
|||
)
|
||||
|
||||
This should return a distinguishedName that we can use to filter for group
|
||||
membership. Then the following LDAP query is executed:
|
||||
membership. Then the following LDAP query is executed:
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
|
|
|
@ -13,7 +13,6 @@ import logging
|
|||
from salt.exceptions import CommandExecutionError, SaltInvocationError
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
# Import third party libs
|
||||
from jinja2 import Environment
|
||||
try:
|
||||
|
@ -111,7 +110,7 @@ class _LDAPConnection(object):
|
|||
)
|
||||
|
||||
|
||||
def _bind(username, password):
|
||||
def _bind(username, password, anonymous=False):
|
||||
'''
|
||||
Authenticate via an LDAP bind
|
||||
'''
|
||||
|
@ -121,8 +120,10 @@ def _bind(username, password):
|
|||
connargs = {}
|
||||
# config params (auth.ldap.*)
|
||||
params = {
|
||||
'mandatory': ['uri', 'server', 'port', 'tls', 'no_verify', 'anonymous', 'accountattributename', 'activedirectory'],
|
||||
'additional': ['binddn', 'bindpw', 'filter', 'groupclass'],
|
||||
'mandatory': ['uri', 'server', 'port', 'tls', 'no_verify', 'anonymous',
|
||||
'accountattributename', 'activedirectory'],
|
||||
'additional': ['binddn', 'bindpw', 'filter', 'groupclass',
|
||||
'auth_by_group_membership_only'],
|
||||
}
|
||||
|
||||
paramvalues = {}
|
||||
|
@ -137,6 +138,7 @@ def _bind(username, password):
|
|||
#except SaltInvocationError:
|
||||
# pass
|
||||
|
||||
paramvalues['anonymous'] = anonymous
|
||||
if paramvalues['binddn']:
|
||||
# the binddn can also be composited, e.g.
|
||||
# - {{ username }}@domain.com
|
||||
|
@ -204,7 +206,10 @@ def _bind(username, password):
|
|||
# Update connection dictionary with the user's password
|
||||
connargs['bindpw'] = password
|
||||
# Attempt bind with user dn and password
|
||||
log.debug('Attempting LDAP bind with user dn: {0}'.format(connargs['binddn']))
|
||||
if paramvalues['anonymous']:
|
||||
log.debug('Attempting anonymous LDAP bind')
|
||||
else:
|
||||
log.debug('Attempting LDAP bind with user dn: {0}'.format(connargs['binddn']))
|
||||
try:
|
||||
ldap_conn = _LDAPConnection(**connargs).ldap
|
||||
except Exception:
|
||||
|
@ -224,8 +229,8 @@ def auth(username, password):
|
|||
'''
|
||||
Simple LDAP auth
|
||||
'''
|
||||
|
||||
if _bind(username, password):
|
||||
if _bind(username, password, anonymous=_config('auth_by_group_membership_only', mandatory=False) and
|
||||
_config('anonymous', mandatory=False)):
|
||||
log.debug('LDAP authentication successful')
|
||||
return True
|
||||
else:
|
||||
|
@ -250,7 +255,8 @@ def groups(username, **kwargs):
|
|||
'''
|
||||
group_list = []
|
||||
|
||||
bind = _bind(username, kwargs['password'])
|
||||
bind = _bind(username, kwargs['password'],
|
||||
anonymous=_config('anonymous', mandatory=False))
|
||||
if bind:
|
||||
log.debug('ldap bind to determine group membership succeeded!')
|
||||
|
||||
|
@ -285,15 +291,24 @@ def groups(username, **kwargs):
|
|||
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')),
|
||||
if _config('groupou'):
|
||||
search_base = 'ou={0},{1}'.format(_config('groupou'), _config('basedn'))
|
||||
else:
|
||||
search_base = '{0}'.format(_config('basedn'))
|
||||
search_string = '(&({0}={1})(objectClass={2}))'.format(_config('accountattributename'),
|
||||
username, _config('groupclass'))
|
||||
search_results = bind.search_s(search_base,
|
||||
ldap.SCOPE_SUBTREE,
|
||||
'(&({0}={1})(objectClass={2}))'.format(_config('accountattributename'),
|
||||
username, _config('groupclass')),
|
||||
search_string,
|
||||
[_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))
|
||||
|
||||
if not auth(username, kwargs['password']):
|
||||
log.error('LDAP username and password do not match')
|
||||
return []
|
||||
else:
|
||||
log.error('ldap bind to determine group membership FAILED!')
|
||||
return group_list
|
||||
|
|
|
@ -2143,9 +2143,14 @@ class ClearFuncs(object):
|
|||
clear_load['groups'] = groups
|
||||
return self.loadauth.mk_token(clear_load)
|
||||
except Exception as exc:
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
type_, value_, traceback_ = sys.exc_info()
|
||||
log.error(
|
||||
'Exception occurred while authenticating: {0}'.format(exc)
|
||||
)
|
||||
log.error(traceback.format_exception(type_, value_, traceback_))
|
||||
return ''
|
||||
|
||||
def get_token(self, clear_load):
|
||||
|
@ -2271,18 +2276,13 @@ class ClearFuncs(object):
|
|||
)
|
||||
return ''
|
||||
try:
|
||||
# The username with which we are attempting to auth
|
||||
name = self.loadauth.load_name(extra)
|
||||
# The groups to which this user belongs
|
||||
groups = self.loadauth.get_groups(extra)
|
||||
# The configured auth groups
|
||||
group_perm_keys = [
|
||||
item for item in self.opts['external_auth'][extra['eauth']]
|
||||
if item.endswith('%')
|
||||
]
|
||||
name = self.loadauth.load_name(extra) # The username we are attempting to auth with
|
||||
groups = self.loadauth.get_groups(extra) # The groups this user belongs to
|
||||
if groups is None:
|
||||
groups = []
|
||||
group_perm_keys = filter(lambda(item): item.endswith('%'), self.opts['external_auth'][extra['eauth']]) # The configured auth groups
|
||||
|
||||
# First we need to know if the user is allowed to proceed via
|
||||
# any of their group memberships.
|
||||
# First we need to know if the user is allowed to proceed via any of their group memberships.
|
||||
group_auth_match = False
|
||||
for group_config in group_perm_keys:
|
||||
group_config = group_config.rstrip('%')
|
||||
|
@ -2322,9 +2322,15 @@ class ClearFuncs(object):
|
|||
return ''
|
||||
|
||||
except Exception as exc:
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
type_, value_, traceback_ = sys.exc_info()
|
||||
log.error(
|
||||
'Exception occurred while authenticating: {0}'.format(exc)
|
||||
)
|
||||
log.error(traceback.format_exception(
|
||||
type_, value_, traceback_))
|
||||
return ''
|
||||
|
||||
# auth_list = self.opts['external_auth'][extra['eauth']][name] if name in self.opts['external_auth'][extra['eauth']] else self.opts['external_auth'][extra['eauth']]['*']
|
||||
|
|
Loading…
Add table
Reference in a new issue