From 2e517208bd07b85c0fd9fc445c4920f2bda44407 Mon Sep 17 00:00:00 2001
From: Sonia Zorba <sonia.zorba@inaf.it>
Date: Fri, 12 Mar 2021 18:48:42 +0100
Subject: [PATCH] Added creation time and creator user to groups, memberships
 and permissions entities

---
 .../inaf/ia2/gms/manager/GroupsManager.java   |   4 +-
 .../manager/InvitedRegistrationManager.java   |   3 +-
 .../ia2/gms/manager/MembershipManager.java    |   1 +
 .../ia2/gms/manager/PermissionsManager.java   |   8 +-
 .../java/it/inaf/ia2/gms/model/GroupNode.java |  20 ++++
 .../inaf/ia2/gms/persistence/GroupsDAO.java   |  70 +++++-------
 .../it/inaf/ia2/gms/persistence/JoinDAO.java  |   7 +-
 .../ia2/gms/persistence/MembershipsDAO.java   |  56 ++++++----
 .../ia2/gms/persistence/PermissionsDAO.java   | 103 +++++++++---------
 .../gms/persistence/model/GroupEntity.java    |  19 ++++
 .../persistence/model/MembershipEntity.java   |  19 ++++
 .../persistence/model/PermissionEntity.java   |  19 ++++
 .../inaf/ia2/gms/service/GroupsService.java   |   3 +-
 .../ia2/gms/service/GroupsTreeBuilder.java    |   2 +
 .../ia2/gms/service/PermissionsService.java   |  13 ++-
 gms/src/main/resources/sql/init.sql           |   6 +
 gms/src/main/resources/sql/upgrade.sql        |   7 ++
 .../InvitedRegistrationManagerTest.java       |   4 +-
 .../gms/manager/PermissionsManagerTest.java   |   4 +-
 .../ia2/gms/persistence/GroupsDAOTest.java    |  65 ++++++++++-
 .../InvitedRegistrationDAOTest.java           |  28 +++++
 .../gms/persistence/MembershipsDAOTest.java   |  90 +++++++++++++++
 .../NestedGroupsIntegrationTest.java          |  12 +-
 .../gms/persistence/PermissionsDAOTest.java   |  98 ++++++++++++++---
 .../gms/service/PermissionsServiceTest.java   |  15 ++-
 25 files changed, 507 insertions(+), 169 deletions(-)
 create mode 100644 gms/src/main/resources/sql/upgrade.sql
 create mode 100644 gms/src/test/java/it/inaf/ia2/gms/persistence/MembershipsDAOTest.java

diff --git a/gms/src/main/java/it/inaf/ia2/gms/manager/GroupsManager.java b/gms/src/main/java/it/inaf/ia2/gms/manager/GroupsManager.java
index 5b36337..f2a2d8d 100644
--- a/gms/src/main/java/it/inaf/ia2/gms/manager/GroupsManager.java
+++ b/gms/src/main/java/it/inaf/ia2/gms/manager/GroupsManager.java
@@ -9,7 +9,7 @@ import it.inaf.ia2.gms.service.GroupsService;
 import org.springframework.stereotype.Service;
 
 @Service
-public class GroupsManager {
+public class GroupsManager extends UserAwareComponent {
 
     private final GroupsService groupsService;
     private final PermissionsManager permissionsManager;
@@ -45,7 +45,7 @@ public class GroupsManager {
 
         verifyUserCanManageGroup(parent);
 
-        return groupsService.addGroup(parent, childGroupName, leaf);
+        return groupsService.addGroup(parent, childGroupName, leaf, getCurrentUserId());
     }
 
     public GroupEntity updateGroup(String groupId, String newGroupName, boolean leaf) {
diff --git a/gms/src/main/java/it/inaf/ia2/gms/manager/InvitedRegistrationManager.java b/gms/src/main/java/it/inaf/ia2/gms/manager/InvitedRegistrationManager.java
index db85c42..cdbc769 100644
--- a/gms/src/main/java/it/inaf/ia2/gms/manager/InvitedRegistrationManager.java
+++ b/gms/src/main/java/it/inaf/ia2/gms/manager/InvitedRegistrationManager.java
@@ -153,9 +153,10 @@ public class InvitedRegistrationManager extends UserAwareComponent {
             MembershipEntity membershipEntity = new MembershipEntity();
             membershipEntity.setUserId(userId);
             membershipEntity.setGroupId(groupId);
+            membershipEntity.setCreatedBy(getCurrentUserId());
             membershipsDAO.addMember(membershipEntity);
 
-            permissionsService.addPermission(groupEntity, userId, entry.getValue());
+            permissionsService.addPermission(groupEntity, userId, entry.getValue(), getCurrentUserId());
         }
 
         invitedRegistration.setUserId(getCurrentUserId());
diff --git a/gms/src/main/java/it/inaf/ia2/gms/manager/MembershipManager.java b/gms/src/main/java/it/inaf/ia2/gms/manager/MembershipManager.java
index 9e27198..ec522fc 100644
--- a/gms/src/main/java/it/inaf/ia2/gms/manager/MembershipManager.java
+++ b/gms/src/main/java/it/inaf/ia2/gms/manager/MembershipManager.java
@@ -83,6 +83,7 @@ public class MembershipManager extends UserAwareComponent {
         MembershipEntity membership = new MembershipEntity();
         membership.setGroupId(group.getId());
         membership.setUserId(userId);
+        membership.setCreatedBy(getCurrentUserId());
 
         membership = membershipsDAO.addMember(membership);
         loggingDAO.logAction("Added member, group_id=" + group.getId() + ", user_id=" + userId);
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 0bdbbb7..eab8686 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
@@ -81,22 +81,22 @@ public class PermissionsManager extends UserAwareComponent {
 
         if (currentUserPermission == Permission.MANAGE_MEMBERS && permission == Permission.VIEW_MEMBERS) {
             // Automatically assign the VIEW_MEMBERS permission ("Add collaborator" feature)
-            return permissionsService.addPermission(group, userId, Permission.VIEW_MEMBERS);
+            return permissionsService.addPermission(group, userId, Permission.VIEW_MEMBERS, getCurrentUserId());
         } else if (currentUserPermission == Permission.ADMIN) {
             // Admin users can specify a permission
-            return permissionsService.addPermission(group, userId, permission);
+            return permissionsService.addPermission(group, userId, permission, getCurrentUserId());
         }
         throw unauthorizedExceptionSupplier(group).get();
     }
 
     public PermissionEntity createOrUpdatePermission(GroupEntity group, String userId, Permission permission) {
         verifyUserCanManagePermissions(group);
-        return permissionsService.createOrUpdatePermission(group, userId, permission);
+        return permissionsService.createOrUpdatePermission(group, userId, permission, getCurrentUserId());
     }
 
     public PermissionEntity updatePermission(GroupEntity group, String userId, Permission permission) {
         verifyUserCanManagePermissions(group);
-        return permissionsService.updatePermission(group, userId, permission);
+        return permissionsService.updatePermission(group, userId, permission, getCurrentUserId());
     }
 
     public void removePermission(GroupEntity group, String userId) {
diff --git a/gms/src/main/java/it/inaf/ia2/gms/model/GroupNode.java b/gms/src/main/java/it/inaf/ia2/gms/model/GroupNode.java
index b7237fc..82f0015 100644
--- a/gms/src/main/java/it/inaf/ia2/gms/model/GroupNode.java
+++ b/gms/src/main/java/it/inaf/ia2/gms/model/GroupNode.java
@@ -1,5 +1,7 @@
 package it.inaf.ia2.gms.model;
 
+import java.util.Date;
+
 public class GroupNode {
 
     private String groupId;
@@ -8,6 +10,8 @@ public class GroupNode {
     private boolean hasChildren;
     private boolean leaf;
     private boolean locked;
+    private Date creationTime;
+    private String createdBy;
 
     public String getGroupId() {
         return groupId;
@@ -56,4 +60,20 @@ public class GroupNode {
     public void setLocked(boolean locked) {
         this.locked = locked;
     }
+
+    public String getCreatedBy() {
+        return createdBy;
+    }
+
+    public void setCreatedBy(String createdBy) {
+        this.createdBy = createdBy;
+    }
+
+    public Date getCreationTime() {
+        return creationTime;
+    }
+
+    public void setCreationTime(Date creationTime) {
+        this.creationTime = creationTime;
+    }
 }
diff --git a/gms/src/main/java/it/inaf/ia2/gms/persistence/GroupsDAO.java b/gms/src/main/java/it/inaf/ia2/gms/persistence/GroupsDAO.java
index abe3041..d188d1c 100644
--- a/gms/src/main/java/it/inaf/ia2/gms/persistence/GroupsDAO.java
+++ b/gms/src/main/java/it/inaf/ia2/gms/persistence/GroupsDAO.java
@@ -9,6 +9,7 @@ import java.sql.SQLException;
 import java.sql.Types;
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.Date;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -23,6 +24,8 @@ import org.springframework.stereotype.Component;
 @Component
 public class GroupsDAO {
 
+    private static final String GROUP_COLUMNS = "id, name, path, is_leaf, locked, creation_time, created_by";
+
     @Autowired(required = false)
     protected GroupsHook groupsHook;
 
@@ -39,7 +42,7 @@ public class GroupsDAO {
             groupsHook.beforeCreate(group);
         }
 
-        String sql = "INSERT INTO gms_group (id, name, path, is_leaf) VALUES (?, ?, ?, ?)";
+        String sql = "INSERT INTO gms_group (id, name, path, is_leaf, created_by) VALUES (?, ?, ?, ?, ?)";
 
         jdbcTemplate.update(conn -> {
             PreparedStatement ps = conn.prepareStatement(sql);
@@ -47,6 +50,7 @@ public class GroupsDAO {
             ps.setString(2, group.getName());
             ps.setObject(3, group.getPath(), Types.OTHER);
             ps.setBoolean(4, group.isLeaf());
+            ps.setString(5, group.getCreatedBy());
             return ps;
         });
 
@@ -85,7 +89,7 @@ public class GroupsDAO {
 
     public Optional<GroupEntity> findGroupById(String groupId) {
 
-        String sql = "SELECT id, name, path, is_leaf, locked from gms_group WHERE id = ?";
+        String sql = "SELECT " + GROUP_COLUMNS + " from gms_group WHERE id = ?";
 
         return jdbcTemplate.query(conn -> {
             PreparedStatement ps = conn.prepareStatement(sql);
@@ -107,7 +111,7 @@ public class GroupsDAO {
 
     public Optional<GroupEntity> findGroupByPath(String path) {
 
-        String sql = "SELECT id, name, is_leaf, locked from gms_group WHERE path = ?";
+        String sql = "SELECT " + GROUP_COLUMNS + " from gms_group WHERE path = ?";
 
         return jdbcTemplate.query(conn -> {
             PreparedStatement ps = conn.prepareStatement(sql);
@@ -115,12 +119,7 @@ public class GroupsDAO {
             return ps;
         }, resultSet -> {
             if (resultSet.next()) {
-                GroupEntity group = new GroupEntity();
-                group.setId(resultSet.getString("id"));
-                group.setName(resultSet.getString("name"));
-                group.setLeaf(resultSet.getBoolean("is_leaf"));
-                group.setLocked(resultSet.getBoolean("locked"));
-                group.setPath(path);
+                GroupEntity group = getGroupFromResultSet(resultSet);
                 return Optional.of(group);
             }
             return Optional.empty();
@@ -135,7 +134,7 @@ public class GroupsDAO {
 
         return jdbcTemplate.query(conn -> {
 
-            String sql = "SELECT id, name, path, is_leaf, locked from gms_group WHERE id IN (";
+            String sql = "SELECT " + GROUP_COLUMNS + " from gms_group WHERE id IN (";
             sql += String.join(",", identifiers.stream().map(p -> "?").collect(Collectors.toList()));
             sql += ")";
 
@@ -146,17 +145,7 @@ public class GroupsDAO {
             }
             return ps;
         }, resultSet -> {
-            List<GroupEntity> groups = new ArrayList<>();
-            while (resultSet.next()) {
-                GroupEntity group = new GroupEntity();
-                group.setId(resultSet.getString("id"));
-                group.setName(resultSet.getString("name"));
-                group.setPath(resultSet.getString("path"));
-                group.setLeaf(resultSet.getBoolean("is_leaf"));
-                group.setLocked(resultSet.getBoolean("locked"));
-                groups.add(group);
-            }
-            return groups;
+            return getGroupsFromResultSet(resultSet);
         });
     }
 
@@ -192,7 +181,7 @@ public class GroupsDAO {
 
     public Optional<GroupEntity> findGroupByParentAndName(String parentPath, String childName) {
 
-        String sql = "SELECT id, path, is_leaf, locked from gms_group WHERE name = ? AND path ~ ?";
+        String sql = "SELECT " + GROUP_COLUMNS + " from gms_group WHERE name = ? AND path ~ ?";
 
         return jdbcTemplate.query(conn -> {
             PreparedStatement ps = conn.prepareStatement(sql);
@@ -201,12 +190,7 @@ public class GroupsDAO {
             return ps;
         }, resultSet -> {
             if (resultSet.next()) {
-                GroupEntity group = new GroupEntity();
-                group.setId(resultSet.getString("id"));
-                group.setName(childName);
-                group.setPath(resultSet.getString("path"));
-                group.setLeaf(resultSet.getBoolean("is_leaf"));
-                group.setLocked(resultSet.getBoolean("locked"));
+                GroupEntity group = getGroupFromResultSet(resultSet);
                 return Optional.of(group);
             }
             return Optional.empty();
@@ -226,9 +210,9 @@ public class GroupsDAO {
 
         String sql;
         if (hasSearchFilter) {
-            sql = "SELECT id, name, path, is_leaf, locked FROM gms_group WHERE path ~ ? AND name ILIKE ? ORDER BY name";
+            sql = "SELECT " + GROUP_COLUMNS + " FROM gms_group WHERE path ~ ? AND name ILIKE ? ORDER BY name";
         } else {
-            sql = "SELECT id, name, path, is_leaf, locked FROM gms_group WHERE path ~ ? ORDER BY name";
+            sql = "SELECT " + GROUP_COLUMNS + " FROM gms_group WHERE path ~ ? ORDER BY name";
         }
 
         return jdbcTemplate.query(conn -> {
@@ -253,7 +237,7 @@ public class GroupsDAO {
 
     public List<GroupEntity> getAllChildren(String path) {
 
-        String sql = "SELECT id, name, path, is_leaf, locked FROM gms_group WHERE path <@ ? AND path <> ? ORDER BY nlevel(path) DESC";
+        String sql = "SELECT " + GROUP_COLUMNS + " FROM gms_group WHERE path <@ ? AND path <> ? ORDER BY nlevel(path) DESC";
 
         return jdbcTemplate.query(conn -> {
             PreparedStatement ps = conn.prepareStatement(sql);
@@ -267,7 +251,7 @@ public class GroupsDAO {
 
     public List<GroupEntity> findGroupsByNames(List<String> names) {
 
-        String sql = "SELECT id, name, path, is_leaf, locked from gms_group WHERE name IN ("
+        String sql = "SELECT " + GROUP_COLUMNS + " from gms_group WHERE name IN ("
                 + String.join(",", names.stream().map(g -> "?").collect(Collectors.toList()))
                 + ")";
 
@@ -286,17 +270,23 @@ public class GroupsDAO {
     private List<GroupEntity> getGroupsFromResultSet(ResultSet resultSet) throws SQLException {
         List<GroupEntity> groups = new ArrayList<>();
         while (resultSet.next()) {
-            GroupEntity group = new GroupEntity();
-            group.setId(resultSet.getString("id"));
-            group.setName(resultSet.getString("name"));
-            group.setPath(resultSet.getString("path"));
-            group.setLeaf(resultSet.getBoolean("is_leaf"));
-            group.setLocked(resultSet.getBoolean("locked"));
-            groups.add(group);
+            groups.add(getGroupFromResultSet(resultSet));
         }
         return groups;
     }
 
+    private GroupEntity getGroupFromResultSet(ResultSet resultSet) throws SQLException {
+        GroupEntity group = new GroupEntity();
+        group.setId(resultSet.getString("id"));
+        group.setName(resultSet.getString("name"));
+        group.setPath(resultSet.getString("path"));
+        group.setLeaf(resultSet.getBoolean("is_leaf"));
+        group.setLocked(resultSet.getBoolean("locked"));
+        group.setCreationTime(new Date(resultSet.getDate("creation_time").getTime()));
+        group.setCreatedBy(resultSet.getString("created_by"));
+        return group;
+    }
+
     public Map<String, Boolean> getHasChildrenMap(Set<String> groupIds) {
 
         if (groupIds.isEmpty()) {
@@ -362,7 +352,7 @@ public class GroupsDAO {
 
     public List<GroupEntity> searchGroups(String searchText) {
 
-        String sql = "SELECT id, name, path, is_leaf, locked from gms_group WHERE name ILIKE ?";
+        String sql = "SELECT " + GROUP_COLUMNS + " from gms_group WHERE name ILIKE ?";
 
         return jdbcTemplate.query(conn -> {
             PreparedStatement ps = conn.prepareStatement(sql);
diff --git a/gms/src/main/java/it/inaf/ia2/gms/persistence/JoinDAO.java b/gms/src/main/java/it/inaf/ia2/gms/persistence/JoinDAO.java
index 6a7b062..ffb46ed 100644
--- a/gms/src/main/java/it/inaf/ia2/gms/persistence/JoinDAO.java
+++ b/gms/src/main/java/it/inaf/ia2/gms/persistence/JoinDAO.java
@@ -59,10 +59,10 @@ public class JoinDAO {
 
     private void addPermissions(Set<PermissionEntity> permissionsToAdd) {
 
-        String sql = "INSERT INTO gms_permission (group_id, user_id, permission, group_path) VALUES "
-                + String.join(", ", Collections.nCopies(permissionsToAdd.size(), "(?, ?, ?, ?)")) + "\n"
+        String sql = "INSERT INTO gms_permission (group_id, user_id, permission, group_path, set_by) VALUES "
+                + String.join(", ", Collections.nCopies(permissionsToAdd.size(), "(?, ?, ?, ?, ?)")) + "\n"
                 + "ON CONFLICT (group_id, user_id) DO UPDATE\n"
-                + "SET permission = EXCLUDED.permission";
+                + "SET permission = EXCLUDED.permission, set_by = EXCLUDED.set_by, update_time = NOW()";
 
         LOG.trace("Executing {}", sql);
 
@@ -74,6 +74,7 @@ public class JoinDAO {
                 ps.setString(++i, permission.getUserId());
                 ps.setObject(++i, permission.getPermission().toString(), Types.OTHER);
                 ps.setObject(++i, permission.getGroupPath(), Types.OTHER);
+                ps.setString(++i, permission.getSetBy());
             }
             return ps;
         });
diff --git a/gms/src/main/java/it/inaf/ia2/gms/persistence/MembershipsDAO.java b/gms/src/main/java/it/inaf/ia2/gms/persistence/MembershipsDAO.java
index d08faf4..2a4309d 100644
--- a/gms/src/main/java/it/inaf/ia2/gms/persistence/MembershipsDAO.java
+++ b/gms/src/main/java/it/inaf/ia2/gms/persistence/MembershipsDAO.java
@@ -3,9 +3,12 @@ package it.inaf.ia2.gms.persistence;
 import it.inaf.ia2.gms.persistence.model.GroupEntity;
 import it.inaf.ia2.gms.persistence.model.MembershipEntity;
 import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
 import java.sql.Types;
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.Date;
 import java.util.List;
 import java.util.stream.Collectors;
 import javax.sql.DataSource;
@@ -25,27 +28,24 @@ public class MembershipsDAO {
 
     public List<MembershipEntity> findByGroup(String groupId) {
 
-        String sql = "SELECT user_id FROM gms_membership WHERE group_id = ?";
+        String sql = "SELECT user_id, group_id, created_by, creation_time FROM gms_membership WHERE group_id = ?";
 
         return jdbcTemplate.query(conn -> {
             PreparedStatement ps = conn.prepareStatement(sql);
             ps.setString(1, groupId);
             return ps;
         }, resultSet -> {
-            List<MembershipEntity> members = new ArrayList<>();
-            while (resultSet.next()) {
-                MembershipEntity membership = new MembershipEntity();
-                membership.setGroupId(groupId);
-                membership.setUserId(resultSet.getString("user_id"));
-                members.add(membership);
-            }
-            return members;
+            return getMembershipsFromResultSet(resultSet);
         });
     }
 
     public List<MembershipEntity> findByGroups(List<String> groupIds) {
 
-        String sql = "SELECT user_id, group_id FROM gms_membership WHERE group_id IN ("
+        if (groupIds.isEmpty()) {
+            return new ArrayList<>();
+        }
+
+        String sql = "SELECT user_id, group_id, created_by, creation_time FROM gms_membership WHERE group_id IN ("
                 + String.join(",", Collections.nCopies(groupIds.size(), "?")) + ")";
 
         return jdbcTemplate.query(conn -> {
@@ -56,25 +56,36 @@ public class MembershipsDAO {
             }
             return ps;
         }, resultSet -> {
-            List<MembershipEntity> members = new ArrayList<>();
-            while (resultSet.next()) {
-                MembershipEntity membership = new MembershipEntity();
-                membership.setGroupId(resultSet.getString("group_id"));
-                membership.setUserId(resultSet.getString("user_id"));
-                members.add(membership);
-            }
-            return members;
+            return getMembershipsFromResultSet(resultSet);
         });
     }
 
+    private List<MembershipEntity> getMembershipsFromResultSet(ResultSet resultSet) throws SQLException {
+        List<MembershipEntity> members = new ArrayList<>();
+        while (resultSet.next()) {
+            members.add(getMembershipEntityFromResultSet(resultSet));
+        }
+        return members;
+    }
+
+    private MembershipEntity getMembershipEntityFromResultSet(ResultSet resultSet) throws SQLException {
+        MembershipEntity membership = new MembershipEntity();
+        membership.setGroupId(resultSet.getString("group_id"));
+        membership.setUserId(resultSet.getString("user_id"));
+        membership.setUserId(resultSet.getString("user_id"));
+        membership.setCreatedBy(resultSet.getString("created_by"));
+        membership.setCreationTime(new Date(resultSet.getDate("creation_time").getTime()));
+        return membership;
+    }
+
     public List<GroupEntity> getUserMemberships(String userId) {
         return getUserMemberships(userId, null);
     }
 
     public List<GroupEntity> getUserMemberships(String userId, String parentPath) {
 
-        String sql = "SELECT g.id, g.name, g.path, g.is_leaf FROM "
-                + " gms_membership m "
+        String sql = "SELECT g.id, g.name, g.path, g.is_leaf, g.creation_time, g.created_by "
+                + " FROM gms_membership m "
                 + " JOIN gms_group g ON g.id = m.group_id"
                 + " WHERE m.user_id = ?";
         if (parentPath != null) {
@@ -99,6 +110,8 @@ public class MembershipsDAO {
                 group.setName(resultSet.getString("name"));
                 group.setPath(resultSet.getString("path"));
                 group.setLeaf(resultSet.getBoolean("is_leaf"));
+                group.setCreationTime(new Date(resultSet.getDate("creation_time").getTime()));
+                group.setCreatedBy(resultSet.getString("created_by"));
                 memberships.add(group);
             }
             return memberships;
@@ -124,13 +137,14 @@ public class MembershipsDAO {
 
     public MembershipEntity addMember(MembershipEntity membership) {
 
-        String sql = "INSERT INTO gms_membership (group_id, user_id) VALUES (?, ?)\n"
+        String sql = "INSERT INTO gms_membership (group_id, user_id, created_by) VALUES (?, ?, ?)\n"
                 + "ON CONFLICT (group_id, user_id) DO NOTHING";
 
         jdbcTemplate.update(conn -> {
             PreparedStatement ps = conn.prepareStatement(sql);
             ps.setString(1, membership.getGroupId());
             ps.setString(2, membership.getUserId());
+            ps.setString(3, membership.getCreatedBy());
             return ps;
         });
 
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 7191496..c37f0dd 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
@@ -3,8 +3,11 @@ package it.inaf.ia2.gms.persistence;
 import it.inaf.ia2.gms.model.Permission;
 import it.inaf.ia2.gms.persistence.model.PermissionEntity;
 import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
 import java.sql.Types;
 import java.util.ArrayList;
+import java.util.Date;
 import java.util.List;
 import java.util.Optional;
 import java.util.stream.Collectors;
@@ -16,6 +19,8 @@ import org.springframework.stereotype.Component;
 @Component
 public class PermissionsDAO {
 
+    private static final String PERMISSION_COLUMNS = "group_id, user_id, permission, group_path, update_time, set_by";
+
     private final JdbcTemplate jdbcTemplate;
 
     @Autowired
@@ -25,34 +30,37 @@ public class PermissionsDAO {
 
     public PermissionEntity createOrUpdatePermission(PermissionEntity userPermission) {
 
-        String sql = "INSERT INTO gms_permission(group_id, user_id, permission, group_path) VALUES(?, ?, ?, ?)\n"
+        String sql = "INSERT INTO gms_permission(group_id, user_id, permission, group_path, set_by) VALUES(?, ?, ?, ?, ?)\n"
                 + "ON CONFLICT (group_id, user_id) DO UPDATE\n"
-                + "SET permission = EXCLUDED.permission";
+                + "SET permission = EXCLUDED.permission, set_by = EXCLUDED.set_by, update_time = NOW()";
 
         jdbcTemplate.update(conn -> {
+            int i = 0;
             PreparedStatement ps = conn.prepareStatement(sql);
-            ps.setString(1, userPermission.getGroupId());
-            ps.setString(2, userPermission.getUserId());
-            ps.setObject(3, userPermission.getPermission().toString(), Types.OTHER);
-            ps.setObject(4, userPermission.getGroupPath(), Types.OTHER);
+            ps.setString(++i, userPermission.getGroupId());
+            ps.setString(++i, userPermission.getUserId());
+            ps.setObject(++i, userPermission.getPermission().toString(), Types.OTHER);
+            ps.setObject(++i, userPermission.getGroupPath(), Types.OTHER);
+            ps.setString(++i, userPermission.getSetBy());
             return ps;
         });
 
         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 = ?";
+    public PermissionEntity updatePermission(PermissionEntity userPermission) {
 
-        userPermission.setPermission(newPermission);
+        String sql = "UPDATE gms_permission SET permission = ?, set_by = ?, update_time = NOW() "
+                + " WHERE group_id = ? AND user_id = ? AND group_path = ?";
 
         jdbcTemplate.update(conn -> {
+            int i = 0;
             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);
+            ps.setObject(++i, userPermission.getPermission().toString(), Types.OTHER);
+            ps.setString(++i, userPermission.getSetBy());
+            ps.setString(++i, userPermission.getGroupId());
+            ps.setString(++i, userPermission.getUserId());
+            ps.setObject(++i, userPermission.getGroupPath(), Types.OTHER);
             return ps;
         });
 
@@ -61,23 +69,14 @@ public class PermissionsDAO {
 
     public List<PermissionEntity> findUserPermissions(String userId) {
 
-        String sql = "SELECT group_id, permission, group_path FROM gms_permission WHERE user_id = ?";
+        String sql = "SELECT " + PERMISSION_COLUMNS + " FROM gms_permission WHERE user_id = ?";
 
         return jdbcTemplate.query(conn -> {
             PreparedStatement ps = conn.prepareStatement(sql);
             ps.setString(1, userId);
             return ps;
         }, resultSet -> {
-            List<PermissionEntity> permissions = new ArrayList<>();
-            while (resultSet.next()) {
-                PermissionEntity permission = new PermissionEntity();
-                permission.setGroupId(resultSet.getString("group_id"));
-                permission.setUserId(userId);
-                permission.setPermission(Permission.valueOf(resultSet.getString("permission")));
-                permission.setGroupPath(resultSet.getString("group_path"));
-                permissions.add(permission);
-            }
-            return permissions;
+            return getPermissionsFromResultSet(resultSet);
         });
     }
 
@@ -87,7 +86,7 @@ public class PermissionsDAO {
      */
     public List<PermissionEntity> findUserPermissions(String userId, String path) {
 
-        String sql = "SELECT group_id, permission, group_path FROM gms_permission WHERE user_id = ?\n"
+        String sql = "SELECT " + PERMISSION_COLUMNS + " FROM gms_permission WHERE user_id = ?\n"
                 + "AND (group_path <@ ? OR group_path @> ?)";
 
         return jdbcTemplate.query(conn -> {
@@ -97,22 +96,13 @@ public class PermissionsDAO {
             ps.setObject(3, path, Types.OTHER);
             return ps;
         }, resultSet -> {
-            List<PermissionEntity> permissions = new ArrayList<>();
-            while (resultSet.next()) {
-                PermissionEntity permission = new PermissionEntity();
-                permission.setGroupId(resultSet.getString("group_id"));
-                permission.setUserId(userId);
-                permission.setPermission(Permission.valueOf(resultSet.getString("permission")));
-                permission.setGroupPath(resultSet.getString("group_path"));
-                permissions.add(permission);
-            }
-            return permissions;
+            return getPermissionsFromResultSet(resultSet);
         });
     }
 
     public Optional<PermissionEntity> findPermissionEntity(String groupId, String userId) {
 
-        String sql = "SELECT group_path, permission FROM gms_permission WHERE group_id = ? AND user_id = ?";
+        String sql = "SELECT " + PERMISSION_COLUMNS + " FROM gms_permission WHERE group_id = ? AND user_id = ?";
 
         return jdbcTemplate.query(conn -> {
             PreparedStatement ps = conn.prepareStatement(sql);
@@ -121,12 +111,7 @@ public class PermissionsDAO {
             return ps;
         }, resultSet -> {
             if (resultSet.next()) {
-                PermissionEntity permissionEntity = new PermissionEntity();
-                permissionEntity.setGroupId(groupId);
-                permissionEntity.setUserId(userId);
-                permissionEntity.setPermission(Permission.valueOf(resultSet.getString("permission")));
-                permissionEntity.setGroupPath(resultSet.getString("group_path"));
-                return Optional.of(permissionEntity);
+                return Optional.of(getPermissionEntityFromResultSet(resultSet));
             }
             return Optional.empty();
         });
@@ -134,26 +119,36 @@ public class PermissionsDAO {
 
     public List<PermissionEntity> getGroupsPermissions(String groupId) {
 
-        String sql = "SELECT user_id, permission, group_path FROM gms_permission WHERE group_id = ?";
+        String sql = "SELECT " + PERMISSION_COLUMNS + " FROM gms_permission WHERE group_id = ?";
 
         return jdbcTemplate.query(conn -> {
             PreparedStatement ps = conn.prepareStatement(sql);
             ps.setString(1, groupId);
             return ps;
         }, resultSet -> {
-            List<PermissionEntity> permissions = new ArrayList<>();
-            while (resultSet.next()) {
-                PermissionEntity permission = new PermissionEntity();
-                permission.setGroupId(groupId);
-                permission.setUserId(resultSet.getString("user_id"));
-                permission.setPermission(Permission.valueOf(resultSet.getString("permission")));
-                permission.setGroupPath(resultSet.getString("group_path"));
-                permissions.add(permission);
-            }
-            return permissions;
+            return getPermissionsFromResultSet(resultSet);
         });
     }
 
+    private List<PermissionEntity> getPermissionsFromResultSet(ResultSet resultSet) throws SQLException {
+        List<PermissionEntity> permissions = new ArrayList<>();
+        while (resultSet.next()) {
+            permissions.add(getPermissionEntityFromResultSet(resultSet));
+        }
+        return permissions;
+    }
+
+    private PermissionEntity getPermissionEntityFromResultSet(ResultSet resultSet) throws SQLException {
+        PermissionEntity permission = new PermissionEntity();
+        permission.setGroupId(resultSet.getString("group_id"));
+        permission.setUserId(resultSet.getString("user_id"));
+        permission.setPermission(Permission.valueOf(resultSet.getString("permission")));
+        permission.setGroupPath(resultSet.getString("group_path"));
+        permission.setSetBy(resultSet.getString("set_by"));
+        permission.setUpdateTime(new Date(resultSet.getDate("update_time").getTime()));
+        return permission;
+    }
+
     public void deletePermission(String groupId, String userId) {
 
         String sql = "DELETE FROM gms_permission WHERE group_id = ? AND user_id = ?";
diff --git a/gms/src/main/java/it/inaf/ia2/gms/persistence/model/GroupEntity.java b/gms/src/main/java/it/inaf/ia2/gms/persistence/model/GroupEntity.java
index 20590cf..e431cda 100644
--- a/gms/src/main/java/it/inaf/ia2/gms/persistence/model/GroupEntity.java
+++ b/gms/src/main/java/it/inaf/ia2/gms/persistence/model/GroupEntity.java
@@ -1,5 +1,6 @@
 package it.inaf.ia2.gms.persistence.model;
 
+import java.util.Date;
 import java.util.Objects;
 
 public class GroupEntity {
@@ -9,6 +10,8 @@ public class GroupEntity {
     private String path;
     private boolean leaf;
     private boolean locked;
+    private Date creationTime;
+    private String createdBy;
 
     public String getId() {
         return id;
@@ -50,6 +53,22 @@ public class GroupEntity {
         this.locked = locked;
     }
 
+    public Date getCreationTime() {
+        return creationTime;
+    }
+
+    public void setCreationTime(Date creationTime) {
+        this.creationTime = creationTime;
+    }
+
+    public String getCreatedBy() {
+        return createdBy;
+    }
+
+    public void setCreatedBy(String createdBy) {
+        this.createdBy = createdBy;
+    }
+
     public String getParentPath() {
         if (path.isEmpty()) {
             return null;
diff --git a/gms/src/main/java/it/inaf/ia2/gms/persistence/model/MembershipEntity.java b/gms/src/main/java/it/inaf/ia2/gms/persistence/model/MembershipEntity.java
index c71bccb..1a3c9c6 100644
--- a/gms/src/main/java/it/inaf/ia2/gms/persistence/model/MembershipEntity.java
+++ b/gms/src/main/java/it/inaf/ia2/gms/persistence/model/MembershipEntity.java
@@ -1,5 +1,6 @@
 package it.inaf.ia2.gms.persistence.model;
 
+import java.util.Date;
 import java.util.Objects;
 import javax.validation.constraints.NotEmpty;
 
@@ -9,6 +10,8 @@ public class MembershipEntity {
     private String groupId;
     @NotEmpty
     private String userId;
+    private Date creationTime;
+    private String createdBy;
 
     public String getGroupId() {
         return groupId;
@@ -26,6 +29,22 @@ public class MembershipEntity {
         this.userId = userId;
     }
 
+    public Date getCreationTime() {
+        return creationTime;
+    }
+
+    public void setCreationTime(Date creationTime) {
+        this.creationTime = creationTime;
+    }
+
+    public String getCreatedBy() {
+        return createdBy;
+    }
+
+    public void setCreatedBy(String createdBy) {
+        this.createdBy = createdBy;
+    }
+
     @Override
     public int hashCode() {
         int hash = 5;
diff --git a/gms/src/main/java/it/inaf/ia2/gms/persistence/model/PermissionEntity.java b/gms/src/main/java/it/inaf/ia2/gms/persistence/model/PermissionEntity.java
index ca6c623..fb2b54c 100644
--- a/gms/src/main/java/it/inaf/ia2/gms/persistence/model/PermissionEntity.java
+++ b/gms/src/main/java/it/inaf/ia2/gms/persistence/model/PermissionEntity.java
@@ -1,6 +1,7 @@
 package it.inaf.ia2.gms.persistence.model;
 
 import it.inaf.ia2.gms.model.Permission;
+import java.util.Date;
 import java.util.Objects;
 import javax.validation.constraints.NotEmpty;
 import javax.validation.constraints.NotNull;
@@ -13,6 +14,8 @@ public class PermissionEntity {
     private String groupId;
     @NotNull
     private Permission permission;
+    private Date updateTime;
+    private String setBy;
 
     // The group path is copied here for performance reasons
     private String groupPath;
@@ -49,6 +52,22 @@ public class PermissionEntity {
         this.groupPath = groupPath;
     }
 
+    public Date getUpdateTime() {
+        return updateTime;
+    }
+
+    public void setUpdateTime(Date updateTime) {
+        this.updateTime = updateTime;
+    }
+
+    public String getSetBy() {
+        return setBy;
+    }
+
+    public void setSetBy(String setBy) {
+        this.setBy = setBy;
+    }
+
     @Override
     public int hashCode() {
         int hash = 5;
diff --git a/gms/src/main/java/it/inaf/ia2/gms/service/GroupsService.java b/gms/src/main/java/it/inaf/ia2/gms/service/GroupsService.java
index f38717f..b0a3fcf 100644
--- a/gms/src/main/java/it/inaf/ia2/gms/service/GroupsService.java
+++ b/gms/src/main/java/it/inaf/ia2/gms/service/GroupsService.java
@@ -49,7 +49,7 @@ public class GroupsService {
         }
     }
 
-    public GroupEntity addGroup(GroupEntity parent, String groupName, boolean leaf) {
+    public GroupEntity addGroup(GroupEntity parent, String groupName, boolean leaf, String createdBy) {
 
         if (groupsDAO.getDirectSubGroups(parent.getPath()).stream()
                 .anyMatch(g -> g.getName().equals(groupName))) {
@@ -69,6 +69,7 @@ public class GroupsService {
         group.setName(groupName);
         group.setPath(path);
         group.setLeaf(leaf);
+        group.setCreatedBy(createdBy);
 
         groupsDAO.createGroup(group);
         loggingDAO.logAction("Added group: parent_path=" + parent.getPath() + ", group_name=" + groupName);
diff --git a/gms/src/main/java/it/inaf/ia2/gms/service/GroupsTreeBuilder.java b/gms/src/main/java/it/inaf/ia2/gms/service/GroupsTreeBuilder.java
index e3c2145..ed3ffc7 100644
--- a/gms/src/main/java/it/inaf/ia2/gms/service/GroupsTreeBuilder.java
+++ b/gms/src/main/java/it/inaf/ia2/gms/service/GroupsTreeBuilder.java
@@ -62,6 +62,8 @@ public class GroupsTreeBuilder {
                 node.setLeaf(group.isLeaf());
                 node.setLocked(group.isLocked());
                 node.setPermission(permission);
+                node.setCreatedBy(group.getCreatedBy());
+                node.setCreationTime(group.getCreationTime());
                 nodes.add(node);
             });
         }
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 e00f47b..d953563 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
@@ -35,7 +35,7 @@ public class PermissionsService {
         loggingDAO.logAction("Removed permission for " + userId + "in group " + group.getId());
     }
 
-    public PermissionEntity addPermission(GroupEntity group, String userId, Permission permission) {
+    public PermissionEntity addPermission(GroupEntity group, String userId, Permission permission, String setBy) {
 
         Optional<PermissionEntity> existingPermissionEntity = permissionsDAO.findPermissionEntity(group.getId(), userId);
 
@@ -54,6 +54,7 @@ public class PermissionsService {
             permissionEntity.setUserId(userId);
             permissionEntity.setPermission(permission);
             permissionEntity.setGroupPath(group.getPath());
+            permissionEntity.setSetBy(setBy);
         }
 
         permissionEntity = permissionsDAO.createOrUpdatePermission(permissionEntity);
@@ -62,22 +63,26 @@ public class PermissionsService {
         return permissionEntity;
     }
 
-    public PermissionEntity createOrUpdatePermission(GroupEntity group, String userId, Permission permission) {
+    public PermissionEntity createOrUpdatePermission(GroupEntity group, String userId, Permission permission, String setBy) {
 
         PermissionEntity permissionEntity = new PermissionEntity();
         permissionEntity.setGroupId(group.getId());
         permissionEntity.setUserId(userId);
         permissionEntity.setPermission(permission);
         permissionEntity.setGroupPath(group.getPath());
+        permissionEntity.setSetBy(setBy);
 
         return permissionsDAO.createOrUpdatePermission(permissionEntity);
     }
 
-    public PermissionEntity updatePermission(GroupEntity group, String userId, Permission permission) {
+    public PermissionEntity updatePermission(GroupEntity group, String userId, Permission permission, String setBy) {
 
         PermissionEntity permissionEntity = permissionsDAO.findPermissionEntity(group.getId(), userId)
                 .orElseThrow(() -> new IllegalArgumentException("Specified permission not found"));
 
-        return permissionsDAO.updatePermission(permissionEntity, permission);
+        permissionEntity.setSetBy(setBy);
+        permissionEntity.setPermission(permission);
+
+        return permissionsDAO.updatePermission(permissionEntity);
     }
 }
diff --git a/gms/src/main/resources/sql/init.sql b/gms/src/main/resources/sql/init.sql
index 41360a8..f8fdad6 100644
--- a/gms/src/main/resources/sql/init.sql
+++ b/gms/src/main/resources/sql/init.sql
@@ -6,6 +6,8 @@ CREATE TABLE gms_group (
   path ltree NOT NULL,
   is_leaf boolean,
   locked boolean,
+  creation_time timestamp DEFAULT NOW(),
+  created_by varchar,
   primary key(id)
 );
 
@@ -16,6 +18,8 @@ CREATE INDEX group_name_idx ON gms_group USING btree(name);
 CREATE TABLE gms_membership (
   group_id varchar NOT NULL,
   user_id varchar NOT NULL,
+  creation_time timestamp DEFAULT NOW(),
+  created_by varchar,
   primary key (group_id, user_id),
   foreign key (group_id) references gms_group(id)
 );
@@ -27,6 +31,8 @@ CREATE TABLE gms_permission (
   user_id varchar NOT NULL,
   permission permission_type NOT NULL,
   group_path ltree NOT NULL, -- group_path is copied here for performance reasons
+  update_time timestamp DEFAULT NOW(),
+  set_by varchar,
   primary key (group_id, user_id), -- we can have 1 permission for each group
   foreign key (group_id) references gms_group(id),
   foreign key (group_path) references gms_group(path)
diff --git a/gms/src/main/resources/sql/upgrade.sql b/gms/src/main/resources/sql/upgrade.sql
new file mode 100644
index 0000000..2b41d53
--- /dev/null
+++ b/gms/src/main/resources/sql/upgrade.sql
@@ -0,0 +1,7 @@
+-- 2021-03-12
+ALTER TABLE gms_group ADD COLUMN creation_time timestamp DEFAULT NOW();
+ALTER TABLE gms_group ADD COLUMN created_by varchar;
+ALTER TABLE gms_membership ADD COLUMN creation_time timestamp DEFAULT NOW();
+ALTER TABLE gms_membership ADD COLUMN created_by varchar;
+ALTER TABLE gms_permission ADD COLUMN update_time timestamp DEFAULT NOW();
+ALTER TABLE gms_permission ADD COLUMN set_by varchar;
diff --git a/gms/src/test/java/it/inaf/ia2/gms/manager/InvitedRegistrationManagerTest.java b/gms/src/test/java/it/inaf/ia2/gms/manager/InvitedRegistrationManagerTest.java
index 7c6956e..962e74a 100644
--- a/gms/src/test/java/it/inaf/ia2/gms/manager/InvitedRegistrationManagerTest.java
+++ b/gms/src/test/java/it/inaf/ia2/gms/manager/InvitedRegistrationManagerTest.java
@@ -130,7 +130,7 @@ public class InvitedRegistrationManagerTest {
         invitedRegistrationManager.completeInvitedRegistrationIfNecessary();
 
         verify(membershipsDAO, times(2)).addMember(any());
-        verify(permissionsService, times(2)).addPermission(any(), any(), any());
+        verify(permissionsService, times(2)).addPermission(any(), any(), any(), any());
     }
 
     @Test
@@ -164,6 +164,6 @@ public class InvitedRegistrationManagerTest {
         invitedRegistrationManager.completeInvitedRegistrationIfNecessary();
 
         verify(membershipsDAO, times(1)).addMember(any());
-        verify(permissionsService, times(1)).addPermission(any(), any(), any());
+        verify(permissionsService, times(1)).addPermission(any(), any(), any(), any());
     }
 }
diff --git a/gms/src/test/java/it/inaf/ia2/gms/manager/PermissionsManagerTest.java b/gms/src/test/java/it/inaf/ia2/gms/manager/PermissionsManagerTest.java
index e1459f0..072a3ee 100644
--- a/gms/src/test/java/it/inaf/ia2/gms/manager/PermissionsManagerTest.java
+++ b/gms/src/test/java/it/inaf/ia2/gms/manager/PermissionsManagerTest.java
@@ -54,7 +54,7 @@ public class PermissionsManagerTest {
 
         permissionsManager.addPermission(group, TARGET_USER_ID, Permission.VIEW_MEMBERS);
 
-        verify(permissionsService, times(1)).addPermission(eq(group), eq(TARGET_USER_ID), eq(Permission.VIEW_MEMBERS));
+        verify(permissionsService, times(1)).addPermission(eq(group), eq(TARGET_USER_ID), eq(Permission.VIEW_MEMBERS), eq(USER_ID));
     }
 
     @Test
@@ -67,7 +67,7 @@ public class PermissionsManagerTest {
 
         permissionsManager.addPermission(group, TARGET_USER_ID, Permission.MANAGE_MEMBERS);
 
-        verify(permissionsService, times(1)).addPermission(eq(group), eq(TARGET_USER_ID), eq(Permission.MANAGE_MEMBERS));
+        verify(permissionsService, times(1)).addPermission(eq(group), eq(TARGET_USER_ID), eq(Permission.MANAGE_MEMBERS), eq(USER_ID));
     }
 
     @Test(expected = UnauthorizedException.class)
diff --git a/gms/src/test/java/it/inaf/ia2/gms/persistence/GroupsDAOTest.java b/gms/src/test/java/it/inaf/ia2/gms/persistence/GroupsDAOTest.java
index 17fb805..8a617b1 100644
--- a/gms/src/test/java/it/inaf/ia2/gms/persistence/GroupsDAOTest.java
+++ b/gms/src/test/java/it/inaf/ia2/gms/persistence/GroupsDAOTest.java
@@ -5,6 +5,7 @@ import it.inaf.ia2.gms.HooksConfig;
 import it.inaf.ia2.gms.model.GroupBreadcrumb;
 import it.inaf.ia2.gms.persistence.model.GroupEntity;
 import it.inaf.ia2.gms.service.hook.GroupsHook;
+import java.util.Arrays;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
@@ -43,7 +44,6 @@ public class GroupsDAOTest {
     @Before
     public void setUp() {
         dao = new GroupsDAO(dataSource);
-        dao.groupsHook = groupsHook;
     }
 
     @Test
@@ -56,7 +56,7 @@ public class GroupsDAOTest {
         root.setPath("");
         dao.createGroup(root);
 
-        verify(groupsHook, times(1)).beforeCreate(eq(root));
+        dao.groupsHook = groupsHook;
 
         GroupEntity lbt = new GroupEntity();
         lbt.setId(getNewGroupId());
@@ -64,20 +64,30 @@ public class GroupsDAOTest {
         lbt.setPath(lbt.getId());
         dao.createGroup(lbt);
 
+        verify(groupsHook, times(1)).beforeCreate(eq(lbt));
+
         GroupEntity tng = new GroupEntity();
         tng.setId(getNewGroupId());
         tng.setName("TNG");
         tng.setPath(tng.getId());
         dao.createGroup(tng);
 
+        verify(groupsHook, times(1)).beforeCreate(eq(tng));
+
         GroupEntity lbtInaf = new GroupEntity();
         lbtInaf.setId(getNewGroupId());
         lbtInaf.setName("INAF");
         lbtInaf.setPath(lbt.getId() + "." + lbtInaf.getId());
         dao.createGroup(lbtInaf);
 
+        GroupEntity lbtInafP1 = new GroupEntity();
+        lbtInafP1.setId(getNewGroupId());
+        lbtInafP1.setName("P1");
+        lbtInafP1.setPath(lbt.getId() + "." + lbtInaf.getId() + "." + lbtInafP1.getId());
+        dao.createGroup(lbtInafP1);
+
         // Check count
-        assertEquals(4, dao.count());
+        assertEquals(5, dao.count());
 
         // Find by id
         Optional<GroupEntity> group = dao.findGroupById(lbtInaf.getId());
@@ -104,8 +114,9 @@ public class GroupsDAOTest {
 
         // All children
         groups = dao.getAllChildren(root.getPath());
-        assertEquals(3, groups.size());
-        assertEquals("INAF", groups.get(0).getName()); // order by path DESC
+        assertEquals(4, groups.size());
+        assertEquals("P1", groups.get(0).getName()); // order by path DESC
+        assertEquals("INAF", groups.get(1).getName());
 
         // Group by parent and name
         Optional<GroupEntity> optGroup = dao.findGroupByParentAndName(root.getPath(), lbt.getName());
@@ -123,7 +134,7 @@ public class GroupsDAOTest {
         Map<String, String> completeGroupNames = dao.getGroupCompleteNamesFromId(groupIds);
         assertEquals(2, completeGroupNames.size());
         assertEquals("LBT", completeGroupNames.get(lbt.getId()));
-        assertEquals("LBT.INAF", completeGroupNames.get(groups.get(0).getId()));
+        assertEquals("LBT.INAF.P1", completeGroupNames.get(groups.get(0).getId()));
 
         // Children map
         Map<String, Boolean> childrenMap = dao.getHasChildrenMap(Sets.newSet(root.getId()));
@@ -151,6 +162,28 @@ public class GroupsDAOTest {
         assertEquals(lbt.getName(), breadcrumbs.get(1).getGroupName());
         assertEquals(lbtInaf.getName(), breadcrumbs.get(2).getGroupName());
 
+        // Search
+        assertEquals(1, dao.searchGroups("renam").size());
+        assertTrue(dao.searchGroups("invalid-name").isEmpty());
+
+        // Find by names
+        List<GroupEntity> groupsByName = dao.findGroupsByNames(Arrays.asList("LBT", "INAF"));
+        assertEquals(2, groupsByName.size());
+        assertEquals("LBT", groupsByName.get(0).getName());
+        assertEquals("INAF", groupsByName.get(1).getName());
+
+        // Find by ids
+        assertEquals(2, dao.findGroupsByIds(new HashSet<>(Arrays.asList(lbt.getId(), tng.getId()))).size());
+
+        // Get direct children
+        List<GroupEntity> lbtChildren = dao.getDirectSubGroups(lbt.getPath());
+        assertEquals(1, lbtChildren.size());
+        assertEquals("INAF", lbtChildren.get(0).getName());
+
+        // Get direct children with filter
+        assertEquals(1, dao.getDirectSubGroups(lbt.getPath(), "INA").size());
+        assertTrue(dao.getDirectSubGroups(lbt.getPath(), "invalid-name").isEmpty());
+
         // Delete
         dao.deleteGroup(lbtInaf);
         groups = dao.getDirectSubGroups(lbt.getId());
@@ -167,4 +200,24 @@ public class GroupsDAOTest {
     public void testGroupCompleteNamesEmptyInput() {
         assertTrue(dao.getGroupCompleteNamesFromId(new HashSet<>()).isEmpty());
     }
+
+    @Test
+    public void testGetHasChildrenMapEmptyInput() {
+        assertTrue(dao.getHasChildrenMap(new HashSet<>()).isEmpty());
+    }
+
+    @Test
+    public void testFindGroupsByIdsEmtpyInput() {
+        assertTrue(dao.findGroupsByIds(new HashSet<>()).isEmpty());
+    }
+
+    @Test
+    public void findInexistentGroupByParentAndName() {
+        assertTrue(dao.findGroupByParentAndName("", "not-exists").isEmpty());
+    }
+
+    @Test
+    public void findInexistentGroupByPath() {
+        assertTrue(dao.findGroupByPath("not.exists").isEmpty());
+    }
 }
diff --git a/gms/src/test/java/it/inaf/ia2/gms/persistence/InvitedRegistrationDAOTest.java b/gms/src/test/java/it/inaf/ia2/gms/persistence/InvitedRegistrationDAOTest.java
index f13c03d..7c5883e 100644
--- a/gms/src/test/java/it/inaf/ia2/gms/persistence/InvitedRegistrationDAOTest.java
+++ b/gms/src/test/java/it/inaf/ia2/gms/persistence/InvitedRegistrationDAOTest.java
@@ -94,7 +94,35 @@ public class InvitedRegistrationDAOTest {
         groupsIds.add(group2.getId());
         dao.deleteAllGroupsInvitedRegistrations(groupsIds);
 
+        assertTrue(dao.getInvitedRegistrationFromToken("token_hash").isEmpty());
+
+        dao.addInvitedRegistration(reg);
+        assertEquals(1, dao.getPendingInvitedRegistrationsForGroup("group1").size());
+        assertEquals(1, dao.getPendingInvitedRegistrationsForGroup("group2").size());
+
+        dao.deleteInvitedRegistrationRequest(reg.getId(), "group1");
+        assertTrue(dao.getPendingInvitedRegistrationsForGroup("group1").isEmpty());
+        assertEquals(1, dao.getPendingInvitedRegistrationsForGroup("group2").size());
+
+        dao.deleteInvitedRegistrationRequest(reg.getId(), "group2");
+
         groupsDAO.deleteGroup(group1);
         groupsDAO.deleteGroup(group2);
     }
+
+    @Test
+    public void testDeleteAllGroupsInvitedRegistrationsEmptyInput() {
+        dao.deleteAllGroupsInvitedRegistrations(new ArrayList<>());
+    }
+
+    @Test
+    public void testgetInvitedRegistrationsFromEmailAddressesEmptyInput() {
+        boolean exception = false;
+        try {
+            dao.getInvitedRegistrationsFromEmailAddresses(new ArrayList<>());
+        } catch (IllegalArgumentException ex) {
+            exception = true;
+        }
+        assertTrue(exception);
+    }
 }
diff --git a/gms/src/test/java/it/inaf/ia2/gms/persistence/MembershipsDAOTest.java b/gms/src/test/java/it/inaf/ia2/gms/persistence/MembershipsDAOTest.java
new file mode 100644
index 0000000..cb32b19
--- /dev/null
+++ b/gms/src/test/java/it/inaf/ia2/gms/persistence/MembershipsDAOTest.java
@@ -0,0 +1,90 @@
+package it.inaf.ia2.gms.persistence;
+
+import it.inaf.ia2.gms.DataSourceConfig;
+import it.inaf.ia2.gms.persistence.model.GroupEntity;
+import it.inaf.ia2.gms.persistence.model.MembershipEntity;
+import java.util.ArrayList;
+import java.util.Arrays;
+import javax.sql.DataSource;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.junit4.SpringRunner;
+
+@RunWith(SpringRunner.class)
+@ContextConfiguration(classes = DataSourceConfig.class)
+public class MembershipsDAOTest {
+
+    private static final String USER_1 = "user-1";
+    private static final String USER_2 = "user-2";
+
+    @Autowired
+    private DataSource dataSource;
+
+    private GroupsDAO groupsDAO;
+    private MembershipsDAO membershipsDAO;
+
+    @Before
+    public void setUp() {
+        groupsDAO = new GroupsDAO(dataSource);
+        membershipsDAO = new MembershipsDAO(dataSource);
+    }
+
+    @Test
+    public void testAddAndRemoveMembers() {
+        groupsDAO.createGroup(groupEntity("A"));
+        groupsDAO.createGroup(groupEntity("B"));
+
+        assertTrue(membershipsDAO.findByGroup("A").isEmpty());
+
+        membershipsDAO.addMember(membershipEntity(USER_1, "A"));
+        membershipsDAO.addMember(membershipEntity(USER_1, "B"));
+        membershipsDAO.addMember(membershipEntity(USER_2, "B"));
+
+        assertTrue(membershipsDAO.findByGroup("A").contains(membershipEntity(USER_1, "A")));
+        assertEquals(3, membershipsDAO.findByGroups(Arrays.asList("A", "B")).size());
+        assertEquals(2, membershipsDAO.getUserMemberships(USER_1).size());
+        assertEquals(1, membershipsDAO.getUserMemberships(USER_2, "").size());
+
+        assertTrue(membershipsDAO.isMemberOf(USER_1, "B"));
+        membershipsDAO.removeMembership("B", USER_1);
+        assertFalse(membershipsDAO.isMemberOf(USER_1, "B"));
+
+        assertTrue(membershipsDAO.isMemberOf(USER_1, "A"));
+        assertTrue(membershipsDAO.isMemberOf(USER_2, "B"));
+        membershipsDAO.deleteAllGroupsMembership(Arrays.asList("A", "B"));
+        assertFalse(membershipsDAO.isMemberOf(USER_1, "A"));
+        assertFalse(membershipsDAO.isMemberOf(USER_2, "B"));
+    }
+
+    @Test
+    public void testFindByGroupsEmptyList() {
+        assertTrue(membershipsDAO.findByGroups(new ArrayList<>()).isEmpty());
+    }
+
+    @Test
+    public void testDeleteAllGroupsMembershipEmptyList() {
+        membershipsDAO.deleteAllGroupsMembership(new ArrayList<>());
+    }
+
+    private GroupEntity groupEntity(String groupId) {
+        GroupEntity groupEntity = new GroupEntity();
+        groupEntity.setId(groupId);
+        groupEntity.setName(groupId);
+        groupEntity.setPath(groupId);
+        groupEntity.setLeaf(false);
+        return groupEntity;
+    }
+
+    private MembershipEntity membershipEntity(String userId, String groupId) {
+        MembershipEntity membershipEntity = new MembershipEntity();
+        membershipEntity.setUserId(userId);
+        membershipEntity.setGroupId(groupId);
+        return membershipEntity;
+    }
+}
diff --git a/gms/src/test/java/it/inaf/ia2/gms/persistence/NestedGroupsIntegrationTest.java b/gms/src/test/java/it/inaf/ia2/gms/persistence/NestedGroupsIntegrationTest.java
index 70896ea..f15f6d5 100644
--- a/gms/src/test/java/it/inaf/ia2/gms/persistence/NestedGroupsIntegrationTest.java
+++ b/gms/src/test/java/it/inaf/ia2/gms/persistence/NestedGroupsIntegrationTest.java
@@ -64,12 +64,12 @@ public class NestedGroupsIntegrationTest {
 
         // Setup groups
         GroupEntity root = groupsManager.getRoot();
-        GroupEntity lbt = groupsService.addGroup(root, "LBT", false);
-        GroupEntity tng = groupsService.addGroup(root, "TNG", false);
-        GroupEntity radio = groupsService.addGroup(root, "Radio", false);
-        GroupEntity lbtInaf = groupsService.addGroup(lbt, "INAF", false);
-        GroupEntity lbtInafProgram1 = groupsService.addGroup(lbtInaf, "P1", false);
-        GroupEntity lbtInafProgram2 = groupsService.addGroup(lbtInaf, "P2", false);
+        GroupEntity lbt = groupsService.addGroup(root, "LBT", false, "creator_id");
+        GroupEntity tng = groupsService.addGroup(root, "TNG", false, "creator_id");
+        GroupEntity radio = groupsService.addGroup(root, "Radio", false, "creator_id");
+        GroupEntity lbtInaf = groupsService.addGroup(lbt, "INAF", false, "creator_id");
+        GroupEntity lbtInafProgram1 = groupsService.addGroup(lbtInaf, "P1", false, "creator_id");
+        GroupEntity lbtInafProgram2 = groupsService.addGroup(lbtInaf, "P2", false, "creator_id");
 
         GroupsRequest request = new GroupsRequest();
         request.setPaginatorPage(1);
diff --git a/gms/src/test/java/it/inaf/ia2/gms/persistence/PermissionsDAOTest.java b/gms/src/test/java/it/inaf/ia2/gms/persistence/PermissionsDAOTest.java
index 0bffd30..93b543b 100644
--- a/gms/src/test/java/it/inaf/ia2/gms/persistence/PermissionsDAOTest.java
+++ b/gms/src/test/java/it/inaf/ia2/gms/persistence/PermissionsDAOTest.java
@@ -4,9 +4,13 @@ import it.inaf.ia2.gms.DataSourceConfig;
 import it.inaf.ia2.gms.model.Permission;
 import it.inaf.ia2.gms.persistence.model.PermissionEntity;
 import it.inaf.ia2.gms.persistence.model.GroupEntity;
+import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.List;
 import javax.sql.DataSource;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 import org.junit.Before;
 import org.junit.Test;
@@ -19,6 +23,8 @@ import org.springframework.test.context.junit4.SpringRunner;
 @ContextConfiguration(classes = DataSourceConfig.class)
 public class PermissionsDAOTest {
 
+    private static final String USER_ID = "user_id";
+
     @Autowired
     private DataSource dataSource;
 
@@ -34,41 +40,99 @@ public class PermissionsDAOTest {
     @Test
     public void testAll() {
 
-        GroupEntity root = new GroupEntity();
-        root.setId("ROOT");
-        root.setName("ROOT");
-        root.setPath("");
-        groupsDAO.createGroup(root);
-
-        String userId = "user_id";
+        GroupEntity group = groupEntity("A");
 
-        PermissionEntity permission = new PermissionEntity();
-        permission.setGroupId(root.getId());
-        permission.setUserId(userId);
-        permission.setPermission(Permission.ADMIN);
-        permission.setGroupPath(root.getPath());
+        PermissionEntity permission = permissionEntity(group, USER_ID, Permission.ADMIN);
 
         permissionsDAO.createOrUpdatePermission(permission);
 
-        List<PermissionEntity> permissions = permissionsDAO.findUserPermissions(userId);
+        List<PermissionEntity> permissions = permissionsDAO.findUserPermissions(USER_ID);
 
         assertEquals(1, permissions.size());
         assertEquals(Permission.ADMIN, permissions.get(0).getPermission());
-        assertEquals(userId, permissions.get(0).getUserId());
+        assertEquals(USER_ID, permissions.get(0).getUserId());
+        assertEquals("admin1", permissions.get(0).getSetBy());
+        assertNotNull(permissions.get(0).getUpdateTime());
 
-        permissions = permissionsDAO.findUserPermissions(userId, root.getPath());
+        permissions = permissionsDAO.findUserPermissions(USER_ID, group.getPath());
         assertEquals(1, permissions.size());
 
-        permissions = permissionsDAO.getGroupsPermissions(root.getId());
+        permissions = permissionsDAO.getGroupsPermissions(group.getId());
         assertEquals(1, permissions.size());
 
         // test upsert
         permission.setPermission(Permission.MANAGE_MEMBERS);
+        permission.setSetBy("admin2");
         permissionsDAO.createOrUpdatePermission(permission);
 
+        permissions = permissionsDAO.findUserPermissions(USER_ID);
+        assertEquals(1, permissions.size());
+        assertEquals(Permission.MANAGE_MEMBERS, permissions.get(0).getPermission());
+        assertEquals("admin2", permissions.get(0).getSetBy());
+        assertNotNull(permissions.get(0).getUpdateTime());
+
+        // test update
+        permission.setPermission(Permission.VIEW_MEMBERS);
+        permission.setSetBy("admin3");
+        permissionsDAO.updatePermission(permission);
+
+        permissions = permissionsDAO.findUserPermissions(USER_ID);
+        assertEquals(1, permissions.size());
+        assertEquals(Permission.VIEW_MEMBERS, permissions.get(0).getPermission());
+        assertEquals("admin3", permissions.get(0).getSetBy());
+        assertNotNull(permissions.get(0).getUpdateTime());
+
+        assertTrue(permissionsDAO.findPermissionEntity(group.getId(), USER_ID).isPresent());
+
         permissionsDAO.deletePermission(permission.getGroupId(), permission.getUserId());
 
-        permissions = permissionsDAO.findUserPermissions(userId);
+        permissions = permissionsDAO.findUserPermissions(USER_ID);
         assertTrue(permissions.isEmpty());
     }
+
+    @Test
+    public void testDeleteAllGroupsPermissions() {
+
+        GroupEntity groupB = groupEntity("B");
+        GroupEntity groupC = groupEntity("C");
+
+        PermissionEntity permissionB = permissionEntity(groupB, USER_ID, Permission.ADMIN);
+        PermissionEntity permissionC = permissionEntity(groupC, USER_ID, Permission.VIEW_MEMBERS);
+
+        permissionsDAO.createOrUpdatePermission(permissionB);
+        permissionsDAO.createOrUpdatePermission(permissionC);
+
+        assertTrue(permissionsDAO.findPermissionEntity(groupB.getId(), USER_ID).isPresent());
+        assertTrue(permissionsDAO.findPermissionEntity(groupC.getId(), USER_ID).isPresent());
+
+        permissionsDAO.deleteAllGroupsPermissions(Arrays.asList(groupB.getId(), groupC.getId()));
+
+        assertFalse(permissionsDAO.findPermissionEntity(groupB.getId(), USER_ID).isPresent());
+        assertFalse(permissionsDAO.findPermissionEntity(groupC.getId(), USER_ID).isPresent());
+    }
+
+    @Test
+    public void testDeleteAllGroupsPermissionsEmptyList() {
+        permissionsDAO.deleteAllGroupsPermissions(new ArrayList<>());
+    }
+
+    private GroupEntity groupEntity(String groupId) {
+        GroupEntity groupEntity = new GroupEntity();
+        groupEntity.setId(groupId);
+        groupEntity.setName(groupId);
+        groupEntity.setPath(groupId);
+        groupEntity.setLeaf(false);
+        groupsDAO.createGroup(groupEntity);
+        return groupEntity;
+    }
+
+    private PermissionEntity permissionEntity(GroupEntity group, String userId, Permission permission) {
+        PermissionEntity permissionEntity = new PermissionEntity();
+        permissionEntity.setGroupId(group.getId());
+        permissionEntity.setUserId(userId);
+        permissionEntity.setPermission(permission);
+        permissionEntity.setSetBy("admin1");
+        permissionEntity.setGroupPath(group.getPath());
+        return permissionEntity;
+    }
 }
diff --git a/gms/src/test/java/it/inaf/ia2/gms/service/PermissionsServiceTest.java b/gms/src/test/java/it/inaf/ia2/gms/service/PermissionsServiceTest.java
index ad03f47..17d2c53 100644
--- a/gms/src/test/java/it/inaf/ia2/gms/service/PermissionsServiceTest.java
+++ b/gms/src/test/java/it/inaf/ia2/gms/service/PermissionsServiceTest.java
@@ -23,6 +23,9 @@ import org.mockito.junit.MockitoJUnitRunner;
 @RunWith(MockitoJUnitRunner.class)
 public class PermissionsServiceTest {
 
+    private static final String ADMIN_ID = "ADMIN_ID";
+    private static final String USER_ID = "USER_ID";
+
     @Mock
     private PermissionsDAO permissionsDAO;
 
@@ -42,12 +45,12 @@ public class PermissionsServiceTest {
         existingEntity.setGroupId("ROOT");
         existingEntity.setPermission(Permission.VIEW_MEMBERS);
 
-        when(permissionsDAO.findPermissionEntity(eq("ROOT"), eq("user_id")))
+        when(permissionsDAO.findPermissionEntity(eq("ROOT"), eq(USER_ID)))
                 .thenReturn(Optional.of(existingEntity));
 
         when(permissionsDAO.createOrUpdatePermission(any())).thenAnswer(invocation -> invocation.getArgument(0));
 
-        PermissionEntity result = permissionsService.addPermission(root, "user_id", Permission.ADMIN);
+        PermissionEntity result = permissionsService.addPermission(root, USER_ID, Permission.ADMIN, ADMIN_ID);
 
         ArgumentCaptor<PermissionEntity> entityCaptor = ArgumentCaptor.forClass(PermissionEntity.class);
         verify(permissionsDAO, times(1)).createOrUpdatePermission(entityCaptor.capture());
@@ -65,10 +68,10 @@ public class PermissionsServiceTest {
         existingEntity.setGroupId("ROOT");
         existingEntity.setPermission(Permission.MANAGE_MEMBERS);
 
-        when(permissionsDAO.findPermissionEntity(eq("ROOT"), eq("user_id")))
+        when(permissionsDAO.findPermissionEntity(eq("ROOT"), eq(USER_ID)))
                 .thenReturn(Optional.of(existingEntity));
 
-        PermissionEntity result = permissionsService.addPermission(root, "user_id", Permission.VIEW_MEMBERS);
+        PermissionEntity result = permissionsService.addPermission(root, USER_ID, Permission.VIEW_MEMBERS, ADMIN_ID);
 
         verify(permissionsDAO, never()).createOrUpdatePermission(any());
         assertEquals(Permission.MANAGE_MEMBERS, result.getPermission());
@@ -84,10 +87,10 @@ public class PermissionsServiceTest {
         existingEntity.setGroupId("ROOT");
         existingEntity.setPermission(Permission.VIEW_MEMBERS);
 
-        when(permissionsDAO.findPermissionEntity(eq("ROOT"), eq("user_id")))
+        when(permissionsDAO.findPermissionEntity(eq("ROOT"), eq(USER_ID)))
                 .thenReturn(Optional.of(existingEntity));
 
-        PermissionEntity result = permissionsService.addPermission(root, "user_id", Permission.VIEW_MEMBERS);
+        PermissionEntity result = permissionsService.addPermission(root, USER_ID, Permission.VIEW_MEMBERS, ADMIN_ID);
 
         verify(permissionsDAO, never()).createOrUpdatePermission(any());
         assertEquals(Permission.VIEW_MEMBERS, result.getPermission());
-- 
GitLab