Last active
December 25, 2015 11:19
-
-
Save eliasp/6968347 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# -*- 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