This is an automated email from the ASF dual-hosted git repository. skylark17 pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/syncope.git
The following commit(s) were added to refs/heads/master by this push: new 85fe179 [SYNCOPE-1399] Added fix for Elasticsearch v6.x 85fe179 is described below commit 85fe179ec541685e28b83cea55cec2b9341a50c3 Author: skylark17 <matteo.alessandr...@tirasa.net> AuthorDate: Tue Nov 20 18:00:24 2018 +0100 [SYNCOPE-1399] Added fix for Elasticsearch v6.x --- .../client/ElasticsearchIndexManager.java | 10 +-- .../elasticsearch/client/ElasticsearchUtils.java | 11 +++ .../jpa/dao/ElasticsearchAnySearchDAO.java | 4 +- .../java/job/ElasticsearchReindex.java | 93 ++++++++++++++++------ .../syncope/core/logic/init/ElasticsearchInit.java | 71 +++++++++++++++++ .../fit/core/reference/ITImplementationLookup.java | 38 +-------- .../reference-guide/concepts/notifications.adoc | 2 +- .../workingwithapachesyncope/customization.adoc | 7 ++ 8 files changed, 167 insertions(+), 69 deletions(-) diff --git a/ext/elasticsearch/client-elasticsearch/src/main/java/org/apache/syncope/ext/elasticsearch/client/ElasticsearchIndexManager.java b/ext/elasticsearch/client-elasticsearch/src/main/java/org/apache/syncope/ext/elasticsearch/client/ElasticsearchIndexManager.java index 1e34f6a..57ba53c 100644 --- a/ext/elasticsearch/client-elasticsearch/src/main/java/org/apache/syncope/ext/elasticsearch/client/ElasticsearchIndexManager.java +++ b/ext/elasticsearch/client-elasticsearch/src/main/java/org/apache/syncope/ext/elasticsearch/client/ElasticsearchIndexManager.java @@ -22,7 +22,6 @@ import java.io.IOException; import org.apache.syncope.core.persistence.api.entity.Any; import org.apache.syncope.core.provisioning.api.event.AnyCreatedUpdatedEvent; import org.apache.syncope.core.provisioning.api.event.AnyDeletedEvent; -import org.apache.syncope.core.spring.security.AuthContextUtils; import org.elasticsearch.action.delete.DeleteResponse; import org.elasticsearch.action.get.GetResponse; import org.elasticsearch.action.index.IndexResponse; @@ -48,7 +47,8 @@ public class ElasticsearchIndexManager { @TransactionalEventListener public void after(final AnyCreatedUpdatedEvent<Any<?>> event) throws IOException { - GetResponse getResponse = client.prepareGet(AuthContextUtils.getDomain().toLowerCase(), + GetResponse getResponse = client.prepareGet( + elasticsearchUtils.getContextDomainName(event.getAny().getType().getKind()), event.getAny().getType().getKind().name(), event.getAny().getKey()). get(); @@ -56,7 +56,7 @@ public class ElasticsearchIndexManager { LOG.debug("About to update index for {}", event.getAny()); UpdateResponse response = client.prepareUpdate( - AuthContextUtils.getDomain().toLowerCase(), + elasticsearchUtils.getContextDomainName(event.getAny().getType().getKind()), event.getAny().getType().getKind().name(), event.getAny().getKey()). setRetryOnConflict(elasticsearchUtils.getRetryOnConflict()). @@ -67,7 +67,7 @@ public class ElasticsearchIndexManager { LOG.debug("About to create index for {}", event.getAny()); IndexResponse response = client.prepareIndex( - AuthContextUtils.getDomain().toLowerCase(), + elasticsearchUtils.getContextDomainName(event.getAny().getType().getKind()), event.getAny().getType().getKind().name(), event.getAny().getKey()). setSource(elasticsearchUtils.builder(event.getAny())). @@ -82,7 +82,7 @@ public class ElasticsearchIndexManager { LOG.debug("About to delete index for {}[{}]", event.getAnyTypeKind(), event.getAnyKey()); DeleteResponse response = client.prepareDelete( - AuthContextUtils.getDomain().toLowerCase(), + elasticsearchUtils.getContextDomainName(event.getAnyTypeKind()), event.getAnyTypeKind().name(), event.getAnyKey()). get(); diff --git a/ext/elasticsearch/client-elasticsearch/src/main/java/org/apache/syncope/ext/elasticsearch/client/ElasticsearchUtils.java b/ext/elasticsearch/client-elasticsearch/src/main/java/org/apache/syncope/ext/elasticsearch/client/ElasticsearchUtils.java index a66f1d2..f5b69be 100644 --- a/ext/elasticsearch/client-elasticsearch/src/main/java/org/apache/syncope/ext/elasticsearch/client/ElasticsearchUtils.java +++ b/ext/elasticsearch/client-elasticsearch/src/main/java/org/apache/syncope/ext/elasticsearch/client/ElasticsearchUtils.java @@ -24,6 +24,7 @@ import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.stream.Collectors; +import org.apache.syncope.common.lib.types.AnyTypeKind; import org.apache.syncope.core.persistence.api.dao.AnyObjectDAO; import org.apache.syncope.core.persistence.api.dao.GroupDAO; import org.apache.syncope.core.persistence.api.dao.UserDAO; @@ -33,6 +34,7 @@ import org.apache.syncope.core.persistence.api.entity.PlainAttr; import org.apache.syncope.core.persistence.api.entity.anyobject.AnyObject; import org.apache.syncope.core.persistence.api.entity.group.Group; import org.apache.syncope.core.persistence.api.entity.user.User; +import org.apache.syncope.core.spring.security.AuthContextUtils; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -182,4 +184,13 @@ public class ElasticsearchUtils { return builder; } + + public String getContextDomainName(final AnyTypeKind kind) { + return AuthContextUtils.getDomain().toLowerCase() + + (kind.equals(AnyTypeKind.USER) + ? "_user" + : (kind.equals(AnyTypeKind.GROUP) + ? "_group" + : "_anyobject")); + } } diff --git a/ext/elasticsearch/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/ElasticsearchAnySearchDAO.java b/ext/elasticsearch/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/ElasticsearchAnySearchDAO.java index 89f7ac2..02b4a98 100644 --- a/ext/elasticsearch/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/ElasticsearchAnySearchDAO.java +++ b/ext/elasticsearch/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/ElasticsearchAnySearchDAO.java @@ -52,7 +52,6 @@ import org.apache.syncope.core.persistence.api.entity.PlainAttrValue; import org.apache.syncope.core.persistence.api.entity.PlainSchema; import org.apache.syncope.core.persistence.api.entity.Realm; import org.apache.syncope.core.provisioning.api.utils.RealmUtils; -import org.apache.syncope.core.spring.security.AuthContextUtils; import org.apache.syncope.ext.elasticsearch.client.ElasticsearchUtils; import org.elasticsearch.action.search.SearchRequestBuilder; import org.elasticsearch.action.search.SearchType; @@ -119,8 +118,7 @@ public class ElasticsearchAnySearchDAO extends AbstractAnySearchDAO { Pair<DisMaxQueryBuilder, Set<String>> filter = adminRealmsFilter(adminRealms); - return client.prepareSearch(AuthContextUtils.getDomain().toLowerCase()). - setTypes(kind.name()). + return client.prepareSearch(elasticsearchUtils.getContextDomainName(kind)). setSearchType(SearchType.QUERY_THEN_FETCH). setQuery(SyncopeConstants.FULL_ADMIN_REALMS.equals(adminRealms) ? getQueryBuilder(cond, kind) diff --git a/ext/elasticsearch/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/ElasticsearchReindex.java b/ext/elasticsearch/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/ElasticsearchReindex.java index 9a0159f..1a3331f 100644 --- a/ext/elasticsearch/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/ElasticsearchReindex.java +++ b/ext/elasticsearch/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/ElasticsearchReindex.java @@ -18,6 +18,7 @@ */ package org.apache.syncope.core.provisioning.java.job; +import java.util.concurrent.ExecutionException; import org.apache.syncope.common.lib.types.AnyTypeKind; import org.apache.syncope.core.persistence.api.dao.AnyDAO; import org.apache.syncope.core.persistence.api.dao.AnyObjectDAO; @@ -66,18 +67,9 @@ public class ElasticsearchReindex extends AbstractSchedTaskJobDelegate { protected String doExecute(final boolean dryRun) throws JobExecutionException { if (!dryRun) { try { - LOG.debug("Start rebuild index {}", AuthContextUtils.getDomain().toLowerCase()); - - IndicesExistsResponse existsIndexResponse = client.admin().indices(). - exists(new IndicesExistsRequest(AuthContextUtils.getDomain().toLowerCase())). - get(); - if (existsIndexResponse.isExists()) { - AcknowledgedResponse acknowledgedResponse = client.admin().indices(). - delete(new DeleteIndexRequest(AuthContextUtils.getDomain().toLowerCase())). - get(); - LOG.debug("Successfully removed {}: {}", - AuthContextUtils.getDomain().toLowerCase(), acknowledgedResponse); - } + checkExistsIndexResponse(AnyTypeKind.USER); + checkExistsIndexResponse(AnyTypeKind.GROUP); + checkExistsIndexResponse(AnyTypeKind.ANY_OBJECT); XContentBuilder settings = XContentFactory.jsonBuilder(). startObject(). @@ -94,7 +86,35 @@ public class ElasticsearchReindex extends AbstractSchedTaskJobDelegate { endObject(). endObject(). endObject(); - XContentBuilder mapping = XContentFactory.jsonBuilder(). + XContentBuilder mappingUser = XContentFactory.jsonBuilder(). + startObject(). + startArray("dynamic_templates"). + startObject(). + startObject("strings"). + field("match_mapping_type", "string"). + startObject("mapping"). + field("type", "keyword"). + field("analyzer", "string_lowercase"). + endObject(). + endObject(). + endObject(). + endArray(). + endObject(); + XContentBuilder mappingGroup = XContentFactory.jsonBuilder(). + startObject(). + startArray("dynamic_templates"). + startObject(). + startObject("strings"). + field("match_mapping_type", "string"). + startObject("mapping"). + field("type", "keyword"). + field("analyzer", "string_lowercase"). + endObject(). + endObject(). + endObject(). + endArray(). + endObject(); + XContentBuilder mappingAnyobject = XContentFactory.jsonBuilder(). startObject(). startArray("dynamic_templates"). startObject(). @@ -108,21 +128,16 @@ public class ElasticsearchReindex extends AbstractSchedTaskJobDelegate { endObject(). endArray(). endObject(); - CreateIndexResponse createIndexResponse = client.admin().indices(). - create(new CreateIndexRequest(AuthContextUtils.getDomain().toLowerCase()). - settings(settings). - mapping(AnyTypeKind.USER.name(), mapping). - mapping(AnyTypeKind.GROUP.name(), mapping). - mapping(AnyTypeKind.ANY_OBJECT.name(), mapping)). - get(); - LOG.debug("Successfully created {}: {}", - AuthContextUtils.getDomain().toLowerCase(), createIndexResponse); + + createIndexResponse(AnyTypeKind.USER, settings, mappingUser); + createIndexResponse(AnyTypeKind.GROUP, settings, mappingGroup); + createIndexResponse(AnyTypeKind.ANY_OBJECT, settings, mappingAnyobject); LOG.debug("Indexing users..."); for (int page = 1; page <= (userDAO.count() / AnyDAO.DEFAULT_PAGE_SIZE) + 1; page++) { for (User user : userDAO.findAll(page, AnyDAO.DEFAULT_PAGE_SIZE)) { IndexResponse response = client.prepareIndex( - AuthContextUtils.getDomain().toLowerCase(), + elasticsearchUtils.getContextDomainName(AnyTypeKind.USER), AnyTypeKind.USER.name(), user.getKey()). setSource(elasticsearchUtils.builder(user)). @@ -134,7 +149,7 @@ public class ElasticsearchReindex extends AbstractSchedTaskJobDelegate { for (int page = 1; page <= (groupDAO.count() / AnyDAO.DEFAULT_PAGE_SIZE) + 1; page++) { for (Group group : groupDAO.findAll(page, AnyDAO.DEFAULT_PAGE_SIZE)) { IndexResponse response = client.prepareIndex( - AuthContextUtils.getDomain().toLowerCase(), + elasticsearchUtils.getContextDomainName(AnyTypeKind.GROUP), AnyTypeKind.GROUP.name(), group.getKey()). setSource(elasticsearchUtils.builder(group)). @@ -146,7 +161,7 @@ public class ElasticsearchReindex extends AbstractSchedTaskJobDelegate { for (int page = 1; page <= (anyObjectDAO.count() / AnyDAO.DEFAULT_PAGE_SIZE) + 1; page++) { for (AnyObject anyObject : anyObjectDAO.findAll(page, AnyDAO.DEFAULT_PAGE_SIZE)) { IndexResponse response = client.prepareIndex( - AuthContextUtils.getDomain().toLowerCase(), + elasticsearchUtils.getContextDomainName(AnyTypeKind.ANY_OBJECT), AnyTypeKind.ANY_OBJECT.name(), anyObject.getKey()). setSource(elasticsearchUtils.builder(anyObject)). @@ -165,6 +180,34 @@ public class ElasticsearchReindex extends AbstractSchedTaskJobDelegate { return "SUCCESS"; } + private void checkExistsIndexResponse(final AnyTypeKind kind) throws InterruptedException, ExecutionException { + LOG.debug("Start rebuild index {}", + elasticsearchUtils.getContextDomainName(kind)); + IndicesExistsResponse existsIndexResponse = client.admin().indices(). + exists(new IndicesExistsRequest(elasticsearchUtils.getContextDomainName(kind))). + get(); + if (existsIndexResponse.isExists()) { + AcknowledgedResponse acknowledgedResponse = client.admin().indices(). + delete(new DeleteIndexRequest(elasticsearchUtils.getContextDomainName(kind))). + get(); + LOG.debug("Successfully removed {}: {}", + elasticsearchUtils.getContextDomainName(kind), acknowledgedResponse); + } + } + + private void createIndexResponse(final AnyTypeKind kind, + final XContentBuilder settings, + final XContentBuilder mapping) throws InterruptedException, ExecutionException { + + CreateIndexResponse createIndexResponseUser = client.admin().indices(). + create(new CreateIndexRequest(elasticsearchUtils.getContextDomainName(kind)). + settings(settings). + mapping(kind.name(), mapping)). + get(); + LOG.debug("Successfully created {} for {}: {}", + elasticsearchUtils.getContextDomainName(kind), kind.name(), createIndexResponseUser); + } + @Override protected boolean hasToBeRegistered(final TaskExec execution) { return true; diff --git a/fit/core-reference/src/main/java/org/apache/syncope/core/logic/init/ElasticsearchInit.java b/fit/core-reference/src/main/java/org/apache/syncope/core/logic/init/ElasticsearchInit.java new file mode 100644 index 0000000..60d1893 --- /dev/null +++ b/fit/core-reference/src/main/java/org/apache/syncope/core/logic/init/ElasticsearchInit.java @@ -0,0 +1,71 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.syncope.core.logic.init; + +import org.apache.syncope.common.lib.to.SchedTaskTO; +import org.apache.syncope.common.lib.types.ImplementationEngine; +import org.apache.syncope.common.lib.types.ImplementationType; +import org.apache.syncope.common.lib.types.TaskType; +import org.apache.syncope.core.logic.TaskLogic; +import org.apache.syncope.core.persistence.api.dao.ImplementationDAO; +import org.apache.syncope.core.persistence.api.entity.EntityFactory; +import org.apache.syncope.core.persistence.api.entity.Implementation; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +@Component +public class ElasticsearchInit { + + @Autowired + private ImplementationDAO implementationDAO; + + @Autowired + private EntityFactory entityFactory; + + @Autowired + private TaskLogic taskLogic; + + private static final String ES_REINDEX = "org.apache.syncope.core.provisioning.java.job.ElasticsearchReindex"; + + @Transactional + public void init() { + Implementation reindex = implementationDAO.find(ImplementationType.TASKJOB_DELEGATE). + stream(). + filter(impl -> impl.getEngine() == ImplementationEngine.JAVA + && ES_REINDEX.equals(impl.getBody())). + findAny().orElse(null); + if (reindex == null) { + reindex = entityFactory.newEntity(Implementation.class); + reindex.setKey(ES_REINDEX); + reindex.setEngine(ImplementationEngine.JAVA); + reindex.setType(ImplementationType.TASKJOB_DELEGATE); + reindex.setBody(ES_REINDEX); + reindex = implementationDAO.save(reindex); + } + + SchedTaskTO task = new SchedTaskTO(); + task.setJobDelegate(reindex.getKey()); + task.setName("Elasticsearch Reindex"); + task = taskLogic.createSchedTask(TaskType.SCHEDULED, task); + + taskLogic.execute(task.getKey(), null, false); + } + +} diff --git a/fit/core-reference/src/main/java/org/apache/syncope/fit/core/reference/ITImplementationLookup.java b/fit/core-reference/src/main/java/org/apache/syncope/fit/core/reference/ITImplementationLookup.java index b3b03f3..c8c2a5e 100644 --- a/fit/core-reference/src/main/java/org/apache/syncope/fit/core/reference/ITImplementationLookup.java +++ b/fit/core-reference/src/main/java/org/apache/syncope/fit/core/reference/ITImplementationLookup.java @@ -40,11 +40,8 @@ import org.apache.syncope.common.lib.report.ReconciliationReportletConf; import org.apache.syncope.common.lib.report.ReportletConf; import org.apache.syncope.common.lib.report.StaticReportletConf; import org.apache.syncope.common.lib.report.UserReportletConf; -import org.apache.syncope.common.lib.to.SchedTaskTO; -import org.apache.syncope.common.lib.types.ImplementationEngine; import org.apache.syncope.common.lib.types.ImplementationType; -import org.apache.syncope.common.lib.types.TaskType; -import org.apache.syncope.core.logic.TaskLogic; +import org.apache.syncope.core.logic.init.ElasticsearchInit; import org.apache.syncope.core.provisioning.java.job.report.AuditReportlet; import org.apache.syncope.core.provisioning.java.job.report.GroupReportlet; import org.apache.syncope.core.provisioning.java.job.report.ReconciliationReportlet; @@ -54,13 +51,10 @@ import org.apache.syncope.core.persistence.api.DomainsHolder; import org.apache.syncope.core.persistence.api.ImplementationLookup; import org.apache.syncope.core.persistence.api.dao.AccountRule; import org.apache.syncope.core.persistence.api.dao.AnySearchDAO; -import org.apache.syncope.core.persistence.api.dao.ImplementationDAO; import org.apache.syncope.core.persistence.api.dao.PasswordRule; import org.apache.syncope.core.persistence.api.dao.PullCorrelationRule; import org.apache.syncope.core.persistence.api.dao.PushCorrelationRule; import org.apache.syncope.core.persistence.api.dao.Reportlet; -import org.apache.syncope.core.persistence.api.entity.EntityFactory; -import org.apache.syncope.core.persistence.api.entity.Implementation; import org.apache.syncope.core.persistence.jpa.attrvalue.validation.AlwaysTrueValidator; import org.apache.syncope.core.persistence.jpa.attrvalue.validation.BasicValidator; import org.apache.syncope.core.persistence.jpa.attrvalue.validation.BinaryValidator; @@ -88,8 +82,6 @@ import org.springframework.beans.factory.annotation.Autowired; */ public class ITImplementationLookup implements ImplementationLookup { - private static final String ES_REINDEX = "org.apache.syncope.core.provisioning.java.job.ElasticsearchReindex"; - private static final Set<Class<?>> JWTSSOPROVIDER_CLASSES = new HashSet<>( Arrays.asList(SyncopeJWTSSOProvider.class, CustomJWTSSOProvider.class)); @@ -243,16 +235,10 @@ public class ITImplementationLookup implements ImplementationLookup { private AnySearchDAO anySearchDAO; @Autowired - private ImplementationDAO implementationDAO; - - @Autowired - private EntityFactory entityFactory; - - @Autowired private DomainsHolder domainsHolder; @Autowired - private TaskLogic taskLogic; + private ElasticsearchInit elasticsearchInit; @Override public Integer getPriority() { @@ -265,25 +251,7 @@ public class ITImplementationLookup implements ImplementationLookup { if (AopUtils.getTargetClass(anySearchDAO).getName().contains("Elasticsearch")) { for (Map.Entry<String, DataSource> entry : domainsHolder.getDomains().entrySet()) { AuthContextUtils.execWithAuthContext(entry.getKey(), () -> { - Implementation reindex = implementationDAO.find(ImplementationType.TASKJOB_DELEGATE). - stream(). - filter(impl -> impl.getEngine() == ImplementationEngine.JAVA - && ES_REINDEX.equals(impl.getBody())). - findAny().orElse(null); - if (reindex == null) { - reindex = entityFactory.newEntity(Implementation.class); - reindex.setEngine(ImplementationEngine.JAVA); - reindex.setType(ImplementationType.TASKJOB_DELEGATE); - reindex.setBody(ES_REINDEX); - reindex = implementationDAO.save(reindex); - } - - SchedTaskTO task = new SchedTaskTO(); - task.setJobDelegate(reindex.getKey()); - task.setName("Elasticsearch Reindex"); - task = taskLogic.createSchedTask(TaskType.SCHEDULED, task); - - taskLogic.execute(task.getKey(), null, false); + elasticsearchInit.init(); return null; }); diff --git a/src/main/asciidoc/reference-guide/concepts/notifications.adoc b/src/main/asciidoc/reference-guide/concepts/notifications.adoc index d1c69ae..71ac025 100644 --- a/src/main/asciidoc/reference-guide/concepts/notifications.adoc +++ b/src/main/asciidoc/reference-guide/concepts/notifications.adoc @@ -65,7 +65,7 @@ An event is identified by the following five coordinates: ** `PUSH` ** `CUSTOM` . category - the possible values depend on the selected type: for `LOGIC` the <<logic>> components available, -for `TASK` the various <<tasks-custom>> Tasks configured, for `PROPAGATION`, `PULL` and `PUSH` the defined Any Types +for `TASK` the various <<tasks-custom, Custom Tasks>> configured, for `PROPAGATION`, `PULL` and `PUSH` the defined Any Types . subcategory - completes category with external resource name, when selecting `PROPAGATION`, `PULL` or `PUSH` . event type - the final identification of the event; depends on the other coordinates . success or failure - whether the current event shall be considered in case of success or failure diff --git a/src/main/asciidoc/reference-guide/workingwithapachesyncope/customization.adoc b/src/main/asciidoc/reference-guide/workingwithapachesyncope/customization.adoc index 986de12..1c4ec41 100644 --- a/src/main/asciidoc/reference-guide/workingwithapachesyncope/customization.adoc +++ b/src/main/asciidoc/reference-guide/workingwithapachesyncope/customization.adoc @@ -487,6 +487,13 @@ classpath*:/provisioning*Context.xml classpath*:/workflow*Context.xml .... +It is also required to initialize the Elasticsearch indexes. + +Add a new Java <<implementations,implementation>> for `TASKJOB_DELEGATE` and use +`org.apache.syncope.core.provisioning.java.job.ElasticsearchReindex` as class. + +Then, create a new <<tasks-custom, Custom task>>, select the implementation just created as job delegate and execute it. + [discrete] ===== Enable the <<SCIM>> extension