Управление учетными записями из кадровых приказов 1C при помощи OpenIDM

Введение

Онбординг сотрудников может занимать довольно длительное время, особенно при трудоустройстве в большую компанию. Не раз и не два встречалась ситуация, что новые сотрудники буквально неделями ждут необходимых доступов. Компания теряет деньги, а сотрудники — мотивацию. OpenIDM поможет автоматизировать процесс онбординга и сократить время на выделение доступов. В статье рассмотрим случай, когда OpenIDM читает кадровые приказы из 1С, создает учетные записи и назначает им соответствующие роли. При создании учетной записи, генерирует новый пароль и отправляет на электронную почту. При кадровом переводе — назначает новую роль, а при увольнении — удаляет учетную запись.

Подготовка 1С

Для интеграции с OpenIDM будем использовать HTTP сервис, который будет возвращать JSON данные по сотрудникам и кадровым приказам. Данные по сотрудникам нужны для первоначальной синхронизации, а данные по приказам нужны, для того, чтобы выполнять синхронизацию итеративно, не затрагивая все учетные записи.

HTTP Сервис 1С

HTTP Сервис 1С

Исходный текст HTTP сервиса, разработанного для 1С Бухгалтерия приведен в листинге ниже:

HTTPIDMСервис

Функция ШаблонURLСотрудникиПолучить(Запрос) 
    Массив = ПолучитьСписокСотрудниковНаТекущуюДату();
    СтрокаJSON = ПолучитьJSON(Массив);
    
    Ответ = ПолучитьHTTPСервисОтвет();
    Ответ.УстановитьТелоИзСтроки(СтрокаJSON);
    
    Возврат Ответ;
КонецФункции

Функция ШаблонURLПриказыПолучить(Запрос)  
    getLatest = Запрос.ПараметрыЗапроса.Получить("getLatest");
    ПолучитьПоследнийПриказ = getLatest = "true";
    date = Запрос.ПараметрыЗапроса.Получить("date");
    ДатаПоследнегоПриказа = Дата("00010101");
    Если ЗначениеЗаполнено(date) Тогда
        ДатаПоследнегоПриказа = ПрочитатьДатуJSON(date, ФорматДатыJSON.ISO);	
    КонецЕсли;
                              
    Массив = ПолучитьСписокПриказов(ПолучитьПоследнийПриказ, ДатаПоследнегоПриказа);
    СтрокаJSON = ПолучитьJSON(Массив);
    
    Ответ = ПолучитьHTTPСервисОтвет();
    Ответ.УстановитьТелоИзСтроки(СтрокаJSON);
    
    Возврат Ответ;
КонецФункции        

Функция ПолучитьСписокПриказов(ПолучитьПоследнийПриказ, ДатаПоследнегоПриказа) 
    Запрос = Новый Запрос;                  
    Лимит = "";
    Порядок = "";
    Если ПолучитьПоследнийПриказ Тогда
        Лимит = " ПЕРВЫЕ 1 ";
        Порядок = "УБЫВ";
    КонецЕсли;
    
    Запрос.Текст = "ВЫБРАТЬ РАЗРЕШЕННЫЕ " + Лимит + "
                   |	КадроваяИсторияСотрудников.Период КАК Период,
                   |	КадроваяИсторияСотрудников.Регистратор КАК Регистратор,
                               |	КадроваяИсторияСотрудников.ВидСобытия КАК ВидСобытия,
                   |	КадроваяИсторияСотрудников.Сотрудник.Ссылка КАК СотрудникСсылка,
                   |	КадроваяИсторияСотрудников.ФизическоеЛицо.Фамилия КАК ФизическоеЛицоФамилия,
                   |	КадроваяИсторияСотрудников.ФизическоеЛицо.Имя КАК ФизическоеЛицоИмя,
                   |	КадроваяИсторияСотрудников.ФизическоеЛицо.Отчество КАК ФизическоеЛицоОтчество,
                   |	КадроваяИсторияСотрудников.Должность КАК Должность
                   |ИЗ
                   |	РегистрСведений.КадроваяИсторияСотрудников КАК КадроваяИсторияСотрудников
                               |ГДЕ    
                               | Период > &МаксДата
                               |
                   |УПОРЯДОЧИТЬ ПО
                   |	Период " + Порядок;
    Запрос.Параметры.Вставить("МаксДата", ДатаПоследнегоПриказа);
    
    Выборка = Запрос.Выполнить().Выбрать();
    Массив = Новый Массив;
    Пока Выборка.Следующий() Цикл
        Структура = Новый Структура("date, type, user, newPosition");
        Структура.date = Выборка.Период;
        Структура.type = Строка(Выборка.ВидСобытия);
        
        СтруктураПользователь = Новый Структура("uid, name, surname, patronymic");
        СтруктураПользователь.uid = Строка(Выборка.СотрудникСсылка.УникальныйИдентификатор());  
        СтруктураПользователь.name = Выборка.ФизическоеЛицоИмя;  
        СтруктураПользователь.surname = Выборка.ФизическоеЛицоФамилия;  
        СтруктураПользователь.patronymic = Выборка.ФизическоеЛицоОтчество; 
        
        Структура.user = СтруктураПользователь;
        
        Если ЗначениеЗаполнено(Выборка.Должность) Тогда
            Структура.newPosition = Выборка.Должность.Наименование;  
        Иначе
            Структура.newPosition = "";  	
        КонецЕсли;
        
        Массив.Добавить(Структура);
    КонецЦикла;
    Возврат Массив;	

КонецФункции

Функция ПолучитьСписокСотрудниковНаТекущуюДату() 
    Запрос = Новый Запрос;
    Запрос.Текст = "ВЫБРАТЬ РАЗРЕШЕННЫЕ
                   |	СправочникСотрудники.Ссылка КАК Ссылка,
                   |	СправочникСотрудники.Код КАК Код,
                   |	СправочникСотрудники.Наименование КАК Наименование,
                   |	СправочникСотрудники.ФизическоеЛицо КАК ФизическоеЛицо,
                   |	СправочникСотрудники.ФизическоеЛицо.Фамилия КАК ФизическоеЛицоФамилия,
                   |	СправочникСотрудники.ФизическоеЛицо.Имя КАК ФизическоеЛицоИмя,
                   |	СправочникСотрудники.ФизическоеЛицо.Отчество КАК ФизическоеЛицоОтчество,
                   |	СправочникСотрудники.ГоловнаяОрганизация КАК ГоловнаяОрганизация,
                   |	ЕСТЬNULL(КадроваяИсторияСотрудниковИнтервальный.Подразделение, ЗНАЧЕНИЕ(Справочник.ПодразделенияОрганизаций.ПустаяСсылка)) КАК ТекущееПодразделение,
                   |	ЕСТЬNULL(КадроваяИсторияСотрудниковИнтервальный.Должность, ЗНАЧЕНИЕ(Справочник.Должности.ПустаяСсылка)) КАК ТекущаяДолжность
                   |ИЗ
                   |	Справочник.Сотрудники КАК СправочникСотрудники
                   |		ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.КадроваяИсторияСотрудниковИнтервальный КАК КадроваяИсторияСотрудниковИнтервальный
                   |		ПО СправочникСотрудники.Ссылка = КадроваяИсторияСотрудниковИнтервальный.Сотрудник
                   |			И (КадроваяИсторияСотрудниковИнтервальный.ДатаНачала В
                   |				(ВЫБРАТЬ
                   |					МАКСИМУМ(Т.ДатаНачала)
                   |				ИЗ
                   |					РегистрСведений.КадроваяИсторияСотрудниковИнтервальный КАК Т
                   |				ГДЕ
                   |					СправочникСотрудники.Ссылка = Т.Сотрудник
                   |					И &МаксимальнаяДатаНачалоДня МЕЖДУ Т.ДатаНачала И Т.ДатаОкончания))
                   |ГДЕ
                                 |	СправочникСотрудники.ПометкаУдаления = ЛОЖЬ";
    Запрос.Параметры.Вставить("МаксимальнаяДатаНачалоДня", ТекущаяДата());
    
    Выборка = Запрос.Выполнить().Выбрать(); 
    Массив = Новый Массив;
    Пока Выборка.Следующий() Цикл
        Структура = Новый Структура("uid, name, surname, patronymic, position");
        Структура.uid = Строка(Выборка.Ссылка.УникальныйИдентификатор());  
        Структура.name = Выборка.ФизическоеЛицоИмя;  
        Структура.surname = Выборка.ФизическоеЛицоФамилия;  
        Структура.patronymic = Выборка.ФизическоеЛицоОтчество;  
        Если ЗначениеЗаполнено(Выборка.ТекущаяДолжность) Тогда
            Структура.position = Выборка.ТекущаяДолжность.Наименование;  
        Иначе
            Структура.position = "";  	
        КонецЕсли;
        
        Массив.Добавить(Структура);
    КонецЦикла;
    Возврат Массив;	
КонецФункции      

Функция ПолучитьJSON(Данные)
    ЗаписьJSON = Новый ЗаписьJSON;
    ЗаписьJSON.УстановитьСтроку();  
    ЗаписатьJSON(ЗаписьJSON, Данные); 
    Возврат ЗаписьJSON.Закрыть();
КонецФункции

Функция ПолучитьHTTPСервисОтвет()  
    Ответ = Новый HTTPСервисОтвет(200);   
    Ответ.Заголовки.Вставить("Content-Type","application/json; charset=utf-8");
    Возврат Ответ;
КонецФункции

Создайте в 1С служебную учетную запись, которая будет иметь право читать данные, нужные для сервиса и с которой OpenIDM будет ходить в сервис для получения данных.

Как создавать HTTP сервисы 1С более подробно описано по ссылкам:

https://infostart.ru/1c/articles/1293341/

https://infostart.ru/1c/articles/842751/

Настройка OpenIDM.

Если OpenIDM у вас еще не установлен, установите его, как описано в статье.

Настройка подключения к 1С.

В каталог conf OpenIDM добавьте файл конфигурации коннектора к 1С

provisioner.openicf-scriptedrest.json

{
    "name" : "scriptedrest",
    "connectorRef" : {
        "connectorHostRef" : "#LOCAL",
        "connectorName" : "org.forgerock.openicf.connectors.scriptedrest.ScriptedRESTConnector",
        "bundleName" : "org.openidentityplatform.openicf.connectors.groovy-connector",
        "bundleVersion" : "[1.4.0.0,2)"
    },
    "poolConfigOption" : {
        "maxObjects" : 10,
        "maxIdle" : 10,
        "maxWait" : 150000,
        "minEvictableIdleTimeMillis" : 120000,
        "minIdle" : 1
    },
    "operationTimeout" : {
        "CREATE" : -1,
        "UPDATE" : -1,
        "DELETE" : -1,
        "TEST" : -1,
        "SCRIPT_ON_CONNECTOR" : -1,
        "SCRIPT_ON_RESOURCE" : -1,
        "GET" : -1,
        "RESOLVEUSERNAME" : -1,
        "AUTHENTICATE" : -1,
        "SEARCH" : -1,
        "VALIDATE" : -1,
        "SYNC" : -1,
        "SCHEMA" : -1
    },
    "resultsHandlerConfig" : {
        "enableNormalizingResultsHandler" : true,
        "enableFilteredResultsHandler" : true,
        "enableCaseInsensitiveFilter" : false,
        "enableAttributesToGetSearchResultsHandler" : true
    },
    "configurationProperties" : {
        "serviceAddress" : "http://localhost:8090",
        "proxyAddress" : null,
        "username" : "idm",
        "password" : "passw0rd",
        "defaultAuthMethod" : "BASIC_PREEMPTIVE",
        "defaultRequestHeaders" : [
            null
        ],
        "defaultContentType" : "application/json",
        "scriptExtensions" : [
            "groovy"
        ],
        "sourceEncoding" : "UTF-8",
        "customizerScriptFileName" : "CustomizerScript.groovy",
        "searchScriptFileName" : "SearchScript.groovy",
        "syncScriptFileName" : "SyncScript.groovy",
        "recompileGroovySource" : false,
        "minimumRecompilationInterval" : 100,
        "debug" : false,
        "verbose" : false,
        "warningLevel" : 1,
        "tolerance" : 10,
        "disabledGlobalASTTransformations" : null,
        "targetDirectory" : null,
        "scriptRoots" : [
            "&{launcher.project.location}/tools"
        ]
    },
    "objectTypes" : {
        "account" : {
            "$schema" : "http://json-schema.org/draft-03/schema",
            "id" : "__ACCOUNT__",
            "type" : "object",
            "nativeType" : "__ACCOUNT__",
            "properties" : {
                "uid" : {
                    "type" : "string",
                    "nativeName" : "__NAME__",
                    "nativeType" : "string",
                    "flags" : [
                        "NOT_UPDATEABLE",
                        "NOT_CREATEABLE"
                    ]
                },
                "name" : {
                    "type" : "string",
                    "nativeName" : "name",
                    "nativeType" : "string",
                    "required" : true
                },
                "surname" : {
                    "type" : "string",
                    "nativeName" : "surname",
                    "nativeType" : "string"
                },
                "patronymic" : {
                    "type" : "string",
                    "nativeName" : "patronymic",
                    "nativeType" : "string"
                },
                "position" : {
                    "type" : "string",
                    "nativeName" : "position",
                    "nativeType" : "string"
                }
            }
        }
    }
}

Измените свойства объекта configurationProperties в соттветствии с настройками подключения к HTTP сервису вашей информационной базы 1С.

В каталог tools добавьте скрипты, которые будут обращаться к API 1C:

CustomizerScript.groovy — скрипт, который отвечает за начальную настройку HTTP клиента

import groovyx.net.http.RESTClient
import groovyx.net.http.StringHashMap
import org.apache.http.HttpHost
import org.apache.http.auth.AuthScope
import org.apache.http.auth.UsernamePasswordCredentials
import org.apache.http.client.ClientProtocolException
import org.apache.http.client.CredentialsProvider
import org.apache.http.client.HttpClient
import org.apache.http.client.config.RequestConfig
import org.apache.http.client.protocol.HttpClientContext
import org.apache.http.conn.routing.HttpRoute
import org.apache.http.impl.auth.BasicScheme
import org.apache.http.impl.client.BasicAuthCache
import org.apache.http.impl.client.BasicCookieStore
import org.apache.http.impl.client.BasicCredentialsProvider
import org.apache.http.impl.client.HttpClientBuilder
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager
import org.forgerock.openicf.connectors.scriptedrest.ScriptedRESTConfiguration
import org.forgerock.openicf.connectors.scriptedrest.ScriptedRESTConfiguration.AuthMethod
import org.identityconnectors.common.security.GuardedString

// must import groovyx.net.http.HTTPBuilder.RequestConfigDelegate
import groovyx.net.http.HTTPBuilder.RequestConfigDelegate

/**
 * A customizer script defines the custom closures to interact with the default implementation and customize it.
 */
customize {
    init { HttpClientBuilder builder ->

        //SETUP: org.apache.http
        def c = delegate as ScriptedRESTConfiguration

        def httpHost = new HttpHost(c.serviceAddress?.host, c.serviceAddress?.port, c.serviceAddress?.scheme);

        PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();
        // Increase max total connection to 200
        cm.setMaxTotal(200);
        // Increase default max connection per route to 20
        cm.setDefaultMaxPerRoute(20);
        // Increase max connections for httpHost to 50
        cm.setMaxPerRoute(new HttpRoute(httpHost), 50);

        builder.setConnectionManager(cm)

        // configure timeout on the entire client
        RequestConfig requestConfig = RequestConfig.custom().build();
        builder.setDefaultRequestConfig(requestConfig)

        if (c.proxyAddress != null) {
            builder.setProxy(new HttpHost(c.proxyAddress?.host, c.proxyAddress?.port, c.proxyAddress?.scheme));
        }

        switch (ScriptedRESTConfiguration.AuthMethod.valueOf(c.defaultAuthMethod)) {
            case ScriptedRESTConfiguration.AuthMethod.BASIC_PREEMPTIVE:
            case ScriptedRESTConfiguration.AuthMethod.BASIC:
                // It's part of the http client spec to request the resource anonymously
                // first and respond to the 401 with the Authorization header.
                final CredentialsProvider credentialsProvider = new BasicCredentialsProvider();

                c.password.access(
                        {
                            credentialsProvider.setCredentials(new AuthScope(httpHost.getHostName(), httpHost.getPort()),
                                    new UsernamePasswordCredentials(c.username, new String(it)));
                        } as GuardedString.Accessor
                );

                builder.setDefaultCredentialsProvider(credentialsProvider);
                break;
            case ScriptedRESTConfiguration.AuthMethod.NONE:
                break;
            default:
                throw new IllegalArgumentException();
        }

        c.propertyBag.put(HttpClientContext.COOKIE_STORE, new BasicCookieStore());
    }

    /**
     * This Closure can customize the httpClient and the returning object is injected into the Script Binding.
     */
    decorate { HttpClient httpClient ->

        //SETUP: groovyx.net.http

        def c = delegate as ScriptedRESTConfiguration

        def authCache = null
        if (AuthMethod.valueOf(c.defaultAuthMethod).equals(AuthMethod.BASIC_PREEMPTIVE)){
            authCache = new BasicAuthCache();
            authCache.put(new HttpHost(c.serviceAddress?.host, c.serviceAddress?.port, c.serviceAddress?.scheme), new BasicScheme());

        }

        def cookieStore = c.propertyBag.get(HttpClientContext.COOKIE_STORE)

        RESTClient restClient = new InnerRESTClient(c.serviceAddress, c.defaultContentType, authCache, cookieStore)

        Map defaultRequestHeaders = new StringHashMap();
        if (null != c.defaultRequestHeaders) {
            c.defaultRequestHeaders.each {
                if (null != it) {                
                    def kv = it.split('=')
                    assert kv.size() == 2
                    defaultRequestHeaders.put(kv[0], kv[1])
                }
            }
        }

        restClient.setClient(httpClient);
        restClient.setHeaders(defaultRequestHeaders)

        // Return with the decorated instance
        return restClient
    }

}

RequestConfigDelegate blank = null;

class InnerRESTClient extends RESTClient {

    def authCache = null;
    def cookieStore = null;

    InnerRESTClient(Object defaultURI, Object defaultContentType, authCache, cookieStore) throws URISyntaxException {
        super(defaultURI, defaultContentType)
        this.authCache = authCache
        this.cookieStore = cookieStore
    }

    @Override
    protected Object doRequest(
            final RequestConfigDelegate delegate) throws ClientProtocolException, IOException {

        // Add AuthCache to the execution context
        if (null != authCache) {
            //do Preemptive Auth
            delegate.getContext().setAttribute(HttpClientContext.AUTH_CACHE, authCache);
        }
        // Add AuthCache to the execution context
        if (null != cookieStore) {
            //do Preemptive Auth
            delegate.getContext().setAttribute(HttpClientContext.COOKIE_STORE, cookieStore);
        }
        return super.doRequest(delegate)
    }
}

SearchScript.groovy — получает данные по сотрудникам из 1С

import groovyx.net.http.RESTClient
import org.apache.http.client.HttpClient
import org.forgerock.openicf.connectors.scriptedrest.ScriptedRESTConfiguration
import org.forgerock.openicf.misc.scriptedcommon.OperationType
import org.identityconnectors.common.logging.Log
import org.identityconnectors.framework.common.objects.Attribute
import org.identityconnectors.framework.common.objects.AttributeUtil
import org.identityconnectors.framework.common.objects.Name
import org.identityconnectors.framework.common.objects.ObjectClass
import org.identityconnectors.framework.common.objects.OperationOptions
import org.identityconnectors.framework.common.objects.SearchResult
import org.identityconnectors.framework.common.objects.Uid
import org.identityconnectors.framework.common.objects.filter.Filter

import static groovyx.net.http.Method.GET

// imports used for CREST based REST APIs
import org.forgerock.openicf.misc.crest.CRESTFilterVisitor
import org.forgerock.openicf.misc.crest.VisitorParameter

def operation = operation as OperationType
def configuration = configuration as ScriptedRESTConfiguration
def httpClient = connection as HttpClient
def connection = customizedConnection as RESTClient
def filter = filter as Filter
def log = log as Log
def objectClass = objectClass as ObjectClass
def options = options as OperationOptions
def resultHandler = handler

log.info("Entering " + operation + " Script")

def queryFilter = 'true'
if (filter != null) {
    queryFilter = filter.accept(CRESTFilterVisitor.VISITOR, [
            translateName: { String name ->
                if (AttributeUtil.namesEqual(name, Uid.NAME)) {
                    return "uid"
                } else if (AttributeUtil.namesEqual(name, "name")) {
                    return "name"
                } else if (AttributeUtil.namesEqual(name, "surname")) {
                    return "surname"
                } else if (AttributeUtil.namesEqual(name, "patronymic")) {
                    return "patronymic"
                } else {
                    throw new IllegalArgumentException("Unknown field name: ${name}");
                }
            },
            convertValue : { Attribute value ->
                if (AttributeUtil.namesEqual(value.name, "position")) {
                    return value.value
                } else {
                    return AttributeUtil.getStringValue(value)
                }
            }] as VisitorParameter).toString();
}
switch (objectClass) {
    case ObjectClass.ACCOUNT:
        def searchResult = connection.request(GET) { req ->
            uri.path = '/api/users'
            uri.query = [
                    _queryFilter: queryFilter
            ]

            response.success = { resp, json ->
                json.each() { value ->
                    resultHandler {
                        uid value.uid
                        id value.uid
                        attribute 'surname', value?.surname
                        attribute 'name', value?.name
                        attribute 'patronymic', value?.patronymic
                        attribute 'position', value?.position
                    }
                }
                json
            }
        }

        return new SearchResult()

    case ObjectClass.GROUP:
        throw new IllegalArgumentException("Group sync is not supported");
}
    

SyncScript.groovy — получает данные по кадровым приказам из 1С

    
import groovyx.net.http.RESTClient
import org.apache.http.client.HttpClient
import org.forgerock.openicf.connectors.scriptedrest.ScriptedRESTConfiguration
import org.forgerock.openicf.misc.scriptedcommon.OperationType
import org.identityconnectors.common.logging.Log
import org.identityconnectors.framework.common.objects.ObjectClass
import org.identityconnectors.framework.common.objects.SyncToken

import static groovyx.net.http.Method.GET

def operation = operation as OperationType
def configuration = configuration as ScriptedRESTConfiguration
def httpClient = connection as HttpClient
def connection = customizedConnection as RESTClient
def log = log as Log
def objectClass = objectClass as ObjectClass

log.info("Entering " + operation + " Script");

if (OperationType.GET_LATEST_SYNC_TOKEN.equals(operation)) {

    return connection.request(GET) { req ->
        uri.path = '/api/orders'
        uri.query = [
                getLatest: 'true',
        ]

        response.success = { resp, json ->
            def lastToken = 0l
            json.each() { it ->
                lastToken = it.date
            }
            return new SyncToken(lastToken)
        }

        response.failure = { resp, json ->
            throw new ConnectException(json.message)
        }
    }

} else if (OperationType.SYNC.equals(operation)) {
    def token = token as Object
    log.info("Entering SYNC");
    switch (objectClass) {
        case ObjectClass.ACCOUNT:
            return connection.request(GET) { req ->
                uri.path = '/api/orders'
                uri.query = [
                        date: "${token}",
                ]

                response.success = { resp, json ->
                    def lastToken = 0l
                    json.each() { changeLogEntry ->
                        lastToken = changeLogEntry.date

                        def user = {
                            uid changeLogEntry.user.uid
                            id changeLogEntry.user.uid
                            attribute 'name', changeLogEntry.user.name
                            attribute 'surname', changeLogEntry.user.surname
                            attribute 'patronymic', changeLogEntry.user.patronymic
                            attribute 'position', changeLogEntry.newPosition?.name
                        }

                        handler({
                            syncToken lastToken
                            if ("hire".equals(changeLogEntry.type)) {
                                CREATE()
                                object user
                            } else if ("transfer".equals(changeLogEntry.type)) {
                                CREATE_OR_UPDATE()
                                object user
                            } else if ("fire".equals(changeLogEntry.type)) {
                                DELETE()
                                object {
                                    uid changeLogEntry.user.uid
                                    id changeLogEntry.user.uid
                                    delegate.objectClass(objectClass)
                                }
                                return
                            } else {
                                CREATE_OR_UPDATE()
                                object user
                            }
                        })
                    }
                    return new SyncToken(lastToken)
                }

                response.failure = { resp, json ->
                    throw new ConnectException(json.message)
                }
            }
            break;

        case ObjectClass.GROUP:
            throw new IllegalArgumentException("Group sync is not supported");
            break;
    }

} else { // action not implemented
    log.error("Sync script: action '" + operation + "' is not implemented in this script");
}
    

Настройка синхронизации

Поместите файл translit.js в каталог script OpenIDM.

translit.js

/*global exports*/
(function () {
    var translit = function (word) {

        var converter = {
            'а': 'a', 'б': 'b', 'в': 'v', 'г': 'g', 'д': 'd',
            'е': 'e', 'ё': 'e', 'ж': 'zh', 'з': 'z', 'и': 'i',
            'й': 'y', 'к': 'k', 'л': 'l', 'м': 'm', 'н': 'n',
            'о': 'o', 'п': 'p', 'р': 'r', 'с': 's', 'т': 't',
            'у': 'u', 'ф': 'f', 'х': 'h', 'ц': 'c', 'ч': 'ch',
            'ш': 'sh', 'щ': 'sch', 'ь': '', 'ы': 'y', 'ъ': '',
            'э': 'e', 'ю': 'yu', 'я': 'ya'
        }
        var answer = '';
        word = word.toLowerCase();
        for (var i = 0; i < word.length; ++i) {
            if (converter[word[i]] == undefined) {
                answer += word[i];
            } else {
                answer += converter[word[i]];
            }
        }
        return answer;
    }

    var generateLogin = function (name, surname) {
        var nameT = translit(name);
        var surnameT = translit(surname);
        return nameT.substring(0, 1) + surnameT;
    }

    exports.translit = translit;
    exports.generateLogin = generateLogin;

}());

Функция generateLogin скрипта при синхронизации вызывается для автоматической генерации логина и адреса электронной почты.

Поместите файл настроек синхронизации sync.json в каталог conf OpenIDM.

sync.json

{
    "mappings" : [
        {
            "target" : "managed/user",
            "source" : "system/scriptedrest/account",
            "name" : "systemScriptedrestAccount_managedUser",
            "properties" : [
                {
                    "target" : "mail",
                    "transform" : {
                        "type" : "text/javascript",
                        "globals" : { },
                        "source" : "require('translit').generateLogin(source.name, source.surname) + \"@example.org\""
                    },
                    "source" : ""
                },
                {
                    "target" : "sn",
                    "source" : "surname"
                },
                {
                    "target" : "givenName",
                    "source" : "name"
                },
                {
                    "target" : "userName",
                    "transform" : {
                        "type" : "text/javascript",
                        "globals" : { },
                        "source" : "require('translit').generateLogin(source.name, source.surname)"
                    },
                    "source" : ""
                },
                {
                    "target" : "roles",
                    "transform" : {
                        "type" : "text/javascript",
                        "globals" : { },
                        "source" : "/*global openidm*/\nlogger.warn(\"set role {} for {}\", source.position, source.name);\n\nfunction getRoles(position) {\n    if(!position) {\n        return [];\n    }\n    var response = openidm.query(\"managed/role\", {\"_queryFilter\": 'name eq \"' + position + '\"'});\n\n    var roleId;\n    if(!response.result || response.result.length === 0) {\n        response = openidm.create(\"managed/role\", null, {'name': position, 'description' : position});\n        logger.warn(\"got new role response: {}\", response);\n        roleId = response[\"_id\"];\n\n    } else {\n        logger.warn(\"existing role response: {}\", response);\n        roleId = response.result[0][\"_id\"];\n    }\n\n    return [{\n        \"_ref\": \"managed/role/\" + roleId\n    }];\n}\n\ngetRoles(source.position)\n\n\n"
                    },
                    "source" : ""
                }
            ],
            "policies" : [
                {
                    "action" : "EXCEPTION",
                    "situation" : "AMBIGUOUS"
                },
                {
                    "action" : "DELETE",
                    "situation" : "SOURCE_MISSING"
                },
                {
                    "action" : "CREATE",
                    "situation" : "MISSING"
                },
                {
                    "action" : "EXCEPTION",
                    "situation" : "FOUND_ALREADY_LINKED"
                },
                {
                    "action" : "DELETE",
                    "situation" : "UNQUALIFIED"
                },
                {
                    "action" : "EXCEPTION",
                    "situation" : "UNASSIGNED"
                },
                {
                    "action" : "EXCEPTION",
                    "situation" : "LINK_ONLY"
                },
                {
                    "action" : "IGNORE",
                    "situation" : "TARGET_IGNORED"
                },
                {
                    "action" : "IGNORE",
                    "situation" : "SOURCE_IGNORED"
                },
                {
                    "action" : "IGNORE",
                    "situation" : "ALL_GONE"
                },
                {
                    "action" : "UPDATE",
                    "situation" : "CONFIRMED"
                },
                {
                    "action" : "UPDATE",
                    "situation" : "FOUND"
                },
                {
                    "action" : "CREATE",
                    "situation" : "ABSENT"
                }
            ],
            "onCreate" : {
                "type" : "text/javascript",
                "globals" : { },
                "source" : "logger.warn(\"generating password for {}\", target);\n// generate random password that aligns with policy requirements\ntarget.password = require(\"crypto\").generateRandomString([\n  { \"rule\": \"UPPERCASE\", \"minimum\": 1 },\n  { \"rule\": \"LOWERCASE\", \"minimum\": 1 },\n  { \"rule\": \"INTEGERS\", \"minimum\": 1 },\n  { \"rule\": \"SPECIAL\", \"minimum\": 0, \"maximum\": 0 }\n], 16);\n\nlogger.warn(\"genered password {}\", target.password);\n\n\nlogger.warn(\"sending email {}\", target.mail);\n\nvar escapedPass = target.password\n  .replace(/&/g, \"&\")\n  .replace(//g, \">\")\n  .replace(/\"/g, \""\")\n  .replace(/'/g, \"'\");\n\nvar params =  new Object();\nparams.from = \"openidm@example.org\";\nparams.to = target.mail;\nparams.subject = \"Ваш новый пароль\";\nparams.type = \"text/html\";\nparams.body = \"

Здравствуйте!
Ваш новый пароль \" + escapedPass + \"

\";\n\ntry {\n openidm.action(\"external/email\", \"send\", params);\n} catch(ex) {\n logger.error(\"error sending mail: {}\", ex);\n}\n" }, "taskThreads" : 1 } ] }

Проверьте настройки синхронизации в консоли администратора OpenIDM. Для этого перейдите в вашем браузере по ссылке http://localhost:8080/admin. Логин и пароль для администратора по умолчанию openidm-admin и openidm-admin. В верхнем меню перейдите Configure → Mappings.

Откройте настройки синхронизации systemScriptedrestAccount_managedUser.

Настройка синхронизации 1С OpenIDM

Настройка синхронизации 1С OpenIDM

Особый интерес представляет синхронизация полей mail,  userName и roles так как они формируются при помощи скриптов. Вы можете посмотреть скрипты на закладке Transformation Script:

Скрипт преобразования поля mail

Скрипт преобразования поля mail

При создании учетной записи для нее генерируется пароль и отправляется по электронной почте. Это так же делается при помощи скрипта в обработчике onCreate настроек синхронизации. Чтобы просмотреть текст скрипта, в настройках синхронизации перейдите на закладку Behaviors . Далее в раздел Situational Event Scripts и откройте обработчик onCreate Скрипт будет выглядеть примерно таким образом:

logger.warn("generating password for {}", target);
// generate random password that aligns with policy requirements
target.password = require("crypto").generateRandomString([
  { "rule": "UPPERCASE", "minimum": 1 },
  { "rule": "LOWERCASE", "minimum": 1 },
  { "rule": "INTEGERS", "minimum": 1 },
  { "rule": "SPECIAL", "minimum": 0, "maximum": 0 }
], 16);

logger.warn("sending email {}", target.mail);

var escapedPass = target.password
  .replace(/&/g, "&")
  .replace(//g, ">")
  .replace(/"/g, """)
  .replace(/'/g, "'");

var params =  new Object();
params.from = "openidm@example.org";
params.to = target.mail;
params.subject = "Ваш новый пароль";
params.type = "text/html";
params.body = "

Здравствуйте!
Ваш новый пароль " + escapedPass + "

"; try { openidm.action("external/email", "send", params); } catch(ex) { logger.error("error sending mail: {}", ex); }

Настройка электронной почты

Для демонстрационных целей мы будем использовать тестовый SMTP сервер с открытым исходным кодом https://github.com/maildev/maildev, запущенный в Docker контейнере.

В каталог conf OpenIDM поместите файл настройки подключения к SMTP серверу, чтобы OpenIDM мог отправлять сгенерированный пароль по почте.

external.email.json

{
    "host" : "localhost",
    "port" : "1025",
    "auth" : {
        "enable" : false
    },
    "starttls" : {
        "enable" : false
    },
    "from" : ""
}

Запустите Docker образ тестового SMTP сервера:

docker run -p 1080:1080 -p 1025:1025 maildev/maildev

Более подробно про настройку отправки почты из OpenIDM вы можете прочитать по ссылке.

Проверка решения

Начальная синхронизация

Зайдите в консоль администратора OpenIDM. В верхнем меню перейдите Configure → Mappings. Выберите systemScriptedrestAccount_managedUser и нажмите кнопку Reconcile. Дождитесь окончания синхронизации.

Успешная синхронизация

Успешная синхронизация

В консоли администратора перейдите в Manage → Users. Вы увидите список учетных записей, загруженных из информационной базы 1С Бухгалтерия. Для каждой записи сгенерирован логин, адрес электронной почты и назначена соответствующая роль.

Список пользователей OpenIDM

Список пользователей OpenIDM

Сгенерированный пароль отправлен по электронной почте. Откройте консоль тестового SMTP сервера по адресу http://localhost:1080/. Вы увидите отправленные письма для каждой учетной записи.

Email со сгенерированым паролем

Email со сгенерированым паролем

Синхронизация приказов

Каждый раз синхронизировать все данные из 1С возможно, но, при большом количестве сотрудников не рационально, так как синхронизация будет занимать долгое время и потреблять большое количество вычислительных ресурсов. Поэтому, далее мы будем синхронизировать только изменения. Изменения в 1С регистрируются при помощи кадровых приказов. Давайте создадим несколько кадровых приказов в 1С и проверим их синхронизацию в OpenIDM.

В консоли администратора OpenIDM в верхнем меню перейдите Configure → Schedules. Создайте новое расписание.

OpenIDM новое расписание

OpenIDM новое расписание

Зайдите в 1С Бухгалтерия и создайте приказ приема на работу сотрудника Иванов Иван Иванович с должностью Кладовщик. Проведите документ.

1С приказ о приеме на работу

1С приказ о приеме на работу

Подождите пару минут и откройте список пользователей OpenIDM

Вы увидите, что в консоли появится новый пользователь iivanov с ролью Кладовщик

Новый пользователь OpenIDM

Новый пользователь OpenIDM

Создайте в 1С документ Кадровый перевод. И назначьте в нем сотруднику новую роль «Кассир». Проведите документ.

1С приказ о переводе

1С приказ о переводе

Подождите пару минут и проверьте роли пользователя iivanov OpenIDM.

Ему будет назначена новая роль «Кассир» из 1С.

OpenIDM новая роль

OpenIDM новая роль

Теперь давайте создадим приказ об увольнении сотрудника Создайте и проведите приказ в 1С.

1С приказ об увольнении

1С приказ об увольнении

Дождитесь синхронизации. Пользователь iivanov будет удален из списка пользователей OpenIDM.

OpenIDM пользователь удален

OpenIDM пользователь удален

© Habrahabr.ru