2019-05-25 17:36:24 -03:00

488 lines
16 KiB
Python

# -*- coding: utf-8 -*-
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
from ansible.plugins.action import ActionBase
from ansible.errors import AnsibleError
from ansible.utils.vars import merge_hash
from ansible.module_utils._text import to_text
from ansible.module_utils.parsing.convert_bool import boolean
import sys
import re
import base64
import io
from six import string_types
try:
import ldap
import ldif
HAS_LDAP = True
class MyLDIF(ldif.LDIFParser):
'''Clase auxiliar para extraer información de LDIF'''
def __init__(self,input):
ldif.LDIFParser.__init__(self,input)
self._entries = []
def handle(self,dn,entry):
self._entries.append((dn,entry))
def entries(self):
return self._entries
except ImportError:
HAS_LDAP = False
class LDAPException(Exception):
'''Excepción genérica de este módulo'''
pass
# ---- funciones para chequeo de claves, sacado de
# http://www.openldap.org/faq/data/cache/347.html
import os
import hashlib
from base64 import encodestring as b64encode
from base64 import decodestring as b64decode
def makeSecret(password):
salt = os.urandom(4)
h = hashlib.sha1(password)
h.update(salt)
return "{SSHA}" + b64encode(h.digest() + salt)[:-1]
def checkPassword(challenge_password, password):
try:
challenge_bytes = b64decode(challenge_password[6:])
digest = challenge_bytes[:20]
salt = challenge_bytes[20:]
hr = hashlib.sha1(password)
hr.update(salt)
return digest == hr.digest()
except:
pass
return False
class ActionModule(ActionBase):
# Nombre de los argumentos
ARG_DN = 'dn'
ARG_OBJCLS = 'objectClass'
ARG_ATTRS = 'attributes'
ARG_FILTER = 'filter'
ARG_LDIF = 'from_ldif'
ARG_REMOTE = 'remote_src'
ARG_RDN = 'dn_relative'
ARG_CURLY = 'dn_curly_hack'
ARG_BINDDN = 'bind_dn'
ARG_BINDPW = 'bind_pw'
ARG_SRVURI = 'server_uri'
ARG_TLS = 'start_tls'
ARG_VALCRT = 'validate_certs'
ARG_STYPE = 'search_type'
ARG_STATE = 'state'
# Atributos que representan claves
ATTR_PASSWORD = ('olcRootPW', 'userPassword')
# Atributos que no se pueden eliminar
ATTR_FRAGILE = ('olcModuleLoad','olcAttributeTypes')
# Atributos RDN, no se pueden modificar
ATTR_RDN = ()
def __init__(self, *args, **kwargs):
super(ActionModule, self).__init__(*args,**kwargs)
self.result= dict(failed=False,changed=False,action_history=[])
self.dn = None
self.attrs = dict()
self.entries = []
def _read_args(self):
'''
Leer argumentos y efectuar búsqueda inicial del DN
'''
# Leer DN y parametros de busqueda
self.req_dn = self._task.args.get(self.ARG_DN, None)
self.dn_relative = self._task.args.get(self.ARG_RDN, False)
self.search_filter = self._task.args.get(self.ARG_FILTER, '(objectClass=*)')
self.search_type = self._task.args.get(self.ARG_STYPE, 'sub')
self.state = self._task.args.get(self.ARG_STATE, 'present')
self.oc = self._task.args.get(self.ARG_OBJCLS, None)
self.req_attrs = self._task.args.get(self.ARG_ATTRS, {})
if self._task.args.get(self.ARG_LDIF, []):
'''Leer LDIF pasado como argumento y usarlo para configurar entrada'''
self._load_from_ldif()
if self.dn_relative:
'''Setear base de busqueda al parent de req_dn'''
reqdn = ldap.dn.str2dn(self.req_dn)
rdnkey = reqdn[0][0][0]
rdnval = reqdn[0][0][1]
if boolean(self._task.args.get(self.ARG_CURLY,False), strict=False):
rdnval = '{*}' + rdnval
self.search_filter = '(&{}({}={}))'.format(
self.search_filter, rdnkey, rdnval)
self.search_base = ldap.dn.dn2str(reqdn[1:])
else:
self.search_base = self.req_dn
# Validar params obligatorios
if not self.req_dn:
raise LDAPException("'{}' argument is mandatory, got value: {}".format(self.ARG_DN,self.req_dn))
if not self.oc:
raise LDAPException("'{}' argument is mandatory".format(self.ARG_OBJCLS))
# parametros de conexion al servidor
self.bdn = self._task.args.get(self.ARG_BINDDN, None)
self.bpw = self._task.args.get(self.ARG_BINDPW, None)
self.uri = self._task.args.get(self.ARG_SRVURI, 'ldapi:///')
self.tls = self._task.args.get(self.ARG_TLS, False)
self.valcert = self._task.args.get(self.ARG_VALCRT, True)
# Armar comando ldapsearch
self.ldapsearch_cmd = 'ldapsearch -LLL -H {}'.format(self.uri)
if self.bdn and self.bpw:
self.ldapsearch_cmd = '{} -D {} -w "{}"'.format(
self.ldapsearch_cmd, self.bdn, self.bpw)
else:
self.ldapsearch_cmd = "{} -Y EXTERNAL".format(
self.ldapsearch_cmd)
if not self.valcert:
self.ldapsearch_cmd = 'LDAPTLS_REQCERT=never {}'.format(self.ldapsearch_cmd)
if self.tls:
self.ldapsearch_cmd = '{} -ZZ'.format(self.ldapsearch_cmd)
# Armar parametros para módulos ldap_entry, ldap_attr
self.ldap_params = {
'bind_dn': self.bdn,
'bind_pw': self.bpw,
'server_uri': self.uri,
'start_tls': self.tls,
'validate_certs': self.valcert
}
def _load_from_ldif(self):
'''Configurar argumentos a partir de archivo LDIF'''
# copiado en buena parte de https://github.com/ansible/ansible/blob/devel/lib/ansible/plugins/action/unarchive.py
source = self._task.args.get(self.ARG_LDIF)
remote_src = boolean(self._task.args.get(self.ARG_REMOTE, False),
strict=False)
ldif_content = io.StringIO()
# el ldif es local
if not remote_src:
'''encontrar nombre real del archivo'''
try:
source = self._loader.get_real_file(self._find_needle('files',source))
except AnsibleError as e:
raise AnsibleActionFail(to_text(e))
'''leer archivo'''
with open(source,'r') as f:
ldif_content.write(unicode(f.read()))
# el ldif es remoto
else:
try:
slurp = self._execute_module(module_name='slurp',module_args=dict(src=source))
if slurp.get('failed',False):
raise AnsibleError('Failed reading remote file!')
if slurp.get('encoding') == 'base64':
ldif_content.write(unicode(b64decode(slurp['content'])))
else:
raise AnsibleError('Unknown encoding returned from slurp, this is a bug.')
except AnsibleError as e:
raise AnsibleActionFail('Error reading remote file: {}'.format(e.message))
ldif_content.write(u'\n\n')
ldif_content.seek(0)
parser = MyLDIF(ldif_content)
parser.parse()
# Sólo se soporta UNA entrada por archivo LDIF
entry = parser.entries()[0]
self.result['entry'] = entry
self.req_dn = entry[0]
self.req_attrs = entry[1].copy()
del self.req_attrs['objectClass']
self.oc = entry[1].get('objectClass',None)
# self.result['req_attr'] = self.req_attrs
def _ldapsearch(self, args):
'''Ejecutar comando ldapsearch y devolver el resultado'''
command = "{} {}".format(self.ldapsearch_cmd,args)
result = {}
failed = False
try:
result = self._execute_module(
module_name='command',
module_args=dict(_raw_params=command))
failed = False if result['rc'] in (0, 32) else result.get('failed',False)
except Exception as e:
raise LDAPException("Failed command: {}, message: {}".format(
command, e.message))
if failed:
raise LDAPException("Failed command: {}, message: {}".format(
command, result['stderr']))
result['failed'] = failed
return result
def _find_dn(self):
'''
Encontrar DN que matchea rdnkey=rdnval dentro del DN self.parent,
aplicando el filtro de búsqueda en self.sfilter.
'''
self.search_type = 'base'
if self.dn_relative:
self.search_type = 'one'
# Ejecutar comando
entries = self.search()
# DN encontrado
founddn = None
if len(entries):
founddn = entries[0][0]
# registrar resultados y salir
self.result['action_history'].append({
'function': 'finddn',
'outcome': ('found' if founddn else 'not found'),
})
self.dn = founddn
def search(self):
'''
Efectuar búsqueda, retornar entrada(s) encontradas
'''
qfilter = '(objectClass=*)'
# armar el filtro de búsqueda agregando los filtros adicionales
if isinstance(self.search_filter, string_types):
qfilter = self.search_filter
else:
for f in self.search_filter:
qfilter = '(&{}{})'.format(qfilter,f)
# efectuar búsqueda mediante ldapsearch
cmd_args = '-s {} -b "{}" "{}"'.format(self.search_type, self.search_base, qfilter)
result = self._ldapsearch(cmd_args)
entries = []
if result['rc'] in (0,32):
# procesar la salida LDIF
ldif_out = io.StringIO(result['stdout']+'\n\n')
parser = MyLDIF(ldif_out)
parser.parse()
entries = parser.entries()
self.result['action_history'].append({
'function': 'search',
'outcome': ('found' if len(entries) else 'no results found'),
'arguments': cmd_args
})
else:
self.result['action_history'].append({
'function': 'search',
'outcome': 'failed',
'arguments': cmd_args
})
return entries
def add_entry(self):
'''
Agregar entrada al directorio llamando al módulo ldap_entry.
Los datos se obtienen de los argumentos pasados al módulo.
'''
attrs = dict(self.req_attrs)
for attr in attrs.keys():
if attr in self.ATTR_PASSWORD:
attrs[attr] = makeSecret(attrs[attr])
result = {}
failed = False
changed = False
args = dict( dn=self.req_dn,
objectClass=self.oc,
attributes=attrs,
params = self.ldap_params
)
try:
# llamar a ldap_entry con argumentos dn, objectClass, attributes
result = self._execute_module(
module_name='ldap_entry',
module_args=args
)
failed = result.get('failed',False)
changed = result.get('changed',False)
except Exception as e:
raise LDAPException("Error invoking ldap_entry: {}: {}".format(
type(e).__name__,to_text(e)))
if failed:
raise LDAPException("Error invoking ldap_entry, args: {}, message: {}".format(
args, result))
# registrar resultados
if changed:
self.result['changed'] = True
self.result['action_history'].append({
'function': 'add_entry',
'outcome': 'failed' if failed else 'success',
'result': result
})
# actualizar self.dn con el DN de la entrada recién creada
self._find_dn()
def findattr(self):
'''
Leer atributos de la entrada con ldapsearch.
'''
# efectuar búsqueda
cmd_args = '-s base -b "{}"'.format(self.dn)
result = self._ldapsearch(cmd_args)
# Leer LDIF resultante
ldif_out = io.StringIO(result['stdout'])
parser = MyLDIF(ldif_out)
parser.parse()
entries = parser.entries()
# validar que el resultado se una única entrada
if len(entries) != 1:
raise LDAPException("Unable to read attributes for DN={}, cmd={}, entries={}".format(self.dn,cmd,entries))
if entries[0][0] != self.dn:
raise LDAPException("Unexpected DN={} while reading entry attributes, expected {}".format(
entries[0][0], self.dn))
# registrar invocación y guardar self.attrs
self.result['action_history'].append({
'function': 'findattr',
'outcome': 'success',
'result': result
})
self.attrs = entries[0][1]
def updateattr(self, attribute, value):
'''Verificar/actualizar valor para un atributo'''
if attribute in self.ATTR_PASSWORD:
if not isinstance(value, string_types):
value = value[0]
if checkPassword(self.attrs.get(attribute,[None])[0], value):
# si la clave matchea, salir
self.result['action_history'].append({
'function': 'updateattr',
'arguments': [attribute, 'hidden password'],
'outcome': 'password not changed'
})
return
else:
# setear el valor al hash salteado
value = [makeSecret(value)]
# convertir el valor a una lista
if isinstance(value, string_types):
value = [value]
changed = False
failed = False
result = None
args = {
'dn': self.dn,
'name': attribute,
'values': value,
'state': 'present' if attribute in self.ATTR_FRAGILE else 'exact',
'params': self.ldap_params
}
if set(value) != set(self.attrs.get(attribute,[])):
# actualizar valor llamando a ldap_attr
try:
result = self._execute_module(
module_name='ldap_attr',
module_args=args
)
changed = result.get('changed',False)
failed = result.get('failed',False)
except Exception as e:
raise LDAPException("Error invoking ldap_attr: {}: {}, result = {}".format(
type(e).__name__,to_text(e),result))
# guardar resultados
if changed:
self.result['changed'] = True
if failed:
self.result['failed'] = True
self.result['action_history'].append({
'function': 'updateattr',
'arguments': [attribute, value],
'outcome': 'failed' if failed else ('changed' if changed else 'unchanged'),
'result': result
})
def run(self, tmp=None, task_vars={}):
'''Función principal, punto de entrada al plugin'''
# Llamar a la función run de la clase madre
result = super(ActionModule, self).run(tmp, task_vars)
# Verificar si debo salir o fallar
if result.get('skipped', False) or result.get('failed', False):
return result
# Asegurar que la importación LDAP no falló, salir con gracia
if not HAS_LDAP:
return merge_hash( self.result, {
'failed': True,
'msg': "Missing required 'ldap' module (pip install python-ldap)."
})
try:
# leer argumentos
self._read_args()
# si solo se requiere buscar, efectuar busqueda y salir
if self.state == 'search':
self.result.update(dict(entries=self.search()))
return self.result
# busqueda inicial del DN
self._find_dn()
if self.dn is None:
# el DN no existe, crear la entrada
self.add_entry()
else:
# el DN existe, verificar atributos
attrs = self.findattr()
# verificar/actualizar cada atributo
for attr in self.req_attrs.keys():
self.updateattr(attr, self.req_attrs[attr])
except LDAPException as e:
return merge_hash( self.result, {
'failed': True,
'msg': "Invalid arguments: {}".format(e.message)
})
self.result['dn'] = self.dn
self.result['attrs'] = self.attrs
return self.result