diff --git a/gms-ui/src/App.vue b/gms-ui/src/App.vue
index cd5b2f3ddde667fb88a475edef23a9b89e251294..aa5de323797d278443026570d34aaa8f8cce679f 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 33bd82d1c17582828e1b4aa1cf06973d8969462e..c1c8e00bdaba2897a574ca703169a57b00ec36b1 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 4006f18ad703965b14e04de30cb28ff2bc36b881..f7079c8850ba39cd5adf841fddf58ca281cda1a6 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
Binary files a/gms-ui/src/assets/logo.png and /dev/null differ
diff --git a/gms-ui/src/components/GenericSearchResults.vue b/gms-ui/src/components/GenericSearchResults.vue
index ea3ae44138f97111ef163e58ed40e8ef705518a8..e0277a7a1abe30798c996c043255a57bec14b4d4 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 629059b322ea306a8eedae93db2860e46332a460..283c847fe6341801e36227c398ae0808d9a32d47 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 dfdb7d72b5231478a89d01ead8316a8cdafe2c41..d7dac8124690720eeafac5f4b80de93bfe9270a4 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 055309c5adfa9708c4bef8c90a0c85b75fd01009..7c7829f99b855ba3660f150ce8a8ab3b9d7b02bf 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 30ea021ede92c526fde3b37728cacbd9a6a1ce3f..34af9ce145a42062404fd6dfcea7f10aeb933fce 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 b6ee2a0688f4cbc1bfe3436a2b9ad9525c6d131b..32c89da70eea1f298136b6dfa4aad2961432ac47 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 a8537deb8cc620069042a85c846d0a55483a307f..8a30a8c42b294b8032896872e8bffdc28e2be30a 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 fc6e5bce2cc68c906952d5ee17245fecf5867987..b71a95cf269bb8ac72c8ff807a700156331802ad 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 2c6c16e33099570e38ebe6e5c49533fa8b2162af..7c2af04e218a81255ff4889df5a7ed562797477b 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 c648affff26627b0c80ac7e54b244f7a0e1fff31..d5ccf14e50fb4b99fd6fc39b22bb18ead0bb696a 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 7f3bcd161723b55c940db2fd493a3b7d57a946b2..8850297263fec4cbd4b77ea1f60a8c70153a7418 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 b021b10428957d880ccd08aa521bdd7a10f25c16..13e91d97725be0b646fdf00ae611a10d357fc38f 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 9fdb437ba33aecd441e507cd73be8deb0d57a56c..b210f476598bebbcddf8e404659fbdabe8c48526 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 0d85f65a6a5003fc611e1ea53c1b03d4323becd5..5227c1edcf8c4d8c678206b2d68e5da3d44d0d73 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 978a5903f4a9b5c3e0e529fe663e677014b6291d..e3d8b9874b7b8caad209ab925f3fb982ff625331 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 d699c641a088f041d9b6bf66608395cae5c4ca0a..bfd117c527284cbdfa2f1ec3b6f09b2e1d1f9c70 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 bb55488d77a4e53330ac8f1a9f07d5059ef7e5b8..ebb028719683e09edc212759b3ee3e14e2bd36c9 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 58c7221a5668405dfddb8ac485651b58915e1cba..255ab4300b156893096e7a9e038adaa2839d3361 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 0000000000000000000000000000000000000000..a250a1f13d6090edf5a3f60bd2b1497f0a4b6c23
--- /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 f175ec5b8f55d8b9deb22e316ff920b9d3e64486..fe02ab560b3b5fc89ff032f2f038595e6f154de7 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 5840978ae51c9bba38628e74e546dd944d46621e..9742396d930bb9bc279952e7c10f2661a78edbc0 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 595b1e48d7181d184bfb5826ed7b3f06ea244d31..e5d908c265228008b552db7aa3efa6cfd0ee826e 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 a1503f09b8d467433ef88e219ff8bd6cb07bb3f0..238797c65ab6282fa6d11917dec90a31b298a50b 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 c10ae5f01a90bb2482a35d596047e5265a2f4ba8..c1d4fd0f3a0adebe285b6ddb5d8dbba84c017e49 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 0000000000000000000000000000000000000000..2125f0d8c70ba003140fcc5d5503112e156e8b47
--- /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 d8d417ee24f340f86bfe14111e3f7fe8f4a03a13..4ffd9b97b6aeaa7a3670a81d449690bd701e322c 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 af9a942c8e0b9f715a6b78ed0170c4a95766f52e..b0525e457852a1bc9f5e8cca8efeaa32726838ac 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);
+    }
 }