diff --git a/src/main/java/org/gridsuite/useradmin/server/controller/UserAdminController.java b/src/main/java/org/gridsuite/useradmin/server/controller/UserAdminController.java index ae0a3df..c4e4234 100644 --- a/src/main/java/org/gridsuite/useradmin/server/controller/UserAdminController.java +++ b/src/main/java/org/gridsuite/useradmin/server/controller/UserAdminController.java @@ -7,7 +7,6 @@ package org.gridsuite.useradmin.server.controller; import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; @@ -16,6 +15,7 @@ import org.gridsuite.useradmin.server.dto.UserConnection; import org.gridsuite.useradmin.server.dto.UserInfos; import org.gridsuite.useradmin.server.dto.UserProfile; +import org.gridsuite.useradmin.server.repository.AnnouncementEntity; import org.gridsuite.useradmin.server.service.UserAdminService; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; @@ -23,6 +23,7 @@ import org.springframework.web.bind.annotation.*; import java.util.List; +import java.util.UUID; /** * @author Etienne Homer @@ -146,27 +147,38 @@ public ResponseEntity> getConnections(@RequestHeader("userI return ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON).body(service.getConnections(userId)); } - @PostMapping(value = "/messages/maintenance") - @Operation(summary = "send a message to all users connected") + @PostMapping(value = "/announcements") + @Operation(summary = "Send an announcement to all the connected users") @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "message sent"), + @ApiResponse(responseCode = "200", description = "announcement sent"), @ApiResponse(responseCode = "403", description = "user is not an admin") }) - public ResponseEntity sendMaintenanceMessage(@RequestHeader("userId") String userId, - @Parameter(description = "the display time of the message in seconds") @RequestParam(value = "durationInSeconds", required = false) Integer duration, - @Parameter(description = "the message to display") @RequestBody String message) { - service.sendMaintenanceMessage(userId, duration, message); + public ResponseEntity sendAnnouncement(@RequestHeader("userId") String userId, + @RequestBody AnnouncementEntity announcement) { + service.sendAnnouncement(announcement, userId); return ResponseEntity.ok().build(); } - @PostMapping(value = "/messages/cancel-maintenance") - @Operation(summary = "send a message to all users connected") + @DeleteMapping(value = "/announcements/{announcementId}") + @Operation(summary = "Cancel and delete an announcement") @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "message sent"), - @ApiResponse(responseCode = "403", description = "user is not an admin") + @ApiResponse(responseCode = "200", description = "announcement canceled"), + @ApiResponse(responseCode = "403", description = "user is not an admin"), + @ApiResponse(responseCode = "404", description = "announcement not found") }) - public ResponseEntity sendCancelMaintenanceMessage(@RequestHeader("userId") String userId) { - service.sendCancelMaintenanceMessage(userId); + public ResponseEntity cancelAnnouncement(@RequestHeader("userId") String userId, + @PathVariable("announcementId") UUID announcementId) { + service.cancelAnnouncement(announcementId, userId); return ResponseEntity.ok().build(); } + + @GetMapping(value = "/announcements") + @Operation(summary = "Get all the announcements") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "announcements retrieved"), + @ApiResponse(responseCode = "403", description = "user is not an admin") + }) + public ResponseEntity> getAllAnnouncements(@RequestHeader("userId") String userId) { + return ResponseEntity.ok(service.getAllAnnouncements(userId)); + } } diff --git a/src/main/java/org/gridsuite/useradmin/server/repository/AnnouncementEntity.java b/src/main/java/org/gridsuite/useradmin/server/repository/AnnouncementEntity.java new file mode 100644 index 0000000..a26eed7 --- /dev/null +++ b/src/main/java/org/gridsuite/useradmin/server/repository/AnnouncementEntity.java @@ -0,0 +1,50 @@ +/** + * Copyright (c) 2024, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +package org.gridsuite.useradmin.server.repository; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.time.Duration; +import java.time.Instant; +import java.util.UUID; + +/** + * @author Florent MILLOT + */ +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter +@Entity +@Table(name = "announcement") +public class AnnouncementEntity { + + public AnnouncementEntity(String message, Duration duration) { + this.message = message; + this.duration = duration; + } + + @Id + @GeneratedValue + @Column + private UUID id; + + @Column(nullable = false) + private Instant creationDate = Instant.now(); + + @Column(nullable = false) + private String message; + + @Column + private Duration duration; + +} diff --git a/src/main/java/org/gridsuite/useradmin/server/repository/AnnouncementRepository.java b/src/main/java/org/gridsuite/useradmin/server/repository/AnnouncementRepository.java new file mode 100644 index 0000000..d13be24 --- /dev/null +++ b/src/main/java/org/gridsuite/useradmin/server/repository/AnnouncementRepository.java @@ -0,0 +1,20 @@ +/** + * Copyright (c) 2024, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +package org.gridsuite.useradmin.server.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.UUID; + +/** + * @author Florent MILLOT + */ +@Repository +public interface AnnouncementRepository extends JpaRepository { +} diff --git a/src/main/java/org/gridsuite/useradmin/server/service/AnnouncementService.java b/src/main/java/org/gridsuite/useradmin/server/service/AnnouncementService.java new file mode 100644 index 0000000..727a6e7 --- /dev/null +++ b/src/main/java/org/gridsuite/useradmin/server/service/AnnouncementService.java @@ -0,0 +1,61 @@ +/** + * Copyright (c) 2024, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +package org.gridsuite.useradmin.server.service; + +import org.gridsuite.useradmin.server.repository.AnnouncementEntity; +import org.gridsuite.useradmin.server.repository.AnnouncementRepository; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.server.ResponseStatusException; + +import java.util.List; +import java.util.Objects; +import java.util.UUID; + +/** + * @author Florent MILLOT + */ +@Service +public class AnnouncementService { + + private final AnnouncementRepository announcementRepository; + private final NotificationService notificationService; + + public AnnouncementService(final AnnouncementRepository announcementRepository, + final NotificationService notificationService) { + this.announcementRepository = Objects.requireNonNull(announcementRepository); + this.notificationService = Objects.requireNonNull(notificationService); + } + + @Transactional + public void sendAnnouncement(AnnouncementEntity announcement) { + this.announcementRepository.deleteAll(); // for now, only one message at a time + this.announcementRepository.save(announcement); + if (announcement.getDuration() == null) { + notificationService.emitMaintenanceMessage(announcement.getMessage()); + } else { + notificationService.emitMaintenanceMessage(announcement.getMessage(), announcement.getDuration().toSeconds()); + } + } + + @Transactional + public void cancelAnnouncement(UUID announcementId) { + if (!announcementRepository.existsById(announcementId)) { + throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Unable to find announcement"); + } + this.announcementRepository.deleteById(announcementId); + notificationService.emitCancelMaintenanceMessage(); + } + + @Transactional(readOnly = true) + public List getAllAnnouncements() { + return this.announcementRepository.findAll(); + } + +} diff --git a/src/main/java/org/gridsuite/useradmin/server/service/NotificationService.java b/src/main/java/org/gridsuite/useradmin/server/service/NotificationService.java index f414a43..0112f92 100644 --- a/src/main/java/org/gridsuite/useradmin/server/service/NotificationService.java +++ b/src/main/java/org/gridsuite/useradmin/server/service/NotificationService.java @@ -42,7 +42,7 @@ private void sendMessage(Message message) { updatePublisher.send("publishMessage-out-0", message); } - public void emitMaintenanceMessage(String message, int duration) { + public void emitMaintenanceMessage(String message, long duration) { sendMessage(MessageBuilder.withPayload(message) .setHeader(HEADER_MESSAGE_TYPE, MESSAGE_TYPE_MAINTENANCE) .setHeader(HEADER_DURATION, duration) diff --git a/src/main/java/org/gridsuite/useradmin/server/service/UserAdminService.java b/src/main/java/org/gridsuite/useradmin/server/service/UserAdminService.java index b4ca965..2b0cd67 100644 --- a/src/main/java/org/gridsuite/useradmin/server/service/UserAdminService.java +++ b/src/main/java/org/gridsuite/useradmin/server/service/UserAdminService.java @@ -12,6 +12,7 @@ import org.gridsuite.useradmin.server.dto.UserInfos; import org.gridsuite.useradmin.server.dto.UserProfile; import org.gridsuite.useradmin.server.entity.UserProfileEntity; +import org.gridsuite.useradmin.server.repository.AnnouncementEntity; import org.gridsuite.useradmin.server.repository.UserInfosRepository; import org.gridsuite.useradmin.server.entity.UserInfosEntity; import org.gridsuite.useradmin.server.repository.UserProfileRepository; @@ -20,12 +21,8 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.util.Collection; -import java.util.List; -import java.util.Objects; -import java.util.Optional; +import java.util.*; -import static org.gridsuite.useradmin.server.UserAdminException.Type.FORBIDDEN; import static org.gridsuite.useradmin.server.UserAdminException.Type.NOT_FOUND; /** @@ -39,6 +36,7 @@ public class UserAdminService { private final NotificationService notificationService; private final AdminRightService adminRightService; private final UserProfileService userProfileService; + private final AnnouncementService announcementService; private final UserAdminService self; private final UserAdminApplicationProps applicationProps; @@ -50,6 +48,7 @@ public UserAdminService(final UserInfosRepository userInfosRepository, final NotificationService notificationService, final UserProfileService userProfileService, final UserAdminApplicationProps applicationProps, + final AnnouncementService announcementService, @Lazy final UserAdminService userAdminService) { this.userInfosRepository = Objects.requireNonNull(userInfosRepository); this.userProfileRepository = Objects.requireNonNull(userProfileRepository); @@ -58,6 +57,7 @@ public UserAdminService(final UserInfosRepository userInfosRepository, this.notificationService = Objects.requireNonNull(notificationService); this.userProfileService = Objects.requireNonNull(userProfileService); this.applicationProps = Objects.requireNonNull(applicationProps); + this.announcementService = Objects.requireNonNull(announcementService); this.self = Objects.requireNonNull(userAdminService); } @@ -108,8 +108,8 @@ public void updateUser(String sub, String userId, UserInfos userInfos) { public boolean subExists(String sub) { final List admins = adminRightService.getAdmins(); final boolean isAllowed = admins.isEmpty() && userInfosRepository.count() == 0L - || admins.contains(sub) - || userInfosRepository.existsBySub(sub); + || admins.contains(sub) + || userInfosRepository.existsBySub(sub); connectionsService.recordConnectionAttempt(sub, isAllowed); return isAllowed; } @@ -146,21 +146,18 @@ public boolean userIsAdmin(@NonNull String userId) { return adminRightService.isAdmin(userId); } - public void sendMaintenanceMessage(String userId, Integer durationInSeconds, String message) { - if (!adminRightService.isAdmin(userId)) { - throw new UserAdminException(FORBIDDEN); - } - if (durationInSeconds == null) { - notificationService.emitMaintenanceMessage(message); - } else { - notificationService.emitMaintenanceMessage(message, durationInSeconds); - } + public void sendAnnouncement(AnnouncementEntity announcement, String userId) { + adminRightService.assertIsAdmin(userId); + announcementService.sendAnnouncement(announcement); } - public void sendCancelMaintenanceMessage(String userId) { - if (!adminRightService.isAdmin(userId)) { - throw new UserAdminException(FORBIDDEN); - } - notificationService.emitCancelMaintenanceMessage(); + public void cancelAnnouncement(UUID announcementId, String userId) { + adminRightService.assertIsAdmin(userId); + announcementService.cancelAnnouncement(announcementId); + } + + public List getAllAnnouncements(String userId) { + adminRightService.assertIsAdmin(userId); + return announcementService.getAllAnnouncements(); } } diff --git a/src/main/resources/db/changelog/changesets/changelog_20240329T134444Z.xml b/src/main/resources/db/changelog/changesets/changelog_20240329T134444Z.xml new file mode 100644 index 0000000..beabc65 --- /dev/null +++ b/src/main/resources/db/changelog/changesets/changelog_20240329T134444Z.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/db/changelog/db.changelog-master.yaml b/src/main/resources/db/changelog/db.changelog-master.yaml index 76bc26a..d8c050e 100644 --- a/src/main/resources/db/changelog/db.changelog-master.yaml +++ b/src/main/resources/db/changelog/db.changelog-master.yaml @@ -17,3 +17,7 @@ databaseChangeLog: - include: file: changesets/changelog_20240715T114052Z.xml relativeToChangelogFile: true + - include: + file: changesets/changelog_20240329T134444Z.xml + relativeToChangelogFile: true + diff --git a/src/test/java/org/gridsuite/useradmin/server/UserAdminTest.java b/src/test/java/org/gridsuite/useradmin/server/UserAdminTest.java index dcdf28d..8c5e09d 100644 --- a/src/test/java/org/gridsuite/useradmin/server/UserAdminTest.java +++ b/src/test/java/org/gridsuite/useradmin/server/UserAdminTest.java @@ -13,9 +13,7 @@ import org.gridsuite.useradmin.server.dto.UserInfos; import org.gridsuite.useradmin.server.dto.UserProfile; import org.gridsuite.useradmin.server.entity.ConnectionEntity; -import org.gridsuite.useradmin.server.repository.ConnectionRepository; -import org.gridsuite.useradmin.server.repository.UserInfosRepository; -import org.gridsuite.useradmin.server.repository.UserProfileRepository; +import org.gridsuite.useradmin.server.repository.*; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -30,10 +28,13 @@ import org.springframework.messaging.MessageHeaders; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import java.time.Duration; import java.time.LocalDateTime; import java.time.ZoneOffset; import java.util.List; +import java.util.UUID; import static org.gridsuite.useradmin.server.service.NotificationService.*; import static org.junit.jupiter.api.Assertions.*; @@ -60,6 +61,9 @@ class UserAdminTest { @Autowired private UserProfileRepository userProfileRepository; + @Autowired + private AnnouncementRepository announcementRepository; + @Autowired private ConnectionRepository connectionRepository; @@ -75,6 +79,7 @@ public void cleanDB() { userInfosRepository.deleteAll(); userProfileRepository.deleteAll(); connectionRepository.deleteAll(); + announcementRepository.deleteAll(); } private static final String USER_SUB = "user1"; @@ -295,49 +300,80 @@ void testGetConnections() throws Exception { @Test void testSendMaintenanceMessage() throws Exception { //Send a maintenance message and expect everything to be ok - String requestBody = objectMapper.writeValueAsString("The application will be on maintenance until the end of the maintenance"); - Integer duration = 300; - mockMvc.perform(post("/" + UserAdminApi.API_VERSION + "/messages/maintenance", USER_SUB) - .queryParam("durationInSeconds", duration.toString()) + String message = "The application will be on maintenance until the end of the maintenance"; + Duration duration = Duration.ofSeconds(300); + AnnouncementEntity announcement = new AnnouncementEntity(message, duration); + String requestBody = objectMapper.writeValueAsString(announcement); + mockMvc.perform(post("/" + UserAdminApi.API_VERSION + "/announcements") .header("userId", ADMIN_USER) .contentType(MediaType.APPLICATION_JSON) .content(requestBody)) .andExpect(status().isOk()); - assertMaintenanceMessageSent(requestBody, duration); + assertMaintenanceMessageSent(message, duration.toSeconds()); + assertEquals(1, announcementRepository.findAll().size()); //Send a maintenance message without duration and expect everything to be ok - mockMvc.perform(post("/" + UserAdminApi.API_VERSION + "/messages/maintenance") + announcement = new AnnouncementEntity(message, null); + requestBody = objectMapper.writeValueAsString(announcement); + mockMvc.perform(post("/" + UserAdminApi.API_VERSION + "/announcements") .header("userId", ADMIN_USER) .contentType(MediaType.APPLICATION_JSON) .content(requestBody)) .andExpect(status().isOk()); - assertMaintenanceMessageSent(requestBody, null); + assertMaintenanceMessageSent(message, null); + assertEquals(1, announcementRepository.findAll().size()); // only one at a time for now //Send a maintenance message with a user that's not an admin and expect 403 FORBIDDEN - mockMvc.perform(post("/" + UserAdminApi.API_VERSION + "/messages/maintenance") - .queryParam("durationInSeconds", String.valueOf(duration)) + mockMvc.perform(post("/" + UserAdminApi.API_VERSION + "/announcements") .header("userId", NOT_ADMIN) .contentType(MediaType.APPLICATION_JSON) .content(requestBody)) .andExpect(status().isForbidden()); - } @Test void testCancelMaintenanceMessage() throws Exception { + AnnouncementEntity announcement = announcementRepository.save(new AnnouncementEntity("I think dangling line is a funny name for a line", Duration.ofSeconds(60))); + assertEquals(1, announcementRepository.findAll().size()); + //Send a cancel maintenance message and expect everything to be ok - mockMvc.perform(post("/" + UserAdminApi.API_VERSION + "/messages/cancel-maintenance") + mockMvc.perform(delete("/" + UserAdminApi.API_VERSION + "/announcements/" + announcement.getId()) .header("userId", ADMIN_USER)) .andExpect(status().isOk()); assertCancelMaintenanceMessageSent(); + assertEquals(0, announcementRepository.findAll().size()); + + // With a non-existing ID, expect 404 + mockMvc.perform(delete("/" + UserAdminApi.API_VERSION + "/announcements/" + UUID.randomUUID()) + .header("userId", ADMIN_USER)) + .andExpect(status().isNotFound()); //Send a cancel maintenance message with a user that's not an admin and expect 403 FORBIDDEN - mockMvc.perform(post("/" + UserAdminApi.API_VERSION + "/messages/cancel-maintenance") + mockMvc.perform(delete("/" + UserAdminApi.API_VERSION + "/announcements/" + UUID.randomUUID()) + .header("userId", NOT_ADMIN)) + .andExpect(status().isForbidden()); + } + + @Test + void testGetAllMaintenanceMessages() throws Exception { + announcementRepository.save(new AnnouncementEntity("I think dangling line is a funny name for a line", Duration.ofSeconds(60))); + assertEquals(1, announcementRepository.findAll().size()); + + // Try to retrieve all the messages and expect everything to be ok + MvcResult mvcResult = mockMvc.perform(get("/" + UserAdminApi.API_VERSION + "/announcements") + .header("userId", ADMIN_USER)) + .andExpect(status().isOk()).andReturn(); + List announcements = objectMapper.readValue(mvcResult.getResponse().getContentAsString(), new TypeReference<>() { + }); + assertEquals(1, announcements.size()); + + // Try to retrieve all the messages with a user that's not an admin and expect 403 FORBIDDEN + mockMvc.perform(get("/" + UserAdminApi.API_VERSION + "/announcements") .header("userId", NOT_ADMIN)) .andExpect(status().isForbidden()); } - private void assertMaintenanceMessageSent(String maintenanceMessage, Integer duration) { + private void assertMaintenanceMessageSent(String maintenanceMessage, Long duration) { Message message = output.receive(TIMEOUT, maintenanceMessageDestination); assertEquals(maintenanceMessage, new String(message.getPayload())); MessageHeaders headers = message.getHeaders();