From ad763dd8456e6d8c7d755d74e4a17775bc257f5a Mon Sep 17 00:00:00 2001
From: pfurio <pedrofurio@gmail.com>
Date: Thu, 17 Oct 2024 15:13:38 +0200
Subject: [PATCH 1/2] catalog: allow study administrators to sync external
 users, #TASK-5688

---
 .../app/cli/main/OpenCgaCompleter.java        |   2 +-
 .../app/cli/main/OpencgaCliOptionsParser.java |   1 +
 .../main/executors/UsersCommandExecutor.java  |  35 ++++
 .../cli/main/options/AdminCommandOptions.java |   2 +-
 .../cli/main/options/UsersCommandOptions.java |  34 ++++
 .../opencga/catalog/managers/UserManager.java | 184 ++++++++++++++++--
 opencga-client/src/main/R/R/Admin-methods.R   |   2 +-
 opencga-client/src/main/R/R/User-methods.R    |   7 +
 .../client/rest/clients/AdminClient.java      |   2 +-
 .../client/rest/clients/UserClient.java       |  14 ++
 opencga-client/src/main/javascript/Admin.js   |   2 +-
 opencga-client/src/main/javascript/User.js    |   8 +
 .../pyopencga/rest_clients/admin_client.py    |   4 +-
 .../pyopencga/rest_clients/user_client.py     |  11 ++
 .../opencga/server/rest/UserWSServer.java     |  35 ++++
 .../server/rest/admin/AdminWSServer.java      |   3 +-
 16 files changed, 325 insertions(+), 21 deletions(-)

diff --git a/opencga-app/src/main/java/org/opencb/opencga/app/cli/main/OpenCgaCompleter.java b/opencga-app/src/main/java/org/opencb/opencga/app/cli/main/OpenCgaCompleter.java
index fa5b3482284..35dc8ce5ccd 100644
--- a/opencga-app/src/main/java/org/opencb/opencga/app/cli/main/OpenCgaCompleter.java
+++ b/opencga-app/src/main/java/org/opencb/opencga/app/cli/main/OpenCgaCompleter.java
@@ -69,7 +69,7 @@ public abstract class OpenCgaCompleter implements Completer {
             .map(Candidate::new)
             .collect(toList());
 
-    private List<Candidate> usersList = asList( "anonymous","create","login","password","search","info","configs","configs-update","filters","password-reset","update")
+    private List<Candidate> usersList = asList( "anonymous","create","login","password","search","sync","info","configs","configs-update","filters","password-reset","update")
             .stream()
             .map(Candidate::new)
             .collect(toList());
diff --git a/opencga-app/src/main/java/org/opencb/opencga/app/cli/main/OpencgaCliOptionsParser.java b/opencga-app/src/main/java/org/opencb/opencga/app/cli/main/OpencgaCliOptionsParser.java
index cfd4cee9919..16e277a9e67 100644
--- a/opencga-app/src/main/java/org/opencb/opencga/app/cli/main/OpencgaCliOptionsParser.java
+++ b/opencga-app/src/main/java/org/opencb/opencga/app/cli/main/OpencgaCliOptionsParser.java
@@ -215,6 +215,7 @@ public OpencgaCliOptionsParser() {
         usersSubCommands.addCommand("login", usersCommandOptions.loginCommandOptions);
         usersSubCommands.addCommand("password", usersCommandOptions.passwordCommandOptions);
         usersSubCommands.addCommand("search", usersCommandOptions.searchCommandOptions);
+        usersSubCommands.addCommand("sync", usersCommandOptions.syncCommandOptions);
         usersSubCommands.addCommand("info", usersCommandOptions.infoCommandOptions);
         usersSubCommands.addCommand("configs", usersCommandOptions.configsCommandOptions);
         usersSubCommands.addCommand("configs-update", usersCommandOptions.updateConfigsCommandOptions);
diff --git a/opencga-app/src/main/java/org/opencb/opencga/app/cli/main/executors/UsersCommandExecutor.java b/opencga-app/src/main/java/org/opencb/opencga/app/cli/main/executors/UsersCommandExecutor.java
index 5f8dfce148c..a30531d4ae8 100644
--- a/opencga-app/src/main/java/org/opencb/opencga/app/cli/main/executors/UsersCommandExecutor.java
+++ b/opencga-app/src/main/java/org/opencb/opencga/app/cli/main/executors/UsersCommandExecutor.java
@@ -18,7 +18,9 @@
 import org.opencb.opencga.catalog.utils.ParamUtils.AddRemoveAction;
 import org.opencb.opencga.client.exceptions.ClientException;
 import org.opencb.opencga.core.common.JacksonUtils;
+import org.opencb.opencga.core.models.admin.GroupSyncParams;
 import org.opencb.opencga.core.models.common.Enums;
+import org.opencb.opencga.core.models.study.Group;
 import org.opencb.opencga.core.models.user.AuthenticationResponse;
 import org.opencb.opencga.core.models.user.ConfigUpdateParams;
 import org.opencb.opencga.core.models.user.FilterUpdateParams;
@@ -80,6 +82,9 @@ public void execute() throws Exception {
             case "search":
                 queryResponse = search();
                 break;
+            case "sync":
+                queryResponse = sync();
+                break;
             case "info":
                 queryResponse = info();
                 break;
@@ -209,6 +214,36 @@ private RestResponse<User> search() throws Exception {
         return openCGAClient.getUserClient().search(queryParams);
     }
 
+    private RestResponse<Group> sync() throws Exception {
+        logger.debug("Executing sync in Users command line");
+
+        UsersCommandOptions.SyncCommandOptions commandOptions = usersCommandOptions.syncCommandOptions;
+
+        GroupSyncParams groupSyncParams = null;
+        if (commandOptions.jsonDataModel) {
+            RestResponse<Group> res = new RestResponse<>();
+            res.setType(QueryType.VOID);
+            PrintUtils.println(getObjectAsJSON(categoryName,"/{apiVersion}/users/sync"));
+            return res;
+        } else if (commandOptions.jsonFile != null) {
+            groupSyncParams = JacksonUtils.getDefaultObjectMapper()
+                    .readValue(new java.io.File(commandOptions.jsonFile), GroupSyncParams.class);
+        } else {
+            ObjectMap beanParams = new ObjectMap();
+            putNestedIfNotEmpty(beanParams, "authenticationOriginId", commandOptions.authenticationOriginId, true);
+            putNestedIfNotEmpty(beanParams, "from", commandOptions.from, true);
+            putNestedIfNotEmpty(beanParams, "to", commandOptions.to, true);
+            putNestedIfNotEmpty(beanParams, "study", commandOptions.study, true);
+            putNestedIfNotNull(beanParams, "syncAll", commandOptions.syncAll, true);
+            putNestedIfNotNull(beanParams, "force", commandOptions.force, true);
+
+            groupSyncParams = JacksonUtils.getDefaultObjectMapper().copy()
+                    .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, true)
+                    .readValue(beanParams.toJson(), GroupSyncParams.class);
+        }
+        return openCGAClient.getUserClient().sync(groupSyncParams);
+    }
+
     private RestResponse<User> info() throws Exception {
         logger.debug("Executing info in Users command line");
 
diff --git a/opencga-app/src/main/java/org/opencb/opencga/app/cli/main/options/AdminCommandOptions.java b/opencga-app/src/main/java/org/opencb/opencga/app/cli/main/options/AdminCommandOptions.java
index 60684639ca0..d0ab19ef505 100644
--- a/opencga-app/src/main/java/org/opencb/opencga/app/cli/main/options/AdminCommandOptions.java
+++ b/opencga-app/src/main/java/org/opencb/opencga/app/cli/main/options/AdminCommandOptions.java
@@ -248,7 +248,7 @@ public class SearchUsersCommandOptions {
     
     }
 
-    @Parameters(commandNames = {"users-sync"}, commandDescription ="Synchronise a group of users from an authentication origin with a group in a study from catalog")
+    @Parameters(commandNames = {"users-sync"}, commandDescription ="[DEPRECATED] Synchronise a group of users from an authentication origin with a group in a study from catalog")
     public class SyncUsersCommandOptions {
     
         @ParametersDelegate
diff --git a/opencga-app/src/main/java/org/opencb/opencga/app/cli/main/options/UsersCommandOptions.java b/opencga-app/src/main/java/org/opencb/opencga/app/cli/main/options/UsersCommandOptions.java
index 66d7cee55a8..3eecd96ccd2 100644
--- a/opencga-app/src/main/java/org/opencb/opencga/app/cli/main/options/UsersCommandOptions.java
+++ b/opencga-app/src/main/java/org/opencb/opencga/app/cli/main/options/UsersCommandOptions.java
@@ -38,6 +38,7 @@ public class UsersCommandOptions extends CustomUsersCommandOptions {
         public LoginCommandOptions loginCommandOptions;
         public PasswordCommandOptions passwordCommandOptions;
         public SearchCommandOptions searchCommandOptions;
+        public SyncCommandOptions syncCommandOptions;
         public InfoCommandOptions infoCommandOptions;
         public ConfigsCommandOptions configsCommandOptions;
         public UpdateConfigsCommandOptions updateConfigsCommandOptions;
@@ -54,6 +55,7 @@ public UsersCommandOptions(CommonCommandOptions commonCommandOptions, JCommander
         this.loginCommandOptions = new LoginCommandOptions();
         this.passwordCommandOptions = new PasswordCommandOptions();
         this.searchCommandOptions = new SearchCommandOptions();
+        this.syncCommandOptions = new SyncCommandOptions();
         this.infoCommandOptions = new InfoCommandOptions();
         this.configsCommandOptions = new ConfigsCommandOptions();
         this.updateConfigsCommandOptions = new UpdateConfigsCommandOptions();
@@ -170,6 +172,38 @@ public class SearchCommandOptions {
     
     }
 
+    @Parameters(commandNames = {"sync"}, commandDescription ="Synchronise a group of users from an authentication origin with a group in a study from catalog")
+    public class SyncCommandOptions {
+    
+        @ParametersDelegate
+        public CommonCommandOptions commonOptions = commonCommandOptions;
+    
+        @Parameter(names = {"--json-file"}, description = "File with the body data in JSON format. Note, that using this parameter will ignore all the other parameters.", required = false, arity = 1)
+        public String jsonFile;
+    
+        @Parameter(names = {"--json-data-model"}, description = "Show example of file structure for body data.", help = true, arity = 0)
+        public Boolean jsonDataModel = false;
+    
+        @Parameter(names = {"--authentication-origin-id"}, description = "The body web service authenticationOriginId parameter", required = false, arity = 1)
+        public String authenticationOriginId;
+    
+        @Parameter(names = {"--from"}, description = "The body web service from parameter", required = false, arity = 1)
+        public String from;
+    
+        @Parameter(names = {"--to"}, description = "The body web service to parameter", required = false, arity = 1)
+        public String to;
+    
+        @Parameter(names = {"--study", "-s"}, description = "The body web service study parameter", required = false, arity = 1)
+        public String study;
+    
+        @Parameter(names = {"--sync-all"}, description = "The body web service syncAll parameter", required = false, help = true, arity = 0)
+        public boolean syncAll = false;
+    
+        @Parameter(names = {"--force"}, description = "The body web service force parameter", required = false, help = true, arity = 0)
+        public boolean force = false;
+    
+    }
+
     @Parameters(commandNames = {"info"}, commandDescription ="Return the user information including its projects and studies")
     public class InfoCommandOptions {
     
diff --git a/opencga-catalog/src/main/java/org/opencb/opencga/catalog/managers/UserManager.java b/opencga-catalog/src/main/java/org/opencb/opencga/catalog/managers/UserManager.java
index 7e8e8cdffeb..9eae9383b33 100644
--- a/opencga-catalog/src/main/java/org/opencb/opencga/catalog/managers/UserManager.java
+++ b/opencga-catalog/src/main/java/org/opencb/opencga/catalog/managers/UserManager.java
@@ -46,6 +46,7 @@
 import org.opencb.opencga.core.models.organizations.Organization;
 import org.opencb.opencga.core.models.study.Group;
 import org.opencb.opencga.core.models.study.GroupUpdateParams;
+import org.opencb.opencga.core.models.study.Study;
 import org.opencb.opencga.core.models.user.*;
 import org.opencb.opencga.core.response.OpenCGAResult;
 import org.slf4j.Logger;
@@ -302,6 +303,7 @@ public JwtPayload validateToken(String token) throws CatalogException {
         return jwtPayload;
     }
 
+    @Deprecated
     public void syncAllUsersOfExternalGroup(String organizationId, String study, String authOrigin, String token) throws CatalogException {
         if (!OPENCGA.equals(authenticationFactory.getUserId(organizationId, authOrigin, token))) {
             throw new CatalogAuthorizationException("Only the root user can perform this action");
@@ -365,19 +367,74 @@ public void syncAllUsersOfExternalGroup(String organizationId, String study, Str
         }
     }
 
-    /**
-     * Register all the users belonging to a remote group. If internalGroup and study are not null, it will also associate the remote group
-     * to the internalGroup defined.
-     *
-     * @param organizationId Organization id.
-     * @param authOrigin     Authentication origin.
-     * @param remoteGroup    Group name of the remote authentication origin.
-     * @param internalGroup  Group name in Catalog that will be associated to the remote group.
-     * @param study          Study where the internal group will be associated.
-     * @param sync           Boolean indicating whether the remote group will be synced with the internal group or not.
-     * @param token          JWT token. The token should belong to the root user.
-     * @throws CatalogException If any of the parameters is wrong or there is any internal error.
-     */
+    public void syncAllUsersOfExternalGroup(String studyStr, String authOrigin, String token) throws CatalogException {
+        JwtPayload tokenPayload = validateToken(token);
+        CatalogFqn studyFqn = CatalogFqn.extractFqnFromStudy(studyStr, tokenPayload);
+        Study study = catalogManager.getStudyManager().resolveId(studyFqn, null, tokenPayload);
+        String organizationId = studyFqn.getOrganizationId();
+        String userId = tokenPayload.getUserId(organizationId);
+
+        authorizationManager.checkIsAtLeastStudyAdministrator(organizationId, study.getUid(), userId);
+
+        OpenCGAResult<Group> allGroups = catalogManager.getStudyManager().getGroup(studyStr, null, token);
+
+        boolean foundAny = false;
+        for (Group group : allGroups.getResults()) {
+            if (group.getSyncedFrom() != null && group.getSyncedFrom().getAuthOrigin().equals(authOrigin)) {
+                logger.info("Fetching users of group '{}' from authentication origin '{}'", group.getSyncedFrom().getRemoteGroup(),
+                        group.getSyncedFrom().getAuthOrigin());
+                foundAny = true;
+
+                List<User> userList;
+                try {
+                    userList = authenticationFactory.getUsersFromRemoteGroup(organizationId, group.getSyncedFrom().getAuthOrigin(),
+                            group.getSyncedFrom().getRemoteGroup());
+                } catch (CatalogException e) {
+                    // There was some kind of issue for which we could not retrieve the group information.
+                    logger.info("Removing all users from group '{}' belonging to group '{}' in the external authentication origin",
+                            group.getId(), group.getSyncedFrom().getAuthOrigin());
+                    logger.info("Please, manually remove group '{}' if external group '{}' was removed from the authentication origin",
+                            group.getId(), group.getSyncedFrom().getAuthOrigin());
+                    catalogManager.getStudyManager().updateGroup(studyStr, group.getId(), ParamUtils.BasicUpdateAction.SET,
+                            new GroupUpdateParams(Collections.emptyList()), token);
+                    continue;
+                }
+                Iterator<User> iterator = userList.iterator();
+                while (iterator.hasNext()) {
+                    User user = iterator.next();
+                    user.setOrganization(organizationId);
+                    try {
+                        create(user, null, token);
+                        logger.info("User '{}' ({}) successfully created", user.getId(), user.getName());
+                    } catch (CatalogParameterException e) {
+                        logger.warn("Could not create user '{}' ({}). {}", user.getId(), user.getName(), e.getMessage());
+                        iterator.remove();
+                    } catch (CatalogException e) {
+                        if (!e.getMessage().contains("already exists")) {
+                            logger.warn("Could not create user '{}' ({}). {}", user.getId(), user.getName(), e.getMessage());
+                            iterator.remove();
+                        }
+                    }
+                }
+
+                GroupUpdateParams updateParams;
+                if (ListUtils.isEmpty(userList)) {
+                    logger.info("No members associated to the external group");
+                    updateParams = new GroupUpdateParams(Collections.emptyList());
+                } else {
+                    logger.info("Associating members to the internal OpenCGA group");
+                    updateParams = new GroupUpdateParams(new ArrayList<>(userList.stream().map(User::getId).collect(Collectors.toSet())));
+                }
+                catalogManager.getStudyManager().updateGroup(studyStr, group.getId(), ParamUtils.BasicUpdateAction.SET,
+                        updateParams, token);
+            }
+        }
+        if (!foundAny) {
+            logger.info("No synced groups found in study '{}' from authentication origin '{}'", studyStr, authOrigin);
+        }
+    }
+
+    @Deprecated
     public void importRemoteGroupOfUsers(String organizationId, String authOrigin, String remoteGroup, @Nullable String internalGroup,
                                          @Nullable String study, boolean sync, String token) throws CatalogException {
         JwtPayload payload = validateToken(token);
@@ -454,6 +511,107 @@ public void importRemoteGroupOfUsers(String organizationId, String authOrigin, S
         }
     }
 
+    /**
+     * Register all the users belonging to a remote group. If internalGroup and study are not null, it will also associate the remote group
+     * to the internalGroup defined.
+     *
+     * @param authOrigin     Authentication origin.
+     * @param remoteGroup    Group name of the remote authentication origin.
+     * @param internalGroup  Group name in Catalog that will be associated to the remote group.
+     * @param studyStr       Study where the internal group will be associated.
+     * @param sync           Boolean indicating whether the remote group will be synced with the internal group or not.
+     * @param token          JWT token. The token should belong to the root user.
+     * @throws CatalogException If any of the parameters is wrong or there is any internal error.
+     */
+    public void importRemoteGroupOfUsers(String authOrigin, String remoteGroup, @Nullable String internalGroup,
+                                         @Nullable String studyStr, boolean sync, String token) throws CatalogException {
+        JwtPayload tokenPayload = validateToken(token);
+        String organizationId;
+        String userId;
+        Study study = null;
+        if (StringUtils.isNotEmpty(studyStr)) {
+            CatalogFqn studyFqn = CatalogFqn.extractFqnFromStudy(studyStr, tokenPayload);
+            study = catalogManager.getStudyManager().resolveId(studyFqn, null, tokenPayload);
+            organizationId = studyFqn.getOrganizationId();
+            userId = tokenPayload.getUserId(organizationId);
+        } else {
+            organizationId = tokenPayload.getOrganization();
+            userId = tokenPayload.getUserId();
+        }
+
+        ObjectMap auditParams = new ObjectMap()
+                .append("organizationId", organizationId)
+                .append("authOrigin", authOrigin)
+                .append("remoteGroup", remoteGroup)
+                .append("internalGroup", internalGroup)
+                .append("study", studyStr)
+                .append("sync", sync)
+                .append("token", token);
+        try {
+            if (studyStr != null) {
+                authorizationManager.checkIsAtLeastStudyAdministrator(organizationId, study.getUid(), userId);
+            } else {
+                authorizationManager.checkIsAtLeastOrganizationOwnerOrAdmin(organizationId, userId);
+            }
+
+            ParamUtils.checkParameter(authOrigin, "Authentication origin");
+            ParamUtils.checkParameter(remoteGroup, "Remote group");
+
+            List<User> userList;
+            if (sync) {
+                // We don't create any user as they will be automatically populated during login
+                userList = Collections.emptyList();
+            } else {
+                logger.info("Fetching users from authentication origin '{}'", authOrigin);
+
+                // Register the users
+                userList = authenticationFactory.getUsersFromRemoteGroup(organizationId, authOrigin, remoteGroup);
+                for (User user : userList) {
+                    user.setOrganization(organizationId);
+                    try {
+                        create(user, null, token);
+                        logger.info("User '{}' successfully created", user.getId());
+                    } catch (CatalogException e) {
+                        logger.warn("{}", e.getMessage());
+                    }
+                }
+            }
+
+            if (StringUtils.isNotEmpty(internalGroup) && StringUtils.isNotEmpty(studyStr)) {
+                // Check if the group already exists
+                OpenCGAResult<Group> groupResult = catalogManager.getStudyManager().getGroup(studyStr, internalGroup, token);
+                if (groupResult.getNumResults() == 1) {
+                    logger.error("Cannot synchronise with group {}. The group already exists and is already in use.", internalGroup);
+                    throw new CatalogException("Cannot synchronise with group " + internalGroup
+                            + ". The group already exists and is already in use.");
+                }
+
+                // Create new group associating it to the remote group
+                try {
+                    logger.info("Attempting to register group '{}' in study '{}'", internalGroup, studyStr);
+                    Group.Sync groupSync = null;
+                    if (sync) {
+                        groupSync = new Group.Sync(authOrigin, remoteGroup);
+                    }
+                    Group group = new Group(internalGroup, userList.stream().map(User::getId).collect(Collectors.toList()))
+                            .setSyncedFrom(groupSync);
+                    catalogManager.getStudyManager().createGroup(studyStr, group, token);
+                    logger.info("Group '{}' created and synchronised with external group", internalGroup);
+                    auditManager.audit(organizationId, userId, Enums.Action.IMPORT_EXTERNAL_GROUP_OF_USERS, Enums.Resource.USER,
+                            group.getId(), "", studyStr, "", auditParams, new AuditRecord.Status(AuditRecord.Status.Result.SUCCESS));
+                } catch (CatalogException e) {
+                    logger.error("Could not register group '{}' in study '{}'\n{}", internalGroup, studyStr, e.getMessage(), e);
+                    throw new CatalogException("Could not register group '" + internalGroup + "' in study '" + studyStr + "': "
+                            + e.getMessage(), e);
+                }
+            }
+        } catch (CatalogException e) {
+            auditManager.audit(organizationId, userId, Enums.Action.IMPORT_EXTERNAL_GROUP_OF_USERS, Enums.Resource.USER, "", "", "", "",
+                    auditParams, new AuditRecord.Status(AuditRecord.Status.Result.ERROR, e.getError()));
+            throw e;
+        }
+    }
+
     /**
      * Register all the ids. If internalGroup and study are not null, it will also associate the users to the internalGroup defined.
      *
diff --git a/opencga-client/src/main/R/R/Admin-methods.R b/opencga-client/src/main/R/R/Admin-methods.R
index d63a1ad3478..1e0d83ec53d 100644
--- a/opencga-client/src/main/R/R/Admin-methods.R
+++ b/opencga-client/src/main/R/R/Admin-methods.R
@@ -101,7 +101,7 @@ setMethod("adminClient", "OpencgaR", function(OpencgaR, user, endpointName, para
                 subcategoryId=NULL, action="search", params=params, httpMethod="GET", as.queryParam=NULL, ...),
 
         #' @section Endpoint /{apiVersion}/admin/users/sync:
-        #' Synchronise a group of users from an authentication origin with a group in a study from catalog.
+        #' [DEPRECATED] Synchronise a group of users from an authentication origin with a group in a study from catalog.
         #' @param organization Organization id.
         #' @param data JSON containing the parameters.
         syncUsers=fetchOpenCGA(object=OpencgaR, category="admin", categoryId=NULL, subcategory="users",
diff --git a/opencga-client/src/main/R/R/User-methods.R b/opencga-client/src/main/R/R/User-methods.R
index ffc1c599ca9..54dc587c346 100644
--- a/opencga-client/src/main/R/R/User-methods.R
+++ b/opencga-client/src/main/R/R/User-methods.R
@@ -24,6 +24,7 @@
 #' | login | /{apiVersion}/users/login | body |
 #' | password | /{apiVersion}/users/password | body[*] |
 #' | search | /{apiVersion}/users/search | include, exclude, limit, skip, count, organization, id, authenticationId |
+#' | sync | /{apiVersion}/users/sync | body[*] |
 #' | info | /{apiVersion}/users/{users}/info | include, exclude, organization, users[*] |
 #' | configs | /{apiVersion}/users/{user}/configs | user[*], name |
 #' | updateConfigs | /{apiVersion}/users/{user}/configs/update | user[*], action, body[*] |
@@ -80,6 +81,12 @@ setMethod("userClient", "OpencgaR", function(OpencgaR, filterId, user, users, en
         search=fetchOpenCGA(object=OpencgaR, category="users", categoryId=NULL, subcategory=NULL, subcategoryId=NULL,
                 action="search", params=params, httpMethod="GET", as.queryParam=NULL, ...),
 
+        #' @section Endpoint /{apiVersion}/users/sync:
+        #' Synchronise a group of users from an authentication origin with a group in a study from catalog.
+        #' @param data JSON containing the parameters.
+        sync=fetchOpenCGA(object=OpencgaR, category="users", categoryId=NULL, subcategory=NULL, subcategoryId=NULL,
+                action="sync", params=params, httpMethod="POST", as.queryParam=NULL, ...),
+
         #' @section Endpoint /{apiVersion}/users/{users}/info:
         #' Return the user information including its projects and studies.
         #' @param include Fields included in the response, whole JSON path must be provided.
diff --git a/opencga-client/src/main/java/org/opencb/opencga/client/rest/clients/AdminClient.java b/opencga-client/src/main/java/org/opencb/opencga/client/rest/clients/AdminClient.java
index 928fd361341..efbd7283fea 100644
--- a/opencga-client/src/main/java/org/opencb/opencga/client/rest/clients/AdminClient.java
+++ b/opencga-client/src/main/java/org/opencb/opencga/client/rest/clients/AdminClient.java
@@ -161,7 +161,7 @@ public RestResponse<Sample> searchUsers(ObjectMap params) throws ClientException
     }
 
     /**
-     * Synchronise a group of users from an authentication origin with a group in a study from catalog.
+     * [DEPRECATED] Synchronise a group of users from an authentication origin with a group in a study from catalog.
      * @param data JSON containing the parameters.
      * @param params Map containing any of the following optional parameters.
      *       organization: Organization id.
diff --git a/opencga-client/src/main/java/org/opencb/opencga/client/rest/clients/UserClient.java b/opencga-client/src/main/java/org/opencb/opencga/client/rest/clients/UserClient.java
index ee290b40d21..d80dd1a5d3e 100644
--- a/opencga-client/src/main/java/org/opencb/opencga/client/rest/clients/UserClient.java
+++ b/opencga-client/src/main/java/org/opencb/opencga/client/rest/clients/UserClient.java
@@ -20,6 +20,8 @@
 import org.opencb.opencga.client.config.ClientConfiguration;
 import org.opencb.opencga.client.exceptions.ClientException;
 import org.opencb.opencga.client.rest.*;
+import org.opencb.opencga.core.models.admin.GroupSyncParams;
+import org.opencb.opencga.core.models.study.Group;
 import org.opencb.opencga.core.models.user.AuthenticationResponse;
 import org.opencb.opencga.core.models.user.ConfigUpdateParams;
 import org.opencb.opencga.core.models.user.FilterUpdateParams;
@@ -121,6 +123,18 @@ public RestResponse<User> search(ObjectMap params) throws ClientException {
         return execute("users", null, null, null, "search", params, GET, User.class);
     }
 
+    /**
+     * Synchronise a group of users from an authentication origin with a group in a study from catalog.
+     * @param data JSON containing the parameters.
+     * @return a RestResponse object.
+     * @throws ClientException ClientException if there is any server error.
+     */
+    public RestResponse<Group> sync(GroupSyncParams data) throws ClientException {
+        ObjectMap params = new ObjectMap();
+        params.put("body", data);
+        return execute("users", null, null, null, "sync", params, POST, Group.class);
+    }
+
     /**
      * Return the user information including its projects and studies.
      * @param users Comma separated list of user IDs.
diff --git a/opencga-client/src/main/javascript/Admin.js b/opencga-client/src/main/javascript/Admin.js
index 3f6204d4165..a616597343e 100644
--- a/opencga-client/src/main/javascript/Admin.js
+++ b/opencga-client/src/main/javascript/Admin.js
@@ -116,7 +116,7 @@ export default class Admin extends OpenCGAParentClass {
         return this._get("admin", null, "users", null, "search", params);
     }
 
-    /** Synchronise a group of users from an authentication origin with a group in a study from catalog
+    /** [DEPRECATED] Synchronise a group of users from an authentication origin with a group in a study from catalog
     * @param {Object} data - JSON containing the parameters.
     * @param {Object} [params] - The Object containing the following optional parameters:
     * @param {String} [params.organization] - Organization id.
diff --git a/opencga-client/src/main/javascript/User.js b/opencga-client/src/main/javascript/User.js
index 32b28182c79..366a84e6254 100644
--- a/opencga-client/src/main/javascript/User.js
+++ b/opencga-client/src/main/javascript/User.js
@@ -84,6 +84,14 @@ export default class User extends OpenCGAParentClass {
         return this._get("users", null, null, null, "search", params);
     }
 
+    /** Synchronise a group of users from an authentication origin with a group in a study from catalog
+    * @param {Object} data - JSON containing the parameters.
+    * @returns {Promise} Promise object in the form of RestResponse instance.
+    */
+    sync(data) {
+        return this._post("users", null, null, null, "sync", data);
+    }
+
     /** Return the user information including its projects and studies
     * @param {String} users - Comma separated list of user IDs.
     * @param {Object} [params] - The Object containing the following optional parameters:
diff --git a/opencga-client/src/main/python/pyopencga/rest_clients/admin_client.py b/opencga-client/src/main/python/pyopencga/rest_clients/admin_client.py
index 7311fa7e5dd..1bc291c6e89 100644
--- a/opencga-client/src/main/python/pyopencga/rest_clients/admin_client.py
+++ b/opencga-client/src/main/python/pyopencga/rest_clients/admin_client.py
@@ -122,8 +122,8 @@ def search_users(self, **options):
 
     def sync_users(self, data=None, **options):
         """
-        Synchronise a group of users from an authentication origin with a
-            group in a study from catalog.
+        [DEPRECATED] Synchronise a group of users from an authentication
+            origin with a group in a study from catalog.
         PATH: /{apiVersion}/admin/users/sync
 
         :param dict data: JSON containing the parameters. (REQUIRED)
diff --git a/opencga-client/src/main/python/pyopencga/rest_clients/user_client.py b/opencga-client/src/main/python/pyopencga/rest_clients/user_client.py
index 93451319d3d..7848b47e358 100644
--- a/opencga-client/src/main/python/pyopencga/rest_clients/user_client.py
+++ b/opencga-client/src/main/python/pyopencga/rest_clients/user_client.py
@@ -84,6 +84,17 @@ def search(self, **options):
 
         return self._get(category='users', resource='search', **options)
 
+    def sync(self, data=None, **options):
+        """
+        Synchronise a group of users from an authentication origin with a
+            group in a study from catalog.
+        PATH: /{apiVersion}/users/sync
+
+        :param dict data: JSON containing the parameters. (REQUIRED)
+        """
+
+        return self._post(category='users', resource='sync', data=data, **options)
+
     def info(self, users, **options):
         """
         Return the user information including its projects and studies.
diff --git a/opencga-server/src/main/java/org/opencb/opencga/server/rest/UserWSServer.java b/opencga-server/src/main/java/org/opencb/opencga/server/rest/UserWSServer.java
index 31fcd3a9720..b61a50f1d5a 100644
--- a/opencga-server/src/main/java/org/opencb/opencga/server/rest/UserWSServer.java
+++ b/opencga-server/src/main/java/org/opencb/opencga/server/rest/UserWSServer.java
@@ -25,6 +25,8 @@
 import org.opencb.opencga.catalog.utils.ParamUtils;
 import org.opencb.opencga.core.api.ParamConstants;
 import org.opencb.opencga.core.exceptions.VersionException;
+import org.opencb.opencga.core.models.admin.GroupSyncParams;
+import org.opencb.opencga.core.models.study.Group;
 import org.opencb.opencga.core.models.user.*;
 import org.opencb.opencga.core.response.OpenCGAResult;
 import org.opencb.opencga.core.tools.annotations.*;
@@ -115,6 +117,39 @@ public Response getInfo(
         }
     }
 
+    @POST
+    @Path("/sync")
+    @ApiOperation(value = "Synchronise a group of users from an authentication origin with a group in a study from catalog", response = Group.class,
+            notes = "Mandatory fields: <b>authOriginId</b>, <b>study</b><br>"
+                    + "<ul>"
+                    + "<li><b>authOriginId</b>: Authentication origin id defined in the main Catalog configuration.</li>"
+                    + "<li><b>study</b>: Study [[organization@]project:]study where the list of users will be associated to.</li>"
+                    + "<li><b>from</b>: Group defined in the authenticated origin to be synchronised.</li>"
+                    + "<li><b>to</b>: Group in a study that will be synchronised.</li>"
+                    + "<li><b>syncAll</b>: Flag indicating whether to synchronise all the groups present in the study with"
+                    + " their corresponding authenticated groups automatically. --from and --to parameters will not be needed when the flag"
+                    + "is active..</li>"
+                    + "<li><b>force</b>: Boolean to force the synchronisation with already existing Catalog groups that are not yet "
+                    + "synchronised with any other group.</li>"
+                    + "</ul>"
+    )
+    public Response externalSync(
+            @ApiParam(value = "JSON containing the parameters", required = true) GroupSyncParams syncParams
+    ) {
+        try {
+            // TODO: These two methods should return an OpenCGAResult containing at least the number of changes
+            if (syncParams.isSyncAll()) {
+                catalogManager.getUserManager().syncAllUsersOfExternalGroup(syncParams.getStudy(), syncParams.getAuthenticationOriginId(), token);
+            } else {
+                catalogManager.getUserManager().importRemoteGroupOfUsers(syncParams.getAuthenticationOriginId(), syncParams.getFrom(),
+                        syncParams.getTo(), syncParams.getStudy(), true, token);
+            }
+            return createOkResponse(OpenCGAResult.empty(Group.class));
+        } catch (Exception e) {
+            return createErrorResponse(e);
+        }
+    }
+
     @POST
     @Path("/login")
     @Consumes(MediaType.APPLICATION_JSON)
diff --git a/opencga-server/src/main/java/org/opencb/opencga/server/rest/admin/AdminWSServer.java b/opencga-server/src/main/java/org/opencb/opencga/server/rest/admin/AdminWSServer.java
index c0345d421f5..2c2e962751a 100644
--- a/opencga-server/src/main/java/org/opencb/opencga/server/rest/admin/AdminWSServer.java
+++ b/opencga-server/src/main/java/org/opencb/opencga/server/rest/admin/AdminWSServer.java
@@ -180,9 +180,10 @@ public Response updateGroups(
         }
     }
 
+    @Deprecated
     @POST
     @Path("/users/sync")
-    @ApiOperation(value = "Synchronise a group of users from an authentication origin with a group in a study from catalog", response = Group.class,
+    @ApiOperation(value = "[DEPRECATED] Synchronise a group of users from an authentication origin with a group in a study from catalog", response = Group.class,
             notes = "Mandatory fields: <b>authOriginId</b>, <b>study</b><br>"
                     + "<ul>"
                     + "<li><b>authOriginId</b>: Authentication origin id defined in the main Catalog configuration.</li>"

From c45bbe8b660d99079bc17f24d179c17cf5febdac Mon Sep 17 00:00:00 2001
From: pfurio <pedrofurio@gmail.com>
Date: Thu, 17 Oct 2024 15:56:10 +0200
Subject: [PATCH 2/2] server: add where the deprecated code was moved,
 #TASK-5688

---
 .../opencga/app/cli/main/options/AdminCommandOptions.java    | 2 +-
 opencga-client/src/main/R/R/Admin-methods.R                  | 2 +-
 .../org/opencb/opencga/client/rest/clients/AdminClient.java  | 2 +-
 opencga-client/src/main/javascript/Admin.js                  | 2 +-
 .../src/main/python/pyopencga/rest_clients/admin_client.py   | 3 +--
 .../org/opencb/opencga/server/rest/admin/AdminWSServer.java  | 5 +++--
 6 files changed, 8 insertions(+), 8 deletions(-)

diff --git a/opencga-app/src/main/java/org/opencb/opencga/app/cli/main/options/AdminCommandOptions.java b/opencga-app/src/main/java/org/opencb/opencga/app/cli/main/options/AdminCommandOptions.java
index d0ab19ef505..8b41e55359c 100644
--- a/opencga-app/src/main/java/org/opencb/opencga/app/cli/main/options/AdminCommandOptions.java
+++ b/opencga-app/src/main/java/org/opencb/opencga/app/cli/main/options/AdminCommandOptions.java
@@ -248,7 +248,7 @@ public class SearchUsersCommandOptions {
     
     }
 
-    @Parameters(commandNames = {"users-sync"}, commandDescription ="[DEPRECATED] Synchronise a group of users from an authentication origin with a group in a study from catalog")
+    @Parameters(commandNames = {"users-sync"}, commandDescription ="[DEPRECATED] Moved to /users/sync")
     public class SyncUsersCommandOptions {
     
         @ParametersDelegate
diff --git a/opencga-client/src/main/R/R/Admin-methods.R b/opencga-client/src/main/R/R/Admin-methods.R
index 1e0d83ec53d..c6f87df5923 100644
--- a/opencga-client/src/main/R/R/Admin-methods.R
+++ b/opencga-client/src/main/R/R/Admin-methods.R
@@ -101,7 +101,7 @@ setMethod("adminClient", "OpencgaR", function(OpencgaR, user, endpointName, para
                 subcategoryId=NULL, action="search", params=params, httpMethod="GET", as.queryParam=NULL, ...),
 
         #' @section Endpoint /{apiVersion}/admin/users/sync:
-        #' [DEPRECATED] Synchronise a group of users from an authentication origin with a group in a study from catalog.
+        #' [DEPRECATED] Moved to /users/sync.
         #' @param organization Organization id.
         #' @param data JSON containing the parameters.
         syncUsers=fetchOpenCGA(object=OpencgaR, category="admin", categoryId=NULL, subcategory="users",
diff --git a/opencga-client/src/main/java/org/opencb/opencga/client/rest/clients/AdminClient.java b/opencga-client/src/main/java/org/opencb/opencga/client/rest/clients/AdminClient.java
index efbd7283fea..2e2127bc499 100644
--- a/opencga-client/src/main/java/org/opencb/opencga/client/rest/clients/AdminClient.java
+++ b/opencga-client/src/main/java/org/opencb/opencga/client/rest/clients/AdminClient.java
@@ -161,7 +161,7 @@ public RestResponse<Sample> searchUsers(ObjectMap params) throws ClientException
     }
 
     /**
-     * [DEPRECATED] Synchronise a group of users from an authentication origin with a group in a study from catalog.
+     * [DEPRECATED] Moved to /users/sync.
      * @param data JSON containing the parameters.
      * @param params Map containing any of the following optional parameters.
      *       organization: Organization id.
diff --git a/opencga-client/src/main/javascript/Admin.js b/opencga-client/src/main/javascript/Admin.js
index a616597343e..97cad09bd3d 100644
--- a/opencga-client/src/main/javascript/Admin.js
+++ b/opencga-client/src/main/javascript/Admin.js
@@ -116,7 +116,7 @@ export default class Admin extends OpenCGAParentClass {
         return this._get("admin", null, "users", null, "search", params);
     }
 
-    /** [DEPRECATED] Synchronise a group of users from an authentication origin with a group in a study from catalog
+    /** [DEPRECATED] Moved to /users/sync
     * @param {Object} data - JSON containing the parameters.
     * @param {Object} [params] - The Object containing the following optional parameters:
     * @param {String} [params.organization] - Organization id.
diff --git a/opencga-client/src/main/python/pyopencga/rest_clients/admin_client.py b/opencga-client/src/main/python/pyopencga/rest_clients/admin_client.py
index 1bc291c6e89..1f4d81be29b 100644
--- a/opencga-client/src/main/python/pyopencga/rest_clients/admin_client.py
+++ b/opencga-client/src/main/python/pyopencga/rest_clients/admin_client.py
@@ -122,8 +122,7 @@ def search_users(self, **options):
 
     def sync_users(self, data=None, **options):
         """
-        [DEPRECATED] Synchronise a group of users from an authentication
-            origin with a group in a study from catalog.
+        [DEPRECATED] Moved to /users/sync.
         PATH: /{apiVersion}/admin/users/sync
 
         :param dict data: JSON containing the parameters. (REQUIRED)
diff --git a/opencga-server/src/main/java/org/opencb/opencga/server/rest/admin/AdminWSServer.java b/opencga-server/src/main/java/org/opencb/opencga/server/rest/admin/AdminWSServer.java
index 2c2e962751a..bc0b23b20c0 100644
--- a/opencga-server/src/main/java/org/opencb/opencga/server/rest/admin/AdminWSServer.java
+++ b/opencga-server/src/main/java/org/opencb/opencga/server/rest/admin/AdminWSServer.java
@@ -183,8 +183,9 @@ public Response updateGroups(
     @Deprecated
     @POST
     @Path("/users/sync")
-    @ApiOperation(value = "[DEPRECATED] Synchronise a group of users from an authentication origin with a group in a study from catalog", response = Group.class,
-            notes = "Mandatory fields: <b>authOriginId</b>, <b>study</b><br>"
+    @ApiOperation(value = "[DEPRECATED] Moved to /users/sync", response = Group.class,
+            notes = "Synchronise a group of users from an authentication origin with a group in a study from catalog.<br>"
+                    + "Mandatory fields: <b>authOriginId</b>, <b>study</b><br>"
                     + "<ul>"
                     + "<li><b>authOriginId</b>: Authentication origin id defined in the main Catalog configuration.</li>"
                     + "<li><b>study</b>: Study [[organization@]project:]study where the list of users will be associated to.</li>"