From a89c5c103f8f07afb8cdd06fd81a3195e4b7fa8b Mon Sep 17 00:00:00 2001
From: Sonia Zorba <sonia.zorba@inaf.it>
Date: Fri, 26 Jun 2020 23:00:52 +0200
Subject: [PATCH] Improved navigation using Vue Router; Improved permission
 editing; Bugfixes and refactoring

---
 gms-ui/src/App.vue                            |  21 +-
 gms-ui/src/api/mock/index.js                  |   3 +
 gms-ui/src/api/server/index.js                | 200 +++++++++++-------
 gms-ui/src/assets/logo.png                    | Bin 6849 -> 0 bytes
 .../src/components/GenericSearchResults.vue   |  21 +-
 gms-ui/src/components/GroupsBreadcrumb.vue    |   8 +-
 gms-ui/src/components/GroupsPanel.vue         |   9 +-
 gms-ui/src/components/Main.vue                |  28 +--
 gms-ui/src/components/MembersPanel.vue        |  12 +-
 gms-ui/src/components/Paginator.vue           |   4 -
 gms-ui/src/components/PermissionsPanel.vue    |  33 ++-
 gms-ui/src/components/TopMenu.vue             |  22 +-
 gms-ui/src/components/User.vue                |   6 +-
 gms-ui/src/components/UserSearchResult.vue    |  23 +-
 .../src/components/modals/AddGroupModal.vue   |  11 +-
 .../src/components/modals/AddMemberModal.vue  |   3 -
 .../components/modals/AddPermissionModal.vue  |  35 ++-
 .../modals/ConfirmRemoveMemberModal.vue       |   5 +-
 .../modals/ConfirmRemovePermissionModal.vue   |   5 +-
 .../src/components/modals/EditGroupModal.vue  |  11 +-
 gms-ui/src/components/modals/SearchUser.vue   |  11 +-
 gms-ui/src/main.js                            |   8 +-
 gms-ui/src/router.js                          |  21 ++
 gms-ui/src/store.js                           | 133 +++++++-----
 .../gms/controller/PermissionsController.java |  21 +-
 .../ia2/gms/controller/UsersController.java   |   2 +-
 .../ia2/gms/manager/PermissionsManager.java   |   5 +
 .../model/request/AddPermissionRequest.java   |  10 +
 .../request/UpdatePermissionRequest.java      |  39 ++++
 .../ia2/gms/persistence/PermissionsDAO.java   |  18 ++
 .../ia2/gms/service/PermissionsService.java   |   8 +
 31 files changed, 465 insertions(+), 271 deletions(-)
 delete mode 100644 gms-ui/src/assets/logo.png
 create mode 100644 gms-ui/src/router.js
 create mode 100644 gms/src/main/java/it/inaf/ia2/gms/model/request/UpdatePermissionRequest.java

diff --git a/gms-ui/src/App.vue b/gms-ui/src/App.vue
index cd5b2f3..aa5de32 100644
--- a/gms-ui/src/App.vue
+++ b/gms-ui/src/App.vue
@@ -2,9 +2,7 @@
 <div id="app" v-if="model">
   <TopMenu v-bind:user="model.user" />
   <div class="container">
-    <Main v-if="page === 'main'" />
-    <GenericSearchResults v-if="page === 'search'" />
-    <UserSearchResult v-if="page === 'userSearch'" />
+    <router-view></router-view>
   </div>
   <div id="loading" v-if="loading">
     <div id="spinner-wrapper">
@@ -16,21 +14,13 @@
 
 <script>
 import TopMenu from './components/TopMenu.vue';
-import Main from './components/Main.vue';
-import GenericSearchResults from './components/GenericSearchResults.vue';
-import UserSearchResult from './components/UserSearchResult.vue';
-import {
-  mapState
-} from 'vuex';
+import { mapState } from 'vuex';
 import client from 'api-client';
 
 export default {
   name: 'app',
   components: {
-    TopMenu,
-    Main,
-    GenericSearchResults,
-    UserSearchResult
+    TopMenu
   },
   computed: mapState({
     model: state => state.model,
@@ -52,10 +42,7 @@ export default {
     });
 
     // retrieve the initial model
-    client.fetchHomePageModel(this.input)
-      .then(model => {
-        this.$store.commit('updateHomePageModel', model);
-      });
+    this.$store.dispatch('loadHomePageModel');
 
     setInterval(client.keepAlive, 60000);
   }
diff --git a/gms-ui/src/api/mock/index.js b/gms-ui/src/api/mock/index.js
index 33bd82d..c1c8e00 100644
--- a/gms-ui/src/api/mock/index.js
+++ b/gms-ui/src/api/mock/index.js
@@ -47,6 +47,9 @@ export default {
   addPermission() {
     return fetch(permissionsPanel);
   },
+  updatePermission() {
+    return fetch({"permission": "ADMIN"});
+  },
   getPermission() {
     return fetch(permission);
   },
diff --git a/gms-ui/src/api/server/index.js b/gms-ui/src/api/server/index.js
index 4006f18..f7079c8 100644
--- a/gms-ui/src/api/server/index.js
+++ b/gms-ui/src/api/server/index.js
@@ -1,35 +1,45 @@
 const BASE_API_URL = process.env.VUE_APP_API_BASE_URL;
 
-function apiRequest(url, options, showLoading = true) {
+import axios from 'axios';
+
+function apiRequest(options, showLoading = true, handleValidationErrors = false) {
   if (showLoading) {
     loading(true);
   }
-  return new Promise((resolve) => {
-    fetch(url, options)
+  return new Promise((resolve, reject) => {
+    axios(options)
       .then(response => {
-        loading(false);
-        if ([200, 201, 204, 400].includes(response.status)) { // valid status codes
-          if (response.status === 204) {
-            resolve({});
-          } else {
-            resolve(response.json());
-          }
+        if (response.status === 204) {
+          resolve({});
         } else {
-          response.json().then(jsonValue => dispatchApiErrorEvent(jsonValue));
+          resolve(response.data);
         }
+        loading(false);
       })
       .catch(error => {
+        if(handleValidationErrors && error.response && error.response.status === 400) {
+          reject(error.response.data);
+        } else {
+          dispatchApiErrorEvent(error);
+        }
         loading(false);
-        dispatchApiErrorEvent(error);
       });
   });
 }
 
 function dispatchApiErrorEvent(error) {
   let event = new CustomEvent('apiError');
+  let errorMessage;
+  if (error.response && error.response.data && error.response.data.message) {
+    errorMessage = error.response.data.message;
+  } else if (error.message) {
+    errorMessage = error.message;
+  } else {
+    errorMessage = 'Unknown error';
+  }
   event.message = {
     title: error.error || 'Error',
-    body: error.message || 'Unknown error'
+    body: errorMessage
   };
   document.dispatchEvent(event);
 }
@@ -47,13 +57,14 @@ export default {
       'home?groupId=' + input.selectedGroupId +
       '&paginatorPageSize=' + input.paginatorPageSize +
       '&paginatorPage=' + input.paginatorPage;
-    return apiRequest(url, {
+    return apiRequest({
       method: 'GET',
-      cache: 'no-cache',
-      credentials: 'include',
+      url: url,
+      withCredentials: true,
       headers: {
         'Content-Type': 'application/json',
         'Accept': 'application/json',
+        'Cache-Control': 'no-cache'
       }
     });
   },
@@ -66,13 +77,14 @@ export default {
     if (input.searchFilter !== null) {
       url += '&searchFilter=' + input.searchFilter;
     }
-    return apiRequest(url, {
+    return apiRequest({
       method: 'GET',
-      cache: 'no-cache',
-      credentials: 'include',
+      url: url,
+      withCredentials: true,
       headers: {
         'Content-Type': 'application/json',
         'Accept': 'application/json',
+        'Cache-Control': 'no-cache'
       }
     });
   },
@@ -85,13 +97,14 @@ export default {
     if (input.searchFilter !== null) {
       url += '&searchFilter=' + input.searchFilter;
     }
-    return apiRequest(url, {
+    return apiRequest({
       method: 'GET',
-      cache: 'no-cache',
-      credentials: 'include',
+      url: url,
+      withCredentials: true,
       headers: {
         'Content-Type': 'application/json',
         'Accept': 'application/json',
+        'Cache-Control': 'no-cache'
       }
     });
   },
@@ -100,13 +113,14 @@ export default {
       'members?groupId=' + input.selectedGroupId +
       '&paginatorPageSize=' + input.paginatorPageSize +
       '&paginatorPage=' + input.paginatorPage;
-    return apiRequest(url, {
+    return apiRequest({
       method: 'GET',
-      cache: 'no-cache',
-      credentials: 'include',
+      url: url,
+      withCredentials: true,
       headers: {
         'Content-Type': 'application/json',
         'Accept': 'application/json',
+        'Cache-Control': 'no-cache'
       }
     });
   },
@@ -115,54 +129,57 @@ export default {
       'permissions?groupId=' + input.selectedGroupId +
       '&paginatorPageSize=' + input.paginatorPageSize +
       '&paginatorPage=' + input.paginatorPage;
-    return apiRequest(url, {
+    return apiRequest({
       method: 'GET',
-      cache: 'no-cache',
-      credentials: 'include',
+      url: url,
+      withCredentials: true,
       headers: {
         'Content-Type': 'application/json',
         'Accept': 'application/json',
+        'Cache-Control': 'no-cache'
       }
     });
   },
   addGroup(newGroupName, leaf, input) {
     let url = BASE_API_URL + 'group';
-    return apiRequest(url, {
+    return apiRequest({
       method: 'POST',
-      cache: 'no-cache',
-      credentials: 'include',
+      url: url,
+      withCredentials: true,
       headers: {
         'Content-Type': 'application/json',
         'Accept': 'application/json',
+        'Cache-Control': 'no-cache'
       },
-      body: JSON.stringify({
+      data: {
         newGroupName: newGroupName,
         parentGroupId: input.selectedGroupId,
         paginatorPageSize: input.paginatorPageSize,
         paginatorPage: input.paginatorPage,
         searchFilter: input.searchFilter,
         leaf: leaf
-      })
-    });
+      }
+    }, true, true);
   },
   updateGroup(groupId, newGroupName, leaf, input) {
     let url = BASE_API_URL + 'group/' + groupId;
-    return apiRequest(url, {
+    return apiRequest({
       method: 'PUT',
-      cache: 'no-cache',
-      credentials: 'include',
+      url: url,
+      withCredentials: true,
       headers: {
         'Content-Type': 'application/json',
         'Accept': 'application/json',
+        'Cache-Control': 'no-cache'
       },
-      body: JSON.stringify({
+      data: {
         newGroupName: newGroupName,
         leaf: leaf,
         paginatorPageSize: input.paginatorPageSize,
         paginatorPage: input.paginatorPage,
         searchFilter: input.searchFilter
-      })
-    });
+      }
+    }, true, true);
   },
   removeGroup(groupId, input) {
     let url = BASE_API_URL + 'group/' + groupId +
@@ -171,80 +188,104 @@ export default {
     if (input.searchFilter !== null) {
       url += '&searchFilter=' + input.searchFilter;
     }
-    return apiRequest(url, {
+    return apiRequest({
       method: 'DELETE',
-      cache: 'no-cache',
-      credentials: 'include',
+      url: url,
+      withCredentials: true,
       headers: {
         'Content-Type': 'application/json',
         'Accept': 'application/json',
+        'Cache-Control': 'no-cache'
       }
     });
   },
   searchUser(searchInput) {
     let url = BASE_API_URL + 'users?search=' + searchInput;
 
-    return apiRequest(url, {
+    return apiRequest({
       method: 'GET',
-      cache: 'no-cache',
-      credentials: 'include',
+      url: url,
+      withCredentials: true,
       headers: {
-        'Content-Type': 'application/json',
         'Accept': 'application/json',
+        'Cache-Control': 'no-cache'
       }
     });
   },
-  addPermission(userId, permission, input) {
+  addPermission(userId, permission, input, override) {
     let url = BASE_API_URL + 'permission';
 
-    return apiRequest(url, {
+    return apiRequest({
       method: 'POST',
-      cache: 'no-cache',
-      credentials: 'include',
+      url: url,
+      withCredentials: true,
       headers: {
         'Content-Type': 'application/json',
         'Accept': 'application/json',
+        'Cache-Control': 'no-cache'
       },
-      body: JSON.stringify({
+      data: {
         groupId: input.selectedGroupId,
         userId: userId,
         permission: permission,
+        override: override,
         paginatorPageSize: input.paginatorPageSize,
         paginatorPage: input.paginatorPage
-      })
+      }
+    });
+  },
+  updatePermission(groupId, userId, permission) {
+    let url = BASE_API_URL + 'permission';
+
+    return apiRequest({
+      method: 'PUT',
+      url: url,
+      withCredentials: true,
+      headers: {
+        'Content-Type': 'application/json',
+        'Accept': 'application/json',
+        'Cache-Control': 'no-cache'
+      },
+      data: {
+        groupId: groupId,
+        userId: userId,
+        permission: permission
+      }
     });
   },
   getPermission(groupId, userId) {
     let url = BASE_API_URL + 'permission?groupId=' + groupId + '&userId=' + userId;
 
-    return apiRequest(url, {
+    return apiRequest({
       method: 'GET',
-      cache: 'no-cache',
-      credentials: 'include',
+      url: url,
+      withCredentials: true,
       headers: {
         'Content-Type': 'application/json',
         'Accept': 'application/json',
+        'Cache-Control': 'no-cache'
       }
     });
   },
   addMember(userId, permission, input) {
     let url = BASE_API_URL + 'member';
 
-    return apiRequest(url, {
+    return apiRequest({
       method: 'POST',
-      cache: 'no-cache',
-      credentials: 'include',
+      url: url,
+      withCredentials: true,
       headers: {
         'Content-Type': 'application/json',
         'Accept': 'application/json',
+        'Cache-Control': 'no-cache'
       },
-      body: JSON.stringify({
+      data: {
         groupId: input.selectedGroupId,
         userId: userId,
         permission: permission,
         paginatorPageSize: input.paginatorPageSize,
         paginatorPage: input.paginatorPage
-      })
+      }
     });
   },
   removeMember(userId, removeAlsoPermission, input) {
@@ -254,13 +295,14 @@ export default {
       '&removeAlsoPermission=' + removeAlsoPermission +
       '&paginatorPageSize=' + input.paginatorPageSize +
       '&paginatorPage=' + input.paginatorPage;
-    return apiRequest(url, {
+    return apiRequest({
       method: 'DELETE',
-      cache: 'no-cache',
-      credentials: 'include',
+      url: url,
+      withCredentials: true,
       headers: {
         'Content-Type': 'application/json',
         'Accept': 'application/json',
+        'Cache-Control': 'no-cache'
       }
     });
   },
@@ -270,13 +312,14 @@ export default {
       '&userId=' + userId +
       '&paginatorPageSize=' + input.paginatorPageSize +
       '&paginatorPage=' + input.paginatorPage;
-    return apiRequest(url, {
+    return apiRequest({
       method: 'DELETE',
-      cache: 'no-cache',
-      credentials: 'include',
+      url: url,
+      withCredentials: true,
       headers: {
         'Content-Type': 'application/json',
         'Accept': 'application/json',
+        'Cache-Control': 'no-cache'
       }
     });
   },
@@ -284,36 +327,41 @@ export default {
     let url = BASE_API_URL + 'search?query=' + input.genericSearch.filter +
       '&page=' + input.genericSearch.paginatorPage + '&pageSize=' + input.genericSearch.paginatorPageSize;
 
-    return apiRequest(url, {
+    return apiRequest({
       method: 'GET',
-      cache: 'no-cache',
-      credentials: 'include',
+      url: url,
+      withCredentials: true,
       headers: {
         'Content-Type': 'application/json',
         'Accept': 'application/json',
+        'Cache-Control': 'no-cache'
       }
     });
   },
   openUserSearchResult(userId) {
     let url = BASE_API_URL + 'search/user/' + userId;
 
-    return apiRequest(url, {
+    return apiRequest({
       method: 'GET',
-      cache: 'no-cache',
-      credentials: 'include',
+      url: url,
+      withCredentials: true,
       headers: {
         'Content-Type': 'application/json',
         'Accept': 'application/json',
+        'Cache-Control': 'no-cache'
       }
     });
   },
   keepAlive() {
     let url = BASE_API_URL + 'keepAlive';
 
-    return apiRequest(url, {
+    return apiRequest({
       method: 'GET',
-      cache: 'no-cache',
-      credentials: 'include'
+      url: url,
+      withCredentials: true,
+      headers: {
+        'Cache-Control': 'no-cache'
+      }
     }, false);
   }
 };
diff --git a/gms-ui/src/assets/logo.png b/gms-ui/src/assets/logo.png
deleted file mode 100644
index f3d2503fc2a44b5053b0837ebea6e87a2d339a43..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 6849
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

diff --git a/gms-ui/src/components/GenericSearchResults.vue b/gms-ui/src/components/GenericSearchResults.vue
index ea3ae44..e0277a7 100644
--- a/gms-ui/src/components/GenericSearchResults.vue
+++ b/gms-ui/src/components/GenericSearchResults.vue
@@ -17,11 +17,8 @@
 </template>
 
 <script>
-import client from 'api-client';
 import Paginator from './Paginator.vue';
-import {
-  mapState
-} from 'vuex';
+import { mapState } from 'vuex';
 
 export default {
   name: 'GenericSearchResults',
@@ -32,22 +29,26 @@ export default {
     model: state => state.model,
     input: state => state.input
   }),
+  created () {
+    this.updateSearchResults();
+  },
+  watch: {
+    // call again the method if the route changes
+    '$route': 'updateSearchResults'
+  },
   methods: {
     openSearchResult: function(result) {
       switch (result.type) {
         case 'GROUP':
-          this.$store.commit('openGroup', result.id);
+          this.$store.dispatch('openGroup', result.id);
           break;
         case 'USER':
-          this.$store.dispatch('openUserPage', result.id);
+          this.$router.push({ path: `/user/${result.id}` }, () => {});
           break;
       }
     },
     updateSearchResults: function() {
-      client.search(this.input)
-        .then(results => {
-          this.$store.commit('displaySearchResults', results);
-        });
+      this.$store.dispatch('search', this.$route.query.q);
     }
   }
 }
diff --git a/gms-ui/src/components/GroupsBreadcrumb.vue b/gms-ui/src/components/GroupsBreadcrumb.vue
index 629059b..283c847 100644
--- a/gms-ui/src/components/GroupsBreadcrumb.vue
+++ b/gms-ui/src/components/GroupsBreadcrumb.vue
@@ -1,7 +1,7 @@
 <template>
 <nav aria-label="breadcrumb">
   <ol class="breadcrumb">
-    <li class="breadcrumb-item" v-for="group in groups" v-bind:class="{ active: group.active }">
+    <li class="breadcrumb-item" v-for="group in groups" v-bind:class="{ active: group.active }" v-bind:key="group.groupId">
       <a href="#" v-on:click="changeBreadcrumb(group.groupId)" v-if="!group.active">{{group.groupName}}</a>
       <span v-if="group.active">{{group.groupName}}</span>
     </li>
@@ -10,9 +10,7 @@
 </template>
 
 <script>
-import {
-  mapState
-} from 'vuex';
+import { mapState } from 'vuex';
 import client from 'api-client';
 
 function buildItems(values) {
@@ -49,7 +47,7 @@ export default {
             this.$store.commit('updateGroups', model);
           });
       } else {
-        this.$store.commit('setTabIndex', 0);
+        this.$store.dispatch('changeTab', 0);
       }
     }
   }
diff --git a/gms-ui/src/components/GroupsPanel.vue b/gms-ui/src/components/GroupsPanel.vue
index dfdb7d7..d7dac81 100644
--- a/gms-ui/src/components/GroupsPanel.vue
+++ b/gms-ui/src/components/GroupsPanel.vue
@@ -6,7 +6,7 @@
     </b-col>
   </b-row>
   <div id="groups-list" v-if="model.groupsPanel !== null">
-    <b-list-group v-for="group in model.groupsPanel.items">
+    <b-list-group v-for="group in model.groupsPanel.items" v-bind:key="group.groupId">
       <b-list-group-item href="#" v-on:click="openGroup(group)">
         <span class="float-left">{{group.groupName}}</span>
         <span v-if="group.permission === 'ADMIN'" class="float-right">
@@ -32,10 +32,7 @@
 import EditGroupModal from './modals/EditGroupModal.vue';
 import ConfirmRemoveGroupModal from './modals/ConfirmRemoveGroupModal.vue';
 import Paginator from './Paginator.vue';
-import {
-  mapState,
-  mapActions
-} from 'vuex';
+import { mapState } from 'vuex';
 import client from 'api-client';
 import debounce from 'debounce'; // for delaying the input event (search filter)
 
@@ -52,7 +49,7 @@ export default {
   }),
   methods: {
     openGroup: function(group) {
-      this.$store.commit('openGroup', group.groupId);
+      this.$store.dispatch('openGroup', group.groupId);
     },
     openEditGroupModal: function(group) {
       this.$refs.editGroupModal.openEditGroupModal(group);
diff --git a/gms-ui/src/components/Main.vue b/gms-ui/src/components/Main.vue
index 055309c..7c7829f 100644
--- a/gms-ui/src/components/Main.vue
+++ b/gms-ui/src/components/Main.vue
@@ -28,10 +28,7 @@ import PermissionsPanel from './PermissionsPanel.vue'
 import AddGroupModal from './modals/AddGroupModal.vue'
 import AddMemberModal from './modals/AddMemberModal.vue'
 import AddPermissionModal from './modals/AddPermissionModal.vue'
-import {
-  mapState
-} from 'vuex';
-import client from 'api-client';
+import { mapState } from 'vuex';
 
 export default {
   name: 'Main',
@@ -54,28 +51,7 @@ export default {
   }),
   methods: {
     tabChanged: function(tabIndex) {
-      // reset paginator
-      this.input.paginatorPage = 1;
-      switch (tabIndex) {
-        case 0:
-          client.fetchGroupsTab(this.input)
-            .then(model => {
-              this.$store.commit('updateGroups', model);
-            });
-          break;
-        case 1:
-          client.fetchMembersPanel(this.input)
-            .then(panel => {
-              this.$store.commit('updateMembersPanel', panel);
-            });
-          break;
-        case 2:
-          client.fetchPermissionsPanel(this.input)
-            .then(panel => {
-              this.$store.commit('updatePermissionsPanel', panel);
-            });
-          break;
-      }
+      this.$store.dispatch('changeTab', tabIndex);
     },
     openAddGroupModal: function() {
       this.$bvModal.show('add-group-modal');
diff --git a/gms-ui/src/components/MembersPanel.vue b/gms-ui/src/components/MembersPanel.vue
index 30ea021..34af9ce 100644
--- a/gms-ui/src/components/MembersPanel.vue
+++ b/gms-ui/src/components/MembersPanel.vue
@@ -1,8 +1,8 @@
 <template>
 <b-tab title="Members" v-if="model.permission === 'ADMIN' || model.permission === 'MANAGE_MEMBERS' || model.permission === 'VIEW_MEMBERS'">
   <div v-if="model.membersPanel !== null">
-    <b-list-group v-for="member in model.membersPanel.items" id="members-list">
-      <b-list-group-item href="#" @click="openUser(member)">
+    <b-list-group v-for="member in model.membersPanel.items" id="members-list" v-bind:key="member.memberId">
+      <b-list-group-item href="#" @click.prevent="openUser(member)">
         <div class="float-left">
           <User :user="member" :anchor="false" />
         </div>
@@ -25,9 +25,7 @@ import client from 'api-client';
 import User from './User.vue';
 import Paginator from './Paginator.vue';
 import ConfirmRemoveMemberModal from './modals/ConfirmRemoveMemberModal.vue';
-import {
-  mapState
-} from 'vuex';
+import { mapState } from 'vuex';
 
 export default {
   name: 'MembersPanel',
@@ -54,13 +52,13 @@ export default {
         });
     },
     openUser(member) {
-      this.$store.dispatch('openUserPage', member.id);
+      this.$router.push({ path: `/user/${member.id}` }, () => {});
     }
   }
 }
 
 function getMemberPermission(self, member) {
-  return new Promise((resolve, reject) => {
+  return new Promise((resolve) => {
     if (self.model.permission === 'ADMIN') {
       let groupId = self.$store.state.input.selectedGroupId;
       client.getPermission(groupId, member.id)
diff --git a/gms-ui/src/components/Paginator.vue b/gms-ui/src/components/Paginator.vue
index b6ee2a0..32c89da 100644
--- a/gms-ui/src/components/Paginator.vue
+++ b/gms-ui/src/components/Paginator.vue
@@ -18,10 +18,6 @@
 </template>
 
 <script>
-import {
-  mapState
-} from 'vuex';
-
 export default {
   name: 'Paginator',
   props: {
diff --git a/gms-ui/src/components/PermissionsPanel.vue b/gms-ui/src/components/PermissionsPanel.vue
index a8537de..8a30a8c 100644
--- a/gms-ui/src/components/PermissionsPanel.vue
+++ b/gms-ui/src/components/PermissionsPanel.vue
@@ -10,11 +10,30 @@
         </tr>
       </thead>
       <tbody>
-        <tr v-for="up in model.permissionsPanel.items">
+        <tr v-for="(up, index) in model.permissionsPanel.items" v-bind:key="index">
           <td>
             <User :user="up.user" :anchor="true" />
           </td>
-          <td>{{up.permission}}</td>
+          <td v-if="!up.editable">
+            {{up.permission}}
+            <a href="#" v-on:click.stop.prevent="openPermissionEditor(index)">
+              <font-awesome-icon icon="edit"></font-awesome-icon>
+            </a>
+          </td>
+          <td v-if="up.editable" class="col-3">
+            <b-input-group>
+              <b-form-select v-model="up.permission">
+                <option value="ADMIN">ADMIN</option>
+                <option value="MANAGE_MEMBERS">MANAGE_MEMBERS</option>
+                <option value="VIEW_MEMBERS">VIEW_MEMBERS</option>
+              </b-form-select>
+              <b-input-group-append>
+                <b-button @click="savePermission(up, index)">
+                  <font-awesome-icon icon="save"></font-awesome-icon>
+                </b-button>
+              </b-input-group-append>
+            </b-input-group>
+          </td>
           <td>
             <a href="#" v-on:click.stop="openRemovePermissionModal(up.user)" class="text-danger" title="Remove permission">
               <font-awesome-icon icon="trash"></font-awesome-icon>
@@ -59,6 +78,16 @@ export default {
         .then(panel => {
           this.$store.commit('updatePermissionsPanel', panel);
         });
+    },
+    openPermissionEditor(index) {
+      this.$store.commit('togglePermissionEditable', index);
+    },
+    savePermission(up, index) {
+      let permission = up.permission;
+      client.updatePermission(this.input.selectedGroupId, up.user.id, permission)
+        .then(() => {
+          this.$store.commit('togglePermissionEditable', index);
+        });
     }
   }
 }
diff --git a/gms-ui/src/components/TopMenu.vue b/gms-ui/src/components/TopMenu.vue
index fc6e5bc..b71a95c 100644
--- a/gms-ui/src/components/TopMenu.vue
+++ b/gms-ui/src/components/TopMenu.vue
@@ -10,7 +10,7 @@
       <!-- Right aligned nav items -->
       <b-navbar-nav class="ml-auto">
         <b-nav-form>
-          <b-form-input size="sm" class="mr-sm-2" placeholder="Search" v-model="input.genericSearch.filter" @keydown.native.enter="genericSearch"></b-form-input>
+          <b-form-input size="sm" class="mr-sm-2" placeholder="Search" v-model="input.genericSearch.filter" @keydown.native.enter.prevent="genericSearch"></b-form-input>
           <b-button size="sm" class="my-2 my-sm-0" type="button" v-on:click="genericSearch()">Search</b-button>
         </b-nav-form>
         <b-nav-item-dropdown :text="user" right v-if="user">
@@ -23,10 +23,7 @@
 </template>
 
 <script>
-import client from 'api-client';
-import {
-  mapState
-} from 'vuex';
+import { mapState } from 'vuex';
 
 export default {
   name: 'TopMenu',
@@ -38,20 +35,13 @@ export default {
   }),
   methods: {
     showMainPage() {
-      this.$store.commit('showMainPage');
+      this.$router.push('/', () => {});
     },
-    genericSearch(event) {
-      // prevent the page reload if enter is pressed
-      if (event) {
-        event.preventDefault();
-      }
-
+    genericSearch() {
       this.input.genericSearch.page = 1;
       this.input.genericSearch.pageSize = 20;
-      client.search(this.input)
-        .then(results => {
-          this.$store.commit('displaySearchResults', results);
-        });
+      this.$router.push({ path: '/search', query: { q: this.input.genericSearch.filter } }, () => {});
+      this.$store.dispatch('search', this.input.genericSearch.filter);
     }
   }
 }
diff --git a/gms-ui/src/components/User.vue b/gms-ui/src/components/User.vue
index 2c6c16e..7c2af04 100644
--- a/gms-ui/src/components/User.vue
+++ b/gms-ui/src/components/User.vue
@@ -1,12 +1,12 @@
 <template>
 <div :id="'user-name-' + user.id">
-  <component :is="anchor ? 'a' : 'span'" :href="anchor ? '#' : false" @click="openUser">{{user.displayName}}</component>
+  <component :is="anchor ? 'a' : 'span'" :href="anchor ? '#' : false" @click.prevent="openUser">{{user.displayName}}</component>
   <b-tooltip ref="user-tooltip" :target="'user-name-' + user.id" placement="bottom">
     <div class="text-left">
       <p><strong>User id</strong>: {{user.id}}</p>
       <p><strong>Identities</strong>:</p>
       <ul>
-        <li v-for="identity in user.identities" v-bind:key="identity.typedId">
+        <li v-for="(identity, index) in user.identities" v-bind:key="index">
           {{identity.email}} ({{identity.type}})
         </li>
       </ul>
@@ -24,7 +24,7 @@ export default {
   },
   methods: {
     openUser() {
-      this.$store.dispatch('openUserPage', this.user.id);
+      this.$router.push({ path: `/user/${this.user.id}` }, () => {});
     }
   }
 }
diff --git a/gms-ui/src/components/UserSearchResult.vue b/gms-ui/src/components/UserSearchResult.vue
index c648aff..d5ccf14 100644
--- a/gms-ui/src/components/UserSearchResult.vue
+++ b/gms-ui/src/components/UserSearchResult.vue
@@ -1,5 +1,5 @@
 <template>
-<div class="mt-sm-3" v-if="user !== null">
+<div class="mt-sm-3" v-if="user">
   <b-button variant="primary" class="float-right" v-on:click="back()">Back</b-button>
   <h5><strong>{{user.displayName}}</strong>:</h5>
 
@@ -29,7 +29,7 @@
         <b-row>
           <b-col lg="10">
             <b-list-group>
-              <b-list-group-item v-for="identity in user.identities" v-bind:key="identity.typedId">
+              <b-list-group-item v-for="(identity, index) in user.identities" v-bind:key="index">
                 <dl class="mb-0 ml-0 row">
                   <dt class="col-3">Type</dt><dd class="col-9">{{identity.type}}</dd>
                   <dt class="col-3">Email</dt><dd class="col-9">{{identity.email}}</dd>
@@ -64,14 +64,11 @@
       </b-col>
     </b-row>
   </b-container>
-
 </div>
 </template>
 
 <script>
-import {
-  mapState
-} from 'vuex';
+import { mapState } from 'vuex';
 
 export default {
   name: 'UserSearchResult',
@@ -80,12 +77,22 @@ export default {
     groups: state => state.model.userSearchResults.groups,
     permissions: state => state.model.userSearchResults.permissions
   }),
+  created() {
+    this.openUserPage();
+  },
+  watch: {
+    // call again the method if the route changes
+    '$route': 'openUserPage'
+  },
   methods: {
     back() {
-      this.$store.commit('backFromUserPage');
+      this.$router.go(-1);
+    },
+    openUserPage() {
+      this.$store.dispatch('openUserPage', this.$route.params.id);
     },
     openGroup(groupId) {
-      this.$store.commit('openGroup', groupId);
+      this.$store.dispatch('openGroup', groupId);
     }
   }
 }
diff --git a/gms-ui/src/components/modals/AddGroupModal.vue b/gms-ui/src/components/modals/AddGroupModal.vue
index 7f3bcd1..8850297 100644
--- a/gms-ui/src/components/modals/AddGroupModal.vue
+++ b/gms-ui/src/components/modals/AddGroupModal.vue
@@ -50,12 +50,11 @@ export default {
       } else {
         client.addGroup(this.newGroupName, this.leaf, this.$store.state.input)
           .then(res => {
-            if (res.status === 400) {
-              this.newGroupNameError = res.message;
-            } else {
-              this.$store.commit('updateGroupsPanel', res);
-              this.$bvModal.hide('add-group-modal');
-            }
+            this.$store.commit('updateGroupsPanel', res);
+            this.$bvModal.hide('add-group-modal');
+          })
+          .catch(res => {
+            this.newGroupNameError = res.message;
           });
       }
     }
diff --git a/gms-ui/src/components/modals/AddMemberModal.vue b/gms-ui/src/components/modals/AddMemberModal.vue
index b021b10..13e91d9 100644
--- a/gms-ui/src/components/modals/AddMemberModal.vue
+++ b/gms-ui/src/components/modals/AddMemberModal.vue
@@ -7,9 +7,6 @@
 <script>
 import client from 'api-client';
 import SearchUser from './SearchUser.vue'
-import {
-  mapState
-} from 'vuex';
 
 export default {
   name: 'AddMemberModal',
diff --git a/gms-ui/src/components/modals/AddPermissionModal.vue b/gms-ui/src/components/modals/AddPermissionModal.vue
index 9fdb437..b210f47 100644
--- a/gms-ui/src/components/modals/AddPermissionModal.vue
+++ b/gms-ui/src/components/modals/AddPermissionModal.vue
@@ -1,6 +1,9 @@
 <template>
-<b-modal id="add-permission-modal" title="Add permission" @shown="afterShow" ok-title="Add" @ok="addPermission">
+<b-modal id="add-permission-modal" title="Add permission" @show="beforeShow" @shown="afterShow" :ok-title="okTitle" @ok="addPermission" :ok-variant="okBtnVariant">
   <SearchUser ref="searchUser" @searchUserEnter="addPermission" />
+  <b-alert :show="!!existingPermission" variant="warning" class="mt-3">
+    <strong>Warning</strong>: the user has already a permission ({{existingPermission}}). Click confirm to override it.
+  </b-alert>
 </b-modal>
 </template>
 
@@ -13,7 +16,23 @@ export default {
   components: {
     SearchUser
   },
+  computed: {
+    okBtnVariant: function() {
+      return this.existingPermission ? 'danger' : 'primary';
+    },
+    okTitle: function() {
+      return this.existingPermission ? 'Confirm' : 'Add';
+    }
+  },
+  data() {
+    return {
+      existingPermission: null
+    };
+  },
   methods: {
+    beforeShow() {
+      this.existingPermission = null;
+    },
     afterShow: function() {
       this.$refs.searchUser.$refs.userInput.focus();
     },
@@ -25,15 +44,23 @@ export default {
 
       let userId = this.$refs.searchUser.selectedUser;
       let permission = this.$refs.searchUser.permission;
+      let input = this.$store.state.input;
 
       if (!userId || !permission) {
         return;
       }
 
-      client.addPermission(userId, permission, this.$store.state.input)
+      client.getPermission(input.selectedGroupId, userId)
         .then(res => {
-          this.$store.commit('updatePermissionsPanel', res);
-          this.$bvModal.hide('add-permission-modal');
+          if (res.permission && res.permission !== permission && !this.existingPermission) {
+            this.existingPermission = res.permission;
+          } else {
+            client.addPermission(userId, permission, input, !!this.existingPermission)
+              .then(res => {
+                this.$store.commit('updatePermissionsPanel', res);
+                this.$bvModal.hide('add-permission-modal');
+              });
+          }
         });
     }
   }
diff --git a/gms-ui/src/components/modals/ConfirmRemoveMemberModal.vue b/gms-ui/src/components/modals/ConfirmRemoveMemberModal.vue
index 0d85f65..5227c1e 100644
--- a/gms-ui/src/components/modals/ConfirmRemoveMemberModal.vue
+++ b/gms-ui/src/components/modals/ConfirmRemoveMemberModal.vue
@@ -9,10 +9,7 @@
 
 <script>
 import client from 'api-client';
-import {
-  mapState,
-  mapActions
-} from 'vuex';
+import { mapState } from 'vuex';
 
 export default {
   name: 'ConfirmRemoveGroupModal',
diff --git a/gms-ui/src/components/modals/ConfirmRemovePermissionModal.vue b/gms-ui/src/components/modals/ConfirmRemovePermissionModal.vue
index 978a590..e3d8b98 100644
--- a/gms-ui/src/components/modals/ConfirmRemovePermissionModal.vue
+++ b/gms-ui/src/components/modals/ConfirmRemovePermissionModal.vue
@@ -6,10 +6,7 @@
 
 <script>
 import client from 'api-client';
-import {
-  mapState,
-  mapActions
-} from 'vuex';
+import { mapState } from 'vuex';
 
 export default {
   name: 'ConfirmRemovePermissionModal',
diff --git a/gms-ui/src/components/modals/EditGroupModal.vue b/gms-ui/src/components/modals/EditGroupModal.vue
index d699c64..bfd117c 100644
--- a/gms-ui/src/components/modals/EditGroupModal.vue
+++ b/gms-ui/src/components/modals/EditGroupModal.vue
@@ -58,12 +58,11 @@ export default {
 
       client.updateGroup(this.groupId, this.newGroupName, this.leaf, this.$store.state.input)
         .then(res => {
-          if (res.status === 400) {
-            this.newGroupNameError = res.message;
-          } else {
-            this.$store.commit('updateGroupsPanel', res);
-            this.$bvModal.hide('edit-group-modal');
-          }
+          this.$store.commit('updateGroupsPanel', res);
+          this.$bvModal.hide('edit-group-modal');
+        })
+        .catch(res => {
+          this.newGroupNameError = res.message;
         });
     }
   }
diff --git a/gms-ui/src/components/modals/SearchUser.vue b/gms-ui/src/components/modals/SearchUser.vue
index bb55488..ebb0287 100644
--- a/gms-ui/src/components/modals/SearchUser.vue
+++ b/gms-ui/src/components/modals/SearchUser.vue
@@ -1,7 +1,7 @@
 <template>
 <b-form inline>
   <label class="w-25" for="user-input">Search:</label>
-  <b-form-input v-model="searchInput" id="user-input" ref="userInput" class="w-75 mb-2" aria-describedby="user-input-feedback" v-on:input="searchUser" @keydown.native.enter="enterPressed" placeholder="User">
+  <b-form-input v-model="searchInput" id="user-input" ref="userInput" class="w-75 mb-2" aria-describedby="user-input-feedback" v-on:input="searchUser" @keydown.native.enter.prevent="enterPressed" placeholder="User">
   </b-form-input>
   <label class="w-25" for="user-input" v-if="users.length > 0">Selected user:</label>
   <b-form-select v-model="selectedUser" :options="users" class="w-75" v-if="users.length > 0"></b-form-select>
@@ -18,18 +18,15 @@
 
 <script>
 import client from 'api-client';
-import {
-  mapState
-} from 'vuex';
 
 export default {
   name: 'SearchUser',
   properties: {
     actionOnEnter: Function
   },
-  computed: mapState({
-    model: state => state.model,
-  }),
+  computed: {
+    model: function() { return this.$store.state.model }
+  },
   data: function() {
     return {
       searchInput: null,
diff --git a/gms-ui/src/main.js b/gms-ui/src/main.js
index 58c7221..255ab43 100644
--- a/gms-ui/src/main.js
+++ b/gms-ui/src/main.js
@@ -5,13 +5,14 @@ import store from './store.js'
 import './plugins/bootstrap-vue'
 import App from './App.vue'
 import { library } from '@fortawesome/fontawesome-svg-core'
-import { faTrash, faEdit, faSpinner, faFolder, faUser } from '@fortawesome/free-solid-svg-icons'
+import { faTrash, faEdit, faSpinner, faFolder, faUser, faSave } from '@fortawesome/free-solid-svg-icons'
 import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
 
 import VueRouter from 'vue-router'
 Vue.use(VueRouter)
+import router from './router.js';
 
-library.add(faTrash, faEdit, faSpinner, faFolder, faUser);
+library.add(faTrash, faEdit, faSpinner, faFolder, faUser, faSave);
 
 Vue.component('font-awesome-icon', FontAwesomeIcon);
 
@@ -19,5 +20,6 @@ Vue.config.productionTip = false;
 
 new Vue({
   render: h => h(App),
-  store
+  store,
+  router
 }).$mount('#app');
diff --git a/gms-ui/src/router.js b/gms-ui/src/router.js
new file mode 100644
index 0000000..a250a1f
--- /dev/null
+++ b/gms-ui/src/router.js
@@ -0,0 +1,21 @@
+import VueRouter from 'vue-router';
+
+import Main from './components/Main.vue';
+import GenericSearchResults from './components/GenericSearchResults.vue';
+import UserSearchResult from './components/UserSearchResult.vue';
+
+export default new VueRouter({
+  routes: [{
+      path: '/',
+      component: Main
+    },
+    {
+      path: '/search',
+      component: GenericSearchResults
+    },
+    {
+      path: '/user/:id',
+      component: UserSearchResult
+    }
+  ]
+});
diff --git a/gms-ui/src/store.js b/gms-ui/src/store.js
index f175ec5..fe02ab5 100644
--- a/gms-ui/src/store.js
+++ b/gms-ui/src/store.js
@@ -3,6 +3,7 @@
 import Vue from 'vue';
 import Vuex from 'vuex';
 import client from 'api-client';
+import router from './router.js';
 
 Vue.use(Vuex);
 
@@ -17,9 +18,9 @@ export default new Vuex.Store({
       permission: null,
       leaf: false,
       user: null,
-      genericSearchResults: [],
+      genericSearchResults: {},
       userSearchResults: {
-        userLabel: null,
+        user: null,
         groups: {},
         permissions: {}
       }
@@ -37,84 +38,112 @@ export default new Vuex.Store({
         paginatorPageSize: 20
       }
     },
-    loading: false,
-    previousPage: null,
-    page: 'main'
+    loading: false
   },
   mutations: {
     updateHomePageModel(state, model) {
-      this.state.model.breadcrumbs = model.breadcrumbs;
-      this.state.model.groupsPanel = model.groupsPanel;
-      this.state.model.permission = model.permission;
-      this.state.model.user = model.user;
-    },
-    openGroup(state, groupId) {
-      let input = this.state.input;
-      input.selectedGroupId = groupId;
-      input.searchFilter = '';
-      client.fetchGroupsTab(input)
-        .then(model => {
-          this.commit('updateGroups', model);
-          if (model.leaf) {
-            // If there are no subgroups show the members panel
-            this.commit('setTabIndex', 1);
-          } else {
-            this.commit('setTabIndex', 0);
-          }
-          this.commit('showMainPage');
-        });
+      state.model.breadcrumbs = model.breadcrumbs;
+      state.model.groupsPanel = model.groupsPanel;
+      state.model.permission = model.permission;
+      state.model.user = model.user;
     },
     updateGroups(state, model) {
-      this.state.model.breadcrumbs = model.breadcrumbs;
-      this.state.model.groupsPanel = model.groupsPanel;
-      this.state.model.permission = model.permission;
-      this.state.model.leaf = model.leaf;
+      state.model.breadcrumbs = model.breadcrumbs;
+      state.model.groupsPanel = model.groupsPanel;
+      state.model.permission = model.permission;
+      state.model.leaf = model.leaf;
     },
     updateGroupsPanel(state, groupsPanel) {
-      this.state.model.groupsPanel = groupsPanel;
-      this.state.input.paginatorPage = groupsPanel.currentPage;
+      state.model.groupsPanel = groupsPanel;
+      state.input.paginatorPage = groupsPanel.currentPage;
     },
     updatePermissionsPanel(state, permissionsPanel) {
-      this.state.model.permissionsPanel = permissionsPanel;
-      this.state.input.paginatorPage = permissionsPanel.currentPage;
+      state.model.permissionsPanel = permissionsPanel;
+      for(let up of permissionsPanel.items) {
+        Vue.set(up, 'editable', false);
+      }
+      state.input.paginatorPage = permissionsPanel.currentPage;
+    },
+    togglePermissionEditable(state, index) {
+      let up = state.model.permissionsPanel.items[index];
+      up.editable = !up.editable;
     },
     updateMembersPanel(state, membersPanel) {
-      this.state.model.membersPanel = membersPanel;
-      this.state.input.paginatorPage = membersPanel.currentPage;
+      state.model.membersPanel = membersPanel;
+      state.input.paginatorPage = membersPanel.currentPage;
     },
     setTabIndex(state, tabIndex) {
       // this will trigger the tabChanged() method in Main.vue
-      this.state.input.tabIndex = tabIndex;
+      state.input.tabIndex = tabIndex;
     },
     setLoading(state, loading) {
-      this.state.loading = loading;
-    },
-    showMainPage(state) {
-      this.state.page = 'main';
+      state.loading = loading;
     },
     displaySearchResults(state, results) {
-      this.state.page = 'search';
       if (results) {
-        this.state.model.genericSearchResults = results;
+        state.model.genericSearchResults = results;
       }
     },
     updateSearchResults(state, results) {
-      this.state.model.genericSearchResults = results;
+      state.model.genericSearchResults = results;
+    },
+    setUserSearchModel(state, model) {
+      state.model.userSearchResults.user = model.user;
+      state.model.userSearchResults.groups = model.groups;
+      state.model.userSearchResults.permissions = model.permissions;
     },
-    backFromUserPage(state) {
-      state.page = state.previousPage;
+    setGenericSearchFilter(state, filter) {
+      state.input.genericSearch.filter = filter;
     }
   },
   actions: {
-    openUserPage({ state }, userId) {
-      state.previousPage = state.page;
-      client.openUserSearchResult(userId)
+    loadHomePageModel({ commit, state }) {
+      client.fetchHomePageModel(state.input)
+        .then(model => commit('updateHomePageModel', model));
+    },
+    search({ commit, state }, filter) {
+      commit('setGenericSearchFilter', filter);
+      client.search(state.input)
+        .then(results => commit('displaySearchResults', results));
+    },
+    changeTab({ commit, state }, tabIndex) {
+      state.input.tabIndex = tabIndex;
+      // reset paginator
+      state.input.paginatorPage = 1;
+      switch (tabIndex) {
+        case 0:
+          client.fetchGroupsTab(state.input)
+            .then(model => commit('updateGroups', model));
+          break;
+        case 1:
+          client.fetchMembersPanel(state.input)
+            .then(panel => commit('updateMembersPanel', panel));
+          break;
+        case 2:
+          client.fetchPermissionsPanel(state.input)
+            .then(panel => commit('updatePermissionsPanel', panel));
+          break;
+      }
+    },
+    openGroup({ commit, dispatch, state }, groupId) {
+      let input = state.input;
+      input.selectedGroupId = groupId;
+      input.searchFilter = '';
+      client.fetchGroupsTab(input)
         .then(model => {
-          state.page = 'userSearch';
-          state.model.userSearchResults.user = model.user;
-          state.model.userSearchResults.groups = model.groups;
-          state.model.userSearchResults.permissions = model.permissions;
+          commit('updateGroups', model);
+          if (model.leaf) {
+            // If there are no subgroups show the members panel
+            dispatch('changeTab', 1);
+          } else {
+            dispatch('changeTab', 0);
+          }
+          router.push('/', () => {});
         });
+    },
+    openUserPage({ commit }, userId) {
+      client.openUserSearchResult(userId)
+        .then(model => commit('setUserSearchModel', model));
     }
   },
   getters: {
diff --git a/gms/src/main/java/it/inaf/ia2/gms/controller/PermissionsController.java b/gms/src/main/java/it/inaf/ia2/gms/controller/PermissionsController.java
index 5840978..9742396 100644
--- a/gms/src/main/java/it/inaf/ia2/gms/controller/PermissionsController.java
+++ b/gms/src/main/java/it/inaf/ia2/gms/controller/PermissionsController.java
@@ -9,7 +9,9 @@ import it.inaf.ia2.gms.model.request.PaginatedModelRequest;
 import it.inaf.ia2.gms.model.Permission;
 import it.inaf.ia2.gms.model.UserPermission;
 import it.inaf.ia2.gms.model.request.TabRequest;
+import it.inaf.ia2.gms.model.request.UpdatePermissionRequest;
 import it.inaf.ia2.gms.persistence.model.GroupEntity;
+import it.inaf.ia2.gms.persistence.model.PermissionEntity;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
@@ -22,6 +24,7 @@ import org.springframework.http.ResponseEntity;
 import org.springframework.web.bind.annotation.DeleteMapping;
 import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
 import org.springframework.web.bind.annotation.RequestBody;
 import org.springframework.web.bind.annotation.RequestParam;
 import org.springframework.web.bind.annotation.RestController;
@@ -63,11 +66,27 @@ public class PermissionsController {
     public ResponseEntity<PaginatedData<UserPermission>> addPermission(@Valid @RequestBody AddPermissionRequest request) {
 
         GroupEntity group = groupsManager.getGroupById(request.getGroupId());
-        permissionsManager.addPermission(group, request.getUserId(), request.getPermission());
+        if (request.isOverride()) {
+            permissionsManager.updatePermission(group, request.getUserId(), request.getPermission());
+        } else {
+            permissionsManager.addPermission(group, request.getUserId(), request.getPermission());
+        }
 
         return new ResponseEntity<>(getPermissionsPanel(group, request), HttpStatus.CREATED);
     }
 
+    @PutMapping(value = "/permission", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
+    public ResponseEntity<Map<String, String>> updatePermission(@Valid @RequestBody UpdatePermissionRequest request) {
+
+        GroupEntity group = groupsManager.getGroupById(request.getGroupId());
+        PermissionEntity updatedEntity = permissionsManager.updatePermission(group, request.getUserId(), request.getPermission());
+
+        Map<String, String> response = new HashMap<>();
+        response.put("permission", updatedEntity.getPermission().toString());
+
+        return ResponseEntity.ok(response);
+    }
+
     @DeleteMapping(value = "/permission", produces = MediaType.APPLICATION_JSON_VALUE)
     public ResponseEntity<PaginatedData<UserPermission>> deletePermission(@Valid MemberRequest request) {
 
diff --git a/gms/src/main/java/it/inaf/ia2/gms/controller/UsersController.java b/gms/src/main/java/it/inaf/ia2/gms/controller/UsersController.java
index 595b1e4..e5d908c 100644
--- a/gms/src/main/java/it/inaf/ia2/gms/controller/UsersController.java
+++ b/gms/src/main/java/it/inaf/ia2/gms/controller/UsersController.java
@@ -16,7 +16,7 @@ public class UsersController {
     @Autowired
     private RapClient rapClient;
 
-    @GetMapping(value = "users", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
+    @GetMapping(value = "users", produces = MediaType.APPLICATION_JSON_VALUE)
     public ResponseEntity<List<RapUser>> searchUsers(@RequestParam("search") String searchText) {
         return ResponseEntity.ok(rapClient.searchUsers(searchText));
     }
diff --git a/gms/src/main/java/it/inaf/ia2/gms/manager/PermissionsManager.java b/gms/src/main/java/it/inaf/ia2/gms/manager/PermissionsManager.java
index a1503f0..238797c 100644
--- a/gms/src/main/java/it/inaf/ia2/gms/manager/PermissionsManager.java
+++ b/gms/src/main/java/it/inaf/ia2/gms/manager/PermissionsManager.java
@@ -83,6 +83,11 @@ public class PermissionsManager extends UserAwareComponent {
         return permissionsService.addPermission(group, userId, permission);
     }
 
+    public PermissionEntity updatePermission(GroupEntity group, String userId, Permission permission) {
+        verifyUserCanManagePermissions(group);
+        return permissionsService.updatePermission(group, userId, permission);
+    }
+
     public void removePermission(GroupEntity group, String userId) {
         verifyUserCanManagePermissions(group);
         permissionsService.removePermission(group, userId);
diff --git a/gms/src/main/java/it/inaf/ia2/gms/model/request/AddPermissionRequest.java b/gms/src/main/java/it/inaf/ia2/gms/model/request/AddPermissionRequest.java
index c10ae5f..c1d4fd0 100644
--- a/gms/src/main/java/it/inaf/ia2/gms/model/request/AddPermissionRequest.java
+++ b/gms/src/main/java/it/inaf/ia2/gms/model/request/AddPermissionRequest.java
@@ -13,6 +13,8 @@ public class AddPermissionRequest extends PaginatedModelRequest {
     @NotNull
     private Permission permission;
 
+    private boolean override;
+    
     public String getGroupId() {
         return groupId;
     }
@@ -36,4 +38,12 @@ public class AddPermissionRequest extends PaginatedModelRequest {
     public void setPermission(Permission permission) {
         this.permission = permission;
     }
+
+    public boolean isOverride() {
+        return override;
+    }
+
+    public void setOverride(boolean override) {
+        this.override = override;
+    }
 }
diff --git a/gms/src/main/java/it/inaf/ia2/gms/model/request/UpdatePermissionRequest.java b/gms/src/main/java/it/inaf/ia2/gms/model/request/UpdatePermissionRequest.java
new file mode 100644
index 0000000..2125f0d
--- /dev/null
+++ b/gms/src/main/java/it/inaf/ia2/gms/model/request/UpdatePermissionRequest.java
@@ -0,0 +1,39 @@
+package it.inaf.ia2.gms.model.request;
+
+import it.inaf.ia2.gms.model.Permission;
+import javax.validation.constraints.NotEmpty;
+import javax.validation.constraints.NotNull;
+
+public class UpdatePermissionRequest {
+
+    @NotEmpty
+    private String groupId;
+    @NotEmpty
+    private String userId;
+    @NotNull
+    private Permission permission;
+
+    public String getGroupId() {
+        return groupId;
+    }
+
+    public void setGroupId(String groupId) {
+        this.groupId = groupId;
+    }
+
+    public String getUserId() {
+        return userId;
+    }
+
+    public void setUserId(String userId) {
+        this.userId = userId;
+    }
+
+    public Permission getPermission() {
+        return permission;
+    }
+
+    public void setPermission(Permission permission) {
+        this.permission = permission;
+    }
+}
diff --git a/gms/src/main/java/it/inaf/ia2/gms/persistence/PermissionsDAO.java b/gms/src/main/java/it/inaf/ia2/gms/persistence/PermissionsDAO.java
index d8d417e..4ffd9b9 100644
--- a/gms/src/main/java/it/inaf/ia2/gms/persistence/PermissionsDAO.java
+++ b/gms/src/main/java/it/inaf/ia2/gms/persistence/PermissionsDAO.java
@@ -41,6 +41,24 @@ public class PermissionsDAO {
         return userPermission;
     }
 
+    public PermissionEntity updatePermission(PermissionEntity userPermission, Permission newPermission) {
+
+        String sql = "UPDATE gms_permission SET permission = ? WHERE group_id = ? AND user_id = ? AND group_path = ?";
+
+        userPermission.setPermission(newPermission);
+        
+        jdbcTemplate.update(conn -> {
+            PreparedStatement ps = conn.prepareStatement(sql);
+            ps.setObject(1, userPermission.getPermission().toString(), Types.OTHER);
+            ps.setString(2, userPermission.getGroupId());
+            ps.setString(3, userPermission.getUserId());
+            ps.setObject(4, userPermission.getGroupPath(), Types.OTHER);
+            return ps;
+        });
+
+        return userPermission;
+    }
+
     public List<PermissionEntity> findUserPermissions(String userId) {
 
         String sql = "SELECT group_id, permission, group_path FROM gms_permission WHERE user_id = ?";
diff --git a/gms/src/main/java/it/inaf/ia2/gms/service/PermissionsService.java b/gms/src/main/java/it/inaf/ia2/gms/service/PermissionsService.java
index af9a942..b0525e4 100644
--- a/gms/src/main/java/it/inaf/ia2/gms/service/PermissionsService.java
+++ b/gms/src/main/java/it/inaf/ia2/gms/service/PermissionsService.java
@@ -61,4 +61,12 @@ public class PermissionsService {
 
         return permissionEntity;
     }
+
+    public PermissionEntity updatePermission(GroupEntity group, String userId, Permission permission) {
+
+        PermissionEntity permissionEntity = permissionsDAO.findPermissionEntity(group.getId(), userId)
+                .orElseThrow(() -> new IllegalArgumentException("Specified permission not found"));
+
+        return permissionsDAO.updatePermission(permissionEntity, permission);
+    }
 }
-- 
GitLab