diff --git a/gms-ui/package.json b/gms-ui/package.json index 287b62f9087cc2cbfd90a08be5ec6ead4aecb3de..65869b384685a8301f8bd0e8fd0400c3e7614e9d 100644 --- a/gms-ui/package.json +++ b/gms-ui/package.json @@ -45,7 +45,9 @@ "plugin:vue/essential", "eslint:recommended" ], - "rules": {}, + "rules": { + "no-console": "warn" + }, "parserOptions": { "parser": "babel-eslint" } diff --git a/gms-ui/src/api/server/index.js b/gms-ui/src/api/server/index.js index f7079c8850ba39cd5adf841fddf58ca281cda1a6..cfb352e6cf78c9b814732462a06512e1fbbcc1ab 100644 --- a/gms-ui/src/api/server/index.js +++ b/gms-ui/src/api/server/index.js @@ -17,7 +17,7 @@ function apiRequest(options, showLoading = true, handleValidationErrors = false) loading(false); }) .catch(error => { - if(handleValidationErrors && error.response && error.response.status === 400) { + if (handleValidationErrors && error.response && error.response.status === 400) { reject(error.response.data); } else { dispatchApiErrorEvent(error); @@ -363,5 +363,19 @@ export default { 'Cache-Control': 'no-cache' } }, false); + }, + deleteInvitedRegistration(requestId, groupId) { + let url = BASE_API_URL + 'registration?' + + 'request_id=' + requestId + '&group_id=' + groupId; + return apiRequest({ + method: 'DELETE', + url: url, + withCredentials: true, + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'Cache-Control': 'no-cache' + } + }); } }; diff --git a/gms-ui/src/components/GenericSearchResults.vue b/gms-ui/src/components/GenericSearchResults.vue index e0277a7a1abe30798c996c043255a57bec14b4d4..ea163ca4b3ced10e2d33fa9d1ddff5162ba78381 100644 --- a/gms-ui/src/components/GenericSearchResults.vue +++ b/gms-ui/src/components/GenericSearchResults.vue @@ -1,18 +1,21 @@ <template> <div class="mt-sm-3"> <div> - <p>Search results:</p> - <b-list-group v-for="item in model.genericSearchResults.items" v-bind:key="item.id"> - <b-list-group-item href="#" v-on:click="openSearchResult(item)"> - <span class="float-left"> - <font-awesome-icon icon="folder" v-if="item.type === 'GROUP'"></font-awesome-icon> - <font-awesome-icon icon="user" v-if="item.type === 'USER'"></font-awesome-icon> - {{item.label}} - </span> - </b-list-group-item> - </b-list-group> - <Paginator :paginatedPanel="model.genericSearchResults" :onUpdate="updateSearchResults" :paginatorInput="input.genericSearch" /> + <div v-if="model.genericSearchResults.items && model.genericSearchResults.items.length > 0"> + <p>Search results:</p> + <b-list-group v-for="item in model.genericSearchResults.items" v-bind:key="item.id"> + <b-list-group-item href="#" v-on:click="openSearchResult(item)"> + <span class="float-left"> + <font-awesome-icon icon="folder" v-if="item.type === 'GROUP'"></font-awesome-icon> + <font-awesome-icon icon="user" v-if="item.type === 'USER'"></font-awesome-icon> + {{item.label}} + </span> + </b-list-group-item> + </b-list-group> + <Paginator :paginatedPanel="model.genericSearchResults" :onUpdate="updateSearchResults" :paginatorInput="input.genericSearch" /> + </div> </div> + <p v-if="model.genericSearchResults.items && model.genericSearchResults.items.length === 0">No entries were found matching your search</p> </div> </template> @@ -29,7 +32,7 @@ export default { model: state => state.model, input: state => state.input }), - created () { + created() { this.updateSearchResults(); }, watch: { diff --git a/gms-ui/src/components/GroupsPanel.vue b/gms-ui/src/components/GroupsPanel.vue index 748119af309fe5e491af5422c7c134b76c452f98..b77bcb1ca27a893db4ed2e7df81369eda1f004c1 100644 --- a/gms-ui/src/components/GroupsPanel.vue +++ b/gms-ui/src/components/GroupsPanel.vue @@ -14,7 +14,7 @@ <font-awesome-icon icon="edit"></font-awesome-icon> </a> - <a href="#" v-on:click.stop.prevent="openRemoveGroupModal(group)" class="text-danger" title="Delete"> + <a href="#" v-on:click.stop.prevent="openRemoveGroupModal(group)" class="text-danger" title="Delete" v-if="!group.locked"> <font-awesome-icon icon="trash"></font-awesome-icon> </a> </span> diff --git a/gms-ui/src/components/InvitedRegistrationPanel.vue b/gms-ui/src/components/InvitedRegistrationPanel.vue new file mode 100644 index 0000000000000000000000000000000000000000..a087bc6a78abeb5874fa7c3c79217a28ff340a1a --- /dev/null +++ b/gms-ui/src/components/InvitedRegistrationPanel.vue @@ -0,0 +1,50 @@ +<template> +<b-tab :title="'Invited (' + registrations.length + ')'" :title-link-class="{ 'd-none': registrations.length === 0 }"> + <table class="table b-table table-striped table-hover" v-if="registrations"> + <thead> + <tr> + <th>Email</th> + <th>Permission</th> + <th>Submitted at</th> + <th></th> + </tr> + </thead> + <tbody> + <tr v-for="(reg, index) in registrations" v-bind:key="index"> + <td>{{reg.email}}</td> + <td>{{reg.permission}}</td> + <td>{{reg.creationTime}}</td> + <td> + <a href="#" v-on:click.stop="openConfirmDeleteInvitedModal(reg)" class="text-danger" title="Remove permission"> + <font-awesome-icon icon="trash"></font-awesome-icon> + </a> + </td> + </tr> + </tbody> + </table> + <ConfirmDeleteInvitedModal ref="confirmDeleteInvitedModal" /> +</b-tab> +</template> + +<script> +import ConfirmDeleteInvitedModal from './modals/ConfirmDeleteInvitedModal.vue'; + +export default { + name: 'InvitedRegistrationPanel', + components: { + ConfirmDeleteInvitedModal + }, + computed: { + registrations() { + return this.$store.state.model.invitedRegistrations === null ? [] : this.$store.state.model.invitedRegistrations; + } + }, + methods: { + openConfirmDeleteInvitedModal(reg) { + let breadcrumbs = this.$store.state.model.breadcrumbs; + let currentGroupId = breadcrumbs[breadcrumbs.length - 1].groupId; + this.$refs.confirmDeleteInvitedModal.openConfirmDeleteInvitedModal(reg, currentGroupId); + } + } +} +</script> diff --git a/gms-ui/src/components/Main.vue b/gms-ui/src/components/Main.vue index 7c7829f99b855ba3660f150ce8a8ab3b9d7b02bf..189dcac16e56183357a02f03711eb9b068b0aa1f 100644 --- a/gms-ui/src/components/Main.vue +++ b/gms-ui/src/components/Main.vue @@ -6,6 +6,7 @@ <GroupsPanel /> <MembersPanel /> <PermissionsPanel /> + <InvitedRegistrationPanel /> <template slot="tabs-end"> <b-button variant="primary" class="in-tabs-header-btn" v-if="showAddGroupBtn" v-on:click="openAddGroupModal">Add group</b-button> <b-button variant="primary" class="in-tabs-header-btn" v-if="showAddMemberBtn" v-on:click="openAddMemberModal">Add member</b-button> @@ -25,6 +26,7 @@ import GroupsBreadcrumb from './GroupsBreadcrumb.vue' import GroupsPanel from './GroupsPanel.vue' import MembersPanel from './MembersPanel.vue' import PermissionsPanel from './PermissionsPanel.vue' +import InvitedRegistrationPanel from './InvitedRegistrationPanel.vue' import AddGroupModal from './modals/AddGroupModal.vue' import AddMemberModal from './modals/AddMemberModal.vue' import AddPermissionModal from './modals/AddPermissionModal.vue' @@ -37,6 +39,7 @@ export default { GroupsPanel, MembersPanel, PermissionsPanel, + InvitedRegistrationPanel, AddGroupModal, AddMemberModal, AddPermissionModal diff --git a/gms-ui/src/components/PermissionsPanel.vue b/gms-ui/src/components/PermissionsPanel.vue index c6631b857c27be3925217b1628d9e9f33099e448..88f2aaa1d0710eb40cb0afe717edddfe1765aa89 100644 --- a/gms-ui/src/components/PermissionsPanel.vue +++ b/gms-ui/src/components/PermissionsPanel.vue @@ -1,7 +1,7 @@ <template> <b-tab title="Permissions" :title-link-class="{ 'd-none': (model.permission !== 'ADMIN') }"> <div v-if="model.permissionsPanel !== null"> - <table class="table b-table table-striped table-hover"> + <table class="table b-table table-striped table-hover" v-if="model.permissionsPanel.items.length > 0"> <thead> <tr> <th>User</th> diff --git a/gms-ui/src/components/UserSearchResult.vue b/gms-ui/src/components/UserSearchResult.vue index d5ccf14e50fb4b99fd6fc39b22bb18ead0bb696a..0f95b90025efe4e1c32e93268a7521fa3247df41 100644 --- a/gms-ui/src/components/UserSearchResult.vue +++ b/gms-ui/src/components/UserSearchResult.vue @@ -29,7 +29,7 @@ <b-row> <b-col lg="10"> <b-list-group> - <b-list-group-item v-for="(identity, index) in user.identities" v-bind:key="index"> + <b-list-group-item :class="{ 'list-group-item-info': identity.primary }" 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> diff --git a/gms-ui/src/components/modals/AddMemberModal.vue b/gms-ui/src/components/modals/AddMemberModal.vue index 13e91d97725be0b646fdf00ae611a10d357fc38f..11634bdefab42cab9a5d124856481ca18bb5dafb 100644 --- a/gms-ui/src/components/modals/AddMemberModal.vue +++ b/gms-ui/src/components/modals/AddMemberModal.vue @@ -1,5 +1,5 @@ <template> -<b-modal id="add-member-modal" title="Add member" ok-title="Add" @shown="afterShow" @ok="addMember"> +<b-modal id="add-member-modal" title="Add member" ok-title="Add" @shown="afterShow" @ok="addMember" size="lg"> <SearchUser ref="searchUser" @searchUserEnter="addMember" /> </b-modal> </template> diff --git a/gms-ui/src/components/modals/AddPermissionModal.vue b/gms-ui/src/components/modals/AddPermissionModal.vue index b210f476598bebbcddf8e404659fbdabe8c48526..e988ee326b81024e71be7767d16225c849dd3f4b 100644 --- a/gms-ui/src/components/modals/AddPermissionModal.vue +++ b/gms-ui/src/components/modals/AddPermissionModal.vue @@ -1,5 +1,5 @@ <template> -<b-modal id="add-permission-modal" title="Add permission" @show="beforeShow" @shown="afterShow" :ok-title="okTitle" @ok="addPermission" :ok-variant="okBtnVariant"> +<b-modal id="add-permission-modal" title="Add permission" @show="beforeShow" @shown="afterShow" :ok-title="okTitle" @ok="addPermission" :ok-variant="okBtnVariant" size="lg"> <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. diff --git a/gms-ui/src/components/modals/ConfirmDeleteInvitedModal.vue b/gms-ui/src/components/modals/ConfirmDeleteInvitedModal.vue new file mode 100644 index 0000000000000000000000000000000000000000..e248834fa1b0c1ae39facb38eba52b7504e4076d --- /dev/null +++ b/gms-ui/src/components/modals/ConfirmDeleteInvitedModal.vue @@ -0,0 +1,37 @@ +<template> +<b-modal id="confirm-delete-invited-modal" title="Confirm action" ok-title="Delete" @ok="deleteInvitedRegistration" ok-variant="danger"> + <p v-if="invitedRegistration">Are you sure that you want to remove the invited registration for {{invitedRegistration.email}}?</p> +</b-modal> +</template> + +<script> +import client from 'api-client'; + +export default { + name: 'ConfirmDeleteInvitedModal', + data: function() { + return { + invitedRegistration: null, + groupId: null + } + }, + methods: { + openConfirmDeleteInvitedModal(invitedRegistration, groupId) { + this.invitedRegistration = invitedRegistration; + this.groupId = groupId; + this.$bvModal.show('confirm-delete-invited-modal'); + }, + deleteInvitedRegistration() { + let regId = this.invitedRegistration.id; + client.deleteInvitedRegistration(regId, this.groupId) + .then(() => { + let model = this.$store.state.model; + this.$store.commit('removeInvitedRegistration', regId); + if (model.invitedRegistrations.length === 0) { + this.$store.commit('setTabIndex', model.leaf ? 1 : 0); + } + }); + } + } +} +</script> diff --git a/gms-ui/src/components/modals/SearchUser.vue b/gms-ui/src/components/modals/SearchUser.vue index a6b058100713be53d11c7d349fffae1f2d346ac2..5f96f4bbdfdb6ffcb7d3e417cc6bfcd9601a57d8 100644 --- a/gms-ui/src/components/modals/SearchUser.vue +++ b/gms-ui/src/components/modals/SearchUser.vue @@ -50,7 +50,7 @@ export default { let user = res[i]; this.users.push({ value: user.id, - text: user.displayName + text: user.displayName + ' [' + user.id + ']' }); } if (this.users.length > 0) { diff --git a/gms-ui/src/store.js b/gms-ui/src/store.js index fe02ab560b3b5fc89ff032f2f038595e6f154de7..a0e8e3866f725d60e6473a30d920c4d0f1f61b68 100644 --- a/gms-ui/src/store.js +++ b/gms-ui/src/store.js @@ -15,6 +15,7 @@ export default new Vuex.Store({ groupsPanel: null, permissionsPanel: null, membersPanel: null, + invitedRegistrations: null, permission: null, leaf: false, user: null, @@ -45,12 +46,14 @@ export default new Vuex.Store({ state.model.breadcrumbs = model.breadcrumbs; state.model.groupsPanel = model.groupsPanel; state.model.permission = model.permission; + state.model.invitedRegistrations = model.invitedRegistrations; state.model.user = model.user; }, updateGroups(state, model) { state.model.breadcrumbs = model.breadcrumbs; state.model.groupsPanel = model.groupsPanel; state.model.permission = model.permission; + state.model.invitedRegistrations = model.invitedRegistrations; state.model.leaf = model.leaf; }, updateGroupsPanel(state, groupsPanel) { @@ -59,7 +62,7 @@ export default new Vuex.Store({ }, updatePermissionsPanel(state, permissionsPanel) { state.model.permissionsPanel = permissionsPanel; - for(let up of permissionsPanel.items) { + for (let up of permissionsPanel.items) { Vue.set(up, 'editable', false); } state.input.paginatorPage = permissionsPanel.currentPage; @@ -94,6 +97,18 @@ export default new Vuex.Store({ }, setGenericSearchFilter(state, filter) { state.input.genericSearch.filter = filter; + }, + removeInvitedRegistration(state, regId) { + let index = -1; + for (var i = 0; i < state.model.invitedRegistrations.length; i++) { + if (state.model.invitedRegistrations[i].id === regId) { + index = i; + break; + } + } + if (index !== -1) { + state.model.invitedRegistrations.splice(index, 1); + } } }, actions: { diff --git a/gms/src/main/java/it/inaf/ia2/gms/controller/GroupsTabResponseBuilder.java b/gms/src/main/java/it/inaf/ia2/gms/controller/GroupsTabResponseBuilder.java index f560d5d236777bf2515ed508bd233d03192a3510..2718b19cd8d586c6fd8ca882b31466a4538f29bb 100644 --- a/gms/src/main/java/it/inaf/ia2/gms/controller/GroupsTabResponseBuilder.java +++ b/gms/src/main/java/it/inaf/ia2/gms/controller/GroupsTabResponseBuilder.java @@ -2,6 +2,7 @@ package it.inaf.ia2.gms.controller; import it.inaf.ia2.gms.authn.SessionData; import it.inaf.ia2.gms.manager.GroupsManager; +import it.inaf.ia2.gms.manager.InvitedRegistrationManager; import it.inaf.ia2.gms.manager.PermissionsManager; import it.inaf.ia2.gms.model.Permission; import it.inaf.ia2.gms.model.request.GroupsRequest; @@ -30,6 +31,9 @@ public class GroupsTabResponseBuilder { @Autowired private GroupsTreeBuilder groupsListBuilder; + @Autowired + private InvitedRegistrationManager invitedRegistrationManager; + public GroupsTabResponse getGroupsTab(GroupsRequest request) { GroupEntity group = groupsService.getGroupById(request.getGroupId()); @@ -46,6 +50,8 @@ public class GroupsTabResponseBuilder { response.setLeaf(group.isLeaf()); + response.setInvitedRegistrations(invitedRegistrationManager.getInvitedRegistrationsForGroup(group)); + return response; } } diff --git a/gms/src/main/java/it/inaf/ia2/gms/controller/InvitedRegistrationController.java b/gms/src/main/java/it/inaf/ia2/gms/controller/InvitedRegistrationController.java index 0a4554cad6fe0b406ef329446d0779239705e8c8..82dd95a212ba2aa5211cea7e6d613a036bac64da 100644 --- a/gms/src/main/java/it/inaf/ia2/gms/controller/InvitedRegistrationController.java +++ b/gms/src/main/java/it/inaf/ia2/gms/controller/InvitedRegistrationController.java @@ -10,7 +10,9 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.servlet.support.ServletUriComponentsBuilder; @@ -55,6 +57,12 @@ public class InvitedRegistrationController { } } + @DeleteMapping(value = "/registration", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity<?> deleteInvitedRegistration(@RequestParam("request_id") String requestId, @RequestParam("group_id") String groupId) { + invitedRegistrationManager.deleteInvitedRegistration(requestId, groupId); + return ResponseEntity.noContent().build(); + } + private String getFileContent(String templateFileName) throws IOException { try (InputStream in = InvitedRegistrationController.class.getClassLoader().getResourceAsStream("templates/" + templateFileName)) { Scanner s = new Scanner(in).useDelimiter("\\A"); 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 73bdea1bf962b3d451624294111d7fbfad141fc0..f30dca3c123a25dc9c7afec26bbe5669cfa81e5f 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 @@ -1,8 +1,10 @@ package it.inaf.ia2.gms.manager; +import it.inaf.ia2.gms.exception.BadRequestException; import it.inaf.ia2.gms.exception.NotFoundException; import it.inaf.ia2.gms.exception.UnauthorizedException; import it.inaf.ia2.gms.model.Permission; +import it.inaf.ia2.gms.model.response.InvitedRegistrationItem; import it.inaf.ia2.gms.persistence.GroupsDAO; import it.inaf.ia2.gms.persistence.InvitedRegistrationDAO; import it.inaf.ia2.gms.persistence.LoggingDAO; @@ -14,8 +16,10 @@ import it.inaf.ia2.gms.service.PermissionsService; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; import java.util.Base64; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Optional; import java.util.UUID; @@ -119,4 +123,47 @@ public class InvitedRegistrationManager extends UserAwareComponent { return Optional.ofNullable(invitedRegistration); } + + public List<InvitedRegistrationItem> getInvitedRegistrationsForGroup(GroupEntity group) { + + if (permissionsManager.getCurrentUserPermission(group) != Permission.ADMIN) { + return null; + } + + List<InvitedRegistrationItem> items = new ArrayList<>(); + + for (InvitedRegistration reg : invitedRegistrationDAO.getPendingInvitedRegistrationsForGroup(group.getId())) { + + Map<String, Permission> map = reg.getGroupsPermissions(); + + if (map != null) { + for (Permission permission : map.values()) { + InvitedRegistrationItem item = new InvitedRegistrationItem() + .setId(reg.getId()) + .setEmail(reg.getEmail()) + .setPermission(permission) + .setCreationTime(reg.getCreationTime()); + items.add(item); + } + } + } + + return items; + } + + public void deleteInvitedRegistration(String registrationId, String groupId) { + + GroupEntity group = groupsDAO.findGroupById(groupId) + .orElseThrow(() -> new BadRequestException("No group found for given id: " + groupId)); + + if (permissionsManager.getUserPermission(group, getCurrentUserId()) != Permission.ADMIN) { + throw new UnauthorizedException("Only administrators can delete invited registrations!"); + } + + invitedRegistrationDAO.deleteInvitedRegistrationRequest(registrationId, groupId); + + loggingDAO.logAction("Deleted invited registration request. " + + "[request_id=" + registrationId + ", group_id=" + groupId + + ", group_name=" + group.getName() + "]"); + } } 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 381d846578c781a40ccb4463bd23983c785d0aaf..b7237fcaa6ed190116a94bc19ae68bc75bf88556 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 @@ -7,6 +7,7 @@ public class GroupNode { private Permission permission; private boolean hasChildren; private boolean leaf; + private boolean locked; public String getGroupId() { return groupId; @@ -47,4 +48,12 @@ public class GroupNode { public void setLeaf(boolean leaf) { this.leaf = leaf; } + + public boolean isLocked() { + return locked; + } + + public void setLocked(boolean locked) { + this.locked = locked; + } } diff --git a/gms/src/main/java/it/inaf/ia2/gms/model/response/GroupsTabResponse.java b/gms/src/main/java/it/inaf/ia2/gms/model/response/GroupsTabResponse.java index 0eb8665d0fa38d2d1de5eaadd0b68d94a76016b2..d3d75dab12ee4b40297da96361a67f137d5dd35c 100644 --- a/gms/src/main/java/it/inaf/ia2/gms/model/response/GroupsTabResponse.java +++ b/gms/src/main/java/it/inaf/ia2/gms/model/response/GroupsTabResponse.java @@ -11,6 +11,7 @@ public class GroupsTabResponse { private PaginatedData<GroupNode> groupsPanel; // current group permissions private Permission permission; + private List<InvitedRegistrationItem> invitedRegistrations; private boolean leaf; @@ -45,4 +46,12 @@ public class GroupsTabResponse { public void setLeaf(boolean leaf) { this.leaf = leaf; } + + public List<InvitedRegistrationItem> getInvitedRegistrations() { + return invitedRegistrations; + } + + public void setInvitedRegistrations(List<InvitedRegistrationItem> invitedRegistrations) { + this.invitedRegistrations = invitedRegistrations; + } } diff --git a/gms/src/main/java/it/inaf/ia2/gms/model/response/InvitedRegistrationItem.java b/gms/src/main/java/it/inaf/ia2/gms/model/response/InvitedRegistrationItem.java new file mode 100644 index 0000000000000000000000000000000000000000..cc31f77c1934108f6c911c6de67ad3590e095989 --- /dev/null +++ b/gms/src/main/java/it/inaf/ia2/gms/model/response/InvitedRegistrationItem.java @@ -0,0 +1,50 @@ +package it.inaf.ia2.gms.model.response; + +import com.fasterxml.jackson.annotation.JsonFormat; +import it.inaf.ia2.gms.model.Permission; +import java.util.Date; + +public class InvitedRegistrationItem { + + private String id; + private String email; + private Permission permission; + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd") + private Date creationTime; + + public String getId() { + return id; + } + + public InvitedRegistrationItem setId(String id) { + this.id = id; + return this; + } + + public String getEmail() { + return email; + } + + public InvitedRegistrationItem setEmail(String email) { + this.email = email; + return this; + } + + public Permission getPermission() { + return permission; + } + + public InvitedRegistrationItem setPermission(Permission permission) { + this.permission = permission; + return this; + } + + public Date getCreationTime() { + return creationTime; + } + + public InvitedRegistrationItem setCreationTime(Date creationTime) { + this.creationTime = creationTime; + return this; + } +} 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 42285935cb9a1324dd87212b566315ce08b8c509..3cbffa59790a17845926da0690266a4bf1e93048 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 @@ -84,7 +84,7 @@ public class GroupsDAO { public Optional<GroupEntity> findGroupById(String groupId) { - String sql = "SELECT id, name, path, is_leaf from gms_group WHERE id = ?"; + String sql = "SELECT id, name, path, is_leaf, locked from gms_group WHERE id = ?"; return jdbcTemplate.query(conn -> { PreparedStatement ps = conn.prepareStatement(sql); @@ -97,6 +97,7 @@ public class GroupsDAO { group.setName(resultSet.getString("name")); group.setPath(resultSet.getString("path")); group.setLeaf(resultSet.getBoolean("is_leaf")); + group.setLocked(resultSet.getBoolean("locked")); return Optional.of(group); } return Optional.empty(); @@ -105,7 +106,7 @@ public class GroupsDAO { public Optional<GroupEntity> findGroupByPath(String path) { - String sql = "SELECT id, name, is_leaf from gms_group WHERE path = ?"; + String sql = "SELECT id, name, is_leaf, locked from gms_group WHERE path = ?"; return jdbcTemplate.query(conn -> { PreparedStatement ps = conn.prepareStatement(sql); @@ -117,6 +118,7 @@ public class GroupsDAO { group.setId(resultSet.getString("id")); group.setName(resultSet.getString("name")); group.setLeaf(resultSet.getBoolean("is_leaf")); + group.setLocked(resultSet.getBoolean("locked")); group.setPath(path); return Optional.of(group); } @@ -132,7 +134,7 @@ public class GroupsDAO { return jdbcTemplate.query(conn -> { - String sql = "SELECT id, name, path, is_leaf from gms_group WHERE id IN ("; + String sql = "SELECT id, name, path, is_leaf, locked from gms_group WHERE id IN ("; sql += String.join(",", identifiers.stream().map(p -> "?").collect(Collectors.toList())); sql += ")"; @@ -150,6 +152,7 @@ public class GroupsDAO { 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; @@ -158,7 +161,7 @@ public class GroupsDAO { public Optional<GroupEntity> findGroupByParentAndName(String parentPath, String childName) { - String sql = "SELECT id, path, is_leaf from gms_group WHERE name = ? AND path ~ ?"; + String sql = "SELECT id, path, is_leaf, locked from gms_group WHERE name = ? AND path ~ ?"; return jdbcTemplate.query(conn -> { PreparedStatement ps = conn.prepareStatement(sql); @@ -172,6 +175,7 @@ public class GroupsDAO { group.setName(childName); group.setPath(resultSet.getString("path")); group.setLeaf(resultSet.getBoolean("is_leaf")); + group.setLocked(resultSet.getBoolean("locked")); return Optional.of(group); } return Optional.empty(); @@ -191,9 +195,9 @@ public class GroupsDAO { String sql; if (hasSearchFilter) { - sql = "SELECT id, name, path, is_leaf FROM gms_group WHERE path ~ ? AND name ILIKE ? ORDER BY name"; + sql = "SELECT id, name, path, is_leaf, locked FROM gms_group WHERE path ~ ? AND name ILIKE ? ORDER BY name"; } else { - sql = "SELECT id, name, path, is_leaf FROM gms_group WHERE path ~ ? ORDER BY name"; + sql = "SELECT id, name, path, is_leaf, locked FROM gms_group WHERE path ~ ? ORDER BY name"; } return jdbcTemplate.query(conn -> { @@ -218,7 +222,7 @@ public class GroupsDAO { public List<GroupEntity> getAllChildren(String path) { - String sql = "SELECT id, name, path, is_leaf FROM gms_group WHERE path <@ ? AND path <> ? ORDER BY nlevel(path) DESC"; + String sql = "SELECT id, name, path, is_leaf, locked FROM gms_group WHERE path <@ ? AND path <> ? ORDER BY nlevel(path) DESC"; return jdbcTemplate.query(conn -> { PreparedStatement ps = conn.prepareStatement(sql); @@ -232,7 +236,7 @@ public class GroupsDAO { public List<GroupEntity> findGroupsByNames(List<String> names) { - String sql = "SELECT id, name, path, is_leaf from gms_group WHERE name IN (" + String sql = "SELECT id, name, path, is_leaf, locked from gms_group WHERE name IN (" + String.join(",", names.stream().map(g -> "?").collect(Collectors.toList())) + ")"; @@ -256,6 +260,7 @@ public class GroupsDAO { 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; @@ -326,7 +331,7 @@ public class GroupsDAO { public List<GroupEntity> searchGroups(String searchText) { - String sql = "SELECT id, name, path, is_leaf from gms_group WHERE name ILIKE ?"; + String sql = "SELECT id, name, path, is_leaf, locked 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/InvitedRegistrationDAO.java b/gms/src/main/java/it/inaf/ia2/gms/persistence/InvitedRegistrationDAO.java index af61db9bfdb9fbb3f51333897cc42b15d73fe29e..43415d614786612d41c30029d7253ea1d76038a6 100644 --- a/gms/src/main/java/it/inaf/ia2/gms/persistence/InvitedRegistrationDAO.java +++ b/gms/src/main/java/it/inaf/ia2/gms/persistence/InvitedRegistrationDAO.java @@ -4,6 +4,9 @@ import it.inaf.ia2.gms.model.Permission; import it.inaf.ia2.gms.persistence.model.InvitedRegistration; import java.sql.PreparedStatement; 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; @@ -54,7 +57,7 @@ public class InvitedRegistrationDAO { public Optional<InvitedRegistration> getInvitedRegistrationFromToken(String tokenHash) { - String sqlReq = "SELECT id, email FROM invited_registration_request WHERE token_hash = ? AND done IS NOT true"; + String sqlReq = "SELECT id, email, creation_time FROM invited_registration_request WHERE token_hash = ? AND done IS NOT true"; InvitedRegistration registration = jdbcTemplate.query(conn -> { PreparedStatement ps = conn.prepareStatement(sqlReq); @@ -65,6 +68,7 @@ public class InvitedRegistrationDAO { InvitedRegistration reg = new InvitedRegistration(); reg.setId(resultSet.getString("id")); reg.setEmail(resultSet.getString("email")); + reg.setCreationTime(new Date(resultSet.getDate("creation_time").getTime())); return reg; } return null; @@ -126,7 +130,7 @@ public class InvitedRegistrationDAO { return; } - String sql = "DELETE FROM invited_registration_request_group WHERE group_id = (" + String sql = "DELETE FROM invited_registration_request_group WHERE group_id IN (" + String.join(",", groupIds.stream().map(g -> "?").collect(Collectors.toList())) + ")"; @@ -143,4 +147,66 @@ public class InvitedRegistrationDAO { jdbcTemplate.update("DELETE FROM invited_registration_request WHERE id NOT IN " + "(SELECT request_id FROM invited_registration_request_group)"); } + + public List<InvitedRegistration> getPendingInvitedRegistrationsForGroup(String groupId) { + + String sql = "SELECT id, email, creation_time, permission\n" + + "FROM invited_registration_request r\n" + + "JOIN invited_registration_request_group rg ON r.id = rg.request_id\n" + + "WHERE done IS NOT TRUE AND rg.group_id = ?"; + + return jdbcTemplate.query(sql, + ps -> { + ps.setString(1, groupId); + }, + rs -> { + // key: id + Map<String, InvitedRegistration> map = new HashMap<>(); + + while (rs.next()) { + + String id = rs.getString("id"); + + InvitedRegistration reg = map.get(id); + if (reg == null) { + String email = rs.getString("email"); + Date creationTime = new Date(rs.getDate("creation_time").getTime()); + reg = new InvitedRegistration() + .setId(id) + .setEmail(email) + .setCreationTime(creationTime); + map.put(id, reg); + } + + if (reg.getGroupsPermissions() == null) { + reg.setGroupsPermissions(new HashMap<>()); + } + + Permission permission = Permission.valueOf(rs.getString("permission")); + + reg.getGroupsPermissions().put(groupId, permission); + } + + List<InvitedRegistration> registrations = new ArrayList<>(map.values()); + + Collections.sort(registrations, (reg1, reg2) -> reg1.getEmail().compareToIgnoreCase(reg2.getEmail())); + + return registrations; + }); + } + + public void deleteInvitedRegistrationRequest(String requestId, String groupId) { + + String sql = "DELETE FROM invited_registration_request_group\n" + + "WHERE request_id = ? AND group_id = ?"; + + jdbcTemplate.update(sql, ps -> { + ps.setString(1, requestId); + ps.setString(2, groupId); + }); + + // Cleanup orphan invited requests + jdbcTemplate.update("DELETE FROM invited_registration_request WHERE id NOT IN " + + "(SELECT request_id FROM invited_registration_request_group)"); + } } 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 1dc3ed1f06e1f4a2b5199137706bf4c56deda296..20590cfc59a0922d54a234ccc90e584f054ba660 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 @@ -8,6 +8,7 @@ public class GroupEntity { private String name; private String path; private boolean leaf; + private boolean locked; public String getId() { return id; @@ -41,6 +42,14 @@ public class GroupEntity { this.leaf = leaf; } + public boolean isLocked() { + return locked; + } + + public void setLocked(boolean locked) { + this.locked = locked; + } + public String getParentPath() { if (path.isEmpty()) { return null; diff --git a/gms/src/main/java/it/inaf/ia2/gms/persistence/model/InvitedRegistration.java b/gms/src/main/java/it/inaf/ia2/gms/persistence/model/InvitedRegistration.java index e7a922be8d7c773046469dc3dc81c8cf0530514d..9acc15e7a07c5e692306fc2158125edf314df99b 100644 --- a/gms/src/main/java/it/inaf/ia2/gms/persistence/model/InvitedRegistration.java +++ b/gms/src/main/java/it/inaf/ia2/gms/persistence/model/InvitedRegistration.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.Map; public class InvitedRegistration { @@ -10,6 +11,7 @@ public class InvitedRegistration { private String email; private boolean done; private String userId; + private Date creationTime; private Map<String, Permission> groupsPermissions; public String getId() { @@ -52,8 +54,18 @@ public class InvitedRegistration { return userId; } - public void setUserId(String userId) { + public InvitedRegistration setUserId(String userId) { this.userId = userId; + return this; + } + + public Date getCreationTime() { + return creationTime; + } + + public InvitedRegistration setCreationTime(Date creationTime) { + this.creationTime = creationTime; + return this; } public Map<String, Permission> getGroupsPermissions() { 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 14d503ee566fb2b779e08a9542d893bf75e717e6..f38717ff2f278b75e0dc74f9264a638e7ae1ae2f 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 @@ -99,6 +99,10 @@ public class GroupsService { throw new UnauthorizedException("It is not possible to remove the ROOT"); } + if (group.isLocked()) { + throw new UnauthorizedException("Group " + group.getId() + " is locked and can't be deleted"); + } + String parentPath = group.getParentPath(); GroupEntity parent = groupsDAO.findGroupByPath(parentPath) .orElseThrow(() -> new BadRequestException("No group found at path " + parentPath)); @@ -117,7 +121,7 @@ public class GroupsService { groupsDAO.deleteGroup(g); } - loggingDAO.logAction("Group deleted, group_id=" + group.getId()); + loggingDAO.logAction("Group deleted [group_id=" + group.getId() + ", group_name=" + group.getName() + "]"); return parent; } 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 80c5384d7e723e51685d97f9c7238c6b44586ecd..e3c214559f241ef428d0c6f21fd28333d3806a1d 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 @@ -60,6 +60,7 @@ public class GroupsTreeBuilder { node.setGroupId(group.getId()); node.setGroupName(group.getName()); node.setLeaf(group.isLeaf()); + node.setLocked(group.isLocked()); node.setPermission(permission); nodes.add(node); }); diff --git a/gms/src/main/resources/sql/init.sql b/gms/src/main/resources/sql/init.sql index 6f4693efa54d57449bc7fc47e8a6df3237885656..05aec53ca465b3b7fe8241685973524e74e2daae 100644 --- a/gms/src/main/resources/sql/init.sql +++ b/gms/src/main/resources/sql/init.sql @@ -5,6 +5,7 @@ CREATE TABLE gms_group ( name text NOT NULL, path ltree NOT NULL, is_leaf boolean, + locked boolean, primary key(id) ); diff --git a/gms/src/main/resources/static/help/help-admin.html b/gms/src/main/resources/static/help/help-admin.html index f452453aba0beea927dad9fcbb399d1b4fcc2daf..093446d5b07012a309b9e922a0ba8cfa9afc1f69 100644 --- a/gms/src/main/resources/static/help/help-admin.html +++ b/gms/src/main/resources/static/help/help-admin.html @@ -102,6 +102,14 @@ You can also use the edit icon (<img src="img/pencil-icon.jpg" alt="" />) in order to change a permission.</p> + <h2 class="mt-4">Pending invited registrations</h2> + + <p>If a registration invitation has been sent and the user has not yet registered an additional tab appears:</p> + + <p> + <img src="img/gms-invited-panel.jpg" alt="" /> + </p> + <h2 class="mt-4">Seeing information about users</h2> <p>You can click on the user names in the Members or Permissions tab in order to see a detailed page about a specific user.</p> diff --git a/gms/src/main/resources/static/help/img/gms-invited-panel.jpg b/gms/src/main/resources/static/help/img/gms-invited-panel.jpg new file mode 100644 index 0000000000000000000000000000000000000000..3f2b7d1cbe2bbac8dd040dfe569ffa9f99748b3c Binary files /dev/null and b/gms/src/main/resources/static/help/img/gms-invited-panel.jpg differ diff --git a/gms/src/test/java/it/inaf/ia2/gms/controller/GroupsTabResponseBuilderTest.java b/gms/src/test/java/it/inaf/ia2/gms/controller/GroupsTabResponseBuilderTest.java index c4ac899f69c721adb51db8354ef17162370d05e8..937fbe74b33dd9e0756099bcb62d47f14a00ffb1 100644 --- a/gms/src/test/java/it/inaf/ia2/gms/controller/GroupsTabResponseBuilderTest.java +++ b/gms/src/test/java/it/inaf/ia2/gms/controller/GroupsTabResponseBuilderTest.java @@ -2,6 +2,7 @@ package it.inaf.ia2.gms.controller; import it.inaf.ia2.gms.authn.SessionData; import it.inaf.ia2.gms.manager.GroupsManager; +import it.inaf.ia2.gms.manager.InvitedRegistrationManager; import it.inaf.ia2.gms.manager.PermissionsManager; import it.inaf.ia2.gms.model.GroupNode; import it.inaf.ia2.gms.model.Permission; @@ -31,7 +32,7 @@ public class GroupsTabResponseBuilderTest { @Mock private GroupsManager groupsManager; - + @Mock private GroupsService groupsService; @@ -41,6 +42,9 @@ public class GroupsTabResponseBuilderTest { @Mock private GroupsTreeBuilder groupsTreeBuilder; + @Mock + private InvitedRegistrationManager invitedRegistrationManager; + @InjectMocks private GroupsTabResponseBuilder groupsTabResponseBuilder; diff --git a/gms/src/test/java/it/inaf/ia2/gms/controller/InvitedRegistrationControllerTest.java b/gms/src/test/java/it/inaf/ia2/gms/controller/InvitedRegistrationControllerTest.java new file mode 100644 index 0000000000000000000000000000000000000000..c6c1c2e5a295afe822145b7f7f2c85e22a72a566 --- /dev/null +++ b/gms/src/test/java/it/inaf/ia2/gms/controller/InvitedRegistrationControllerTest.java @@ -0,0 +1,44 @@ +package it.inaf.ia2.gms.controller; + +import it.inaf.ia2.gms.manager.InvitedRegistrationManager; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import static org.mockito.ArgumentMatchers.eq; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.test.web.servlet.MockMvc; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +@RunWith(MockitoJUnitRunner.class) +public class InvitedRegistrationControllerTest { + + @Mock + private InvitedRegistrationManager manager; + + @InjectMocks + private InvitedRegistrationController controller; + + private MockMvc mockMvc; + + @Before + public void init() { + mockMvc = MockMvcBuilders.standaloneSetup(controller).build(); + } + + @Test + public void testDeleteInvitedRegistration() throws Exception { + + mockMvc.perform(delete("/registration?request_id=req1&group_id=group1")) + .andDo(print()) + .andExpect(status().isNoContent()); + + verify(manager, times(1)).deleteInvitedRegistration(eq("req1"), eq("group1")); + } +} 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 new file mode 100644 index 0000000000000000000000000000000000000000..555f27b6b5d992148d60e315b2191955e0941460 --- /dev/null +++ b/gms/src/test/java/it/inaf/ia2/gms/persistence/InvitedRegistrationDAOTest.java @@ -0,0 +1,95 @@ +package it.inaf.ia2.gms.persistence; + +import it.inaf.ia2.gms.DataSourceConfig; +import it.inaf.ia2.gms.HooksConfig; +import it.inaf.ia2.gms.model.Permission; +import it.inaf.ia2.gms.persistence.model.GroupEntity; +import it.inaf.ia2.gms.persistence.model.InvitedRegistration; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import javax.sql.DataSource; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +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, HooksConfig.class}) +public class InvitedRegistrationDAOTest { + + @Autowired + private DataSource dataSource; + + private GroupsDAO groupsDAO; + + private InvitedRegistrationDAO dao; + + @Before + public void setUp() { + groupsDAO = new GroupsDAO(dataSource); + dao = new InvitedRegistrationDAO(dataSource); + } + + @Test + public void test() { + + GroupEntity root = new GroupEntity(); + root.setId("ROOT"); + root.setName("ROOT"); + root.setPath(""); + groupsDAO.createGroup(root); + + GroupEntity group1 = new GroupEntity(); + group1.setId("group1"); + group1.setName("Group1"); + group1.setPath(group1.getId()); + groupsDAO.createGroup(group1); + + GroupEntity group2 = new GroupEntity(); + group2.setId("group2"); + group2.setName("Group2"); + group2.setPath(group2.getId()); + groupsDAO.createGroup(group2); + + Map<String, Permission> groupsPermissions = new HashMap<>(); + groupsPermissions.put("group1", Permission.VIEW_MEMBERS); + groupsPermissions.put("group2", Permission.MANAGE_MEMBERS); + + InvitedRegistration reg = new InvitedRegistration() + .setId("id1") + .setEmail("test@inaf.it") + .setTokenHash("token_hash") + .setGroupsPermissions(groupsPermissions); + + dao.addInvitedRegistration(reg); + + InvitedRegistration regFromToken = dao.getInvitedRegistrationFromToken("token_hash").get(); + assertEquals(reg.getId(), regFromToken.getId()); + assertEquals(reg.getEmail(), regFromToken.getEmail()); + assertNotNull(regFromToken.getCreationTime()); + + InvitedRegistration regFromGroup = dao.getPendingInvitedRegistrationsForGroup("group1").get(0); + assertEquals(reg.getId(), regFromGroup.getId()); + assertEquals(reg.getEmail(), regFromGroup.getEmail()); + assertNotNull(regFromGroup.getCreationTime()); + + dao.setRegistrationDone(regFromGroup); + + assertTrue(dao.getPendingInvitedRegistrationsForGroup("group1").isEmpty()); + + List<String> groupsIds = new ArrayList<>(); + groupsIds.add(group1.getId()); + groupsIds.add(group2.getId()); + dao.deleteAllGroupsInvitedRegistrations(groupsIds); + + groupsDAO.deleteGroup(group1); + groupsDAO.deleteGroup(group2); + } +}