Диаграммы последовательностей работы коннектора AD Exchange#

Данный документ содержит диаграммы последовательностей (PlantUML), описывающие алгоритмы работы коннектора Microsoft Active Directory и Exchange.

Тестирование подключения (test)#

@startuml
title Диаграмма последовательности: Тестирование подключения

participant "IDM Framework" as Framework
participant "ADExchangeConnector" as Connector
participant "ConnectorHandler" as Handler
participant "MailService" as Mail
participant "ADService" as AD
participant "LdapClient" as Ldap

Framework -> Connector: test()
activate Connector
Connector -> Handler: testConnection()
activate Handler

note right: Тестирование Exchange
Handler -> Mail: testConnection()
activate Mail
Mail -> Mail: session.testConnection()
Mail --> Handler: OK/KO
deactivate Mail

note right: Тестирование AD
Handler -> AD: createAdServiceIfNot()
activate AD
AD -> Ldap: new LdapClient(configuration)
activate Ldap
Ldap -> Ldap: establishConnection()
Ldap --> Ldap: bind (SASL_GSSAPI/SIMPLE)
Ldap --> AD: подключение установлено
deactivate Ldap
AD -> Ldap: getRootDseFresh()
Ldap --> AD: RootDSE
AD --> Handler: OK
deactivate AD

Handler --> Connector: успешно
deactivate Handler
Connector --> Framework: test завершён
deactivate Connector
@enduml

Создание объекта (create)#

@startuml
title Диаграмма последовательности: Создание объекта AD + Exchange

participant "IDM Framework" as Framework
participant "ADExchangeConnector" as Connector
participant "ConnectorHandler" as Handler
participant "ADService" as AD
participant "MailService" as Mail
participant "LdapClient" as Ldap
participant "AttributesProcessorHolder" as Processors

Framework -> Connector: create(objectClass, attributes)
activate Connector
Connector -> Handler: create(objectClass, attributes)
activate Handler
Handler -> Handler: connectIfNot()

note right: Поиск существующего объекта\nпо correlation attributes
Handler -> AD: createOrUpdate(dn, objectClass, attributesMap)
activate AD
AD -> AD: searchUserByCorrelationAttributes()
AD -> Ldap: searchByObjectClass(filter)
Ldap --> AD: Optional<Entry>

alt объект не найден
    note right: Создание нового объекта AD
    AD -> Processors: processPreCreateAttributes()
    Processors --> AD: обработанные атрибуты
    AD -> Ldap: addEntry(entry)
    activate Ldap
    Ldap -> Ldap: LDAP ADD
    Ldap --> AD: entry added
    AD -> Ldap: searchByDn(dn)
    Ldap --> AD: Entry с objectGUID
    AD -> AD: extractObjectGuid()
    AD -> Processors: processCreateAttributes()
    Processors --> AD: post-create обработка
    AD --> Handler: uid (GUID)
    deactivate Ldap
else объект найден
    note right: Обновление существующего
    AD -> AD: updateInternal()
    AD --> Handler: uid (GUID)
end

alt тип объекта имеет mailbox
    Handler -> Mail: createOrUpdate(uid, attributesMap)
    activate Mail
    Mail -> Mail: mailboxExists()?
    alt mailbox не существует
        Mail -> Mail: EnableMailboxCommand
        Mail -> Mail: SetMailboxCommand
        Mail -> Mail: SetCASMailboxCommand
    else mailbox существует
        Mail -> Mail: SetMailboxCommand
        Mail -> Mail: SetCASMailboxCommand
    end
    Mail -> Mail: session.executeCommands()
    Mail --> Handler: готово
    deactivate Mail
end

Handler --> Connector: Uid
deactivate Handler
Connector --> Framework: Uid
deactivate Connector
@enduml

Обновление объекта (update)#

@startuml
title Диаграмма последовательности: Обновление объекта по GUID

participant "IDM Framework" as Framework
participant "ADExchangeConnector" as Connector
participant "ConnectorHandler" as Handler
participant "ADService" as AD
participant "MailService" as Mail
participant "LdapClient" as Ldap
participant "AttributesProcessorHolder" as Processors

Framework -> Connector: update(uid, attributes)
activate Connector
Connector -> Handler: update(uid, attributes)
activate Handler
Handler -> Handler: connectIfNot()

note right: Поиск объекта по GUID
Handler -> AD: update(guid, objectClass, attributesMap)
activate AD
AD -> Ldap: searchByGUID(guid)
activate Ldap
Ldap -> Ldap: LDAP search\n(objectGUID=binary)
Ldap --> AD: Optional<Entry>

alt объект не найден
    AD --> Handler: UnknownUidException
    deactivate Ldap
    deactivate AD
else объект найден
    AD -> AD: updateInternal(entry, guid, objectClass, attributesMap, UPDATE)
    
    note right: Предварительная обработка
    AD -> Processors: processPreUpdateAttributes()
    Processors --> AD: postUpdateMap
    
    note right: Формирование LDAP модификаций
    AD -> AD: для каждого атрибута:\n- определить тип операции\n  (ADD/REPLACE/REMOVE)
    AD -> Ldap: modify(dn, attributes, newAttributes)
    activate Ldap
    Ldap -> Ldap: LDAP MODIFY
    Ldap --> AD: modified
    deactivate Ldap
    
    note right: Пост-обработка
    AD -> Processors: processUpdateAttributes()
    Processors --> AD: готово
    
    alt изменён DN
        AD -> Ldap: moveObject(oldDn, newDn)
        activate Ldap
        Ldap -> Ldap: LDAP MODIFY DN
        Ldap --> AD: moved
        deactivate Ldap
    end
    
    AD --> Handler: uid
    deactivate AD
end

alt тип объекта имеет mailbox
    Handler -> Mail: createOrUpdate(guid, attributesMap)
    activate Mail
    Mail -> Mail: update()
    Mail -> Mail: session.executeCommands()
    Mail --> Handler: готово
    deactivate Mail
end

Handler --> Connector: Uid
deactivate Handler
Connector --> Framework: Uid
deactivate Connector
@enduml

Обновление объекта (updateDelta)#

@startuml
title Диаграмма последовательности: Обновление объекта (Delta)

participant "IDM Framework" as Framework
participant "ADExchangeConnector" as Connector
participant "ConnectorHandler" as Handler
participant "ADService" as AD
participant "MailService" as Mail
participant "LdapClient" as Ldap

Framework -> Connector: updateDelta(uid, attributeDeltas)
activate Connector
Connector -> Handler: updateDelta(uid, deltas)
activate Handler
Handler -> Handler: connectIfNot()

Handler -> AD: updateDelta(guid, objectClass, deltas)
activate AD
AD -> Ldap: searchByGUID(guid)
activate Ldap
Ldap --> AD: Entry
deactivate Ldap

AD -> AD: updateDeltaInternal(entry, uid, deltas, UPDATE)

note right: Обработка дельт
AD -> AD: для каждой AttributeDelta:\n- valuesToReplace\n- valuesToAdd\n- valuesToRemove

AD -> AD: сформировать:\n- attributesToReplace\n- attributesToAdd\n- attributesToRemove

AD -> Ldap: modify(dn, replace, add, remove)
activate Ldap
Ldap -> Ldap: LDAP MODIFY (batch)
Ldap --> AD: modified
deactivate Ldap

alt изменён DN
    AD -> Ldap: moveObject(oldDn, newDn)
    activate Ldap
    Ldap --> AD: moved
    deactivate Ldap
end

AD --> Handler: uid
deactivate AD

alt тип объекта имеет mailbox
    Handler -> Mail: createOrUpdateDelta(guid, deltas)
    activate Mail
    Mail -> Mail: отфильтровать атрибуты Exchange
    Mail -> Mail: update()
    Mail -> Mail: session.executeCommands()
    Mail --> Handler: готово
    deactivate Mail
end

Handler --> Connector: Uid
deactivate Handler
Connector --> Framework: Uid
deactivate Connector
@enduml

Удаление объекта (delete)#

@startuml
title Диаграмма последовательности: Удаление объекта

participant "IDM Framework" as Framework
participant "ADExchangeConnector" as Connector
participant "ConnectorHandler" as Handler
participant "ADService" as AD
participant "LdapClient" as Ldap

Framework -> Connector: delete(uid, objectClass)
activate Connector
Connector -> Handler: delete(uid, objectClass)
activate Handler
Handler -> Handler: connectIfNot()

Handler -> AD: delete(uid, objectClass)
activate AD
AD -> Ldap: searchByGUID(uid)
activate Ldap
Ldap -> Ldap: LDAP search
Ldap --> AD: Optional<Entry>

alt объект не найден
    AD --> Handler: UnknownUidException
    deactivate Ldap
    deactivate AD
else объект найден
    AD -> AD: extractDn(entry)
    AD -> Ldap: delete(dn)
    activate Ldap
    Ldap -> Ldap: LDAP DELETE
    Ldap --> AD: deleted
    deactivate Ldap
    AD --> Handler: готово
    deactivate AD
end

Handler --> Connector: void
deactivate Handler
Connector --> Framework: завершено
deactivate Connector
@enduml

Поиск объектов (search/query)#

@startuml
title Диаграмма последовательности: Поиск объектов

participant "IDM Framework" as Framework
participant "ADExchangeConnector" as Connector
participant "ConnectorHandler" as Handler
participant "ADService" as AD
participant "MailService" as Mail
participant "LdapClient" as Ldap
participant "ResultsHandler" as Results

Framework -> Connector: executeQuery(objectClass, filter, resultsHandler)
activate Connector
Connector -> Handler: execQuery(objectClass, filter, resultsHandler, options)
activate Handler
Handler -> Handler: connectIfNot()

alt фильтр по GUID
    Handler -> AD: getAccountByGUID(uid, objectClass)
    activate AD
    AD -> Ldap: searchByGUID(uid)
    Ldap --> AD: Entry
    AD -> AD: entry2ConnectorObject()
    AD --> Handler: ADObject
    deactivate AD
    
    alt тип имеет mailbox
        Handler -> Mail: getAccountByName(uid)
        activate Mail
        Mail -> Mail: GetMailboxCommand + GetCASMailboxCommand
        Mail -> Mail: session.executeCommands()
        Mail --> Handler: ExchangeMailbox
        deactivate Mail
    end
    
    Handler -> Handler: toConnectorObject(ADObject, ExchangeMailbox)
    Handler -> Results: handle(connectorObject)
else фильтр по DN
    Handler -> AD: getAccountByDn(dn, objectClass)
    activate AD
    AD -> Ldap: searchByDn(objectClass, dn)
    Ldap --> AD: Entry
    AD -> AD: entry2ConnectorObject()
    AD --> Handler: ADObject
    deactivate AD
    
    alt тип имеет mailbox
        Handler -> Mail: getAccountByName(uid)
        activate Mail
        Mail --> Handler: ExchangeMailbox
        deactivate Mail
    end
    
    Handler -> Handler: toConnectorObject()
    Handler -> Results: handle(connectorObject)
else обычный поиск
    Handler -> AD: getAccountsByObjectClass(objectClass, function, options, filter)
    activate AD
    AD -> Ldap: searchByObjectClass(objectClass, function, filter, options)
    activate Ldap
    
    loop для каждой записи
        Ldap -> Ldap: LDAP SEARCH\n(с pagination: VLV или SPR)
        Ldap --> AD: Entry
        AD -> AD: entry2ConnectorObject()
        AD --> Handler: ADObject
        
        alt тип имеет mailbox
            Handler -> Mail: getAccountByName(uid)
            activate Mail
            Mail --> Handler: ExchangeMailbox
            deactivate Mail
        end
        
        Handler -> Handler: toConnectorObject()
        Handler -> Results: handle(connectorObject)
    end
    
    deactivate Ldap
    deactivate AD
end

Handler --> Connector: завершено
deactivate Handler
Connector --> Framework: завершено
deactivate Connector
@enduml

Синхронизация (sync)#

@startuml
title Диаграмма последовательности: Синхронизация изменений

participant "IDM Framework" as Framework
participant "ADExchangeConnector" as Connector
participant "ConnectorHandler" as Handler
participant "ADService" as AD
participant "MailService" as Mail
participant "LdapClient" as Ldap
participant "SyncResultsHandler" as SyncHandler

Framework -> Connector: sync(objectClass, syncToken, syncResultsHandler)
activate Connector
Connector -> Handler: sync(objectClass, syncToken, syncResultsHandler)
activate Handler
Handler -> Handler: connectIfNot()

alt новый токен (null)
    Handler -> Handler: handleNewToken()
    Handler -> Handler: newSyncToken()
    Handler -> SyncHandler: handleResult(SyncToken(newToken))
else существующий токен
    Handler -> Handler: doSync(syncToken, syncResultsHandler, objectClass)
    
    note right: Получение токена времени\n(GeneralizedTime)
    Handler -> Handler: timeToken = syncToken.getValue()
    Handler -> Handler: finalToken = newSyncToken()
    
    Handler -> AD: findUpdatedEntriesFromTimestamp(objectClass, timeToken, function)
    activate AD
    
    note right: Поиск по whenChanged >= timeToken
    AD -> Ldap: searchByObjectClass(\n  filter=(whenChanged>=timeToken)\n)
    activate Ldap
    
    loop для каждого изменённого объекта
        Ldap --> AD: Entry
        AD -> AD: entry2ConnectorObject()
        AD --> Handler: ADObject
        
        alt тип имеет mailbox
            Handler -> Mail: getAccountByName()
            activate Mail
            Mail --> Handler: ExchangeMailbox
            deactivate Mail
        end
        
        Handler -> Handler: toConnectorObject()
        Handler -> Handler: buildSyncDelta(UPDATE, finalToken, connectorObject)
        Handler -> SyncHandler: handle(SyncDelta)
    end
    
    deactivate Ldap
    deactivate AD
    
    Handler -> SyncHandler: handleResult(finalToken)
end

Handler --> Connector: завершено
deactivate Handler
Connector --> Framework: завершено
deactivate Connector
@enduml

Получение последнего токена синхронизации#

@startuml
title Диаграмма последовательности: Получение последнего токена синхронизации

participant "IDM Framework" as Framework
participant "ADExchangeConnector" as Connector
participant "ConnectorHandler" as Handler
participant "ADService" as AD

Framework -> Connector: getLatestSyncToken(objectClass)
activate Connector
Connector -> Handler: getLastSyncToken()
activate Handler
Handler -> Handler: connectIfNot()
Handler -> AD: (через connectIfNot)
activate AD

Handler -> Handler: newSyncToken()
note right: Создание токена\nна основе текущего UTC времени
Handler -> Handler: OffsetDateTime.now(ZoneOffset.UTC)
Handler -> Handler: FormatUtil.constructGeneralizedTimeString(now)

Handler --> Connector: SyncToken(token)
deactivate AD
deactivate Handler
Connector --> Framework: SyncToken
deactivate Connector
@enduml

Выполнение скрипта (runScriptOnResource)#

@startuml
title Диаграмма последовательности: Выполнение скрипта searchByCorrelation

participant "IDM Framework" as Framework
participant "ADExchangeConnector" as Connector
participant "ConnectorHandler" as Handler
participant "ADService" as AD
participant "LdapClient" as Ldap

Framework -> Connector: runScriptOnResource(scriptContext, options)
activate Connector
Connector -> Handler: runScriptOnResource(scriptContext)
activate Handler

note right: Парсинг скрипта\n"searchByCorrelation#args{...}"
Handler -> Handler: split("#args")
Handler -> Handler: parse JSON аргументы

alt некорректный скрипт
    Handler --> Connector: IllegalArgumentException
    deactivate Handler
    Connector --> Framework: ConnectorException
    deactivate Connector
else searchByCorrelation
    Handler -> AD: searchUserByCorrelationAttributes(dn, objectClass, attributesMap)
    activate AD
    
    loop для каждой корреляции
        AD -> AD: создать фильтры из атрибутов
        AD -> Ldap: searchByObjectClass(filter)
        activate Ldap
        Ldap --> AD: List<Entry>
        deactivate Ldap
        
        alt найден 1 объект
            AD --> Handler: Optional<Entry>
            deactivate AD
        else найдено >1 объектов
            alt nestedSearchEnabled=true
                AD -> AD: refinedSearchUserByCorrelationAttributes()
                AD --> Handler: Optional<Entry>
            else
                AD --> Handler: ConnectorException
            end
        else не найдено
            AD -> AD: следующая корреляция
        end
    end
    
    alt объект найден
        Handler -> AD: getEntryAttributes(entry, objectClass)
        activate AD
        AD -> AD: entry2ConnectorObject()
        AD --> Handler: Map<String, List<Object>>
        deactivate AD
        Handler --> Connector: атрибуты объекта
    else объект не найден
        Handler --> Connector: null
    end
end

deactivate Handler
deactivate Connector
@enduml

LDAP поиск с пагинацией (SPR - Simple Paged Results)#

@startuml
title Диаграмма последовательности: LDAP поиск с пагинацией (SPR)

participant "LdapClient" as Client
participant "Apache LDAP API" as API
participant "AD Server" as Server

Client -> API: create SearchRequest
note right: Параметры:\n- baseDn\n- filter\n- scope=SUBTREE

loop пока есть страницы
    Client -> API: добавить PagedResults control\n(size=pageSize, cookie)
    Client -> API: добавить SortRequest control\n(sortKey="cn")
    Client -> API: connection.search(searchRequest)
    API -> Server: LDAP SEARCH REQUEST
    Server --> API: SEARCH RESULT ENTRY (page)
    
    loop для каждой записи на странице
        API --> Client: SearchResultEntry
        Client -> Client: function.apply(entry)
        alt function вернул false
            Client -> API: закрыть курсор
            Client --> Client: завершить
        end
    end
    
    API --> Client: SearchResultDone\n(PagedResultsResponse)
    Client -> Client: cookie = response.getCookie()
    
    alt cookie == null или пустой
        Client --> Client: завершить
    end
end
@enduml

LDAP поиск с пагинацией (VLV - Virtual List View)#

@startuml
title Диаграмма последовательности: LDAP поиск с пагинацией (VLV)

participant "LdapClient" as Client
participant "Apache LDAP API" as API
participant "AD Server" as Server

Client -> API: create SearchRequest
note right: Параметры:\n- baseDn\n- filter\n- scope=SUBTREE

Client -> Client: offset = 1\ncontentCount = 0

loop пока не достигнут конец
    Client -> API: добавить VirtualListViewRequest\n(offset, beforeCount, afterCount, contentCount, cookie)
    Client -> API: добавить SortRequest control\n(sortKey="cn")
    Client -> API: connection.search(searchRequest)
    API -> Server: LDAP SEARCH REQUEST (VLV)
    Server --> API: SEARCH RESULT ENTRY
    
    loop для каждой записи
        API --> Client: SearchResultEntry
        Client -> Client: offset++
        Client -> Client: function.apply(entry)
    end
    
    API --> Client: SearchResultDone\n(VirtualListViewResponse)
    Client -> Client: contentCount = response.getContentCount()
    Client -> Client: cookie = response.getContextId()
    
    alt offset > contentCount
        Client --> Client: завершить
    else resultCode = OFFSET_RANGE_ERROR
        Client --> Client: завершить
    end
end
@enduml

Exchange PowerShell команды#

@startuml
title Диаграмма последовательности: Выполнение Exchange PowerShell команд

participant "MailService" as Service
participant "Session" as Session
participant "WinRmExchangeSession" as WinRm
participant "PowerShell" as PS

note right: Создание команд\nдля Enable-Mailbox
Service -> Service: EnableMailboxCommand.builder()\n  .setIdentity(name)\n  .setDatabase(db)\n  .setForce()\n  .build()

note right: Создание команд\nдля Set-Mailbox
Service -> Service: SetMailboxCommand.builder()\n  .setIdentity(name)\n  .setOtherAttribute("-Alias value")\n  .setOtherAttribute("-HiddenFromAddressListsEnabled $true")\n  .build()

note right: Создание команд\nдля Set-CASMailbox
Service -> Service: SetCASMailboxCommand.builder()\n  .setIdentity(name)\n  .setOtherAttribute("-OWAEnabled $true")\n  .setOtherAttribute("-ActiveSyncEnabled $false")\n  .build()

Service -> Session: executeCommands(cmd1, cmd2, ...)
activate Session

alt WinRM сессия
    Session -> WinRm: установить соединение
    WinRm -> WinRm: аутентификация (Kerberos/NTLM)
    WinRm -> PS: Invoke-Command { cmd1 }
    PS --> WinRm: результат
    WinRm -> PS: Invoke-Command { cmd2 }
    PS --> WinRm: результат
    WinRm -> PS: Invoke-Command { cmd3 }
    PS --> WinRm: результат
    WinRm --> Session: ExecuteCommandResult
    deactivate Session
else SSH сессия
    Session -> Session: SSH подключение
    Session -> Session: выполнить pwsh -Command "cmd1; cmd2; cmd3"
    Session --> Session: вывод
end

Session --> Service: результат выполнения
@enduml