Skip to content

Instantly share code, notes, and snippets.

@eliasp
Last active December 25, 2015 11:19
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save eliasp/6968347 to your computer and use it in GitHub Desktop.
Save eliasp/6968347 to your computer and use it in GitHub Desktop.
# -*- coding: utf-8 -*-
'''
Provide authentication using simple LDAP binds
:depends: - ldap Python module
'''
# Import python libs
from __future__ import absolute_import
import logging
# Import salt libs
from salt.exceptions import CommandExecutionError, SaltInvocationError
log = logging.getLogger(__name__)
# Import third party libs
from jinja2 import Environment
try:
import ldap
import ldap.modlist
HAS_LDAP = True
except ImportError:
HAS_LDAP = False
# Defaults, override in master config
__defopts__ = {'auth.ldap.server': 'localhost',
'auth.ldap.port': '389',
'auth.ldap.tls': False,
'auth.ldap.no_verify': False,
'auth.ldap.anonymous': False,
'auth.ldap.scope': 2
}
def _config(key):
'''
Return a value for 'name' from master config file options or defaults.
'''
try:
value = __opts__['auth.ldap.{0}'.format(key)]
except KeyError:
try:
value = __defopts__['auth.ldap.{0}'.format(key)]
except KeyError:
msg = 'missing auth.ldap.{0} in master config'.format(key)
return SaltInvocationError(msg)
return value
def _render_template(filter_, username):
'''
Render filter_ template, substituting username where found.
'''
env = Environment()
template = env.from_string(filter_)
variables = {'username': username}
return template.render(variables)
class _LDAPConnection(object):
'''
Setup an LDAP connection.
'''
def __init__(self, server, port, tls, no_verify, binddn, bindpw,
anonymous):
'''
Bind to an LDAP directory using passed credentials.
'''
self.server = server
self.port = port
self.tls = tls
self.binddn = binddn
self.bindpw = bindpw
schema = 'ldap'
if not HAS_LDAP:
raise CommandExecutionError('Failed to connect to LDAP, module '
'not loaded')
try:
if no_verify:
ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT,
ldap.OPT_X_TLS_NEVER)
if self.tls:
schema = 'ldaps'
self.ldap = ldap.initialize(
'{0}://{1}:{2}'.format(schema, self.server, self.port)
)
self.ldap.protocol_version = 3 # ldap.VERSION3
self.ldap.set_option(ldap.OPT_REFERRALS, 0) # Needed for AD
if not anonymous:
self.ldap.simple_bind_s(self.binddn, self.bindpw)
except Exception as ldap_error:
raise CommandExecutionError(
'Failed to bind to LDAP server {0}:{1} as {2}: {3}'.format(
self.server, self.port, self.binddn, ldap_error
)
)
def auth(username, password):
'''
Authenticate via an LDAP bind
'''
# Get config params; create connection dictionary
filter_ = _render_template(_config('filter'), username)
basedn = _config('basedn')
scope = _config('scope')
connargs = {}
# config params (auth.ldap.*)
params = {
'mandatory': ['server', 'port', 'tls', 'no_verify', 'anonymous'],
'additional': ['binddn', 'bindpw'],
}
paramvalues = {}
for param in params['mandatory']:
paramvalues[param] = _config(param)
for param in params['additional']:
try:
paramvalues[param] = _config(param)
except SaltInvocationError:
pass
if paramvalues['binddn']:
# the binddn can also be composited, e.g.
# - {{ username }}@domain.com
# - cn={{ username }},ou=users,dc=company,dc=tld
# so make sure to render it first before using it
paramvalues['binddn'] = _render_template(paramvalues['binddn'], username)
# Only add binddn/bindpw to the connargs when they're set, as they're not
# mandatory for initializing the LDAP object, but if they're provided
# initially, a bind attempt will be done during the initialization to
# validate them
if paramvalues['binddn']:
connargs['binddn'] = paramvalues['binddn']
if paramvalues['bindpw']:
params['mandatory'].append('bindpw')
for name in params['mandatory']:
connargs[name] = paramvalues['name']
if not paramvalues['anonymous']:
if paramvalues['binddn'] and paramvalues['bindpw']:
# search for the user's DN to be used for the actual authentication
_ldap = _LDAPConnection(**connargs).ldap
log.debug(
'Running LDAP user dn search with filter:{0}, dn:{1}, '
'scope:{2}'.format(
filter_, basedn, scope
)
)
result = _ldap.search_s(basedn, int(scope), filter_)
if len(result) < 1:
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
connargs['binddn'] = result[0][0]
if paramvalue['binddn'] and not paramvalues['bindpw']:
connargs['binddn'] = paramvalues['binddn']
elif paramvalues['binddn'] and not paramvalues['bindpw']:
connargs['binddn'] = paramvalues['binddn']
# 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']))
try:
_LDAPConnection(**connargs).ldap
except Exception:
log.warn('Failed to authenticate user dn via LDAP: {0}'.format(connargs['binddn']))
return False
log.debug(
'Successfully authenticated user dn via LDAP: {0}'.format(
connargs['binddn']
)
)
return True
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment