diff --git a/dbrepo-authentication-service/Dockerfile b/dbrepo-authentication-service/Dockerfile index d5aae517441875eae082a8f1a4fffd5fe33dcf43..a44ea5df157b59f694ff1040cb3595ebd394faa6 100644 --- a/dbrepo-authentication-service/Dockerfile +++ b/dbrepo-authentication-service/Dockerfile @@ -5,13 +5,14 @@ MAINTAINER Martin Weise <martin.weise@tuwien.ac.at> # Enable health and metrics support ENV KC_HEALTH_ENABLED=true ENV KC_METRICS_ENABLED=true -ENV KC_FEATURES=update-email # Configure a database vendor ENV KC_DB=mariadb WORKDIR /opt/keycloak +COPY ./server.keystore ./conf/server.keystore + RUN /opt/keycloak/bin/kc.sh build ###### SECOND STAGE ###### diff --git a/dbrepo-authentication-service/dbrepo-realm.json b/dbrepo-authentication-service/dbrepo-realm.json index e2fc0ac59053bfaf65b27253d44479db4b5aa4ef..040ad7b26115e80bacaf40a769c0f9876233f0ba 100644 --- a/dbrepo-authentication-service/dbrepo-realm.json +++ b/dbrepo-authentication-service/dbrepo-realm.json @@ -55,6 +55,17 @@ "clientRole" : false, "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", "attributes" : { } + }, { + "id" : "9bb4a8dc-28e0-4645-b62f-cc94425f0cb0", + "name" : "default-maintenance-handling", + "description" : "${default-maintenance-handling}", + "composite" : true, + "composites" : { + "realm" : [ "create-maintenance-message", "find-maintenance-message", "update-maintenance-message", "delete-maintenance-message", "list-maintenance-messages" ] + }, + "clientRole" : false, + "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", + "attributes" : { } }, { "id" : "5136d7a3-e3f0-4585-bacd-15cb8a56095c", "name" : "escalated-container-handling", @@ -198,6 +209,14 @@ "clientRole" : false, "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", "attributes" : { } + }, { + "id" : "f4116230-8642-4bb7-bbc8-db9c5c07b558", + "name" : "create-maintenance-message", + "description" : "${create-maintenance-message}", + "composite" : false, + "clientRole" : false, + "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", + "attributes" : { } }, { "id" : "973f0999-cc70-4b28-9f43-979c470bea8e", "name" : "default-data-steward-roles", @@ -249,6 +268,14 @@ "clientRole" : false, "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", "attributes" : { } + }, { + "id" : "272a79a7-e282-4261-8f7d-5d5d1364243a", + "name" : "update-maintenance-message", + "description" : "${update-maintenance-message}", + "composite" : false, + "clientRole" : false, + "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", + "attributes" : { } }, { "id" : "64c16bfb-2015-48ad-a23f-637ff24419cb", "name" : "default-query-handling", @@ -268,6 +295,14 @@ "clientRole" : false, "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", "attributes" : { } + }, { + "id" : "c047d521-cec3-4444-86c4-aef098489b7b", + "name" : "delete-maintenance-message", + "description" : "${delete-maintenance-message}", + "composite" : false, + "clientRole" : false, + "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", + "attributes" : { } }, { "id" : "e14ab76b-1c24-484d-ae2d-478b8457edea", "name" : "list-licenses", @@ -435,7 +470,7 @@ "description" : "${default-developer-roles}", "composite" : true, "composites" : { - "realm" : [ "escalated-query-handling", "default-table-handling", "escalated-database-handling", "default-container-handling", "default-query-handling", "default-user-handling", "default-database-handling", "escalated-container-handling", "escalated-table-handling", "default-identifier-handling" ] + "realm" : [ "escalated-query-handling", "default-table-handling", "escalated-database-handling", "default-container-handling", "default-query-handling", "default-user-handling", "default-database-handling", "default-maintenance-handling", "escalated-container-handling", "escalated-table-handling", "default-identifier-handling" ] }, "clientRole" : false, "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", @@ -504,6 +539,14 @@ "clientRole" : false, "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", "attributes" : { } + }, { + "id" : "d6e38368-b40f-423b-82e4-e8aa595237c9", + "name" : "find-maintenance-message", + "description" : "${find-maintenance-message}", + "composite" : false, + "clientRole" : false, + "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", + "attributes" : { } }, { "id" : "fd1cc463-3e67-49d9-81b8-2cd90c1daa9c", "name" : "check-database-access", @@ -539,6 +582,14 @@ "clientRole" : false, "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", "attributes" : { } + }, { + "id" : "09f7bdb0-296f-46c8-a3a3-8f9254fb17e4", + "name" : "list-maintenance-messages", + "description" : "${list-maintenance-messages}", + "composite" : false, + "clientRole" : false, + "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", + "attributes" : { } }, { "id" : "fe3bc45c-61c2-4ece-bcaf-d410dc7de501", "name" : "update-database-access", @@ -909,7 +960,7 @@ "otpPolicyLookAheadWindow" : 1, "otpPolicyPeriod" : 30, "otpPolicyCodeReusable" : false, - "otpSupportedApplications" : [ "totpAppFreeOTPName", "totpAppMicrosoftAuthenticatorName", "totpAppGoogleName" ], + "otpSupportedApplications" : [ "totpAppMicrosoftAuthenticatorName", "totpAppGoogleName", "totpAppFreeOTPName" ], "webAuthnPolicyRpEntityName" : "keycloak", "webAuthnPolicySignatureAlgorithms" : [ "ES256" ], "webAuthnPolicyRpId" : "", @@ -1932,7 +1983,7 @@ "subType" : "authenticated", "subComponents" : { }, "config" : { - "allowed-protocol-mapper-types" : [ "oidc-usermodel-attribute-mapper", "oidc-full-name-mapper", "oidc-usermodel-property-mapper", "oidc-sha256-pairwise-sub-mapper", "saml-user-property-mapper", "saml-role-list-mapper", "oidc-address-mapper", "saml-user-attribute-mapper" ] + "allowed-protocol-mapper-types" : [ "saml-user-attribute-mapper", "oidc-usermodel-attribute-mapper", "saml-user-property-mapper", "oidc-address-mapper", "oidc-usermodel-property-mapper", "saml-role-list-mapper", "oidc-full-name-mapper", "oidc-sha256-pairwise-sub-mapper" ] } }, { "id" : "3ab11d74-5e76-408a-b85a-26bf8950f979", @@ -1941,7 +1992,7 @@ "subType" : "anonymous", "subComponents" : { }, "config" : { - "allowed-protocol-mapper-types" : [ "saml-user-property-mapper", "oidc-usermodel-attribute-mapper", "oidc-sha256-pairwise-sub-mapper", "saml-role-list-mapper", "oidc-address-mapper", "oidc-usermodel-property-mapper", "saml-user-attribute-mapper", "oidc-full-name-mapper" ] + "allowed-protocol-mapper-types" : [ "oidc-usermodel-attribute-mapper", "oidc-usermodel-property-mapper", "saml-user-property-mapper", "oidc-full-name-mapper", "oidc-sha256-pairwise-sub-mapper", "oidc-address-mapper", "saml-user-attribute-mapper", "saml-role-list-mapper" ] } } ], "org.keycloak.keys.KeyProvider" : [ { @@ -1993,7 +2044,7 @@ "internationalizationEnabled" : false, "supportedLocales" : [ ], "authenticationFlows" : [ { - "id" : "7e7d6810-5b6c-4ec6-865c-5f0b62ec56d7", + "id" : "a50c392e-a870-484c-b7c4-7c85e886ee46", "alias" : "Account verification options", "description" : "Method with which to verity the existing account", "providerId" : "basic-flow", @@ -2015,7 +2066,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "6d972ab3-0618-4971-b44a-0fc0d11c7280", + "id" : "3f384243-bf93-4535-8811-b23526f12963", "alias" : "Authentication Options", "description" : "Authentication options.", "providerId" : "basic-flow", @@ -2044,7 +2095,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "821a14e0-ef26-4b07-b716-fa34393eda56", + "id" : "478c7d4f-86f2-42eb-91db-4bc03e20e416", "alias" : "Browser - Conditional OTP", "description" : "Flow to determine if the OTP is required for the authentication", "providerId" : "basic-flow", @@ -2066,7 +2117,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "e70eadbd-4c39-4cfd-86ac-e50acc753b1b", + "id" : "cca43646-a74e-4439-b029-3c0a11cd3693", "alias" : "Direct Grant - Conditional OTP", "description" : "Flow to determine if the OTP is required for the authentication", "providerId" : "basic-flow", @@ -2088,7 +2139,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "4e35af97-acf4-4ca8-bc81-0477c1adfb6d", + "id" : "815189fe-ef5f-4865-be03-1e4b5dd9eafe", "alias" : "First broker login - Conditional OTP", "description" : "Flow to determine if the OTP is required for the authentication", "providerId" : "basic-flow", @@ -2110,7 +2161,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "2e0bd063-274a-4aab-a5f0-038a0bca5b98", + "id" : "b72709d3-246d-4057-a82f-1a6e721bdd0c", "alias" : "Handle Existing Account", "description" : "Handle what to do if there is existing account with same email/username like authenticated identity provider", "providerId" : "basic-flow", @@ -2132,7 +2183,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "6a20fab2-44bb-4451-b29a-6fb7e14a52ce", + "id" : "191c4793-7fdd-449a-8218-70d047d882b7", "alias" : "Reset - Conditional OTP", "description" : "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.", "providerId" : "basic-flow", @@ -2154,7 +2205,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "159d7398-74a7-4f60-a3fd-eb2df46f5ce7", + "id" : "afd9a108-753a-4012-a65f-69d149d8313a", "alias" : "User creation or linking", "description" : "Flow for the existing/non-existing user alternatives", "providerId" : "basic-flow", @@ -2177,7 +2228,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "85a66c55-4665-4ba0-bec9-7254eb8e5895", + "id" : "95c1347f-8248-4506-8ac7-77da64331658", "alias" : "Verify Existing Account by Re-authentication", "description" : "Reauthentication of existing account", "providerId" : "basic-flow", @@ -2199,7 +2250,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "c002e6da-2397-4fae-8d48-1eec3719ca15", + "id" : "00aa962b-69e7-4032-99d6-33d2e15689bf", "alias" : "browser", "description" : "browser based authentication", "providerId" : "basic-flow", @@ -2235,7 +2286,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "a03631cf-2fea-4a12-a35c-8137023503bd", + "id" : "b7efe33a-b00a-4830-b362-739101e1a437", "alias" : "clients", "description" : "Base authentication for clients", "providerId" : "client-flow", @@ -2271,7 +2322,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "a89940e4-bf4d-4a04-8fdf-dcf775336b20", + "id" : "39a89fb2-74b0-44bb-877c-a2f75044f5e1", "alias" : "direct grant", "description" : "OpenID Connect Resource Owner Grant", "providerId" : "basic-flow", @@ -2300,7 +2351,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "2dc2582b-be6f-4d9a-b545-b2c0e79a3581", + "id" : "c0b9af60-9bb2-42e2-815d-c03144b4773c", "alias" : "docker auth", "description" : "Used by Docker clients to authenticate against the IDP", "providerId" : "basic-flow", @@ -2315,7 +2366,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "09e56692-226f-4384-85e0-e33463cdb226", + "id" : "50e548b3-60a1-4511-838a-894462d7437a", "alias" : "first broker login", "description" : "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account", "providerId" : "basic-flow", @@ -2338,7 +2389,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "1439c900-92e0-4230-a1a7-ae82c3b8ddc9", + "id" : "260eb8b4-8889-41e4-b3d4-ce8adf15474c", "alias" : "forms", "description" : "Username, password, otp and other auth forms.", "providerId" : "basic-flow", @@ -2360,7 +2411,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "4cc3bb1b-e85d-447e-b50e-1afbe107bafe", + "id" : "d7db5234-08e2-44ed-8708-5f4e906fe8a5", "alias" : "http challenge", "description" : "An authentication flow based on challenge-response HTTP Authentication Schemes", "providerId" : "basic-flow", @@ -2382,7 +2433,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "04c49d80-30e4-4a37-b1c7-4d18c1b6a7f1", + "id" : "73ada652-053b-42f3-be1f-af907cdd3151", "alias" : "registration", "description" : "registration flow", "providerId" : "basic-flow", @@ -2398,7 +2449,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "85abb75a-0774-4b2d-8a71-2a92b0cfb639", + "id" : "ef0d04cc-a30d-4cdc-8e62-26075e256f41", "alias" : "registration form", "description" : "registration form", "providerId" : "form-flow", @@ -2434,7 +2485,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "948f68c1-015b-4349-a56f-6ee177d558ce", + "id" : "c6db550e-4542-4474-8e3d-955fb50b1a05", "alias" : "reset credentials", "description" : "Reset credentials for a user if they forgot their password or something", "providerId" : "basic-flow", @@ -2470,7 +2521,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "6046d416-4a88-4af6-b440-9fbc87fba478", + "id" : "9ba81baf-f271-44df-b712-1a58af78c27c", "alias" : "saml ecp", "description" : "SAML ECP Profile Authentication Flow", "providerId" : "basic-flow", @@ -2486,13 +2537,13 @@ } ] } ], "authenticatorConfig" : [ { - "id" : "3c91aefc-127f-4722-8375-72e8434d6266", + "id" : "d70d482f-3f61-4514-918f-9634c6dc7200", "alias" : "create unique user config", "config" : { "require.password.update.after.registration" : "false" } }, { - "id" : "1041c583-4682-44a8-b61b-9712bd4987c4", + "id" : "b5e8a2b3-a407-42f4-bc5d-c82c2c77c8d1", "alias" : "review profile config", "config" : { "update.profile.on.first.login" : "missing" diff --git a/dbrepo-authentication-service/generate-keystore.sh b/dbrepo-authentication-service/generate-keystore.sh new file mode 100644 index 0000000000000000000000000000000000000000..8b68c44a1febcac8c308a8c443a4a41f5ea21d2f --- /dev/null +++ b/dbrepo-authentication-service/generate-keystore.sh @@ -0,0 +1,2 @@ +#!/bin/bash +keytool -genkey -alias server -keyalg RSA -keypass password -storepass password -keystore server.keystore \ No newline at end of file diff --git a/dbrepo-authentication-service/server.keystore b/dbrepo-authentication-service/server.keystore new file mode 100644 index 0000000000000000000000000000000000000000..9dcd5051210b5bd2f945a8325610684c8e0029a8 Binary files /dev/null and b/dbrepo-authentication-service/server.keystore differ diff --git a/dbrepo-container-service/pom.xml b/dbrepo-container-service/pom.xml index 286be07560852c79f76480948c12b3e0bd97b218..e780d3f864a797d6915f27cae7bfbb714b0ef321 100644 --- a/dbrepo-container-service/pom.xml +++ b/dbrepo-container-service/pom.xml @@ -188,6 +188,7 @@ <exclude>at/tuwien/exception/**/*</exclude> <exclude>at/tuwien/config/**/*</exclude> <exclude>at/tuwien/handlers/**/*</exclude> + <exclude>at/tuwien/auth/**/*</exclude> <exclude>**/DbrepoContainerManagingApplication.class</exclude> </excludes> </configuration> diff --git a/dbrepo-container-service/rest-service/src/main/java/at/tuwien/endpoints/ContainerEndpoint.java b/dbrepo-container-service/rest-service/src/main/java/at/tuwien/endpoints/ContainerEndpoint.java index 8f69d140c8386a5f0f6ca1df4b392c5610b7c98e..2619f4537a9dfdcc3d5561b50bff63a94eff5d96 100644 --- a/dbrepo-container-service/rest-service/src/main/java/at/tuwien/endpoints/ContainerEndpoint.java +++ b/dbrepo-container-service/rest-service/src/main/java/at/tuwien/endpoints/ContainerEndpoint.java @@ -26,6 +26,7 @@ import org.springframework.web.bind.annotation.*; import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; + import java.security.Principal; import java.util.List; import java.util.Optional; @@ -152,12 +153,18 @@ public class ContainerEndpoint { mediaType = "application/json", schema = @Schema(implementation = ApiErrorDto.class))}), }) - public ResponseEntity<ContainerDto> findById(@NotNull @PathVariable("id") Long containerId) throws DockerClientException, - ContainerNotFoundException, ContainerNotRunningException { + public ResponseEntity<ContainerDto> findById(@NotNull @PathVariable("id") Long containerId) + throws DockerClientException, ContainerNotFoundException { log.debug("endpoint find container, id={}", containerId); - final Container container = containerService.inspect(containerId); - final ContainerDto dto = containerMapper.containerToContainerDto(container); - dto.setState(ContainerStateDto.RUNNING); + ContainerDto dto; + try { + dto = containerService.inspect(containerId); + } catch (ContainerNotRunningException e) { + /* ignore */ + dto = containerMapper.containerToContainerDto(containerService.find(containerId)); + dto.setRunning(false); + dto.setState(ContainerStateDto.EXITED); + } log.trace("find container resulted in container {}", dto); return ResponseEntity.ok() .body(dto); diff --git a/dbrepo-container-service/rest-service/src/test/java/at/tuwien/endpoint/ContainerEndpointIntegrationTest.java b/dbrepo-container-service/rest-service/src/test/java/at/tuwien/endpoint/ContainerEndpointIntegrationTest.java index c49b7ea08ee1de453788526a3017c11816cdd277..328f77a6fc2ad0dbdc8a74336e908725e4b53e17 100644 --- a/dbrepo-container-service/rest-service/src/test/java/at/tuwien/endpoint/ContainerEndpointIntegrationTest.java +++ b/dbrepo-container-service/rest-service/src/test/java/at/tuwien/endpoint/ContainerEndpointIntegrationTest.java @@ -2,13 +2,19 @@ package at.tuwien.endpoint; import at.tuwien.BaseUnitTest; import at.tuwien.api.container.*; +import at.tuwien.config.DockerConfig; import at.tuwien.config.ReadyConfig; import at.tuwien.endpoints.ContainerEndpoint; import at.tuwien.entities.container.Container; import at.tuwien.exception.*; +import at.tuwien.repository.jpa.ContainerRepository; +import at.tuwien.repository.jpa.ImageRepository; +import at.tuwien.repository.jpa.RealmRepository; import at.tuwien.repository.jpa.UserRepository; import at.tuwien.service.impl.ContainerServiceImpl; import lombok.extern.log4j.Log4j2; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; @@ -26,7 +32,8 @@ import java.util.List; import java.util.Optional; import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.when; @Log4j2 @ExtendWith(SpringExtension.class) @@ -36,330 +43,90 @@ public class ContainerEndpointIntegrationTest extends BaseUnitTest { @MockBean private ReadyConfig readyConfig; - @MockBean - private ContainerServiceImpl containerService; - - @MockBean - private UserRepository userRepository; - @Autowired - private ContainerEndpoint containerEndpoint; - - @Test - @WithAnonymousUser - public void findById_anonymous_succeeds() throws DockerClientException, ContainerNotFoundException, - ContainerNotRunningException { - - /* test */ - findById_generic(CONTAINER_1_ID, CONTAINER_1); - } - - @Test - @WithMockUser(username = USER_1_USERNAME, authorities = {"find-container"}) - public void findById_hasRole_succeeds() throws DockerClientException, ContainerNotFoundException, - ContainerNotRunningException { - - /* mock */ - when(userRepository.findByUsername(USER_1_USERNAME)) - .thenReturn(Optional.of(USER_1)); - - /* test */ - findById_generic(CONTAINER_1_ID, CONTAINER_1); - } - - @Test - @WithMockUser(username = USER_4_USERNAME) - public void findById_noRole_succeeds() throws DockerClientException, ContainerNotFoundException, - ContainerNotRunningException { - - /* mock */ - when(userRepository.findByUsername(USER_4_USERNAME)) - .thenReturn(Optional.of(USER_4)); - - /* test */ - findById_generic(CONTAINER_1_ID, CONTAINER_1); - } - - @Test - @WithAnonymousUser - public void delete_anonymous_fails() { - - /* test */ - assertThrows(AccessDeniedException.class, () -> { - delete_generic(CONTAINER_1_ID, CONTAINER_1, null); - }); - } - - @Test - @WithMockUser(username = USER_3_USERNAME) - public void delete_noRole_fails() { - - /* mock */ - when(userRepository.findByUsername(USER_4_USERNAME)) - .thenReturn(Optional.of(USER_4)); + private RealmRepository realmRepository; - /* test */ - assertThrows(AccessDeniedException.class, () -> { - delete_generic(CONTAINER_1_ID, CONTAINER_1, USER_4_PRINCIPAL); - }); - } - - @Test - @WithMockUser(username = USER_2_USERNAME, authorities = {"delete-container"}) - public void delete_hasRole_succeeds() throws ContainerStillRunningException, ContainerAlreadyRemovedException, - ContainerNotFoundException, DockerClientException { - - /* mock */ - when(userRepository.findByUsername(USER_2_USERNAME)) - .thenReturn(Optional.of(USER_2)); - - /* test */ - delete_generic(CONTAINER_1_ID, CONTAINER_1, USER_2_PRINCIPAL); - } - - @Test - @WithAnonymousUser - public void findAll_anonymous_succeeds() { - - /* test */ - findAll_generic(null, null); - } - - @Test - @WithMockUser(username = USER_1_USERNAME, authorities = {"find-containers"}) - public void findAll_hasRole_succeeds() { - - /* mock */ - when(userRepository.findByUsername(USER_1_USERNAME)) - .thenReturn(Optional.of(USER_1)); - - /* test */ - findAll_generic(USER_1_PRINCIPAL, null); - } - - @Test - @WithMockUser(username = USER_4_USERNAME) - public void findAll_noRole_succeeds() { - - /* mock */ - when(userRepository.findByUsername(USER_4_USERNAME)) - .thenReturn(Optional.of(USER_4)); - - /* test */ - findAll_generic(USER_4_PRINCIPAL, null); - } - - @Test - @WithAnonymousUser - public void create_anonymous_fails() { - final ContainerCreateRequestDto request = ContainerCreateRequestDto.builder() - .name(CONTAINER_1_NAME) - .repository(IMAGE_1_REPOSITORY) - .tag(IMAGE_1_TAG) - .build(); + @Autowired + private ImageRepository imageRepository; - /* test */ - assertThrows(AccessDeniedException.class, () -> { - create_generic(request, null); - }); - } + @Autowired + private ContainerRepository containerRepository; - @Test - @WithMockUser(username = USER_1_USERNAME, authorities = {"create-container"}) - public void create_hasRole_succeeds() throws UserNotFoundException, DockerClientException, - ContainerAlreadyExistsException, ImageNotFoundException { - final ContainerCreateRequestDto request = ContainerCreateRequestDto.builder() - .name(CONTAINER_1_NAME) - .repository(IMAGE_1_REPOSITORY) - .tag(IMAGE_1_TAG) - .build(); + @Autowired + private UserRepository userRepository; - /* mock */ - when(userRepository.findByUsername(USER_1_USERNAME)) - .thenReturn(Optional.of(USER_1)); + @Autowired + private ContainerEndpoint containerEndpoint; - /* test */ - create_generic(request, USER_1_PRINCIPAL); + @BeforeEach + public void beforeEach() { + afterEach(); + /* networks */ + DockerConfig.createAllNetworks(); + /* metadata database */ + realmRepository.save(REALM_DBREPO); + userRepository.save(USER_1_SIMPLE); + userRepository.save(USER_2_SIMPLE); + userRepository.save(USER_3_SIMPLE); + imageRepository.save(IMAGE_1); } - @Test - @WithMockUser(username = USER_4_USERNAME) - public void create_noRole_fails() { - final ContainerCreateRequestDto request = ContainerCreateRequestDto.builder() - .name(CONTAINER_1_NAME) - .repository(IMAGE_1_REPOSITORY) - .tag(IMAGE_1_TAG) - .build(); - - /* mock */ - when(userRepository.findByUsername(USER_4_USERNAME)) - .thenReturn(Optional.of(USER_4)); - - /* test */ - assertThrows(AccessDeniedException.class, () -> { - create_generic(request, USER_4_PRINCIPAL); - }); + @AfterEach + public void afterEach() { + DockerConfig.removeAllContainers(); + DockerConfig.removeAllNetworks(); } @Test @WithAnonymousUser - public void modify_anonymous_fails() { - - /* test */ - assertThrows(AccessDeniedException.class, () -> { - modify_generic(ContainerActionTypeDto.START, CONTAINER_1_ID, CONTAINER_1, null); - }); - } - - @Test - @WithMockUser(username = USER_1_USERNAME, authorities = {"modify-container-state"}) - public void modify_hasRole_succeeds() throws ContainerAlreadyRunningException, - ContainerAlreadyStoppedException, ContainerNotFoundException, UserNotFoundException, NotAllowedException, - DockerClientException { - - /* mock */ - when(userRepository.findByUsername(USER_1_USERNAME)) - .thenReturn(Optional.of(USER_1)); - - /* test */ - modify_generic(ContainerActionTypeDto.START, CONTAINER_1_ID, CONTAINER_1, USER_1_PRINCIPAL); - } - - @Test - @WithMockUser(username = USER_4_USERNAME) - public void modify_noRole_fails() { + public void findAll_anonymousNoLimit_succeeds() throws InterruptedException { /* mock */ - when(userRepository.findByUsername(USER_4_USERNAME)) - .thenReturn(Optional.of(USER_4)); + DockerConfig.createContainer(null, CONTAINER_1_SIMPLE, CONTAINER_1_ENV); + DockerConfig.startContainer(CONTAINER_1_SIMPLE); + containerRepository.save(CONTAINER_1_SIMPLE); /* test */ - assertThrows(AccessDeniedException.class, () -> { - modify_generic(ContainerActionTypeDto.START, CONTAINER_1_ID, CONTAINER_1, USER_4_PRINCIPAL); - }); - } - - @Test - @WithMockUser(username = USER_2_USERNAME, authorities = {"modify-foreign-container-state"}) - public void modify_hasRoleForeign_succeeds() throws UserNotFoundException, ContainerAlreadyRunningException, - NotAllowedException, ContainerAlreadyStoppedException, ContainerNotFoundException, DockerClientException { - - /* mock */ - when(userRepository.findByUsername(USER_2_USERNAME)) - .thenReturn(Optional.of(USER_2)); - - /* test */ - modify_generic(ContainerActionTypeDto.START, CONTAINER_1_ID, CONTAINER_1, USER_2_PRINCIPAL); - } - - @Test - @WithMockUser(username = USER_4_USERNAME) - public void modify_noRoleForeign_fails() { - - /* mock */ - when(userRepository.findByUsername(USER_4_USERNAME)) - .thenReturn(Optional.of(USER_4)); - - /* test */ - assertThrows(AccessDeniedException.class, () -> { - modify_generic(ContainerActionTypeDto.STOP, CONTAINER_1_ID, CONTAINER_1, USER_4_PRINCIPAL); - }); - } - - /* ################################################################################################### */ - /* ## GENERIC TEST CASES ## */ - /* ################################################################################################### */ - - public void findById_generic(Long containerId, Container container) throws DockerClientException, - ContainerNotFoundException, ContainerNotRunningException { - - /* mock */ - when(containerService.find(containerId)) - .thenReturn(container); - when(containerService.inspect(containerId)) - .thenReturn(container); - - /* test */ - final ResponseEntity<ContainerDto> response = containerEndpoint.findById(containerId); - assertEquals(HttpStatus.OK, response.getStatusCode()); - assertNotNull(response.getBody()); - final ContainerDto dto = response.getBody(); - assertEquals(ContainerStateDto.RUNNING, dto.getState()); - } - - public void delete_generic(Long containerId, Container container, Principal principal) throws ContainerNotFoundException, - ContainerStillRunningException, ContainerAlreadyRemovedException, DockerClientException { - - /* mock */ - when(containerService.find(containerId)) - .thenReturn(container); - doNothing() - .when(containerService) - .remove(CONTAINER_1_ID); - - /* test */ - final ResponseEntity<?> response = containerEndpoint.delete(containerId, principal); - assertEquals(HttpStatus.ACCEPTED, response.getStatusCode()); - assertNull(response.getBody()); - } - - public void findAll_generic(Principal principal, Integer limit) { - - /* mock */ - when(containerService.getAll(limit)) - .thenReturn(List.of(CONTAINER_1, CONTAINER_2)); - - /* test */ - final ResponseEntity<List<ContainerBriefDto>> response = containerEndpoint.findAll(principal, limit); + final ResponseEntity<List<ContainerBriefDto>> response = containerEndpoint.findAll(null, null); assertEquals(HttpStatus.OK, response.getStatusCode()); assertNotNull(response.getBody()); final List<ContainerBriefDto> body = response.getBody(); - assertEquals(2, body.size()); - final ContainerBriefDto container1 = body.get(0); - assertEquals(CONTAINER_1_ID, container1.getId()); - assertEquals(CONTAINER_1_NAME, container1.getName()); - assertEquals(CONTAINER_1_INTERNALNAME, container1.getInternalName()); - final ContainerBriefDto container2 = body.get(1); - assertEquals(CONTAINER_2_ID, container2.getId()); - assertEquals(CONTAINER_2_NAME, container2.getName()); - assertEquals(CONTAINER_2_INTERNALNAME, container2.getInternalName()); + assertEquals(1, body.size()); + final ContainerBriefDto container0 = body.get(0); + assertTrue(container0.getRunning()); } - public void create_generic(ContainerCreateRequestDto data, Principal principal) throws UserNotFoundException, - DockerClientException, ContainerAlreadyExistsException, ImageNotFoundException { + @Test + @WithAnonymousUser + public void findById_anonymousNotRunning_succeeds() throws DockerClientException, ContainerNotFoundException { /* mock */ - when(containerService.create(data, principal)) - .thenReturn(CONTAINER_1); + DockerConfig.createContainer(null, CONTAINER_1_SIMPLE, CONTAINER_1_ENV); + containerRepository.save(CONTAINER_1_SIMPLE); /* test */ - final ResponseEntity<ContainerBriefDto> response = containerEndpoint.create(data, principal); - assertEquals(HttpStatus.CREATED, response.getStatusCode()); + final ResponseEntity<ContainerDto> response = containerEndpoint.findById(CONTAINER_1_ID); + assertEquals(HttpStatus.OK, response.getStatusCode()); assertNotNull(response.getBody()); + final ContainerDto body = response.getBody(); + assertFalse(body.getRunning()); + assertEquals(ContainerStateDto.EXITED, body.getState()); } - public void modify_generic(ContainerActionTypeDto data, Long containerId, Container container, Principal principal) - throws ContainerAlreadyRunningException, ContainerNotFoundException, ContainerAlreadyStoppedException, - UserNotFoundException, NotAllowedException, DockerClientException { + @Test + @WithMockUser(username = USER_3_USERNAME, authorities = {"modify-container-state"}) + public void modify_foreign_fails() { final ContainerChangeDto request = ContainerChangeDto.builder() - .action(data) + .action(ContainerActionTypeDto.STOP) .build(); /* mock */ - when(containerService.find(containerId)) - .thenReturn(container); - if (data.equals(ContainerActionTypeDto.START)) { - when(containerService.start(containerId)) - .thenReturn(container); - } else if (data.equals(ContainerActionTypeDto.STOP)) { - when(containerService.stop(containerId)) - .thenReturn(container); - } + containerRepository.save(CONTAINER_1_SIMPLE); /* test */ - final ResponseEntity<ContainerBriefDto> response = containerEndpoint.modify(containerId, request, principal); - assertEquals(HttpStatus.ACCEPTED, response.getStatusCode()); - assertNotNull(response.getBody()); + assertThrows(NotAllowedException.class, () -> { + containerEndpoint.modify(CONTAINER_1_ID, request, USER_3_PRINCIPAL); + }); } } diff --git a/dbrepo-container-service/rest-service/src/test/java/at/tuwien/endpoint/ContainerEndpointUnitTest.java b/dbrepo-container-service/rest-service/src/test/java/at/tuwien/endpoint/ContainerEndpointUnitTest.java new file mode 100644 index 0000000000000000000000000000000000000000..921fc436a34e0b74e6199a76419471a6f7233d3f --- /dev/null +++ b/dbrepo-container-service/rest-service/src/test/java/at/tuwien/endpoint/ContainerEndpointUnitTest.java @@ -0,0 +1,364 @@ +package at.tuwien.endpoint; + +import at.tuwien.BaseUnitTest; +import at.tuwien.api.container.*; +import at.tuwien.config.ReadyConfig; +import at.tuwien.endpoints.ContainerEndpoint; +import at.tuwien.entities.container.Container; +import at.tuwien.exception.*; +import at.tuwien.repository.jpa.UserRepository; +import at.tuwien.service.impl.ContainerServiceImpl; +import lombok.extern.log4j.Log4j2; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.test.context.support.WithAnonymousUser; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import java.security.Principal; +import java.util.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +@Log4j2 +@ExtendWith(SpringExtension.class) +@SpringBootTest +public class ContainerEndpointUnitTest extends BaseUnitTest { + + @MockBean + private ReadyConfig readyConfig; + + @MockBean + private ContainerServiceImpl containerService; + + @MockBean + private UserRepository userRepository; + + @Autowired + private ContainerEndpoint containerEndpoint; + + @Test + @WithAnonymousUser + public void findById_anonymous_succeeds() throws DockerClientException, ContainerNotFoundException, + ContainerNotRunningException { + + /* test */ + findById_generic(CONTAINER_1_ID, CONTAINER_1_HASH, CONTAINER_1, CONTAINER_1_DTO); + } + + @Test + @WithMockUser(username = USER_1_USERNAME, authorities = {"find-container"}) + public void findById_hasRole_succeeds() throws DockerClientException, ContainerNotFoundException, + ContainerNotRunningException { + + /* mock */ + when(userRepository.findByUsername(USER_1_USERNAME)) + .thenReturn(Optional.of(USER_1)); + + /* test */ + findById_generic(CONTAINER_1_ID, CONTAINER_1_HASH, CONTAINER_1, CONTAINER_1_DTO); + } + + @Test + @WithMockUser(username = USER_4_USERNAME) + public void findById_noRole_succeeds() throws DockerClientException, ContainerNotFoundException, + ContainerNotRunningException { + + /* mock */ + when(userRepository.findByUsername(USER_4_USERNAME)) + .thenReturn(Optional.of(USER_4)); + + /* test */ + findById_generic(CONTAINER_1_ID, CONTAINER_1_HASH, CONTAINER_1, CONTAINER_1_DTO); + } + + @Test + @WithAnonymousUser + public void delete_anonymous_fails() { + + /* test */ + assertThrows(AccessDeniedException.class, () -> { + delete_generic(CONTAINER_1_ID, CONTAINER_1, null); + }); + } + + @Test + @WithMockUser(username = USER_3_USERNAME) + public void delete_noRole_fails() { + + /* mock */ + when(userRepository.findByUsername(USER_4_USERNAME)) + .thenReturn(Optional.of(USER_4)); + + /* test */ + assertThrows(AccessDeniedException.class, () -> { + delete_generic(CONTAINER_1_ID, CONTAINER_1, USER_4_PRINCIPAL); + }); + } + + @Test + @WithMockUser(username = USER_2_USERNAME, authorities = {"delete-container"}) + public void delete_hasRole_succeeds() throws ContainerStillRunningException, ContainerAlreadyRemovedException, + ContainerNotFoundException { + + /* mock */ + when(userRepository.findByUsername(USER_2_USERNAME)) + .thenReturn(Optional.of(USER_2)); + + /* test */ + delete_generic(CONTAINER_1_ID, CONTAINER_1, USER_2_PRINCIPAL); + } + + @Test + @WithAnonymousUser + public void findAll_anonymous_succeeds() { + + /* test */ + findAll_generic(null, null); + } + + @Test + @WithMockUser(username = USER_1_USERNAME, authorities = {"find-containers"}) + public void findAll_hasRole_succeeds() { + + /* mock */ + when(userRepository.findByUsername(USER_1_USERNAME)) + .thenReturn(Optional.of(USER_1)); + + /* test */ + findAll_generic(USER_1_PRINCIPAL, null); + } + + @Test + @WithMockUser(username = USER_4_USERNAME) + public void findAll_noRole_succeeds() { + + /* mock */ + when(userRepository.findByUsername(USER_4_USERNAME)) + .thenReturn(Optional.of(USER_4)); + + /* test */ + findAll_generic(USER_4_PRINCIPAL, null); + } + + @Test + @WithAnonymousUser + public void create_anonymous_fails() { + final ContainerCreateRequestDto request = ContainerCreateRequestDto.builder() + .name(CONTAINER_1_NAME) + .repository(IMAGE_1_REPOSITORY) + .tag(IMAGE_1_TAG) + .build(); + + /* test */ + assertThrows(AccessDeniedException.class, () -> { + create_generic(request, null); + }); + } + + @Test + @WithMockUser(username = USER_1_USERNAME, authorities = {"create-container"}) + public void create_hasRole_succeeds() throws UserNotFoundException, DockerClientException, + ContainerAlreadyExistsException, ImageNotFoundException { + final ContainerCreateRequestDto request = ContainerCreateRequestDto.builder() + .name(CONTAINER_1_NAME) + .repository(IMAGE_1_REPOSITORY) + .tag(IMAGE_1_TAG) + .build(); + + /* mock */ + when(userRepository.findByUsername(USER_1_USERNAME)) + .thenReturn(Optional.of(USER_1)); + + /* test */ + create_generic(request, USER_1_PRINCIPAL); + } + + @Test + @WithMockUser(username = USER_4_USERNAME) + public void create_noRole_fails() { + final ContainerCreateRequestDto request = ContainerCreateRequestDto.builder() + .name(CONTAINER_1_NAME) + .repository(IMAGE_1_REPOSITORY) + .tag(IMAGE_1_TAG) + .build(); + + /* mock */ + when(userRepository.findByUsername(USER_4_USERNAME)) + .thenReturn(Optional.of(USER_4)); + + /* test */ + assertThrows(AccessDeniedException.class, () -> { + create_generic(request, USER_4_PRINCIPAL); + }); + } + + @Test + @WithAnonymousUser + public void modify_anonymous_fails() { + + /* test */ + assertThrows(AccessDeniedException.class, () -> { + modify_generic(ContainerActionTypeDto.START, CONTAINER_1_ID, CONTAINER_1, null); + }); + } + + @Test + @WithMockUser(username = USER_1_USERNAME, authorities = {"modify-container-state"}) + public void modify_hasRole_succeeds() throws ContainerAlreadyRunningException, + ContainerAlreadyStoppedException, ContainerNotFoundException, UserNotFoundException, NotAllowedException { + + /* mock */ + when(userRepository.findByUsername(USER_1_USERNAME)) + .thenReturn(Optional.of(USER_1)); + + /* test */ + modify_generic(ContainerActionTypeDto.START, CONTAINER_1_ID, CONTAINER_1, USER_1_PRINCIPAL); + } + + @Test + @WithMockUser(username = USER_4_USERNAME) + public void modify_noRole_fails() { + + /* mock */ + when(userRepository.findByUsername(USER_4_USERNAME)) + .thenReturn(Optional.of(USER_4)); + + /* test */ + assertThrows(AccessDeniedException.class, () -> { + modify_generic(ContainerActionTypeDto.START, CONTAINER_1_ID, CONTAINER_1, USER_4_PRINCIPAL); + }); + } + + @Test + @WithMockUser(username = USER_2_USERNAME, authorities = {"modify-foreign-container-state"}) + public void modify_hasRoleForeign_succeeds() throws UserNotFoundException, ContainerAlreadyRunningException, + NotAllowedException, ContainerAlreadyStoppedException, ContainerNotFoundException { + + /* mock */ + when(userRepository.findByUsername(USER_2_USERNAME)) + .thenReturn(Optional.of(USER_2)); + + /* test */ + modify_generic(ContainerActionTypeDto.START, CONTAINER_1_ID, CONTAINER_1, USER_2_PRINCIPAL); + } + + @Test + @WithMockUser(username = USER_4_USERNAME) + public void modify_noRoleForeign_fails() { + + /* mock */ + when(userRepository.findByUsername(USER_4_USERNAME)) + .thenReturn(Optional.of(USER_4)); + + /* test */ + assertThrows(AccessDeniedException.class, () -> { + modify_generic(ContainerActionTypeDto.STOP, CONTAINER_1_ID, CONTAINER_1, USER_4_PRINCIPAL); + }); + } + + /* ################################################################################################### */ + /* ## GENERIC TEST CASES ## */ + /* ################################################################################################### */ + + public void findById_generic(Long containerId, String containerHash, Container container, ContainerDto containerDto) + throws DockerClientException, ContainerNotFoundException, ContainerNotRunningException { + + /* mock */ + when(containerService.find(containerId)) + .thenReturn(container); + when(containerService.inspect(containerId)) + .thenReturn(containerDto); + + /* test */ + final ResponseEntity<ContainerDto> response = containerEndpoint.findById(containerId); + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertNotNull(response.getBody()); + final ContainerDto dto = response.getBody(); + assertEquals(ContainerStateDto.RUNNING, dto.getState()); + } + + public void delete_generic(Long containerId, Container container, Principal principal) throws ContainerNotFoundException, + ContainerStillRunningException, ContainerAlreadyRemovedException { + + /* mock */ + when(containerService.find(containerId)) + .thenReturn(container); + doNothing() + .when(containerService) + .remove(CONTAINER_1_ID); + + /* test */ + final ResponseEntity<?> response = containerEndpoint.delete(containerId, principal); + assertEquals(HttpStatus.ACCEPTED, response.getStatusCode()); + assertNull(response.getBody()); + } + + public void findAll_generic(Principal principal, Integer limit) { + + /* mock */ + when(containerService.getAll(limit)) + .thenReturn(List.of(CONTAINER_1, CONTAINER_2)); + + /* test */ + final ResponseEntity<List<ContainerBriefDto>> response = containerEndpoint.findAll(principal, limit); + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertNotNull(response.getBody()); + final List<ContainerBriefDto> body = response.getBody(); + assertEquals(2, body.size()); + final ContainerBriefDto container1 = body.get(0); + assertEquals(CONTAINER_1_ID, container1.getId()); + assertEquals(CONTAINER_1_NAME, container1.getName()); + assertEquals(CONTAINER_1_INTERNALNAME, container1.getInternalName()); + final ContainerBriefDto container2 = body.get(1); + assertEquals(CONTAINER_2_ID, container2.getId()); + assertEquals(CONTAINER_2_NAME, container2.getName()); + assertEquals(CONTAINER_2_INTERNALNAME, container2.getInternalName()); + } + + public void create_generic(ContainerCreateRequestDto data, Principal principal) throws UserNotFoundException, + DockerClientException, ContainerAlreadyExistsException, ImageNotFoundException { + + /* mock */ + when(containerService.create(data, principal)) + .thenReturn(CONTAINER_1); + + /* test */ + final ResponseEntity<ContainerBriefDto> response = containerEndpoint.create(data, principal); + assertEquals(HttpStatus.CREATED, response.getStatusCode()); + assertNotNull(response.getBody()); + } + + public void modify_generic(ContainerActionTypeDto data, Long containerId, Container container, Principal principal) + throws ContainerAlreadyRunningException, ContainerNotFoundException, ContainerAlreadyStoppedException, + UserNotFoundException, NotAllowedException { + final ContainerChangeDto request = ContainerChangeDto.builder() + .action(data) + .build(); + + /* mock */ + when(containerService.find(containerId)) + .thenReturn(container); + if (data.equals(ContainerActionTypeDto.START)) { + when(containerService.start(containerId)) + .thenReturn(container); + } else if (data.equals(ContainerActionTypeDto.STOP)) { + when(containerService.stop(containerId)) + .thenReturn(container); + } + + /* test */ + final ResponseEntity<ContainerBriefDto> response = containerEndpoint.modify(containerId, request, principal); + assertEquals(HttpStatus.ACCEPTED, response.getStatusCode()); + assertNotNull(response.getBody()); + } + +} diff --git a/dbrepo-container-service/rest-service/src/test/java/at/tuwien/endpoint/ImageEndpointIntegrationTest.java b/dbrepo-container-service/rest-service/src/test/java/at/tuwien/endpoint/ImageEndpointIntegrationTest.java new file mode 100644 index 0000000000000000000000000000000000000000..689ac6ea555c3d00418c055ad72307b51653a552 --- /dev/null +++ b/dbrepo-container-service/rest-service/src/test/java/at/tuwien/endpoint/ImageEndpointIntegrationTest.java @@ -0,0 +1,83 @@ +package at.tuwien.endpoint; + +import at.tuwien.BaseUnitTest; +import at.tuwien.api.container.image.ImageBriefDto; +import at.tuwien.api.container.image.ImageChangeDto; +import at.tuwien.api.container.image.ImageCreateDto; +import at.tuwien.api.container.image.ImageDto; +import at.tuwien.config.DockerConfig; +import at.tuwien.config.DockerDaemonConfig; +import at.tuwien.config.ReadyConfig; +import at.tuwien.endpoints.ImageEndpoint; +import at.tuwien.entities.container.image.ContainerImage; +import at.tuwien.exception.*; +import at.tuwien.repository.jpa.ImageRepository; +import at.tuwien.repository.jpa.RealmRepository; +import at.tuwien.repository.jpa.UserRepository; +import lombok.extern.log4j.Log4j2; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.test.context.support.WithAnonymousUser; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import java.security.Principal; +import java.util.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.when; + +@Log4j2 +@ExtendWith(SpringExtension.class) +@SpringBootTest +public class ImageEndpointIntegrationTest extends BaseUnitTest { + + @MockBean + private ReadyConfig readyConfig; + + @MockBean + private RealmRepository realmRepository; + + @MockBean + private UserRepository userRepository; + + @Autowired + private ImageEndpoint imageEndpoint; + + @BeforeEach + public void beforeEach() { + afterEach(); + /* networks */ + DockerConfig.createAllNetworks(); + /* metadata database */ + realmRepository.save(REALM_DBREPO); + userRepository.save(USER_2_SIMPLE); + } + + @AfterEach + public void afterEach() { + DockerConfig.removeAllContainers(); + DockerConfig.removeAllNetworks(); + } + + @Test + @WithMockUser(username = USER_2_USERNAME, authorities = {"create-image"}) + public void create_succeeds() throws UserNotFoundException, ImageAlreadyExistsException, DockerClientException, + ImageNotFoundException, ImageInvalidException { + + + /* test */ + imageEndpoint.create(IMAGE_1_CREATE_DTO, USER_2_PRINCIPAL); + } + +} diff --git a/dbrepo-container-service/rest-service/src/test/java/at/tuwien/service/ContainerServiceIntegrationTest.java b/dbrepo-container-service/rest-service/src/test/java/at/tuwien/service/ContainerServiceIntegrationTest.java index 88e2943220db0ad616e7534e2658e4c22744756b..35765c567f3a2d55a32a16df1f3b414f030a7978 100644 --- a/dbrepo-container-service/rest-service/src/test/java/at/tuwien/service/ContainerServiceIntegrationTest.java +++ b/dbrepo-container-service/rest-service/src/test/java/at/tuwien/service/ContainerServiceIntegrationTest.java @@ -2,6 +2,7 @@ package at.tuwien.service; import at.tuwien.BaseUnitTest; import at.tuwien.api.container.ContainerCreateRequestDto; +import at.tuwien.api.container.ContainerDto; import at.tuwien.config.DockerConfig; import at.tuwien.config.ReadyConfig; import at.tuwien.entities.container.Container; @@ -318,7 +319,7 @@ public class ContainerServiceIntegrationTest extends BaseUnitTest { containerRepository.save(CONTAINER_1_SIMPLE); /* test */ - final Container response = containerService.inspect(CONTAINER_1_ID); + final ContainerDto response = containerService.inspect(CONTAINER_1_ID); assertEquals(CONTAINER_1_ID, response.getId()); assertEquals(CONTAINER_1_NAME, response.getName()); assertEquals(CONTAINER_1_INTERNALNAME, response.getInternalName()); @@ -346,4 +347,19 @@ public class ContainerServiceIntegrationTest extends BaseUnitTest { containerService.inspect(CONTAINER_1_ID); }); } + + @Test + public void list_notRunning_succeeds() throws InterruptedException { + + /* mock */ + DockerConfig.createContainer(null, CONTAINER_1_SIMPLE, CONTAINER_1_ENV); + DockerConfig.createContainer(null, CONTAINER_2_SIMPLE, CONTAINER_2_ENV); + DockerConfig.startContainer(CONTAINER_2_SIMPLE); + containerRepository.save(CONTAINER_1_SIMPLE); + containerRepository.save(CONTAINER_2_SIMPLE); + + /* test */ + final List<com.github.dockerjava.api.model.Container> response = containerService.list(); + assertEquals(2, response.size()); + } } diff --git a/dbrepo-container-service/rest-service/src/test/java/at/tuwien/service/UserServiceIntegrationTest.java b/dbrepo-container-service/rest-service/src/test/java/at/tuwien/service/UserServiceIntegrationTest.java new file mode 100644 index 0000000000000000000000000000000000000000..a1a06e35c2e8fec2b8de56c2f56c10d7de307308 --- /dev/null +++ b/dbrepo-container-service/rest-service/src/test/java/at/tuwien/service/UserServiceIntegrationTest.java @@ -0,0 +1,64 @@ +package at.tuwien.service; + +import at.tuwien.BaseUnitTest; +import at.tuwien.config.ReadyConfig; +import at.tuwien.entities.user.User; +import at.tuwien.exception.UserNotFoundException; +import at.tuwien.repository.jpa.RealmRepository; +import at.tuwien.repository.jpa.UserRepository; +import lombok.extern.log4j.Log4j2; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +@Log4j2 +@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD) +@SpringBootTest +@ExtendWith(SpringExtension.class) +public class UserServiceIntegrationTest extends BaseUnitTest { + + @MockBean + private ReadyConfig readyConfig; + + @Autowired + private UserRepository userRepository; + + @Autowired + private RealmRepository realmRepository; + + @Autowired + private UserService userService; + + @BeforeEach + public void beforeEach() { + realmRepository.save(REALM_DBREPO); + userRepository.save(USER_1_SIMPLE); + } + + @Test + public void findByUsername_succeeds() throws UserNotFoundException { + + /* test */ + final User response = userService.findByUsername(USER_1_USERNAME); + assertEquals(USER_1_ID, response.getId()); + assertEquals(USER_1_USERNAME, response.getUsername()); + } + + @Test + public void findByUsername_fails() { + + /* test */ + assertThrows(UserNotFoundException.class, () -> { + userService.findByUsername(USER_2_USERNAME); + }); + } + +} diff --git a/dbrepo-container-service/services/src/main/java/at/tuwien/service/ContainerService.java b/dbrepo-container-service/services/src/main/java/at/tuwien/service/ContainerService.java index badd04c8db0c8a4a6370a5312c0aa6dbde546a57..9f1cee243b03fa059059836691f52b0332702d48 100644 --- a/dbrepo-container-service/services/src/main/java/at/tuwien/service/ContainerService.java +++ b/dbrepo-container-service/services/src/main/java/at/tuwien/service/ContainerService.java @@ -1,8 +1,10 @@ package at.tuwien.service; import at.tuwien.api.container.ContainerCreateRequestDto; +import at.tuwien.api.container.ContainerDto; import at.tuwien.entities.container.Container; import at.tuwien.exception.*; +import org.springframework.transaction.annotation.Transactional; import java.security.Principal; import java.util.List; @@ -10,49 +12,58 @@ import java.util.List; public interface ContainerService { /** - * @param createDto - * @param principal - * @return - * @throws ImageNotFoundException - * @throws DockerClientException - * @throws ContainerAlreadyExistsException - * @throws UserNotFoundException + * Creates a container. + * + * @param createDto The container metadata. + * @param principal The principal of the creating user. + * @return The container object, if successful. + * @throws ImageNotFoundException The image of the container was not found in the metadata database. + * @throws DockerClientException The docker client was unable to perform this action. + * @throws ContainerAlreadyExistsException A container with this name already exists. + * @throws UserNotFoundException The user creating the container was not found in the metadata database. */ Container create(ContainerCreateRequestDto createDto, Principal principal) throws ImageNotFoundException, DockerClientException, ContainerAlreadyExistsException, UserNotFoundException; /** - * @param containerId - * @return - * @throws ContainerNotFoundException - * @throws DockerClientException + * Stops a container by given id from the metadata database. + * + * @param containerId The container id. + * @return The container object, if successful. + * @throws ContainerNotFoundException The container was not found in the metadata database. + * @throws DockerClientException The docker client was unable to perform this action. */ Container stop(Long containerId) throws ContainerNotFoundException, DockerClientException, ContainerAlreadyStoppedException; /** - * @param containerId - * @throws ContainerNotFoundException - * @throws DockerClientException - * @throws ContainerStillRunningException + * Removes a stopped container by given id from the metadata database. + * + * @param containerId The container id. + * @throws ContainerNotFoundException The container was not found in the metadata database. + * @throws DockerClientException The docker client was unable to perform this action. + * @throws ContainerStillRunningException The container is still running and this action cannot be performed. */ void remove(Long containerId) throws ContainerNotFoundException, DockerClientException, ContainerStillRunningException, ContainerAlreadyRemovedException; /** - * @param id - * @return - * @throws ContainerNotFoundException + * Finds a container with a specific id from the metadata database. + * + * @param id The container id. + * @return The container object, if successful. + * @throws ContainerNotFoundException The container was not found in the metadata database. */ Container find(Long id) throws ContainerNotFoundException; /** - * @param id - * @return - * @throws ContainerNotFoundException - * @throws DockerClientException - * @throws ContainerNotRunningException + * Inspects a container state and resources by given id. + * + * @param id The container id. + * @return The container object. + * @throws DockerClientException The docker client was unable to perform this action. + * @throws ContainerNotRunningException The docker container is not running. */ - Container inspect(Long id) throws ContainerNotFoundException, DockerClientException, ContainerNotRunningException; + ContainerDto inspect(Long id) throws DockerClientException, ContainerNotRunningException, ContainerNotFoundException; /** * Retrieve a list of all containers from the metadata database @@ -62,13 +73,20 @@ public interface ContainerService { */ List<Container> getAll(Integer limit); + /** + * Find all containers on the server. + * + * @return List of containers. + */ List<com.github.dockerjava.api.model.Container> list(); /** - * @param containerId - * @return - * @throws ContainerNotFoundException - * @throws DockerClientException + * Starts a container with given id from the metadata database. + * + * @param containerId The container id. + * @return The container object, if successful. + * @throws ContainerNotFoundException The container was not found in the metadata database. + * @throws DockerClientException The docker client was unable to perform this action. */ Container start(Long containerId) throws ContainerNotFoundException, DockerClientException, ContainerAlreadyRunningException; } diff --git a/dbrepo-container-service/services/src/main/java/at/tuwien/service/impl/ContainerServiceImpl.java b/dbrepo-container-service/services/src/main/java/at/tuwien/service/impl/ContainerServiceImpl.java index 8c18fc898711887755d17b4661182df37f2f6cc1..e25363f4436dda27098d56a8f1a02913b2c2267a 100644 --- a/dbrepo-container-service/services/src/main/java/at/tuwien/service/impl/ContainerServiceImpl.java +++ b/dbrepo-container-service/services/src/main/java/at/tuwien/service/impl/ContainerServiceImpl.java @@ -1,6 +1,8 @@ package at.tuwien.service.impl; import at.tuwien.api.container.ContainerCreateRequestDto; +import at.tuwien.api.container.ContainerDto; +import at.tuwien.api.container.ContainerStateDto; import at.tuwien.config.DockerDaemonConfig; import at.tuwien.entities.container.Container; import at.tuwien.entities.container.image.ContainerImage; @@ -184,8 +186,7 @@ public class ContainerServiceImpl implements ContainerService { @Override @Transactional - public Container inspect(Long id) throws ContainerNotFoundException, DockerClientException, - ContainerNotRunningException { + public ContainerDto inspect(Long id) throws DockerClientException, ContainerNotRunningException, ContainerNotFoundException { final Container container = find(id); final InspectContainerResponse response; try { @@ -207,15 +208,19 @@ public class ContainerServiceImpl implements ContainerService { log.error("Failed to inspect container state: container is not running"); throw new ContainerNotRunningException("Failed to inspect container state"); } + final ContainerDto dto = containerMapper.containerToContainerDto(container); + dto.setHash(container.getHash()); + dto.setRunning(response.getState().getRunning()); + dto.setState(containerMapper.containerStateToContainerStateDto(response.getState())); /* now we only support one network */ response.getNetworkSettings() .getNetworks() .forEach((key, network) -> { log.trace("key {} network {}", key, network); - container.setIpAddress(network.getIpAddress()); + dto.setIpAddress(network.getIpAddress()); }); - log.info("Inspect container with id {}", id); - return container; + log.info("Inspected container with hash {}", container.getHash()); + return dto; } @Override diff --git a/dbrepo-database-service/rest-service/src/main/java/at/tuwien/endpoints/DatabaseEndpoint.java b/dbrepo-database-service/rest-service/src/main/java/at/tuwien/endpoints/DatabaseEndpoint.java index 63114d8ccee3353eca4e90fc34f36dfb98e65f3a..e07a29bf0b2012e4910602939f8b4125f1607976 100644 --- a/dbrepo-database-service/rest-service/src/main/java/at/tuwien/endpoints/DatabaseEndpoint.java +++ b/dbrepo-database-service/rest-service/src/main/java/at/tuwien/endpoints/DatabaseEndpoint.java @@ -1,5 +1,6 @@ package at.tuwien.endpoints; +import at.tuwien.api.container.ContainerDto; import at.tuwien.api.database.*; import at.tuwien.api.error.ApiErrorDto; import at.tuwien.entities.container.Container; @@ -257,7 +258,7 @@ public class DatabaseEndpoint { mediaType = "application/json", schema = @Schema(implementation = DatabaseDto.class))}), @ApiResponse(responseCode = "404", - description = "Database could not be found", + description = "Database or container could not be found", content = {@Content( mediaType = "application/json", schema = @Schema(implementation = ApiErrorDto.class))}), @@ -270,7 +271,7 @@ public class DatabaseEndpoint { public ResponseEntity<DatabaseDto> findById(@NotNull @PathVariable("id") Long containerId, @NotNull @PathVariable Long databaseId, Principal principal) - throws DatabaseNotFoundException, AccessDeniedException { + throws DatabaseNotFoundException, AccessDeniedException, ContainerNotFoundException { log.debug("endpoint find database, containerId={}, databaseId={}", containerId, databaseId); final Database database = databaseService.findById(containerId, databaseId); final DatabaseDto dto = databaseMapper.databaseToDatabaseDto(database); @@ -281,7 +282,9 @@ public class DatabaseEndpoint { .map(databaseMapper::databaseAccessToDatabaseAccessDto) .collect(Collectors.toList())); } - log.trace("find database resulted in database {}", database); + final ContainerDto containerDto = containerService.inspect(containerId); + dto.setContainer(containerDto); + log.trace("find database resulted in dto {}", dto); return ResponseEntity.ok(dto); } diff --git a/dbrepo-database-service/rest-service/src/test/java/at/tuwien/endpoint/DatabaseEndpointUnitTest.java b/dbrepo-database-service/rest-service/src/test/java/at/tuwien/endpoint/DatabaseEndpointUnitTest.java index c32330b76d496cf6232be98a237062ee37f156d4..4cf0fc3edb0a0dc7898a07e5de495c825be5456b 100644 --- a/dbrepo-database-service/rest-service/src/test/java/at/tuwien/endpoint/DatabaseEndpointUnitTest.java +++ b/dbrepo-database-service/rest-service/src/test/java/at/tuwien/endpoint/DatabaseEndpointUnitTest.java @@ -350,7 +350,7 @@ public class DatabaseEndpointUnitTest extends BaseUnitTest { @Test @WithAnonymousUser - public void findById_anonymous_succeeds() throws AccessDeniedException, DatabaseNotFoundException { + public void findById_anonymous_succeeds() throws AccessDeniedException, DatabaseNotFoundException, ContainerNotFoundException { /* test */ findById_generic(CONTAINER_1_ID, CONTAINER_1, DATABASE_1_ID, DATABASE_1, null); @@ -368,7 +368,7 @@ public class DatabaseEndpointUnitTest extends BaseUnitTest { @Test @WithMockUser(username = USER_1_USERNAME, authorities = {"find-database"}) - public void findById_hasRole_succeeds() throws AccessDeniedException, DatabaseNotFoundException { + public void findById_hasRole_succeeds() throws AccessDeniedException, DatabaseNotFoundException, ContainerNotFoundException { /* pre-condition */ assertTrue(DATABASE_3_PUBLIC); @@ -380,7 +380,7 @@ public class DatabaseEndpointUnitTest extends BaseUnitTest { @Test @WithMockUser(username = USER_1_USERNAME, authorities = {"find-database"}) public void findById_hasRoleForeign_succeeds() throws AccessDeniedException, - DatabaseNotFoundException { + DatabaseNotFoundException, ContainerNotFoundException { /* pre-condition */ assertTrue(DATABASE_3_PUBLIC); @@ -392,7 +392,7 @@ public class DatabaseEndpointUnitTest extends BaseUnitTest { @Test @WithMockUser(username = USER_1_USERNAME, authorities = {"find-database"}) public void findById_ownerSeesAccessRights_succeeds() throws AccessDeniedException, - DatabaseNotFoundException { + DatabaseNotFoundException, ContainerNotFoundException { /* mock */ when(accessService.list(DATABASE_1_ID)) @@ -506,7 +506,7 @@ public class DatabaseEndpointUnitTest extends BaseUnitTest { } public DatabaseDto findById_generic(Long containerId, Container container, Long databaseId, Database database, - Principal principal) throws DatabaseNotFoundException, AccessDeniedException { + Principal principal) throws DatabaseNotFoundException, AccessDeniedException, ContainerNotFoundException { /* mock */ if (database != null) { diff --git a/dbrepo-database-service/services/src/main/java/at/tuwien/config/GatewayConfig.java b/dbrepo-database-service/services/src/main/java/at/tuwien/config/GatewayConfig.java index b30f9a567ca1abd794c1f630e633815606c14d00..f3db3e030a05b7e91135fd8a0968d29eb9b6d2d5 100644 --- a/dbrepo-database-service/services/src/main/java/at/tuwien/config/GatewayConfig.java +++ b/dbrepo-database-service/services/src/main/java/at/tuwien/config/GatewayConfig.java @@ -27,8 +27,8 @@ public class GatewayConfig { @Value("${spring.rabbitmq.password}") private String brokerPassword; - @Bean("authenticationRestTemplate") - public RestTemplate authenticationRestTemplate() { + @Bean("gatewayRestTemplate") + public RestTemplate gatewayRestTemplate() { final RestTemplate restTemplate = new RestTemplate(); restTemplate.setUriTemplateHandler(new DefaultUriBuilderFactory(gatewayEndpoint)); return restTemplate; diff --git a/dbrepo-database-service/services/src/main/java/at/tuwien/gateway/ContainerServiceGateway.java b/dbrepo-database-service/services/src/main/java/at/tuwien/gateway/ContainerServiceGateway.java new file mode 100644 index 0000000000000000000000000000000000000000..8fcd57a66e49f5c73a44e41e9958c2606344819c --- /dev/null +++ b/dbrepo-database-service/services/src/main/java/at/tuwien/gateway/ContainerServiceGateway.java @@ -0,0 +1,14 @@ +package at.tuwien.gateway; + +import at.tuwien.api.container.ContainerDto; +import at.tuwien.exception.ContainerNotFoundException; + +public interface ContainerServiceGateway { + + /** + * @param id + * @return + * @throws ContainerNotFoundException + */ + ContainerDto find(Long id) throws ContainerNotFoundException; +} diff --git a/dbrepo-database-service/services/src/main/java/at/tuwien/gateway/impl/ContainerServiceGatewayImpl.java b/dbrepo-database-service/services/src/main/java/at/tuwien/gateway/impl/ContainerServiceGatewayImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..8e366fdf984df58824a2fead84c68a535e6b2a41 --- /dev/null +++ b/dbrepo-database-service/services/src/main/java/at/tuwien/gateway/impl/ContainerServiceGatewayImpl.java @@ -0,0 +1,41 @@ +package at.tuwien.gateway.impl; + +import at.tuwien.api.container.ContainerDto; +import at.tuwien.config.GatewayConfig; +import at.tuwien.exception.ContainerNotFoundException; +import at.tuwien.gateway.ContainerServiceGateway; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +@Slf4j +@Service +public class ContainerServiceGatewayImpl implements ContainerServiceGateway { + + private final RestTemplate restTemplate; + private final GatewayConfig gatewayConfig; + + @Autowired + public ContainerServiceGatewayImpl(@Qualifier("gatewayRestTemplate") RestTemplate restTemplate, + GatewayConfig gatewayConfig) { + this.restTemplate = restTemplate; + this.gatewayConfig = gatewayConfig; + } + + @Override + public ContainerDto find(Long id) throws ContainerNotFoundException { + final String url = gatewayConfig.getGatewayEndpoint() + "/api/container/" + id; + final ResponseEntity<ContainerDto> response = restTemplate.exchange(url, HttpMethod.GET, null, ContainerDto.class); + if (!response.getStatusCode().equals(HttpStatus.OK)) { + log.error("Failed to find container: {}", response.getStatusCode()); + throw new ContainerNotFoundException("Failed to find container"); + } + return response.getBody(); + } + +} diff --git a/dbrepo-database-service/services/src/main/java/at/tuwien/service/ContainerService.java b/dbrepo-database-service/services/src/main/java/at/tuwien/service/ContainerService.java index ee70355c6d596650f7b0896dba30882cd760eeae..d58e11d4416cb62727f6a12688c2ff419fab2add 100644 --- a/dbrepo-database-service/services/src/main/java/at/tuwien/service/ContainerService.java +++ b/dbrepo-database-service/services/src/main/java/at/tuwien/service/ContainerService.java @@ -1,8 +1,11 @@ package at.tuwien.service; +import at.tuwien.api.container.ContainerDto; import at.tuwien.entities.container.Container; import at.tuwien.exception.ContainerNotFoundException; public interface ContainerService { Container find(Long id) throws ContainerNotFoundException; + + ContainerDto inspect(Long id) throws ContainerNotFoundException; } diff --git a/dbrepo-database-service/services/src/main/java/at/tuwien/service/impl/ContainerServiceImpl.java b/dbrepo-database-service/services/src/main/java/at/tuwien/service/impl/ContainerServiceImpl.java index bb657b1812fc208a85020c84a28856c084273ac6..76c29263d4b42461ac01274551f5650a5b6397ba 100644 --- a/dbrepo-database-service/services/src/main/java/at/tuwien/service/impl/ContainerServiceImpl.java +++ b/dbrepo-database-service/services/src/main/java/at/tuwien/service/impl/ContainerServiceImpl.java @@ -1,7 +1,9 @@ package at.tuwien.service.impl; +import at.tuwien.api.container.ContainerDto; import at.tuwien.entities.container.Container; import at.tuwien.exception.ContainerNotFoundException; +import at.tuwien.gateway.ContainerServiceGateway; import at.tuwien.repository.jpa.ContainerRepository; import at.tuwien.service.ContainerService; import lombok.extern.log4j.Log4j2; @@ -15,10 +17,13 @@ import java.util.Optional; public class ContainerServiceImpl implements ContainerService { private final ContainerRepository containerRepository; + private final ContainerServiceGateway containerServiceGateway; @Autowired - public ContainerServiceImpl(ContainerRepository containerRepository) { + public ContainerServiceImpl(ContainerRepository containerRepository, + ContainerServiceGateway containerServiceGateway) { this.containerRepository = containerRepository; + this.containerServiceGateway = containerServiceGateway; } @Override @@ -30,4 +35,9 @@ public class ContainerServiceImpl implements ContainerService { } return optional.get(); } + + @Override + public ContainerDto inspect(Long id) throws ContainerNotFoundException { + return containerServiceGateway.find(id); + } } diff --git a/dbrepo-metadata-db/api/src/main/java/at/tuwien/api/maintenance/BannerMessageBriefDto.java b/dbrepo-metadata-db/api/src/main/java/at/tuwien/api/maintenance/BannerMessageBriefDto.java new file mode 100644 index 0000000000000000000000000000000000000000..a11c70f6219d1bd362775c50fedbefa5a9aa07f4 --- /dev/null +++ b/dbrepo-metadata-db/api/src/main/java/at/tuwien/api/maintenance/BannerMessageBriefDto.java @@ -0,0 +1,33 @@ +package at.tuwien.api.maintenance; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.*; +import lombok.extern.jackson.Jacksonized; + +@Getter +@Setter +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Jacksonized +@ToString +public class BannerMessageBriefDto { + + @NotNull + private BannerMessageTypeDto type; + + @NotBlank + @Schema(example = "Maintenance starts on 8am on Monday") + private String message; + + @Schema(example = "https://example.com") + private String link; + + @JsonProperty("link_text") + @Schema(example = "More") + private String linkText; + +} diff --git a/dbrepo-metadata-db/api/src/main/java/at/tuwien/api/maintenance/BannerMessageCreateDto.java b/dbrepo-metadata-db/api/src/main/java/at/tuwien/api/maintenance/BannerMessageCreateDto.java new file mode 100644 index 0000000000000000000000000000000000000000..c2278f343f1186875b83510b9ea906c53930ce02 --- /dev/null +++ b/dbrepo-metadata-db/api/src/main/java/at/tuwien/api/maintenance/BannerMessageCreateDto.java @@ -0,0 +1,44 @@ +package at.tuwien.api.maintenance; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.*; +import lombok.extern.jackson.Jacksonized; + +import java.time.Instant; + +@Getter +@Setter +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Jacksonized +@ToString +public class BannerMessageCreateDto { + + @NotNull + private BannerMessageTypeDto type; + + @NotBlank + @Schema(example = "Maintenance starts on 8am on Monday") + private String message; + + @Schema(example = "https://example.com") + private String link; + + @JsonProperty("link_text") + @Schema(example = "More") + private String linkText; + + @JsonProperty("display_start") + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX", timezone = "UTC") + private Instant displayStart; + + @JsonProperty("display_end") + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX", timezone = "UTC") + private Instant displayEnd; + +} diff --git a/dbrepo-metadata-db/api/src/main/java/at/tuwien/api/maintenance/BannerMessageDto.java b/dbrepo-metadata-db/api/src/main/java/at/tuwien/api/maintenance/BannerMessageDto.java new file mode 100644 index 0000000000000000000000000000000000000000..1b822b9ed45b79c78b4a9c6f8c0b177d9d5e15ed --- /dev/null +++ b/dbrepo-metadata-db/api/src/main/java/at/tuwien/api/maintenance/BannerMessageDto.java @@ -0,0 +1,47 @@ +package at.tuwien.api.maintenance; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.*; +import lombok.extern.jackson.Jacksonized; + +import java.time.Instant; + +@Getter +@Setter +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Jacksonized +@ToString +public class BannerMessageDto { + + @NotNull + private Long id; + + @NotNull + private BannerMessageTypeDto type; + + @NotBlank + @Schema(example = "Maintenance starts on 8am on Monday") + private String message; + + @Schema(example = "https://example.com") + private String link; + + @JsonProperty("link_text") + @Schema(example = "More") + private String linkText; + + @JsonProperty("display_start") + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX", timezone = "UTC") + private Instant displayStart; + + @JsonProperty("display_end") + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX", timezone = "UTC") + private Instant displayEnd; + +} diff --git a/dbrepo-metadata-db/api/src/main/java/at/tuwien/api/maintenance/BannerMessageTypeDto.java b/dbrepo-metadata-db/api/src/main/java/at/tuwien/api/maintenance/BannerMessageTypeDto.java new file mode 100644 index 0000000000000000000000000000000000000000..8a867f5ea4b06b44aff2efde9ee36c371718f374 --- /dev/null +++ b/dbrepo-metadata-db/api/src/main/java/at/tuwien/api/maintenance/BannerMessageTypeDto.java @@ -0,0 +1,28 @@ +package at.tuwien.api.maintenance; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; + +@Getter +public enum BannerMessageTypeDto { + + @JsonProperty("error") + ERROR("error"), + + @JsonProperty("warning") + WARNING("warning"), + + @JsonProperty("info") + INFO("info"); + + private String name; + + BannerMessageTypeDto(String name) { + this.name = name; + } + + @Override + public String toString() { + return this.name; + } +} diff --git a/dbrepo-metadata-db/api/src/main/java/at/tuwien/api/maintenance/BannerMessageUpdateDto.java b/dbrepo-metadata-db/api/src/main/java/at/tuwien/api/maintenance/BannerMessageUpdateDto.java new file mode 100644 index 0000000000000000000000000000000000000000..107c2405ca76a791dbf8684eb8a7f510be3589f4 --- /dev/null +++ b/dbrepo-metadata-db/api/src/main/java/at/tuwien/api/maintenance/BannerMessageUpdateDto.java @@ -0,0 +1,44 @@ +package at.tuwien.api.maintenance; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.*; +import lombok.extern.jackson.Jacksonized; + +import java.time.Instant; + +@Getter +@Setter +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Jacksonized +@ToString +public class BannerMessageUpdateDto { + + @NotNull + private BannerMessageTypeDto type; + + @NotBlank + @Schema(example = "Maintenance starts on 8am on Monday") + private String message; + + @Schema(example = "https://example.com") + private String link; + + @JsonProperty("link_text") + @Schema(example = "More") + private String linkText; + + @JsonProperty("display_start") + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX", timezone = "UTC") + private Instant displayStart; + + @JsonProperty("display_end") + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX", timezone = "UTC") + private Instant displayEnd; + +} diff --git a/dbrepo-metadata-db/entities/src/main/java/at/tuwien/entities/maintenance/BannerMessage.java b/dbrepo-metadata-db/entities/src/main/java/at/tuwien/entities/maintenance/BannerMessage.java new file mode 100644 index 0000000000000000000000000000000000000000..f3cf82d4997a7b67de2b0ee834615fd56a379cb4 --- /dev/null +++ b/dbrepo-metadata-db/entities/src/main/java/at/tuwien/entities/maintenance/BannerMessage.java @@ -0,0 +1,43 @@ +package at.tuwien.entities.maintenance; + +import jakarta.persistence.*; +import lombok.*; +import org.hibernate.annotations.GenericGenerator; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import java.time.Instant; + +@Data +@Entity +@Builder +@ToString +@AllArgsConstructor +@NoArgsConstructor +@EntityListeners(AuditingEntityListener.class) +@Table(name = "mdb_banner_messages") +@NamedQueries({ + @NamedQuery(name = "BannerMessage.findByActive", query = "select m from BannerMessage m where (m.displayStart = null and m.displayEnd = null) or (m.displayStart = null and m.displayEnd >= NOW()) or (m.displayStart <= NOW() and m.displayEnd >= NOW()) or (m.displayStart <= NOW() and m.displayEnd = null)") +}) +public class BannerMessage { + + @Id + @EqualsAndHashCode.Include + @GeneratedValue(generator = "messages-sequence") + @GenericGenerator(name = "messages-sequence", strategy = "increment") + @Column(updatable = false, nullable = false) + private Long id; + + @Enumerated(EnumType.STRING) + @Column(nullable = false, columnDefinition = "enum('ERROR','WARNING','INFO')") + private BannerMessageType type; + + @Column(nullable = false) + private String message; + + @Column(columnDefinition = "TIMESTAMP") + private Instant displayStart; + + @Column(columnDefinition = "TIMESTAMP") + private Instant displayEnd; + +} diff --git a/dbrepo-metadata-db/entities/src/main/java/at/tuwien/entities/maintenance/BannerMessageType.java b/dbrepo-metadata-db/entities/src/main/java/at/tuwien/entities/maintenance/BannerMessageType.java new file mode 100644 index 0000000000000000000000000000000000000000..8d17965f482754e5c9e9aed8e6c9e9c82b6ab650 --- /dev/null +++ b/dbrepo-metadata-db/entities/src/main/java/at/tuwien/entities/maintenance/BannerMessageType.java @@ -0,0 +1,13 @@ + +package at.tuwien.entities.maintenance; + +import lombok.Getter; +import lombok.ToString; + +@Getter +@ToString +public enum BannerMessageType { + WARNING, + ERROR, + INFO; +} \ No newline at end of file diff --git a/dbrepo-metadata-db/setup-schema.sql b/dbrepo-metadata-db/setup-schema.sql index eb7809d1409e253272d63244c3ec444f14220b81..24798819e914a7e5c2bf34c44c561cee6bbfc936 100644 --- a/dbrepo-metadata-db/setup-schema.sql +++ b/dbrepo-metadata-db/setup-schema.sql @@ -332,6 +332,18 @@ CREATE TABLE IF NOT EXISTS `fda`.`mdb_view` FOREIGN KEY (vdbid) REFERENCES mdb_databases (id) ) WITH SYSTEM VERSIONING; +CREATE TABLE IF NOT EXISTS `fda`.`mdb_banner_messages` +( + id bigint NOT NULL AUTO_INCREMENT, + type ENUM ('ERROR', 'WARNING', 'INFO') NOT NULL default 'INFO', + message TEXT NOT NULL, + link TEXT NULL, + link_text VARCHAR(255) NULL, + display_start timestamp NULL, + display_end timestamp NULL, + PRIMARY KEY (id) +) WITH SYSTEM VERSIONING; + CREATE TABLE IF NOT EXISTS `fda`.`mdb_view_columns` ( id BIGINT NOT NULL AUTO_INCREMENT, diff --git a/dbrepo-metadata-db/test/src/main/java/at/tuwien/test/BaseTest.java b/dbrepo-metadata-db/test/src/main/java/at/tuwien/test/BaseTest.java index 2d91e8ac304ec00b5c851dc14599803c6b65a85d..6265b027b9757a6b0c311096023478fb0533a957 100644 --- a/dbrepo-metadata-db/test/src/main/java/at/tuwien/test/BaseTest.java +++ b/dbrepo-metadata-db/test/src/main/java/at/tuwien/test/BaseTest.java @@ -3,8 +3,9 @@ package at.tuwien.test; import at.tuwien.api.amqp.CreateVirtualHostDto; import at.tuwien.api.amqp.GrantVirtualHostPermissionsDto; import at.tuwien.api.auth.SignupRequestDto; -import at.tuwien.api.container.image.ImageEnvItemDto; -import at.tuwien.api.container.image.ImageEnvItemTypeDto; +import at.tuwien.api.container.ContainerDto; +import at.tuwien.api.container.ContainerStateDto; +import at.tuwien.api.container.image.*; import at.tuwien.api.database.DatabaseCreateDto; import at.tuwien.api.database.DatabaseDto; import at.tuwien.api.database.LicenseDto; @@ -20,6 +21,9 @@ import at.tuwien.api.database.table.columns.concepts.ColumnSemanticsUpdateDto; import at.tuwien.api.database.table.constraints.ConstraintsCreateDto; import at.tuwien.api.database.table.constraints.foreignKey.ForeignKeyCreateDto; import at.tuwien.api.identifier.*; +import at.tuwien.api.maintenance.BannerMessageCreateDto; +import at.tuwien.api.maintenance.BannerMessageTypeDto; +import at.tuwien.api.maintenance.BannerMessageUpdateDto; import at.tuwien.api.user.*; import at.tuwien.entities.container.image.ContainerImageDate; import at.tuwien.entities.database.*; @@ -30,6 +34,8 @@ import at.tuwien.entities.database.table.constraints.foreignKey.ForeignKey; import at.tuwien.entities.database.table.constraints.foreignKey.ForeignKeyReference; import at.tuwien.entities.database.table.constraints.unique.Unique; import at.tuwien.entities.identifier.*; +import at.tuwien.entities.maintenance.BannerMessage; +import at.tuwien.entities.maintenance.BannerMessageType; import at.tuwien.entities.user.Realm; import at.tuwien.entities.user.Role; import at.tuwien.entities.user.User; @@ -96,7 +102,7 @@ import static java.time.temporal.ChronoUnit.*; * <br /> * User 2 (authorities=default developer) * <br /> - * User 3 (authorities=empty) + * User 3 (authorities=default data-steward) */ public abstract class BaseTest { @@ -304,6 +310,14 @@ public abstract class BaseTest { .attributes(USER_1_ATTRIBUTES_DTO) .build(); + public final static UserBriefDto USER_1_BRIEF_DTO = UserBriefDto.builder() + .id(USER_1_ID) + .username(USER_1_USERNAME) + .firstname(USER_1_FIRSTNAME) + .lastname(USER_1_LASTNAME) + .emailVerified(USER_1_VERIFIED) + .build(); + public final static UserDetails USER_1_DETAILS = UserDetailsDto.builder() .username(USER_1_USERNAME) .email(USER_1_EMAIL) @@ -701,6 +715,24 @@ public abstract class BaseTest { .hasTime(IMAGE_DATE_1_HAS_TIME) .build(); + public final static ImageDateDto IMAGE_DATE_1_DTO = ImageDateDto.builder() + .id(IMAGE_DATE_1_ID) + .unixFormat(IMAGE_DATE_1_UNIX_FORMAT) + .databaseFormat(IMAGE_DATE_1_DATABASE_FORMAT) + .example(IMAGE_DATE_1_EXAMPLE) + .hasTime(IMAGE_DATE_1_HAS_TIME) + .build(); + + public final static ImageCreateDto IMAGE_1_CREATE_DTO = ImageCreateDto.builder() + .repository(IMAGE_1_REPOSITORY) + .tag(IMAGE_1_TAG) + .dialect(IMAGE_1_DIALECT) + .jdbcMethod(IMAGE_1_JDBC) + .driverClass(IMAGE_1_DRIVER) + .defaultPort(IMAGE_1_PORT) + .environment(IMAGE_1_ENV_DTO) + .build(); + public final static Long IMAGE_DATE_2_ID = 2L; public final static Long IMAGE_DATE_2_IMAGE_ID = IMAGE_1_ID; public final static String IMAGE_DATE_2_UNIX_FORMAT = "dd.MM.yy"; @@ -717,6 +749,14 @@ public abstract class BaseTest { .hasTime(IMAGE_DATE_2_HAS_TIME) .build(); + public final static ImageDateDto IMAGE_DATE_2_DTO = ImageDateDto.builder() + .id(IMAGE_DATE_2_ID) + .unixFormat(IMAGE_DATE_2_UNIX_FORMAT) + .databaseFormat(IMAGE_DATE_2_DATABASE_FORMAT) + .example(IMAGE_DATE_2_EXAMPLE) + .hasTime(IMAGE_DATE_2_HAS_TIME) + .build(); + public final static Long IMAGE_DATE_3_ID = 3L; public final static Long IMAGE_DATE_3_IMAGE_ID = IMAGE_1_ID; public final static String IMAGE_DATE_3_UNIX_FORMAT = "yyyy-MM-dd'T'HH:mm:ss.SSSSSS"; @@ -733,6 +773,14 @@ public abstract class BaseTest { .hasTime(IMAGE_DATE_3_HAS_TIME) .build(); + public final static ImageDateDto IMAGE_DATE_3_DTO = ImageDateDto.builder() + .id(IMAGE_DATE_3_ID) + .unixFormat(IMAGE_DATE_3_UNIX_FORMAT) + .databaseFormat(IMAGE_DATE_3_DATABASE_FORMAT) + .example(IMAGE_DATE_3_EXAMPLE) + .hasTime(IMAGE_DATE_3_HAS_TIME) + .build(); + public final static ContainerImage IMAGE_1 = ContainerImage.builder() .id(IMAGE_1_ID) .repository(IMAGE_1_REPOSITORY) @@ -763,6 +811,27 @@ public abstract class BaseTest { .environment(List.of() /* for jpa */) .build(); + public final static ImageDto IMAGE_1_DTO = ImageDto.builder() + .id(IMAGE_1_ID) + .repository(IMAGE_1_REPOSITORY) + .tag(IMAGE_1_TAG) + .hash(IMAGE_1_HASH) + .compiled(IMAGE_1_BUILT) + .dialect(IMAGE_1_DIALECT) + .jdbcMethod(IMAGE_1_JDBC) + .driverClass(IMAGE_1_DRIVER) + .size(BigInteger.valueOf(IMAGE_1_SIZE)) + .environment(IMAGE_1_ENV_DTO) + .defaultPort(IMAGE_1_PORT) + .dateFormats(List.of(IMAGE_DATE_1_DTO, IMAGE_DATE_2_DTO, IMAGE_DATE_3_DTO)) + .build(); + + public final static ImageBriefDto IMAGE_1_BRIEF_DTO = ImageBriefDto.builder() + .id(IMAGE_1_ID) + .repository(IMAGE_1_REPOSITORY) + .tag(IMAGE_1_TAG) + .build(); + public final static Long IMAGE_2_ID = 2L; public final static String IMAGE_2_REPOSITORY = "mysql"; public final static String IMAGE_2_TAG = "8.0"; @@ -824,6 +893,7 @@ public abstract class BaseTest { public final static Long CONTAINER_1_ID = 1L; public final static String CONTAINER_1_HASH = "deadbeef"; public final static ContainerImage CONTAINER_1_IMAGE = IMAGE_1; + public final static ImageBriefDto CONTAINER_1_IMAGE_BRIEF_DTO = IMAGE_1_BRIEF_DTO; public final static String CONTAINER_1_NAME = "u01"; public final static String CONTAINER_1_INTERNALNAME = "dbrepo-userdb-u01"; public final static String CONTAINER_1_IP = "172.30.0.5"; @@ -831,6 +901,8 @@ public abstract class BaseTest { public final static HealthCheck CONTAINER_1_HEALTHCHECK = new HealthCheck() .withTest(List.of("CMD", "mysqladmin", "ping", "--host=127.0.0.1", "--password=mariadb")); public final static String[] CONTAINER_1_ENV = new String[]{"MARIADB_ROOT_PASSWORD=mariadb", "MARIADB_DATABASE=weather"}; + public final static ContainerStateDto CONTAINER_1_STATE = ContainerStateDto.RUNNING; + public final static Boolean CONTAINER_1_RUNNING = true; public final static Container CONTAINER_1 = Container.builder() .id(CONTAINER_1_ID) @@ -862,6 +934,19 @@ public abstract class BaseTest { .owner(null /* for jpa */) .build(); + public final static ContainerDto CONTAINER_1_DTO = ContainerDto.builder() + .id(CONTAINER_1_ID) + .name(CONTAINER_1_NAME) + .internalName(CONTAINER_1_INTERNALNAME) + .image(CONTAINER_1_IMAGE_BRIEF_DTO) + .hash(CONTAINER_1_HASH) + .created(CONTAINER_1_CREATED) + .ipAddress(CONTAINER_1_IP) + .owner(USER_1_BRIEF_DTO) + .state(CONTAINER_1_STATE) + .running(CONTAINER_1_RUNNING) + .build(); + public final static Long CONTAINER_2_ID = 2L; public final static String CONTAINER_2_HASH = "deadbeef"; public final static ContainerImage CONTAINER_2_IMAGE = IMAGE_1; @@ -5440,4 +5525,55 @@ public abstract class BaseTest { .configure(".*") .build(); + public final static Long BANNER_MESSAGE_1_ID = 1L; + public final static String BANNER_MESSAGE_1_MESSAGE = "Next maintenance in 7 days!"; + public final static BannerMessageType BANNER_MESSAGE_1_TYPE = BannerMessageType.INFO; + public final static BannerMessageTypeDto BANNER_MESSAGE_1_TYPE_DTO = BannerMessageTypeDto.INFO; + public final static Instant BANNER_MESSAGE_1_START = Instant.ofEpochSecond(1684577786); + public final static Instant BANNER_MESSAGE_1_END = null; + + public final static BannerMessage BANNER_MESSAGE_1 = BannerMessage.builder() + .id(BANNER_MESSAGE_1_ID) + .message(BANNER_MESSAGE_1_MESSAGE) + .type(BANNER_MESSAGE_1_TYPE) + .displayStart(BANNER_MESSAGE_1_START) + .displayEnd(BANNER_MESSAGE_1_END) + .build(); + + public final static BannerMessageCreateDto BANNER_MESSAGE_1_CREATE_DTO = BannerMessageCreateDto.builder() + .message(BANNER_MESSAGE_1_MESSAGE) + .type(BANNER_MESSAGE_1_TYPE_DTO) + .displayStart(BANNER_MESSAGE_1_START) + .displayEnd(BANNER_MESSAGE_1_END) + .build(); + + public final static BannerMessageUpdateDto BANNER_MESSAGE_1_UPDATE_DTO = BannerMessageUpdateDto.builder() + .message(BANNER_MESSAGE_1_MESSAGE) + .type(BannerMessageTypeDto.WARNING) + .displayStart(BANNER_MESSAGE_1_START) + .displayEnd(BANNER_MESSAGE_1_END) + .build(); + + public final static Long BANNER_MESSAGE_2_ID = 2L; + public final static String BANNER_MESSAGE_2_MESSAGE = "No operation on Christmas 2022!"; + public final static BannerMessageType BANNER_MESSAGE_2_TYPE = BannerMessageType.ERROR; + public final static BannerMessageTypeDto BANNER_MESSAGE_2_TYPE_DTO = BannerMessageTypeDto.ERROR; + public final static Instant BANNER_MESSAGE_2_START = Instant.ofEpochSecond(1671836400); + public final static Instant BANNER_MESSAGE_2_END = Instant.ofEpochSecond(1672009200); + + public final static BannerMessage BANNER_MESSAGE_2 = BannerMessage.builder() + .id(BANNER_MESSAGE_2_ID) + .message(BANNER_MESSAGE_2_MESSAGE) + .type(BANNER_MESSAGE_2_TYPE) + .displayStart(BANNER_MESSAGE_2_START) + .displayEnd(BANNER_MESSAGE_2_END) + .build(); + + public final static BannerMessageCreateDto BANNER_MESSAGE_2_CREATE_DTO = BannerMessageCreateDto.builder() + .message(BANNER_MESSAGE_2_MESSAGE) + .type(BANNER_MESSAGE_2_TYPE_DTO) + .displayStart(BANNER_MESSAGE_2_START) + .displayEnd(BANNER_MESSAGE_2_END) + .build(); + } diff --git a/dbrepo-metadata-service/pom.xml b/dbrepo-metadata-service/pom.xml index 6c3514de1ebfeaddd3b01c471881b13f4ff7b32a..8dfab218eee806e01a1c1d7ad8d91e54ba352ae4 100644 --- a/dbrepo-metadata-service/pom.xml +++ b/dbrepo-metadata-service/pom.xml @@ -114,6 +114,12 @@ <version>${project.version}</version> <scope>compile</scope> </dependency> + <!-- Authentication --> + <dependency> + <groupId>com.auth0</groupId> + <artifactId>java-jwt</artifactId> + <version>${jwt.version}</version> + </dependency> <!-- IDE --> <dependency> <groupId>org.projectlombok</groupId> diff --git a/dbrepo-metadata-service/services/src/main/java/at/tuwien/config/WebSecurityConfig.java b/dbrepo-metadata-service/services/src/main/java/at/tuwien/config/WebSecurityConfig.java index 13d92092fc3e74832d3e1ae48610a1614ba223a7..7bec3fbf94675ec5f48f7b0b46298da40c9d51cb 100644 --- a/dbrepo-metadata-service/services/src/main/java/at/tuwien/config/WebSecurityConfig.java +++ b/dbrepo-metadata-service/services/src/main/java/at/tuwien/config/WebSecurityConfig.java @@ -2,7 +2,6 @@ package at.tuwien.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.http.HttpMethod; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; diff --git a/dbrepo-query-service/services/src/main/java/at/tuwien/config/WebSecurityConfig.java b/dbrepo-query-service/services/src/main/java/at/tuwien/config/WebSecurityConfig.java index 582fe83630c25eee8b35c90b1000db5c1fd48564..1c36be92a450333a9a36b2ae24c9f7c1de5a90da 100644 --- a/dbrepo-query-service/services/src/main/java/at/tuwien/config/WebSecurityConfig.java +++ b/dbrepo-query-service/services/src/main/java/at/tuwien/config/WebSecurityConfig.java @@ -5,7 +5,6 @@ import io.swagger.v3.oas.annotations.enums.SecuritySchemeType; import io.swagger.v3.oas.annotations.security.SecurityScheme; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.http.HttpMethod; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; diff --git a/dbrepo-ui/api/metadata.service.js b/dbrepo-ui/api/metadata.service.js new file mode 100644 index 0000000000000000000000000000000000000000..9c02ea9247eea2ab242bf5387652d4ed2e50b5bc --- /dev/null +++ b/dbrepo-ui/api/metadata.service.js @@ -0,0 +1,104 @@ +import Vue from 'vue' +import api from '@/api' + +class MetadataService { + findAllMessages () { + return new Promise((resolve, reject) => { + api.get('/api/maintenance/message', { headers: { Accept: 'application/json' } }) + .then((response) => { + const messages = response.data + console.debug('response messages', messages) + resolve(messages) + }) + .catch((error) => { + const { code, message } = error + console.error('Failed to load messages', error) + Vue.$toast.error(`[${code}] Failed to load messages: ${message}`) + reject(error) + }) + }) + } + + createMessage (data) { + return new Promise((resolve, reject) => { + api.post('/api/maintenance/message', data, { headers: { Accept: 'application/json' } }) + .then((response) => { + const messages = response.data + console.debug('response message', messages) + resolve(messages) + }) + .catch((error) => { + const { code, message } = error + console.error('Failed to create message', error) + Vue.$toast.error(`[${code}] Failed to create message: ${message}`) + reject(error) + }) + }) + } + + findMessage (id) { + return new Promise((resolve, reject) => { + api.get(`/api/maintenance/message/${id}`, { headers: { Accept: 'application/json' } }) + .then((response) => { + const messages = response.data + console.debug('response message', messages) + resolve(messages) + }) + .catch((error) => { + const { code, message } = error + console.error('Failed to find message', error) + Vue.$toast.error(`[${code}] Failed to find message: ${message}`) + reject(error) + }) + }) + } + + updateMessage (id, data) { + return new Promise((resolve, reject) => { + api.put(`/api/maintenance/message/${id}`, data, { headers: { Accept: 'application/json' } }) + .then((response) => { + const messages = response.data + console.debug('response message', messages) + resolve(messages) + }) + .catch((error) => { + const { code, message } = error + console.error('Failed to update message', error) + Vue.$toast.error(`[${code}] Failed to update message: ${message}`) + reject(error) + }) + }) + } + + deleteMessage (id) { + return new Promise((resolve, reject) => { + api.delete(`/api/maintenance/message/${id}`, { headers: { Accept: 'application/json' } }) + .then(() => resolve()) + .catch((error) => { + const { code, message } = error + console.error('Failed to delete message', error) + Vue.$toast.error(`[${code}] Failed to delete message: ${message}`) + reject(error) + }) + }) + } + + findActiveMessages () { + return new Promise((resolve, reject) => { + api.get('/api/maintenance/message/active', { headers: { Accept: 'application/json' } }) + .then((response) => { + const messages = response.data + console.debug('response messages', messages) + resolve(messages) + }) + .catch((error) => { + const { code, message } = error + console.error('Failed to load active messages', error) + Vue.$toast.error(`[${code}] Failed to load active messages: ${message}`) + reject(error) + }) + }) + } +} + +export default new MetadataService() diff --git a/dbrepo-ui/components/DBToolbar.vue b/dbrepo-ui/components/DBToolbar.vue index b8590bc855355dd9d2e0207a20becb97c25f4e6d..c899ab03eb0afde4713b7b5b77a6a2b70727c021 100644 --- a/dbrepo-ui/components/DBToolbar.vue +++ b/dbrepo-ui/components/DBToolbar.vue @@ -55,7 +55,7 @@ <v-tab :to="`/container/${$route.params.container_id}/database/${$route.params.database_id}/table`"> {{ $t('databases.toolbar.tables', { name: 'vue-i18n' }) }} </v-tab> - <v-tab v-if="hasReadAccess" :to="`/container/${$route.params.container_id}/database/${$route.params.database_id}/query`"> + <v-tab v-if="canViewQueries" :to="`/container/${$route.params.container_id}/database/${$route.params.database_id}/query`"> {{ $t('databases.toolbar.subsets', { name: 'vue-i18n' }) }} </v-tab> <v-tab :to="`/container/${$route.params.container_id}/database/${$route.params.database_id}/view`"> @@ -121,6 +121,9 @@ export default { } return this.roles.includes('execute-query') }, + canViewQueries () { + return this.database.is_public || this.hasReadAccess + }, canCreateView () { if (!this.user || !this.isOwner) { return false diff --git a/dbrepo-ui/components/DatabaseList.vue b/dbrepo-ui/components/DatabaseList.vue index fc044ee2ff1321fdcb9e61aee930dbca0accd0eb..bdca982e74d2a9902c1fe37f35af6c39a3733441 100644 --- a/dbrepo-ui/components/DatabaseList.vue +++ b/dbrepo-ui/components/DatabaseList.vue @@ -1,13 +1,16 @@ <template> <div> <v-progress-linear v-if="loadingContainers || loadingDatabases" :indeterminate="!error" /> + <v-card v-if="!$vuetify.theme.dark && containers.length> 0" flat tile> + <v-divider class="mx-4" /> + </v-card> <v-card v-for="(container, idx) in containers" :key="idx" :to="link(container)" flat tile> - <v-divider v-if="!$vuetify.theme.dark" class="mx-4" /> + <v-divider v-if="idx !== 0" class="mx-4" /> <v-card-title v-if="!hasDatabase(container)" v-text="container.name" /> <v-card-title v-if="hasDatabase(container)"> <a :href="`/container/${container.id}/database/${container.database.id}`">{{ container.name }}</a> @@ -29,18 +32,8 @@ v-text="container.database.identifier.publisher" /> </div> <div v-text="identifierDescription(container)" /> - </v-card-text> - <v-card-text v-if="needsStart(container) || needsDatabase(container)" class="db-buttons"> - <v-btn - v-if="needsStart(container)" - small - secondary - :loading="container?.loading" - @click.stop="startContainer(container).then(() => createDatabase(container))"> - Start - </v-btn> <v-btn - v-else-if="needsDatabase(container)" + v-if="needsDatabase(container)" small secondary :loading="container?.loading" @@ -98,15 +91,6 @@ export default { formatCreators (container) { return ContainerMapper.containerToCreator(container) }, - needsStart (container) { - if (!this.user) { - return false - } - if (container.creator.username !== this.user.username) { - return false - } - return container.running === false - }, needsDatabase (container) { if (!this.user) { return false @@ -144,16 +128,6 @@ export default { }) this.loadingContainers = false }, - startContainer (container) { - container.loading = true - return new Promise((resolve, reject) => { - ContainerService.modify(container.id, 'start') - .then(() => resolve()) - .finally(() => { - container.loading = false - }) - }) - }, createDatabase (container) { container.loading = true DatabaseService.create(container.id, { name: container.name, is_public: true }) diff --git a/dbrepo-ui/components/QueryList.vue b/dbrepo-ui/components/QueryList.vue index 1bead29f290f43e95e83cb3a37ce4b35180558fd..4485e3494518a7b1e0f23b4053025358d8edbcd1 100644 --- a/dbrepo-ui/components/QueryList.vue +++ b/dbrepo-ui/components/QueryList.vue @@ -1,11 +1,16 @@ <template> <div> - <v-progress-linear v-if="loadingIdentifiers || loadingQueries || error" :color="loadingColor" :value="loadProgress" /> - <v-card v-if="!(loadingIdentifiers || loadingQueries) && queries && queries.length === 0" flat> + <v-progress-linear v-if="loadingIdentifiers || loadingQueries || error" :color="loadingColor" :indeterminate="!error" /> + <v-card v-if="!error && !(loadingIdentifiers || loadingQueries) && queries && queries.length === 0" flat tile> <v-card-text> (no subsets) </v-card-text> </v-card> + <v-card v-if="error" flat tile> + <v-card-text> + Failed to load queries: database is not reachable + </v-card-text> + </v-card> <v-tabs-items> <div v-if="!loadingQueries && !error"> <div v-for="(item,i) in queries" :key="i"> @@ -77,7 +82,6 @@ export default { return { loadingQueries: false, loadingIdentifiers: false, - loadProgress: 0, error: false, queries: [], identifiers: [] @@ -129,7 +133,6 @@ export default { mounted () { this.loadQueries() this.loadIdentifiers() - this.simulateProgress() }, methods: { loadIdentifiers () { @@ -148,6 +151,9 @@ export default { .then((queries) => { this.queries = queries }) + .catch(() => { + this.error = true + }) .finally(() => { this.loadingQueries = false }) @@ -181,20 +187,6 @@ export default { return 'primary--text' } return null - }, - simulateProgress () { - if (this.loadProgress !== 0) { - return - } - const timeout = 30 * 1000 /* ms */ - const ticks = 100 /* ms */ - let i = 0 - setInterval(() => { - if (i++ >= timeout && !this.error) { - return - } - this.loadProgress = ((i * 100) / timeout) * 100 - }, ticks) } } } diff --git a/dbrepo-ui/components/TableToolbar.vue b/dbrepo-ui/components/TableToolbar.vue index b3f22595c99e203c8b9309e3b9cb29aa998d3f0b..69351b3121ea40023089d48934d99fa77ad74d9e 100644 --- a/dbrepo-ui/components/TableToolbar.vue +++ b/dbrepo-ui/components/TableToolbar.vue @@ -108,12 +108,18 @@ export default { return UserUtils.hasWriteAccess(this.access) && this.roles.includes('insert-table-data') }, canEditTuple () { + if (this.selection === null || this.selection.length !== 1) { + return false + } if (!this.roles || !this.isDataTab) { return false } return UserUtils.hasWriteAccess(this.access) && this.roles.includes('insert-table-data') }, canDeleteTuple () { + if (this.selection === null || this.selection.length < 1) { + return false + } if (!this.roles || !this.isDataTab) { return false } @@ -199,7 +205,7 @@ export default { }) TableService.deleteTuple(this.$route.params.container_id, this.$route.params.database_id, this.$route.params.table_id, { keys: constraints }) .then(() => { - this.$toast.success(`Deleted ${this.selection.length} rows(s)`) + this.$toast.success(`Deleted ${this.selection.length} row${this.selection.length !== 1 ? 's' : ''}`) this.$emit('modified', { success: true, action: 'delete' }) }) } diff --git a/dbrepo-ui/components/UserToolbar.vue b/dbrepo-ui/components/UserToolbar.vue index 462b4384962ad83edd2297cc7303df6152b5776e..878a48085952cbb9774cba87e61aad394b419bfe 100644 --- a/dbrepo-ui/components/UserToolbar.vue +++ b/dbrepo-ui/components/UserToolbar.vue @@ -12,6 +12,9 @@ <v-tab to="/user/authentication"> Authentication </v-tab> + <v-tab v-if="canHandleMessages" to="/user/developer"> + Developer + </v-tab> </v-tabs> </div> </template> @@ -23,6 +26,29 @@ export default { return { tab: null } + }, + computed: { + user () { + return this.$store.state.user + }, + roles () { + return this.$store.state.roles + }, + canCreateMessage () { + if (!this.roles) { + return false + } + return this.roles.includes('create-maintenance-message') + }, + canModifyMessage () { + if (!this.roles) { + return false + } + return this.roles.includes('modify-maintenance-message') + }, + canHandleMessages () { + return this.canCreateMessage || this.canModifyMessage + } } } </script> diff --git a/dbrepo-ui/components/dialogs/EditMaintenanceMessage.vue b/dbrepo-ui/components/dialogs/EditMaintenanceMessage.vue new file mode 100644 index 0000000000000000000000000000000000000000..f3090c1e139dfc4dd0768afd59e4f78150c9687e --- /dev/null +++ b/dbrepo-ui/components/dialogs/EditMaintenanceMessage.vue @@ -0,0 +1,222 @@ +<template> + <div> + <v-form ref="form" v-model="valid" autocomplete="off" @submit.prevent="submit"> + <v-card> + <v-card-title v-text="title" /> + <v-card-text> + <v-row dense> + <v-col> + <v-select + v-model="localMessage.type" + :items="types" + item-text="name" + item-value="value" + :rules="[v => !!v || $t('Required')]" + required + label="Type *" /> + </v-col> + </v-row> + <v-row dense> + <v-col> + <v-text-field + v-model="localMessage.message" + :rules="[v => !!v || $t('Required')]" + required + label="Message *" /> + </v-col> + </v-row> + <v-row dense> + <v-col cols="6"> + <v-text-field + v-model="localMessage.display_start" + clearable + hint="YYYY-MM-dd HH:mm:ss" + label="Start timestamp" /> + </v-col> + <v-col cols="6"> + <v-text-field + v-model="localMessage.display_end" + clearable + hint="YYYY-MM-dd HH:mm:ss" + label="End timestamp" /> + </v-col> + </v-row> + </v-card-text> + <v-card-actions> + <v-btn + v-if="isModification" + class="ml-2" + color="error" + @click="deleteMessage"> + Delete + </v-btn> + <v-spacer /> + <v-btn + class="mb-2" + @click="cancel"> + Cancel + </v-btn> + <v-btn + id="database" + class="mb-2 ml-3 mr-2" + :disabled="!valid || loading" + :color="buttonColor" + type="submit" + :loading="loading" + @click="submitButton"> + {{ buttonText }} + </v-btn> + </v-card-actions> + </v-card> + </v-form> + </div> +</template> + +<script> +import MetadataService from '@/api/metadata.service' +import { timestampToTimeZonedTimestamp, formatTimestampUTC } from '@/utils' + +export default { + props: { + id: { + type: Number, + default () { + return null + } + } + }, + data () { + return { + valid: false, + loading: false, + error: false, + types: [ + { name: 'Error', value: 'error' }, + { name: 'Warning', value: 'warning' }, + { name: 'Info', value: 'info' } + ], + localMessage: { + type: null, + message: null, + display_start: null, + display_end: null + }, + modify: { + username: null, + type: null + } + } + }, + computed: { + database () { + return this.$store.state.database + }, + title () { + return (!this.isModification ? 'Create' : 'Modify') + ' maintenance message' + }, + buttonColor () { + if (this.modify.type && this.modify.type === 'revoke') { + return 'error' + } + return 'secondary' + }, + isModification () { + return this.id !== null + }, + buttonText () { + return (this.isModification ? 'Modify' : 'Create') + ' message' + } + }, + watch: { + id () { + this.init() + } + }, + mounted () { + this.init() + }, + methods: { + submit () { + this.$refs.form.validate() + }, + cancel () { + this.$emit('close-dialog', { success: false }) + }, + init () { + if (!this.id) { + this.localMessage = { + type: null, + message: null, + display_start: null, + display_end: null + } + } else { + this.loadMessage(this.id) + } + }, + loadMessage (id) { + MetadataService.findMessage(id) + .then((message) => { + message.display_start = formatTimestampUTC(message.display_start) + message.display_end = formatTimestampUTC(message.display_end) + this.localMessage = message + }) + }, + submitButton () { + if (this.isModification) { + this.updateMessage() + } else { + this.createMessage() + } + }, + createMessage () { + this.loading = true + const payload = Object.assign({}, this.localMessage) + if (payload.display_start) { + payload.display_start = timestampToTimeZonedTimestamp(payload.display_start) + } + if (payload.display_end) { + payload.display_end = timestampToTimeZonedTimestamp(payload.display_end) + } + MetadataService.createMessage(payload) + .then(() => { + this.$emit('close-dialog', { success: true }) + this.$emit('reload-messages', { success: true }) + }) + .finally(() => { + this.loading = false + }) + }, + updateMessage () { + this.loading = true + const payload = Object.assign({}, this.localMessage) + delete payload.id + if (payload.display_start) { + payload.display_start = timestampToTimeZonedTimestamp(payload.display_start) + } + if (payload.display_end) { + payload.display_end = timestampToTimeZonedTimestamp(payload.display_end) + } + MetadataService.updateMessage(this.localMessage.id, payload) + .then(() => { + this.$emit('close-dialog', { success: true }) + this.$emit('reload-messages', { success: true }) + }) + .finally(() => { + this.loading = false + }) + }, + deleteMessage () { + this.loading = true + MetadataService.deleteMessage(this.localMessage.id) + .then(() => { + this.$emit('close-dialog', { success: true }) + this.$emit('reload-messages', { success: true }) + }) + .finally(() => { + this.loading = false + }) + } + } +} +</script> diff --git a/dbrepo-ui/layouts/default.vue b/dbrepo-ui/layouts/default.vue index 8f9358cb23445b85c536ecd2a37662f3f8e0bbae..198c87bb14fa6e6109dc256a354448028a93a785 100644 --- a/dbrepo-ui/layouts/default.vue +++ b/dbrepo-ui/layouts/default.vue @@ -39,6 +39,16 @@ </v-list-item-content> </v-list-item> </v-list> + <div id="messages"> + <v-alert + v-for="(message, idx) in messages" + :key="idx" + class="banner" + border="left" + tile + :type="message.type" + v-text="message.messages" /> + </div> </v-navigation-drawer> <v-form ref="form" @submit.prevent="submit"> <v-app-bar fixed app> @@ -70,8 +80,8 @@ {{ $t('layout.signup', { name: 'vue-i18n' }) }} </v-btn> </div> - <div> - <v-btn v-if="user" to="/user" plain> + <div v-if="user"> + <v-btn to="/user" plain> {{ user.username }} </v-btn> <v-menu bottom offset-y left> @@ -105,19 +115,6 @@ <nuxt /> </v-container> </v-main> - <v-footer - v-if="sandbox" - padless> - <v-card - flat - tile - width="100%" - class="banner text-center"> - <v-card-text class="black--text"> - This is a <strong>TEST</strong> environment, do not use production/confidential data! — <a href="//github.com/fair-data-austria/dbrepo/issues/new" class="black--text">Report a bug</a> - </v-card-text> - </v-card> - </v-footer> </v-app> </template> @@ -160,6 +157,9 @@ export default { locale () { return this.$store.state.locale }, + messages () { + return this.$store.state.messages + }, table () { return this.$store.state.table }, @@ -215,9 +215,7 @@ export default { } }, mounted () { - if (this.refreshToken) { - AuthenticationService.authenticateToken(this.refreshToken) - } + this.$store.dispatch('reloadMessages') if (this.locale) { this.$i18n.locale = this.locale } @@ -302,6 +300,16 @@ export default { } </script> <style> +#messages { + position: absolute; + left: 0; + right: 0; + bottom: 0; +} +.banner { + width: 100%; + margin: 8px 0 0 0; +} .search-result-title, .search-result-subtitle { overflow: hidden; diff --git a/dbrepo-ui/nuxt.config.js b/dbrepo-ui/nuxt.config.js index 9c06aff0c1b6dadcfebcf8354041a1a89ccb1562..9db2e6dc80c7f97dd3a17dca49d279c9bfc241a8 100644 --- a/dbrepo-ui/nuxt.config.js +++ b/dbrepo-ui/nuxt.config.js @@ -110,11 +110,10 @@ export default { primary: colors.blue.darken2, accent: colors.amber.darken3, secondary: colors.blueGrey.base, - info: colors.amber.lighten1, + info: colors.blue.lighten2, code: colors.grey.lighten4, warning: colors.orange.lighten2, error: colors.red.base /* is used by forms */, - banner: colors.red.lighten2, success: colors.teal.base }, dark: { diff --git a/dbrepo-ui/pages/container/_container_id/database/_database_id/info.vue b/dbrepo-ui/pages/container/_container_id/database/_database_id/info.vue index 36e9bcea2483b8cc10c8a5456b6be0242c8d154c..3abeb30d7c7cc34d070b973cddabe068e015a335 100644 --- a/dbrepo-ui/pages/container/_container_id/database/_database_id/info.vue +++ b/dbrepo-ui/pages/container/_container_id/database/_database_id/info.vue @@ -213,9 +213,43 @@ <v-skeleton-loader v-if="loading" type="text" class="skeleton-small" /> <span v-if="!loading" v-text="container_internal_name" /> </v-list-item-content> + <v-list-item-title class="mt-2"> + Container IP + </v-list-item-title> + <v-list-item-content> + <v-skeleton-loader v-if="loading" type="text" class="skeleton-small" /> + <span v-if="!loading" v-text="container_ip" /> + </v-list-item-content> + <v-list-item-title class="mt-2"> + Container State + </v-list-item-title> + <v-list-item-content> + <v-skeleton-loader v-if="loading" type="text" class="skeleton-small" /> + <span v-if="!loading" v-text="container_state" /> + </v-list-item-content> </v-list-item-content> </v-list-item> </v-list> + <v-card-actions> + <v-btn + v-if="canStartContainer && needsStart" + small + secondary + color="secondary" + :loading="loadingStart" + @click.stop="startContainer"> + Start Container + </v-btn> + <v-btn + v-if="canStopContainer && !needsStart" + small + secondary + color="error" + :loading="loadingStop" + @click.stop="stopContainer"> + Stop Container + </v-btn> + </v-card-actions> </v-card-text> </v-card> </v-tab-item> @@ -245,6 +279,7 @@ import { formatTimestampUTCLabel } from '@/utils' import Banner from '@/components/identifier/Banner' import DatabaseMapper from '@/api/database.mapper' import DeleteIdentifier from '@/components/dialogs/DeleteIdentifier.vue' +import ContainerService from '@/api/container.service' export default { components: { @@ -259,6 +294,8 @@ export default { return { loading: false, loadingDelete: false, + loadingStart: false, + loadingStop: false, editDialog: false, deleteDialog: false, persistDialog: false, @@ -330,6 +367,12 @@ export default { container_internal_name () { return this.database.container.internal_name }, + container_state () { + return this.database.container.state + }, + container_ip () { + return this.database.container.ip_address + }, showIdentifierCard () { if (this.hasIdentifier) { return true @@ -348,6 +391,21 @@ export default { } return this.roles.includes('create-identifier') && this.isOwner }, + canStartContainer () { + if (!this.roles) { + return false + } + if (this.roles.includes('modify-foreign-container-state')) { + return true + } + return this.roles.includes('modify-container-state') && this.isOwner + }, + canStopContainer () { + if (!this.roles) { + return false + } + return this.roles.includes('modify-foreign-container-state') + }, canEditIdentifier () { if (!this.roles || !this.hasIdentifier) { return false @@ -410,6 +468,9 @@ export default { return false } return this.database.owner.username === this.user.username + }, + needsStart () { + return !this.database.container.running } }, methods: { @@ -425,6 +486,30 @@ export default { await this.$store.dispatch('reloadDatabase') } this.deleteDialog = false + }, + startContainer () { + this.loadingStart = true + return new Promise(() => { + ContainerService.modify(this.database.container.id, 'start') + .then(() => { + this.$store.dispatch('reloadDatabase') + }) + .finally(() => { + this.loadingStart = false + }) + }) + }, + stopContainer () { + this.loadingStop = true + return new Promise(() => { + ContainerService.modify(this.database.container.id, 'stop') + .then(() => { + this.$store.dispatch('reloadDatabase') + }) + .finally(() => { + this.loadingStop = false + }) + }) } } } diff --git a/dbrepo-ui/pages/container/_container_id/database/_database_id/query/_query_id/index.vue b/dbrepo-ui/pages/container/_container_id/database/_database_id/query/_query_id/index.vue index dcbe33966032f5b0c9bbfe9062d54ef259da85a9..eddbcca9f421c42e9b633547e3555325b896ada1 100644 --- a/dbrepo-ui/pages/container/_container_id/database/_database_id/query/_query_id/index.vue +++ b/dbrepo-ui/pages/container/_container_id/database/_database_id/query/_query_id/index.vue @@ -25,7 +25,7 @@ </DownloadButton> </v-toolbar-title> </v-toolbar> - <v-card v-if="query && query.identifier" flat tile> + <v-card v-if="query.identifier" flat tile> <v-card-title>Identifier</v-card-title> <v-card-text> <v-list dense> @@ -38,7 +38,7 @@ Persistent Identifier </v-list-item-title> <v-list-item-content> - <Banner :identifier="query.identifier" /> + <Banner v-if="canPersistQuery" :identifier="query.identifier" /> </v-list-item-content> <v-list-item-title class="mt-2"> Title @@ -306,7 +306,7 @@ export default { return this.query.result_hash }, canPersistQuery () { - if (!this.query || this.query.is_persisted) { + if (this.loadingQuery || !this.query || this.query.is_persisted) { return false } return UserUtils.hasReadAccess(this.access) diff --git a/dbrepo-ui/pages/container/_container_id/database/_database_id/table/_table_id/data.vue b/dbrepo-ui/pages/container/_container_id/database/_database_id/table/_table_id/data.vue index dfd27fc763f40c273408f55263ea0ecd76647700..925403b70d519a406a8353fc407225bc663a9f40 100644 --- a/dbrepo-ui/pages/container/_container_id/database/_database_id/table/_table_id/data.vue +++ b/dbrepo-ui/pages/container/_container_id/database/_database_id/table/_table_id/data.vue @@ -23,8 +23,14 @@ </v-toolbar-title> </v-toolbar> <v-card tile> - <v-progress-linear v-if="loadingData > 0 || error" :value="loadProgress" :color="error ? 'error' : 'primary'" /> + <v-progress-linear v-if="loadingData > 0 || error" :indeterminate="!error" :color="loadingColor" /> + <v-card v-if="error" flat tile> + <v-card-text> + Failed to load table data: database is not reachable + </v-card-text> + </v-card> <v-data-table + v-if="!error" :headers="headers" :items="rows" :options.sync="options" @@ -53,7 +59,6 @@ export default { return { loading: true, loadingData: 0, - loadProgress: 0, editTupleDialog: false, total: -1, footerProps: { @@ -67,7 +72,8 @@ export default { version: null, lastReload: new Date(), tab: null, - error: false, // XXX: `error` is never changed + edit: false, + error: false, options: { page: 1, itemsPerPage: 10 @@ -85,7 +91,7 @@ export default { }, computed: { loadingColor () { - return this.error ? 'red lighten-2' : 'primary' + return this.error ? 'error' : 'primary' }, token () { return this.$store.state.token @@ -168,7 +174,6 @@ export default { }, mounted () { this.reload() - this.simulateProgress() this.loadProperties() }, methods: { @@ -277,6 +282,9 @@ export default { return row }) }) + .catch(() => { + this.error = true + }) .finally(() => { this.loadingData-- }) @@ -290,20 +298,6 @@ export default { .finally(() => { this.loadingData-- }) - }, - simulateProgress () { - if (this.loadProgress !== 0) { - return - } - const timeout = 30 * 1000 /* ms */ - const ticks = 100 /* ms */ - let i = 0 - setInterval(() => { - if (i++ >= timeout && !this.error) { - return - } - this.loadProgress = ((i * 100) / timeout) * 100 - }, ticks) } } } diff --git a/dbrepo-ui/pages/container/_container_id/database/_database_id/table/_table_id/info.vue b/dbrepo-ui/pages/container/_container_id/database/_database_id/table/_table_id/info.vue index 171c0e219e5d4d5e861ff06a8ed8e57c4faf2b4d..13fd4569163b0a6d08979ba84c2e660c654c80bd 100644 --- a/dbrepo-ui/pages/container/_container_id/database/_database_id/table/_table_id/info.vue +++ b/dbrepo-ui/pages/container/_container_id/database/_database_id/table/_table_id/info.vue @@ -83,10 +83,6 @@ </v-list-item-title> <v-list-item-content v-if="canWriteQueues" class="amqp-consumer"> <span v-text="`${consumersUp}/${consumersTotal}`" /> - <v-badge - class="ml-1" - :color="consumersState.color" - :content="consumersState.text" /> </v-list-item-content> </v-list-item-content> </v-list-item> @@ -154,15 +150,6 @@ export default { access () { return this.$store.state.access }, - consumersState () { - if (this.consumersTotal === 0 || this.consumersTotal - this.consumersUp > 0 || this.loadingConsumers) { - return { color: 'warning', text: 'pending' } - } - if (this.consumersTotal === 0) { - return { color: 'error', text: 'down' } - } - return { color: 'success', text: 'up' } - }, consumersTotal () { return this.consumers.length }, diff --git a/dbrepo-ui/pages/user/authentication.vue b/dbrepo-ui/pages/user/authentication.vue index 92af24111141ae469c878c4e6eb8df2132e550e2..877ae8ce165781981e83898c878df80318f55584 100644 --- a/dbrepo-ui/pages/user/authentication.vue +++ b/dbrepo-ui/pages/user/authentication.vue @@ -40,7 +40,6 @@ </v-btn> </v-col> </v-row> - <pre>{{ $refs.form3 }}</pre> </v-form> </v-card-text> </v-card> diff --git a/dbrepo-ui/pages/user/developer.vue b/dbrepo-ui/pages/user/developer.vue new file mode 100644 index 0000000000000000000000000000000000000000..7825e5624307bd69ddea0c086d411adf3afd5e22 --- /dev/null +++ b/dbrepo-ui/pages/user/developer.vue @@ -0,0 +1,133 @@ +<template> + <div v-if="canHandleMessages"> + <UserToolbar /> + <v-tabs-items v-model="tab"> + <v-tab-item> + <v-card flat tile> + <v-card-title>Maintenance Messages</v-card-title> + <v-data-table + :headers="headers" + :items="messages" + :loading="loadingMessages" + :items-per-page="10"> + <template v-slot:item.action="{ item }"> + <v-btn + x-small + @click="modifyMessage(item)"> + Modify + </v-btn> + </template> + </v-data-table> + <v-card-text> + <v-btn + small + color="secondary" + :disabled="!canCreateMessage" + @click="createMessage"> + Create Message + </v-btn> + </v-card-text> + </v-card> + </v-tab-item> + </v-tabs-items> + <v-dialog + v-model="dialog" + persistent + max-width="640"> + <EditMaintenanceMessage :id="messageId" @close-dialog="closeDialog" /> + </v-dialog> + </div> +</template> + +<script> +import UserToolbar from '@/components/UserToolbar' +import MetadataService from '@/api/metadata.service' +import EditMaintenanceMessage from '@/components/dialogs/EditMaintenanceMessage' +import { isActiveMessage } from '@/utils' + +export default { + components: { + UserToolbar, + EditMaintenanceMessage + }, + data () { + return { + tab: 0, + headers: [ + { text: 'Active', value: 'active' }, + { text: 'Type', value: 'type' }, + { text: 'Message', value: 'message' }, + { text: 'Action', value: 'action' } + ], + messages: [], + loadingMessages: false, + dialog: false, + messageId: null + } + }, + computed: { + token () { + return this.$store.state.token + }, + user () { + return this.$store.state.user + }, + roles () { + return this.$store.state.roles + }, + canCreateMessage () { + if (!this.roles) { + return false + } + return this.roles.includes('create-maintenance-message') + }, + canModifyMessage () { + if (!this.roles) { + return false + } + return this.roles.includes('modify-maintenance-message') + }, + canHandleMessages () { + return this.canCreateMessage || this.canModifyMessage + } + }, + mounted () { + this.loadMessages() + }, + methods: { + submit () { + }, + modifyMessage (message) { + this.messageId = message.id + this.dialog = true + }, + createMessage () { + this.messageId = null + this.dialog = true + }, + loadMessages () { + MetadataService.findAllMessages() + .then((messages) => { + this.messages = messages.map((message) => { + message.active = isActiveMessage(message) ? '● true' : 'false' + message.action = 'hello' + return message + }) + }) + .catch(() => { + this.loadingMessages = false + }) + .finally(() => { + this.loadingMessages = false + }) + }, + closeDialog (event) { + this.dialog = false + if (event.success) { + this.loadMessages() + this.$store.dispatch('reloadMessages') + } + } + } +} +</script> diff --git a/dbrepo-ui/store/index.js b/dbrepo-ui/store/index.js index e42e49e0d00ab0266b0b35aabab5c273a190146c..690814ed85e1e2528880a6a27a037f669f577136 100644 --- a/dbrepo-ui/store/index.js +++ b/dbrepo-ui/store/index.js @@ -3,6 +3,7 @@ import Vuex, { Store } from 'vuex' import UserService from '@/api/user.service' import DatabaseService from '@/api/database.service' import TableService from '@/api/table.service' +import MetadataService from '@/api/metadata.service' Vue.use(Vuex) @@ -16,7 +17,8 @@ const store = new Store({ database: null, table: null, access: null, - locale: null + locale: null, + messages: [] }, getters: { getToken: state => state.token, @@ -26,7 +28,8 @@ const store = new Store({ getDatabase: state => state.database, getTable: state => state.table, getAccess: state => state.access, - getLocale: state => state.locale + getLocale: state => state.locale, + getMessages: state => state.messages }, mutations: { SET_TOKEN (state, token) { @@ -52,6 +55,9 @@ const store = new Store({ }, SET_LOCALE (state, locale) { state.locale = locale + }, + SET_MESSAGES (state, messages) { + state.messages = messages } }, actions: { @@ -78,6 +84,12 @@ const store = new Store({ .then((table) => { commit('SET_TABLE', table) }) + }, + reloadMessages ({ state, commit }) { + MetadataService.findActiveMessages() + .then((messages) => { + commit('SET_MESSAGES', messages) + }) } } }) diff --git a/dbrepo-ui/utils/index.js b/dbrepo-ui/utils/index.js index 01ed03767c643e6b57c5bf64c5c0c62710c3781e..f7b9f553b55076aa9a5846993a4ffc3376386299 100644 --- a/dbrepo-ui/utils/index.js +++ b/dbrepo-ui/utils/index.js @@ -99,6 +99,32 @@ function isOrcid (orcid) { return orcid.charAt(orcid.length - 1) === checksum } +function isActiveMessage (message) { + if (!message) { + return false + } + if (message.display_start === null || message.display_end === null) { + return true + } + if (message.display_start === null || new Date(message.display_end) >= new Date()) { + return true + } + if (new Date(message.display_start) <= new Date() || new Date(message.display_end) >= new Date()) { + return true + } + if (new Date(message.display_start) <= new Date() || message.display_end === null) { + return true + } + return false +} + +function timestampToTimeZonedTimestamp (str) { + if (str === null) { + return null + } + return format(new Date(str), 'yyyy-MM-dd\'T\'HH:mm:ss.SSS\'Z\'') +} + module.exports = { notEmpty, formatTimestamp, @@ -109,5 +135,7 @@ module.exports = { formatYearUTC, formatMonthUTC, formatDayUTC, - isOrcid + isOrcid, + isActiveMessage, + timestampToTimeZonedTimestamp } diff --git a/dbrepo-user-service/rest-service/src/main/java/at/tuwien/endpoint/MaintenanceEndpoint.java b/dbrepo-user-service/rest-service/src/main/java/at/tuwien/endpoint/MaintenanceEndpoint.java new file mode 100644 index 0000000000000000000000000000000000000000..18fa41da61db5dc80d8870cb5bb2099ca5dbab81 --- /dev/null +++ b/dbrepo-user-service/rest-service/src/main/java/at/tuwien/endpoint/MaintenanceEndpoint.java @@ -0,0 +1,161 @@ +package at.tuwien.endpoint; + +import at.tuwien.api.maintenance.BannerMessageBriefDto; +import at.tuwien.api.maintenance.BannerMessageCreateDto; +import at.tuwien.api.maintenance.BannerMessageDto; +import at.tuwien.api.maintenance.BannerMessageUpdateDto; +import at.tuwien.api.user.UserBriefDto; +import at.tuwien.exception.BannerMessageNotFoundException; +import at.tuwien.mapper.BannerMessageMapper; +import at.tuwien.service.BannerMessageService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import lombok.extern.log4j.Log4j2; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@Log4j2 +@CrossOrigin(origins = "*") +@RestController +@RequestMapping("/api/maintenance") +public class MaintenanceEndpoint { + + private final BannerMessageMapper bannerMessageMapper; + private final BannerMessageService bannerMessageService; + + @Autowired + public MaintenanceEndpoint(BannerMessageMapper bannerMessageMapper, BannerMessageService bannerMessageService) { + this.bannerMessageMapper = bannerMessageMapper; + this.bannerMessageService = bannerMessageService; + } + + @GetMapping("/message") + @Operation(summary = "Find maintenance messages") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", + description = "List messages", + content = {@Content( + mediaType = "application/json", + schema = @Schema(implementation = BannerMessageDto[].class))}), + }) + public ResponseEntity<List<BannerMessageDto>> list() { + log.debug("endpoint list active maintenance messages"); + final List<BannerMessageDto> dtos = bannerMessageService.findAll() + .stream() + .map(bannerMessageMapper::bannerMessageToBannerMessageDto) + .toList(); + log.trace("list maintenance messages results in dtos {}", dtos); + return ResponseEntity.ok(dtos); + } + + @GetMapping("/message/{id}") + @Operation(summary = "Find one maintenance message") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", + description = "Get messages", + content = {@Content( + mediaType = "application/json", + schema = @Schema(implementation = BannerMessageDto.class))}), + }) + public ResponseEntity<BannerMessageDto> find(@NotNull @PathVariable("id") Long messageId) + throws BannerMessageNotFoundException { + log.debug("endpoint find one maintenance messages"); + final BannerMessageDto dto = bannerMessageMapper.bannerMessageToBannerMessageDto(bannerMessageService.find(messageId)); + log.trace("find one maintenance message results in dto {}", dto); + return ResponseEntity.ok(dto); + } + + @GetMapping("/message/active") + @Operation(summary = "Find active maintenance messages") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", + description = "List messages", + content = {@Content( + mediaType = "application/json", + schema = @Schema(implementation = BannerMessageBriefDto[].class))}), + }) + public ResponseEntity<List<BannerMessageBriefDto>> active() { + log.debug("endpoint list active maintenance messages"); + final List<BannerMessageBriefDto> dtos = bannerMessageService.getActive() + .stream() + .map(bannerMessageMapper::bannerMessageToBannerMessageBriefDto) + .toList(); + log.trace("list active maintenance messages results in dtos {}", dtos); + return ResponseEntity.ok(dtos); + } + + @PostMapping("/message") + @Operation(summary = "Create maintenance message") + @PreAuthorize("hasAuthority('create-maintenance-message')") + @ApiResponses(value = { + @ApiResponse(responseCode = "201", + description = "Created message", + content = {@Content( + mediaType = "application/json", + schema = @Schema(implementation = BannerMessageBriefDto.class))}), + }) + public ResponseEntity<BannerMessageDto> create(@Valid @RequestBody BannerMessageCreateDto data) { + log.debug("endpoint create maintenance message, data={}", data); + final BannerMessageDto dto = bannerMessageMapper.bannerMessageToBannerMessageDto(bannerMessageService.create(data)); + log.trace("create maintenance message results in dto {}", dto); + return ResponseEntity.status(HttpStatus.CREATED) + .body(dto); + } + + @PutMapping("/message/{id}") + @Operation(summary = "Update maintenance message") + @PreAuthorize("hasAuthority('update-maintenance-message')") + @ApiResponses(value = { + @ApiResponse(responseCode = "202", + description = "Updated message", + content = {@Content( + mediaType = "application/json", + schema = @Schema(implementation = BannerMessageBriefDto.class))}), + @ApiResponse(responseCode = "404", + description = "Could not find message", + content = {@Content( + mediaType = "application/json", + schema = @Schema(implementation = BannerMessageNotFoundException.class))}), + }) + public ResponseEntity<BannerMessageDto> update(@NotNull @PathVariable("id") Long messageId, + @Valid @RequestBody BannerMessageUpdateDto data) + throws BannerMessageNotFoundException { + log.debug("endpoint update maintenance message, messageId={}, data={}", messageId, data); + final BannerMessageDto dto = bannerMessageMapper.bannerMessageToBannerMessageDto(bannerMessageService.update(messageId, data)); + log.trace("update maintenance message results in dto {}", dto); + return ResponseEntity.status(HttpStatus.ACCEPTED) + .body(dto); + } + + @DeleteMapping("/message/{id}") + @Operation(summary = "Delete maintenance message") + @PreAuthorize("hasAuthority('delete-maintenance-message')") + @ApiResponses(value = { + @ApiResponse(responseCode = "202", + description = "Deleted message", + content = {@Content(mediaType = "application/json")}), + @ApiResponse(responseCode = "404", + description = "Could not find message", + content = {@Content( + mediaType = "application/json", + schema = @Schema(implementation = BannerMessageNotFoundException.class))}), + }) + public ResponseEntity<?> delete(@NotNull @PathVariable("id") Long messageId) + throws BannerMessageNotFoundException { + log.debug("endpoint delete maintenance message, messageId={}", messageId); + bannerMessageService.delete(messageId); + return ResponseEntity.status(HttpStatus.ACCEPTED) + .build(); + } + +} diff --git a/dbrepo-user-service/rest-service/src/test/java/at/tuwien/endpoint/MaintenanceEndpointUnitTest.java b/dbrepo-user-service/rest-service/src/test/java/at/tuwien/endpoint/MaintenanceEndpointUnitTest.java new file mode 100644 index 0000000000000000000000000000000000000000..36097258c132aa0525a1d5c180ca150fc9709d68 --- /dev/null +++ b/dbrepo-user-service/rest-service/src/test/java/at/tuwien/endpoint/MaintenanceEndpointUnitTest.java @@ -0,0 +1,352 @@ +package at.tuwien.endpoint; + +import at.tuwien.BaseUnitTest; +import at.tuwien.api.maintenance.BannerMessageBriefDto; +import at.tuwien.api.maintenance.BannerMessageCreateDto; +import at.tuwien.api.maintenance.BannerMessageDto; +import at.tuwien.api.maintenance.BannerMessageUpdateDto; +import at.tuwien.config.ReadyConfig; +import at.tuwien.entities.maintenance.BannerMessage; +import at.tuwien.exception.*; +import at.tuwien.service.BannerMessageService; +import lombok.extern.log4j.Log4j2; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.test.context.support.WithAnonymousUser; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +@Log4j2 +@EnableAutoConfiguration(exclude = RabbitAutoConfiguration.class) +@SpringBootTest +@ExtendWith(SpringExtension.class) +public class MaintenanceEndpointUnitTest extends BaseUnitTest { + + @MockBean + private ReadyConfig readyConfig; + + @MockBean + private BannerMessageService bannerMessageService; + + @Autowired + private MaintenanceEndpoint maintenanceEndpoint; + + @Test + @WithAnonymousUser + public void list_anonymous_succeeds() { + + /* test */ + list_generic(); + } + + @Test + @WithMockUser(username = USER_4_USERNAME) + public void list_noRole_succeeds() { + + /* test */ + list_generic(); + } + + @Test + @WithMockUser(username = USER_1_USERNAME, authorities = {"list-maintenance-messages"}) + public void list_hasRole_succeeds() { + + /* test */ + list_generic(); + } + + @Test + @WithAnonymousUser + public void find_anonymous_succeeds() throws BannerMessageNotFoundException { + + /* test */ + find_generic(BANNER_MESSAGE_1_ID, BANNER_MESSAGE_1); + } + + @Test + @WithMockUser(username = USER_4_USERNAME) + public void find_noRole_succeeds() throws BannerMessageNotFoundException { + + /* test */ + find_generic(BANNER_MESSAGE_1_ID, BANNER_MESSAGE_1); + } + + @Test + @WithMockUser(username = USER_1_USERNAME, authorities = {"find-maintenance-message"}) + public void find_hasRole_succeeds() throws BannerMessageNotFoundException { + + /* test */ + find_generic(BANNER_MESSAGE_1_ID, BANNER_MESSAGE_1); + } + + @Test + @WithMockUser(username = USER_1_USERNAME, authorities = {"find-maintenance-message"}) + public void find_hasRoleNotFound_fails() { + + /* test */ + assertThrows(BannerMessageNotFoundException.class, () -> { + find_generic(BANNER_MESSAGE_1_ID, null); + }); + } + + @Test + @WithAnonymousUser + public void active_anonymous_succeeds() { + + /* test */ + active_generic(); + } + + @Test + @WithMockUser(username = USER_4_USERNAME) + public void active_noRole_succeeds() { + + /* test */ + active_generic(); + } + + @Test + @WithMockUser(username = USER_1_USERNAME, authorities = {"list-maintenance-messages"}) + public void active_hasRole_succeeds() { + + /* test */ + active_generic(); + } + + @Test + @WithAnonymousUser + public void create_anonymous_fails() { + + /* test */ + assertThrows(AccessDeniedException.class, () -> { + create_generic(BANNER_MESSAGE_1_CREATE_DTO, BANNER_MESSAGE_1); + }); + } + + @Test + @WithMockUser(username = USER_4_USERNAME) + public void create_noRole_fails() { + + /* test */ + assertThrows(AccessDeniedException.class, () -> { + create_generic(BANNER_MESSAGE_1_CREATE_DTO, BANNER_MESSAGE_1); + }); + } + + @Test + @WithMockUser(username = USER_2_USERNAME, authorities = {"create-maintenance-message"}) + public void create_hasRole_succeeds() { + + /* test */ + create_generic(BANNER_MESSAGE_1_CREATE_DTO, BANNER_MESSAGE_1); + } + + @Test + @WithAnonymousUser + public void update_anonymous_fails() { + + /* test */ + assertThrows(AccessDeniedException.class, () -> { + update_generic(BANNER_MESSAGE_1_UPDATE_DTO, BANNER_MESSAGE_1_ID, BANNER_MESSAGE_1); + }); + } + + @Test + @WithMockUser(username = USER_4_USERNAME) + public void update_noRole_fails() { + + /* test */ + assertThrows(AccessDeniedException.class, () -> { + update_generic(BANNER_MESSAGE_1_UPDATE_DTO, BANNER_MESSAGE_1_ID, BANNER_MESSAGE_1); + }); + } + + @Test + @WithMockUser(username = USER_2_USERNAME, authorities = {"update-maintenance-message"}) + public void update_hasRole_succeeds() throws BannerMessageNotFoundException { + + /* test */ + update_generic(BANNER_MESSAGE_1_UPDATE_DTO, BANNER_MESSAGE_1_ID, BANNER_MESSAGE_1); + } + + @Test + @WithMockUser(username = USER_2_USERNAME, authorities = {"update-maintenance-message"}) + public void update_hasRoleNotFound_fails() { + + /* test */ + assertThrows(BannerMessageNotFoundException.class, () -> { + update_generic(BANNER_MESSAGE_1_UPDATE_DTO, BANNER_MESSAGE_1_ID, null); + }); + } + + @Test + @WithAnonymousUser + public void delete_anonymous_fails() { + + /* test */ + assertThrows(AccessDeniedException.class, () -> { + delete_generic(BANNER_MESSAGE_1_ID, BANNER_MESSAGE_1); + }); + } + + @Test + @WithMockUser(username = USER_4_USERNAME) + public void delete_noRole_fails() { + + /* test */ + assertThrows(AccessDeniedException.class, () -> { + delete_generic(BANNER_MESSAGE_1_ID, BANNER_MESSAGE_1); + }); + } + + @Test + @WithMockUser(username = USER_2_USERNAME, authorities = {"delete-maintenance-message"}) + public void delete_hasRole_succeeds() throws BannerMessageNotFoundException { + + /* test */ + delete_generic(BANNER_MESSAGE_1_ID, BANNER_MESSAGE_1); + } + + @Test + @WithMockUser(username = USER_2_USERNAME, authorities = {"delete-maintenance-message"}) + public void delete_hasRoleNotFound_fails() { + + /* test */ + assertThrows(BannerMessageNotFoundException.class, () -> { + delete_generic(BANNER_MESSAGE_1_ID, null); + }); + } + + /* ################################################################################################### */ + /* ## GENERIC TEST CASES ## */ + /* ################################################################################################### */ + + protected void list_generic() { + + /* mock */ + when(bannerMessageService.findAll()) + .thenReturn(List.of(BANNER_MESSAGE_1, BANNER_MESSAGE_2)); + + /* test */ + final ResponseEntity<List<BannerMessageDto>> response = maintenanceEndpoint.list(); + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertNotNull(response.getBody()); + final List<BannerMessageDto> body = response.getBody(); + assertEquals(2, body.size()); + final BannerMessageDto message0 = body.get(0); + assertEquals(BANNER_MESSAGE_1_ID, message0.getId()); + assertEquals(BANNER_MESSAGE_1_TYPE_DTO, message0.getType()); + assertEquals(BANNER_MESSAGE_1_MESSAGE, message0.getMessage()); + final BannerMessageDto message1 = body.get(1); + assertEquals(BANNER_MESSAGE_2_ID, message1.getId()); + assertEquals(BANNER_MESSAGE_2_TYPE_DTO, message1.getType()); + assertEquals(BANNER_MESSAGE_2_MESSAGE, message1.getMessage()); + } + + protected void find_generic(Long messageId, BannerMessage message) throws BannerMessageNotFoundException { + + /* mock */ + if (message != null) { + when(bannerMessageService.find(messageId)) + .thenReturn(message); + } else { + doThrow(BannerMessageNotFoundException.class) + .when(bannerMessageService) + .find(messageId); + } + + /* test */ + final ResponseEntity<BannerMessageDto> response = maintenanceEndpoint.find(messageId); + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertNotNull(response.getBody()); + final BannerMessageDto body = response.getBody(); + assertEquals(BANNER_MESSAGE_1_ID, body.getId()); + assertEquals(BANNER_MESSAGE_1_MESSAGE, body.getMessage()); + assertEquals(BANNER_MESSAGE_1_TYPE_DTO, body.getType()); + assertEquals(BANNER_MESSAGE_1_START, body.getDisplayStart()); + assertEquals(BANNER_MESSAGE_1_END, body.getDisplayEnd()); + } + + protected void active_generic() { + + /* mock */ + when(bannerMessageService.getActive()) + .thenReturn(List.of(BANNER_MESSAGE_1)); + + /* test */ + final ResponseEntity<List<BannerMessageBriefDto>> response = maintenanceEndpoint.active(); + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertNotNull(response.getBody()); + final List<BannerMessageBriefDto> body = response.getBody(); + assertEquals(1, body.size()); + final BannerMessageBriefDto message0 = body.get(0); + assertEquals(BANNER_MESSAGE_1_TYPE_DTO, message0.getType()); + assertEquals(BANNER_MESSAGE_1_MESSAGE, message0.getMessage()); + } + + protected void create_generic(BannerMessageCreateDto data, BannerMessage message) { + + /* mock */ + when(bannerMessageService.create(data)) + .thenReturn(message); + + /* test */ + final ResponseEntity<BannerMessageDto> response = maintenanceEndpoint.create(data); + assertEquals(HttpStatus.CREATED, response.getStatusCode()); + assertNotNull(response.getBody()); + } + + protected void update_generic(BannerMessageUpdateDto data, Long messageId, BannerMessage message) + throws BannerMessageNotFoundException { + + /* mock */ + if (message != null) { + when(bannerMessageService.update(messageId, data)) + .thenReturn(message); + } else { + doThrow(BannerMessageNotFoundException.class) + .when(bannerMessageService) + .update(messageId, data); + } + + /* test */ + final ResponseEntity<BannerMessageDto> response = maintenanceEndpoint.update(messageId, data); + assertEquals(HttpStatus.ACCEPTED, response.getStatusCode()); + assertNotNull(response.getBody()); + } + + protected void delete_generic(Long messageId, BannerMessage message) + throws BannerMessageNotFoundException { + + /* mock */ + if (message != null) { + when(bannerMessageService.find(messageId)) + .thenReturn(message); + doNothing() + .when(bannerMessageService) + .delete(messageId); + } else { + doThrow(BannerMessageNotFoundException.class) + .when(bannerMessageService) + .delete(messageId); + } + + /* test */ + final ResponseEntity<?> response = maintenanceEndpoint.delete(messageId); + assertEquals(HttpStatus.ACCEPTED, response.getStatusCode()); + assertNull(response.getBody()); + } +} diff --git a/dbrepo-user-service/rest-service/src/test/java/at/tuwien/service/BannerMessageServiceIntegrationTest.java b/dbrepo-user-service/rest-service/src/test/java/at/tuwien/service/BannerMessageServiceIntegrationTest.java new file mode 100644 index 0000000000000000000000000000000000000000..8759d8e3c684e7df97c79adcfa756e3b1a220f14 --- /dev/null +++ b/dbrepo-user-service/rest-service/src/test/java/at/tuwien/service/BannerMessageServiceIntegrationTest.java @@ -0,0 +1,141 @@ +package at.tuwien.service; + +import at.tuwien.BaseUnitTest; +import at.tuwien.api.maintenance.BannerMessageCreateDto; +import at.tuwien.api.maintenance.BannerMessageTypeDto; +import at.tuwien.api.maintenance.BannerMessageUpdateDto; +import at.tuwien.entities.maintenance.BannerMessage; +import at.tuwien.entities.maintenance.BannerMessageType; +import at.tuwien.entities.user.Realm; +import at.tuwien.exception.BannerMessageNotFoundException; +import at.tuwien.exception.RealmNotFoundException; +import at.tuwien.repository.jpa.BannerMessageRepository; +import at.tuwien.repository.jpa.RealmRepository; +import lombok.extern.log4j.Log4j2; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +@Log4j2 +@EnableAutoConfiguration(exclude = RabbitAutoConfiguration.class) +@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD) +@SpringBootTest +@ExtendWith(SpringExtension.class) +public class BannerMessageServiceIntegrationTest extends BaseUnitTest { + + @Autowired + private BannerMessageRepository bannerMessageRepository; + + @Autowired + private BannerMessageService bannerMessageService; + + @BeforeEach + public void beforeEach() { + bannerMessageRepository.save(BANNER_MESSAGE_1); + bannerMessageRepository.save(BANNER_MESSAGE_2); + } + + @Test + public void findAll_succeeds() { + + /* test */ + final List<BannerMessage> response = bannerMessageService.findAll(); + assertEquals(2, response.size()); + } + + @Test + public void getActive_succeeds() { + + /* test */ + final List<BannerMessage> response = bannerMessageService.getActive(); + assertEquals(1, response.size()); + final BannerMessage message0 = response.get(0); + assertEquals(BANNER_MESSAGE_1_ID, message0.getId()); + assertEquals(BANNER_MESSAGE_1_MESSAGE, message0.getMessage()); + assertEquals(BANNER_MESSAGE_1_TYPE, message0.getType()); + } + + @Test + public void find_succeeds() throws BannerMessageNotFoundException { + + /* test */ + final BannerMessage response = bannerMessageService.find(BANNER_MESSAGE_1_ID); + assertEquals(BANNER_MESSAGE_1_ID, response.getId()); + assertEquals(BANNER_MESSAGE_1_MESSAGE, response.getMessage()); + assertEquals(BANNER_MESSAGE_1_TYPE, response.getType()); + } + + @Test + public void find_notFound_fails() { + + /* test */ + assertThrows(BannerMessageNotFoundException.class, () -> { + bannerMessageService.find(9999L); + }); + } + + @Test + public void create_succeeds() { + final BannerMessageCreateDto request = BannerMessageCreateDto.builder() + .message("test") + .type(BannerMessageTypeDto.INFO) + .build(); + + /* test */ + final BannerMessage response = bannerMessageService.create(request); + assertEquals("test", response.getMessage()); + assertEquals(BannerMessageType.INFO, response.getType()); + } + + @Test + public void update_succeeds() throws BannerMessageNotFoundException { + final BannerMessageUpdateDto request = BannerMessageUpdateDto.builder() + .message("test") + .type(BannerMessageTypeDto.INFO) + .build(); + + /* test */ + final BannerMessage response = bannerMessageService.update(BANNER_MESSAGE_1_ID, request); + assertEquals("test", response.getMessage()); + assertEquals(BannerMessageType.INFO, response.getType()); + } + + @Test + public void update_notFound_fails() { + final BannerMessageUpdateDto request = BannerMessageUpdateDto.builder() + .message("test") + .type(BannerMessageTypeDto.INFO) + .build(); + + /* test */ + assertThrows(BannerMessageNotFoundException.class, () -> { + bannerMessageService.update(9999L, request); + }); + } + + @Test + public void delete_succeeds() throws BannerMessageNotFoundException { + + /* test */ + bannerMessageService.delete(BANNER_MESSAGE_1_ID); + } + + @Test + public void delete_notFound_fails() { + + /* test */ + assertThrows(BannerMessageNotFoundException.class, () -> { + bannerMessageService.delete(9999L); + }); + } +} diff --git a/dbrepo-user-service/services/src/main/java/at/tuwien/config/WebSecurityConfig.java b/dbrepo-user-service/services/src/main/java/at/tuwien/config/WebSecurityConfig.java index 3f1cf7c1ce8ee31ca780df5040443152d00c915b..fe44809a4407c9c4e8f3b9a9e89222894a3da6d2 100644 --- a/dbrepo-user-service/services/src/main/java/at/tuwien/config/WebSecurityConfig.java +++ b/dbrepo-user-service/services/src/main/java/at/tuwien/config/WebSecurityConfig.java @@ -43,6 +43,7 @@ public class WebSecurityConfig { final OrRequestMatcher publicEndpoints = new OrRequestMatcher( new AntPathRequestMatcher("/api/user/**", "GET"), new AntPathRequestMatcher("/api/user/**", "POST"), + new AntPathRequestMatcher("/api/maintenance/**", "GET"), new AntPathRequestMatcher("/v3/api-docs.yaml"), new AntPathRequestMatcher("/v3/api-docs/**"), new AntPathRequestMatcher("/swagger-ui/**"), diff --git a/dbrepo-user-service/services/src/main/java/at/tuwien/exception/BannerMessageNotFoundException.java b/dbrepo-user-service/services/src/main/java/at/tuwien/exception/BannerMessageNotFoundException.java new file mode 100644 index 0000000000000000000000000000000000000000..e4587b14539c329f409be25e5e67b2651771be88 --- /dev/null +++ b/dbrepo-user-service/services/src/main/java/at/tuwien/exception/BannerMessageNotFoundException.java @@ -0,0 +1,20 @@ +package at.tuwien.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(code = HttpStatus.NOT_FOUND) +public class BannerMessageNotFoundException extends Exception { + + public BannerMessageNotFoundException(String msg) { + super(msg); + } + + public BannerMessageNotFoundException(String msg, Throwable thr) { + super(msg, thr); + } + + public BannerMessageNotFoundException(Throwable thr) { + super(thr); + } +} diff --git a/dbrepo-user-service/services/src/main/java/at/tuwien/mapper/BannerMessageMapper.java b/dbrepo-user-service/services/src/main/java/at/tuwien/mapper/BannerMessageMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..8fd2b7dc51936768af6e2d9410887c15e6f19c1b --- /dev/null +++ b/dbrepo-user-service/services/src/main/java/at/tuwien/mapper/BannerMessageMapper.java @@ -0,0 +1,24 @@ +package at.tuwien.mapper; + +import at.tuwien.api.maintenance.BannerMessageBriefDto; +import at.tuwien.api.maintenance.BannerMessageCreateDto; +import at.tuwien.api.maintenance.BannerMessageDto; +import at.tuwien.api.maintenance.BannerMessageTypeDto; +import at.tuwien.entities.maintenance.BannerMessage; +import at.tuwien.entities.maintenance.BannerMessageType; +import org.mapstruct.Mapper; + +@Mapper(componentModel = "spring") +public interface BannerMessageMapper { + + org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(BannerMessageMapper.class); + + BannerMessageDto bannerMessageToBannerMessageDto(BannerMessage data); + + BannerMessageBriefDto bannerMessageToBannerMessageBriefDto(BannerMessage data); + + BannerMessage bannerMessageCreateDtoToBannerMessage(BannerMessageCreateDto data); + + BannerMessageType bannerMessageTypeDtoToBannerMessageType(BannerMessageTypeDto data); + +} diff --git a/dbrepo-user-service/services/src/main/java/at/tuwien/repository/jpa/BannerMessageRepository.java b/dbrepo-user-service/services/src/main/java/at/tuwien/repository/jpa/BannerMessageRepository.java new file mode 100644 index 0000000000000000000000000000000000000000..bcdbf9f83266e10f0fab4f59126ffa936bb9b537 --- /dev/null +++ b/dbrepo-user-service/services/src/main/java/at/tuwien/repository/jpa/BannerMessageRepository.java @@ -0,0 +1,14 @@ +package at.tuwien.repository.jpa; + +import at.tuwien.entities.maintenance.BannerMessage; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface BannerMessageRepository extends JpaRepository<BannerMessage, Long> { + + List<BannerMessage> findByActive(); + +} diff --git a/dbrepo-user-service/services/src/main/java/at/tuwien/service/BannerMessageService.java b/dbrepo-user-service/services/src/main/java/at/tuwien/service/BannerMessageService.java new file mode 100644 index 0000000000000000000000000000000000000000..a674fbbbdd494f9999bf1bcdcc92ff68e892a465 --- /dev/null +++ b/dbrepo-user-service/services/src/main/java/at/tuwien/service/BannerMessageService.java @@ -0,0 +1,60 @@ +package at.tuwien.service; + +import at.tuwien.api.maintenance.BannerMessageCreateDto; +import at.tuwien.api.maintenance.BannerMessageUpdateDto; +import at.tuwien.entities.maintenance.BannerMessage; +import at.tuwien.exception.BannerMessageNotFoundException; + +import java.util.List; + +public interface BannerMessageService { + + /** + * Finds all messages in the metadata database. + * + * @return List of messages. + */ + List<BannerMessage> findAll(); + + /** + * Finds all messages that are valid at the current point in time. + * + * @return List of active messages. + */ + List<BannerMessage> getActive(); + + /** + * Finds a specific message by given id in the metadata database. + * + * @param id The message id. + * @return The message, if successful. + * @throws BannerMessageNotFoundException The message was not found in the metadata database. + */ + BannerMessage find(Long id) throws BannerMessageNotFoundException; + + /** + * Creates a new maintenance message in the metadata database. + * + * @param data The message data. + * @return The created message, if successful. + */ + BannerMessage create(BannerMessageCreateDto data); + + /** + * Updates a maintenance message by given id in the metadata database. + * + * @param id The message id. + * @param data The updated message data. + * @return The updated message, if successful. + * @throws BannerMessageNotFoundException The message was not found in the metadata database. + */ + BannerMessage update(Long id, BannerMessageUpdateDto data) throws BannerMessageNotFoundException; + + /** + * Deletes a maintenance message by given id in the metadata database. + * + * @param id The message id. + * @throws BannerMessageNotFoundException The message was not found in the metadata database. + */ + void delete(Long id) throws BannerMessageNotFoundException; +} diff --git a/dbrepo-user-service/services/src/main/java/at/tuwien/service/impl/BannerMessageServiceImpl.java b/dbrepo-user-service/services/src/main/java/at/tuwien/service/impl/BannerMessageServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..5c38de25fcf98665ee2d3945a35fd2082837256d --- /dev/null +++ b/dbrepo-user-service/services/src/main/java/at/tuwien/service/impl/BannerMessageServiceImpl.java @@ -0,0 +1,78 @@ +package at.tuwien.service.impl; + +import at.tuwien.api.maintenance.BannerMessageCreateDto; +import at.tuwien.api.maintenance.BannerMessageUpdateDto; +import at.tuwien.entities.maintenance.BannerMessage; +import at.tuwien.exception.BannerMessageNotFoundException; +import at.tuwien.mapper.BannerMessageMapper; +import at.tuwien.repository.jpa.BannerMessageRepository; +import at.tuwien.service.BannerMessageService; +import lombok.extern.log4j.Log4j2; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Optional; + +@Log4j2 +@Service +public class BannerMessageServiceImpl implements BannerMessageService { + + private final BannerMessageMapper bannerMessageMapper; + private final BannerMessageRepository bannerMessageRepository; + + @Autowired + public BannerMessageServiceImpl(BannerMessageMapper bannerMessageMapper, + BannerMessageRepository bannerMessageRepository) { + this.bannerMessageMapper = bannerMessageMapper; + this.bannerMessageRepository = bannerMessageRepository; + } + + @Override + public List<BannerMessage> findAll() { + return bannerMessageRepository.findAll(); + } + + @Override + public List<BannerMessage> getActive() { + return bannerMessageRepository.findByActive(); + } + + @Override + public BannerMessage find(Long id) throws BannerMessageNotFoundException { + final Optional<BannerMessage> optional = bannerMessageRepository.findById(id); + if (optional.isEmpty()) { + log.error("Failed to find banner message with id {}", id); + throw new BannerMessageNotFoundException("Failed to find banner message with id " + id); + } + return optional.get(); + } + + @Override + public BannerMessage create(BannerMessageCreateDto data) { + final BannerMessage entity = bannerMessageMapper.bannerMessageCreateDtoToBannerMessage(data); + final BannerMessage message = bannerMessageRepository.save(entity); + log.info("Created banner message with id {}", message.getId()); + return message; + } + + @Override + public BannerMessage update(Long id, BannerMessageUpdateDto data) throws BannerMessageNotFoundException { + final BannerMessage entity = find(id); + entity.setMessage(data.getMessage()); + entity.setDisplayEnd(data.getDisplayEnd()); + entity.setDisplayStart(data.getDisplayStart()); + entity.setType(bannerMessageMapper.bannerMessageTypeDtoToBannerMessageType(data.getType())); + final BannerMessage message = bannerMessageRepository.save(entity); + log.info("Updated banner message with id {}", message.getId()); + return message; + } + + @Override + public void delete(Long id) throws BannerMessageNotFoundException { + find(id); + bannerMessageRepository.deleteById(id); + log.info("Deleted banner message with id {}", id); + } + +} diff --git a/dbrepo.conf b/dbrepo.conf index e972bfcd581b9c95d82ac18eb99870a197df5a56..4220e14b6820ec93a8f500ac35bb57cd600ee411 100644 --- a/dbrepo.conf +++ b/dbrepo.conf @@ -105,6 +105,15 @@ server { proxy_read_timeout 90; } + location /api/maintenance { + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_pass http://user; + proxy_read_timeout 90; + } + location /api/identifier { proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr;