diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 45e5fd67fe1055a03f7f5daeca663cdbf47ddf96..80e43de02e6b333441c54db6298fa4dd3888e8ed 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -21,8 +21,8 @@ before_script:
   - sed 's!<server>.*</server>!<server>postgres</server>!;s!<password>.*</password>!<password>maarch</password>!;s!<name>.*</name>!<name>MaarchParapheur</name>!;s!<user>.*</user>!<user>maarch</user>!;s!<enable>.*</enable>!<enable>true</enable>!' config/config.xml.default > config/config.xml
   - sed -i 's/rights="none" pattern="PDF"/rights="read | write" pattern="PDF"/' /etc/ImageMagick-6/policy.xml
 
-job_php-7.4:
-  image: php:7.4-apache
+job_php-8.1:
+  image: php:8.1-apache
   stage: test
   services:
     - name: postgres:10.1
@@ -30,18 +30,22 @@ job_php-7.4:
   script:
     - curl --location -s --output /usr/local/bin/phpunit https://phar.phpunit.de/phpunit-9.phar
     - chmod +x /usr/local/bin/phpunit
-    - phpunit --coverage-text --colors=never -c phpunit.xml
-  only:
-    - develop
-  except:
-    - schedules
+    - phpunit --coverage-text --colors=never
+  # only:
+  #   - develop
+  # except:
+  #   - schedules
+  rules:
+  - if: '$CI_COMMIT_BRANCH =~ /(feat|fix)\/[0-9]{4,5}\/develop/'
+
   artifacts:
     paths:
       - test/unitTests/build/
     expire_in: 2h
+  # coverage: '^\s*Lines:\s*\d+.\d+\%'
 
-job_php-7.3:
-  image: php:7.3-apache
+job_php-8.0:
+  image: php:8.0-apache
   stage: test
   services:
     - name: postgres:10.1
@@ -50,90 +54,118 @@ job_php-7.3:
     - curl --location -s --output /usr/local/bin/phpunit https://phar.phpunit.de/phpunit-9.phar
     - chmod +x /usr/local/bin/phpunit
     - phpunit --coverage-text --colors=never
+  # only:
+  #   - develop
+  # except:
+  #   - schedules
+  rules:
+    - if: '$CI_COMMIT_BRANCH =~ /(feat|fix)\/[0-9]{4,5}\/develop/'
+  artifacts:
+    paths:
+      - test/unitTests/build/
+    expire_in: 2h
+  # coverage: '^\s*Lines:\s*\d+.\d+\%'
+
+job_php-7.4:
+  image: php:7.4-apache
+  stage: test
+  services:
+    - name: postgres:10.1
+      command: [ "-c", "datestyle=iso,dmy" ]
+  script:
+    - curl --location -s --output /usr/local/bin/phpunit https://phar.phpunit.de/phpunit-9.phar
+    - chmod +x /usr/local/bin/phpunit
+    - phpunit --coverage-text --colors=never -c phpunit.xml
+  # only:
+  #   - develop
+  # except:
+  #   - schedules
+  rules:
+    - if: '$CI_COMMIT_BRANCH =~ /(feat|fix)\/[0-9]{4,5}\/develop/'
+  artifacts:
+    paths:
+      - test/unitTests/build/
+    expire_in: 2h
+  # coverage: '^\s*Lines:\s*\d+.\d+\%'
+
+commits:
+  image: debian:10-slim
+  stage: synchronization
   only:
     - develop
+    - "21.03"
   except:
+    - tags
     - schedules
-
-
-commits:
- image: debian:10-slim
- stage: synchronization
- only:
-   - develop
-   - "21.03"
- except:
-   - tags
-   - schedules
- before_script:
-   # Skip the synchronisation if it is not enabled
-   - if [ $SYNC_ENABLED = "true" ]; then echo "Sync enabled"; else echo "Sync disabled, stopping the job" && exit 0; fi
-   # Configure ssh, with the private key to push to the private repository
-   - 'which ssh-agent || ( apt-get update -y && apt-get install openssh-client -y )'
-   - eval $(ssh-agent -s)
-   - echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -
-   - mkdir -p ~/.ssh
-   - chmod 700 ~/.ssh
-   - ssh-keyscan "$GITLAB_URL" >> ~/.ssh/known_hosts
-   - chmod 644 ~/.ssh/known_hosts
-   - '[[ -f /.dockerenv ]] && echo -e "Host *\n\tStrictHostKeyChecking no\n\n" >> ~/.ssh/config'
-   # Install git command
-   - apt install -y git
- script:
-   - chmod +x ./ci/commit_synchronization.sh
-   - ./ci/commit_synchronization.sh
+  before_script:
+    # Skip the synchronisation if it is not enabled
+    - if [ $SYNC_ENABLED = "true" ]; then echo "Sync enabled"; else echo "Sync disabled, stopping the job" && exit 0; fi
+    # Configure ssh, with the private key to push to the private repository
+    - 'which ssh-agent || ( apt-get update -y && apt-get install openssh-client -y )'
+    - eval $(ssh-agent -s)
+    - echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -
+    - mkdir -p ~/.ssh
+    - chmod 700 ~/.ssh
+    - ssh-keyscan "$GITLAB_URL" >> ~/.ssh/known_hosts
+    - chmod 644 ~/.ssh/known_hosts
+    - '[[ -f /.dockerenv ]] && echo -e "Host *\n\tStrictHostKeyChecking no\n\n" >> ~/.ssh/config'
+    # Install git command
+    - apt install -y git
+  script:
+    - chmod +x ./ci/commit_synchronization.sh
+    - ./ci/commit_synchronization.sh
 
 tags:
- image: debian:10-slim
- stage: synchronization
- only:
-   - tags
- except:
-   - schedules
- before_script:
-   # Skip the synchronisation if it is not enabled
-   - if [ $SYNC_ENABLED = "true" ]; then echo "Sync enabled"; else echo "Sync disabled, stopping the job" && exit 0; fi
-   # Configure ssh, with the private key to push to the private repository
-   - 'which ssh-agent || ( apt-get update -y && apt-get install openssh-client -y )'
-   - eval $(ssh-agent -s)
-   - echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -
-   - mkdir -p ~/.ssh
-   - chmod 700 ~/.ssh
-   - ssh-keyscan "$GITLAB_URL" >> ~/.ssh/known_hosts
-   - chmod 644 ~/.ssh/known_hosts
-   - '[[ -f /.dockerenv ]] && echo -e "Host *\n\tStrictHostKeyChecking no\n\n" >> ~/.ssh/config'
-   # Install git and curl command
-   - apt install -y git
-   - apt install -y curl
-   # Install npm
-   - curl -sL https://deb.nodesource.com/setup_14.x | bash -
-   - apt install -y nodejs
- script:
-   - git config --global user.email "$CI_EMAIL" && git config --global user.name "$CI_USER"
-   # We will work in another directory, to avoid git conflicts
-   - mkdir tmp
-   - cd tmp
-   # Find the branch name from tag name
-   - VERSION1=$(echo $CI_COMMIT_TAG| cut -d'.' -f 1)
-   - VERSION2=$(echo $CI_COMMIT_TAG| cut -d'.' -f 2)
-   - VERSION="${VERSION1}.${VERSION2}"
-   # Pull the private repository
-   - git init && git remote add origin $PRIVATE_REPOSITORY_URL_SSH
-   - git pull origin $VERSION
-   # Update and push build prod
-   - npm install
-   - npm run build-prod
-   - git status
-   - git add -f dist/
-   - git status
-   - git commit -m "Build prod for tag ${CI_COMMIT_TAG}"
-   - git show-ref
-   - git push origin HEAD:$VERSION
-   - git status
-   # Do the tag on the private repo
-   - git tag $CI_COMMIT_TAG
-   - git status
-   - git push origin --tags
+  image: debian:10-slim
+  stage: synchronization
+  only:
+    - tags
+  except:
+    - schedules
+  before_script:
+    # Skip the synchronisation if it is not enabled
+    - if [ $SYNC_ENABLED = "true" ]; then echo "Sync enabled"; else echo "Sync disabled, stopping the job" && exit 0; fi
+    # Configure ssh, with the private key to push to the private repository
+    - 'which ssh-agent || ( apt-get update -y && apt-get install openssh-client -y )'
+    - eval $(ssh-agent -s)
+    - echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -
+    - mkdir -p ~/.ssh
+    - chmod 700 ~/.ssh
+    - ssh-keyscan "$GITLAB_URL" >> ~/.ssh/known_hosts
+    - chmod 644 ~/.ssh/known_hosts
+    - '[[ -f /.dockerenv ]] && echo -e "Host *\n\tStrictHostKeyChecking no\n\n" >> ~/.ssh/config'
+    # Install git and curl command
+    - apt install -y git
+    - apt install -y curl
+    # Install npm
+    - curl -sL https://deb.nodesource.com/setup_14.x | bash -
+    - apt install -y nodejs
+  script:
+    - git config --global user.email "$CI_EMAIL" && git config --global user.name "$CI_USER"
+    # We will work in another directory, to avoid git conflicts
+    - mkdir tmp
+    - cd tmp
+    # Find the branch name from tag name
+    - VERSION1=$(echo $CI_COMMIT_TAG| cut -d'.' -f 1)
+    - VERSION2=$(echo $CI_COMMIT_TAG| cut -d'.' -f 2)
+    - VERSION="${VERSION1}.${VERSION2}"
+    # Pull the private repository
+    - git init && git remote add origin $PRIVATE_REPOSITORY_URL_SSH
+    - git pull origin $VERSION
+    # Update and push build prod
+    - npm install
+    - npm run build-prod
+    - git status
+    - git add -f dist/
+    - git status
+    - git commit -m "Build prod for tag ${CI_COMMIT_TAG}"
+    - git show-ref
+    - git push origin HEAD:$VERSION
+    - git status
+    # Do the tag on the private repo
+    - git tag $CI_COMMIT_TAG
+    - git status
+    - git push origin --tags
 
 
 logs:
@@ -177,39 +209,39 @@ logs:
     - curl -v -H 'Content-Type:application/json' -H "X-Redmine-API-Key:$REDMINE_API_KEY" -d "$BODY" -X PUT https://forge.maarch.org/issues/$ISSUE_ID.json
 
 new_branch:
- image: debian:10-slim
- stage: new_branch
- only:
-   - branches
- before_script:
-   # Install git and curl command
-   - apt-get update -yqq > /dev/null
-   - apt install -y curl
-   - apt install -y jq
- script:
-   - chmod +x ./ci/create_mr.sh
-   - ./ci/create_mr.sh
+  image: debian:10-slim
+  stage: new_branch
+  only:
+    - branches
+  before_script:
+    # Install git and curl command
+    - apt-get update -yqq > /dev/null
+    - apt install -y curl
+    - apt install -y jq
+  script:
+    - chmod +x ./ci/create_mr.sh
+    - ./ci/create_mr.sh
 
 new_tag:
- image: debian:10-slim
- stage: new_tag
- only:
-   - tags
- before_script:
-   # Install git and curl command
-   - apt-get update -yqq > /dev/null
-   - 'which ssh-agent || ( apt-get install openssh-client -y )'
-   - eval $(ssh-agent -s)
-   - echo "$SSH_PRIVATE_KEY_2" | tr -d '\r' | ssh-add -
-   - mkdir -p ~/.ssh
-   - chmod 700 ~/.ssh
-   - ssh-keyscan "$GITLAB_URL" >> ~/.ssh/known_hosts
-   - chmod 644 ~/.ssh/known_hosts
-   - '[[ -f /.dockerenv ]] && echo -e "Host *\n\tStrictHostKeyChecking no\n\n" >> ~/.ssh/config'
-   - apt install -y git
-   - apt install -y curl
-   - apt install -y jq
- script:
-   - chmod +x ./ci/new_tag.sh
-   - ./ci/new_tag.sh
+  image: debian:10-slim
+  stage: new_tag
+  only:
+    - tags
+  before_script:
+    # Install git and curl command
+    - apt-get update -yqq > /dev/null
+    - 'which ssh-agent || ( apt-get install openssh-client -y )'
+    - eval $(ssh-agent -s)
+    - echo "$SSH_PRIVATE_KEY_2" | tr -d '\r' | ssh-add -
+    - mkdir -p ~/.ssh
+    - chmod 700 ~/.ssh
+    - ssh-keyscan "$GITLAB_URL" >> ~/.ssh/known_hosts
+    - chmod 644 ~/.ssh/known_hosts
+    - '[[ -f /.dockerenv ]] && echo -e "Host *\n\tStrictHostKeyChecking no\n\n" >> ~/.ssh/config'
+    - apt install -y git
+    - apt install -y curl
+    - apt install -y jq
+  script:
+    - chmod +x ./ci/new_tag.sh
+    - ./ci/new_tag.sh
 
diff --git a/lang/en.json b/lang/en.json
index 24daf0836aa17d2d4dbc721e9dcdac4a3ff3282c..b2147d5c4b1c9a3aa2a7e1e3cd431f63f6b51266 100755
--- a/lang/en.json
+++ b/lang/en.json
@@ -648,6 +648,11 @@
 		"credits": "Credits",
 		"allCredits": "And the whole Maarch community!",
 		"licence": "GNU GPLv3 license",
+        "groupsToManage": "Choose the authorized assignment groups",
+		"unlinkGroup": "Unlink group",
+        "emptyGroups": "No groups available to associate",
+        "emptyGroupUsers": "No users associated with this group",
+        "emptyUsers": "No users available to associate",
         "can_purgeAdmin": "Logically and physically remove interrupted or terminated signature processes",
         "purgeDocument": "Delete the document",
         "downloadProofOnPurge": "You may not view the document after it is removed; its complete proof folder will be suggested for download afterwards.",
diff --git a/lang/fr.json b/lang/fr.json
index df76d8c0677242fe78e0a4eec339e2ce36417e4e..3f36dde67682c66bf264475bcf15da9521514028 100755
--- a/lang/fr.json
+++ b/lang/fr.json
@@ -647,6 +647,11 @@
 		"credits": "Crédits",
 		"allCredits": "Et toute la communauté Maarch !",
 		"licence": "licence GNU GPLv3",
+		"groupsToManage": "Choisir les groupes d'affectations autorisés",
+		"unlinkGroup": "Dissocier le groupe",
+		"emptyGroups": "Aucun groupe disponible à associer",
+		"emptyGroupUsers": "Aucun utilisateur associé à ce groupe",
+		"emptyUsers": "Aucun utilisateur disponible à associer",
 		"can_purgeAdmin": "Supprimer logiquement les processus de signature interrompus ou terminés",
 		"purgeDocument": "Supprimer le document",
 		"downloadProofOnPurge": "Après suppression, le document ne sera plus consultable ; son dossier complet vous sera ensuite proposé au téléchargement.",
diff --git a/phpunit.xml b/phpunit.xml
index f062ec810c67a21709f308afaeb05b6273df07de..8648b358c763e5e56ae2432487ee1c87ec7cd8c4 100755
--- a/phpunit.xml
+++ b/phpunit.xml
@@ -1,5 +1,9 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" colors="true" bootstrap="test/unitTests/define.php" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd">
+  <php>
+    <server name='HTTP_HOST' value='http://localhost/MaarchParapheur' />
+    <server name='SERVER_PORT' value='80' />
+  </php>
   <coverage>
     <include>
       <directory suffix="Test.php">test</directory>
diff --git a/rest/index.php b/rest/index.php
index b8880bdf34d07abdfbfb09cea986ffe391111cc1..51c418e3c10316f7195e59bcba0dcc3556cd0835 100755
--- a/rest/index.php
+++ b/rest/index.php
@@ -145,6 +145,7 @@ $app->get('/users/{id}/history', \History\controllers\HistoryController::class .
 $app->post('/password', \User\controllers\UserController::class . ':forgotPassword');
 $app->put('/password', \User\controllers\UserController::class . ':updateForgottenPassword');
 $app->put('/users/{id}/accountActivationNotification', \User\controllers\UserController::class . ':sendAccountActivationNotification');
+$app->get('/manageableGroups', \User\controllers\UserController::class . ':getManageableGroupsOfCurrentUser');
 
 //Search
 $app->post('/search/documents', \Search\controllers\SearchController::class . ':getDocuments');
diff --git a/sql/data_fr.sql b/sql/data_fr.sql
index 58764fa9e239bc76f058e35adfbaa29a41470cde..be8d892e4bc3a1c100070747e162091ac358ba4e 100755
--- a/sql/data_fr.sql
+++ b/sql/data_fr.sql
@@ -22,24 +22,24 @@ TRUNCATE TABLE external_signatory_book;
 INSERT INTO external_signatory_book (id, label, type, connection_data, otp_code, message_content) VALUES (1, 'Signature externe Yousign', 'yousign', '{"apiKey": "a24291dc9d3475ec4fddded67df782b9", "apiUri": "https://staging-api.yousign.com"}', '["sms", "email"]', '{"otp_sms": "eSignature : votre code de sécurité pour confirmer la signature de vos documents est {{code}}", "notification": {"body": "<div>Bonjour&nbsp;<span class=\"mceNonEditable\"><tag data-tag-name=\"recipient.firstname\" data-tag-type=\"string\">Pr&eacute;nom du destinataire</tag></span>&nbsp;<span class=\"mceNonEditable\"><tag data-tag-name=\"recipient.lastname\" data-tag-type=\"string\">Nom du destinataire</tag></span>,<br /><br /></div>\n<div>Des documents mis &agrave; votre disposition requi&egrave;rent votre signature ou validation.<br /><br /></div>\n<div>Merci de cliquer sur le bouton ci-dessous pour les lire :<br />&nbsp;<span class=\"mceNonEditable\"><tag data-tag-name=\"url\" data-tag-type=\"button\" data-tag-title=\"Acc&eacute;dez aux documents\">Lien d''acc&egrave;s</tag></span>&nbsp;</div>\n<div>&nbsp;</div>", "subject": "Document à traiter"}}');
 
 TRUNCATE TABLE groups_privileges;
-INSERT INTO groups_privileges (id, group_id, privilege) VALUES (1, 1, 'manage_users');
-INSERT INTO groups_privileges (id, group_id, privilege) VALUES (2, 1, 'manage_documents');
-INSERT INTO groups_privileges (id, group_id, privilege) VALUES (3, 1, 'manage_email_configuration');
-INSERT INTO groups_privileges (id, group_id, privilege) VALUES (4, 1, 'manage_connections');
-INSERT INTO groups_privileges (id, group_id, privilege) VALUES (5, 1, 'manage_groups');
-INSERT INTO groups_privileges (id, group_id, privilege) VALUES (7, 2, 'manage_users');
-INSERT INTO groups_privileges (id, group_id, privilege) VALUES (8, 2, 'manage_documents');
-INSERT INTO groups_privileges (id, group_id, privilege) VALUES (9, 2, 'indexation');
-INSERT INTO groups_privileges (id, group_id, privilege) VALUES (10, 2, 'manage_groups');
-INSERT INTO groups_privileges (id, group_id, privilege) VALUES (11, 1, 'manage_history');
-INSERT INTO groups_privileges (id, group_id, privilege) VALUES (12, 1, 'manage_password_rules');
-INSERT INTO groups_privileges (id, group_id, privilege) VALUES (13, 4, 'indexation');
-INSERT INTO groups_privileges (id, group_id, privilege) VALUES (14, 5, 'manage_users');
-INSERT INTO groups_privileges (id, group_id, privilege) VALUES (15, 5, 'manage_documents');
-INSERT INTO groups_privileges (id, group_id, privilege) VALUES (17, 5, 'manage_history');
-INSERT INTO groups_privileges (id, group_id, privilege) VALUES (18, 1, 'indexation');
-INSERT INTO groups_privileges (id, group_id, privilege) VALUES (19, 1, 'manage_notifications');
-INSERT INTO groups_privileges (id, group_id, privilege) VALUES (20, 1, 'manage_customization');
+INSERT INTO groups_privileges (id, group_id, privilege, parameters) VALUES (1, 1, 'manage_users', '{}');
+INSERT INTO groups_privileges (id, group_id, privilege, parameters) VALUES (2, 1, 'manage_documents', '{}');
+INSERT INTO groups_privileges (id, group_id, privilege, parameters) VALUES (3, 1, 'manage_email_configuration', '{}');
+INSERT INTO groups_privileges (id, group_id, privilege, parameters) VALUES (4, 1, 'manage_connections', '{}');
+INSERT INTO groups_privileges (id, group_id, privilege, parameters) VALUES (5, 1, 'manage_groups', '{}');
+INSERT INTO groups_privileges (id, group_id, privilege, parameters) VALUES (7, 2, 'manage_users', '{}');
+INSERT INTO groups_privileges (id, group_id, privilege, parameters) VALUES (8, 2, 'manage_documents', '{}');
+INSERT INTO groups_privileges (id, group_id, privilege, parameters) VALUES (9, 2, 'indexation', '{}');
+INSERT INTO groups_privileges (id, group_id, privilege, parameters) VALUES (10, 2, 'manage_groups', '{}');
+INSERT INTO groups_privileges (id, group_id, privilege, parameters) VALUES (11, 1, 'manage_history', '{}');
+INSERT INTO groups_privileges (id, group_id, privilege, parameters) VALUES (12, 1, 'manage_password_rules', '{}');
+INSERT INTO groups_privileges (id, group_id, privilege, parameters) VALUES (13, 4, 'indexation', '{}');
+INSERT INTO groups_privileges (id, group_id, privilege, parameters) VALUES (14, 5, 'manage_users', '{}');
+INSERT INTO groups_privileges (id, group_id, privilege, parameters) VALUES (15, 5, 'manage_documents', '{}');
+INSERT INTO groups_privileges (id, group_id, privilege, parameters) VALUES (17, 5, 'manage_history', '{}');
+INSERT INTO groups_privileges (id, group_id, privilege, parameters) VALUES (18, 1, 'indexation', '{}');
+INSERT INTO groups_privileges (id, group_id, privilege, parameters) VALUES (19, 1, 'manage_notifications', '{}');
+INSERT INTO groups_privileges (id, group_id, privilege, parameters) VALUES (20, 1, 'manage_customization', '{}');
 
 
 TRUNCATE TABLE password_rules;
diff --git a/sql/structure.sql b/sql/structure.sql
index a54dccfc5e5cea27cc80ce128fc97be6faa074f9..a8ebd68a8c659196a614aad3a9dd99a4e6abaaef 100755
--- a/sql/structure.sql
+++ b/sql/structure.sql
@@ -127,6 +127,7 @@ CREATE TABLE groups_privileges
   id serial NOT NULL,
   group_id INTEGER NOT NULL,
   privilege character varying(128) NOT NULL,
+  parameters jsonb DEFAULT '{}' NOT NULL,
   CONSTRAINT groups_privileges_pkey PRIMARY KEY (id),
   CONSTRAINT groups_privileges_unique_key UNIQUE (group_id, privilege)
 )
diff --git a/src/app/email/controllers/EmailController.php b/src/app/email/controllers/EmailController.php
index 8601bbad15c93bf1ec57b8472437354d4dcca8dc..b4cc19f7e9b6cac8f25bc82c286efd26fa290a7d 100644
--- a/src/app/email/controllers/EmailController.php
+++ b/src/app/email/controllers/EmailController.php
@@ -132,10 +132,10 @@ class EmailController
         $email['cci']         = json_decode($email['cci']);
 
         $configuration = ConfigurationModel::getByIdentifier(['identifier' => 'emailServer', 'select' => ['value']]);
-        $configuration = json_decode($configuration[0]['value'], true);
         if (empty($configuration)) {
             return ['errors' => 'Configuration is missing'];
         }
+        $configuration = json_decode($configuration[0]['value'], true);
 
         $phpmailer = new PHPMailer();
         $phpmailer->setFrom($configuration['from']);
diff --git a/src/app/group/controllers/GroupController.php b/src/app/group/controllers/GroupController.php
index 122722e88a2c4a0f00c69c1a04be39b76beb66c7..4fa407b5ee4367b484fcede562f7e6423725459d 100755
--- a/src/app/group/controllers/GroupController.php
+++ b/src/app/group/controllers/GroupController.php
@@ -20,6 +20,7 @@ use History\controllers\HistoryController;
 use Respect\Validation\Validator;
 use Slim\Http\Request;
 use Slim\Http\Response;
+use User\controllers\UserController;
 use User\models\UserGroupModel;
 use User\models\UserModel;
 
@@ -56,13 +57,20 @@ class GroupController
             'select' => ['users.id', 'users.firstname', 'users.lastname']
         ]);
 
-        $groupPrivileges = GroupPrivilegeModel::getPrivilegesByGroupId(['groupId' => $args['id']]);
-        $groupPrivileges = array_column($groupPrivileges, 'privilege');
+        $groupPrivileges = GroupPrivilegeModel::getPrivilegesByGroupId([
+            'select' => ['privilege', 'parameters'],
+            'groupId' => $args['id']
+        ]);
+        $groupPrivileges = array_column($groupPrivileges, 'parameters', 'privilege');
+        $groupPrivileges = array_map(function ($parameters) {
+            return json_decode($parameters, 'true');
+        }, $groupPrivileges);
 
         $aPrivileges = PrivilegeController::PRIVILEGES;
         foreach ($aPrivileges as $key => $value) {
-            if (in_array($value['id'], $groupPrivileges)) {
+            if (array_key_exists($value['id'], $groupPrivileges)) {
                 $aPrivileges[$key]['checked'] = true;
+                $aPrivileges[$key]['parameters'] = $groupPrivileges[$value['id']];
             } else {
                 $aPrivileges[$key]['checked'] = false;
             }
@@ -172,7 +180,7 @@ class GroupController
         return $response->withStatus(204);
     }
 
-    public function updateGroupPrivilege(Request $request, Response $response, array $aArgs)
+    public function updateGroupPrivilege(Request $request, Response $response, array $args)
     {
         if (!PrivilegeController::hasPrivilege(['userId' => $GLOBALS['id'], 'privilege' => 'manage_groups'])) {
             return $response->withStatus(403)->withJson(['errors' => 'Privilege forbidden']);
@@ -182,73 +190,106 @@ class GroupController
 
         if (empty($body)) {
             return $response->withStatus(400)->withJson(['errors' => 'Body is not set or empty']);
-        } elseif (!Validator::stringType()->notEmpty()->validate($aArgs['privilegeId'])) {
+        } elseif (!Validator::stringType()->notEmpty()->validate($args['privilegeId'])) {
             return $response->withStatus(400)->withJson(['errors' => 'privilegeId is empty']);
         } elseif (!Validator::boolType()->validate($body['checked'])) {
             return $response->withStatus(400)->withJson(['errors' => 'Body checked is empty']);
-        } elseif (!Validator::intVal()->notEmpty()->validate($aArgs['id'])) {
+        } elseif (!Validator::intVal()->notEmpty()->validate($args['id'])) {
             return $response->withStatus(400)->withJson(['errors' => 'Id must be an integer']);
         }
 
-        $group = GroupModel::getById(['id' => $aArgs['id']]);
+        $group = GroupModel::getById(['id' => $args['id']]);
         if (empty($group)) {
             return $response->withStatus(400)->withJson(['errors' => 'Group not found']);
         }
 
-        if ($body['checked'] === true && !empty(GroupPrivilegeModel::getPrivileges(['select' => [1], 'where' => ['privilege = ?', 'group_id = ?'], 'data' => [$aArgs['privilegeId'], $aArgs['id']]]))) {
+        $parameters = $body['parameters'] ?? [];
+        $parametersJson = empty($parameters) ? '{}' : json_encode($parameters);
+
+        $privilege = GroupPrivilegeModel::getPrivileges([
+            'select' => ['parameters'],
+            'where' => [
+                'privilege = ?',
+                'group_id = ?'
+            ],
+            'data' => [
+                $args['privilegeId'],
+                $args['id']
+            ]
+        ]);
+        $privilege = $privilege[0] ?? null;
+
+        if ($body['checked'] === true && !empty($privilege) && $privilege['parameters'] == $parametersJson) {
             return $response->withStatus(400)->withJson(['errors' => 'Privilege is already linked to this group']);
         }
 
         if ($body['checked']) {
-            GroupPrivilegeModel::addPrivilege(['groupId' => $aArgs['id'], 'privilegeId' => $aArgs['privilegeId']]);
+            if (!PrivilegeController::hasRightByPrivilege(['userId' => $GLOBALS['id'], 'groupId' => $args['id'], 'privilegeId' => $args['privilegeId'], 'parameters' => $parameters])) {
+                return $response->withStatus(400)->withJson(['errors' => 'Privilege not allowed with these parameters']);
+            }
+            if (empty($privilege)) {
+                GroupPrivilegeModel::addPrivilege(['groupId' => $args['id'], 'privilegeId' => $args['privilegeId']]);
+            }
+            GroupPrivilegeModel::updateParameters(['groupId' => $args['id'], 'privilegeId' => $args['privilegeId'], 'parameters' => $parametersJson]);
         } else {
-            GroupPrivilegeModel::deletePrivilege(['groupId' => $aArgs['id'], 'privilegeId' => $aArgs['privilegeId']]);
+            GroupPrivilegeModel::deletePrivilege(['groupId' => $args['id'], 'privilegeId' => $args['privilegeId']]);
         }
 
         HistoryController::add([
             'code'       => 'OK',
             'objectType' => 'groups',
-            'objectId'   => $aArgs['id'],
+            'objectId'   => $args['id'],
             'type'       => 'MODIFICATION',
-            'message'    => "{privilegeUpdated} : {$aArgs['privilegeId']}"
+            'message'    => "{privilegeUpdated} : {$args['privilegeId']}"
         ]);
 
         return $response->withStatus(204);
     }
 
-    public function addUser(Request $request, Response $response, array $aArgs)
+    public function addUser(Request $request, Response $response, array $args)
     {
-        if (!PrivilegeController::hasPrivilege(['userId' => $GLOBALS['id'], 'privilege' => 'manage_groups']) && !PrivilegeController::hasPrivilege(['userId' => $GLOBALS['id'], 'privilege' => 'manage_users'])) {
-            return $response->withStatus(403)->withJson(['errors' => 'Privilege forbidden']);
-        }
-
         $body = $request->getParsedBody();
-        if (!Validator::intVal()->notEmpty()->validate($aArgs['id'])) {
+        if (empty($args['id']) || !Validator::intVal()->validate($args['id'])) {
             return $response->withStatus(400)->withJson(['errors' => 'Id must be an integer']);
-        } elseif (!Validator::intVal()->notEmpty()->validate($body['userId'])) {
-            return $response->withStatus(400)->withJson(['errors' => 'userId must be an integer']);
         }
 
-        $group = GroupModel::getById(['id' => $aArgs['id']]);
+        $hasGroupPrivilege = PrivilegeController::hasPrivilege(['userId' => $GLOBALS['id'], 'privilege' => 'manage_groups']);
+        $hasUserPrivilege = false;
+
+        $manageableGroups = array_column(UserController::getManageableGroups(['userId' => $GLOBALS['id']]), 'id');
+        $targetUserGroups = UserGroupModel::get([
+            'select' => ['group_id'],
+            'where'  => ['user_id = ?'],
+            'data'   => [$args['userId']]
+        ]);
+        $targetUserGroups = array_column($targetUserGroups, 'group_id');
+        if (in_array($args['id'], $manageableGroups) && in_array($args['id'], $targetUserGroups)) {
+            $hasUserPrivilege = true;
+        }
+        if (!$hasGroupPrivilege && !$hasUserPrivilege) {
+            return $response->withStatus(403)->withJson(['errors' => 'Privilege forbidden']);
+        }
+
+        $group = GroupModel::getById(['id' => $args['id']]);
         $user  = UserModel::getById(['id' => $body['userId'], 'select' => ['firstname', 'lastname']]);
         
         if (empty($group)) {
             return $response->withStatus(400)->withJson(['errors' => 'Group not found']);
         } elseif (empty($user)) {
             return $response->withStatus(400)->withJson(['errors' => 'User not found']);
-        } elseif (UserGroupModel::hasGroup(['groupId' => $aArgs['id'], 'userId' => $body['userId']])) {
+        } elseif (UserGroupModel::hasGroup(['groupId' => $args['id'], 'userId' => $body['userId']])) {
             return $response->withStatus(400)->withJson(['errors' => 'This user already has this group']);
         }
 
         UserGroupModel::addUser([
-            'groupId' => $aArgs['id'],
+            'groupId' => $args['id'],
             'userId'  => $body['userId']
         ]);
 
         HistoryController::add([
             'code'          => 'OK',
             'objectType'    => 'groups',
-            'objectId'      => $aArgs['id'],
+            'objectId'      => $args['id'],
             'type'          => 'MODIFICATION',
             'message'       => "{userAddedToGroup} : {$user['firstname']} {$user['lastname']}"
         ]);
@@ -264,20 +305,18 @@ class GroupController
         return $response->withStatus(204);
     }
 
-    public function removeUser(Request $request, Response $response, array $aArgs)
+    public function removeUser(Request $request, Response $response, array $args)
     {
-        if (!PrivilegeController::hasPrivilege(['userId' => $GLOBALS['id'], 'privilege' => 'manage_groups']) && !PrivilegeController::hasPrivilege(['userId' => $GLOBALS['id'], 'privilege' => 'manage_users'])) {
-            return $response->withStatus(403)->withJson(['errors' => 'Privilege forbidden']);
-        }
-
-        if (!Validator::intVal()->notEmpty()->validate($aArgs['id'])) {
+        if (empty($args['id']) || !Validator::intVal()->validate($args['id'])) {
             return $response->withStatus(400)->withJson(['errors' => 'Id must be an integer']);
-        } elseif (!Validator::intVal()->notEmpty()->validate($aArgs['userId'])) {
+        } elseif (empty($args['userId']) || !Validator::intVal()->validate($args['userId'])) {
             return $response->withStatus(400)->withJson(['errors' => 'userId must be an integer']);
+        } elseif (!UserController::hasRightByUserId(['activeUserId' => $GLOBALS['id'], 'targetUserId' => $args['userId'], 'targetGroupId' => $args['id']])) {
+            return $response->withStatus(403)->withJson(['errors' => 'Privilege forbidden']);
         }
 
-        $group = GroupModel::getById(['id' => $aArgs['id']]);
-        $user  = UserModel::getById(['id' => $aArgs['userId'], 'select' => ['firstname', 'lastname']]);
+        $group = GroupModel::getById(['id' => $args['id']]);
+        $user  = UserModel::getById(['id' => $args['userId'], 'select' => ['firstname', 'lastname']]);
         
         if (empty($group)) {
             return $response->withStatus(400)->withJson(['errors' => 'Group not found']);
@@ -286,14 +325,14 @@ class GroupController
         }
 
         UserGroupModel::removeUser([
-            'groupId' => $aArgs['id'],
-            'userId'  => $aArgs['userId']
+            'groupId' => $args['id'],
+            'userId'  => $args['userId']
         ]);
 
         HistoryController::add([
             'code'          => 'OK',
             'objectType'    => 'groups',
-            'objectId'      => $aArgs['id'],
+            'objectId'      => $args['id'],
             'type'          => 'MODIFICATION',
             'message'       => "{removedFromGroup} : {$user['firstname']} {$user['lastname']}"
         ]);
@@ -301,7 +340,7 @@ class GroupController
         HistoryController::add([
             'code'          => 'OK',
             'objectType'    => 'users',
-            'objectId'      => $aArgs['userId'],
+            'objectId'      => $args['userId'],
             'type'          => 'MODIFICATION',
             'message'       => "{removedFromGroup} : {$group['label']}"
         ]);
diff --git a/src/app/group/controllers/PrivilegeController.php b/src/app/group/controllers/PrivilegeController.php
index c02ae36e50fedd278b113f050ccc7ca171094cf1..ca2773ec5c5df691396cd51808f0955690ca9419 100755
--- a/src/app/group/controllers/PrivilegeController.php
+++ b/src/app/group/controllers/PrivilegeController.php
@@ -17,6 +17,7 @@ namespace Group\controllers;
 use SrcCore\models\ValidatorModel;
 use User\models\UserGroupModel;
 use Group\models\GroupPrivilegeModel;
+use User\controllers\UserController;
 
 class PrivilegeController
 {
@@ -79,4 +80,30 @@ class PrivilegeController
 
         return false;
     }
+
+    public static function hasRightByPrivilege(array $args)
+    {
+        ValidatorModel::notEmpty($args, ['userId', 'groupId', 'privilegeId']);
+        ValidatorModel::intVal($args, ['userId', 'groupId']);
+        ValidatorModel::stringType($args, ['privilegeId']);
+
+        if ($args['privilegeId'] == 'manage_users') {
+            if (empty($args['readOnly']) && !isset($args['parameters']['authorized'])) {
+                return false;
+            }
+            if (PrivilegeController::hasPrivilege(['userId' => $args['userId'], 'privilege' => 'manage_groups'])) {
+                return true;
+            } elseif (!PrivilegeController::hasPrivilege(['userId' => $args['userId'], 'privilege' => 'manage_users'])) {
+                return false;
+            } else {
+                $candidateGroups = $args['parameters']['authorized'] ?? [];
+                $manageableGroups = array_column(UserController::getManageableGroups(['userId' => $args['userId']]), 'id');
+                if (!empty(array_diff($candidateGroups, $manageableGroups))) {
+                    return false;
+                }
+            }
+        }
+
+        return true;
+    }
 }
diff --git a/src/app/group/models/GroupPrivilegeModel.php b/src/app/group/models/GroupPrivilegeModel.php
index e831de8c3eaf58af793e2ab646887f195b01a0bf..0b7698485e735b2377ee2e11c43c0d7459af7313 100755
--- a/src/app/group/models/GroupPrivilegeModel.php
+++ b/src/app/group/models/GroupPrivilegeModel.php
@@ -95,4 +95,20 @@ class GroupPrivilegeModel
 
         return true;
     }
+
+    public static function updateParameters(array $args)
+    {
+        ValidatorModel::notEmpty($args, ['groupId', 'privilegeId']);
+        ValidatorModel::intVal($args, ['groupId']);
+        ValidatorModel::stringType($args, ['privilegeId', 'parameters']);
+
+        DatabaseModel::update([
+            'table' => 'groups_privileges',
+            'where' => ['group_id = ?', 'privilege = ?'],
+            'data'  => [$args['groupId'], $args['privilegeId']],
+            'set'   => ['parameters' => $args['parameters']]
+        ]);
+
+        return true;
+    }
 }
diff --git a/src/app/history/controllers/HistoryController.php b/src/app/history/controllers/HistoryController.php
index 6ea5807dc1147068d7a72b33f18781603bf3c23b..a7df7fa2e01c321ccf9e54a0bb2c55f8e36f2557 100644
--- a/src/app/history/controllers/HistoryController.php
+++ b/src/app/history/controllers/HistoryController.php
@@ -38,6 +38,7 @@ use SrcCore\models\PasswordModel;
 use SrcCore\models\TextFormatModel;
 use SrcCore\models\ValidatorModel;
 use User\models\UserModel;
+use User\controllers\UserController;
 use Workflow\controllers\YousignController;
 use Workflow\models\WorkflowExternalInformationModel;
 use Workflow\models\WorkflowModel;
@@ -674,6 +675,9 @@ class HistoryController
         if ($GLOBALS['id'] != $args['id'] && !PrivilegeController::hasPrivilege(['userId' => $GLOBALS['id'], 'privilege' => 'manage_users'])) {
             return $response->withStatus(403)->withJson(['errors' => 'Privilege forbidden']);
         }
+        if (!UserController::hasRightByUserId(['activeUserId' => $GLOBALS['id'], 'targetUserId' => $args['id']])) {
+            return $response->withStatus(403)->withJson(['errors' => 'Privilege forbidden']);
+        }
 
         if (!Validator::intVal()->notEmpty()->validate($args['id'])) {
             return $response->withStatus(400)->withJson(['errors' => 'Route id is not an integer']);
diff --git a/src/app/user/controllers/SignatureController.php b/src/app/user/controllers/SignatureController.php
index c8c15d4bcde07fd74fa4d34ea7696a2316138ff1..060b20e2dfe28ea44afb8a2a2fc0883a51bc0388 100755
--- a/src/app/user/controllers/SignatureController.php
+++ b/src/app/user/controllers/SignatureController.php
@@ -25,6 +25,7 @@ use SrcCore\models\CoreConfigModel;
 use SrcCore\models\ValidatorModel;
 use User\models\SignatureModel;
 use User\models\UserModel;
+use User\controllers\UserController;
 
 class SignatureController
 {
@@ -77,6 +78,9 @@ class SignatureController
         if ($GLOBALS['id'] != $args['id'] && !PrivilegeController::hasPrivilege(['userId' => $GLOBALS['id'], 'privilege' => 'manage_users'])) {
             return $response->withStatus(403)->withJson(['errors' => 'Privilege forbidden']);
         }
+        if (!UserController::hasRightByUserId(['activeUserId' => $GLOBALS['id'], 'targetUserId' => $args['id']])) {
+            return $response->withStatus(403)->withJson(['errors' => 'Privilege forbidden']);
+        }
 
         $body = $request->getParsedBody();
 
@@ -132,6 +136,9 @@ class SignatureController
         if ($GLOBALS['id'] != $args['id'] && !PrivilegeController::hasPrivilege(['userId' => $GLOBALS['id'], 'privilege' => 'manage_users'])) {
             return $response->withStatus(403)->withJson(['errors' => 'Privilege forbidden']);
         }
+        if (!UserController::hasRightByUserId(['activeUserId' => $GLOBALS['id'], 'targetUserId' => $args['id']])) {
+            return $response->withStatus(403)->withJson(['errors' => 'Privilege forbidden']);
+        }
 
         SignatureModel::delete(['where' => ['user_id = ?', 'id = ?'], 'data' => [$args['id'], $args['signatureId']]]);
 
@@ -152,6 +159,9 @@ class SignatureController
         if ($GLOBALS['id'] != $args['id'] && !PrivilegeController::hasPrivilege(['userId' => $GLOBALS['id'], 'privilege' => 'manage_users'])) {
             return $response->withStatus(403)->withJson(['errors' => 'Privilege forbidden']);
         }
+        if (!UserController::hasRightByUserId(['activeUserId' => $GLOBALS['id'], 'targetUserId' => $args['id']])) {
+            return $response->withStatus(403)->withJson(['errors' => 'Privilege forbidden']);
+        }
 
         $user = UserModel::getById(['select' => [1], 'id' => $args['id']]);
         if (empty($user)) {
@@ -212,6 +222,9 @@ class SignatureController
         if ($GLOBALS['id'] != $args['id'] && !PrivilegeController::hasPrivilege(['userId' => $GLOBALS['id'], 'privilege' => 'manage_users'])) {
             return $response->withStatus(403)->withJson(['errors' => 'Privilege forbidden']);
         }
+        if (!UserController::hasRightByUserId(['activeUserId' => $GLOBALS['id'], 'targetUserId' => $args['id']])) {
+            return $response->withStatus(403)->withJson(['errors' => 'Privilege forbidden']);
+        }
 
         $user = UserModel::getById(['select' => [1], 'id' => $args['id']]);
         if (empty($user)) {
diff --git a/src/app/user/controllers/UserController.php b/src/app/user/controllers/UserController.php
index b24c49658fc38943e7e7567ebde6fcc1ac0f416b..ed9398ada46fd0c28a6943bd3b2d84a1aa0aa170 100755
--- a/src/app/user/controllers/UserController.php
+++ b/src/app/user/controllers/UserController.php
@@ -21,6 +21,7 @@ use Email\controllers\EmailController;
 use Firebase\JWT\JWT;
 use Group\controllers\PrivilegeController;
 use Group\models\GroupModel;
+use Group\models\GroupPrivilegeModel;
 use History\controllers\HistoryController;
 use Respect\Validation\Validator;
 use Slim\Http\Request;
@@ -28,7 +29,6 @@ use Slim\Http\Response;
 use SrcCore\controllers\AuthenticationController;
 use SrcCore\controllers\LanguageController;
 use SrcCore\controllers\PasswordController;
-use SrcCore\controllers\UrlController;
 use SrcCore\models\AuthenticationModel;
 use SrcCore\models\CoreConfigModel;
 use SrcCore\models\PasswordModel;
@@ -66,6 +66,10 @@ class UserController
         ]);
 
         $currentUser = UserModel::getById(['select' => ['"isRest"'], 'id' => $GLOBALS['id']]);
+        $manageableGroups = UserController::getManageableGroups(['userId' => $GLOBALS['id']]);
+        $manageableGroups = array_column($manageableGroups, 'id');
+        $allGroupLabels = GroupModel::get(['select' => ['label', 'id']]);
+        $allUsersGroups = UserGroupModel::get(['select' => ['user_id', 'group_id']]);
 
         foreach ($users as $key => $user) {
             $users[$key]['substitute'] = !empty($user['substitute']);
@@ -73,9 +77,27 @@ class UserController
                 $users[$key]['x509Fingerprint'] = $users[$key]['x509_fingerprint'];
             }
             unset($users[$key]['x509_fingerprint']);
+
+            $users[$key]['groups'] = [];
+            $groupsIds = array_column(array_filter($allUsersGroups, function ($userGroup) use ($user) {
+                return $userGroup['user_id'] == $user['id'];
+            }), 'group_id');
+            $actuallyAlone = false;
+            if (empty($groupsIds)) {
+                $actuallyAlone = true;
+            } elseif ($GLOBALS['id'] != $user['id']) {
+                $groupsIds = array_values(array_intersect($groupsIds, $manageableGroups));
+            }
+            if (!empty($groupsIds)) {
+                $users[$key]['groups'] = array_filter($allGroupLabels, function ($group) use ($groupsIds) {
+                    return in_array($group['id'], $groupsIds);
+                });
+            } elseif (!$actuallyAlone) {
+                unset($users[$key]);
+            }
         }
 
-        return $response->withJson(['users' => $users]);
+        return $response->withJson(['users' => array_values($users)]);
     }
 
     public function getById(Request $request, Response $response, array $args)
@@ -94,14 +116,22 @@ class UserController
             return $response->withStatus(400)->withJson(['errors' => 'User does not exist']);
         }
 
-        if ($GLOBALS['id'] == $args['id'] || PrivilegeController::hasPrivilege(['userId' => $GLOBALS['id'], 'privilege' => 'manage_users'])) {
-            $user['groups'] = [];
-            $userGroups = UserGroupModel::get(['select' => ['group_id'], 'where' => ['user_id = ?'], 'data' => [$args['id']]]);
-            $groupsIds  = array_column($userGroups, 'group_id');
-            if (!empty($groupsIds)) {
-                $groups = GroupModel::get(['select' => ['label'], 'where' => ['id in (?)'], 'data' => [$groupsIds]]);
-                $user['groups'] = $groups;
-            }
+        $user['groups'] = [];
+        $userGroups = UserGroupModel::get(['select' => ['group_id'], 'where' => ['user_id = ?'], 'data' => [$args['id']]]);
+        $groupsIds  = array_column($userGroups, 'group_id');
+
+        $actuallyAlone = false;
+        if (empty($groupsIds)) {
+            $actuallyAlone = true;
+        } elseif ($GLOBALS['id'] != $args['id']) {
+            $groupsIds = array_values(array_intersect($groupsIds, array_column(UserController::getManageableGroups(['userId' => $GLOBALS['id']]), 'id')));
+        }
+
+        if (!empty($groupsIds)) {
+            $groups = GroupModel::get(['select' => ['label', 'id'], 'where' => ['id in (?)'], 'data' => [$groupsIds]]);
+            $user['groups'] = $groups;
+        } elseif (!$actuallyAlone) {
+            return $response->withStatus(403)->withJson(['errors' => 'User out of perimeter']);
         }
 
         HistoryController::add([
@@ -135,15 +165,25 @@ class UserController
             return $response->withStatus(400)->withJson(['errors' => 'Body email is empty or not a valid email']);
         } elseif (!empty($body['x509Fingerprint']) && !Validator::stringType()->validate($body['x509Fingerprint'])) {
             return $response->withStatus(400)->withJson(['errors' => 'Body x509Fingerprint is not a string']);
+        } elseif (!empty($body['groups']) && !Validator::arrayType()->validate($body['groups'])) {
+            return $response->withStatus(400)->withJson(['errors' => 'Body groups is not an array']);
         }
 
+        $body['groups'] = !empty($body['groups']) ? array_column($body['groups'], 'id') : [];
+        $manageableGroups = array_column(UserController::getManageableGroups(['userId' => $GLOBALS['id']]), 'id');
+        $body['groups'] = array_values(array_intersect($body['groups'], $manageableGroups));
+
         $body['login'] = strtolower($body['login']);
         $existingUser = UserModel::getByLogin(['login' => $body['login'], 'select' => [1]]);
         if (!empty($existingUser)) {
             return $response->withStatus(400)->withJson(['errors' => 'Login already exists', 'lang' => 'userLoginAlreadyExists']);
         }
 
-        $body['x509_fingerprint'] = $body['x509Fingerprint'];
+        $body['x509_fingerprint'] = !empty($body['x509Fingerprint']) ? $body['x509Fingerprint'] : null;
+
+        if (empty($body['phone'])) {
+            $body['phone'] = null;
+        }
 
         if (!empty($body['isRest'])) {
             $body['"isRest"'] = true;
@@ -189,7 +229,7 @@ class UserController
     public function update(Request $request, Response $response, array $args)
     {
         $connection = ConfigurationModel::getConnection();
-        if (($GLOBALS['id'] != $args['id'] || $connection != 'default') && !PrivilegeController::hasPrivilege(['userId' => $GLOBALS['id'], 'privilege' => 'manage_users'])) {
+        if (($GLOBALS['id'] != $args['id'] || $connection != 'default') && !UserController::hasRightByUserId(['activeUserId' => $GLOBALS['id'], 'targetUserId' => $args['id']])) {
             return $response->withStatus(403)->withJson(['errors' => 'Privilege forbidden']);
         }
 
@@ -205,6 +245,8 @@ class UserController
             return $response->withStatus(400)->withJson(['errors' => 'Body email is empty or not a valid email']);
         } elseif (!empty($body['x509Fingerprint']) && !Validator::stringType()->validate($body['x509Fingerprint'])) {
             return $response->withStatus(400)->withJson(['errors' => 'Body x509Fingerprint is not a string']);
+        } elseif (!Validator::arrayType()->each(Validator::intType())->validate($body['groups'])) {
+            return $response->withStatus(400)->withJson(['errors' => 'Body groups is not an array of integers']);
         }
 
         $user = UserModel::getById(['id' => $args['id'], 'select' => [1]]);
@@ -216,7 +258,6 @@ class UserController
             'firstname'       => $body['firstname'],
             'lastname'        => $body['lastname'],
             'email'           => $body['email'],
-            'phone'           => $body['phone'],
             'signature_modes' => []
         ];
 
@@ -266,6 +307,30 @@ class UserController
             'data'  => [$args['id']]
         ]);
 
+        if (!empty($body['groups']) && Validator::arrayType()->each(Validator::intType())->validate($body['groups'])) {
+            /**
+             * Alice wants to edit Bob’s groups.
+             *
+             * B: $body[groups]: groups Alice wants to apply to Bob
+             * M: $GLOBALS[id] manageable groups: groups Alice has right on
+             * C: current groups: Bob’s current groups
+             *
+             * given these, Bob’s new groups are the groups Alice asked for,
+             * plus “Bob’s current groups except groups Alice has right on”:
+             *
+             * B union (C - M)
+             */
+            $targetCurrentGroups = UserGroupModel::get([
+                'select' => ['group_id'],
+                'where'  => ['user_id = ?'],
+                'data'   => [$args['id']]
+            ]);
+            $targetCurrentGroups = !empty($targetCurrentGroups) ? array_column($targetCurrentGroups, 'group_id') : [];
+            $manageableGroups = array_column(UserController::getManageableGroups(['userId' => $GLOBALS['id']]), 'id');
+            $appliedGroups = array_unique(array_merge($body['groups'], array_diff($targetCurrentGroups, $manageableGroups)));
+            UserGroupModel::setUserGroups(['userId' => $args['id'], 'groups' => $appliedGroups]);
+        }
+
         HistoryController::add([
             'code'          => 'OK',
             'objectType'    => 'users',
@@ -339,7 +404,7 @@ class UserController
 
     public function delete(Request $request, Response $response, array $args)
     {
-        if (!PrivilegeController::hasPrivilege(['userId' => $GLOBALS['id'], 'privilege' => 'manage_users']) || $GLOBALS['id'] == $args['id']) {
+        if ($GLOBALS['id'] == $args['id'] || !UserController::hasRightByUserId(['activeUserId' => $GLOBALS['id'], 'targetUserId' => $args['id']])) {
             return $response->withStatus(403)->withJson(['errors' => 'Privilege forbidden']);
         }
 
@@ -444,7 +509,7 @@ class UserController
             return $response->withStatus(400)->withJson(['errors' => 'Route id is not an integer']);
         }
 
-        if (!PrivilegeController::hasPrivilege(['userId' => $GLOBALS['id'], 'privilege' => 'manage_users']) && $GLOBALS['id'] != $args['id']) {
+        if ($GLOBALS['id'] != $args['id'] && !UserController::hasRightByUserId(['activeUserId' => $GLOBALS['id'], 'targetUserId' => $args['id']])) {
             return $response->withStatus(403)->withJson(['errors' => 'Privilege forbidden']);
         }
 
@@ -591,7 +656,7 @@ class UserController
         }
 
         if ($GLOBALS['id'] != $args['id']) {
-            if (!PrivilegeController::hasPrivilege(['userId' => $GLOBALS['id'], 'privilege' => 'manage_users'])) {
+            if (!UserController::hasRightByUserId(['activeUserId' => $GLOBALS['id'], 'targetUserId' => $args['id']])) {
                 return $response->withStatus(403)->withJson(['errors' => 'Privilege forbidden']);
             }
         }
@@ -740,12 +805,12 @@ class UserController
 
     public function sendAccountActivationNotification(Request $request, Response $response, array $args)
     {
-        if (!PrivilegeController::hasPrivilege(['userId' => $GLOBALS['id'], 'privilege' => 'manage_users'])) {
-            return $response->withStatus(403)->withJson(['errors' => 'Privilege forbidden']);
+        if (empty($args['id']) || !Validator::intVal()->validate($args['id'])) {
+            return $response->withStatus(400)->withJson(['errors' => 'Route id is not an integer']);
         }
 
-        if (!Validator::intVal()->notEmpty()->validate($args['id'])) {
-            return $response->withStatus(400)->withJson(['errors' => 'Route id is not an integer']);
+        if (!UserController::hasRightByUserId(['activeUserId' => $GLOBALS['id'], 'targetUserId' => $args['id']])) {
+            return $response->withStatus(403)->withJson(['errors' => 'Privilege forbidden']);
         }
 
         $connection = ConfigurationModel::getConnection();
@@ -760,6 +825,17 @@ class UserController
         return $response->withStatus(204);
     }
 
+    public function getManageableGroupsOfCurrentUser(Request $request, Response $response)
+    {
+        if (!PrivilegeController::hasPrivilege(['userId' => $GLOBALS['id'], 'privilege' => 'manage_users'])) {
+            return $response->withStatus(403)->withJson(['errors' => 'Privilege forbidden']);
+        }
+
+        $manageableGroups = UserController::getManageableGroups(['userId' => $GLOBALS['id']]);
+
+        return $response->withJson(['groups' => $manageableGroups]);
+    }
+
     public static function getUserInformationsById(array $args)
     {
         ValidatorModel::notEmpty($args, ['id']);
@@ -802,4 +878,75 @@ class UserController
 
         return $user;
     }
+
+    public static function getManageableGroups(array $args)
+    {
+        ValidatorModel::notEmpty($args, ['userId']);
+        ValidatorModel::intVal($args, ['userId']);
+
+        if (PrivilegeController::hasPrivilege(['userId' => $args['userId'], 'privilege' => 'manage_groups'])) {
+            $groups = GroupModel::get(['select' => ['id']]);
+            $groups = array_column($groups, 'id');
+            return GroupModel::get();
+        }
+
+        $groups = UserGroupModel::get(['select' => ['group_id'], 'where' => ['user_id = ?'], 'data' => [$args['userId']]]);
+
+        $manageableGroups = [];
+        foreach ($groups as $group) {
+            $privilege = GroupPrivilegeModel::getPrivileges([
+                'select' => ['parameters'],
+                'where'  =>  ['group_id = ?', 'privilege = ?'],
+                'data'   => [$group['group_id'], 'manage_users']
+            ]);
+            $parameters = empty($privilege[0]['parameters']) ? [] : json_decode($privilege[0]['parameters'], true);
+            $currentGroups = $parameters['authorized'] ?? [];
+            $manageableGroups = array_merge($manageableGroups, $currentGroups);
+        }
+        $manageableGroups = array_unique($manageableGroups);
+
+        if (empty($manageableGroups)) {
+            return [];
+        }
+
+        $manageableGroups = GroupModel::get([
+            'where' => ['id in (?)'],
+            'data'  => [$manageableGroups]
+        ]);
+
+        return $manageableGroups;
+    }
+
+    /**
+     * hasRightByUserId returns whether activeUser has right on targetUser,
+     * with an optional focus targetGroup
+     *
+     * @return bool
+     */
+    public static function hasRightByUserId(array $args)
+    {
+        ValidatorModel::notEmpty($args, ['activeUserId', 'targetUserId']);
+        ValidatorModel::intVal($args, ['activeUserId', 'targetUserId', 'targetGroupId']);
+
+        $activeUserManageableGroups = array_column(UserController::getManageableGroups(['userId' => $args['activeUserId']]), 'id');
+        if (empty($activeUserManageableGroups)) {
+            return false;
+        }
+
+        $targetUserGroups = array_column(UserGroupModel::get([
+            'select' => ['group_id'],
+            'where'  => ['user_id = ?'],
+            'data'   => [$args['targetUserId']]
+        ]), 'group_id');
+        if (empty($targetUserGroups)) {
+            return true;
+        }
+
+        $groupsIntersection = array_intersect($targetUserGroups, $activeUserManageableGroups);
+        if (empty($args['targetGroupId'])) {
+            return $args['activeUserId'] == $args['targetUserId'] || !empty($groupsIntersection);
+        }
+
+        return in_array($args['targetGroupId'], $groupsIntersection);
+    }
 }
diff --git a/src/app/user/models/UserGroupModel.php b/src/app/user/models/UserGroupModel.php
index 9fefc0be3121cf865c526228bf43102fd2bd862f..b0a9c87b925c5b872cd0dc97db75227525f332b4 100755
--- a/src/app/user/models/UserGroupModel.php
+++ b/src/app/user/models/UserGroupModel.php
@@ -117,4 +117,30 @@ class UserGroupModel
 
         return true;
     }
+
+    public static function setUserGroups(array $args) {
+        ValidatorModel::notEmpty($args, ['userId']);
+        ValidatorModel::intVal($args, ['userId']);
+        ValidatorModel::arrayType($args, ['groups']);
+
+        $currentGroups = UserGroupModel::get([
+            'select' => ['group_id'],
+            'where'  => ['user_id = ?'],
+            'data'   => [$args['userId']]
+        ]);
+        $currentGroups = array_column($currentGroups, 'group_id');
+
+        foreach ($currentGroups as $key => $currentGroup) {
+            if (!in_array($currentGroup, $args['groups'])) {
+                UserGroupModel::removeUser(['userId' => $args['userId'], 'groupId' => $currentGroup]);
+                unset($currentGroups[$key]);
+            }
+        }
+
+        foreach ($args['groups'] as $newGroup) {
+            if (!in_array($newGroup, $currentGroups)) {
+                UserGroupModel::addUser(['userId' => $args['userId'], 'groupId' => $newGroup]);
+            }
+        }
+    }
 }
diff --git a/src/app/user/models/UserModel.php b/src/app/user/models/UserModel.php
index 42ca5c83f34afc8a855c464f5c1b026d688be060..8cef89e10253b8a781bf7a5f12f832544eb70b92 100755
--- a/src/app/user/models/UserModel.php
+++ b/src/app/user/models/UserModel.php
@@ -14,6 +14,7 @@
 
 namespace User\models;
 
+use PHPUnit\Util\Xml\Validator;
 use SrcCore\models\AuthenticationModel;
 use SrcCore\models\DatabaseModel;
 use SrcCore\models\ValidatorModel;
@@ -81,6 +82,7 @@ class UserModel
     {
         ValidatorModel::notEmpty($args, ['login', 'email', 'firstname', 'lastname', 'picture']);
         ValidatorModel::stringType($args, ['login', 'email', 'firstname', 'lastname', 'phone', 'picture', 'mode', 'signatureModes', 'x509_fingerprint']);
+        ValidatorModel::arrayType($args, ['groups']);
 
         if (empty($args['password'])) {
             $args['password'] = AuthenticationModel::generatePassword();
@@ -101,10 +103,12 @@ class UserModel
                 'picture'                       => $args['picture'],
                 'password_modification_date'    => 'CURRENT_TIMESTAMP',
                 'signature_modes'               => $args['signatureModes'],
-                'x509_fingerprint'              => $args['x509_fingerprint'],
+                'x509_fingerprint'              => $args['x509_fingerprint']
             ]
         ]);
 
+        UserGroupModel::setUserGroups(['userId' => $nextSequenceId, 'groups' => $args['groups']]);
+
         return $nextSequenceId;
     }
 
diff --git a/src/core/controllers/AuthenticationController.php b/src/core/controllers/AuthenticationController.php
index 32dde88a04da6ff6514252c465a40d18c0c5adbe..fed906f671102396ab98c4c002c01778e69e7980 100755
--- a/src/core/controllers/AuthenticationController.php
+++ b/src/core/controllers/AuthenticationController.php
@@ -105,7 +105,7 @@ class AuthenticationController
 
         $connection = ConfigurationModel::getConnection();
         if (in_array($connection, ['default', 'ldap'])) {
-            if (!Validator::stringType()->notEmpty()->validate($body['login']) || !Validator::stringType()->notEmpty()->validate($body['password'])) {
+            if (!array_key_exists('login', $body) || !array_key_exists('password', $body) || !Validator::stringType()->notEmpty()->validate($body['login']) || !Validator::stringType()->notEmpty()->validate($body['password'])) {
                 return $response->withStatus(400)->withJson(['errors' => 'Bad Request']);
             }
         }
diff --git a/src/core/models/DatabasePDO.php b/src/core/models/DatabasePDO.php
index ba426ebf7c06933a36d764b6ca5872e84a616bd4..e47f6c31a80b3ac35ea81f79bf5eeff31f055ebe 100755
--- a/src/core/models/DatabasePDO.php
+++ b/src/core/models/DatabasePDO.php
@@ -69,7 +69,7 @@ class DatabasePDO
                 \PDO::ATTR_ERRMODE      => \PDO::ERRMODE_EXCEPTION,
                 \PDO::ATTR_CASE         => \PDO::CASE_LOWER
             ];
-    
+            
             try {
                 self::$pdo = new \PDO($dsn, $user, $password, $options);
                 break;
@@ -79,7 +79,7 @@ class DatabasePDO
                     self::$pdo = new \PDO($dsn, $user, $password, $options);
                     break;
                 } catch (\PDOException $PDOException) {
-                    if (!empty($loadedXml->database[$key + 1])) {
+                    if (!empty($loadedXml->{$key.'1'})) {
                         continue;
                     } else {
                         throw new \Exception($PDOException->getMessage());
diff --git a/src/core/models/PasswordModel.php b/src/core/models/PasswordModel.php
index 2ddb2fae0a3e11f0a603c5555daccf3515514158..c2e2ee351e48d5d24c250afba3b5f97385256557 100755
--- a/src/core/models/PasswordModel.php
+++ b/src/core/models/PasswordModel.php
@@ -23,8 +23,8 @@ class PasswordModel
         $aRules = DatabaseModel::select([
             'select'    => empty($aArgs['select']) ? ['*'] : $aArgs['select'],
             'table'     => ['password_rules'],
-            'where'     => $aArgs['where'],
-            'data'      => $aArgs['data'],
+            'where'     => $aArgs['where'] ?? [],
+            'data'      => $aArgs['data'] ?? [],
         ]);
 
         return $aRules;
diff --git a/src/frontend/app/administration/group/group.component.html b/src/frontend/app/administration/group/group.component.html
index 5627cd3c1f1adcba9365b9670506f0cb1fad60b4..2d2fd6526dd72e574b4a8e27bc1ff80d2aa3e024 100644
--- a/src/frontend/app/administration/group/group.component.html
+++ b/src/frontend/app/administration/group/group.component.html
@@ -22,54 +22,68 @@
                 <ion-toggle slot="start" [name]="privilege.id" color="primary" [checked]="privilege.checked"
                     [(ngModel)]="privilege.checked" (click)="togglePrivilege(privilege, true)"></ion-toggle>
                 <ion-label (click)="togglePrivilege(privilege, false)">{{'lang.' + privilege.id + 'Admin' | translate}}</ion-label>
+                <ion-button *ngIf="canManage(privilege)" style="margin-right: 43%; margin-bottom: 10px;" fill="clear" shape="round"
+                    (click)="$event.stopPropagation(); openGroupList()"
+                    [title]="'lang.groupsToManage' | translate">
+                    <ion-icon name="people" [color]="getChecked() > 0 ? 'secondary' : 'primary'" style="font-size: 20px;"></ion-icon>
+                </ion-button>
             </ion-item>
         </div>
-        <ion-item lines="none" *ngIf="!creationMode">
-            <ion-label color="secondary">{{'lang.linkedUsers' | translate}} :</ion-label>
-        </ion-item>
-        <ion-searchbar [placeholder]="'lang.filter' | translate" style="margin-left: 4x; display: flex; width: 50%;"
-            (ionChange)="applyFilter($event.detail.value)">
-        </ion-searchbar>
-        <ion-card *ngIf="!creationMode" style="height: 400px; overflow-y: auto;">
-            <ion-list>
-                <ion-item style="position: sticky;top:0px;z-index:1;">
-                    <ng-container style="display: flex;align-items: center;justify-content: center;width: 100%;background: white;">
-                        <ion-label color="primary" matSort [matSortActive]="displayedColumns[1]" matSortDirection='asc'
-                        style="display: flex;font-size: 12px;align-items: center;" (matSortChange)="sortData($event)">
-                        <ng-container *ngFor="let col of displayedColumns">
-                            <div [mat-sort-header]="col" disableClear style="flex: 1" *ngIf="col!=='actions'">
-                                {{'lang.' + col | translate}}
-                            </div>
-                        </ng-container>
-                        <div style="flex: 1;text-align: right;" *ngIf="displayedColumns.indexOf('actions') > -1">
-                            <ion-button slot="end" color="primary" fille="outline" shape="round"
-                                (click)="openUserList()">
-                                {{'lang.add' | translate}}
-                            </ion-button>
-                        </div>
-                    </ion-label>
-                    </ng-container>
-                    <ion-button slot="end" fill="clear" shape="round" disabled>
-                        <ion-icon></ion-icon>
-                    </ion-button>
-                </ion-item>
-                <ion-virtual-scroll [items]="sortedData" approxItemHeight="50px">
-                    <ion-item *virtualItem="let element" style="display: flex;">
-                        <ion-label style="display: flex;cursor: pointer;"
-                            routerLink="/administration/users/{{element.id}}">
-                            <div style="flex: 1" *ngFor="let col of displayedColumns">
-                                {{element[col]}}
+        <ng-container *ngIf="!creationMode">
+            <ion-item lines="none">
+                <ion-label color="secondary">{{'lang.linkedUsers' | translate}} :</ion-label>
+            </ion-item>
+            <ion-searchbar [placeholder]="'lang.filter' | translate" style="margin-left: 4x; display: flex; width: 50%;"
+                (ionChange)="applyFilter($event.detail.value)">
+            </ion-searchbar>
+            <ion-content style="height: 400px; overflow-y: auto;">
+                <ion-list>
+                    <ion-item style="position: sticky;top:0px;z-index:1;">
+                        <ng-container style="display: flex;align-items: center;justify-content: center;width: 100%;background: white;">
+                            <ion-label color="primary" matSort [matSortActive]="displayedColumns[1]" matSortDirection='asc'
+                            style="display: flex;font-size: 12px;align-items: center;" (matSortChange)="sortData($event)">
+                            <ng-container *ngFor="let col of displayedColumns">
+                                <div [mat-sort-header]="col" disableClear style="flex: 1" *ngIf="col!=='actions'">
+                                    {{'lang.' + col | translate}}
+                                </div>
+                            </ng-container>
+                            <div style="flex: 1;text-align: right;" *ngIf="displayedColumns.indexOf('actions') > -1">
+                                <ion-button slot="end" color="primary" fille="outline" shape="round"
+                                    (click)="openUserList()">
+                                    {{'lang.add' | translate}}
+                                </ion-button>
                             </div>
                         </ion-label>
-                        <ion-button slot="end" fill="clear" shape="round"
-                            (click)="$event.stopPropagation();unlinkUser(element)"
-                            title="{{'lang.unlinkUser' | translate}}">
-                            <ion-icon color="danger" slot="icon-only" name="close-outline"></ion-icon>
+                        </ng-container>
+                        <ion-button slot="end" fill="clear" shape="round" disabled>
+                            <ion-icon></ion-icon>
                         </ion-button>
                     </ion-item>
-                </ion-virtual-scroll>
-            </ion-list>
-        </ion-card>
+                    <ion-virtual-scroll [items]="sortedData" approxItemHeight="50px">
+                        <ion-item *virtualItem="let element" style="display: flex;">
+                            <ion-label style="display: flex;cursor: pointer;"
+                                routerLink="/administration/users/{{element.id}}">
+                                <div style="flex: 1" *ngFor="let col of displayedColumns">
+                                    {{element[col]}}
+                                </div>
+                            </ion-label>
+                            <ion-button slot="end" fill="clear" shape="round"
+                                (click)="$event.stopPropagation();unlinkUser(element)"
+                                title="{{'lang.unlinkUser' | translate}}">
+                                <ion-icon color="danger" slot="icon-only" name="close-outline"></ion-icon>
+                            </ion-button>
+                        </ion-item>
+                    </ion-virtual-scroll>
+                    <ion-infinite-scroll threshold="100px" (ionInfinite)="loadData($event)" *ngIf="group.users.length > 7">
+                        <ion-infinite-scroll-content loadingSpinner="bubbles" [loadingText]="'lang.loadingMoreData' | translate">
+                        </ion-infinite-scroll-content>
+                    </ion-infinite-scroll>
+                </ion-list>
+                <ion-item lines="none" *ngIf="group.users.length === 0" style="text-align: center; font-size: 20px; color: gray; margin-top: 5px;">
+                    <ion-label>{{ 'lang.emptyGroupUsers' | translate }}</ion-label>
+                </ion-item>
+            </ion-content>
+        </ng-container>
         <ion-item text-center lines="none" style="position: sticky;bottom:0px;z-index:1;">
             <div style="display: flex;align-items: center;justify-content: center;width: 100%;background: white;">
                 <ion-button type="submit" shape="round" size="large" fill="outline" color="primary"
diff --git a/src/frontend/app/administration/group/group.component.ts b/src/frontend/app/administration/group/group.component.ts
index d77a98b4715e849b9667bf15322c6b59a834fb3b..4f7916496dfd136f71a46c28a9f61d1ab93850de 100644
--- a/src/frontend/app/administration/group/group.component.ts
+++ b/src/frontend/app/administration/group/group.component.ts
@@ -13,6 +13,8 @@ import { AlertController, ModalController, PopoverController } from '@ionic/angu
 import { UsersComponent } from './list/users.component';
 import { of } from 'rxjs';
 import { LatinisePipe } from 'ngx-pipes';
+import { GroupModalComponent } from './modal/group-modal.component';
+import { FunctionsService } from '../../service/functions.service';
 
 
 export interface Group {
@@ -41,6 +43,10 @@ export class GroupComponent implements OnInit {
     displayedColumns: string[];
     usersList: any[];
     sortedData: any[];
+    groups: any[] = [];
+    allGroups: any[] = [];
+
+    dataChanged: boolean = false;
 
     constructor(
         public http: HttpClient,
@@ -54,7 +60,8 @@ export class GroupComponent implements OnInit {
         public popoverController: PopoverController,
         public modalController: ModalController,
         public alertController: AlertController,
-        private latinisePipe: LatinisePipe
+        private latinisePipe: LatinisePipe,
+        public functions: FunctionsService
     ) {
         this.displayedColumns = ['firstname', 'lastname', 'actions'];
         this.group = {
@@ -77,7 +84,7 @@ export class GroupComponent implements OnInit {
                 this.creationMode = false;
                 this.usersList = [];
 
-                this.http.get('../rest/groups/' + params['id'])
+                this.http.get(`../rest/groups/${params['id']}`)
                     .pipe(
                         map((data: any) => data.group),
                         finalize(() => {
@@ -88,6 +95,9 @@ export class GroupComponent implements OnInit {
                             this.groupClone = JSON.parse(JSON.stringify(this.group));
                             this.title = this.group.label;
                             this.updateDataTable();
+                            if (this.group.privileges.find((privilige: any) => privilige.id === 'manage_users') !== undefined) {
+                                this.getGroups();
+                            }
                         }),
                         catchError((err: any) => {
                             this.notificationService.handleErrors(err);
@@ -130,6 +140,64 @@ export class GroupComponent implements OnInit {
         }
     }
 
+    async openGroupList() {
+        await this.getPrivilegeParameters();
+        const modal = await this.modalController.create({
+            component: GroupModalComponent,
+            componentProps: {
+                groups: this.groups,
+                editUser: false
+            }
+        });
+        await modal.present();
+        const { data } = await modal.onWillDismiss();
+        if (data !== undefined) {
+            this.groups = data;
+            const privilege: any = this.group.privileges.find((item: any) => item.id === 'manage_users');
+            this.dataChanged = true;
+            this.updatePrivilege(privilege);
+        }
+    }
+
+    getPrivilegeParameters() {
+        return new Promise((resolve) => {
+            this.http.get('../rest/groups/' + this.group.id).pipe(
+                tap((data: any) => {
+                    const manageUsers: any = data.group.privileges.find((item: any) => item.id === 'manage_users');
+                    if (!this.functions.empty(manageUsers)) {
+                        this.allGroups.forEach((element: any, index: number) => {
+                            if (this.groups.find((item: any) => item.id === element.id) === undefined) {
+                                let checked: boolean;
+                                if (!this.functions.empty(manageUsers.parameters.authorized)) {
+                                    checked = manageUsers.parameters.authorized.indexOf(element.id) > -1;
+                                } else {
+                                    checked = true;
+                                }
+                                this.groups.push(
+                                    {
+                                        id: element.id,
+                                        label: element.label,
+                                        checked:  checked
+                                    }
+                                );
+                            }
+                        });
+                        this.groups = [...new Set(this.groups)];
+                    }
+                    resolve(true);
+                }),
+                catchError((err: any) => {
+                    this.notificationService.handleErrors(err);
+                    return of(false);
+                })
+            ).subscribe();
+        });
+    }
+
+    getChecked() {
+        return this.groups.filter((item: any) => item.checked).length;
+    }
+
     canValidate() {
         if (this.group.label === this.groupClone.label) {
             return false;
@@ -301,6 +369,13 @@ export class GroupComponent implements OnInit {
             });
             await alert.present();
         } else {
+            if (privilege.id === 'manage_users') {
+                if (!privilege.checked) {
+                    this.getGroups();
+                } else {
+                    this.groups = [];
+                }
+            }
             if (!toggle) {
                 privilege.checked = !privilege.checked;
             }
@@ -310,8 +385,37 @@ export class GroupComponent implements OnInit {
         }
     }
 
+    getGroups() {
+        this.http.get('../rest/groups').pipe(
+            tap((data: any) => {
+                this.allGroups = data.groups;
+            }),
+            catchError((err: any) => {
+                this.notificationService.handleErrors(err);
+                return of(false);
+            })
+        ).subscribe();
+    }
+
     updatePrivilege(privilege: any) {
-        this.http.put('../rest/groups/' + this.group.id + '/privilege/' + privilege.id, { checked: privilege.checked })
+        if (!this.dataChanged && this.groups.length === 0 && privilege.checked) {
+            this.groups = this.allGroups.map((group: any) => ({
+                id: group.id,
+                label: group.label,
+                checked: true
+            }));
+        }
+
+        const objTosend: any = {
+            checked: privilege.checked,
+            parameters: {
+                authorized: this.groups.filter((element: any) => element.checked).map((item: any) => item.id)
+            }
+        };
+        if (privilege.id !== 'manage_users') {
+            delete objTosend.parameters.authorized;
+        }
+        this.http.put('../rest/groups/' + this.group.id + '/privilege/' + privilege.id, objTosend)
             .pipe(
                 tap(() => {
                     this.notificationService.success('lang.privilegeUpdated');
@@ -354,6 +458,15 @@ export class GroupComponent implements OnInit {
             return state;
         });
     }
+
+    canManage(privilege: any) {
+        return privilege.id === 'manage_users' && privilege.checked;
+    }
+
+    loadData(event: any) {
+        event.target.complete();
+        event.target.disabled = true;
+    }
 }
 
 
diff --git a/src/frontend/app/administration/group/list/users.component.html b/src/frontend/app/administration/group/list/users.component.html
index 2af53a9763a8c074cdb47950f5e74a4dbad21a1d..7d5ba32e6c5388a536e983d87cc83ac976aaf380 100644
--- a/src/frontend/app/administration/group/list/users.component.html
+++ b/src/frontend/app/administration/group/list/users.component.html
@@ -4,7 +4,7 @@
     </ion-toolbar>
 </ion-header>
 <ion-content>
-    <ion-list>
+    <ion-list *ngIf="usersList.length > 0">
         <ion-virtual-scroll [items]="usersList" approxItemHeight="50px" style="height: 450px;">
             <ion-item button *virtualItem="let element" (click)="selectUser(element)">
                 <ion-label>
@@ -13,4 +13,7 @@
             </ion-item>
         </ion-virtual-scroll>
     </ion-list>
+    <ion-item lines="none" *ngIf="usersList.length === 0" style="text-align: center; font-size: 20px; color: gray; margin-top: 35%;">
+        <ion-label>{{'lang.emptyUsers' | translate}}</ion-label>
+    </ion-item>
 </ion-content>
\ No newline at end of file
diff --git a/src/frontend/app/administration/group/modal/group-modal.component.html b/src/frontend/app/administration/group/modal/group-modal.component.html
new file mode 100644
index 0000000000000000000000000000000000000000..2949ef7b1fc2228f468fa2ca964878fc1a4eb3d3
--- /dev/null
+++ b/src/frontend/app/administration/group/modal/group-modal.component.html
@@ -0,0 +1,26 @@
+<ion-header [translucent]="true">
+    <ion-toolbar color="primary">
+        <ion-title>{{(editUser ? 'lang.groups' : 'lang.groupsToManage') | translate}}</ion-title>
+    </ion-toolbar>
+</ion-header>
+<ion-content>
+    <ion-list>
+        <ion-virtual-scroll [items]="groups" approxItemHeight="50px" style="height: 450px;">
+            <ion-item *virtualItem="let element">
+                <ion-label>
+                    {{element.label}}
+                </ion-label>
+                <ion-checkbox slot="end" [value]="element.id" [checked]="element.checked" (ionChange)="element.checked = !element.checked"></ion-checkbox>
+            </ion-item>
+        </ion-virtual-scroll>
+    </ion-list>
+</ion-content>
+<ion-footer class="ion-no-border">
+    <ion-toolbar>
+        <ion-buttons class="ion-justify-content-center">
+            <ion-button type="submit" color="primary" (click)="saveChanges()">
+                <ion-label>{{'lang.save' | translate}}</ion-label>
+            </ion-button>
+        </ion-buttons>
+    </ion-toolbar>
+</ion-footer>
\ No newline at end of file
diff --git a/src/frontend/app/administration/group/modal/group-modal.component.scss b/src/frontend/app/administration/group/modal/group-modal.component.scss
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/src/frontend/app/administration/group/modal/group-modal.component.ts b/src/frontend/app/administration/group/modal/group-modal.component.ts
new file mode 100644
index 0000000000000000000000000000000000000000..162ba6b945e61d84be29b406417bc46ffe0c275a
--- /dev/null
+++ b/src/frontend/app/administration/group/modal/group-modal.component.ts
@@ -0,0 +1,24 @@
+import { Component, Input, OnInit } from '@angular/core';
+import { ModalController } from '@ionic/angular';
+
+@Component({
+    templateUrl: 'group-modal.component.html',
+    styleUrls: ['group-modal.component.scss'],
+})
+
+export class GroupModalComponent implements OnInit {
+
+    @Input() groups: any;
+    @Input() editUser: boolean;
+
+    constructor(
+        public modalController: ModalController,
+    ) {}
+
+    async ngOnInit() {
+    }
+
+    saveChanges() {
+        this.modalController.dismiss(this.groups);
+    }
+}
diff --git a/src/frontend/app/administration/user/user.component.html b/src/frontend/app/administration/user/user.component.html
index 2ca44e3acb0665f534014f7268e98848e053856a..f413a7788d9239e999bab64bbf53e30f4dca4d2b 100644
--- a/src/frontend/app/administration/user/user.component.html
+++ b/src/frontend/app/administration/user/user.component.html
@@ -18,7 +18,7 @@
                 <ion-label>{{'lang.informations' | translate}}</ion-label>
                 <ion-icon name="information-circle"></ion-icon>
             </ion-segment-button>
-            <ion-segment-button [disabled]="creationMode" value="groups">
+            <ion-segment-button *ngIf="!creationMode" value="groups">
                 <ion-label>{{'lang.manage_groups' | translate}}</ion-label>
                 <ion-icon name="people-sharp"></ion-icon>
             </ion-segment-button>
@@ -118,14 +118,27 @@
 
 <ng-container *ngIf="currentTool === 'groups'">
     <ion-content>
+        <ion-item lines="none" style="position: sticky;top:0px;z-index:1;">
+            <ng-container style="display: flex;align-items: center;justify-content: center;width: 100%;background: white;">
+                <div style="flex: 1;text-align: right;">
+                    <ion-button slot="end" color="primary" fille="outline" shape="round" [disabled]="userGroupsClone.length === 0" (click)="openGroupsList()">
+                        {{'lang.add' | translate}}
+                    </ion-button>
+                </div>
+            </ng-container>
+        </ion-item>
         <ion-list *ngIf="user.groups.length > 0">
             <ion-item *ngFor="let group of user.groups">
                 <ion-label>{{group.label}}</ion-label>
+                <ion-button  slot="end" fill="clear" shape="round"
+                    (click)="unlinkGroup(group);" [title]="'lang.unlinkGroup' | translate" [disabled]="!isManageableGroup(group.id)">
+                    <ion-icon color="danger" slot="icon-only" name="close-outline"></ion-icon>
+                </ion-button>
             </ion-item>
         </ion-list>
         <ion-list class="no-result" *ngIf="user.groups.length === 0">
             <ion-item lines="none">
-                <ion-label class="no-result-label" color="medium">{{'lang.noAssociatedGroup' | translate}}</ion-label>
+                <ion-label class="no-result-label" color="medium">{{(userGroupsClone.length === 0 ? 'lang.emptyGroups' : 'lang.noAssociatedGroup') | translate}}</ion-label>
             </ion-item>
         </ion-list>
     </ion-content>
diff --git a/src/frontend/app/administration/user/user.component.ts b/src/frontend/app/administration/user/user.component.ts
index 93bf2955f252822d93872fc1f0ce1e07927ab235..5c08ccc3a1e50ad4d1d55de2be63cc4da1cb2667 100644
--- a/src/frontend/app/administration/user/user.component.ts
+++ b/src/frontend/app/administration/user/user.component.ts
@@ -1,4 +1,4 @@
-import { Component, OnInit } from '@angular/core';
+import { Component, OnInit, ViewChild } from '@angular/core';
 import { SignaturesContentService } from '../../service/signatures.service';
 import { NotificationService } from '../../service/notification.service';
 import { HttpClient, HttpHeaders } from '@angular/common/http';
@@ -9,7 +9,8 @@ import { ConfirmComponent } from '../../plugins/confirm.component';
 import { TranslateService } from '@ngx-translate/core';
 import { AuthService } from '../../service/auth.service';
 import { of } from 'rxjs';
-import { AlertController } from '@ionic/angular';
+import { AlertController, ModalController } from '@ionic/angular';
+import { GroupModalComponent } from '../group/modal/group-modal.component';
 
 
 export interface User {
@@ -77,6 +78,10 @@ export class UserComponent implements OnInit {
         errorMsg: ''
     };
 
+    hasGroup: boolean = false;
+    userGroupsClone: any;
+    manageableGroups: any;
+
     constructor(
         public http: HttpClient,
         private translate: TranslateService,
@@ -86,12 +91,13 @@ export class UserComponent implements OnInit {
         public notificationService: NotificationService,
         public dialog: MatDialog,
         public authService: AuthService,
-        public alertController: AlertController
+        public alertController: AlertController,
+        public modalController: ModalController,
     ) {
     }
 
-    ngOnInit(): void {
-        this.route.params.subscribe((params: any) => {
+    async ngOnInit(): Promise<void> {
+        this.route.params.subscribe(async (params: any) => {
             if (params['id'] === undefined) {
                 this.creationMode = true;
                 this.title = this.translate.instant('lang.userCreation');
@@ -108,29 +114,56 @@ export class UserComponent implements OnInit {
                     canSendActivationNotification: false
                 };
                 this.loading = false;
+                await this.isCreationMode();
             } else {
-                this.creationMode = false;
-                this.http.get('../rest/users/' + params['id'])
-                    .pipe(
-                        map((data: any) => data.user),
-                        finalize(() => this.loading = false)
-                    )
-                    .subscribe({
-                        next: data => {
-                            this.user = data;
-                            this.userClone = JSON.parse(JSON.stringify(this.user));
-                            this.title = this.user.firstname + ' ' + this.user.lastname;
-                            if (this.user.isRest) {
-                                this.getPassRules({ detail: {
-                                    checked: true
-                                } });
-                            }
-                        },
-                    });
+                this.isUpdateMode(params['id']);
+                this.loading = false;
             }
         });
     }
 
+    async isCreationMode() {
+        this.creationMode = true;
+        this.title = this.translate.instant('lang.userCreation');
+        this.user = {
+            id: '',
+            firstname: '',
+            lastname: '',
+            login: '',
+            email: '',
+            phone: '',
+            picture: '',
+            signatureModes: ['stamp'],
+            isRest: false,
+            canSendActivationNotification: false
+        };
+        await this.getManageableGroups();
+        this.loading = false;
+    }
+
+    isUpdateMode(id: any) {
+        this.creationMode = false;
+        this.http.get(`../rest/users/${id}`)
+            .pipe(
+                map((data: any) => data.user)
+            )
+            .subscribe({
+                next: async data => {
+                    this.user = data;
+                    this.hasGroup = data.groups.length > 0;
+                    this.userGroupsClone = JSON.parse(JSON.stringify(data.groups));
+                    await this.getManageableGroups();
+                    this.userClone = JSON.parse(JSON.stringify(this.user));
+                    this.title = this.user.firstname + ' ' + this.user.lastname;
+                    if (this.user.isRest) {
+                        this.getPassRules({ detail: {
+                            checked: true
+                        } });
+                    }
+                },
+            });
+    }
+
     canValidate() {
         if (this.user.isRest && this.passwordRest.newPassword !== '' && (this.handlePassword.error || this.passwordRest.passwordConfirmation !== this.passwordRest.newPassword)) {
             return false;
@@ -151,7 +184,8 @@ export class UserComponent implements OnInit {
 
     modifyUser() {
         this.loading = true;
-        this.http.put('../rest/users/' + this.user.id, this.user)
+        const objToSend = {...this.user, groups: this.user['groups'].map((group: any) => group.id)};
+        this.http.put('../rest/users/' + this.user.id, objToSend)
             .pipe(
                 finalize(() => this.loading = false),
                 tap(() => {
@@ -161,7 +195,7 @@ export class UserComponent implements OnInit {
                     if (this.passwordRest.newPassword !== '') {
                         this.updateRestUser();
                     }
-                    this.router.navigate(['/administration/users']);
+                    this.router.navigate(['/administration/users/' + this.user.id]);
                     this.notificationService.success('lang.userUpdated');
                 }),
                 catchError((err: any) => {
@@ -195,7 +229,7 @@ export class UserComponent implements OnInit {
                         this.user.id = data.id;
                         this.updateRestUser();
                     }
-                    this.router.navigate(['/administration/users']);
+                    this.router.navigate([`/administration/users/${data.id}`]);
                     this.notificationService.success('lang.userAdded');
                 }),
                 catchError((err: any) => {
@@ -361,4 +395,101 @@ export class UserComponent implements OnInit {
 
         await alert.present();
     }
+
+    getManageableGroups() {
+        return new Promise((resolve) => {
+            this.http.get('../rest/manageableGroups').pipe(
+                tap((data: any) => {
+                    this.manageableGroups = JSON.parse(JSON.stringify(data.groups));
+                    if (this.hasGroup) {
+                        const groupIds: number[] = this.userGroupsClone.map((group: any) => group.id);
+                        this.userGroupsClone = JSON.parse(JSON.stringify(data.groups.filter((group: any) => groupIds.indexOf(group.id) === -1)));
+                    } else  {
+                        this.userGroupsClone = JSON.parse(JSON.stringify(data.groups));
+                    }
+                    resolve(true);
+                }),
+                finalize(() => this.loading = false),
+                catchError((err: any) => {
+                    this.notificationService.handleErrors(err);
+                    return of(false);
+                })
+            ).subscribe();
+        });
+    }
+
+    isManageableGroup(groupId: any) {
+        return this.manageableGroups.find((userGroup: any) => userGroup.id === groupId) !== undefined;
+    }
+
+    async unlinkGroup(group: any) {
+        const cannotManage: boolean = this.user['groups'].length === 1 && this.user.id === this.authService.user.id && this.authService.user.administrativePrivileges.find((privilege: any) => privilege.id === 'manage_users') !== undefined;
+        const alert = await this.alertController.create({
+            header: this.translate.instant('lang.confirmMsg'),
+            message: cannotManage ? this.translate.instant('lang.groupWarnMsg') : '',
+            buttons: [
+                {
+                    text: this.translate.instant('lang.no'),
+                    role: 'cancel',
+                    cssClass: 'secondary',
+                    handler: () => { }
+                },
+                {
+                    text: this.translate.instant('lang.yes'),
+                    handler: () => {
+                        this.http.delete(`../rest/groups/${group.id}/users/${this.user.id}`, {})
+                            .pipe(
+                                tap(() => {
+                                    this.isUpdateMode(this.user.id);
+                                    this.userGroupsClone.push(group);
+                                }),
+                                finalize(() => this.loading = false)
+                            )
+                            .subscribe({
+                                next: data => {
+                                    const indexToDelete = this.user['groups'].findIndex((item: any) => item.id === group.id);
+                                    this.user['groups'].splice(indexToDelete, 1);
+                                    this.notificationService.success('lang.groupDeleted');
+                                    this.authService.updateUserInfoWithTokenRefresh();
+                                    this.http.get(`../rest/users/${this.user.id}`).pipe(
+                                        tap(() => {}),
+                                        catchError((err: any) => {
+                                            if (err.error.errors === 'User out of perimeter') {
+                                                this.router.navigate(['/administration/users']);
+                                            }
+                                            return of(false);
+                                        })
+                                    ).subscribe();
+                                },
+                                error: err => {
+                                    this.notificationService.handleErrors(err);
+                                }
+                            });
+                    }
+                }
+            ]
+        });
+        await alert.present();
+    }
+
+    async openGroupsList() {
+        const modal = await this.modalController.create({
+            component: GroupModalComponent,
+            componentProps: {
+                groups: this.userGroupsClone,
+                editUser: true
+            }
+        });
+        await modal.present();
+        const { data } = await modal.onWillDismiss();
+        if (data !== undefined) {
+            this.user['groups'] = this.user['groups'].concat(data.filter((group: any) => group.checked).map((item: any) => ({
+                id: item.id,
+                label: item.label
+            })));
+            const groupIds: number[] = this.user['groups'].map((group: any) => group.id);
+            this.userGroupsClone = this.userGroupsClone.filter((group: any) => groupIds.indexOf(group.id) === -1);
+            this.modifyUser();
+        }
+    }
 }
diff --git a/src/frontend/app/app.module.ts b/src/frontend/app/app.module.ts
index 01d6147279a0e188e0fd1ea80b0f1da33ca0c828..de52beba4beb533e55ca37b39ab416411a43d177 100755
--- a/src/frontend/app/app.module.ts
+++ b/src/frontend/app/app.module.ts
@@ -85,6 +85,7 @@ import { HistoryListComponent } from './administration/history/history-list.comp
 import { OtpListComponent } from './administration/otp/otp-list.component';
 import { OtpComponent } from './administration/otp/otp.component';
 import { CustomizationComponent } from './administration/customization/customization.component';
+import { GroupModalComponent } from './administration/group/modal/group-modal.component';
 
 
 // SERVICES
@@ -172,7 +173,8 @@ registerLocaleData(localeFr, 'fr-FR');
         GridButtonComponent,
         NotificationsListComponent,
         MessageBoxComponent,
-        AboutUsComponent
+        AboutUsComponent,
+        GroupModalComponent
     ],
     imports: [
         FormsModule,
diff --git a/src/frontend/app/document/document-list/document-list.component.ts b/src/frontend/app/document/document-list/document-list.component.ts
index ab11d5a3b09ccc908af835bc74be82cce1b4b02d..ba25663d8e30f36924afe7cef0abc4d3c47f3ae9 100644
--- a/src/frontend/app/document/document-list/document-list.component.ts
+++ b/src/frontend/app/document/document-list/document-list.component.ts
@@ -1,15 +1,17 @@
-import { Component, Input, OnInit, Output, EventEmitter, ViewChild, AfterViewInit } from '@angular/core';
+import { Component, Input, OnInit, Output, EventEmitter, ViewChild, AfterViewInit, OnDestroy } from '@angular/core';
 import { SignaturesContentService } from '../../service/signatures.service';
 import { HttpClient } from '@angular/common/http';
 import { DomSanitizer } from '@angular/platform-browser';
 import { IonSlides, MenuController } from '@ionic/angular';
+import { Subscription } from 'rxjs';
+import { ActionsService } from '../../service/actions.service';
 
 @Component({
     selector: 'app-document-list',
     templateUrl: 'document-list.component.html',
     styleUrls: ['document-list.component.scss'],
 })
-export class DocumentListComponent implements OnInit, AfterViewInit {
+export class DocumentListComponent implements OnInit, AfterViewInit, OnDestroy {
 
     @Input() docList: any;
     @Input() currentDocId: any;
@@ -18,6 +20,8 @@ export class DocumentListComponent implements OnInit, AfterViewInit {
 
     @ViewChild('slides', { static: false }) slides: IonSlides;
 
+    subscription: Subscription;
+
     loading: boolean = true;
 
     scrolling: boolean = false;
@@ -30,9 +34,16 @@ export class DocumentListComponent implements OnInit, AfterViewInit {
     constructor(
         public http: HttpClient,
         public signaturesService: SignaturesContentService,
+        private menu: MenuController,
+        private actionsService: ActionsService,
         private sanitizer: DomSanitizer,
-        private menu: MenuController
-    ) { }
+    ) {
+        this.subscription = this.actionsService.catchEvent().subscribe((event: any) => {
+            if (event === 'scrollToTop') {
+                this.slides.slideTo(0, 0);
+            }
+        });
+    }
 
     ngOnInit(): void {
         this.docList.forEach((element: any, index: number) => {
@@ -49,6 +60,10 @@ export class DocumentListComponent implements OnInit, AfterViewInit {
         this.loading = false;
     }
 
+    ngOnDestroy(): void {
+        this.subscription.unsubscribe();
+    }
+
     loadDoc(id: string) {
         this.triggerEvent.emit(id);
         this.menu.close('right-menu');
diff --git a/src/frontend/app/document/document.component.ts b/src/frontend/app/document/document.component.ts
index f5732e14764dd8cc642586a3f48ade5b39c41442..ffbda0c365058190e2c5b137684a86ecd4f63c4b 100755
--- a/src/frontend/app/document/document.component.ts
+++ b/src/frontend/app/document/document.component.ts
@@ -1,4 +1,4 @@
-import { Component, OnInit, ViewChild, TemplateRef, ViewContainerRef, Injectable } from '@angular/core';
+import { Component, OnInit, ViewChild, TemplateRef, ViewContainerRef, Injectable, OnDestroy } from '@angular/core';
 import { SignaturesContentService } from '../service/signatures.service';
 import { DomSanitizer } from '@angular/platform-browser';
 import { MatBottomSheet, MatBottomSheetConfig } from '@angular/material/bottom-sheet';
@@ -18,7 +18,7 @@ import { LocalStorageService } from '../service/local-storage.service';
 import { ActionSheetController, AlertController, LoadingController, MenuController, ModalController, NavController } from '@ionic/angular';
 import { NgxExtendedPdfViewerService } from 'ngx-extended-pdf-viewer';
 import { catchError, exhaustMap, tap } from 'rxjs/operators';
-import { of } from 'rxjs';
+import { of, Subscription } from 'rxjs';
 import { SignatureMethodService } from '../service/signature-method/signature-method.service';
 import { FunctionsService } from '../service/functions.service';
 import { ActionsService } from '../service/actions.service';
@@ -31,7 +31,7 @@ import { SuccessInfoValidBottomSheetComponent } from '../modal/success-info-vali
 })
 
 
-export class DocumentComponent implements OnInit {
+export class DocumentComponent implements OnInit, OnDestroy {
 
     @ViewChild('mainContent') mainContent: any;
     @ViewChild('img') img: any;
@@ -73,6 +73,8 @@ export class DocumentComponent implements OnInit {
         workflow: [],
     };
 
+    subscription: Subscription;
+
     signaturesContent: any = [];
     docList: any = [];
 
@@ -125,6 +127,16 @@ export class DocumentComponent implements OnInit {
         private cookieService: CookieService,
     ) {
         this.draggable = false;
+        this.subscription = this.actionsService.catchEvent().subscribe((event: any) => {
+            if (event.id === 'gotoDocument' && event.documentIndex === 0) {
+                this.loadDoc(event.documentIndex);
+                this.actionsService.setEvent('scrollToTop');
+            }
+        });
+    }
+
+    ngOnDestroy(): void {
+        this.subscription.unsubscribe();
     }
 
     imageLoaded(ev: any) {
diff --git a/src/frontend/app/document/visa-workflow/otps/yousign/otp-yousign.component.ts b/src/frontend/app/document/visa-workflow/otps/yousign/otp-yousign.component.ts
index 107b1a2872a979921c0a4a16dc0293bfb0860677..eadc3da2b68fc8f80a0fdd5f3f2ce288fd7c4832 100644
--- a/src/frontend/app/document/visa-workflow/otps/yousign/otp-yousign.component.ts
+++ b/src/frontend/app/document/visa-workflow/otps/yousign/otp-yousign.component.ts
@@ -6,6 +6,7 @@ import { NgForm } from '@angular/forms';
 import { catchError, tap } from 'rxjs/operators';
 import { of } from 'rxjs';
 import { OtpService } from '../otp.service';
+import { FunctionsService } from '../../../../service/functions.service';
 
 @Component({
     selector: 'app-otp-yousign',
@@ -38,7 +39,8 @@ export class OtpYousignComponent implements OnInit {
         public http: HttpClient,
         private translate: TranslateService,
         public notificationService: NotificationService,
-        public otpService: OtpService
+        public otpService: OtpService,
+        private functions: FunctionsService
     ) {
         this.otpService.catchEvent().subscribe(async (res) => {
             if (res.id === 'connector') {
@@ -75,7 +77,9 @@ export class OtpYousignComponent implements OnInit {
     }
 
     getData() {
-        this.formatPhone();
+        if (!this.functions.empty(this.otp.phone)) {
+            this.formatPhone();
+        }
         return this.otp;
     }
 
diff --git a/src/frontend/app/sidebar/sidebar.component.html b/src/frontend/app/sidebar/sidebar.component.html
index 29ecbe9a84fe5a128cb3193fd8706bd8dc0cf07d..5c8945f6379725935eae057c153271201f489343 100755
--- a/src/frontend/app/sidebar/sidebar.component.html
+++ b/src/frontend/app/sidebar/sidebar.component.html
@@ -77,7 +77,7 @@
             </ion-label>
         </ion-item>
         <ion-menu-toggle auto-hide="false" *ngFor="let document of signaturesService.documentsList;let i=index">
-            <ion-item class="doc-item" (click)="filterService.currentIndex = i" routerDirection="root"
+            <ion-item class="doc-item" (click)="filterService.currentIndex = i; goTo(document.id);" routerDirection="root"
                 [routerLink]="['/documents/'+document.id]" detail="false"
                 [class.selected]="router.url === '/documents/'+document.id">
                 <ion-icon *ngIf="document.mode == 'sign'" color="primary" slot="start" name="pencil-outline"></ion-icon>
diff --git a/src/frontend/app/sidebar/sidebar.component.ts b/src/frontend/app/sidebar/sidebar.component.ts
index e5d662cce750f4c5858709c47cfdd2e86146fe98..9bcbedeeb78ec9d5b3173270f521bad30e01ef27 100755
--- a/src/frontend/app/sidebar/sidebar.component.ts
+++ b/src/frontend/app/sidebar/sidebar.component.ts
@@ -165,4 +165,8 @@ export class SidebarComponent implements OnInit, AfterViewInit {
     canIndex() {
         return this.authService.user.appPrivileges.map((item: any) => item.id).indexOf('indexation') > -1;
     }
+
+    goTo() {
+        this.actionsService.setEvent({id: 'gotoDocument', documentIndex: 0});
+    }
 }
diff --git a/test/unitTests/app/user/UserControllerTest.php b/test/unitTests/app/user/UserControllerTest.php
index 1d573d5b63e680f81feace4edfd08aab1600e66d..6bd12b4a1bc05f96d3f7cab7e3df7a7f7ed8dde3 100755
--- a/test/unitTests/app/user/UserControllerTest.php
+++ b/test/unitTests/app/user/UserControllerTest.php
@@ -13,6 +13,7 @@ class UserControllerTest extends TestCase
 {
     private static $signatureId = null;
     private static $userId = null;
+    private static $userIdToDelete = null;
 
     public function testCreateUser()
     {
@@ -22,10 +23,11 @@ class UserControllerTest extends TestCase
         $request        = \Slim\Http\Request::createFromEnvironment($environment);
 
         $aArgs = [
-            'login'     => 'emailLogin',
-            'firstname' => 'Prénom',
-            'lastname'  => 'Nom',
-            'email'     => 'email@test.fr'
+            'login'             => 'emailLoginFingerprint',
+            'firstname'         => 'Prénom',
+            'lastname'          => 'Nom',
+            'email'             => 'email@test.fr',
+            'phone'             => '0701020304'
         ];
 
         $fullRequest = \httpRequestCustom::addContentInBody($aArgs, $request);
@@ -35,6 +37,23 @@ class UserControllerTest extends TestCase
         $this->assertIsInt($responseBody->id);
         self::$userId = $responseBody->id;
 
+        //with x509Fingerprint
+        $aArgs = [
+            'login'             => 'emailLogin',
+            'firstname'         => 'Prénom',
+            'lastname'          => 'Nom',
+            'email'             => 'email@test.fr',
+            'x509Fingerprint'   => 'fingerprint',
+            'isRest'            => true
+        ];
+
+        $fullRequest    = \httpRequestCustom::addContentInBody($aArgs, $request);
+        $response       = $userController->create($fullRequest, new \Slim\Http\Response());
+        $responseBody   = json_decode((string)$response->getBody());
+
+        $this->assertIsInt($responseBody->id);
+        self::$userIdToDelete = $responseBody->id;
+
         //Mail missing
         $aArgs = [
             'login'     => 'failLogin',
@@ -162,11 +181,27 @@ class UserControllerTest extends TestCase
         $this->assertSame('email@test.fr', $responseBody->user->email);
         $this->assertSame('Prénom', $responseBody->user->firstname);
         $this->assertSame('Nom', $responseBody->user->lastname);
+        $this->assertSame('0701020304', $responseBody->user->phone);
 
         $response     = $userController->getById($request, new \Slim\Http\Response(), ['id' => -1]);
+        $this->assertSame(400, $response->getStatusCode());
         $responseBody = json_decode((string)$response->getBody());
+        $this->assertSame('User does not exist', $responseBody->errors);
+    }
 
-        $this->assertEmpty($responseBody->users);
+    public function testGetFingerprintById()
+    {
+        $previousUserId = $GLOBALS['id'];
+        $GLOBALS['id']  = self::$userIdToDelete;
+        $userController = new \User\controllers\UserController();
+
+        //find "userIdToDelete" fingerprint before user gets deleted
+        $response       = $userController->getUserInformationsById(['id' => self::$userIdToDelete]);
+
+        $this->assertNotEmpty($response);
+        $this->assertNotEmpty($response['x509Fingerprint']);
+        $this->assertSame('fingerprint', $response['x509Fingerprint']);
+        $GLOBALS['id']  = $previousUserId;
     }
 
     public function testUpdate()
@@ -181,13 +216,14 @@ class UserControllerTest extends TestCase
         $request        = \Slim\Http\Request::createFromEnvironment($environment);
 
         $aArgs = [
-            'writingMode'   => 'stylus',
-            'writingSize'   => 2,
-            'writingColor'  => '#F1F1F1',
-            'lang'          => 'fr',
-            'notifications' => [
-                'instant'   => true,
-                'summaries' => [],
+            'writingMode'       => 'stylus',
+            'writingSize'       => 2,
+            'writingColor'      => '#F1F1F1',
+            'lang'              => 'fr',
+            'signatureScaling'  => false,
+            'notifications'     => [
+                'instant'       => true,
+                'summaries'     => [],
             ],
         ];
 
@@ -203,7 +239,8 @@ class UserControllerTest extends TestCase
         $aArgs = [
             'firstname'     => 'Jolly',
             'lastname'      => 'Jumper',
-            'email'         => 'email@test.fr'
+            'email'         => 'email@test.fr',
+            'phone'         => '0701020304'
         ];
 
         $fullRequest = \httpRequestCustom::addContentInBody($aArgs, $request);
@@ -241,7 +278,7 @@ class UserControllerTest extends TestCase
         $aArgs = [
             'encodedSignature'  => base64_encode(file_get_contents('test/unitTests/samples/signature.jpg')),
             'format'            => 'jpg'
-        ];
+        ];     
 
         $fullRequest = \httpRequestCustom::addContentInBody($aArgs, $request);
         $response     = $signatureController->create($fullRequest, new \Slim\Http\Response(), ['id' => self::$userId]);
@@ -290,4 +327,30 @@ class UserControllerTest extends TestCase
         ]);
         $GLOBALS['id'] = $previousUserId;
     }
+
+    public function testDelete()
+    {
+        $userController = new \User\controllers\UserController();
+
+        $environment    = \Slim\Http\Environment::mock(['REQUEST_METHOD' => 'DELETE']);
+        $request        = \Slim\Http\Request::createFromEnvironment($environment);
+        
+        $response       = $userController->delete($request, new \Slim\Http\Response(), ['id' => self::$userIdToDelete]);
+        $this->assertSame(204, $response->getStatusCode());
+
+        $response       = $userController->delete($request, new \Slim\Http\Response(), ['id' => self::$userIdToDelete]);
+        $responseBody   = json_decode((string)$response->getBody());
+        $this->assertSame(400, $response->getStatusCode());
+        $this->assertSame('User does not exist', $responseBody->errors);
+        
+        $response       = $userController->delete($request, new \Slim\Http\Response(), ['id' => '1']);
+        $responseBody   = json_decode((string)$response->getBody());
+        $this->assertSame(403, $response->getStatusCode());
+        $this->assertSame('Privilege forbidden', $responseBody->errors);
+
+        $response       = $userController->delete($request, new \Slim\Http\Response(), ['id' => 1.1]);
+        $responseBody   = json_decode((string)$response->getBody());
+        $this->assertSame(400, $response->getStatusCode());
+        $this->assertSame('Route id is not an integer', $responseBody->errors);
+    }
 }