You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@syncope.apache.org by mm...@apache.org on 2020/02/06 11:20:38 UTC

[syncope] branch 2_1_X updated: SYNCOPE-1506: Merge Accounts (#153)

This is an automated email from the ASF dual-hosted git repository.

mmoayyed pushed a commit to branch 2_1_X
in repository https://gitbox.apache.org/repos/asf/syncope.git


The following commit(s) were added to refs/heads/2_1_X by this push:
     new 112acd6  SYNCOPE-1506: Merge Accounts (#153)
112acd6 is described below

commit 112acd62d3f0d0a637de204f9d999f28122432f0
Author: Misagh Moayyed <mm...@gmail.com>
AuthorDate: Thu Feb 6 15:20:30 2020 +0400

    SYNCOPE-1506: Merge Accounts (#153)
    
    * initial pass at merging users/accounts
    
    * clean up config; working on wizard and user selection
    
    * clean up config; working on wizard and user selection
    
    * polish
    
    * working on account review wizard page
    
    * working on batch items
    
    * working on batch requests
    
    * testing batch ops
    
    * fix tests
    
    * fix test
    
    * fix formatting issues
    
    * apply changes based on review suggestions
    
    * working on ITs for merge-accounts feature
    
    * updated test
    
    * adjustments to query filter
    
    * tweaks to UI
    
    * cntd with wicket changes and merging functionality
    
    * working on merge wizard. added panels to show resources, and preview
    
    * add panel for preview
    
    * polish
    
    * polish
    
    * cntd with tests - wip
    
    * addressing feedback after review - tests WIP.
    
    * fix issues after review
    
    * highlight item when selected in data table
    
    * work out events with item selection after the merge
    
    * fix tests - let ci run
    
    * fix checkstyle
---
 .../panels/MergeLinkedAccountsResourcesPanel.java  | 182 +++++++++++++++++++
 .../panels/MergeLinkedAccountsReviewPanel.java     | 177 ++++++++++++++++++
 .../panels/MergeLinkedAccountsSearchPanel.java     | 112 ++++++++++++
 .../client/console/panels/UserDirectoryPanel.java  |  20 +++
 .../client/console/rest/ResourceRestClient.java    |  15 ++
 .../repeater/data/table/AjaxFallbackDataTable.java |   1 +
 .../wicket/markup/html/form/ActionLink.java        |   1 +
 .../syncope/client/console/wizards/AjaxWizard.java |   9 +-
 .../any/MergeLinkedAccountsWizardBuilder.java      | 200 +++++++++++++++++++++
 .../any/MergeLinkedAccountsWizardModel.java        |  58 ++++++
 .../panels/MergeLinkedAccountsResourcesPanel.html  |  24 +++
 ...> MergeLinkedAccountsResourcesPanel.properties} |   7 +-
 ...eLinkedAccountsResourcesPanel_fr_CA.properties} |   7 +-
 ...ergeLinkedAccountsResourcesPanel_it.properties} |   7 +-
 ...ergeLinkedAccountsResourcesPanel_ja.properties} |   7 +-
 ...eLinkedAccountsResourcesPanel_pt_BR.properties} |   7 +-
 ...ergeLinkedAccountsResourcesPanel_ru.properties} |   7 +-
 .../panels/MergeLinkedAccountsReviewPanel.html     |  24 +++
 ...s => MergeLinkedAccountsReviewPanel.properties} |  10 +-
 ...ergeLinkedAccountsReviewPanel_fr_CA.properties} |  10 +-
 ...> MergeLinkedAccountsReviewPanel_it.properties} |  10 +-
 ...> MergeLinkedAccountsReviewPanel_ja.properties} |  10 +-
 ...ergeLinkedAccountsReviewPanel_pt_BR.properties} |  10 +-
 ...> MergeLinkedAccountsReviewPanel_ru.properties} |  10 +-
 .../panels/MergeLinkedAccountsSearchPanel.html     |  31 ++++
 ...s => MergeLinkedAccountsSearchPanel.properties} |   5 +-
 ...ergeLinkedAccountsSearchPanel_fr_CA.properties} |   5 +-
 ...> MergeLinkedAccountsSearchPanel_it.properties} |   5 +-
 ...> MergeLinkedAccountsSearchPanel_ja.properties} |   5 +-
 ...ergeLinkedAccountsSearchPanel_pt_BR.properties} |   5 +-
 ...> MergeLinkedAccountsSearchPanel_ru.properties} |   5 +-
 .../console/panels/UserDirectoryPanel.properties   |   1 +
 .../panels/UserDirectoryPanel_fr_CA.properties     |   7 +-
 .../panels/UserDirectoryPanel_it.properties        |   1 +
 .../panels/UserDirectoryPanel_ja.properties        |   1 +
 .../panels/UserDirectoryPanel_pt_BR.properties     |   1 +
 .../panels/UserDirectoryPanel_ru.properties        |   1 +
 .../markup/html/form/ActionsPanel.properties       |   4 +
 .../syncope/common/rest/api/RESTHeaders.java       |   2 +
 .../common/rest/api/service/ResourceService.java   |  25 +++
 .../apache/syncope/core/logic/ResourceLogic.java   |  24 +++
 .../core/rest/cxf/service/ResourceServiceImpl.java |   6 +
 .../syncope/fit/console/LinkedAccountsITCase.java  | 197 ++++++++++++++++++++
 .../apache/syncope/fit/console/PoliciesITCase.java |   4 +-
 .../apache/syncope/fit/console/UsersITCase.java    |   4 +-
 45 files changed, 1184 insertions(+), 80 deletions(-)

diff --git a/client/console/src/main/java/org/apache/syncope/client/console/panels/MergeLinkedAccountsResourcesPanel.java b/client/console/src/main/java/org/apache/syncope/client/console/panels/MergeLinkedAccountsResourcesPanel.java
new file mode 100644
index 0000000..628b1a6
--- /dev/null
+++ b/client/console/src/main/java/org/apache/syncope/client/console/panels/MergeLinkedAccountsResourcesPanel.java
@@ -0,0 +1,182 @@
+/*
+ * 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.client.console.panels;
+
+import de.agilecoders.wicket.core.markup.html.bootstrap.dialog.Modal;
+import org.apache.syncope.client.console.SyncopeConsoleApplication;
+import org.apache.syncope.client.console.commons.Constants;
+import org.apache.syncope.client.console.commons.DirectoryDataProvider;
+import org.apache.syncope.client.console.commons.SortableDataProviderComparator;
+import org.apache.syncope.client.console.pages.BasePage;
+import org.apache.syncope.client.console.rest.ResourceRestClient;
+import org.apache.syncope.client.console.wicket.markup.html.form.ActionLink;
+import org.apache.syncope.client.console.wicket.markup.html.form.ActionsPanel;
+import org.apache.syncope.client.console.wizards.any.MergeLinkedAccountsWizardModel;
+import org.apache.syncope.common.lib.to.ResourceTO;
+import org.apache.syncope.common.lib.types.StandardEntitlement;
+import org.apache.wicket.PageReference;
+import org.apache.wicket.ajax.AjaxRequestTarget;
+import org.apache.wicket.extensions.markup.html.repeater.data.sort.SortOrder;
+import org.apache.wicket.extensions.markup.html.repeater.data.table.IColumn;
+import org.apache.wicket.extensions.markup.html.repeater.data.table.PropertyColumn;
+import org.apache.wicket.extensions.wizard.WizardModel.ICondition;
+import org.apache.wicket.extensions.wizard.WizardStep;
+import org.apache.wicket.model.CompoundPropertyModel;
+import org.apache.wicket.model.IModel;
+import org.apache.wicket.model.Model;
+import org.apache.wicket.model.ResourceModel;
+import org.apache.wicket.model.StringResourceModel;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+
+public class MergeLinkedAccountsResourcesPanel extends WizardStep implements ICondition {
+    private static final long serialVersionUID = 1221037007528732347L;
+
+    private final MergeLinkedAccountsWizardModel wizardModel;
+
+    public MergeLinkedAccountsResourcesPanel(final MergeLinkedAccountsWizardModel wizardModel,
+                                             final PageReference pageReference) {
+        super();
+        setOutputMarkupId(true);
+        this.wizardModel = wizardModel;
+        add(new ResourceSelectionDirectoryPanel("resources", pageReference));
+    }
+
+    @Override
+    public boolean evaluate() {
+        return SyncopeConsoleApplication.get().getSecuritySettings().getAuthorizationStrategy().
+            isActionAuthorized(this, RENDER);
+    }
+
+    @Override
+    public String getTitle() {
+        setSummaryModel(new StringResourceModel("mergeLinkedAccounts.searchResource.summary",
+            Model.of(wizardModel.getMergingUser())));
+        setTitleModel(new StringResourceModel("mergeLinkedAccounts.searchResource.title",
+            Model.of(wizardModel.getMergingUser())));
+        return super.getTitle();
+    }
+
+    private class ResourceSelectionDirectoryPanel extends
+        DirectoryPanel<ResourceTO, ResourceTO,
+            ResourceSelectionDirectoryPanel.ResourcesDataProvider, ResourceRestClient> {
+
+        private static final long serialVersionUID = 6005711052393825472L;
+
+        ResourceSelectionDirectoryPanel(final String id, final PageReference pageReference) {
+            super(id, pageReference, true);
+
+            this.restClient = new ResourceRestClient();
+            modal.size(Modal.Size.Large);
+            setOutputMarkupId(true);
+            disableCheckBoxes();
+            initResultTable();
+        }
+
+        @Override
+        protected ResourcesDataProvider dataProvider() {
+            return new ResourcesDataProvider(this.rows);
+        }
+
+        @Override
+        protected String paginatorRowsKey() {
+            return Constants.PREF_RESOURCES_PAGINATOR_ROWS;
+        }
+
+        @Override
+        protected List<IColumn<ResourceTO, String>> getColumns() {
+            List<IColumn<ResourceTO, String>> columns = new ArrayList<>();
+            columns.add(new PropertyColumn<>(new ResourceModel("resource"), "key", "key"));
+            return columns;
+        }
+
+        @Override
+        protected ActionsPanel<ResourceTO> getActions(final IModel<ResourceTO> model) {
+            final ActionsPanel<ResourceTO> panel = super.getActions(model);
+            panel.add(new ActionLink<ResourceTO>() {
+                private static final long serialVersionUID = -7978723352517770644L;
+
+                @Override
+                public void onClick(final AjaxRequestTarget target, final ResourceTO resource) {
+                    MergeLinkedAccountsWizardModel model = MergeLinkedAccountsResourcesPanel.this.wizardModel;
+                    String connObjectKeyValue = restClient.getConnObjectKeyValue(
+                        resource.getKey(),
+                        model.getMergingUser().getType(),
+                        model.getMergingUser().getKey());
+                    if (connObjectKeyValue != null) {
+                        model.setResource(resource);
+                        String tableId = MergeLinkedAccountsResourcesPanel.this.
+                            get("resources:container:content:searchContainer:resultTable"
+                                + ":tablePanel:groupForm:checkgroup:dataTable").
+                            getMarkupId();
+                        String js = "$('#" + tableId + "').removeClass('active');";
+                        js += "$('#" + tableId + " tbody tr td div').filter(function() "
+                            + "{return $(this).text() === \"" + resource.getKey() + "\";})"
+                            + ".parent().parent().addClass('active');";
+                        target.prependJavaScript(js);
+
+                    } else {
+                        error("Unable to determine connector object key");
+                        ((BasePage) pageRef.getPage()).getNotificationPanel().refresh(target);
+                    }
+                }
+            }, ActionLink.ActionType.SELECT, StandardEntitlement.RESOURCE_READ);
+            return panel;
+        }
+
+        @Override
+        protected Collection<ActionLink.ActionType> getBatches() {
+            return Collections.emptyList();
+        }
+
+        protected final class ResourcesDataProvider extends DirectoryDataProvider<ResourceTO> {
+
+            private static final long serialVersionUID = -185944053385660794L;
+
+            private final SortableDataProviderComparator<ResourceTO> comparator;
+
+            private ResourcesDataProvider(final int paginatorRows) {
+                super(paginatorRows);
+                setSort("key", SortOrder.ASCENDING);
+                comparator = new SortableDataProviderComparator<>(this);
+            }
+
+            @Override
+            public Iterator<ResourceTO> iterator(final long first, final long count) {
+                List<ResourceTO> list = restClient.list();
+                Collections.sort(list, comparator);
+                return list.subList((int) first, (int) first + (int) count).iterator();
+            }
+
+            @Override
+            public long size() {
+                return restClient.list().size();
+            }
+
+            @Override
+            public IModel<ResourceTO> model(final ResourceTO object) {
+                return new CompoundPropertyModel<>(object);
+            }
+        }
+    }
+}
diff --git a/client/console/src/main/java/org/apache/syncope/client/console/panels/MergeLinkedAccountsReviewPanel.java b/client/console/src/main/java/org/apache/syncope/client/console/panels/MergeLinkedAccountsReviewPanel.java
new file mode 100644
index 0000000..73ab52a
--- /dev/null
+++ b/client/console/src/main/java/org/apache/syncope/client/console/panels/MergeLinkedAccountsReviewPanel.java
@@ -0,0 +1,177 @@
+/*
+ * 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.client.console.panels;
+
+import de.agilecoders.wicket.core.markup.html.bootstrap.dialog.Modal;
+import org.apache.syncope.client.console.commons.DirectoryDataProvider;
+import org.apache.syncope.client.console.commons.SortableDataProviderComparator;
+import org.apache.syncope.client.console.rest.ResourceRestClient;
+import org.apache.syncope.client.console.wicket.extensions.markup.html.repeater.data.table.BooleanPropertyColumn;
+import org.apache.syncope.client.console.wicket.markup.html.form.ActionLink;
+import org.apache.syncope.client.console.wizards.any.MergeLinkedAccountsWizardModel;
+import org.apache.syncope.common.lib.to.LinkedAccountTO;
+import org.apache.syncope.common.lib.to.UserTO;
+import org.apache.wicket.PageReference;
+import org.apache.wicket.extensions.markup.html.repeater.data.sort.SortOrder;
+import org.apache.wicket.extensions.markup.html.repeater.data.table.IColumn;
+import org.apache.wicket.extensions.markup.html.repeater.data.table.PropertyColumn;
+import org.apache.wicket.extensions.wizard.WizardStep;
+import org.apache.wicket.model.CompoundPropertyModel;
+import org.apache.wicket.model.IModel;
+import org.apache.wicket.model.ResourceModel;
+import org.apache.wicket.model.StringResourceModel;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.stream.Collectors;
+
+public class MergeLinkedAccountsReviewPanel extends WizardStep {
+    private static final long serialVersionUID = 1221037007528732347L;
+
+    public MergeLinkedAccountsReviewPanel(final MergeLinkedAccountsWizardModel wizardModel,
+                                          final PageReference pageReference) {
+        super();
+        setOutputMarkupId(true);
+        setTitleModel(new StringResourceModel("mergeLinkedAccounts.reviewAccounts.title"));
+        add(new LinkedAccountsReviewDirectoryPanel("linkedAccounts", pageReference, wizardModel));
+    }
+
+    private static class LinkedAccountsReviewDirectoryPanel extends
+        DirectoryPanel<LinkedAccountTO, LinkedAccountTO,
+            LinkedAccountsReviewDirectoryPanel.LinkedAccountsDataProvider, ResourceRestClient> {
+
+        private static final String PAGINATOR_ROWS = "linked.account.review.paginator.rows";
+
+        private static final long serialVersionUID = 6005711052393825472L;
+
+        private final MergeLinkedAccountsWizardModel wizardModel;
+
+        LinkedAccountsReviewDirectoryPanel(final String id, final PageReference pageReference,
+                                           final MergeLinkedAccountsWizardModel wizardModel) {
+            super(id, pageReference, true);
+            this.restClient = new ResourceRestClient();
+            this.wizardModel = wizardModel;
+            modal.size(Modal.Size.Large);
+            setOutputMarkupId(true);
+            disableCheckBoxes();
+            initResultTable();
+        }
+
+        @Override
+        protected LinkedAccountsDataProvider dataProvider() {
+            return new LinkedAccountsDataProvider(this.rows);
+        }
+
+        @Override
+        protected String paginatorRowsKey() {
+            return PAGINATOR_ROWS;
+        }
+
+        @Override
+        protected List<IColumn<LinkedAccountTO, String>> getColumns() {
+            List<IColumn<LinkedAccountTO, String>> columns = new ArrayList<>();
+            columns.add(new PropertyColumn<>(new ResourceModel("resource"), "resource", "resource"));
+            columns.add(new PropertyColumn<>(
+                new ResourceModel("connObjectKeyValue"), "connObjectKeyValue", "connObjectKeyValue"));
+            columns.add(new PropertyColumn<>(
+                new ResourceModel("username"), "username", "username"));
+            columns.add(new BooleanPropertyColumn<>(
+                new ResourceModel("suspended"), "suspended", "suspended"));
+            return columns;
+        }
+
+        @Override
+        protected Collection<ActionLink.ActionType> getBatches() {
+            return Collections.emptyList();
+        }
+
+        private List<LinkedAccountTO> previewAccounts() {
+            UserTO mergingUser = wizardModel.getMergingUser();
+
+            // Move linked accounts into the target/base user as linked accounts
+            List<LinkedAccountTO> accounts = mergingUser.getLinkedAccounts().stream().map(acct -> {
+                LinkedAccountTO linkedAccount =
+                    new LinkedAccountTO.Builder(acct.getResource(), acct.getConnObjectKeyValue())
+                        .password(acct.getPassword())
+                        .suspended(acct.isSuspended())
+                        .username(acct.getUsername())
+                        .build();
+                linkedAccount.getPlainAttrs().addAll(acct.getPlainAttrs());
+                linkedAccount.getPrivileges().addAll(acct.getPrivileges());
+                return linkedAccount;
+            }).collect(Collectors.toList());
+
+            // Move merging user's resources into the target/base user as a linked account
+            accounts.addAll(mergingUser.getResources().stream().map(resource -> {
+                String connObjectKeyValue = restClient.getConnObjectKeyValue(resource,
+                    mergingUser.getType(), mergingUser.getKey());
+                return new LinkedAccountTO.Builder(resource, connObjectKeyValue).build();
+            }).collect(Collectors.toList()));
+
+            // Move merging user into target/base user as a linked account
+            String connObjectKeyValue = restClient.getConnObjectKeyValue(
+                wizardModel.getResource().getKey(),
+                mergingUser.getType(), mergingUser.getKey());
+            LinkedAccountTO linkedAccount =
+                new LinkedAccountTO.Builder(wizardModel.getResource().getKey(), connObjectKeyValue)
+                    .password(mergingUser.getPassword())
+                    .suspended(mergingUser.isSuspended())
+                    .username(mergingUser.getUsername())
+                    .build();
+            linkedAccount.getPlainAttrs().addAll(mergingUser.getPlainAttrs());
+            linkedAccount.getPrivileges().addAll(mergingUser.getPrivileges());
+            accounts.add(linkedAccount);
+
+            return accounts;
+        }
+
+        protected final class LinkedAccountsDataProvider extends DirectoryDataProvider<LinkedAccountTO> {
+
+            private static final long serialVersionUID = -185944053385660794L;
+
+            private final SortableDataProviderComparator<LinkedAccountTO> comparator;
+
+            private LinkedAccountsDataProvider(final int paginatorRows) {
+                super(paginatorRows);
+                setSort("resource", SortOrder.ASCENDING);
+                comparator = new SortableDataProviderComparator<>(this);
+            }
+
+            @Override
+            public Iterator<LinkedAccountTO> iterator(final long first, final long count) {
+                List<LinkedAccountTO> list = previewAccounts();
+                Collections.sort(list, comparator);
+                return list.subList((int) first, (int) first + (int) count).iterator();
+            }
+
+            @Override
+            public long size() {
+                return previewAccounts().size();
+            }
+
+            @Override
+            public IModel<LinkedAccountTO> model(final LinkedAccountTO object) {
+                return new CompoundPropertyModel<>(object);
+            }
+        }
+    }
+}
diff --git a/client/console/src/main/java/org/apache/syncope/client/console/panels/MergeLinkedAccountsSearchPanel.java b/client/console/src/main/java/org/apache/syncope/client/console/panels/MergeLinkedAccountsSearchPanel.java
new file mode 100644
index 0000000..5e8d55a
--- /dev/null
+++ b/client/console/src/main/java/org/apache/syncope/client/console/panels/MergeLinkedAccountsSearchPanel.java
@@ -0,0 +1,112 @@
+/*
+ * 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.client.console.panels;
+
+import org.apache.syncope.client.console.panels.search.AnySelectionDirectoryPanel;
+import org.apache.syncope.client.console.panels.search.SearchClausePanel;
+import org.apache.syncope.client.console.panels.search.SearchUtils;
+import org.apache.syncope.client.console.panels.search.UserSearchPanel;
+import org.apache.syncope.client.console.panels.search.UserSelectionDirectoryPanel;
+import org.apache.syncope.client.console.rest.AnyTypeClassRestClient;
+import org.apache.syncope.client.console.rest.AnyTypeRestClient;
+import org.apache.syncope.client.console.rest.UserRestClient;
+import org.apache.syncope.client.console.wizards.any.MergeLinkedAccountsWizardModel;
+import org.apache.syncope.client.lib.SyncopeClient;
+import org.apache.syncope.common.lib.to.AnyTO;
+import org.apache.syncope.common.lib.to.AnyTypeTO;
+import org.apache.syncope.common.lib.types.AnyTypeKind;
+import org.apache.wicket.Component;
+import org.apache.wicket.PageReference;
+import org.apache.wicket.ajax.AjaxRequestTarget;
+import org.apache.wicket.event.IEvent;
+import org.apache.wicket.extensions.wizard.WizardStep;
+import org.apache.wicket.markup.html.WebMarkupContainer;
+import org.apache.wicket.markup.html.panel.Fragment;
+import org.apache.wicket.model.Model;
+import org.apache.wicket.model.StringResourceModel;
+import org.apache.wicket.model.util.ListModel;
+
+import java.util.ArrayList;
+
+public class MergeLinkedAccountsSearchPanel extends WizardStep {
+    private static final long serialVersionUID = 1221037007528732347L;
+
+    private final WebMarkupContainer ownerContainer;
+
+    private final UserSearchPanel userSearchPanel;
+
+    private final AnyTypeClassRestClient anyTypeClassRestClient = new AnyTypeClassRestClient();
+
+    private final AnyTypeRestClient anyTypeRestClient = new AnyTypeRestClient();
+
+    private final UserSelectionDirectoryPanel userDirectoryPanel;
+
+    private final Fragment userSearchFragment;
+
+    private final MergeLinkedAccountsWizardModel wizardModel;
+
+    public MergeLinkedAccountsSearchPanel(final MergeLinkedAccountsWizardModel model, final PageReference pageRef) {
+        super();
+        setOutputMarkupId(true);
+
+        this.wizardModel = model;
+        setTitleModel(new StringResourceModel("mergeLinkedAccounts.searchUser", Model.of(model.getBaseUser())));
+        ownerContainer = new WebMarkupContainer("ownerContainer");
+        ownerContainer.setOutputMarkupId(true);
+        add(ownerContainer);
+
+        userSearchFragment = new Fragment("search", "userSearchFragment", this);
+        userSearchPanel = UserSearchPanel.class.cast(new UserSearchPanel.Builder(
+            new ListModel<>(new ArrayList<>())).required(false).enableSearch(MergeLinkedAccountsSearchPanel.this).
+            build("usersearch"));
+        userSearchFragment.add(userSearchPanel);
+
+        AnyTypeTO anyTypeTO = anyTypeRestClient.read(AnyTypeKind.USER.name());
+        userDirectoryPanel = UserSelectionDirectoryPanel.class.cast(new UserSelectionDirectoryPanel.Builder(
+            anyTypeClassRestClient.list(anyTypeTO.getClasses()), anyTypeTO.getKey(), pageRef).
+            build("searchResult"));
+
+        userSearchFragment.add(userDirectoryPanel);
+        ownerContainer.add(userSearchFragment);
+    }
+
+    @Override
+    public void onEvent(final IEvent<?> event) {
+        if (event.getPayload() instanceof SearchClausePanel.SearchEvent) {
+            final AjaxRequestTarget target = SearchClausePanel.SearchEvent.class.cast(event.getPayload()).getTarget();
+            final String fiql = "username!~" + this.wizardModel.getBaseUser().getUsername() + ';'
+                + SearchUtils.buildFIQL(userSearchPanel.getModel().getObject(),
+                SyncopeClient.getUserSearchConditionBuilder());
+            userDirectoryPanel.search(fiql, target);
+        } else if (event.getPayload() instanceof AnySelectionDirectoryPanel.ItemSelection) {
+            AnySelectionDirectoryPanel.ItemSelection payload =
+                (AnySelectionDirectoryPanel.ItemSelection) event.getPayload();
+
+            final AnyTO sel = payload.getSelection();
+            this.wizardModel.setMergingUser(new UserRestClient().read(sel.getKey()));
+
+            String tableId = ((Component) event.getSource()).
+                get("container:content:searchContainer:resultTable:tablePanel:groupForm:checkgroup:dataTable").
+                getMarkupId();
+            String js = "$('#" + tableId + " tr').removeClass('active');";
+            js += "$('#" + tableId + " td[title=" + sel.getKey() + "]').parent().addClass('active');";
+            payload.getTarget().prependJavaScript(js);
+        }
+    }
+}
diff --git a/client/console/src/main/java/org/apache/syncope/client/console/panels/UserDirectoryPanel.java b/client/console/src/main/java/org/apache/syncope/client/console/panels/UserDirectoryPanel.java
index 7c437ea..5d3bc79b 100644
--- a/client/console/src/main/java/org/apache/syncope/client/console/panels/UserDirectoryPanel.java
+++ b/client/console/src/main/java/org/apache/syncope/client/console/panels/UserDirectoryPanel.java
@@ -40,6 +40,7 @@ import org.apache.syncope.client.console.wicket.markup.html.form.ActionsPanel;
 import org.apache.syncope.client.console.wizards.AjaxWizard;
 import org.apache.syncope.client.console.wizards.WizardMgtPanel;
 import org.apache.syncope.client.console.wizards.any.AnyWrapper;
+import org.apache.syncope.client.console.wizards.any.MergeLinkedAccountsWizardBuilder;
 import org.apache.syncope.client.console.wizards.any.UserWrapper;
 import org.apache.syncope.common.lib.AnyOperations;
 import org.apache.syncope.common.lib.SyncopeConstants;
@@ -342,6 +343,25 @@ public class UserDirectoryPanel extends AnyDirectoryPanel<UserTO, UserRestClient
             }, ActionType.MANAGE_ACCOUNTS,
                     String.format("%s,%s,%s", StandardEntitlement.USER_READ, StandardEntitlement.USER_UPDATE,
                             StandardEntitlement.RESOURCE_GET_CONNOBJECT));
+
+            if (wizardInModal) {
+                panel.add(new ActionLink<UserTO>() {
+                    private static final long serialVersionUID = 8011039414597736111L;
+                    
+                    @Override
+                    public void onClick(final AjaxRequestTarget target, final UserTO ignore) {
+                        model.setObject(UserRestClient.class.cast(restClient).read(model.getObject().getKey()));
+                        MergeLinkedAccountsWizardBuilder builder =
+                          new MergeLinkedAccountsWizardBuilder(model, pageRef, UserDirectoryPanel.this, modal);
+                        builder.setEventSink(builder);
+                        target.add(modal.setContent(builder.build(BaseModal.CONTENT_ID, AjaxWizard.Mode.CREATE)));
+                        modal.header(new StringResourceModel("mergeLinkedAccounts.title", model));
+                        modal.show(true);
+                    }
+                    }, ActionType.MERGE_ACCOUNTS,
+                    String.format("%s,%s,%s,%s", StandardEntitlement.USER_READ, StandardEntitlement.USER_UPDATE,
+                        StandardEntitlement.USER_DELETE, StandardEntitlement.RESOURCE_GET_CONNOBJECT));
+            }
         }
 
         if (wizardInModal) {
diff --git a/client/console/src/main/java/org/apache/syncope/client/console/rest/ResourceRestClient.java b/client/console/src/main/java/org/apache/syncope/client/console/rest/ResourceRestClient.java
index a90911f..1e0fdf3 100644
--- a/client/console/src/main/java/org/apache/syncope/client/console/rest/ResourceRestClient.java
+++ b/client/console/src/main/java/org/apache/syncope/client/console/rest/ResourceRestClient.java
@@ -27,6 +27,7 @@ import org.apache.commons.lang3.tuple.Pair;
 import org.apache.syncope.common.lib.to.ConnObjectTO;
 import org.apache.syncope.common.lib.to.PagedConnObjectTOResult;
 import org.apache.syncope.common.lib.to.ResourceTO;
+import org.apache.syncope.common.rest.api.RESTHeaders;
 import org.apache.syncope.common.rest.api.beans.ConnObjectTOQuery;
 import org.apache.syncope.common.rest.api.service.ResourceService;
 import org.apache.wicket.extensions.markup.html.repeater.util.SortParam;
@@ -56,6 +57,20 @@ public class ResourceRestClient extends BaseRestClient {
         return getService(ResourceService.class).readConnObject(resource, anyTypeKey, anyKey);
     }
 
+    public String getConnObjectKeyValue(final String resource, final String anyTypeKey, final String anyKey) {
+        try {
+            Response response = getService(ResourceService.class).getConnObjectKeyValue(resource, anyTypeKey, anyKey);
+            if (response.getStatusInfo().getFamily() == Response.Status.Family.SUCCESSFUL) {
+                return response.getHeaderString(RESTHeaders.CONNOBJECT_KEY);
+            }
+        } catch (Exception e) {
+            LOG.debug("Error fetching connector object key", e);
+        }
+        LOG.error("Unable to determine connector object key value for resource {}, {} and {}",
+            resource, anyTypeKey, anyKey);
+        return null;
+    }
+
     public Pair<String, List<ConnObjectTO>> searchConnObjects(
             final String resource,
             final String anyTypeKey,
diff --git a/client/console/src/main/java/org/apache/syncope/client/console/wicket/extensions/markup/html/repeater/data/table/AjaxFallbackDataTable.java b/client/console/src/main/java/org/apache/syncope/client/console/wicket/extensions/markup/html/repeater/data/table/AjaxFallbackDataTable.java
index ecd70d0..a3ffc29 100644
--- a/client/console/src/main/java/org/apache/syncope/client/console/wicket/extensions/markup/html/repeater/data/table/AjaxFallbackDataTable.java
+++ b/client/console/src/main/java/org/apache/syncope/client/console/wicket/extensions/markup/html/repeater/data/table/AjaxFallbackDataTable.java
@@ -119,6 +119,7 @@ public class AjaxFallbackDataTable<T extends Serializable, S> extends DataTable<
         return null;
     }
 
+
     @Override
     protected Item<T> newRowItem(final String id, final int index, final IModel<T> model) {
         final OddEvenItem<T> item = new OddEvenItem<>(id, index, model);
diff --git a/client/console/src/main/java/org/apache/syncope/client/console/wicket/markup/html/form/ActionLink.java b/client/console/src/main/java/org/apache/syncope/client/console/wicket/markup/html/form/ActionLink.java
index d62156f..306bbf3 100644
--- a/client/console/src/main/java/org/apache/syncope/client/console/wicket/markup/html/form/ActionLink.java
+++ b/client/console/src/main/java/org/apache/syncope/client/console/wicket/markup/html/form/ActionLink.java
@@ -93,6 +93,7 @@ public abstract class ActionLink<T extends Serializable> implements Serializable
         RECONCILIATION_PULL("update"),
         MANAGE_RESOURCES("update"),
         MANAGE_ACCOUNTS("update"),
+        MERGE_ACCOUNTS("update"),
         MANAGE_USERS("update"),
         MANAGE_GROUPS("update"),
         PROPAGATION_TASKS("read"),
diff --git a/client/console/src/main/java/org/apache/syncope/client/console/wizards/AjaxWizard.java b/client/console/src/main/java/org/apache/syncope/client/console/wizards/AjaxWizard.java
index b1e71ce..7485492 100644
--- a/client/console/src/main/java/org/apache/syncope/client/console/wizards/AjaxWizard.java
+++ b/client/console/src/main/java/org/apache/syncope/client/console/wizards/AjaxWizard.java
@@ -81,6 +81,8 @@ public abstract class AjaxWizard<T extends Serializable> extends Wizard
 
     private final PageReference pageRef;
 
+    private AjaxWizardMgtButtonBar<T> buttonBar;
+
     /**
      * Construct.
      *
@@ -154,7 +156,12 @@ public abstract class AjaxWizard<T extends Serializable> extends Wizard
 
     @Override
     protected Component newButtonBar(final String id) {
-        return new AjaxWizardMgtButtonBar<>(id, this, mode);
+        this.buttonBar = new AjaxWizardMgtButtonBar<>(id, this, mode);
+        return this.buttonBar;
+    }
+
+    public AjaxWizardMgtButtonBar<T> getButtonBar() {
+        return buttonBar;
     }
 
     protected abstract void onCancelInternal();
diff --git a/client/console/src/main/java/org/apache/syncope/client/console/wizards/any/MergeLinkedAccountsWizardBuilder.java b/client/console/src/main/java/org/apache/syncope/client/console/wizards/any/MergeLinkedAccountsWizardBuilder.java
new file mode 100644
index 0000000..ed9a119
--- /dev/null
+++ b/client/console/src/main/java/org/apache/syncope/client/console/wizards/any/MergeLinkedAccountsWizardBuilder.java
@@ -0,0 +1,200 @@
+/*
+ * 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.client.console.wizards.any;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.syncope.client.console.SyncopeConsoleSession;
+import org.apache.syncope.client.console.commons.Constants;
+import org.apache.syncope.client.console.pages.BasePage;
+import org.apache.syncope.client.console.panels.MergeLinkedAccountsResourcesPanel;
+import org.apache.syncope.client.console.panels.MergeLinkedAccountsReviewPanel;
+import org.apache.syncope.client.console.panels.MergeLinkedAccountsSearchPanel;
+import org.apache.syncope.client.console.panels.UserDirectoryPanel;
+import org.apache.syncope.client.console.rest.ResourceRestClient;
+import org.apache.syncope.client.console.rest.UserRestClient;
+import org.apache.syncope.client.console.wicket.markup.html.bootstrap.dialog.BaseModal;
+import org.apache.syncope.client.console.wizards.AjaxWizard;
+import org.apache.syncope.client.console.wizards.AjaxWizardBuilder;
+import org.apache.syncope.client.lib.batch.BatchRequest;
+import org.apache.syncope.common.lib.patch.LinkedAccountPatch;
+import org.apache.syncope.common.lib.patch.UserPatch;
+import org.apache.syncope.common.lib.to.LinkedAccountTO;
+import org.apache.syncope.common.lib.to.UserTO;
+import org.apache.syncope.common.lib.types.PatchOperation;
+import org.apache.syncope.common.rest.api.Preference;
+import org.apache.syncope.common.rest.api.RESTHeaders;
+import org.apache.syncope.common.rest.api.batch.BatchRequestItem;
+import org.apache.wicket.PageReference;
+import org.apache.wicket.ajax.AjaxRequestTarget;
+import org.apache.wicket.event.IEvent;
+import org.apache.wicket.event.IEventSink;
+import org.apache.wicket.extensions.wizard.WizardModel;
+import org.apache.wicket.model.IModel;
+
+import javax.ws.rs.HttpMethod;
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.MediaType;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+
+public class MergeLinkedAccountsWizardBuilder extends AjaxWizardBuilder<UserTO> implements IEventSink {
+    private static final long serialVersionUID = -9142332740863374891L;
+
+    private static final ObjectMapper MAPPER = new ObjectMapper();
+
+    private final UserDirectoryPanel parentPanel;
+
+    private final BaseModal<?> modal;
+
+    private MergeLinkedAccountsWizardModel model;
+
+    public MergeLinkedAccountsWizardBuilder(final IModel<UserTO> model, final PageReference pageRef,
+                                            final UserDirectoryPanel parentPanel, final BaseModal<?> modal) {
+        super(model.getObject(), pageRef);
+        this.parentPanel = parentPanel;
+        this.modal = modal;
+    }
+
+    @Override
+    protected WizardModel buildModelSteps(final UserTO modelObject, final WizardModel wizardModel) {
+        this.model = new MergeLinkedAccountsWizardModel(modelObject);
+        wizardModel.add(new MergeLinkedAccountsSearchPanel(model, getPageReference()));
+        wizardModel.add(new MergeLinkedAccountsResourcesPanel(model, getPageReference()));
+        wizardModel.add(new MergeLinkedAccountsReviewPanel(model, getPageReference()));
+        return wizardModel;
+    }
+
+    @Override
+    public void onEvent(final IEvent<?> event) {
+        if (event.getPayload() instanceof AjaxWizard.NewItemCancelEvent) {
+            ((AjaxWizard.NewItemCancelEvent<?>) event.getPayload()).getTarget().ifPresent(modal::close);
+        }
+        if (event.getPayload() instanceof AjaxWizard.NewItemFinishEvent) {
+            Optional<AjaxRequestTarget> targetResult = ((AjaxWizard.NewItemFinishEvent<?>)
+                event.getPayload()).getTarget();
+            try {
+                mergeAccounts();
+                this.parentPanel.info(this.parentPanel.getString(Constants.OPERATION_SUCCEEDED));
+                targetResult.ifPresent(target -> {
+                    ((BasePage) this.parentPanel.getPage()).getNotificationPanel().refresh(target);
+                    parentPanel.updateResultTable(target);
+                    modal.close(target);
+                });
+            }  catch (Exception e) {
+                this.parentPanel.error(this.parentPanel.getString(Constants.ERROR) + ": " + e.getMessage());
+                targetResult.ifPresent(target -> ((BasePage) pageRef.getPage()).getNotificationPanel().refresh(target));
+            }
+        }
+    }
+
+    private void mergeAccounts() throws Exception {
+        final UserTO mergingUserTO = this.model.getMergingUser();
+        final ResourceRestClient resourceRestClient = new ResourceRestClient();
+
+        UserPatch userPatch = new UserPatch();
+        userPatch.setKey(this.model.getBaseUser().getUsername());
+
+        // Move linked accounts into the target/base user as linked accounts
+        mergingUserTO.getLinkedAccounts().forEach(acct -> {
+            LinkedAccountTO linkedAccount =
+                new LinkedAccountTO.Builder(acct.getResource(), acct.getConnObjectKeyValue())
+                    .password(acct.getPassword())
+                    .suspended(acct.isSuspended())
+                    .username(acct.getUsername())
+                    .build();
+            linkedAccount.getPlainAttrs().addAll(acct.getPlainAttrs());
+            linkedAccount.getPrivileges().addAll(acct.getPrivileges());
+            LinkedAccountPatch patch = new LinkedAccountPatch.Builder()
+                .linkedAccountTO(linkedAccount)
+                .operation(PatchOperation.ADD_REPLACE)
+                .build();
+            userPatch.getLinkedAccounts().add(patch);
+        });
+
+        // Move merging user's resources into the target/base user as a linked account
+        mergingUserTO.getResources().forEach(resource -> {
+            String connObjectKeyValue = resourceRestClient.getConnObjectKeyValue(resource,
+                mergingUserTO.getType(), mergingUserTO.getKey());
+            LinkedAccountTO linkedAccount =
+                new LinkedAccountTO.Builder(resource, connObjectKeyValue)
+                    .build();
+            linkedAccount.getPlainAttrs().addAll(mergingUserTO.getPlainAttrs());
+            linkedAccount.getPrivileges().addAll(mergingUserTO.getPrivileges());
+            LinkedAccountPatch patch = new LinkedAccountPatch.Builder()
+                .linkedAccountTO(linkedAccount)
+                .operation(PatchOperation.ADD_REPLACE)
+                .build();
+            userPatch.getLinkedAccounts().add(patch);
+        });
+
+        // Move merging user into target/base user as a linked account
+        String connObjectKeyValue = resourceRestClient.getConnObjectKeyValue(
+            this.model.getResource().getKey(),
+            mergingUserTO.getType(), mergingUserTO.getKey());
+        LinkedAccountTO linkedAccount =
+            new LinkedAccountTO.Builder(this.model.getResource().getKey(), connObjectKeyValue)
+                .password(mergingUserTO.getPassword())
+                .suspended(mergingUserTO.isSuspended())
+                .username(mergingUserTO.getUsername())
+                .build();
+        linkedAccount.getPlainAttrs().addAll(mergingUserTO.getPlainAttrs());
+        linkedAccount.getPrivileges().addAll(mergingUserTO.getPrivileges());
+        LinkedAccountPatch patch = new LinkedAccountPatch.Builder().linkedAccountTO(linkedAccount)
+            .operation(PatchOperation.ADD_REPLACE)
+            .build();
+        userPatch.getLinkedAccounts().add(patch);
+
+        BatchRequest batchRequest = SyncopeConsoleSession.get().batch();
+
+        // Delete merging user
+        BatchRequestItem deleteRequest = new BatchRequestItem();
+        deleteRequest.setMethod(HttpMethod.DELETE);
+        deleteRequest.setRequestURI("/users/" + mergingUserTO.getKey());
+        deleteRequest.getHeaders().put(HttpHeaders.CONTENT_TYPE, Collections.singletonList(MediaType.APPLICATION_JSON));
+        batchRequest.getItems().add(deleteRequest);
+
+        // Update user with linked accounts
+        String updateUserPayload = MAPPER.writeValueAsString(userPatch);
+        BatchRequestItem updateUser = new BatchRequestItem();
+        updateUser.setMethod(HttpMethod.PATCH);
+        updateUser.setRequestURI("/users/" + this.model.getBaseUser().getUsername());
+        updateUser.setHeaders(new HashMap<>());
+        updateUser.getHeaders().put(RESTHeaders.PREFER,
+            Collections.singletonList(Preference.RETURN_NO_CONTENT.toString()));
+        updateUser.getHeaders().put(HttpHeaders.ACCEPT,
+            Collections.singletonList(MediaType.APPLICATION_JSON));
+        updateUser.getHeaders().put(HttpHeaders.CONTENT_TYPE,
+            Collections.singletonList(MediaType.APPLICATION_JSON));
+        updateUser.getHeaders().put(HttpHeaders.CONTENT_LENGTH,
+            Collections.singletonList(updateUserPayload.length()));
+        updateUser.setContent(updateUserPayload);
+        batchRequest.getItems().add(updateUser);
+
+        Map<String, String> batchResponse = new UserRestClient().batch(batchRequest);
+        batchResponse.forEach((key, value) -> {
+            if (!value.equalsIgnoreCase("success")) {
+                throw new IllegalArgumentException("Unable to report a success operation status for " + key);
+            }
+        });
+    }
+
+}
diff --git a/client/console/src/main/java/org/apache/syncope/client/console/wizards/any/MergeLinkedAccountsWizardModel.java b/client/console/src/main/java/org/apache/syncope/client/console/wizards/any/MergeLinkedAccountsWizardModel.java
new file mode 100644
index 0000000..46c80cc
--- /dev/null
+++ b/client/console/src/main/java/org/apache/syncope/client/console/wizards/any/MergeLinkedAccountsWizardModel.java
@@ -0,0 +1,58 @@
+/*
+ * 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.client.console.wizards.any;
+
+import org.apache.syncope.common.lib.to.ResourceTO;
+import org.apache.syncope.common.lib.to.UserTO;
+
+import java.io.Serializable;
+
+public class MergeLinkedAccountsWizardModel implements Serializable {
+    private static final long serialVersionUID = -2420343164344634869L;
+
+    private final UserTO baseUser;
+
+    private UserTO mergingUser;
+
+    private ResourceTO resource;
+
+    public MergeLinkedAccountsWizardModel(final UserTO baseUser) {
+        this.baseUser = baseUser;
+    }
+
+    public ResourceTO getResource() {
+        return resource;
+    }
+
+    public UserTO getBaseUser() {
+        return baseUser;
+    }
+
+    public UserTO getMergingUser() {
+        return mergingUser;
+    }
+
+    public void setMergingUser(final UserTO mergingUser) {
+        this.mergingUser = mergingUser;
+    }
+
+    public void setResource(final ResourceTO resource) {
+        this.resource = resource;
+    }
+}
diff --git a/client/console/src/main/resources/org/apache/syncope/client/console/panels/MergeLinkedAccountsResourcesPanel.html b/client/console/src/main/resources/org/apache/syncope/client/console/panels/MergeLinkedAccountsResourcesPanel.html
new file mode 100644
index 0000000..c8f3ec9
--- /dev/null
+++ b/client/console/src/main/resources/org/apache/syncope/client/console/panels/MergeLinkedAccountsResourcesPanel.html
@@ -0,0 +1,24 @@
+<!--
+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.
+-->
+<html xmlns="http://www.w3.org/1999/xhtml" xmlns:wicket="http://wicket.apache.org">
+<wicket:panel>
+    <div wicket:id="resources">
+    </div>
+</wicket:panel>
+</html>
diff --git a/client/console/src/main/resources/org/apache/syncope/client/console/panels/UserDirectoryPanel_fr_CA.properties b/client/console/src/main/resources/org/apache/syncope/client/console/panels/MergeLinkedAccountsResourcesPanel.properties
similarity index 76%
copy from client/console/src/main/resources/org/apache/syncope/client/console/panels/UserDirectoryPanel_fr_CA.properties
copy to client/console/src/main/resources/org/apache/syncope/client/console/panels/MergeLinkedAccountsResourcesPanel.properties
index 0611fa8..ecc7e94 100644
--- a/client/console/src/main/resources/org/apache/syncope/client/console/panels/UserDirectoryPanel_fr_CA.properties
+++ b/client/console/src/main/resources/org/apache/syncope/client/console/panels/MergeLinkedAccountsResourcesPanel.properties
@@ -14,7 +14,6 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
-any.edit=Modifier ${anyTO.type} ${anyTO.username}
-any.propagation.tasks=T�ches de propagation pour ${type} ${username}
-any.notification.tasks=T�ches de notification pour ${type} ${username}
-linkedAccounts.title=G�rer les comptes utilisateur
+resource=Resource
+mergeLinkedAccounts.searchResource.title=Select a resource to use for ${username}, merged as a linked account.
+mergeLinkedAccounts.searchResource.summary=The selected resource must have USER provisioning rules defined.
diff --git a/client/console/src/main/resources/org/apache/syncope/client/console/panels/UserDirectoryPanel_fr_CA.properties b/client/console/src/main/resources/org/apache/syncope/client/console/panels/MergeLinkedAccountsResourcesPanel_fr_CA.properties
similarity index 76%
copy from client/console/src/main/resources/org/apache/syncope/client/console/panels/UserDirectoryPanel_fr_CA.properties
copy to client/console/src/main/resources/org/apache/syncope/client/console/panels/MergeLinkedAccountsResourcesPanel_fr_CA.properties
index 0611fa8..ecc7e94 100644
--- a/client/console/src/main/resources/org/apache/syncope/client/console/panels/UserDirectoryPanel_fr_CA.properties
+++ b/client/console/src/main/resources/org/apache/syncope/client/console/panels/MergeLinkedAccountsResourcesPanel_fr_CA.properties
@@ -14,7 +14,6 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
-any.edit=Modifier ${anyTO.type} ${anyTO.username}
-any.propagation.tasks=T�ches de propagation pour ${type} ${username}
-any.notification.tasks=T�ches de notification pour ${type} ${username}
-linkedAccounts.title=G�rer les comptes utilisateur
+resource=Resource
+mergeLinkedAccounts.searchResource.title=Select a resource to use for ${username}, merged as a linked account.
+mergeLinkedAccounts.searchResource.summary=The selected resource must have USER provisioning rules defined.
diff --git a/client/console/src/main/resources/org/apache/syncope/client/console/panels/UserDirectoryPanel_fr_CA.properties b/client/console/src/main/resources/org/apache/syncope/client/console/panels/MergeLinkedAccountsResourcesPanel_it.properties
similarity index 76%
copy from client/console/src/main/resources/org/apache/syncope/client/console/panels/UserDirectoryPanel_fr_CA.properties
copy to client/console/src/main/resources/org/apache/syncope/client/console/panels/MergeLinkedAccountsResourcesPanel_it.properties
index 0611fa8..ecc7e94 100644
--- a/client/console/src/main/resources/org/apache/syncope/client/console/panels/UserDirectoryPanel_fr_CA.properties
+++ b/client/console/src/main/resources/org/apache/syncope/client/console/panels/MergeLinkedAccountsResourcesPanel_it.properties
@@ -14,7 +14,6 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
-any.edit=Modifier ${anyTO.type} ${anyTO.username}
-any.propagation.tasks=T�ches de propagation pour ${type} ${username}
-any.notification.tasks=T�ches de notification pour ${type} ${username}
-linkedAccounts.title=G�rer les comptes utilisateur
+resource=Resource
+mergeLinkedAccounts.searchResource.title=Select a resource to use for ${username}, merged as a linked account.
+mergeLinkedAccounts.searchResource.summary=The selected resource must have USER provisioning rules defined.
diff --git a/client/console/src/main/resources/org/apache/syncope/client/console/panels/UserDirectoryPanel_fr_CA.properties b/client/console/src/main/resources/org/apache/syncope/client/console/panels/MergeLinkedAccountsResourcesPanel_ja.properties
similarity index 76%
copy from client/console/src/main/resources/org/apache/syncope/client/console/panels/UserDirectoryPanel_fr_CA.properties
copy to client/console/src/main/resources/org/apache/syncope/client/console/panels/MergeLinkedAccountsResourcesPanel_ja.properties
index 0611fa8..ecc7e94 100644
--- a/client/console/src/main/resources/org/apache/syncope/client/console/panels/UserDirectoryPanel_fr_CA.properties
+++ b/client/console/src/main/resources/org/apache/syncope/client/console/panels/MergeLinkedAccountsResourcesPanel_ja.properties
@@ -14,7 +14,6 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
-any.edit=Modifier ${anyTO.type} ${anyTO.username}
-any.propagation.tasks=T�ches de propagation pour ${type} ${username}
-any.notification.tasks=T�ches de notification pour ${type} ${username}
-linkedAccounts.title=G�rer les comptes utilisateur
+resource=Resource
+mergeLinkedAccounts.searchResource.title=Select a resource to use for ${username}, merged as a linked account.
+mergeLinkedAccounts.searchResource.summary=The selected resource must have USER provisioning rules defined.
diff --git a/client/console/src/main/resources/org/apache/syncope/client/console/panels/UserDirectoryPanel_fr_CA.properties b/client/console/src/main/resources/org/apache/syncope/client/console/panels/MergeLinkedAccountsResourcesPanel_pt_BR.properties
similarity index 76%
copy from client/console/src/main/resources/org/apache/syncope/client/console/panels/UserDirectoryPanel_fr_CA.properties
copy to client/console/src/main/resources/org/apache/syncope/client/console/panels/MergeLinkedAccountsResourcesPanel_pt_BR.properties
index 0611fa8..ecc7e94 100644
--- a/client/console/src/main/resources/org/apache/syncope/client/console/panels/UserDirectoryPanel_fr_CA.properties
+++ b/client/console/src/main/resources/org/apache/syncope/client/console/panels/MergeLinkedAccountsResourcesPanel_pt_BR.properties
@@ -14,7 +14,6 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
-any.edit=Modifier ${anyTO.type} ${anyTO.username}
-any.propagation.tasks=T�ches de propagation pour ${type} ${username}
-any.notification.tasks=T�ches de notification pour ${type} ${username}
-linkedAccounts.title=G�rer les comptes utilisateur
+resource=Resource
+mergeLinkedAccounts.searchResource.title=Select a resource to use for ${username}, merged as a linked account.
+mergeLinkedAccounts.searchResource.summary=The selected resource must have USER provisioning rules defined.
diff --git a/client/console/src/main/resources/org/apache/syncope/client/console/panels/UserDirectoryPanel_fr_CA.properties b/client/console/src/main/resources/org/apache/syncope/client/console/panels/MergeLinkedAccountsResourcesPanel_ru.properties
similarity index 76%
copy from client/console/src/main/resources/org/apache/syncope/client/console/panels/UserDirectoryPanel_fr_CA.properties
copy to client/console/src/main/resources/org/apache/syncope/client/console/panels/MergeLinkedAccountsResourcesPanel_ru.properties
index 0611fa8..ecc7e94 100644
--- a/client/console/src/main/resources/org/apache/syncope/client/console/panels/UserDirectoryPanel_fr_CA.properties
+++ b/client/console/src/main/resources/org/apache/syncope/client/console/panels/MergeLinkedAccountsResourcesPanel_ru.properties
@@ -14,7 +14,6 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
-any.edit=Modifier ${anyTO.type} ${anyTO.username}
-any.propagation.tasks=T�ches de propagation pour ${type} ${username}
-any.notification.tasks=T�ches de notification pour ${type} ${username}
-linkedAccounts.title=G�rer les comptes utilisateur
+resource=Resource
+mergeLinkedAccounts.searchResource.title=Select a resource to use for ${username}, merged as a linked account.
+mergeLinkedAccounts.searchResource.summary=The selected resource must have USER provisioning rules defined.
diff --git a/client/console/src/main/resources/org/apache/syncope/client/console/panels/MergeLinkedAccountsReviewPanel.html b/client/console/src/main/resources/org/apache/syncope/client/console/panels/MergeLinkedAccountsReviewPanel.html
new file mode 100644
index 0000000..7927485
--- /dev/null
+++ b/client/console/src/main/resources/org/apache/syncope/client/console/panels/MergeLinkedAccountsReviewPanel.html
@@ -0,0 +1,24 @@
+<!--
+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.
+-->
+<html xmlns="http://www.w3.org/1999/xhtml" xmlns:wicket="http://wicket.apache.org">
+<wicket:panel>
+    <div wicket:id="linkedAccounts">
+    </div>
+</wicket:panel>
+</html>
diff --git a/client/console/src/main/resources/org/apache/syncope/client/console/panels/UserDirectoryPanel_fr_CA.properties b/client/console/src/main/resources/org/apache/syncope/client/console/panels/MergeLinkedAccountsReviewPanel.properties
similarity index 76%
copy from client/console/src/main/resources/org/apache/syncope/client/console/panels/UserDirectoryPanel_fr_CA.properties
copy to client/console/src/main/resources/org/apache/syncope/client/console/panels/MergeLinkedAccountsReviewPanel.properties
index 0611fa8..3fd6639 100644
--- a/client/console/src/main/resources/org/apache/syncope/client/console/panels/UserDirectoryPanel_fr_CA.properties
+++ b/client/console/src/main/resources/org/apache/syncope/client/console/panels/MergeLinkedAccountsReviewPanel.properties
@@ -14,7 +14,9 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
-any.edit=Modifier ${anyTO.type} ${anyTO.username}
-any.propagation.tasks=T�ches de propagation pour ${type} ${username}
-any.notification.tasks=T�ches de notification pour ${type} ${username}
-linkedAccounts.title=G�rer les comptes utilisateur
+mergeLinkedAccounts.reviewAccounts.title=Preview finalized linked accounts
+connObjectKeyValue=Connector Key
+resource=Resource
+key=Key
+username=Username
+suspended=Suspended
diff --git a/client/console/src/main/resources/org/apache/syncope/client/console/panels/UserDirectoryPanel_fr_CA.properties b/client/console/src/main/resources/org/apache/syncope/client/console/panels/MergeLinkedAccountsReviewPanel_fr_CA.properties
similarity index 76%
copy from client/console/src/main/resources/org/apache/syncope/client/console/panels/UserDirectoryPanel_fr_CA.properties
copy to client/console/src/main/resources/org/apache/syncope/client/console/panels/MergeLinkedAccountsReviewPanel_fr_CA.properties
index 0611fa8..c2d89fa 100644
--- a/client/console/src/main/resources/org/apache/syncope/client/console/panels/UserDirectoryPanel_fr_CA.properties
+++ b/client/console/src/main/resources/org/apache/syncope/client/console/panels/MergeLinkedAccountsReviewPanel_fr_CA.properties
@@ -14,7 +14,9 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
-any.edit=Modifier ${anyTO.type} ${anyTO.username}
-any.propagation.tasks=T�ches de propagation pour ${type} ${username}
-any.notification.tasks=T�ches de notification pour ${type} ${username}
-linkedAccounts.title=G�rer les comptes utilisateur
+mergeLinkedAccounts.reviewAccounts.title=Preview finalized linked accounts
+connObjectKeyValue=Connector
+resource=Resource
+key=Key
+username=Username
+suspended=Suspended
diff --git a/client/console/src/main/resources/org/apache/syncope/client/console/panels/UserDirectoryPanel_fr_CA.properties b/client/console/src/main/resources/org/apache/syncope/client/console/panels/MergeLinkedAccountsReviewPanel_it.properties
similarity index 76%
copy from client/console/src/main/resources/org/apache/syncope/client/console/panels/UserDirectoryPanel_fr_CA.properties
copy to client/console/src/main/resources/org/apache/syncope/client/console/panels/MergeLinkedAccountsReviewPanel_it.properties
index 0611fa8..c2d89fa 100644
--- a/client/console/src/main/resources/org/apache/syncope/client/console/panels/UserDirectoryPanel_fr_CA.properties
+++ b/client/console/src/main/resources/org/apache/syncope/client/console/panels/MergeLinkedAccountsReviewPanel_it.properties
@@ -14,7 +14,9 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
-any.edit=Modifier ${anyTO.type} ${anyTO.username}
-any.propagation.tasks=T�ches de propagation pour ${type} ${username}
-any.notification.tasks=T�ches de notification pour ${type} ${username}
-linkedAccounts.title=G�rer les comptes utilisateur
+mergeLinkedAccounts.reviewAccounts.title=Preview finalized linked accounts
+connObjectKeyValue=Connector
+resource=Resource
+key=Key
+username=Username
+suspended=Suspended
diff --git a/client/console/src/main/resources/org/apache/syncope/client/console/panels/UserDirectoryPanel_fr_CA.properties b/client/console/src/main/resources/org/apache/syncope/client/console/panels/MergeLinkedAccountsReviewPanel_ja.properties
similarity index 76%
copy from client/console/src/main/resources/org/apache/syncope/client/console/panels/UserDirectoryPanel_fr_CA.properties
copy to client/console/src/main/resources/org/apache/syncope/client/console/panels/MergeLinkedAccountsReviewPanel_ja.properties
index 0611fa8..c2d89fa 100644
--- a/client/console/src/main/resources/org/apache/syncope/client/console/panels/UserDirectoryPanel_fr_CA.properties
+++ b/client/console/src/main/resources/org/apache/syncope/client/console/panels/MergeLinkedAccountsReviewPanel_ja.properties
@@ -14,7 +14,9 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
-any.edit=Modifier ${anyTO.type} ${anyTO.username}
-any.propagation.tasks=T�ches de propagation pour ${type} ${username}
-any.notification.tasks=T�ches de notification pour ${type} ${username}
-linkedAccounts.title=G�rer les comptes utilisateur
+mergeLinkedAccounts.reviewAccounts.title=Preview finalized linked accounts
+connObjectKeyValue=Connector
+resource=Resource
+key=Key
+username=Username
+suspended=Suspended
diff --git a/client/console/src/main/resources/org/apache/syncope/client/console/panels/UserDirectoryPanel_fr_CA.properties b/client/console/src/main/resources/org/apache/syncope/client/console/panels/MergeLinkedAccountsReviewPanel_pt_BR.properties
similarity index 76%
copy from client/console/src/main/resources/org/apache/syncope/client/console/panels/UserDirectoryPanel_fr_CA.properties
copy to client/console/src/main/resources/org/apache/syncope/client/console/panels/MergeLinkedAccountsReviewPanel_pt_BR.properties
index 0611fa8..c2d89fa 100644
--- a/client/console/src/main/resources/org/apache/syncope/client/console/panels/UserDirectoryPanel_fr_CA.properties
+++ b/client/console/src/main/resources/org/apache/syncope/client/console/panels/MergeLinkedAccountsReviewPanel_pt_BR.properties
@@ -14,7 +14,9 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
-any.edit=Modifier ${anyTO.type} ${anyTO.username}
-any.propagation.tasks=T�ches de propagation pour ${type} ${username}
-any.notification.tasks=T�ches de notification pour ${type} ${username}
-linkedAccounts.title=G�rer les comptes utilisateur
+mergeLinkedAccounts.reviewAccounts.title=Preview finalized linked accounts
+connObjectKeyValue=Connector
+resource=Resource
+key=Key
+username=Username
+suspended=Suspended
diff --git a/client/console/src/main/resources/org/apache/syncope/client/console/panels/UserDirectoryPanel_fr_CA.properties b/client/console/src/main/resources/org/apache/syncope/client/console/panels/MergeLinkedAccountsReviewPanel_ru.properties
similarity index 76%
copy from client/console/src/main/resources/org/apache/syncope/client/console/panels/UserDirectoryPanel_fr_CA.properties
copy to client/console/src/main/resources/org/apache/syncope/client/console/panels/MergeLinkedAccountsReviewPanel_ru.properties
index 0611fa8..c2d89fa 100644
--- a/client/console/src/main/resources/org/apache/syncope/client/console/panels/UserDirectoryPanel_fr_CA.properties
+++ b/client/console/src/main/resources/org/apache/syncope/client/console/panels/MergeLinkedAccountsReviewPanel_ru.properties
@@ -14,7 +14,9 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
-any.edit=Modifier ${anyTO.type} ${anyTO.username}
-any.propagation.tasks=T�ches de propagation pour ${type} ${username}
-any.notification.tasks=T�ches de notification pour ${type} ${username}
-linkedAccounts.title=G�rer les comptes utilisateur
+mergeLinkedAccounts.reviewAccounts.title=Preview finalized linked accounts
+connObjectKeyValue=Connector
+resource=Resource
+key=Key
+username=Username
+suspended=Suspended
diff --git a/client/console/src/main/resources/org/apache/syncope/client/console/panels/MergeLinkedAccountsSearchPanel.html b/client/console/src/main/resources/org/apache/syncope/client/console/panels/MergeLinkedAccountsSearchPanel.html
new file mode 100644
index 0000000..82edc13
--- /dev/null
+++ b/client/console/src/main/resources/org/apache/syncope/client/console/panels/MergeLinkedAccountsSearchPanel.html
@@ -0,0 +1,31 @@
+<!--
+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.
+-->
+<html xmlns="http://www.w3.org/1999/xhtml" xmlns:wicket="http://wicket.apache.org">
+<wicket:panel>
+    <div wicket:id="ownerContainer">
+        <span wicket:id="search">[SEARCH]</span>
+    </div>
+    <wicket:fragment wicket:id="userSearchFragment">
+        <span wicket:id="usersearch">[USER SEARCH]</span>
+        <div class="searchResult">
+            <span wicket:id="searchResult">[USER SEARCH RESULT]</span>
+        </div>
+    </wicket:fragment>
+</wicket:panel>
+</html>
diff --git a/client/console/src/main/resources/org/apache/syncope/client/console/panels/UserDirectoryPanel_fr_CA.properties b/client/console/src/main/resources/org/apache/syncope/client/console/panels/MergeLinkedAccountsSearchPanel.properties
similarity index 76%
copy from client/console/src/main/resources/org/apache/syncope/client/console/panels/UserDirectoryPanel_fr_CA.properties
copy to client/console/src/main/resources/org/apache/syncope/client/console/panels/MergeLinkedAccountsSearchPanel.properties
index 0611fa8..5d25d9c 100644
--- a/client/console/src/main/resources/org/apache/syncope/client/console/panels/UserDirectoryPanel_fr_CA.properties
+++ b/client/console/src/main/resources/org/apache/syncope/client/console/panels/MergeLinkedAccountsSearchPanel.properties
@@ -14,7 +14,4 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
-any.edit=Modifier ${anyTO.type} ${anyTO.username}
-any.propagation.tasks=T�ches de propagation pour ${type} ${username}
-any.notification.tasks=T�ches de notification pour ${type} ${username}
-linkedAccounts.title=G�rer les comptes utilisateur
+mergeLinkedAccounts.searchUser=Select user to merge
diff --git a/client/console/src/main/resources/org/apache/syncope/client/console/panels/UserDirectoryPanel_fr_CA.properties b/client/console/src/main/resources/org/apache/syncope/client/console/panels/MergeLinkedAccountsSearchPanel_fr_CA.properties
similarity index 76%
copy from client/console/src/main/resources/org/apache/syncope/client/console/panels/UserDirectoryPanel_fr_CA.properties
copy to client/console/src/main/resources/org/apache/syncope/client/console/panels/MergeLinkedAccountsSearchPanel_fr_CA.properties
index 0611fa8..5d25d9c 100644
--- a/client/console/src/main/resources/org/apache/syncope/client/console/panels/UserDirectoryPanel_fr_CA.properties
+++ b/client/console/src/main/resources/org/apache/syncope/client/console/panels/MergeLinkedAccountsSearchPanel_fr_CA.properties
@@ -14,7 +14,4 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
-any.edit=Modifier ${anyTO.type} ${anyTO.username}
-any.propagation.tasks=T�ches de propagation pour ${type} ${username}
-any.notification.tasks=T�ches de notification pour ${type} ${username}
-linkedAccounts.title=G�rer les comptes utilisateur
+mergeLinkedAccounts.searchUser=Select user to merge
diff --git a/client/console/src/main/resources/org/apache/syncope/client/console/panels/UserDirectoryPanel_fr_CA.properties b/client/console/src/main/resources/org/apache/syncope/client/console/panels/MergeLinkedAccountsSearchPanel_it.properties
similarity index 76%
copy from client/console/src/main/resources/org/apache/syncope/client/console/panels/UserDirectoryPanel_fr_CA.properties
copy to client/console/src/main/resources/org/apache/syncope/client/console/panels/MergeLinkedAccountsSearchPanel_it.properties
index 0611fa8..9ed65ca 100644
--- a/client/console/src/main/resources/org/apache/syncope/client/console/panels/UserDirectoryPanel_fr_CA.properties
+++ b/client/console/src/main/resources/org/apache/syncope/client/console/panels/MergeLinkedAccountsSearchPanel_it.properties
@@ -14,7 +14,4 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
-any.edit=Modifier ${anyTO.type} ${anyTO.username}
-any.propagation.tasks=T�ches de propagation pour ${type} ${username}
-any.notification.tasks=T�ches de notification pour ${type} ${username}
-linkedAccounts.title=G�rer les comptes utilisateur
+mergeLinkedAccounts.searchUser=Selezione dell'utente
diff --git a/client/console/src/main/resources/org/apache/syncope/client/console/panels/UserDirectoryPanel_fr_CA.properties b/client/console/src/main/resources/org/apache/syncope/client/console/panels/MergeLinkedAccountsSearchPanel_ja.properties
similarity index 76%
copy from client/console/src/main/resources/org/apache/syncope/client/console/panels/UserDirectoryPanel_fr_CA.properties
copy to client/console/src/main/resources/org/apache/syncope/client/console/panels/MergeLinkedAccountsSearchPanel_ja.properties
index 0611fa8..5d25d9c 100644
--- a/client/console/src/main/resources/org/apache/syncope/client/console/panels/UserDirectoryPanel_fr_CA.properties
+++ b/client/console/src/main/resources/org/apache/syncope/client/console/panels/MergeLinkedAccountsSearchPanel_ja.properties
@@ -14,7 +14,4 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
-any.edit=Modifier ${anyTO.type} ${anyTO.username}
-any.propagation.tasks=T�ches de propagation pour ${type} ${username}
-any.notification.tasks=T�ches de notification pour ${type} ${username}
-linkedAccounts.title=G�rer les comptes utilisateur
+mergeLinkedAccounts.searchUser=Select user to merge
diff --git a/client/console/src/main/resources/org/apache/syncope/client/console/panels/UserDirectoryPanel_fr_CA.properties b/client/console/src/main/resources/org/apache/syncope/client/console/panels/MergeLinkedAccountsSearchPanel_pt_BR.properties
similarity index 76%
copy from client/console/src/main/resources/org/apache/syncope/client/console/panels/UserDirectoryPanel_fr_CA.properties
copy to client/console/src/main/resources/org/apache/syncope/client/console/panels/MergeLinkedAccountsSearchPanel_pt_BR.properties
index 0611fa8..5d25d9c 100644
--- a/client/console/src/main/resources/org/apache/syncope/client/console/panels/UserDirectoryPanel_fr_CA.properties
+++ b/client/console/src/main/resources/org/apache/syncope/client/console/panels/MergeLinkedAccountsSearchPanel_pt_BR.properties
@@ -14,7 +14,4 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
-any.edit=Modifier ${anyTO.type} ${anyTO.username}
-any.propagation.tasks=T�ches de propagation pour ${type} ${username}
-any.notification.tasks=T�ches de notification pour ${type} ${username}
-linkedAccounts.title=G�rer les comptes utilisateur
+mergeLinkedAccounts.searchUser=Select user to merge
diff --git a/client/console/src/main/resources/org/apache/syncope/client/console/panels/UserDirectoryPanel_fr_CA.properties b/client/console/src/main/resources/org/apache/syncope/client/console/panels/MergeLinkedAccountsSearchPanel_ru.properties
similarity index 76%
copy from client/console/src/main/resources/org/apache/syncope/client/console/panels/UserDirectoryPanel_fr_CA.properties
copy to client/console/src/main/resources/org/apache/syncope/client/console/panels/MergeLinkedAccountsSearchPanel_ru.properties
index 0611fa8..5d25d9c 100644
--- a/client/console/src/main/resources/org/apache/syncope/client/console/panels/UserDirectoryPanel_fr_CA.properties
+++ b/client/console/src/main/resources/org/apache/syncope/client/console/panels/MergeLinkedAccountsSearchPanel_ru.properties
@@ -14,7 +14,4 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
-any.edit=Modifier ${anyTO.type} ${anyTO.username}
-any.propagation.tasks=T�ches de propagation pour ${type} ${username}
-any.notification.tasks=T�ches de notification pour ${type} ${username}
-linkedAccounts.title=G�rer les comptes utilisateur
+mergeLinkedAccounts.searchUser=Select user to merge
diff --git a/client/console/src/main/resources/org/apache/syncope/client/console/panels/UserDirectoryPanel.properties b/client/console/src/main/resources/org/apache/syncope/client/console/panels/UserDirectoryPanel.properties
index 1d4db0d..9cbfc62 100644
--- a/client/console/src/main/resources/org/apache/syncope/client/console/panels/UserDirectoryPanel.properties
+++ b/client/console/src/main/resources/org/apache/syncope/client/console/panels/UserDirectoryPanel.properties
@@ -18,4 +18,5 @@ any.edit=Edit ${anyTO.type} ${anyTO.username}
 any.propagation.tasks=Propagation tasks for ${type} ${username}
 any.notification.tasks=Notification tasks for ${type} ${username}
 linkedAccounts.title=Manage user accounts
+mergeLinkedAccounts.title=Merge user accounts with ${username}
 auditHistory.title=${anyTO.type} ${anyTO.username} history
diff --git a/client/console/src/main/resources/org/apache/syncope/client/console/panels/UserDirectoryPanel_fr_CA.properties b/client/console/src/main/resources/org/apache/syncope/client/console/panels/UserDirectoryPanel_fr_CA.properties
index 0611fa8..60a2dfe 100644
--- a/client/console/src/main/resources/org/apache/syncope/client/console/panels/UserDirectoryPanel_fr_CA.properties
+++ b/client/console/src/main/resources/org/apache/syncope/client/console/panels/UserDirectoryPanel_fr_CA.properties
@@ -15,6 +15,7 @@
 # specific language governing permissions and limitations
 # under the License.
 any.edit=Modifier ${anyTO.type} ${anyTO.username}
-any.propagation.tasks=T�ches de propagation pour ${type} ${username}
-any.notification.tasks=T�ches de notification pour ${type} ${username}
-linkedAccounts.title=G�rer les comptes utilisateur
+any.propagation.tasks=Tâches de propagation pour ${type} ${username}
+any.notification.tasks=Tâches de notification pour ${type} ${username}
+linkedAccounts.title=Gérer les comptes utilisateur
+mergeLinkedAccounts.title=Merge user accounts with ${username}
diff --git a/client/console/src/main/resources/org/apache/syncope/client/console/panels/UserDirectoryPanel_it.properties b/client/console/src/main/resources/org/apache/syncope/client/console/panels/UserDirectoryPanel_it.properties
index ef6c91a..aedc995 100644
--- a/client/console/src/main/resources/org/apache/syncope/client/console/panels/UserDirectoryPanel_it.properties
+++ b/client/console/src/main/resources/org/apache/syncope/client/console/panels/UserDirectoryPanel_it.properties
@@ -18,4 +18,5 @@ any.edit=Modifica ${anyTO.type} ${anyTO.username}
 any.propagation.tasks=Task di propagazione per ${type} ${username}
 any.notification.tasks=Task di notifica per ${type} ${username}
 linkedAccounts.title=Gestisci account utente
+mergeLinkedAccounts.title=Combinare account utente con ${username}
 auditHistory.title=${anyTO.type} ${anyTO.username} history
diff --git a/client/console/src/main/resources/org/apache/syncope/client/console/panels/UserDirectoryPanel_ja.properties b/client/console/src/main/resources/org/apache/syncope/client/console/panels/UserDirectoryPanel_ja.properties
index a2193e2..a4745d4 100644
--- a/client/console/src/main/resources/org/apache/syncope/client/console/panels/UserDirectoryPanel_ja.properties
+++ b/client/console/src/main/resources/org/apache/syncope/client/console/panels/UserDirectoryPanel_ja.properties
@@ -19,3 +19,4 @@ any.propagation.tasks=${type} ${username} \u306e\u4f1d\u64ad\u30bf\u30b9\u30af
 any.notification.tasks=${type} ${username} \u306e\u901a\u77e5\u30bf\u30b9\u30af
 linkedAccounts.title=\u30e6\u30fc\u30b6\u30fc\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u7ba1\u7406\u3059\u308b
 auditHistory.title=${anyTO.type} ${anyTO.username} history
+mergeLinkedAccounts.title=Merge user accounts with ${username}
diff --git a/client/console/src/main/resources/org/apache/syncope/client/console/panels/UserDirectoryPanel_pt_BR.properties b/client/console/src/main/resources/org/apache/syncope/client/console/panels/UserDirectoryPanel_pt_BR.properties
index 84d5d86..230d7b1 100644
--- a/client/console/src/main/resources/org/apache/syncope/client/console/panels/UserDirectoryPanel_pt_BR.properties
+++ b/client/console/src/main/resources/org/apache/syncope/client/console/panels/UserDirectoryPanel_pt_BR.properties
@@ -19,3 +19,4 @@ any.propagation.tasks=Propagation tasks for ${type} ${username}
 any.notification.tasks=Notification tasks for ${type} ${username}
 linkedAccounts.title=Gerenciar contas de usu\u00e1rio
 auditHistory.title=${anyTO.type} ${anyTO.username} history
+mergeLinkedAccounts.title=Merge user accounts with ${username}
diff --git a/client/console/src/main/resources/org/apache/syncope/client/console/panels/UserDirectoryPanel_ru.properties b/client/console/src/main/resources/org/apache/syncope/client/console/panels/UserDirectoryPanel_ru.properties
index 378c49a..b3b1b1d 100644
--- a/client/console/src/main/resources/org/apache/syncope/client/console/panels/UserDirectoryPanel_ru.properties
+++ b/client/console/src/main/resources/org/apache/syncope/client/console/panels/UserDirectoryPanel_ru.properties
@@ -20,3 +20,4 @@ any.propagation.tasks=\u0417\u0430\u0434\u0430\u0447\u0438 \u0432\u044b\u043f\u0
 any.notification.tasks=\u0417\u0430\u0434\u0430\u0447\u0438 \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0443\u0432\u0435\u0434\u043e\u043c\u043b\u0435\u043d\u0438\u0439 \u0434\u043b\u044f ${type} ${username}
 linkedAccounts.title=\u0423\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u043c\u0438 \u0437\u0430\u043f\u0438\u0441\u044f\u043c\u0438 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0435\u0439
 auditHistory.title=${anyTO.type} ${anyTO.username} history
+mergeLinkedAccounts.title=Merge user accounts with ${username}
diff --git a/client/console/src/main/resources/org/apache/syncope/client/console/wicket/markup/html/form/ActionsPanel.properties b/client/console/src/main/resources/org/apache/syncope/client/console/wicket/markup/html/form/ActionsPanel.properties
index af95e02..9806974 100644
--- a/client/console/src/main/resources/org/apache/syncope/client/console/wicket/markup/html/form/ActionsPanel.properties
+++ b/client/console/src/main/resources/org/apache/syncope/client/console/wicket/markup/html/form/ActionsPanel.properties
@@ -281,3 +281,7 @@ external_editor.title=external editor icon
 external_editor.class=fa fa-picture-o
 external_editor.alt=external editor icon
 
+merge_accounts.class=fa fa-compress
+merge_accounts.title=merge accounts
+merge_accounts.alt=merge accounts icon
+
diff --git a/common/rest-api/src/main/java/org/apache/syncope/common/rest/api/RESTHeaders.java b/common/rest-api/src/main/java/org/apache/syncope/common/rest/api/RESTHeaders.java
index 34be920..b5f67c6 100644
--- a/common/rest-api/src/main/java/org/apache/syncope/common/rest/api/RESTHeaders.java
+++ b/common/rest-api/src/main/java/org/apache/syncope/common/rest/api/RESTHeaders.java
@@ -35,6 +35,8 @@ public final class RESTHeaders {
 
     public static final String RESOURCE_KEY = "X-Syncope-Key";
 
+    public static final String CONNOBJECT_KEY = "X-Syncope-ConnObject-Key";
+
     /**
      * Asks for asynchronous propagation towards external resources with null priority.
      */
diff --git a/common/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/ResourceService.java b/common/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/ResourceService.java
index cca2ac0..54e9df9 100644
--- a/common/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/ResourceService.java
+++ b/common/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/ResourceService.java
@@ -33,6 +33,7 @@ import javax.ws.rs.BeanParam;
 import javax.ws.rs.Consumes;
 import javax.ws.rs.DELETE;
 import javax.ws.rs.GET;
+import javax.ws.rs.OPTIONS;
 import javax.ws.rs.POST;
 import javax.ws.rs.PUT;
 import javax.ws.rs.Path;
@@ -58,6 +59,30 @@ import org.apache.syncope.common.rest.api.beans.ConnObjectTOQuery;
 public interface ResourceService extends JAXRSService {
 
     /**
+     * Returns the calculated connObjectKey value for the given type and key.
+     *
+     * @param key name of resource to read connector object from
+     * @param anyTypeKey any object type
+     * @param anyKey user, group or any object key
+     * @return connObjectKey value for the external resource, for the given type and key
+     */
+    @ApiResponses({
+        @ApiResponse(responseCode = "201",
+                description = "connObjectKey value for the external resource, for the given type and key", headers = {
+                    @Header(name = RESTHeaders.CONNOBJECT_KEY, schema =
+                            @Schema(type = "string"),
+                            description = "connObjectKey value for the external resource, for the given type and key")
+                }),
+        @ApiResponse(responseCode = "404",
+                description = "user, group or any object not found, or connObjectKey cannot be calculated") })
+    @OPTIONS
+    @Path("{key}/{anyTypeKey}/{anyKey}")
+    Response getConnObjectKeyValue(
+            @NotNull @PathParam("key") String key,
+            @NotNull @PathParam("anyTypeKey") String anyTypeKey,
+            @NotNull @PathParam("anyKey") String anyKey);
+
+    /**
      * Returns connector object from the external resource, for the given type and key.
      *
      * @param key name of resource to read connector object from
diff --git a/core/logic/src/main/java/org/apache/syncope/core/logic/ResourceLogic.java b/core/logic/src/main/java/org/apache/syncope/core/logic/ResourceLogic.java
index b888641..ba0946c 100644
--- a/core/logic/src/main/java/org/apache/syncope/core/logic/ResourceLogic.java
+++ b/core/logic/src/main/java/org/apache/syncope/core/logic/ResourceLogic.java
@@ -53,6 +53,7 @@ import org.apache.syncope.core.persistence.api.entity.AnyUtilsFactory;
 import org.apache.syncope.core.persistence.api.entity.ConnInstance;
 import org.apache.syncope.core.persistence.api.entity.VirSchema;
 import org.apache.syncope.core.persistence.api.entity.resource.Provision;
+import org.apache.syncope.core.provisioning.api.MappingManager;
 import org.apache.syncope.core.provisioning.api.VirAttrHandler;
 import org.apache.syncope.core.provisioning.api.data.ConnInstanceDataBinder;
 import org.apache.syncope.core.provisioning.api.utils.RealmUtils;
@@ -99,6 +100,9 @@ public class ResourceLogic extends AbstractTransactionalLogic<ResourceTO> {
     private OutboundMatcher outboundMatcher;
 
     @Autowired
+    private MappingManager mappingManager;
+
+    @Autowired
     private ConnectorFactory connFactory;
 
     @Autowired
@@ -281,6 +285,26 @@ public class ResourceLogic extends AbstractTransactionalLogic<ResourceTO> {
                 "Provision on resource '" + resourceKey + "' for type '" + anyTypeKey + "'"));
     }
 
+    @Transactional(readOnly = true)
+    public String getConnObjectKeyValue(
+            final String key,
+            final String anyTypeKey,
+            final String anyKey) {
+
+        Provision provision = getProvision(key, anyTypeKey);
+
+        // 1. find any
+        Any<?> any = anyUtilsFactory.getInstance(provision.getAnyType().getKind()).dao().authFind(anyKey);
+        if (any == null) {
+            throw new NotFoundException(provision.getAnyType() + " " + anyKey);
+        }
+
+        // 2.get ConnObjectKey value
+        return mappingManager.getConnObjectKeyValue(any, provision).
+                orElseThrow(() -> new NotFoundException(
+                "No ConnObjectKey value found for " + anyTypeKey + " " + anyKey + " on resource '" + key + "'"));
+    }
+
     @PreAuthorize("hasRole('" + StandardEntitlement.RESOURCE_GET_CONNOBJECT + "')")
     @Transactional(readOnly = true)
     public ConnObjectTO readConnObjectByAnyKey(
diff --git a/core/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/ResourceServiceImpl.java b/core/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/ResourceServiceImpl.java
index beb377a..5197c82 100644
--- a/core/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/ResourceServiceImpl.java
+++ b/core/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/ResourceServiceImpl.java
@@ -93,6 +93,12 @@ public class ResourceServiceImpl extends AbstractServiceImpl implements Resource
     }
 
     @Override
+    public Response getConnObjectKeyValue(final String key, final String anyTypeKey, final String anyKey) {
+        String connObjectKeyValue = logic.getConnObjectKeyValue(key, anyTypeKey, anyKey);
+        return Response.noContent().header(RESTHeaders.CONNOBJECT_KEY, connObjectKeyValue).build();
+    }
+
+    @Override
     public ConnObjectTO readConnObject(final String key, final String anyTypeKey, final String value) {
         return SyncopeConstants.UUID_PATTERN.matcher(value).matches()
                 ? logic.readConnObjectByAnyKey(key, anyTypeKey, value)
diff --git a/fit/core-reference/src/test/java/org/apache/syncope/fit/console/LinkedAccountsITCase.java b/fit/core-reference/src/test/java/org/apache/syncope/fit/console/LinkedAccountsITCase.java
new file mode 100644
index 0000000..c0ca592
--- /dev/null
+++ b/fit/core-reference/src/test/java/org/apache/syncope/fit/console/LinkedAccountsITCase.java
@@ -0,0 +1,197 @@
+/*
+ * 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.fit.console;
+
+import org.apache.commons.lang3.RandomStringUtils;
+import org.apache.syncope.client.console.SyncopeConsoleSession;
+import org.apache.syncope.client.console.commons.Constants;
+import org.apache.syncope.client.console.pages.Login;
+import org.apache.syncope.client.console.panels.search.SearchClause;
+import org.apache.syncope.common.lib.SyncopeClientException;
+import org.apache.syncope.common.lib.to.AttrTO;
+import org.apache.syncope.common.lib.to.LinkedAccountTO;
+import org.apache.syncope.common.lib.to.ProvisioningResult;
+import org.apache.syncope.common.lib.to.UserTO;
+import org.apache.syncope.common.lib.types.ClientExceptionType;
+import org.apache.syncope.common.rest.api.service.UserService;
+import org.apache.syncope.fit.core.UserITCase;
+import org.apache.wicket.Component;
+import org.apache.wicket.ajax.AjaxEventBehavior;
+import org.apache.wicket.extensions.wizard.NextButton;
+import org.apache.wicket.markup.html.form.DropDownChoice;
+import org.apache.wicket.markup.html.form.TextField;
+import org.apache.wicket.markup.repeater.OddEvenItem;
+import org.apache.wicket.util.tester.FormTester;
+import org.apache.wicket.util.tester.WicketTesterHelper;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import javax.ws.rs.core.GenericType;
+import javax.ws.rs.core.Response;
+
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+public class LinkedAccountsITCase extends AbstractConsoleITCase {
+    private static final String TAB_PANEL = "body:content:body:container:content:tabbedPanel:panel:searchResult:";
+
+    private static final String RESULT_DATA_TABLE = "searchResult:container:content:searchContainer:resultTable:tablePanel:groupForm:checkgroup:dataTable:";
+
+    private static final String RESOURCES_DATA_TABLE = "view:resources:container:content:searchContainer:resultTable:tablePanel:groupForm:checkgroup:dataTable:";
+
+    private static final String SELECT_USER_ACTION = "searchResult:outerObjectsRepeater:1:outer:container:content:"
+        + "togglePanelContainer:container:actions:actions:actionRepeater:0:action:action";
+
+    private static final String SELECT_RESOURCE_ACTION = "view:resources:outerObjectsRepeater:1:outer:container:content:"
+        + "togglePanelContainer:container:actions:actions:actionRepeater:0:action:action";
+
+    private static final String PARENT_FORM = "outerObjectsRepeater:0:outer:form:";
+
+    private static final String FORM = PARENT_FORM + "content:form:";
+    
+    private static final String SEARCH_PANEL = FORM + "view:ownerContainer:search:";
+
+    private static final String USER_SEARCH_PANEL = SEARCH_PANEL + "usersearch:";
+
+    private static final String USER_SEARCH_FORM = TAB_PANEL + USER_SEARCH_PANEL + "searchFormContainer:search:multiValueContainer:innerForm:";
+
+    private static final String CONTAINER = TAB_PANEL + "container:content:";
+
+    private static UserTO user;
+
+    @BeforeEach
+    public void login() {
+        doLogin(ADMIN_UNAME, ADMIN_PWD);
+
+        // create user with linked account
+        String email = "linkedAccount" + RandomStringUtils.randomNumeric(4) + "@syncope.apache.org";
+        user = UserITCase.getSampleTO(email);
+        String connObjectKeyValue = "uid=" + user.getUsername() + ",ou=People,o=isp";
+
+        LinkedAccountTO account = new LinkedAccountTO.Builder("resource-ldap", connObjectKeyValue).build();
+        account.getPlainAttrs().add(new AttrTO.Builder().schema("surname").value("LINKED_SURNAME").build());
+        user.getLinkedAccounts().add(account);
+
+        UserService userService = SyncopeConsoleSession.get().getService(UserService.class);
+        Response response = userService.create(user, true);
+        user = response.readEntity(new GenericType<ProvisioningResult<UserTO>>() {
+        }).getEntity();
+        assertNotNull(user.getKey());
+        assertEquals(account.getConnObjectKeyValue(), user.getLinkedAccounts().get(0).getConnObjectKeyValue());
+    }
+
+    @AfterEach
+    public void cleanUp() {
+        try {
+            SyncopeConsoleSession.get().getService(UserService.class).delete(user.getKey());
+        } catch (final SyncopeClientException e) {
+            if (e.getType() != ClientExceptionType.NotFound) {
+                throw e;
+            }
+        }
+    }
+
+    @Test
+    public void createLinkedAccountAndMergeWithUser() {
+
+        // Locate and select first user
+        TESTER.clickLink("body:realmsLI:realms");
+        TESTER.clickLink("body:content:body:container:content:tabbedPanel:tabs-container:tabs:1:link");
+
+        Component verdiUserComponent = findComponentByProp("username", CONTAINER
+            + ":searchContainer:resultTable:tablePanel:groupForm:checkgroup:dataTable", "verdi");
+        assertNotNull(verdiUserComponent);
+        TESTER.executeAjaxEvent(verdiUserComponent.getPageRelativePath(), Constants.ON_CLICK);
+
+        // Click action menu to bring up merge window
+        TESTER.clickLink(TAB_PANEL + "outerObjectsRepeater:1:outer:container:content:togglePanelContainer:container:"
+            + "actions:actions:actionRepeater:8:action:action");
+
+
+        // Search for user
+        TESTER.executeAjaxEvent(USER_SEARCH_FORM + "content:panelPlus:add", Constants.ON_CLICK);
+        FormTester formTester = TESTER.newFormTester(USER_SEARCH_FORM);
+
+        DropDownChoice type = (DropDownChoice) TESTER.getComponentFromLastRenderedPage(USER_SEARCH_FORM + "content:view:0:panel:container:type:dropDownChoiceField");
+        TESTER.executeAjaxEvent(USER_SEARCH_FORM + "content:view:0:panel:container:type:dropDownChoiceField", Constants.ON_CHANGE);
+        type.setModelValue(new String[]{"ATTRIBUTE"});
+        type.setDefaultModelObject(SearchClause.Type.ATTRIBUTE);
+
+        TextField property = (TextField) TESTER.getComponentFromLastRenderedPage(USER_SEARCH_FORM + "content:view:0:panel:container:property:textField");
+        assertNotNull(property);
+        property.setModelValue(new String[]{"username"});
+        
+        TextField value = (TextField) TESTER.getComponentFromLastRenderedPage(USER_SEARCH_FORM + "content:view:0:panel:container:value:textField");
+        assertNotNull(value);
+        value.setModelValue(new String[]{user.getUsername()});
+        
+        TESTER.cleanupFeedbackMessages();
+        formTester.submit("content:view:0:panel:container:operatorContainer:operator:search");
+        TESTER.assertNoErrorMessage();
+
+        // Locate result in data table
+        Component comp = findComponentByProp("username", TAB_PANEL + SEARCH_PANEL + RESULT_DATA_TABLE, user.getUsername());
+        TESTER.executeAjaxEvent(comp.getPageRelativePath(), Constants.ON_CLICK);
+
+        UserTO userTO = (UserTO) ((OddEvenItem) TESTER.getComponentFromLastRenderedPage(TAB_PANEL + SEARCH_PANEL
+            + RESULT_DATA_TABLE + "body:rows:1")).getModel().getObject();
+        assertNotNull(userTO);
+
+        // Select user
+        TESTER.clickLink(TAB_PANEL + SEARCH_PANEL + SELECT_USER_ACTION);
+
+        // move onto the next panel
+        TESTER.getComponentFromLastRenderedPage(TAB_PANEL + FORM + "view").setEnabled(false);
+        formTester = TESTER.newFormTester(TAB_PANEL + FORM);
+        formTester.submit("buttons:next");
+        
+        // Select a resource
+        comp = findComponentByProp ("key", TAB_PANEL + FORM + RESOURCES_DATA_TABLE + "body:rows", "resource-ldap");
+        assertNotNull(comp);
+        TESTER.executeAjaxEvent(comp.getPageRelativePath(), Constants.ON_CLICK);
+        TESTER.clickLink(TAB_PANEL + FORM + SELECT_RESOURCE_ACTION);
+
+        // move onto the next panel
+        TESTER.getComponentFromLastRenderedPage(TAB_PANEL + FORM + "view").setEnabled(false);
+        formTester = TESTER.newFormTester(TAB_PANEL + FORM);
+        formTester.submit("buttons:next");
+        
+        // Finish merge
+        TESTER.getComponentFromLastRenderedPage(TAB_PANEL + FORM + "view").setEnabled(false);
+        formTester = TESTER.newFormTester(TAB_PANEL + FORM);
+        formTester.submit("buttons:finish");
+
+        UserService userService = SyncopeConsoleSession.get().getService(UserService.class);
+
+        // User must have been deleted after the merge
+        try {
+            userService.read(user.getKey());
+            fail("User must have been deleted; expect an exception here");
+        } catch (final SyncopeClientException e) {
+            if (e.getType() != ClientExceptionType.NotFound) {
+                fail(e.getMessage(), e);
+            }
+        }
+        // User must include merged accounts now
+        UserTO verdi = userService.read(UserTO.class.cast(verdiUserComponent.getDefaultModelObject()).getKey());
+        assertFalse(verdi.getLinkedAccounts().isEmpty());
+    }
+}
diff --git a/fit/core-reference/src/test/java/org/apache/syncope/fit/console/PoliciesITCase.java b/fit/core-reference/src/test/java/org/apache/syncope/fit/console/PoliciesITCase.java
index bd2d559..c4241ed 100644
--- a/fit/core-reference/src/test/java/org/apache/syncope/fit/console/PoliciesITCase.java
+++ b/fit/core-reference/src/test/java/org/apache/syncope/fit/console/PoliciesITCase.java
@@ -727,7 +727,7 @@ public class PoliciesITCase extends AbstractConsoleITCase {
 
         TESTER.executeAjaxEvent(component.getPageRelativePath(), Constants.ON_CLICK);
         TESTER.clickLink("body:content:body:container:content:tabbedPanel:panel:searchResult:outerObjectsRepeater:"
-                + "1:outer:container:content:togglePanelContainer:container:actions:actions:actionRepeater:9:"
+                + "1:outer:container:content:togglePanelContainer:container:actions:actions:actionRepeater:10:"
                 + "action:action");
 
         TESTER.assertComponent(
@@ -784,7 +784,7 @@ public class PoliciesITCase extends AbstractConsoleITCase {
 
         TESTER.executeAjaxEvent(TESTER.getComponentFromLastRenderedPage(
                 "body:content:body:container:content:tabbedPanel:panel:searchResult:outerObjectsRepeater:"
-                + "1:outer:container:content:togglePanelContainer:container:actions:actions:actionRepeater:10:"
+                + "1:outer:container:content:togglePanelContainer:container:actions:actions:actionRepeater:11:"
                 + "action:action"), Constants.ON_CLICK);
 
         assertSuccessMessage();
diff --git a/fit/core-reference/src/test/java/org/apache/syncope/fit/console/UsersITCase.java b/fit/core-reference/src/test/java/org/apache/syncope/fit/console/UsersITCase.java
index 030b7cb..1507310 100644
--- a/fit/core-reference/src/test/java/org/apache/syncope/fit/console/UsersITCase.java
+++ b/fit/core-reference/src/test/java/org/apache/syncope/fit/console/UsersITCase.java
@@ -91,7 +91,7 @@ public class UsersITCase extends AbstractConsoleITCase {
 
         TESTER.executeAjaxEvent(component.getPageRelativePath(), Constants.ON_CLICK);
         TESTER.clickLink(TAB_PANEL + "outerObjectsRepeater:1:outer:container:content:togglePanelContainer:container:"
-                + "actions:actions:actionRepeater:9:action:action");
+                + "actions:actions:actionRepeater:10:action:action");
 
         TESTER.assertComponent(TAB_PANEL + "outerObjectsRepeater:0:outer:form:content:form:view:username:textField",
                 TextField.class);
@@ -551,7 +551,7 @@ public class UsersITCase extends AbstractConsoleITCase {
         TESTER.executeAjaxEvent(component.getPageRelativePath(), Constants.ON_CLICK);
         TESTER.assertComponent(TAB_PANEL
                 + "outerObjectsRepeater:1:outer:container:content:togglePanelContainer:container:"
-                + "actions:actions:actionRepeater:10:action:action", IndicatingOnConfirmAjaxLink.class);
+                + "actions:actions:actionRepeater:11:action:action", IndicatingOnConfirmAjaxLink.class);
     }
 
     @Test