Last active
December 10, 2015 15:39
-
-
Save mbrownnycnyc/4455988 to your computer and use it in GitHub Desktop.
a resource model script for use with rundeck, queries an active directory ldap server and returns a resource model in XML form ( http://rundeck.org/docs/manpages/man5/resource-v13.html ). Supports LDAPS via START TLS. Not sure if I followed the documented conventions for resource model plugin scripts.
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
#! /usr/bin/python | |
# ex: python ldap_resmodel.py -q -u mbrown@domain.local -p 'password_with_a_trailing_backslash\' -b 'OU=Child OU,DC=domain,DC=local' -f | |
# see python ldap_resmodel.py -h | |
#this is written to support python 2.6 | |
# on EL, must yum -y install python-ldap | |
# you must create /usr/lib/python2.*/ldaphelper.py from https://gist.github.com/raw/4453803/ (see http://www.packtpub.com/article/python-ldap-applications-ldap-opearations) | |
# curl https://gist.github.com/raw/4453803/ > /usr/lib/python2.6/ldaphelper.py | |
# using TLS (LDAPS): | |
# Simply: You need to save the issueing CA for LDAPS as /etc/pki/tls/certs/ca_issuer_cert.pem | |
# Also see: http://technet.microsoft.com/en-us/library/cc778124(WS.10).aspx | |
# | |
# openssl version -d # returns the directory where you want to save the pem to in the following: | |
# | |
# To obtain and verify the LDAPS certificate: | |
# | |
# echo "" | openssl s_client -connect LDAPSSERVER:636 -showcerts 2>/dev/null | sed -n -e '/BEGIN\ CERTIFICATE/,/END\ CERTIFICATE/ p' > /etc/pki/tls/certs/imported.pem | |
# ln -s /etc/pki/tls/certs/imported.pem /etc/pki/tls/certs/$(openssl x509 -noout -hash -in /etc/pki/tls/certs/imported.pem).0 | |
# openssl verify -verbose /etc/pki/tls/certs/imported.pem | |
# # you will receive the following error because you are missing the "certificate issuer" aka CA certificate | |
# # "unable to get local issuer certificate" | |
# # It doesn't appear that the LDAPS server on Windows 2003 (at least) presents the CA certificate during connection (as with this: http://serverfault.com/questions/225449/ssl-certificate-error-verify-errornum-20unable-to-get-local-issuer-certificat). | |
# # Therefore, you must "manually" obtain the Base64 X.509 for the issuer and store it in this case /etc/pki/tls/certs/ca_issuer_cert.pem | |
# openssl verify -CAfile /etc/pki/tls/certs/ca_issuer_cert.pem -verbose /etc/pki/tls/certs/imported.pem | |
# Note: | |
# This script produces a comma-separated list of servicePrincipalName attributes. | |
# sources: | |
# http://rundeck.org/docs/manual/plugins.html#script-resource-model-source-configuration | |
# https://gist.github.com/1288159 | |
# http://stackoverflow.com/a/11706378/843000 | |
# http://www.packtpub.com/article/python-ldap-applications-ldap-opearations ldaphelper.py improved error handling and structure | |
# http://stackoverflow.com/a/9923346/843000 | |
# http://postneo.com/projects/pyxml/ | |
# http://rundeck.org/docs/manpages/man5/resource-v13.html | |
# https://bugzilla.redhat.com/show_bug.cgi?id=800787 #TLS error -8179 | |
# http://python-ldap.sourcearchive.com/documentation/2.3.10-1/initialize_8py-source.html tls | |
# http://serverfault.com/questions/225449/ssl-certificate-error-verify-errornum-20unable-to-get-local-issuer-certificat LDAPS and AD | |
import sys, ldap, getpass, re | |
import ldaphelper | |
from ldaphelper import * | |
from decimal import * | |
from xml.dom.minidom import Document | |
from optparse import OptionParser | |
parser = OptionParser() | |
#the options are all strings and accept whatever is after them... meaning using -q is a problem | |
#parser.add_option('-h', '--help', action='store_true', help='will display this message.') | |
parser.add_option('-q', '--query', action='store_true', help='will execute a dig for ldapserver and ldapport by looking up the _ldap._tcp SRV record (disregards -s and -p)') | |
parser.add_option('-s', '--ldapserver', help='ldap server fqdn/hostname (automatically attempts ldaps first)') | |
parser.add_option('-t', '--ldapport', help='ldap server tcp port (default is 389)') | |
parser.add_option('-b', '--ldapbasedn', help='ldap instance base designated name, as this script will search all of the tree below (domain.local would be dc=domain,dc=local)') | |
parser.add_option('-u', '--binduser', help='ldap binding user with FQDN (user@domain.local)') | |
parser.add_option('-p', '--bindpassword', help='ldap binding user password (or enter password during execution)') | |
parser.add_option('-f', '--ldaps', action='store_true', help='forces ldaps (TLS) connection, exit if unavailable. Combining with --query will result in attempt LDAPS against the port specified, and will fail if TLS is unavailable.') | |
parser.add_option('-e', '--executionuser', help='assigns username in output resource model XML.') | |
options, args = parser.parse_args() | |
errors = [] | |
error_msg = 'No %s specified. Use option %s' | |
#print help if no arguments are given | |
if len(sys.argv) <= 1: | |
sys.stderr.write( parser.format_help().strip() ) | |
sys.exit(0) | |
if not options.query: | |
if not options.ldapserver: | |
errors.append(error_msg % ('--ldapserver [fqdn]', '-s or --ldapserver or specify --query')) | |
elif not options.ldapport: | |
errors.append(error_msg % ('--ldapport [port]', '-t or --ldapport or specify --query')) | |
if not options.ldapbasedn: | |
errors.append(error_msg % ('--ldapbasedn [basedn]', '-b or --ldapbasedn')) | |
if not options.binduser: | |
errors.append(error_msg % ('--user [bind username with fqdn]', '-u or --user')) | |
if not options.bindpassword: | |
options.bindpasswordword = getpass.getpass("Enter the password for %s: " % options.binduser) | |
if not options.bindpassword: | |
errors.append(error_msg % ('--password [bind username password]', '-p or --password')) | |
if not options.executionuser: | |
errors.append(error_msg % ('--executionuser [username]', '-e or --executionuser')) | |
#option.query not required | |
if options.query: | |
if options.binduser.find('@') > -1: | |
import subprocess | |
#execute a dig for _ldap._tcp and get the host info | |
#dig -t SRV _ldap._tdp.(option. | |
fqdn = options.binduser[options.binduser.find('@')+1:] | |
dug = subprocess.Popen(['dig', '-t', 'SRV', '_ldap._tcp.'+fqdn, '+short'], stdout=subprocess.PIPE).communicate()[0] | |
if len(dug.split('\n')) > 1: | |
sys.stderr.write("WARNING dig dug " + str(len(dug.split('\n'))) + " dns SRV records for _ldap._tcp." + fqdn + ". Using the first returned.\n") | |
dug = dug.split('\n') | |
options.ldapserver = dug[0].split()[3][:len(dug[0].split()[3])-1] | |
sys.stderr.write("INFO using ldap server: " + options.ldapserver + "\n") | |
options.ldapport = dug[0].split()[2] | |
sys.stderr.write("INFO using ldap port : " + options.ldapport + "\n") | |
else: | |
errors.append(\ | |
error_msg % ('--query [bind username with fqdn as user@domainname.tld]', '--query only when qualifying user entity with fqdn in user@domainname.tld')) | |
#you do not need to handle assigning an ldapport, as python-ldap will use default ports. | |
# in order to support a variable port, we will assign default ports anyway. | |
#option.forceldaps not required | |
if options.ldaps: | |
if len(options.ldapport) == 0: | |
options.ldapport = '636' | |
if options.ldapport: | |
if len(options.ldapport) == 0: | |
options.ldapport = '389' | |
#using ldap with ldap_client.start_tls_s() function to use TLS | |
ldapconstr = 'ldap://'+options.ldapserver+':'+options.ldapport | |
if errors: | |
sys.stderr.write('\n'.join(errors) + "\n") | |
sys.exit(1) | |
try: | |
#ldap.set_option(ldap.OPT_DEBUG_LEVEL,0) | |
#ldapmodule_trace_level = 1 | |
#ldapmodule_trace_file = sys.stderr | |
#ldap_client = ldap.initialize(ldapconstr,trace_level=ldapmodule_trace_level,trace_file=ldapmodule_trace_file) | |
ldap_client = ldap.initialize(ldapconstr) | |
sys.stderr.write("INFO initialized: " + ldapconstr + "\n") | |
if options.ldaps: | |
ldap_client.protocol_version=ldap.VERSION3 | |
ldap_client.set_option(ldap.OPT_NETWORK_TIMEOUT, 10) | |
# uncomment the following and comment out the other lines in this block to drop server authenticity checks provided by TLS | |
#sys.stderr.write("WARNING code maintains no server authenticity. The server may be a different server."+ "\n") | |
#ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_ALLOW) # allow invalid cert | |
#check if /etc/pki/tls/certs/imported.pem exists | |
try: | |
with open('/etc/pki/tls/certs/ca_issuer_cert.pem') as f: pass | |
sys.stderr.write("INFO found file at /etc/pki/tls/certs/ca_issuer_cert.pem" + "\n") | |
except IOError as e: | |
sys.stderr.write("\n\n" + '\033[01;31m' + "CRITICAL " + '\033[0;0m' + "ldaps attempt without the file /etc/pki/tls/certs/ca_issuer_cert.pem present (should contain the issueing CA X.509 cert in Base64)." + "\n") | |
sys.stderr.write("try: `echo "" | openssl s_client -connect LDAPSSERVER:636 -prexit 2>/dev/null | sed -n -e '/BEGIN\ CERTIFICATE/,/END\ CERTIFICATE/ p' > /etc/pki/tls/certs/imported.pem`" + "\n") | |
sys.exit(0) | |
sys.stderr.write("INFO setting the cert requirements globally" + "\n") | |
ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_DEMAND) # allow invalid cert | |
sys.stderr.write("INFO configuring which cert to use" + "\n") | |
ldap.set_option(ldap.OPT_X_TLS_CACERTFILE,'/etc/pki/tls/certs/ca_issuer_cert.pem') #set path to ca cert file | |
sys.stderr.write("INFO running python-ldap.start_tls_s()" + "\n") | |
ldap_client.start_tls_s() | |
sys.stderr.write("INFO issued synchronous connection to negotiate tls to: " + ldapconstr + "\n") | |
ldap_client.set_option(ldap.OPT_REFERRALS,0) | |
ldap_client.simple_bind_s(options.binduser, options.bindpassword) | |
sys.stderr.write("INFO synchronously bound with: " + options.binduser + "\n") | |
# perform a synchronous search, async and sync with timeout are also available: search(), search_st() | |
sys.stderr.write("INFO synchronously searching the entire subtree: " + options.ldapbasedn + "\n") | |
raw_ldap_results = ldap_client.search_s(options.ldapbasedn, ldap.SCOPE_SUBTREE, '(objectClass=computer)', ['dNSHostname','operatingSystem','operatingSystemVersion','operatingSystemServicePack','servicePrincipalName','description']) | |
clean_ldap_results = ldaphelper.get_search_results( raw_ldap_results ) | |
sys.stderr.write("INFO number of LDAPSearchResult records in search result: "+str(len(clean_ldap_results))+"\n\n") | |
sys.stderr.write("INFO producing xml structure for query to stdout:" + "\n\n\n") | |
#referring to http://rundeck.org/docs/manpages/man5/resource-v13.html | |
# for each found object return and spit out: | |
# property = "objectClass computer".attribute | |
# to be included with <node /> | |
# name = dNSHostName | |
# hostname = dNSHostName | |
# username = The username used for the remote connection. [probably should accept this as an argument passed into the script] | |
# to be included as <attribute ...> within <node> tag | |
# description = description | |
# osArch = $((operatingSystem contains 'Windows') AND (operatingSystemVersion{$1} > 5.1), then return 'x86_64'; else return 'x86') | |
# osFamily = $(if operatingSystem contains 'Windows', then return 'windows') | |
# osName = operatingSystem | |
# osVersion = $(operatingSystemVersion.substr(' ','').substr('(','.').substr(')','')) | |
# osServicePack = operatingSystemServicePack | |
# servicePrincipalName = servicePrincipalName | |
xmldoc = Document() | |
project = xmldoc.createElement("project") | |
xmldoc.appendChild(project) | |
#note that record.get_attr_values() returns a list... str(record.get_attr_values())="['attribute value']" (attribute value may be a list) | |
for record in clean_ldap_results: | |
node = xmldoc.createElement("node") | |
node.setAttribute("name", str(record.get_attr_values('dNSHostname')[0])) | |
node.setAttribute("hostname", str(record.get_attr_values('dNSHostname')[0])) | |
node.setAttribute("username", options.executionuser) | |
project.appendChild(node) | |
node_attribute_values = {} | |
node_attribute_values['description'] = str(record.get_attr_values("description")[0]).lower() | |
#this doesn't account for ia64 based arch and doesn't properly report 2003 64 bit (since you can't conclude this from the default info stored in AD) | |
os = str(record.get_attr_values("operatingSystem")[0]) | |
osversion = str(record.get_attr_values("operatingSystemVersion")[0]) | |
if ( os.find("Windows", 0) > -1 ): | |
node_attribute_values['osFamily'] = 'windows' | |
node_attribute_values['osVersion'] = osversion[:osversion.find(' (')] + '.' + osversion[(osversion.find(' (')+2):-(len(osversion)-osversion.find(')'))] | |
node_attribute_values['osServicePack'] = str(record.get_attr_values("operatingSystemServicePack")[0]) | |
node_attribute_values['servicePrincipalName'] = record.get_attr_values("servicePrincipalName") | |
if type(node_attribute_values['servicePrincipalName']) == list: | |
node_attribute_values['servicePrincipalName'] = ', '.join(node_attribute_values['servicePrincipalName']) | |
temposver = Decimal(osversion[:(osversion.find(" ",0))]) | |
if os.find("XP", 0) and ( temposver == Decimal('5.1') ) or os.find("2003", 0) and ( temposver == Decimal('5.2') ): | |
node_attribute_values['osArch'] = 'x86' | |
else: | |
node_attribute_values['osArch'] = 'x86_64' | |
else: | |
node_attribute_values['osFamily'] = '' | |
node_attribute_values['osName'] = os.lower() | |
#actually add the values to the xmldoc | |
for attribute in node_attribute_values.keys(): | |
nodeattrib = xmldoc.createElement("attribute") | |
nodeattrib.setAttribute("name", attribute) | |
nodeattrib.setAttribute("value", node_attribute_values[attribute]) | |
node.appendChild(nodeattrib) | |
sys.stdout.write(xmldoc.toprettyxml(indent=" ")) | |
#sys.stdout.write(xmldoc.toxml) | |
except ldap.LDAPError: | |
sys.stderr.write(str(sys.exc_type)+"\n") | |
sys.stderr.write(str(sys.exc_value)+"\n") | |
sys.exit(1) | |
finally: | |
ldap_client.unbind() | |
sys.exit(0) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment