[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)
|
---|