1""" 2ldap.syncrepl - for implementing syncrepl consumer (see RFC 4533) 3 4See https://www.python-ldap.org/ for project details. 5""" 6 7from uuid import UUID 8 9# Imports from pyasn1 10from pyasn1.type import tag, namedtype, namedval, univ, constraint 11from pyasn1.codec.ber import encoder, decoder 12 13from ldap.pkginfo import __version__, __author__, __license__ 14from ldap.controls import RequestControl, ResponseControl, KNOWN_RESPONSE_CONTROLS 15 16__all__ = [ 17 'SyncreplConsumer', 18] 19 20 21class SyncUUID(univ.OctetString): 22 """ 23 syncUUID ::= OCTET STRING (SIZE(16)) 24 """ 25 subtypeSpec = constraint.ValueSizeConstraint(16, 16) 26 27 28class SyncCookie(univ.OctetString): 29 """ 30 syncCookie ::= OCTET STRING 31 """ 32 33 34class SyncRequestMode(univ.Enumerated): 35 """ 36 mode ENUMERATED { 37 -- 0 unused 38 refreshOnly (1), 39 -- 2 reserved 40 refreshAndPersist (3) 41 }, 42 """ 43 namedValues = namedval.NamedValues( 44 ('refreshOnly', 1), 45 ('refreshAndPersist', 3) 46 ) 47 subtypeSpec = univ.Enumerated.subtypeSpec + constraint.SingleValueConstraint(1, 3) 48 49 50class SyncRequestValue(univ.Sequence): 51 """ 52 syncRequestValue ::= SEQUENCE { 53 mode ENUMERATED { 54 -- 0 unused 55 refreshOnly (1), 56 -- 2 reserved 57 refreshAndPersist (3) 58 }, 59 cookie syncCookie OPTIONAL, 60 reloadHint BOOLEAN DEFAULT FALSE 61 } 62 """ 63 componentType = namedtype.NamedTypes( 64 namedtype.NamedType('mode', SyncRequestMode()), 65 namedtype.OptionalNamedType('cookie', SyncCookie()), 66 namedtype.DefaultedNamedType('reloadHint', univ.Boolean(False)) 67 ) 68 69 70class SyncRequestControl(RequestControl): 71 """ 72 The Sync Request Control is an LDAP Control [RFC4511] where the 73 controlType is the object identifier 1.3.6.1.4.1.4203.1.9.1.1 and the 74 controlValue, an OCTET STRING, contains a BER-encoded 75 syncRequestValue. The criticality field is either TRUE or FALSE. 76 [..] 77 The Sync Request Control is only applicable to the SearchRequest 78 Message. 79 """ 80 controlType = '1.3.6.1.4.1.4203.1.9.1.1' 81 82 def __init__(self, criticality=1, cookie=None, mode='refreshOnly', reloadHint=False): 83 self.criticality = criticality 84 self.cookie = cookie 85 self.mode = mode 86 self.reloadHint = reloadHint 87 88 def encodeControlValue(self): 89 rcv = SyncRequestValue() 90 rcv.setComponentByName('mode', SyncRequestMode(self.mode)) 91 if self.cookie is not None: 92 rcv.setComponentByName('cookie', SyncCookie(self.cookie)) 93 if self.reloadHint: 94 rcv.setComponentByName('reloadHint', univ.Boolean(self.reloadHint)) 95 return encoder.encode(rcv) 96 97 98class SyncStateOp(univ.Enumerated): 99 """ 100 state ENUMERATED { 101 present (0), 102 add (1), 103 modify (2), 104 delete (3) 105 }, 106 """ 107 namedValues = namedval.NamedValues( 108 ('present', 0), 109 ('add', 1), 110 ('modify', 2), 111 ('delete', 3) 112 ) 113 subtypeSpec = univ.Enumerated.subtypeSpec + constraint.SingleValueConstraint(0, 1, 2, 3) 114 115 116class SyncStateValue(univ.Sequence): 117 """ 118 syncStateValue ::= SEQUENCE { 119 state ENUMERATED { 120 present (0), 121 add (1), 122 modify (2), 123 delete (3) 124 }, 125 entryUUID syncUUID, 126 cookie syncCookie OPTIONAL 127 } 128 """ 129 componentType = namedtype.NamedTypes( 130 namedtype.NamedType('state', SyncStateOp()), 131 namedtype.NamedType('entryUUID', SyncUUID()), 132 namedtype.OptionalNamedType('cookie', SyncCookie()) 133 ) 134 135 136class SyncStateControl(ResponseControl): 137 """ 138 The Sync State Control is an LDAP Control [RFC4511] where the 139 controlType is the object identifier 1.3.6.1.4.1.4203.1.9.1.2 and the 140 controlValue, an OCTET STRING, contains a BER-encoded SyncStateValue. 141 The criticality is FALSE. 142 [..] 143 The Sync State Control is only applicable to SearchResultEntry and 144 SearchResultReference Messages. 145 """ 146 controlType = '1.3.6.1.4.1.4203.1.9.1.2' 147 opnames = ('present', 'add', 'modify', 'delete') 148 149 def decodeControlValue(self, encodedControlValue): 150 d = decoder.decode(encodedControlValue, asn1Spec=SyncStateValue()) 151 state = d[0].getComponentByName('state') 152 uuid = UUID(bytes=bytes(d[0].getComponentByName('entryUUID'))) 153 cookie = d[0].getComponentByName('cookie') 154 if cookie is not None and cookie.hasValue(): 155 self.cookie = str(cookie) 156 else: 157 self.cookie = None 158 self.state = self.__class__.opnames[int(state)] 159 self.entryUUID = str(uuid) 160 161KNOWN_RESPONSE_CONTROLS[SyncStateControl.controlType] = SyncStateControl 162 163 164class SyncDoneValue(univ.Sequence): 165 """ 166 syncDoneValue ::= SEQUENCE { 167 cookie syncCookie OPTIONAL, 168 refreshDeletes BOOLEAN DEFAULT FALSE 169 } 170 """ 171 componentType = namedtype.NamedTypes( 172 namedtype.OptionalNamedType('cookie', SyncCookie()), 173 namedtype.DefaultedNamedType('refreshDeletes', univ.Boolean(False)) 174 ) 175 176 177class SyncDoneControl(ResponseControl): 178 """ 179 The Sync Done Control is an LDAP Control [RFC4511] where the 180 controlType is the object identifier 1.3.6.1.4.1.4203.1.9.1.3 and the 181 controlValue contains a BER-encoded syncDoneValue. The criticality 182 is FALSE (and hence absent). 183 [..] 184 The Sync Done Control is only applicable to the SearchResultDone 185 Message. 186 """ 187 controlType = '1.3.6.1.4.1.4203.1.9.1.3' 188 189 def decodeControlValue(self, encodedControlValue): 190 d = decoder.decode(encodedControlValue, asn1Spec=SyncDoneValue()) 191 cookie = d[0].getComponentByName('cookie') 192 if cookie.hasValue(): 193 self.cookie = str(cookie) 194 else: 195 self.cookie = None 196 refresh_deletes = d[0].getComponentByName('refreshDeletes') 197 if refresh_deletes.hasValue(): 198 self.refreshDeletes = bool(refresh_deletes) 199 else: 200 self.refreshDeletes = None 201 202KNOWN_RESPONSE_CONTROLS[SyncDoneControl.controlType] = SyncDoneControl 203 204 205class RefreshDelete(univ.Sequence): 206 """ 207 refreshDelete [1] SEQUENCE { 208 cookie syncCookie OPTIONAL, 209 refreshDone BOOLEAN DEFAULT TRUE 210 }, 211 """ 212 componentType = namedtype.NamedTypes( 213 namedtype.OptionalNamedType('cookie', SyncCookie()), 214 namedtype.DefaultedNamedType('refreshDone', univ.Boolean(True)) 215 ) 216 217 218class RefreshPresent(univ.Sequence): 219 """ 220 refreshPresent [2] SEQUENCE { 221 cookie syncCookie OPTIONAL, 222 refreshDone BOOLEAN DEFAULT TRUE 223 }, 224 """ 225 componentType = namedtype.NamedTypes( 226 namedtype.OptionalNamedType('cookie', SyncCookie()), 227 namedtype.DefaultedNamedType('refreshDone', univ.Boolean(True)) 228 ) 229 230 231class SyncUUIDs(univ.SetOf): 232 """ 233 syncUUIDs SET OF syncUUID 234 """ 235 componentType = SyncUUID() 236 237 238class SyncIdSet(univ.Sequence): 239 """ 240 syncIdSet [3] SEQUENCE { 241 cookie syncCookie OPTIONAL, 242 refreshDeletes BOOLEAN DEFAULT FALSE, 243 syncUUIDs SET OF syncUUID 244 } 245 """ 246 componentType = namedtype.NamedTypes( 247 namedtype.OptionalNamedType('cookie', SyncCookie()), 248 namedtype.DefaultedNamedType('refreshDeletes', univ.Boolean(False)), 249 namedtype.NamedType('syncUUIDs', SyncUUIDs()) 250 ) 251 252 253class SyncInfoValue(univ.Choice): 254 """ 255 syncInfoValue ::= CHOICE { 256 newcookie [0] syncCookie, 257 refreshDelete [1] SEQUENCE { 258 cookie syncCookie OPTIONAL, 259 refreshDone BOOLEAN DEFAULT TRUE 260 }, 261 refreshPresent [2] SEQUENCE { 262 cookie syncCookie OPTIONAL, 263 refreshDone BOOLEAN DEFAULT TRUE 264 }, 265 syncIdSet [3] SEQUENCE { 266 cookie syncCookie OPTIONAL, 267 refreshDeletes BOOLEAN DEFAULT FALSE, 268 syncUUIDs SET OF syncUUID 269 } 270 } 271 """ 272 componentType = namedtype.NamedTypes( 273 namedtype.NamedType( 274 'newcookie', 275 SyncCookie().subtype( 276 implicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 0) 277 ) 278 ), 279 namedtype.NamedType( 280 'refreshDelete', 281 RefreshDelete().subtype( 282 implicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 1) 283 ) 284 ), 285 namedtype.NamedType( 286 'refreshPresent', 287 RefreshPresent().subtype( 288 implicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 2) 289 ) 290 ), 291 namedtype.NamedType( 292 'syncIdSet', 293 SyncIdSet().subtype( 294 implicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 3) 295 ) 296 ) 297 ) 298 299 300class SyncInfoMessage: 301 """ 302 The Sync Info Message is an LDAP Intermediate Response Message 303 [RFC4511] where responseName is the object identifier 304 1.3.6.1.4.1.4203.1.9.1.4 and responseValue contains a BER-encoded 305 syncInfoValue. The criticality is FALSE (and hence absent). 306 """ 307 responseName = '1.3.6.1.4.1.4203.1.9.1.4' 308 309 def __init__(self, encodedMessage): 310 d = decoder.decode(encodedMessage, asn1Spec=SyncInfoValue()) 311 self.newcookie = None 312 self.refreshDelete = None 313 self.refreshPresent = None 314 self.syncIdSet = None 315 316 # Due to the way pyasn1 works, refreshDelete and refreshPresent are both 317 # valid in the components as they are fully populated defaults. We must 318 # get the component directly from the message, not by iteration. 319 attr = d[0].getName() 320 comp = d[0].getComponent() 321 322 if comp is not None and comp.hasValue(): 323 if attr == 'newcookie': 324 self.newcookie = str(comp) 325 return 326 327 val = {} 328 329 cookie = comp.getComponentByName('cookie') 330 if cookie.hasValue(): 331 val['cookie'] = str(cookie) 332 333 if attr.startswith('refresh'): 334 val['refreshDone'] = bool(comp.getComponentByName('refreshDone')) 335 elif attr == 'syncIdSet': 336 uuids = [] 337 ids = comp.getComponentByName('syncUUIDs') 338 for i in range(len(ids)): 339 uuid = UUID(bytes=bytes(ids.getComponentByPosition(i))) 340 uuids.append(str(uuid)) 341 val['syncUUIDs'] = uuids 342 val['refreshDeletes'] = bool(comp.getComponentByName('refreshDeletes')) 343 344 setattr(self, attr, val) 345 346 347class SyncreplConsumer: 348 """ 349 SyncreplConsumer - LDAP syncrepl consumer object. 350 """ 351 352 def syncrepl_search(self, base, scope, mode='refreshOnly', cookie=None, **search_args): 353 """ 354 Starts syncrepl search operation. 355 356 base, scope, and search_args are passed along to 357 self.search_ext unmodified (aside from adding a Sync 358 Request control to any serverctrls provided). 359 360 mode provides syncrepl mode. Can be 'refreshOnly' 361 to finish after synchronization, or 362 'refreshAndPersist' to persist (continue to 363 receive updates) after synchronization. 364 365 cookie: an opaque value representing the replication 366 state of the client. Subclasses should override 367 the syncrepl_set_cookie() and syncrepl_get_cookie() 368 methods to store the cookie appropriately, rather than 369 passing it. 370 371 Only a single syncrepl search may be active on a SyncreplConsumer 372 object. Multiple concurrent syncrepl searches require multiple 373 separate SyncreplConsumer objects and thus multiple connections 374 (LDAPObject instances). 375 """ 376 if cookie is None: 377 cookie = self.syncrepl_get_cookie() 378 379 syncreq = SyncRequestControl(cookie=cookie, mode=mode) 380 381 if 'serverctrls' in search_args: 382 search_args['serverctrls'] += [syncreq] 383 else: 384 search_args['serverctrls'] = [syncreq] 385 386 self.__refreshDone = False 387 return self.search_ext(base, scope, **search_args) 388 389 def syncrepl_poll(self, msgid=-1, timeout=None, all=0): 390 """ 391 polls for and processes responses to the syncrepl_search() operation. 392 Returns False when operation finishes, True if it is in progress, or 393 raises an exception on error. 394 395 If timeout is specified, raises ldap.TIMEOUT in the event of a timeout. 396 397 If all is set to a nonzero value, poll() will return only when finished 398 or when an exception is raised. 399 400 """ 401 while True: 402 type, msg, mid, ctrls, n, v = self.result4( 403 msgid=msgid, 404 timeout=timeout, 405 add_intermediates=1, 406 add_ctrls=1, 407 all=0, 408 ) 409 410 if type == 101: 411 # search result. This marks the end of a refreshOnly session. 412 # look for a SyncDone control, save the cookie, and if necessary 413 # delete non-present entries. 414 for c in ctrls: 415 if c.__class__.__name__ != 'SyncDoneControl': 416 continue 417 self.syncrepl_present(None, refreshDeletes=c.refreshDeletes) 418 if c.cookie is not None: 419 self.syncrepl_set_cookie(c.cookie) 420 421 return False 422 423 elif type == 100: 424 # search entry with associated SyncState control 425 for m in msg: 426 dn, attrs, ctrls = m 427 for c in ctrls: 428 if c.__class__.__name__ != 'SyncStateControl': 429 continue 430 if c.state == 'present': 431 self.syncrepl_present([c.entryUUID]) 432 elif c.state == 'delete': 433 self.syncrepl_delete([c.entryUUID]) 434 else: 435 self.syncrepl_entry(dn, attrs, c.entryUUID) 436 if self.__refreshDone is False: 437 self.syncrepl_present([c.entryUUID]) 438 if c.cookie is not None: 439 self.syncrepl_set_cookie(c.cookie) 440 break 441 442 elif type == 121: 443 # Intermediate message. If it is a SyncInfoMessage, parse it 444 for m in msg: 445 rname, resp, ctrls = m 446 if rname != SyncInfoMessage.responseName: 447 continue 448 sim = SyncInfoMessage(resp) 449 if sim.newcookie is not None: 450 self.syncrepl_set_cookie(sim.newcookie) 451 elif sim.refreshPresent is not None: 452 self.syncrepl_present(None, refreshDeletes=False) 453 if 'cookie' in sim.refreshPresent: 454 self.syncrepl_set_cookie(sim.refreshPresent['cookie']) 455 if sim.refreshPresent['refreshDone']: 456 self.__refreshDone = True 457 self.syncrepl_refreshdone() 458 elif sim.refreshDelete is not None: 459 self.syncrepl_present(None, refreshDeletes=True) 460 if 'cookie' in sim.refreshDelete: 461 self.syncrepl_set_cookie(sim.refreshDelete['cookie']) 462 if sim.refreshDelete['refreshDone']: 463 self.__refreshDone = True 464 self.syncrepl_refreshdone() 465 elif sim.syncIdSet is not None: 466 if sim.syncIdSet['refreshDeletes'] is True: 467 self.syncrepl_delete(sim.syncIdSet['syncUUIDs']) 468 else: 469 self.syncrepl_present(sim.syncIdSet['syncUUIDs']) 470 if 'cookie' in sim.syncIdSet: 471 self.syncrepl_set_cookie(sim.syncIdSet['cookie']) 472 473 if all == 0: 474 return True 475 476 477 # virtual methods -- subclass must override these to do useful work 478 479 def syncrepl_set_cookie(self, cookie): 480 """ 481 Called by syncrepl_poll() to store a new cookie provided by the server. 482 """ 483 pass 484 485 def syncrepl_get_cookie(self): 486 """ 487 Called by syncrepl_search() to retrieve the cookie stored by syncrepl_set_cookie() 488 """ 489 pass 490 491 def syncrepl_present(self, uuids, refreshDeletes=False): 492 """ 493 Called by syncrepl_poll() whenever entry UUIDs are presented to the client. 494 syncrepl_present() is given a list of entry UUIDs (uuids) and a flag 495 (refreshDeletes) which indicates whether the server explicitly deleted 496 non-present entries during the refresh operation. 497 498 If called with a list of uuids, the syncrepl_present() implementation 499 should record those uuids as present in the directory. 500 501 If called with uuids set to None and refreshDeletes set to False, 502 syncrepl_present() should delete all non-present entries from the local 503 mirror, and reset the list of recorded uuids. 504 505 If called with uuids set to None and refreshDeletes set to True, 506 syncrepl_present() should reset the list of recorded uuids, without 507 deleting any entries. 508 """ 509 pass 510 511 def syncrepl_delete(self, uuids): 512 """ 513 Called by syncrepl_poll() to delete entries. A list 514 of UUIDs of the entries to be deleted is given in the 515 uuids parameter. 516 """ 517 pass 518 519 def syncrepl_entry(self, dn, attrs, uuid): 520 """ 521 Called by syncrepl_poll() for any added or modified entries. 522 523 The provided uuid is used to identify the provided entry in 524 any future modification (including dn modification), deletion, 525 and presentation operations. 526 """ 527 pass 528 529 def syncrepl_refreshdone(self): 530 """ 531 Called by syncrepl_poll() between refresh and persist phase. 532 533 It indicates that initial synchronization is done and persist phase 534 follows. 535 """ 536 pass 537