| [1218] | 1 | #!/usr/bin/python | 
|---|
|  | 2 | # vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4 | 
|---|
|  | 3 |  | 
|---|
|  | 4 | import ldap | 
|---|
|  | 5 | import ldif | 
|---|
|  | 6 | import ldap.modlist | 
|---|
|  | 7 | import ConfigParser | 
|---|
|  | 8 | import os | 
|---|
|  | 9 | import sys | 
|---|
|  | 10 |  | 
|---|
|  | 11 | import dateutil.parser | 
|---|
|  | 12 | import dateutil.tz | 
|---|
|  | 13 | import datetime | 
|---|
|  | 14 |  | 
|---|
|  | 15 | class Options: | 
|---|
|  | 16 | def __init__(self): | 
|---|
|  | 17 | self.delete=True | 
|---|
|  | 18 | self.starttls=False | 
|---|
|  | 19 | self.updateonly=False | 
|---|
|  | 20 | self.attrfilter=None | 
|---|
|  | 21 | self.exclude=None | 
|---|
|  | 22 | self.renameattr=None | 
|---|
|  | 23 | self.renamecommand=None | 
|---|
|  | 24 | self.verbose=False | 
|---|
|  | 25 |  | 
|---|
|  | 26 | def notify_created(dn): | 
|---|
|  | 27 | print "notify_created",dn | 
|---|
|  | 28 |  | 
|---|
|  | 29 | def notify_modified(dn): | 
|---|
|  | 30 | print "notify_modified",dn | 
|---|
|  | 31 |  | 
|---|
|  | 32 | def notify_deleted(dn): | 
|---|
|  | 33 | print "notify_deleted",dn | 
|---|
|  | 34 |  | 
|---|
|  | 35 | def notify_renamed(dn,newdn,uid,newuid,options): | 
|---|
|  | 36 | print "notify_renamed",dn,newdn | 
|---|
|  | 37 | subprocess.check_call("%s %s %s %s %s" % (options.renamecommand,dn,newdn,uid,newuid),shell=True) | 
|---|
|  | 38 |  | 
|---|
|  | 39 | def readLDIFSource(path): | 
|---|
|  | 40 | with open(path,'r') as f: | 
|---|
|  | 41 | parser = ldif.LDIFRecordList(f) | 
|---|
|  | 42 | parser.parse() | 
|---|
|  | 43 | result = parser.all_records | 
|---|
|  | 44 | return result | 
|---|
|  | 45 |  | 
|---|
|  | 46 | def readLdapSource(server,binddn,bindpw,basedn,filter,starttls=False): | 
|---|
|  | 47 | con = ldap.open(server,port=389) | 
|---|
|  | 48 | if starttls: | 
|---|
|  | 49 | con.start_tls_s() | 
|---|
|  | 50 | con.simple_bind_s(binddn,bindpw) | 
|---|
|  | 51 | results=con.search_s(basedn,ldap.SCOPE_SUBTREE,filter,None) | 
|---|
|  | 52 | return results | 
|---|
|  | 53 |  | 
|---|
|  | 54 | def syncLdapDestination(searchresult,destserver,destbinddn,destbindpw,srcbasedn,destbasedn,destrdn,options=Options()): | 
|---|
|  | 55 |  | 
|---|
|  | 56 | attrmap=ldap.cidict.cidict({ | 
|---|
|  | 57 | }) | 
|---|
|  | 58 | classmap={ | 
|---|
|  | 59 | } | 
|---|
|  | 60 |  | 
|---|
|  | 61 | junk_attrs = [ "memberof", "modifiersname", "modifytimestamp", "entryuuid", "entrycsn", "contextcsn", "creatorsname", "createtimestamp", "structuralobjectclass", "pwdchangedtime", "pwdfailuretime" ] | 
|---|
|  | 62 | update_objects=[] | 
|---|
|  | 63 |  | 
|---|
|  | 64 | if len(searchresult)==0: | 
|---|
|  | 65 | print "empty source, aborting" | 
|---|
|  | 66 | return | 
|---|
|  | 67 |  | 
|---|
|  | 68 | for r in searchresult: | 
|---|
|  | 69 | dn=r[0] | 
|---|
|  | 70 |  | 
|---|
|  | 71 | d=ldap.cidict.cidict(r[1]) | 
|---|
|  | 72 | objectclasses=d["objectclass"] | 
|---|
|  | 73 |  | 
|---|
|  | 74 | newObjectclasses=[] | 
|---|
|  | 75 | for o in objectclasses: | 
|---|
|  | 76 | if o.lower() in classmap: | 
|---|
|  | 77 | new_oc = classmap[o.lower()] | 
|---|
|  | 78 | if not new_oc in newObjectclasses: | 
|---|
|  | 79 | newObjectclasses.append(new_oc) | 
|---|
|  | 80 | else: | 
|---|
|  | 81 | #pass | 
|---|
|  | 82 | if not o in newObjectclasses: | 
|---|
|  | 83 | newObjectclasses.append(o) | 
|---|
|  | 84 |  | 
|---|
|  | 85 | d["objectclass"]=newObjectclasses | 
|---|
|  | 86 |  | 
|---|
|  | 87 | rpath = dn[:-len(srcbasedn)] | 
|---|
|  | 88 | # print "dn:",dn,"src:",srcbasedn,"rpath:",rpath,"dest:",destbasedn | 
|---|
|  | 89 |  | 
|---|
|  | 90 | for a in d.keys(): | 
|---|
|  | 91 | attr=a | 
|---|
|  | 92 | if attrmap.has_key(a.lower()): | 
|---|
|  | 93 | attr=attrmap[attr].lower() | 
|---|
|  | 94 | if attr.lower()!=a.lower(): | 
|---|
|  | 95 | # print "# ",a," -> ",attr | 
|---|
|  | 96 | values=d[a] | 
|---|
|  | 97 | del d[a] | 
|---|
|  | 98 | d[attr]=values | 
|---|
|  | 99 | else: | 
|---|
|  | 100 | # del d[a] | 
|---|
|  | 101 | continue | 
|---|
|  | 102 |  | 
|---|
|  | 103 | dn=rpath+destbasedn | 
|---|
|  | 104 |  | 
|---|
|  | 105 | update_objects.append((dn,d)) | 
|---|
|  | 106 |  | 
|---|
|  | 107 | con = ldap.open(destserver,port=389) | 
|---|
|  | 108 | if options.starttls: | 
|---|
|  | 109 | con.start_tls_s() | 
|---|
|  | 110 | con.simple_bind_s(destbinddn,destbindpw) | 
|---|
|  | 111 |  | 
|---|
|  | 112 | exist=0 | 
|---|
|  | 113 | failed=0 | 
|---|
|  | 114 | good=0 | 
|---|
|  | 115 | deleted=0 | 
|---|
|  | 116 | existing=[] | 
|---|
|  | 117 | tzutc = dateutil.tz.gettz('UTC') | 
|---|
|  | 118 | now = datetime.datetime.now(tzutc) | 
|---|
|  | 119 | max_age = datetime.timedelta(days=pwd_max_days) | 
|---|
|  | 120 |  | 
|---|
|  | 121 | for o in update_objects: | 
|---|
|  | 122 | dn,entry=o | 
|---|
|  | 123 | try: | 
|---|
|  | 124 | result = None | 
|---|
|  | 125 | if options.renameattr and entry.has_key(options.renameattr): | 
|---|
|  | 126 | result = con.search_s(destbasedn,ldap.SCOPE_SUB,"%s=%s" % (options.renameattr,entry[options.renameattr][0])) | 
|---|
|  | 127 | if result != None and len(result)>0: | 
|---|
|  | 128 | existingDn, existingEntry = result[0] | 
|---|
|  | 129 | if existingDn.lower() != dn.lower(): | 
|---|
|  | 130 | con.modrdn_s(existingDn,dn) | 
|---|
|  | 131 | notify_renamed(existingDn,dn,existingEntry[options.renameattr][0],entry[options.renameattr][0],options) | 
|---|
|  | 132 | continue | 
|---|
|  | 133 |  | 
|---|
|  | 134 | result=con.search_s(dn,ldap.SCOPE_BASE,"objectclass=*") | 
|---|
|  | 135 | destDn,destEntry=result[0] | 
|---|
|  | 136 |  | 
|---|
|  | 137 | if options.exclude!=None and destDn.lower().endswith(options.exclude): | 
|---|
|  | 138 | continue | 
|---|
|  | 139 |  | 
|---|
|  | 140 | # hack for syncing accounts locked by password policy | 
|---|
|  | 141 | do_unlock = False | 
|---|
|  | 142 | if pwd_max_days>0 and entry.has_key('pwdChangedTime'): | 
|---|
|  | 143 | # print "pwdChangedTime set for",dn | 
|---|
|  | 144 | pwdChange = entry['pwdChangedTime'][0] | 
|---|
|  | 145 | d = dateutil.parser.parse(pwdChange) | 
|---|
|  | 146 | if (now-d)>max_age: | 
|---|
|  | 147 | if dn.startswith('cn=haydar aldetest'): | 
|---|
|  | 148 | entry['pwdAccountLockedTime']=[ '000001010000Z' ] | 
|---|
|  | 149 | print "locking",dn,pwdChange | 
|---|
|  | 150 | else: | 
|---|
|  | 151 | result = con.search_s(dn,ldap.SCOPE_BASE,"objectclass=*", \ | 
|---|
|  | 152 | attrlist = [ 'pwdAccountLockedTime' ]) | 
|---|
|  | 153 | tmp_dn, tmp_entry = result[0] | 
|---|
|  | 154 | if tmp_entry.has_key('pwdAccountLockedTime'): | 
|---|
|  | 155 | print "unlocking",dn,pwdChange | 
|---|
|  | 156 | do_unlock = True | 
|---|
|  | 157 |  | 
|---|
|  | 158 | mod_attrs=ldap.modlist.modifyModlist(destEntry,entry) | 
|---|
|  | 159 |  | 
|---|
|  | 160 | # hack for unlocking, see above | 
|---|
|  | 161 | if do_unlock: | 
|---|
|  | 162 | mod_attrs.append( (ldap.MOD_DELETE,'pwdAccountLockedTime',None) ) | 
|---|
|  | 163 |  | 
|---|
|  | 164 | if options.attrfilter!=None: | 
|---|
|  | 165 | mod_attrs=[ a for a in mod_attrs if a[1] in options.attrfilter] | 
|---|
|  | 166 |  | 
|---|
|  | 167 | if junk_attrs!=None: | 
|---|
|  | 168 | mod_attrs=[ a for a in mod_attrs if a[1].lower() not in junk_attrs] | 
|---|
|  | 169 |  | 
|---|
|  | 170 | if mod_attrs!=[]: | 
|---|
|  | 171 | exist=exist+1 | 
|---|
|  | 172 | #if options.verbose: | 
|---|
|  | 173 | #       print dn, "already exists" | 
|---|
|  | 174 | try: | 
|---|
|  | 175 | # print dn,destEntry['objectClass'],entry['objectClass'] | 
|---|
|  | 176 | con.modify_s(dn,mod_attrs) | 
|---|
|  | 177 | except: | 
|---|
|  | 178 | print "error",dn,mod_attrs | 
|---|
|  | 179 | notify_modified(dn) | 
|---|
|  | 180 | else: | 
|---|
|  | 181 | pass | 
|---|
|  | 182 | # print "no changes, not modified" | 
|---|
|  | 183 |  | 
|---|
|  | 184 | except ldap.NO_SUCH_OBJECT: | 
|---|
|  | 185 | if options.updateonly==True: | 
|---|
|  | 186 | continue | 
|---|
|  | 187 |  | 
|---|
|  | 188 | try: | 
|---|
|  | 189 | con.add_s(dn,ldap.modlist.addModlist(entry,junk_attrs)) | 
|---|
|  | 190 | notify_created(dn) | 
|---|
|  | 191 | if options.verbose: | 
|---|
|  | 192 | print dn,"created" | 
|---|
|  | 193 | good=good+1 | 
|---|
|  | 194 | except (ldap.OBJECT_CLASS_VIOLATION,ldap.NO_SUCH_OBJECT): | 
|---|
|  | 195 | print dn, "failed" | 
|---|
|  | 196 | failed=failed+1 | 
|---|
|  | 197 |  | 
|---|
|  | 198 | if options.delete==True and options.updateonly==False: | 
|---|
|  | 199 | result=con.search_s(destbasedn,ldap.SCOPE_SUBTREE,filter) | 
|---|
|  | 200 | existing=[ x[0].lower() for x in result ] | 
|---|
|  | 201 |  | 
|---|
|  | 202 | morituri=existing | 
|---|
|  | 203 |  | 
|---|
|  | 204 | if destbasedn.lower() in existing: | 
|---|
|  | 205 | morituri.remove(destbasedn.lower()) | 
|---|
|  | 206 |  | 
|---|
|  | 207 | for o in update_objects: | 
|---|
|  | 208 | dn,entry=o | 
|---|
|  | 209 | if dn.lower() in existing: | 
|---|
|  | 210 | morituri.remove(dn.lower()) | 
|---|
|  | 211 | for dn in morituri: | 
|---|
|  | 212 | if options.exclude != None and dn.lower().endswith(options.exclude): | 
|---|
|  | 213 | # print "ignoring",dn | 
|---|
|  | 214 | continue | 
|---|
|  | 215 |  | 
|---|
|  | 216 | try: | 
|---|
|  | 217 | con.delete_s(dn) | 
|---|
|  | 218 | except: | 
|---|
|  | 219 | print "failed to delete",dn | 
|---|
|  | 220 |  | 
|---|
|  | 221 | notify_deleted(dn) | 
|---|
|  | 222 | if options.verbose: | 
|---|
|  | 223 | print dn,"deleted" | 
|---|
|  | 224 | deleted=deleted+1 | 
|---|
|  | 225 |  | 
|---|
|  | 226 | con.unbind() | 
|---|
|  | 227 | print good,"entries created,",exist,"updated,",deleted,"deleted,",failed,"failed." | 
|---|
|  | 228 |  | 
|---|
|  | 229 |  | 
|---|
|  | 230 | if __name__ == "__main__": | 
|---|
|  | 231 | conffile="ldapsync.conf" | 
|---|
|  | 232 | filter = None | 
|---|
|  | 233 | exclude = None | 
|---|
|  | 234 | if len(sys.argv)>1: | 
|---|
|  | 235 | conffile=sys.argv[1] | 
|---|
|  | 236 |  | 
|---|
|  | 237 | config=ConfigParser.ConfigParser() | 
|---|
|  | 238 | config.read(conffile) | 
|---|
|  | 239 |  | 
|---|
|  | 240 | srcfile = None | 
|---|
|  | 241 | try: | 
|---|
|  | 242 | srcfile = config.get("source","file") | 
|---|
|  | 243 | except: | 
|---|
|  | 244 | pass | 
|---|
|  | 245 |  | 
|---|
|  | 246 | basedn = config.get("source","baseDn") | 
|---|
|  | 247 |  | 
|---|
|  | 248 | if srcfile==None: | 
|---|
|  | 249 | srv = config.get("source","server") | 
|---|
|  | 250 | admindn = config.get("source","bindDn") | 
|---|
|  | 251 | adminpw = config.get("source","bindPassword") | 
|---|
|  | 252 | filter = config.get("source","filter") | 
|---|
|  | 253 | starttls = config.getboolean("source","starttls") | 
|---|
|  | 254 |  | 
|---|
|  | 255 | if filter==None: | 
|---|
|  | 256 | filter = '(objectClass=*)' | 
|---|
|  | 257 |  | 
|---|
|  | 258 | options = Options() | 
|---|
|  | 259 |  | 
|---|
|  | 260 | try: | 
|---|
|  | 261 | options.exclude = config.get("destination","excludesubtree").lower() | 
|---|
|  | 262 | except: | 
|---|
|  | 263 | pass | 
|---|
|  | 264 |  | 
|---|
|  | 265 | destsrv = config.get("destination","server") | 
|---|
|  | 266 | destadmindn = config.get("destination","bindDn") | 
|---|
|  | 267 | destadminpw = config.get("destination","bindPassword") | 
|---|
|  | 268 | destbasedn = config.get("destination","baseDn") | 
|---|
|  | 269 | destdelete = config.getboolean("destination","delete") | 
|---|
|  | 270 | rdn = config.get("destination","rdn") | 
|---|
|  | 271 |  | 
|---|
|  | 272 | try: | 
|---|
|  | 273 | options.updateonly = not config.getboolean("destination","create") | 
|---|
|  | 274 | except: | 
|---|
|  | 275 | options.updateonly = False | 
|---|
|  | 276 | options.starttls = config.getboolean("destination","starttls") | 
|---|
|  | 277 | try: | 
|---|
|  | 278 | options.attrfilter = config.get("destination","attributes").split(",") | 
|---|
|  | 279 | except: | 
|---|
|  | 280 | options.attrfilter = None | 
|---|
|  | 281 |  | 
|---|
|  | 282 | try: | 
|---|
|  | 283 | options.renameattr = config.get("destination","detectRename") | 
|---|
|  | 284 | except: | 
|---|
|  | 285 | options.renameattr = None | 
|---|
|  | 286 |  | 
|---|
|  | 287 | try: | 
|---|
|  | 288 | options.renamecommand = config.get("destination","detectRename") | 
|---|
|  | 289 | except: | 
|---|
|  | 290 | options.renamecommand = None | 
|---|
|  | 291 |  | 
|---|
|  | 292 | if srcfile: | 
|---|
|  | 293 | result = readLDIFSource(srcfile) | 
|---|
|  | 294 | else: | 
|---|
|  | 295 | result = readLdapSource(srv,admindn,adminpw,basedn,filter,starttls) | 
|---|
|  | 296 |  | 
|---|
|  | 297 | try: | 
|---|
|  | 298 | pwd_max_days = int(config.get("source","pwd_max_days")) | 
|---|
|  | 299 | except: | 
|---|
|  | 300 | pwd_max_days = 0 | 
|---|
|  | 301 |  | 
|---|
|  | 302 | syncLdapDestination(result,destsrv,destadmindn,destadminpw,basedn,destbasedn,rdn,options) | 
|---|