diff --git a/fda-authentication-service/Dockerfile b/fda-authentication-service/Dockerfile index 2dd80a74938d54c01ba65c954ab80a4179913caf..3cbf6b92959593544bda157bb14f481eca5aadbb 100644 --- a/fda-authentication-service/Dockerfile +++ b/fda-authentication-service/Dockerfile @@ -43,7 +43,7 @@ ENV METADATA_USERNAME=root ENV METADATA_PASSWORD=dbrepo ENV KC_DB=mariadb -ENV KC_DB_URL=jdbc:mariadb://fda-metadata-db/keycloak +ENV KC_DB_URL=jdbc:mariadb://fda-metadata-db/fda ENV KC_DB_USERNAME=${METADATA_USERNAME} ENV KC_DB_PASSWORD=${METADATA_PASSWORD} ENV KC_HOSTNAME=localhost diff --git a/fda-authentication-service/dbrepo-realm.json b/fda-authentication-service/dbrepo-realm.json index 461d2cb8a773ca071f1c0e2308e7de1d7225573c..628fe47aa796139e344bd53abb143bad2aad49dd 100644 --- a/fda-authentication-service/dbrepo-realm.json +++ b/fda-authentication-service/dbrepo-realm.json @@ -117,6 +117,17 @@ "clientRole" : false, "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", "attributes" : { } + }, { + "id" : "3a801b48-f3c2-4bc6-aa25-c7a91d5b32a7", + "name" : "default-researcher-roles", + "description" : "${default-researcher-roles}", + "composite" : true, + "composites" : { + "realm" : [ "default-table-handling", "default-container-handling", "default-query-handling", "default-database-handling", "default-identifier-handling" ] + }, + "clientRole" : false, + "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", + "attributes" : { } }, { "id" : "3d8104fb-8307-40f0-b4b2-c3e518957110", "name" : "view-table-data", @@ -249,7 +260,7 @@ "description" : "${role_default-roles}", "composite" : true, "composites" : { - "realm" : [ "default-table-handling", "default-roles-dbrepo", "default-container-handling", "default-query-handling", "offline_access", "default-database-handling", "uma_authorization", "default-identifier-handling" ] + "realm" : [ "default-researcher-roles" ] }, "clientRole" : false, "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", @@ -831,7 +842,7 @@ "otpPolicyLookAheadWindow" : 1, "otpPolicyPeriod" : 30, "otpPolicyCodeReusable" : false, - "otpSupportedApplications" : [ "totpAppGoogleName", "totpAppMicrosoftAuthenticatorName", "totpAppFreeOTPName" ], + "otpSupportedApplications" : [ "totpAppMicrosoftAuthenticatorName", "totpAppGoogleName", "totpAppFreeOTPName" ], "webAuthnPolicyRpEntityName" : "keycloak", "webAuthnPolicySignatureAlgorithms" : [ "ES256" ], "webAuthnPolicyRpId" : "", @@ -1727,7 +1738,7 @@ "subType" : "authenticated", "subComponents" : { }, "config" : { - "allowed-protocol-mapper-types" : [ "oidc-usermodel-attribute-mapper", "oidc-address-mapper", "saml-role-list-mapper", "oidc-usermodel-property-mapper", "oidc-full-name-mapper", "saml-user-property-mapper", "oidc-sha256-pairwise-sub-mapper", "saml-user-attribute-mapper" ] + "allowed-protocol-mapper-types" : [ "oidc-usermodel-attribute-mapper", "oidc-usermodel-property-mapper", "saml-user-attribute-mapper", "oidc-sha256-pairwise-sub-mapper", "saml-role-list-mapper", "saml-user-property-mapper", "oidc-address-mapper", "oidc-full-name-mapper" ] } }, { "id" : "3ab11d74-5e76-408a-b85a-26bf8950f979", @@ -1736,7 +1747,7 @@ "subType" : "anonymous", "subComponents" : { }, "config" : { - "allowed-protocol-mapper-types" : [ "saml-user-attribute-mapper", "saml-user-property-mapper", "oidc-usermodel-property-mapper", "saml-role-list-mapper", "oidc-usermodel-attribute-mapper", "oidc-full-name-mapper", "oidc-sha256-pairwise-sub-mapper", "oidc-address-mapper" ] + "allowed-protocol-mapper-types" : [ "oidc-address-mapper", "saml-user-attribute-mapper", "saml-user-property-mapper", "oidc-usermodel-property-mapper", "saml-role-list-mapper", "oidc-usermodel-attribute-mapper", "oidc-sha256-pairwise-sub-mapper", "oidc-full-name-mapper" ] } } ], "org.keycloak.keys.KeyProvider" : [ { @@ -1788,7 +1799,7 @@ "internationalizationEnabled" : false, "supportedLocales" : [ ], "authenticationFlows" : [ { - "id" : "c5a3dd66-04ab-412f-a687-a86750997514", + "id" : "8dfb9b83-2bd4-4c87-871b-f29b56992215", "alias" : "Account verification options", "description" : "Method with which to verity the existing account", "providerId" : "basic-flow", @@ -1810,7 +1821,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "77db7a9d-32ea-46ea-965c-7543585f0a8a", + "id" : "b88cbaec-5a8d-4425-bcef-19a6344e9276", "alias" : "Authentication Options", "description" : "Authentication options.", "providerId" : "basic-flow", @@ -1839,7 +1850,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "d728bf4f-c26a-4a2f-8968-7bd190abedfe", + "id" : "77defea0-f083-4103-ab7b-a7d9165d7297", "alias" : "Browser - Conditional OTP", "description" : "Flow to determine if the OTP is required for the authentication", "providerId" : "basic-flow", @@ -1861,7 +1872,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "a196acb5-833c-4a34-83c7-c37a45cb144f", + "id" : "7ff9f69f-527e-4f75-bdab-ed2b142b1cba", "alias" : "Direct Grant - Conditional OTP", "description" : "Flow to determine if the OTP is required for the authentication", "providerId" : "basic-flow", @@ -1883,7 +1894,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "73bb1748-899a-4160-b66b-d587f88ca23f", + "id" : "058c5fb2-3e97-4d22-aa5a-e3376bc72df4", "alias" : "First broker login - Conditional OTP", "description" : "Flow to determine if the OTP is required for the authentication", "providerId" : "basic-flow", @@ -1905,7 +1916,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "e4fd72f0-e9c6-4100-99d8-fa3dc6f475c0", + "id" : "3d152b4b-0598-45ce-9b06-7e576deeefc4", "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", @@ -1927,7 +1938,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "cfae0db9-63b8-43b3-9e35-a5980edcfb92", + "id" : "08eab043-42e9-41bf-a9ec-adb00027c557", "alias" : "Reset - Conditional OTP", "description" : "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.", "providerId" : "basic-flow", @@ -1949,7 +1960,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "623d89f1-4fa7-4d75-a7b0-92aea5d39c9e", + "id" : "31b88f44-e960-48d3-af15-00aed87e0d0f", "alias" : "User creation or linking", "description" : "Flow for the existing/non-existing user alternatives", "providerId" : "basic-flow", @@ -1972,7 +1983,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "c9977d2b-dfa5-4d71-9822-953d9b25a6b6", + "id" : "454e1da2-eb82-439a-9cac-9e77a42ca69b", "alias" : "Verify Existing Account by Re-authentication", "description" : "Reauthentication of existing account", "providerId" : "basic-flow", @@ -1994,7 +2005,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "6d3c2734-7f38-4d56-afd5-de813f0b7557", + "id" : "2eb0ccb2-9ef4-4829-aba8-597ec6bf6437", "alias" : "browser", "description" : "browser based authentication", "providerId" : "basic-flow", @@ -2030,7 +2041,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "ba8e84c8-cc24-4830-b7d0-ab8974d3877e", + "id" : "9c9c98b8-6e60-45cb-96c1-8feb6d63a08f", "alias" : "clients", "description" : "Base authentication for clients", "providerId" : "client-flow", @@ -2066,7 +2077,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "7e5174e7-c796-47e5-878d-c9843e79b20d", + "id" : "cfcdd3c8-435f-4f29-95d6-57abe3fb30e7", "alias" : "direct grant", "description" : "OpenID Connect Resource Owner Grant", "providerId" : "basic-flow", @@ -2095,7 +2106,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "5f501679-10bd-484e-bd5e-851ff4086942", + "id" : "6bba0ee6-e128-46ae-bb71-21e2a15b7c2e", "alias" : "docker auth", "description" : "Used by Docker clients to authenticate against the IDP", "providerId" : "basic-flow", @@ -2110,7 +2121,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "fd11f60f-ee22-4eb9-8721-c2feee85f36f", + "id" : "00636f61-55eb-4910-9ee4-e8a7d8c0e531", "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", @@ -2133,7 +2144,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "b7b5a91d-3d5a-40e6-b705-3bc9aa06c5af", + "id" : "35d38b7f-2881-4b5b-b114-e207e836ea51", "alias" : "forms", "description" : "Username, password, otp and other auth forms.", "providerId" : "basic-flow", @@ -2155,7 +2166,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "22d18eb6-1172-4651-b39e-1b31884e728a", + "id" : "cefd13b8-ff6d-4d1d-b3dc-3cd9d02c3314", "alias" : "http challenge", "description" : "An authentication flow based on challenge-response HTTP Authentication Schemes", "providerId" : "basic-flow", @@ -2177,7 +2188,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "90eb8d81-b244-4d02-b513-3519cb44c5cf", + "id" : "54354e5c-fc51-434a-b452-d1abbd1cafac", "alias" : "registration", "description" : "registration flow", "providerId" : "basic-flow", @@ -2193,7 +2204,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "937bff8d-128a-42bf-9437-598541744fa1", + "id" : "8c7e832c-5874-4914-957a-deed6a182473", "alias" : "registration form", "description" : "registration form", "providerId" : "form-flow", @@ -2229,7 +2240,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "ce4981b4-a9c8-4389-b039-8c94517753b0", + "id" : "881146e6-4fbf-4155-88bf-275912cf6f22", "alias" : "reset credentials", "description" : "Reset credentials for a user if they forgot their password or something", "providerId" : "basic-flow", @@ -2265,7 +2276,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "45bd9132-b308-4971-841b-ade951fcd58b", + "id" : "3d8bda77-c045-400c-86bb-97a9497234ea", "alias" : "saml ecp", "description" : "SAML ECP Profile Authentication Flow", "providerId" : "basic-flow", @@ -2281,13 +2292,13 @@ } ] } ], "authenticatorConfig" : [ { - "id" : "d243532c-fb83-4523-9aec-32dcb0d5119a", + "id" : "e5216582-408a-4892-a969-8eada7331caa", "alias" : "create unique user config", "config" : { "require.password.update.after.registration" : "false" } }, { - "id" : "29dc7168-78ae-407a-b974-15dfb79dc5c0", + "id" : "9ac544c2-0b2b-4df3-b1d6-9565caac9cbd", "alias" : "review profile config", "config" : { "update.profile.on.first.login" : "missing" diff --git a/fda-container-service/rest-service/src/main/java/at/tuwien/endpoints/ContainerEndpoint.java b/fda-container-service/rest-service/src/main/java/at/tuwien/endpoints/ContainerEndpoint.java index 10455f9f964dd5385f1a3a84b0b40f57d0e6c0c5..cdec045020c3b5cc554e4fc81d6bfae8087e2a01 100644 --- a/fda-container-service/rest-service/src/main/java/at/tuwien/endpoints/ContainerEndpoint.java +++ b/fda-container-service/rest-service/src/main/java/at/tuwien/endpoints/ContainerEndpoint.java @@ -91,7 +91,7 @@ public class ContainerEndpoint { @PutMapping("/{id}") @Transactional @Timed(value = "container.modify", description = "Time needed to modify the container state") - @PreAuthorize("hasRole('ROLE_RESEARCHER') or hasRole('ROLE_DEVELOPER')") + @PreAuthorize("hasAuthority('modify-container-state')") @Operation(summary = "Modify some container", security = @SecurityRequirement(name = "bearerAuth")) public ResponseEntity<ContainerBriefDto> modify(@NotNull @PathVariable("id") Long containerId, @Valid @RequestBody ContainerChangeDto changeDto, @@ -101,7 +101,7 @@ public class ContainerEndpoint { log.debug("endpoint modify container, containerId={}, changeDto={}, principal={}", containerId, changeDto, principal); final User user = userService.findByUsername(principal.getName()); final Container container = containerService.find(containerId); - if (!(container.getCreator().getId().equals(user.getId()) || user.getRoles().stream().anyMatch(r -> r.name().equals("ROLE_DEVELOPER")))) { + if (!(container.getCreator().getId().equals(user.getId()))) { log.error("Failed to modify container because it is not owned '{}' by the current user {} or is not developer", container.getCreator().getUsername(), user.getUsername()); throw new NotAllowedException("Failed to modify container because it is not owned by the current user or is not developer"); } @@ -122,7 +122,7 @@ public class ContainerEndpoint { @DeleteMapping("/{id}") @Transactional @Timed(value = "container.delete", description = "Time needed to delete the container") - @PreAuthorize("hasRole('ROLE_DEVELOPER')") + @PreAuthorize("hasAuthority('delete-container')") @Operation(summary = "Delete some container", security = @SecurityRequirement(name = "bearerAuth")) public ResponseEntity<?> delete(@NotNull @PathVariable("id") Long containerId, @NotNull Principal principal) throws ContainerNotFoundException, diff --git a/fda-container-service/rest-service/src/main/java/at/tuwien/endpoints/ImageEndpoint.java b/fda-container-service/rest-service/src/main/java/at/tuwien/endpoints/ImageEndpoint.java index bbb5d2ecba97a1acd2314ce9a9cbb9b16de68e82..97a1ab755cdaf7a89471f4a7394ce35cd5bbfba6 100644 --- a/fda-container-service/rest-service/src/main/java/at/tuwien/endpoints/ImageEndpoint.java +++ b/fda-container-service/rest-service/src/main/java/at/tuwien/endpoints/ImageEndpoint.java @@ -57,7 +57,7 @@ public class ImageEndpoint { @PostMapping @Transactional @Timed(value = "image.create", description = "Time needed to create a container image") - @PreAuthorize("hasRole('ROLE_DEVELOPER')") + @PreAuthorize("hasAuthority('create-image')") @Operation(summary = "Create image", security = @SecurityRequirement(name = "bearerAuth")) public ResponseEntity<ImageDto> create(@Valid @RequestBody ImageCreateDto data, @NotNull Principal principal) throws ImageNotFoundException, @@ -91,7 +91,7 @@ public class ImageEndpoint { @PutMapping("/{id}") @Transactional @Timed(value = "image.update", description = "Time needed to update a container image") - @PreAuthorize("hasRole('DEVELOPER')") + @PreAuthorize("hasAuthority('modify-image')") @Operation(summary = "Update some image", security = @SecurityRequirement(name = "bearerAuth")) public ResponseEntity<ImageDto> update(@NotNull @PathVariable Long id, @RequestBody @Valid ImageChangeDto changeDto, @@ -108,7 +108,7 @@ public class ImageEndpoint { @DeleteMapping("/{id}") @Transactional @Timed(value = "image.delete", description = "Time needed to delete a container image") - @PreAuthorize("hasRole('DEVELOPER')") + @PreAuthorize("hasAuthority('delete-image')") @Operation(summary = "Delete some image", security = @SecurityRequirement(name = "bearerAuth")) public ResponseEntity<?> delete(@NotNull @PathVariable Long imageId, @NotNull Principal principal) throws ImageNotFoundException { diff --git a/fda-container-service/rest-service/src/test/java/at/tuwien/BaseUnitTest.java b/fda-container-service/rest-service/src/test/java/at/tuwien/BaseUnitTest.java index 5fd75bb0250fededa0da2b1a2401f56cbbbc725b..6dc27c2260a57820cbedf9516124bbb0283f1eba 100644 --- a/fda-container-service/rest-service/src/test/java/at/tuwien/BaseUnitTest.java +++ b/fda-container-service/rest-service/src/test/java/at/tuwien/BaseUnitTest.java @@ -9,9 +9,7 @@ import at.tuwien.entities.container.Container; import at.tuwien.entities.container.image.ContainerImage; import at.tuwien.entities.container.image.ContainerImageEnvironmentItem; import at.tuwien.entities.container.image.ContainerImageEnvironmentItemType; -import at.tuwien.entities.user.RoleType; import at.tuwien.entities.user.User; -import com.github.dockerjava.api.command.InspectContainerResponse; import com.github.dockerjava.api.model.HealthCheck; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.GrantedAuthority; @@ -49,11 +47,7 @@ public abstract class BaseUnitTest { .username(USER_1_USERNAME) .email(USER_1_EMAIL) .emailVerified(USER_1_EMAIL_VERIFIED) - .affiliation(USER_1_AFFILIATION) - .themeDark(USER_1_THEME_DARK) - .password(USER_1_PASSWORD) .databasePassword(USER_1_DATABASE_PASSWORD) - .roles(List.of(RoleType.ROLE_RESEARCHER)) .build(); public final static UserDto USER_1_DTO = UserDto.builder() @@ -61,11 +55,6 @@ public abstract class BaseUnitTest { .username(USER_1_USERNAME) .email(USER_1_EMAIL) .emailVerified(USER_1_EMAIL_VERIFIED) - .affiliation(USER_1_AFFILIATION) - .themeDark(USER_1_THEME_DARK) - .password(USER_1_PASSWORD) - .roles(List.of("ROLE_RESEARCHER")) - .authorities(List.of(RESEARCHER_AUTHORITY_DTO)) .build(); public final static UserDetails USER_1_DETAILS = UserDetailsDto.builder() @@ -96,7 +85,6 @@ public abstract class BaseUnitTest { public final static Boolean USER_2_THEME_DARK = false; public final static String USER_2_PASSWORD = "p455w0rdh45"; public final static String USER_2_DATABASE_PASSWORD = "*A8C67ABBEAE837AABCF49680A157D85D44A117E9"; - public final static RoleType USER_2_ROLE_TYPE = RoleType.ROLE_DEVELOPER; public final static GrantedAuthority USER_2_AUTHORITY = new SimpleGrantedAuthority("ROLE_DEVELOPER"); public final static User USER_2 = User.builder() @@ -104,10 +92,7 @@ public abstract class BaseUnitTest { .username(USER_2_USERNAME) .email(USER_2_EMAIL) .emailVerified(USER_2_EMAIL_VERIFIED) - .themeDark(USER_2_THEME_DARK) - .password(USER_2_PASSWORD) .databasePassword(USER_2_DATABASE_PASSWORD) - .roles(List.of(USER_2_ROLE_TYPE)) .build(); public final static UserDetails USER_2_DETAILS = UserDetailsDto.builder() @@ -133,7 +118,6 @@ public abstract class BaseUnitTest { public final static Boolean USER_3_THEME_DARK = false; public final static String USER_3_PASSWORD = "p455w0rdh45"; public final static String USER_3_DATABASE_PASSWORD = "*A8C67ABBEAE837AABCF49680A157D85D44A117E9"; - public final static RoleType USER_3_ROLE_TYPE = RoleType.ROLE_DATA_STEWARD; public final static GrantedAuthority USER_3_AUTHORITY = new SimpleGrantedAuthority("ROLE_DATA_STEWARD"); public final static User USER_3 = User.builder() @@ -141,10 +125,7 @@ public abstract class BaseUnitTest { .username(USER_3_USERNAME) .email(USER_3_EMAIL) .emailVerified(USER_3_EMAIL_VERIFIED) - .themeDark(USER_3_THEME_DARK) - .password(USER_3_PASSWORD) .databasePassword(USER_3_DATABASE_PASSWORD) - .roles(List.of(USER_3_ROLE_TYPE)) .build(); public final static UserDetails USER_3_DETAILS = UserDetailsDto.builder() @@ -176,10 +157,7 @@ public abstract class BaseUnitTest { .username(USER_4_USERNAME) .email(USER_4_EMAIL) .emailVerified(USER_4_EMAIL_VERIFIED) - .themeDark(USER_4_THEME_DARK) - .password(USER_4_PASSWORD) .databasePassword(USER_4_DATABASE_PASSWORD) - .roles(List.of()) .build(); public final static String USER_5_ID = "d2f3a8f4-c7fe-49e8-9d14-6dad0f6b9406"; diff --git a/fda-database-service/rest-service/src/main/java/at/tuwien/endpoints/AbstractEndpoint.java b/fda-database-service/rest-service/src/main/java/at/tuwien/endpoints/AbstractEndpoint.java deleted file mode 100644 index a88cfd05ac78072cde63205b2a1fcec6f35ae60b..0000000000000000000000000000000000000000 --- a/fda-database-service/rest-service/src/main/java/at/tuwien/endpoints/AbstractEndpoint.java +++ /dev/null @@ -1,147 +0,0 @@ -package at.tuwien.endpoints; - -import at.tuwien.entities.container.Container; -import at.tuwien.entities.database.AccessType; -import at.tuwien.entities.database.Database; -import at.tuwien.entities.database.DatabaseAccess; -import at.tuwien.exception.ContainerNotFoundException; -import at.tuwien.exception.DatabaseNotFoundException; -import at.tuwien.repository.jpa.DatabaseAccessRepository; -import at.tuwien.service.ContainerService; -import at.tuwien.service.DatabaseService; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.core.Authentication; - -import java.security.Principal; -import java.util.List; -import java.util.Optional; - -@Slf4j -public abstract class AbstractEndpoint { - - private final DatabaseService databaseService; - private final ContainerService containerService; - private final DatabaseAccessRepository databaseAccessRepository; - - @Autowired - protected AbstractEndpoint(DatabaseService databaseService, ContainerService containerService, - DatabaseAccessRepository databaseAccessRepository) { - this.databaseService = databaseService; - this.containerService = containerService; - this.databaseAccessRepository = databaseAccessRepository; - } - - protected Boolean hasDatabasePermission(Long containerId, Long databaseId, String permissionCode, - Principal principal) { - log.trace("validate has database permission, containerId={}, databaseId={}, permissionCode={}, principal={}", - containerId, databaseId, permissionCode, principal); - final Database database; - try { - database = databaseService.findById(containerId, databaseId); - } catch (DatabaseNotFoundException e) { - log.error("Failed to find database"); - return false; - } - /* view-only operations are allowed on public databases */ - if (database.getIsPublic() && false) { - log.trace("grant permission {} because database is public", permissionCode); - return true; - } - if (principal == null) { - log.error("Failed to grant permission {} because principal is null", permissionCode); - return false; - } - /* view-only operations are allowed on all databases */ - if (List.of("CHECK_ACCESS").contains(permissionCode)) { - log.debug("grant permission {} because of public/private database", permissionCode); - return true; - } - /* modification operations are limited to the owner */ - if (database.getOwner().getUsername().equals(principal.getName())) { - log.trace("grant permission {} because user {} is owner {}", permissionCode, principal.getName(), - database.getOwner().getUsername()); - return true; - } - final Authentication authentication = (Authentication) principal /* with pre-authorization this always holds */; - if (List.of("VISIBILITY_DATABASE").contains(permissionCode) && isDeveloper(authentication)) { - log.debug("grant permission {} because user {} is developer", permissionCode, principal.getName()); - return true; - } - final Optional<DatabaseAccess> optional = databaseAccessRepository.findByDatabaseIdAndUsername(databaseId, principal.getName()); - if (optional.isEmpty()) { - log.error("Failed to grant permission {} because user {} does not have access", permissionCode, principal.getName()); - return false; - } - final DatabaseAccess access = optional.get(); - log.trace("access type: {}", access.getType()); - if (List.of().contains(permissionCode) && hasAccess(access) && isResearcher(authentication)) { - log.debug("grant permission {} because user {} has access type {} and is researcher", permissionCode, - principal.getName(), optional.get().getType()); - return true; - } - log.error("Failed to grant permission {} because user {} does not have access", permissionCode, principal.getName()); - return false; - } - - protected Boolean hasContainerPermission(Long containerId, String permissionCode, Principal principal) { - log.trace("validate has container permission, containerId={}, permissionCode={}, principal={}", - containerId, permissionCode, principal); - final Container container; - try { - container = containerService.find(containerId); - } catch (ContainerNotFoundException e) { - return false; - } - /* view-only operations are allowed on public databases */ - if (List.of().contains(permissionCode)) { - log.debug("grant permission {} because it does not require authentication", permissionCode); - return true; - } - if (principal == null) { - log.error("Failed to grant permission {} because principal is null", permissionCode); - return false; - } - /* modification operations are limited to the owner */ - if (container.getOwner().getUsername().equals(principal.getName())) { - log.debug("grant permission {} because user {} is owner {}", permissionCode, principal.getName(), - container.getOwner().getUsername()); - return true; - } - final Authentication authentication = (Authentication) principal /* with pre-authorization this always holds */; - if (List.of("CREATE_DATABASE").contains(permissionCode) && - authentication.getAuthorities().stream().anyMatch(a -> a.getAuthority().equals("ROLE_DEVELOPER"))) { - log.debug("grant permission {} because user {} is developer", permissionCode, principal.getName()); - return true; - } - if (authentication.getAuthorities().stream().noneMatch(a -> a.getAuthority().equals("ROLE_RESEARCHER"))) { - log.error("Failed to grant permission {} because current user misses authority 'ROLE_RESEARCHER'", - permissionCode); - return false; - } - log.error("Failed to grant permission {} because container is not owner by the current user", permissionCode); - return false; - } - - protected boolean isResearcher(Authentication authentication) { - return authentication.getAuthorities().stream() - .anyMatch(a -> a.getAuthority().equals("ROLE_RESEARCHER")); - } - - protected boolean isDeveloper(Authentication authentication) { - return authentication.getAuthorities().stream() - .anyMatch(a -> a.getAuthority().equals("ROLE_DEVELOPER")); - } - - protected boolean hasReadAccess(DatabaseAccess access) { - return access.getType().equals(AccessType.READ); - } - - protected boolean hasWriteAccess(DatabaseAccess access) { - return access.getType().equals(AccessType.WRITE_OWN) || access.getType().equals(AccessType.WRITE_ALL); - } - - protected boolean hasAccess(DatabaseAccess access) { - return hasReadAccess(access) || hasWriteAccess(access); - } -} diff --git a/fda-database-service/rest-service/src/main/java/at/tuwien/endpoints/AccessEndpoint.java b/fda-database-service/rest-service/src/main/java/at/tuwien/endpoints/AccessEndpoint.java index b8206f09e5b43b1f608da23c2796e0aa73f2ef7d..3c83982839537f9244f5eb1093b6072b292e208b 100644 --- a/fda-database-service/rest-service/src/main/java/at/tuwien/endpoints/AccessEndpoint.java +++ b/fda-database-service/rest-service/src/main/java/at/tuwien/endpoints/AccessEndpoint.java @@ -15,6 +15,7 @@ import io.swagger.v3.oas.annotations.security.SecurityRequirement; import lombok.extern.log4j.Log4j2; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.*; @@ -27,22 +28,20 @@ import java.security.Principal; @RestController @CrossOrigin(origins = "*") @RequestMapping("/api/container/{id}/database/{databaseId}/access") -public class AccessEndpoint extends AbstractEndpoint { +public class AccessEndpoint { private final AccessService accessService; private final DatabaseMapper databaseMapper; @Autowired - public AccessEndpoint(DatabaseAccessRepository databaseAccessRepository, DatabaseService databaseService, - ContainerService containerService, AccessService accessService, - DatabaseMapper databaseMapper) { - super(databaseService, containerService, databaseAccessRepository); + public AccessEndpoint(AccessService accessService, DatabaseMapper databaseMapper) { this.accessService = accessService; this.databaseMapper = databaseMapper; } @PostMapping @Transactional + @PreAuthorize("hasAuthority('create-access')") @Operation(summary = "Give access to some database", security = @SecurityRequirement(name = "bearerAuth")) public ResponseEntity<?> create(@NotBlank @PathVariable("id") Long containerId, @NotBlank @PathVariable("databaseId") Long databaseId, @@ -52,10 +51,6 @@ public class AccessEndpoint extends AbstractEndpoint { DatabaseMalformedException { log.debug("endpoint give access to database, containerId={}, databaseId={}, accessDto={}, principal={}", containerId, databaseId, accessDto, principal); - if (!hasDatabasePermission(containerId, databaseId, "GIVE_ACCESS", principal)) { - log.error("Missing give access permission"); - throw new NotAllowedException("Missing give access permission"); - } try { accessService.find(databaseId, accessDto.getUsername()); log.error("Failed to give access to user with username {}, already has access", accessDto.getUsername()); @@ -70,6 +65,7 @@ public class AccessEndpoint extends AbstractEndpoint { @PutMapping("/{username}") @Transactional + @PreAuthorize("hasAuthority('modify-access')") @Operation(summary = "Modify access to some database", security = @SecurityRequirement(name = "bearerAuth")) public ResponseEntity<?> update(@NotBlank @PathVariable("id") Long containerId, @NotBlank @PathVariable("databaseId") Long databaseId, @@ -80,10 +76,6 @@ public class AccessEndpoint extends AbstractEndpoint { QueryMalformedException, DatabaseMalformedException { log.debug("endpoint modify access to database, containerId={}, databaseId={}, username={}, accessDto={}, principal={}", containerId, databaseId, username, accessDto, principal); - if (!hasDatabasePermission(containerId, databaseId, "MODIFY_ACCESS", principal)) { - log.error("Missing modify access permission"); - throw new NotAllowedException("Missing modify access permission"); - } accessService.find(databaseId, username); accessService.update(containerId, databaseId, username, accessDto); return ResponseEntity.accepted() @@ -92,6 +84,7 @@ public class AccessEndpoint extends AbstractEndpoint { @GetMapping @Transactional + @PreAuthorize("hasAuthority('check-access')") @Operation(summary = "Check access to some database", security = @SecurityRequirement(name = "bearerAuth")) public ResponseEntity<DatabaseAccessDto> find(@NotBlank @PathVariable("id") Long containerId, @NotBlank @PathVariable("databaseId") Long databaseId, @@ -99,10 +92,6 @@ public class AccessEndpoint extends AbstractEndpoint { AccessDeniedException { log.debug("endpoint check access to database, containerId={}, databaseId={}, principal={}", containerId, databaseId, principal); - if (!hasDatabasePermission(containerId, databaseId, "CHECK_ACCESS", principal)) { - log.error("Missing modify access permission"); - throw new NotAllowedException("Missing modify access permission"); - } final DatabaseAccess access = accessService.find(databaseId, principal.getName()); final DatabaseAccessDto dto = databaseMapper.databaseAccessToDatabaseAccessDto(access); log.trace("check access resulted in dto {}", dto); @@ -111,6 +100,7 @@ public class AccessEndpoint extends AbstractEndpoint { @DeleteMapping("/{username}") @Transactional + @PreAuthorize("hasAuthority('modify-access')") @Operation(summary = "Revoke access to some database", security = @SecurityRequirement(name = "bearerAuth")) public ResponseEntity<?> revoke(@NotBlank @PathVariable("id") Long containerId, @NotBlank @PathVariable("databaseId") Long databaseId, @@ -120,10 +110,6 @@ public class AccessEndpoint extends AbstractEndpoint { QueryMalformedException, DatabaseMalformedException { log.debug("endpoint revoke access to database, containerId={}, databaseId={}, username={}, principal={}", containerId, databaseId, username, principal); - if (!hasDatabasePermission(containerId, databaseId, "REVOKE_ACCESS", principal)) { - log.error("Missing revoke access permission"); - throw new NotAllowedException("Missing revoke access permission"); - } accessService.find(databaseId, username); accessService.delete(containerId, databaseId, username); return ResponseEntity.accepted() diff --git a/fda-database-service/rest-service/src/main/java/at/tuwien/endpoints/DatabaseEndpoint.java b/fda-database-service/rest-service/src/main/java/at/tuwien/endpoints/DatabaseEndpoint.java index bfeb98549b44ad1c38c2dbc84c399aa99f475cf6..11121c05c8123583d2f4b2b0c5eb5a4bbd43fdef 100644 --- a/fda-database-service/rest-service/src/main/java/at/tuwien/endpoints/DatabaseEndpoint.java +++ b/fda-database-service/rest-service/src/main/java/at/tuwien/endpoints/DatabaseEndpoint.java @@ -30,7 +30,7 @@ import java.util.stream.Collectors; @RestController @CrossOrigin(origins = "*") @RequestMapping("/api/container/{id}/database") -public class DatabaseEndpoint extends AbstractEndpoint { +public class DatabaseEndpoint { private final UserService userService; private final AccessService accessService; @@ -41,11 +41,9 @@ public class DatabaseEndpoint extends AbstractEndpoint { private final DatabaseAccessRepository databaseAccessRepository; @Autowired - public DatabaseEndpoint(DatabaseMapper databaseMapper, ContainerService containerService, - UserService userService, MariaDbServiceImpl databaseService, QueryStoreService queryStoreService, - MessageQueueService messageQueueService, AccessService accessService, - DatabaseAccessRepository databaseAccessRepository) { - super(databaseService, containerService, databaseAccessRepository); + public DatabaseEndpoint(DatabaseMapper databaseMapper, UserService userService, MariaDbServiceImpl databaseService, + QueryStoreService queryStoreService, MessageQueueService messageQueueService, + AccessService accessService, DatabaseAccessRepository databaseAccessRepository) { this.userService = userService; this.accessService = accessService; this.databaseMapper = databaseMapper; @@ -72,7 +70,7 @@ public class DatabaseEndpoint extends AbstractEndpoint { @PostMapping @Transactional - @PreAuthorize("hasRole('ROLE_RESEARCHER') or hasRole('ROLE_DEVELOPER')") + @PreAuthorize("hasAuthority('create-database')") @Timed(value = "database.create", description = "Time needed to create a database") @Operation(summary = "Create database", security = @SecurityRequirement(name = "bearerAuth")) public ResponseEntity<DatabaseBriefDto> create(@NotNull @PathVariable("id") Long containerId, @@ -85,10 +83,6 @@ public class DatabaseEndpoint extends AbstractEndpoint { BrokerVirtualHostGrantException { log.debug("endpoint create database, containerId={}, createDto={}, principal={}", containerId, createDto, principal); - if (!hasContainerPermission(containerId, "CREATE_DATABASE", principal)) { - log.error("Missing database create permission"); - throw new NotAllowedException("Missing database create permission"); - } final Database database = databaseService.create(containerId, createDto, principal); final User user = userService.findByUsername(principal.getName()); messageQueueService.createExchange(database, principal); @@ -103,7 +97,7 @@ public class DatabaseEndpoint extends AbstractEndpoint { @PutMapping("/{databaseId}/visibility") @Transactional - @PreAuthorize("hasRole('ROLE_RESEARCHER') or hasRole('ROLE_DEVELOPER')") + @PreAuthorize("hasAuthority('modify-database')") @Timed(value = "database.visibility", description = "Time needed to modify a database visibility") @Operation(summary = "Update database", security = @SecurityRequirement(name = "bearerAuth")) public ResponseEntity<DatabaseDto> visibility(@NotNull @PathVariable("id") Long containerId, @@ -113,10 +107,6 @@ public class DatabaseEndpoint extends AbstractEndpoint { throws DatabaseNotFoundException, NotAllowedException { log.debug("endpoint update database, containerId={}, databaseId={}, data={}, principal={}", containerId, databaseId, data, principal); - if (!hasDatabasePermission(containerId, databaseId, "VISIBILITY_DATABASE", principal)) { - log.error("Missing database update visibility permission"); - throw new NotAllowedException("Missing database update visibility permission"); - } final Database database = databaseService.visibility(containerId, databaseId, data); final DatabaseDto dto = databaseMapper.databaseToDatabaseDto(database); log.trace("update database resulted in database {}", dto); @@ -126,9 +116,9 @@ public class DatabaseEndpoint extends AbstractEndpoint { @PutMapping("/{databaseId}/transfer") @Transactional - @PreAuthorize("isAuthenticated()") + @PreAuthorize("hasAuthority('transfer-database')") @Timed(value = "database.transfer", description = "Time needed to transfer a database ownership") - @Operation(summary = "Update database", security = @SecurityRequirement(name = "bearerAuth")) + @Operation(summary = "Transfer database", security = @SecurityRequirement(name = "bearerAuth")) public ResponseEntity<DatabaseDto> transfer(@NotNull @PathVariable("id") Long containerId, @NotNull @PathVariable Long databaseId, @Valid @RequestBody DatabaseTransferDto transferDto, @@ -136,10 +126,6 @@ public class DatabaseEndpoint extends AbstractEndpoint { throws DatabaseNotFoundException, NotAllowedException, UserNotFoundException { log.debug("endpoint update database, containerId={}, databaseId={}, transferDto={}, principal={}", containerId, databaseId, transferDto, principal); - if (!hasDatabasePermission(containerId, databaseId, "TRANSFER_DATABASE", principal)) { - log.error("Missing database transfer ownership permission"); - throw new NotAllowedException("Missing database transfer ownership permission"); - } final Database database = databaseService.transfer(containerId, databaseId, transferDto); final DatabaseDto dto = databaseMapper.databaseToDatabaseDto(database); log.trace("update database resulted in database {}", dto); @@ -171,7 +157,7 @@ public class DatabaseEndpoint extends AbstractEndpoint { @DeleteMapping("/{databaseId}") @Transactional - @PreAuthorize("hasRole('ROLE_DEVELOPER')") + @PreAuthorize("hasAuthority('delete-database')") @Timed(value = "database.delete", description = "Time needed to delete a database") @Operation(summary = "Delete some database", security = @SecurityRequirement(name = "bearerAuth")) public ResponseEntity<?> delete(@NotNull @PathVariable("id") Long containerId, diff --git a/fda-database-service/rest-service/src/main/java/at/tuwien/endpoints/LicenseEndpoint.java b/fda-database-service/rest-service/src/main/java/at/tuwien/endpoints/LicenseEndpoint.java index 8298ad461f9f275b884a2610f7380b65fde12a3b..9e8cb566d1d3c87abc5d53c913da792339c8ad7f 100644 --- a/fda-database-service/rest-service/src/main/java/at/tuwien/endpoints/LicenseEndpoint.java +++ b/fda-database-service/rest-service/src/main/java/at/tuwien/endpoints/LicenseEndpoint.java @@ -9,6 +9,7 @@ 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.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.*; diff --git a/fda-database-service/rest-service/src/test/java/at/tuwien/BaseUnitTest.java b/fda-database-service/rest-service/src/test/java/at/tuwien/BaseUnitTest.java index d19118589c651d4751587d5802f7fec8dadae332..e1d061dc6dfb42386926e233cd9a49330d1d7a40 100644 --- a/fda-database-service/rest-service/src/test/java/at/tuwien/BaseUnitTest.java +++ b/fda-database-service/rest-service/src/test/java/at/tuwien/BaseUnitTest.java @@ -90,8 +90,6 @@ public abstract class BaseUnitTest { .username(USER_1_USERNAME) .email(USER_1_EMAIL) .emailVerified(USER_1_VERIFIED) - .themeDark(USER_1_THEME) - .password(USER_1_PASSWORD) .databasePassword(USER_1_DATABASE_PASSWORD) .build(); @@ -99,7 +97,6 @@ public abstract class BaseUnitTest { .id(USER_1_ID) .username(USER_1_USERNAME) .emailVerified(USER_1_VERIFIED) - .themeDark(USER_1_THEME) .build(); public final static UserDto USER_1_DTO = UserDto.builder() @@ -107,10 +104,6 @@ public abstract class BaseUnitTest { .username(USER_1_USERNAME) .email(USER_1_EMAIL) .emailVerified(USER_1_VERIFIED) - .themeDark(USER_1_THEME) - .password(USER_1_PASSWORD) - .roles(List.of("ROLE_RESEARCHER")) - .authorities(List.of(new GrantedAuthorityDto("ROLE_RESEARCHER"))) .build(); public final static UserDetails USER_1_DETAILS = UserDetailsDto.builder() @@ -139,8 +132,6 @@ public abstract class BaseUnitTest { .username(USER_2_USERNAME) .email(USER_2_EMAIL) .emailVerified(USER_2_VERIFIED) - .themeDark(USER_2_THEME) - .password(USER_2_PASSWORD) .databasePassword(USER_2_DATABASE_PASSWORD) .build(); @@ -149,17 +140,12 @@ public abstract class BaseUnitTest { .username(USER_2_USERNAME) .email(USER_2_EMAIL) .emailVerified(USER_2_VERIFIED) - .themeDark(USER_2_THEME) - .password(USER_2_PASSWORD) - .roles(List.of("ROLE_DATA_STEWARD")) - .authorities(List.of(new GrantedAuthorityDto("ROLE_DATA_STEWARD"))) .build(); public final static UserBriefDto USER_2_BRIEF_DTO = UserBriefDto.builder() .id(USER_2_ID) .username(USER_2_USERNAME) .emailVerified(USER_2_VERIFIED) - .themeDark(USER_2_THEME) .build(); public final static UserDetails USER_2_DETAILS = UserDetailsDto.builder() @@ -187,8 +173,6 @@ public abstract class BaseUnitTest { .username(USER_3_USERNAME) .email(USER_3_EMAIL) .emailVerified(USER_3_EMAIL_VERIFIED) - .themeDark(USER_3_THEME_DARK) - .password(USER_3_PASSWORD) .databasePassword(USER_3_DATABASE_PASSWORD) .build(); @@ -197,10 +181,6 @@ public abstract class BaseUnitTest { .username(USER_3_USERNAME) .email(USER_3_EMAIL) .emailVerified(USER_3_VERIFIED) - .themeDark(USER_3_THEME) - .password(USER_3_PASSWORD) - .roles(List.of("ROLE_DEVELOPER")) - .authorities(List.of(new GrantedAuthorityDto("ROLE_DEVELOPER"))) .build(); public final static UserDetails USER_3_DETAILS = UserDetailsDto.builder() @@ -226,8 +206,6 @@ public abstract class BaseUnitTest { .username(USER_4_USERNAME) .email(USER_4_EMAIL) .emailVerified(USER_4_EMAIL_VERIFIED) - .themeDark(USER_4_THEME_DARK) - .password(USER_4_PASSWORD) .databasePassword(USER_4_DATABASE_PASSWORD) .build(); diff --git a/fda-database-service/rest-service/src/test/java/at/tuwien/mapper/DatabaseMapperTest.java b/fda-database-service/rest-service/src/test/java/at/tuwien/mapper/DatabaseMapperTest.java index 48d94926281e6d2008816e9e196b631801e07e7c..5159e734eea655be0b3a447667d3bdda1cede67e 100644 --- a/fda-database-service/rest-service/src/test/java/at/tuwien/mapper/DatabaseMapperTest.java +++ b/fda-database-service/rest-service/src/test/java/at/tuwien/mapper/DatabaseMapperTest.java @@ -47,11 +47,9 @@ public class DatabaseMapperTest extends BaseUnitTest { final UserBriefDto creator = response.getCreator(); assertEquals(USER_1_ID, creator.getId()); assertEquals(USER_1_USERNAME, creator.getUsername()); - assertEquals(USER_1_THEME, creator.getThemeDark()); final UserBriefDto owner = response.getOwner(); assertEquals(USER_1_ID, owner.getId()); assertEquals(USER_1_USERNAME, owner.getUsername()); - assertEquals(USER_1_THEME, owner.getThemeDark()); } } diff --git a/fda-database-service/services/src/main/java/at/tuwien/mapper/DatabaseMapper.java b/fda-database-service/services/src/main/java/at/tuwien/mapper/DatabaseMapper.java index f986f7577245bb0161e9d5d046da92fd22e40012..51e9c382be0e9fdf6616f0489ad69d590f16dd5f 100644 --- a/fda-database-service/services/src/main/java/at/tuwien/mapper/DatabaseMapper.java +++ b/fda-database-service/services/src/main/java/at/tuwien/mapper/DatabaseMapper.java @@ -77,7 +77,6 @@ public interface DatabaseMapper { .get(0); return User.builder() .username(username) - .databasePassword(password) .build(); } @@ -105,7 +104,6 @@ public interface DatabaseMapper { final StringBuilder statement = new StringBuilder("CREATE USER IF NOT EXISTS `") .append(user.getUsername()) .append("`@`%` IDENTIFIED BY PASSWORD '") - .append(user.getDatabasePassword()) .append("';"); log.trace("statement={}", statement); try { diff --git a/fda-identifier-service/rest-service/src/main/java/at/tuwien/endpoints/AbstractEndpoint.java b/fda-identifier-service/rest-service/src/main/java/at/tuwien/endpoints/AbstractEndpoint.java deleted file mode 100644 index 937e07c404b694916c7b25703af94f7700243dca..0000000000000000000000000000000000000000 --- a/fda-identifier-service/rest-service/src/main/java/at/tuwien/endpoints/AbstractEndpoint.java +++ /dev/null @@ -1,59 +0,0 @@ -package at.tuwien.endpoints; - -import at.tuwien.entities.database.Database; -import at.tuwien.entities.user.User; -import at.tuwien.exception.DatabaseNotFoundException; -import at.tuwien.exception.UserNotFoundException; -import at.tuwien.service.DatabaseService; -import at.tuwien.service.UserService; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; - -import java.security.Principal; -import java.util.List; - -@Slf4j -public abstract class AbstractEndpoint { - - private final UserService userService; - private final DatabaseService databaseService; - - @Autowired - protected AbstractEndpoint(UserService userService, DatabaseService databaseService) { - this.userService = userService; - this.databaseService = databaseService; - } - - protected Boolean hasDatabasePermission(Long containerId, Long databaseId, String permissionCode, - Principal principal) throws UserNotFoundException { - log.trace("validate has database permission, containerId={}, databaseId={}, permissionCode={}, principal={}", - containerId, databaseId, permissionCode, principal); - final Database database; - try { - database = databaseService.find(containerId, databaseId); - } catch (DatabaseNotFoundException e) { - log.error("Failed to find database with container id {} and database id {}", containerId, databaseId); - return false; - } - /* view-only operations are allowed on public databases */ - if (principal == null) { - log.error("Failed to grant permission {} because principal is null", permissionCode); - return false; - } - final User user = userService.findByUsername(principal.getName()); - /* data steward */ - if (List.of("CREATE_IDENTIFIER").contains(permissionCode) && user.getRoles().stream().anyMatch(r -> r.name().equals("ROLE_DATA_STEWARD"))) { - log.debug("grant permission {} because of role data steward", permissionCode); - return true; - } - /* modification operations are limited to the creator */ - if (database.getCreator().getUsername().equals(principal.getName())) { - log.trace("grant permission {} because user {} is creator {}", permissionCode, principal.getName(), - database.getCreator().getUsername()); - return true; - } - log.error("Failed to grant permission {} because database owner with id {} is not the current user with id {}", permissionCode, database.getCreator().getId(), user.getId()); - return false; - } - -} diff --git a/fda-identifier-service/rest-service/src/main/java/at/tuwien/endpoints/IdentifierEndpoint.java b/fda-identifier-service/rest-service/src/main/java/at/tuwien/endpoints/IdentifierEndpoint.java index 1f98d9560c0f219e048b9531a8ae85605e713a96..d75aeb79eba1389df1ea154fe7202c505c7d95e5 100644 --- a/fda-identifier-service/rest-service/src/main/java/at/tuwien/endpoints/IdentifierEndpoint.java +++ b/fda-identifier-service/rest-service/src/main/java/at/tuwien/endpoints/IdentifierEndpoint.java @@ -6,9 +6,7 @@ import at.tuwien.api.identifier.IdentifierTypeDto; import at.tuwien.entities.identifier.Identifier; import at.tuwien.exception.*; import at.tuwien.mapper.IdentifierMapper; -import at.tuwien.service.DatabaseService; import at.tuwien.service.IdentifierService; -import at.tuwien.service.UserService; import io.micrometer.core.annotation.Timed; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.security.SecurityRequirement; @@ -30,21 +28,20 @@ import java.util.stream.Collectors; @CrossOrigin(origins = "*") @RestController @RequestMapping("/api/identifier") -public class IdentifierEndpoint extends AbstractEndpoint { +public class IdentifierEndpoint { private final IdentifierMapper identifierMapper; private final IdentifierService identifierService; @Autowired - public IdentifierEndpoint(IdentifierMapper identifierMapper, IdentifierService identifierService, - DatabaseService databaseService, UserService userService) { - super(userService, databaseService); + public IdentifierEndpoint(IdentifierMapper identifierMapper, IdentifierService identifierService) { this.identifierMapper = identifierMapper; this.identifierService = identifierService; } @GetMapping @Transactional(readOnly = true) + @PreAuthorize("hasAuthority('find-identifiers')") @Timed(value = "identifier.list", description = "Time needed to list the identifiers") @Operation(summary = "Find identifiers") public ResponseEntity<List<IdentifierDto>> list(@RequestParam(required = false) Long dbid, @@ -70,19 +67,14 @@ public class IdentifierEndpoint extends AbstractEndpoint { @PostMapping @Transactional @Timed(value = "identifier.create", description = "Time needed to create an identifier") - @PreAuthorize("hasRole('ROLE_RESEARCHER') or hasRole('ROLE_DATA_STEWARD')") + @PreAuthorize("hasAuthority('create-identifier')") @Operation(summary = "Create identifier", security = @SecurityRequirement(name = "bearerAuth")) public ResponseEntity<IdentifierDto> create(@NotNull @Valid @RequestBody IdentifierCreateDto data, @NotNull @RequestHeader(name = "Authorization") String authorization, @NotNull Principal principal) throws IdentifierAlreadyExistsException, QueryNotFoundException, IdentifierPublishingNotAllowedException, - RemoteUnavailableException, UserNotFoundException, DatabaseNotFoundException, IdentifierRequestException, - NotAllowedException { + RemoteUnavailableException, UserNotFoundException, DatabaseNotFoundException, IdentifierRequestException { log.debug("endpoint create identifier, data={}, authorization={}, principal={}", data, authorization, principal); - if (!hasDatabasePermission(data.getCid(), data.getDbid(), "CREATE_IDENTIFIER", principal)) { - log.error("Missing identifier create permission"); - throw new NotAllowedException("Missing identifier create permission"); - } if (data.getType().equals(IdentifierTypeDto.SUBSET) && data.getQid() == null) { log.error("Identifier of type subset need to have a qid present"); throw new IdentifierRequestException("Identifier of type subset need to have a qid present"); @@ -98,7 +90,7 @@ public class IdentifierEndpoint extends AbstractEndpoint { @PutMapping("/{id}") @Transactional @Timed(value = "identifier.update", description = "Time needed to update an identifier") - @PreAuthorize("hasRole('ROLE_DATA_STEWARD')") + @PreAuthorize("hasAuthority('update-identifier')") @Operation(summary = "Update some identifier", security = @SecurityRequirement(name = "bearerAuth")) public ResponseEntity<IdentifierDto> update(@NotNull @PathVariable("id") Long id, @NotNull @Valid @RequestBody IdentifierDto data) @@ -112,7 +104,7 @@ public class IdentifierEndpoint extends AbstractEndpoint { @DeleteMapping("/{id}") @Transactional @Timed(value = "identifier.delete", description = "Time needed to delete an identifier") - @PreAuthorize("hasRole('ROLE_DATA_STEWARD')") + @PreAuthorize("hasAuthority('delete-identifier')") @Operation(summary = "Delete some identifier", security = @SecurityRequirement(name = "bearerAuth")) public ResponseEntity<?> delete(@NotNull @PathVariable("id") Long id) throws IdentifierNotFoundException { diff --git a/fda-identifier-service/rest-service/src/main/java/at/tuwien/endpoints/PersistenceEndpoint.java b/fda-identifier-service/rest-service/src/main/java/at/tuwien/endpoints/PersistenceEndpoint.java index 278f074a3ba5261baf134559490e924b0786d7a6..a540a807e741ca0df2e495197ad4a061562c8852 100644 --- a/fda-identifier-service/rest-service/src/main/java/at/tuwien/endpoints/PersistenceEndpoint.java +++ b/fda-identifier-service/rest-service/src/main/java/at/tuwien/endpoints/PersistenceEndpoint.java @@ -9,9 +9,7 @@ import at.tuwien.exception.IdentifierRequestException; import at.tuwien.exception.QueryNotFoundException; import at.tuwien.exception.RemoteUnavailableException; import at.tuwien.mapper.IdentifierMapper; -import at.tuwien.service.DatabaseService; import at.tuwien.service.IdentifierService; -import at.tuwien.service.UserService; import io.micrometer.core.annotation.Timed; import io.swagger.v3.oas.annotations.Operation; import lombok.extern.log4j.Log4j2; @@ -20,6 +18,7 @@ import org.springframework.core.io.InputStreamResource; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.*; @@ -31,7 +30,7 @@ import java.util.regex.Pattern; @CrossOrigin(origins = "*") @RestController @RequestMapping("/api/pid") -public class PersistenceEndpoint extends AbstractEndpoint { +public class PersistenceEndpoint { private final EndpointConfig endpointConfig; private final IdentifierMapper identifierMapper; @@ -39,9 +38,7 @@ public class PersistenceEndpoint extends AbstractEndpoint { @Autowired public PersistenceEndpoint(EndpointConfig endpointConfig, IdentifierMapper identifierMapper, - IdentifierService identifierService, DatabaseService databaseService, - UserService userService) { - super(userService, databaseService); + IdentifierService identifierService) { this.endpointConfig = endpointConfig; this.identifierMapper = identifierMapper; this.identifierService = identifierService; @@ -49,6 +46,7 @@ public class PersistenceEndpoint extends AbstractEndpoint { @GetMapping("/{pid}") @Transactional(readOnly = true) + @PreAuthorize("hasAuthority('find-identifier')") @Timed(value = "pid.find", description = "Time needed to find a persisted identifier") @Operation(summary = "Find some identifier") public ResponseEntity<?> find(@Valid @PathVariable("pid") Long pid, diff --git a/fda-identifier-service/rest-service/src/test/java/at/tuwien/BaseUnitTest.java b/fda-identifier-service/rest-service/src/test/java/at/tuwien/BaseUnitTest.java index b02f63c19a919f4fd5680aeaf5412f3c0eb4a016..29893e0027e264c0e0129c1516c613b09bfe6529 100644 --- a/fda-identifier-service/rest-service/src/test/java/at/tuwien/BaseUnitTest.java +++ b/fda-identifier-service/rest-service/src/test/java/at/tuwien/BaseUnitTest.java @@ -58,24 +58,16 @@ public abstract class BaseUnitTest { public final static User USER_1 = User.builder() .id(USER_1_ID) .username(USER_1_USERNAME) - .password(USER_1_PASSWORD) .databasePassword(USER_1_DATABASE_PASSWORD) .email(USER_1_EMAIL) .emailVerified(USER_1_EMAIL_VERIFIED) - .themeDark(USER_1_THEME_DARK) - .created(USER_1_CREATED) - .lastModified(USER_1_LAST_MODIFIED) .build(); public final static UserDto USER_1_DTO = UserDto.builder() .id(USER_1_ID) .username(USER_1_USERNAME) - .password(USER_1_PASSWORD) .email(USER_1_EMAIL) - .authorities(List.of(AUTHORITY_RESEARCHER_DTO)) - .roles(List.of("ROLE_RESEARCHER")) .emailVerified(USER_1_EMAIL_VERIFIED) - .themeDark(USER_1_THEME_DARK) .build(); public final static UserDetails USER_1_DETAILS = UserDetailsDto.builder() @@ -102,24 +94,16 @@ public abstract class BaseUnitTest { public final static User USER_2 = User.builder() .id(USER_2_ID) .username(USER_2_USERNAME) - .password(USER_2_PASSWORD) .databasePassword(USER_2_DATABASE_PASSWORD) .email(USER_2_EMAIL) .emailVerified(USER_2_EMAIL_VERIFIED) - .themeDark(USER_2_THEME_DARK) - .created(USER_2_CREATED) - .lastModified(USER_2_LAST_MODIFIED) .build(); public final static UserDto USER_2_DTO = UserDto.builder() .id(USER_2_ID) .username(USER_2_USERNAME) - .password(USER_2_PASSWORD) .email(USER_2_EMAIL) - .authorities(List.of(AUTHORITY_RESEARCHER_DTO)) - .roles(List.of("ROLE_RESEARCHER")) .emailVerified(USER_2_EMAIL_VERIFIED) - .themeDark(USER_2_THEME_DARK) .build(); public final static UserDetails USER_2_DETAILS = UserDetailsDto.builder() @@ -146,13 +130,9 @@ public abstract class BaseUnitTest { public final static User USER_3 = User.builder() .username(USER_3_USERNAME) - .password(USER_3_PASSWORD) .databasePassword(USER_3_DATABASE_PASSWORD) .email(USER_3_EMAIL) .emailVerified(USER_3_EMAIL_VERIFIED) - .themeDark(USER_3_THEME_DARK) - .created(USER_3_CREATED) - .lastModified(USER_3_LAST_MODIFIED) .build(); public final static UserDetails USER_3_DETAILS = UserDetailsDto.builder() diff --git a/fda-metadata-db/51-dbrepo.cnf b/fda-metadata-db/51-dbrepo.cnf new file mode 100644 index 0000000000000000000000000000000000000000..ed1bf6617fd4513a4a122170a6b24edc72a51330 --- /dev/null +++ b/fda-metadata-db/51-dbrepo.cnf @@ -0,0 +1,2 @@ +[mysqld] +lower_case_table_names=1 \ No newline at end of file diff --git a/fda-metadata-db/Dockerfile b/fda-metadata-db/Dockerfile index 9e86f075b2f86cf07c8cafaf220a2efbea5c76e9..aa268acd8548df9d848a82e2851cd4347888fa8a 100644 --- a/fda-metadata-db/Dockerfile +++ b/fda-metadata-db/Dockerfile @@ -21,6 +21,7 @@ RUN mvn -q clean install > /dev/null ###### SECOND STAGE ###### FROM mariadb:10.5 as runtime +# install curl for prometheus metrics RUN apt-get update && apt-get install -y curl RUN cd /tmp && curl -LO https://github.com/prometheus/node_exporter/releases/download/v0.18.1/node_exporter-0.18.1.linux-amd64.tar.gz && tar -xvf node_exporter-0.18.1.linux-amd64.tar.gz && mv node_exporter-0.18.1.linux-amd64/node_exporter /usr/local/bin/ @@ -30,6 +31,9 @@ ENV METADATA_PASSWORD=dbrepo ENV MARIADB_DATABASE="${METADATA_DB}" ENV MARIADB_ROOT_PASSWORD="${METADATA_PASSWORD}" +# force lower-case table names +COPY ./51-dbrepo.cnf /etc/mysql/mariadb.conf.d/51-dbrepo.cnf + # Scripts are copied to /docker-entrypoint-initdb.d/ in docker-compose from analyze service HEALTHCHECK --interval=10s --timeout=5s --retries=12 CMD mysqladmin ping --user="$METADATA_USERNAME" --password="$METADATA_PASSWORD" --silent diff --git a/fda-metadata-db/api/src/main/java/at/tuwien/api/user/UserBriefDto.java b/fda-metadata-db/api/src/main/java/at/tuwien/api/user/UserBriefDto.java index db7b84038c5018395849a532c8926a315aebd9d8..fd4c54484fe1a7f755e71041899de9ffeaf547c2 100644 --- a/fda-metadata-db/api/src/main/java/at/tuwien/api/user/UserBriefDto.java +++ b/fda-metadata-db/api/src/main/java/at/tuwien/api/user/UserBriefDto.java @@ -22,10 +22,6 @@ public class UserBriefDto { @JsonProperty("sub") private String id; - @ToString.Exclude - @org.springframework.data.annotation.Transient - private List<GrantedAuthorityDto> authorities; - @NotNull @JsonProperty("preferred_username") @Schema(example = "jcarberry", description = "Only contains lowercase characters") @@ -34,13 +30,6 @@ public class UserBriefDto { @Schema(example = "Josiah Carberry") private String name; - @JsonProperty("titles_before") - @Schema(example = "Prof.") - private String titlesBefore; - - @JsonProperty("titles_after") - private String titlesAfter; - @JsonProperty("given_name") @Schema(example = "Josiah") private String firstname; @@ -49,18 +38,6 @@ public class UserBriefDto { @Schema(example = "Carberry") private String lastname; - @Schema(example = "Brown University") - private String affiliation; - - @Schema(example = "0000-0002-1825-0097") - private String orcid; - - @JsonIgnore - @JsonProperty("theme_dark") - @Schema(example = "true") - @org.springframework.data.annotation.Transient - private Boolean themeDark; - @JsonIgnore @JsonProperty("email_verified") @Schema(example = "true") diff --git a/fda-metadata-db/api/src/main/java/at/tuwien/api/user/UserDto.java b/fda-metadata-db/api/src/main/java/at/tuwien/api/user/UserDto.java index 4ea96b26876d00cc68a845ad78733e7301de5e94..f48aab4e6938cf87a4c9b9cf16089296621fcf1d 100644 --- a/fda-metadata-db/api/src/main/java/at/tuwien/api/user/UserDto.java +++ b/fda-metadata-db/api/src/main/java/at/tuwien/api/user/UserDto.java @@ -22,10 +22,6 @@ public class UserDto { @JsonProperty("sub") private String id; - @ToString.Exclude - @org.springframework.data.annotation.Transient - private List<GrantedAuthorityDto> authorities; - @NotNull @JsonProperty("preferred_username") @Schema(example = "jcarberry", description = "Only contains lowercase characters") @@ -34,13 +30,6 @@ public class UserDto { @Schema(example = "Josiah Carberry") private String name; - @JsonProperty("titles_before") - @Schema(example = "Prof.") - private String titlesBefore; - - @JsonProperty("titles_after") - private String titlesAfter; - @JsonProperty("given_name") @Schema(example = "Josiah") private String firstname; @@ -49,23 +38,6 @@ public class UserDto { @Schema(example = "Carberry") private String lastname; - @Schema(example = "Brown University") - private String affiliation; - - @Schema(example = "0000-0002-1825-0097") - private String orcid; - - @NotNull - @Schema(description = "Roles of the user", example = "[ROLE_RESEARCHER]") - @org.springframework.data.annotation.Transient - private List<String> roles; - - @NotNull - @JsonProperty("theme_dark") - @Schema(example = "true") - @org.springframework.data.annotation.Transient - private Boolean themeDark; - @EqualsAndHashCode.Exclude @org.springframework.data.annotation.Transient private List<ContainerDto> containers; @@ -78,12 +50,6 @@ public class UserDto { @org.springframework.data.annotation.Transient private List<ContainerDto> identifiers; - @JsonIgnore - @ToString.Exclude - @EqualsAndHashCode.Exclude - @org.springframework.data.annotation.Transient - private String password; - @NotNull @Schema(example = "jcarberry@brown.edu") @org.springframework.data.annotation.Transient diff --git a/fda-metadata-db/entities/src/main/java/at/tuwien/entities/user/User.java b/fda-metadata-db/entities/src/main/java/at/tuwien/entities/user/User.java index aebd5075b6def7c4542399d4f719d5e26def42db..f394a018034cd8eaa4d9e44d619934fcb480be25 100644 --- a/fda-metadata-db/entities/src/main/java/at/tuwien/entities/user/User.java +++ b/fda-metadata-db/entities/src/main/java/at/tuwien/entities/user/User.java @@ -1,14 +1,13 @@ package at.tuwien.entities.user; import at.tuwien.entities.container.Container; +import at.tuwien.entities.database.Database; +import at.tuwien.entities.identifier.Identifier; import lombok.*; -import org.hibernate.annotations.GenericGenerator; -import org.springframework.data.annotation.CreatedDate; -import org.springframework.data.annotation.LastModifiedDate; +import org.hibernate.annotations.Immutable; import org.springframework.data.jpa.domain.support.AuditingEntityListener; import javax.persistence.*; -import java.time.Instant; import java.util.List; @Data @@ -17,25 +16,15 @@ import java.util.List; @AllArgsConstructor @NoArgsConstructor @ToString +@Immutable @EntityListeners(AuditingEntityListener.class) @EqualsAndHashCode(onlyExplicitlyIncluded = true) -@Table(name = "mdb_users") -@NamedNativeQueries({ - @NamedNativeQuery(name = "User.findAll", - query = "SELECT e.* FROM `keycloak`.`REALM` r JOIN `keycloak`.`USER_ENTITY` e ON r.`ID` = e.`REALM_ID` WHERE r.`NAME` = 'dbrepo' AND e.`USERNAME` != 'system'", - resultClass = User.class), - @NamedNativeQuery(name = "User.findByUsername", - query = "SELECT e.* FROM `keycloak`.`REALM` r JOIN `keycloak`.`USER_ENTITY` e ON r.`ID` = e.`REALM_ID` WHERE r.`NAME` = 'dbrepo' AND e.`USERNAME` = ?", - resultClass = User.class) -}) +@Table(name = "user_entity") public class User { - @Id @EqualsAndHashCode.Include - @GeneratedValue(generator = "users-sequence") - @GenericGenerator(name = "users-sequence", strategy = "increment") - @Column(updatable = false, nullable = false) + @Column(nullable = false) private String id; @Column(unique = true, nullable = false) @@ -47,46 +36,30 @@ public class User { @Column(name = "last_name") private String lastname; - @Column(name = "preceding_titles") - private String titlesBefore; - - @Column(name = "postpositioned_title") - private String titlesAfter; - - @Column(name = "main_email", unique = true, nullable = false) + @Column(unique = true, nullable = false) private String email; - @Column - private String affiliation; - - @Column - private String orcid; - @Column(nullable = false) - private Boolean themeDark; - - @Column(name = "main_email_verified", nullable = false) private Boolean emailVerified; - @ToString.Exclude - @Column(nullable = false) - private String password; - + @Transient @ToString.Exclude @Column(nullable = false) private String databasePassword; @Transient @ToString.Exclude - @OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL, mappedBy = "creator") + @OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL, mappedBy = "owner") private List<Container> containers; - @CreatedDate - @Column(nullable = false, updatable = false) - private Instant created; + @Transient + @ToString.Exclude + @OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL, mappedBy = "owner") + private List<Database> databases; - @LastModifiedDate - @Column(name = "last_modified") - private Instant lastModified; + @Transient + @ToString.Exclude + @OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL, mappedBy = "creator") + private List<Identifier> identifiers; } diff --git a/fda-metadata-db/setup-schema.sql b/fda-metadata-db/setup-schema.sql index b2f0309313b59294ff2e059f7d2af5e24093bf37..d390aac4874431231bd4095e7abacedd30ba0c60 100644 --- a/fda-metadata-db/setup-schema.sql +++ b/fda-metadata-db/setup-schema.sql @@ -1,5 +1,3 @@ -CREATE DATABASE IF NOT EXISTS `keycloak`; - BEGIN; CREATE TABLE IF NOT EXISTS `fda`.`mdb_images` diff --git a/fda-query-service/rest-service/src/main/java/at/tuwien/endpoint/AbstractEndpoint.java b/fda-query-service/rest-service/src/main/java/at/tuwien/endpoint/AbstractEndpoint.java deleted file mode 100644 index 0abead058bd32565d4625f123668684eef1f12b4..0000000000000000000000000000000000000000 --- a/fda-query-service/rest-service/src/main/java/at/tuwien/endpoint/AbstractEndpoint.java +++ /dev/null @@ -1,301 +0,0 @@ -package at.tuwien.endpoint; - -import at.tuwien.SortType; -import at.tuwien.api.database.query.ExecuteStatementDto; -import at.tuwien.config.QueryConfig; -import at.tuwien.entities.database.AccessType; -import at.tuwien.entities.database.Database; -import at.tuwien.entities.database.DatabaseAccess; -import at.tuwien.entities.database.table.Table; -import at.tuwien.entities.identifier.Identifier; -import at.tuwien.exception.*; -import at.tuwien.service.AccessService; -import at.tuwien.service.DatabaseService; -import at.tuwien.service.IdentifierService; -import at.tuwien.service.TableService; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.core.Authentication; - -import java.security.Principal; -import java.util.Arrays; -import java.util.LinkedList; -import java.util.List; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import static at.tuwien.entities.identifier.VisibilityType.EVERYONE; - -@Slf4j -public abstract class AbstractEndpoint { - - private final QueryConfig queryConfig; - private final TableService tableService; - private final AccessService accessService; - private final DatabaseService databaseService; - private final IdentifierService identifierService; - - @Autowired - protected AbstractEndpoint(TableService tableService, AccessService accessService, DatabaseService databaseService, - IdentifierService identifierService, QueryConfig queryConfig) { - this.queryConfig = queryConfig; - this.tableService = tableService; - this.accessService = accessService; - this.databaseService = databaseService; - this.identifierService = identifierService; - } - - protected Boolean hasDatabasePermission(Long containerId, Long databaseId, String permissionCode, - Principal principal) throws NotAllowedException { - log.trace("validate database permission, containerId={}, databaseId={}, permissionCode={}, principal={}", - containerId, databaseId, permissionCode, principal); - final Database database; - try { - database = databaseService.find(containerId, databaseId); - } catch (DatabaseNotFoundException e) { - log.error("Failed to find database with id {}", databaseId); - return false; - } - /* view-only operations are allowed on public databases */ - if (database.getIsPublic() && List.of("DATA_VIEW", "DATA_HISTORY", "QUERY_VIEW_ALL").contains(permissionCode)) { - log.debug("grant permission {} because database is public", permissionCode); - return true; - } - if (List.of("LIST_VIEWS", "FIND_VIEW", "DATA_VIEW").contains(permissionCode)) { - log.debug("grant permission {} because it is allowed on public/private databases", permissionCode); - return true; - } - if (principal == null) { - log.error("Failed to grant permission {} because principal is null", permissionCode); - return false; - } - final DatabaseAccess access = accessService.find(databaseId, principal.getName()); - /* modification operations are limited to the creator */ - if (database.getOwner().getUsername().equals(principal.getName())) { - log.debug("grant permission {} because user {} is owner {}", permissionCode, principal.getName(), - database.getOwner().getUsername()); - return true; - } - /* check view access */ - if (List.of("QUERY_EXECUTE").contains(permissionCode)) { - log.debug("grant permission {} because user has access {}", permissionCode, access.getType()); - return true; - } - /* write permission */ - final Authentication authentication = (Authentication) principal /* with pre-authorization this always holds */; - if (authentication.getAuthorities().stream().noneMatch(a -> a.getAuthority().equals("ROLE_RESEARCHER"))) { - log.error("Failed to grant permission {} because current user misses authority 'ROLE_RESEARCHER'", - permissionCode); - return false; - } - log.error("Failed to grant permission {} because database is not owned by the current user", permissionCode); - return false; - } - - protected void validateDataParams(Long page, Long size) throws PaginationException { - log.trace("validate data params, page={}, size={}", page, size); - if ((page == null && size != null) || (page != null && size == null)) { - log.error("Failed to validate page and/or size number, either both are present or none"); - throw new PaginationException("Failed to validate page and/or size number"); - } - if (page != null && page < 0) { - log.error("Failed to validate page number, is lower than zero"); - throw new PaginationException("Failed to validate page number"); - } - if (size != null && size <= 0) { - log.error("Failed to validate size number, is lower or equal than zero"); - throw new PaginationException("Failed to validate size number"); - } - } - - protected void validateDataParams(Long page, Long size, SortType sortDirection, String sortColumn) - throws PaginationException, SortException { - log.trace("validate data params, page={}, size={}, sortDirection={}, sortColumn={}", page, size, - sortDirection, sortColumn); - validateDataParams(page, size); - if ((sortDirection == null && sortColumn != null) || (sortDirection != null && sortColumn == null)) { - log.error("Failed to validate sort direction and/or sort column, either both are present or none"); - throw new SortException("Failed to validate sort direction and/or sort column"); - } - } - - /** - * Do not allow aggregate functions and comments - * https://mariadb.com/kb/en/aggregate-functions/ - */ - protected void validateForbiddenStatements(ExecuteStatementDto data) throws QueryMalformedException { - final List<String> words = new LinkedList<>(); - Arrays.stream(queryConfig.getNotSupportedKeywords()) - .forEach(keyword -> { - final Pattern pattern = Pattern.compile(keyword); - final Matcher matcher = pattern.matcher(data.getStatement()); - final boolean found = matcher.find(); - if (found) { - words.add(keyword); - } - }); - if (words.size() == 0) { - return; - } - log.error("Query contains forbidden keyword(s): {}", words); - log.debug("forbidden keywords: {}", words); - throw new QueryMalformedException("Query contains forbidden keyword(s): " + Arrays.toString(words.toArray())); - } - - protected Boolean hasTablePermission(Long containerId, Long databaseId, Long tableId, String permissionCode, - Principal principal) throws NotAllowedException { - log.trace("validate queue permission, containerId={}, databaseId={}, tableId={}, permissionCode={}, principal={}", - containerId, databaseId, tableId, permissionCode, principal); - final Database database; - try { - database = databaseService.find(containerId, databaseId); - } catch (DatabaseNotFoundException e) { - log.error("Failed to find database with id {}", databaseId); - return false; - } - final Table table; - try { - table = tableService.find(containerId, databaseId, tableId); - } catch (TableNotFoundException e) { - log.error("Failed to find table with id {} in database with id {}", tableId, databaseId); - return false; - } catch (DatabaseNotFoundException e) { - /* can never occur here */ - return false; - } - /* view-only operations are allowed on public databases */ - if (database.getIsPublic() && List.of("TABLE_EXPORT", "DATA_VIEW", "DATA_HISTORY").contains(permissionCode)) { - log.debug("grant permission {} because database is public", permissionCode); - return true; - } - if (principal == null) { - log.error("Failed to grant permission {} because principal is null", permissionCode); - return false; - } - /* modification operations for creators are trivial */ - if (table.getCreator().getUsername().equals(principal.getName())) { - log.debug("grant permission {} because user {} is table creator {}", permissionCode, principal.getName(), - table.getCreator().getUsername()); - return true; - } - final Authentication authentication = (Authentication) principal /* with pre-authorization this always holds */; - if (authentication.getAuthorities().stream().noneMatch(a -> a.getAuthority().equals("ROLE_RESEARCHER"))) { - log.error("Failed to grant permission {} because current user misses authority 'ROLE_RESEARCHER'", - permissionCode); - return false; - } - final DatabaseAccess access = accessService.find(databaseId, principal.getName()); - /* check view access */ - if (List.of("TABLE_EXPORT", "DATA_VIEW", "DATA_HISTORY", "QUERY_VIEW_ALL", "QUERY_RE_EXECUTE", "QUERY_VIEW", "FIND_VIEW").contains(permissionCode)) { - log.trace("grant permission {} because user has access {}", permissionCode, access.getType()); - return true; - } - if (List.of("DATA_INSERT", "DATA_UPDATE", "DATA_DELETE", "QUERY_PERSIST").contains(permissionCode) && (access.getType().equals(AccessType.WRITE_ALL))) { - /* write own is already effective with creator check above */ - log.debug("grant permission {} because user {} is has table write permission {}", permissionCode, principal.getName(), - access.getType()); - return true; - } - log.debug("failed to grant permission {} because database is not owner by the current user and also has not appropriate access", permissionCode); - return false; - } - - protected Boolean hasQueryPermission(Long containerId, Long databaseId, Long queryId, String permissionCode, - Principal principal) throws NotAllowedException { - log.trace("validate query permission, containerId={}, databaseId={}, queryId={}, permissionCode={}, principal={}", - containerId, databaseId, queryId, permissionCode, principal); - final Database database; - try { - database = databaseService.find(containerId, databaseId); - } catch (DatabaseNotFoundException e) { - log.error("Failed to find database with id {}", databaseId); - return false; - } - if (hasPublicIdentifier(databaseId, queryId, permissionCode)) { - return true; - } - /* modification operations are limited to the creator */ - if (isMyPrivateIdentifier(databaseId, queryId, principal, permissionCode)) { - return true; - } - /* view-only operations are allowed on public databases */ - if (database.getIsPublic() && List.of("QUERY_VIEW_ALL", "QUERY_VIEW", "QUERY_EXPORT", "QUERY_RE_EXECUTE").contains( - permissionCode)) { - log.debug("grant permission {} because database is public", permissionCode); - return true; - } - if (principal == null) { - log.error("Failed to grant permission {} because principal is null", permissionCode); - return false; - } - final Authentication authentication = (Authentication) principal /* with pre-authorization this always holds */; - if (authentication.getAuthorities().stream().noneMatch(a -> a.getAuthority().equals("ROLE_RESEARCHER"))) { - log.error("Failed to grant permission {} because current user misses authority 'ROLE_RESEARCHER'", - permissionCode); - return false; - } - /* modification operations are limited to the creator */ - if (database.getCreator().getUsername().equals(principal.getName())) { - log.debug("grant permission {} because database is private and creator is the current user", - permissionCode); - return true; - } - final DatabaseAccess access = accessService.find(databaseId, principal.getName()); - /* check view access */ - if (List.of("DATA_VIEW", "DATA_HISTORY", "QUERY_VIEW_ALL", "QUERY_RE_EXECUTE", "QUERY_VIEW", "FIND_VIEW", "QUERY_EXPORT").contains(permissionCode)) { - log.trace("grant permission {} because user has access {}", permissionCode, access.getType()); - return true; - } - if (access.getType().equals(AccessType.WRITE_ALL)) { - log.trace("grant permission {} because user has access {}", permissionCode, access.getType()); - return true; - } - log.debug("failed to grant permission {} because database is not owner by the current user and also has not appropriate access", permissionCode); - return false; - } - - protected Boolean hasPublicIdentifier(Long databaseId, Long queryId, String permissionCode) { - log.trace("validate has public identifier, databaseId={}, queryId={}, permissionCode={}", databaseId, queryId, - permissionCode); - final Identifier identifier; - try { - identifier = identifierService.findByDatabaseIdAndQueryId(databaseId, queryId); - } catch (IdentifierNotFoundException e) { - return false; - } - if (identifier.getVisibility().equals(EVERYONE)) { - log.debug("grant permission {} because identifier visibility is public", permissionCode); - return true; - } - log.error("Failed to grant permission {} because identifier visibility is not public", permissionCode); - return false; - } - - protected Boolean isMyPrivateIdentifier(Long databaseId, Long queryId, Principal principal, String permissionCode) { - log.trace("validate is my private identifier, databaseId={}, queryId={}, permissionCode={}", databaseId, queryId, - permissionCode); - final Identifier identifier; - try { - identifier = identifierService.findByDatabaseIdAndQueryId(databaseId, queryId); - } catch (IdentifierNotFoundException e) { - return false; - } - if (identifier.getDatabase().getIsPublic()) { - log.debug("grant permission {} because database is public", permissionCode); - return true; - } - if (principal == null) { - log.error("Failed to grant permission {} because database is private and principal is null", - permissionCode); - return false; - } - if (identifier.getCreator().getUsername().equals(principal.getName())) { - log.debug("grant permission {} because database is private and identifier creator is the current user", - permissionCode); - return true; - } - log.error("Failed to grant permission {} because database is private and identifier creator is not the current user", permissionCode); - return false; - } - -} diff --git a/fda-query-service/rest-service/src/main/java/at/tuwien/endpoint/ExportEndpoint.java b/fda-query-service/rest-service/src/main/java/at/tuwien/endpoint/ExportEndpoint.java index 33a9c761a6ebd7d6ffd065362c5db011faf9483f..356a3fc77ae7d5d975277e1d17c95454585b95c2 100644 --- a/fda-query-service/rest-service/src/main/java/at/tuwien/endpoint/ExportEndpoint.java +++ b/fda-query-service/rest-service/src/main/java/at/tuwien/endpoint/ExportEndpoint.java @@ -12,6 +12,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.io.InputStreamResource; import org.springframework.http.HttpHeaders; import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.*; @@ -23,19 +24,18 @@ import java.time.Instant; @CrossOrigin(origins = "*") @RestController @RequestMapping("/api/container/{id}/database/{databaseId}/table/{tableId}/export") -public class ExportEndpoint extends AbstractEndpoint { +public class ExportEndpoint { private final QueryService queryService; @Autowired - public ExportEndpoint(QueryService queryService, DatabaseService databaseService, AccessService accessService, - IdentifierService identifierService, TableService tableService, QueryConfig queryConfig) { - super(tableService, accessService, databaseService, identifierService, queryConfig); + public ExportEndpoint(QueryService queryService) { this.queryService = queryService; } @GetMapping @Transactional(readOnly = true) + @PreAuthorize("hasAuthority('export-table')") @Timed(value = "table.export", description = "Time needed to export table data") @Operation(summary = "Export table", security = @SecurityRequirement(name = "bearerAuth")) public ResponseEntity<InputStreamResource> export(@NotNull @PathVariable("id") Long containerId, @@ -45,13 +45,9 @@ public class ExportEndpoint extends AbstractEndpoint { Principal principal) throws TableNotFoundException, DatabaseConnectionException, TableMalformedException, DatabaseNotFoundException, ImageNotSupportedException, PaginationException, ContainerNotFoundException, - FileStorageException, NotAllowedException, QueryMalformedException, UserNotFoundException { + FileStorageException, QueryMalformedException, UserNotFoundException { log.debug("endpoint export table, id={}, databaseId={}, tableId={}, timestamp={}, principal={}", containerId, databaseId, tableId, timestamp, principal); - if (!hasTablePermission(containerId, databaseId, tableId, "TABLE_EXPORT", principal)) { - log.error("Missing data export permission"); - throw new NotAllowedException("Missing data export permission"); - } final HttpHeaders headers = new HttpHeaders(); final ExportResource resource = queryService.tableFindAll(containerId, databaseId, tableId, timestamp, principal); headers.add("Content-Disposition", "attachment; filename=\"" + resource.getFilename() + "\""); diff --git a/fda-query-service/rest-service/src/main/java/at/tuwien/endpoint/QueryEndpoint.java b/fda-query-service/rest-service/src/main/java/at/tuwien/endpoint/QueryEndpoint.java index 6f4faa2b4197be398b573ea68f76eddc8c42ab8b..31f447c71ce14c54fbcc7591e30a88f492165e29 100644 --- a/fda-query-service/rest-service/src/main/java/at/tuwien/endpoint/QueryEndpoint.java +++ b/fda-query-service/rest-service/src/main/java/at/tuwien/endpoint/QueryEndpoint.java @@ -7,6 +7,7 @@ import at.tuwien.config.QueryConfig; import at.tuwien.querystore.Query; import at.tuwien.exception.*; import at.tuwien.service.*; +import at.tuwien.validation.EndpointValidator; import io.micrometer.core.annotation.Timed; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.security.SecurityRequirement; @@ -15,6 +16,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.*; @@ -22,19 +24,20 @@ import javax.validation.Valid; import javax.validation.constraints.NotNull; import java.security.Principal; + @Log4j2 @RestController @RequestMapping("/api/container/{id}/database/{databaseId}/query") -public class QueryEndpoint extends AbstractEndpoint { +public class QueryEndpoint { private final QueryService queryService; private final StoreService storeService; + private final EndpointValidator endpointValidator; @Autowired - public QueryEndpoint(QueryService queryService, StoreService storeService, DatabaseService databaseService, - IdentifierService identifierService, TableService tableService, AccessService accessService, - QueryConfig queryConfig) { - super(tableService, accessService, databaseService, identifierService, queryConfig); + public QueryEndpoint(QueryService queryService, StoreService storeService, + EndpointValidator endpointValidator) { + this.endpointValidator = endpointValidator; this.queryService = queryService; this.storeService = storeService; } @@ -42,6 +45,7 @@ public class QueryEndpoint extends AbstractEndpoint { @PostMapping @Transactional(readOnly = true) @Timed(value = "query.execute", description = "Time needed to execute a query") + @PreAuthorize("hasAuthority('execute-query')") @Operation(summary = "Execute query", security = @SecurityRequirement(name = "bearerAuth")) public ResponseEntity<QueryResultDto> execute(@NotNull @PathVariable("id") Long containerId, @NotNull @PathVariable("databaseId") Long databaseId, @@ -57,16 +61,12 @@ public class QueryEndpoint extends AbstractEndpoint { log.debug("endpoint execute query, containerId={}, databaseId={}, data={}, page={}, size={}, principal={}, sortDirection={}, sortColumn={}", containerId, databaseId, data, page, size, principal, sortDirection, sortColumn); /* check */ - if (!hasDatabasePermission(containerId, databaseId, "QUERY_EXECUTE", principal)) { - log.error("Missing execute query permission"); - throw new NotAllowedException("Missing execute query permission"); - } if (data.getStatement() == null || data.getStatement().isBlank()) { log.error("Failed to execute empty query"); throw new QueryMalformedException("Failed to execute empty query"); } - validateForbiddenStatements(data); - validateDataParams(page, size, sortDirection, sortColumn); + endpointValidator.validateForbiddenStatements(data); + endpointValidator.validateDataParams(page, size, sortDirection, sortColumn); /* execute */ final QueryResultDto result = queryService.execute(containerId, databaseId, data, principal, page, size, sortDirection, sortColumn); @@ -77,6 +77,7 @@ public class QueryEndpoint extends AbstractEndpoint { @GetMapping("/{queryId}/data") @Transactional(readOnly = true) + @PreAuthorize("hasAuthority('re-execute-query')") @Timed(value = "query.reexecute", description = "Time needed to re-execute a query") @Operation(summary = "Re-execute some query", security = @SecurityRequirement(name = "bearerAuth")) public ResponseEntity<QueryResultDto> reExecute(@NotNull @PathVariable("id") Long containerId, @@ -92,12 +93,7 @@ public class QueryEndpoint extends AbstractEndpoint { DatabaseConnectionException, SortException, PaginationException, UserNotFoundException { log.debug("endpoint re-execute query, containerId={}, databaseId={}, queryId={}, principal={}, page={}, size={}, sortDirection={}, sortColumn={}", containerId, databaseId, queryId, principal, page, size, sortDirection, sortColumn); - /* check */ - if (!hasQueryPermission(containerId, databaseId, queryId, "QUERY_RE_EXECUTE", principal)) { - log.error("Missing re-execute query permission"); - throw new NotAllowedException("Missing re-execute query permission"); - } - validateDataParams(page, size, sortDirection, sortColumn); + endpointValidator.validateDataParams(page, size, sortDirection, sortColumn); /* execute */ final Query query = storeService.findOne(containerId, databaseId, queryId, principal); final QueryResultDto result = queryService.reExecute(containerId, databaseId, query, page, size, @@ -110,6 +106,7 @@ public class QueryEndpoint extends AbstractEndpoint { @GetMapping("/{queryId}/data/count") @Transactional(readOnly = true) + @PreAuthorize("hasAuthority('re-execute-query')") @Timed(value = "query.reexecute.count", description = "Time needed to re-execute a query") @Operation(summary = "Re-execute some query", security = @SecurityRequirement(name = "bearerAuth")) public ResponseEntity<Long> reExecuteCount(@NotNull @PathVariable("id") Long containerId, @@ -121,11 +118,6 @@ public class QueryEndpoint extends AbstractEndpoint { DatabaseConnectionException, UserNotFoundException { log.debug("endpoint re-execute query count, containerId={}, databaseId={}, queryId={}, principal={}", containerId, databaseId, queryId, principal); - /* check */ - if (!hasQueryPermission(containerId, databaseId, queryId, "QUERY_RE_EXECUTE", principal)) { - log.error("Missing re-execute query permission"); - throw new NotAllowedException("Missing re-execute query permission"); - } /* execute */ final Query query = storeService.findOne(containerId, databaseId, queryId, principal); final Long result = queryService.reExecuteCount(containerId, databaseId, query, principal); @@ -136,6 +128,7 @@ public class QueryEndpoint extends AbstractEndpoint { @GetMapping("/{queryId}/export") @Transactional(readOnly = true) + @PreAuthorize("hasAuthority('export-query')") @Timed(value = "query.export", description = "Time needed to export query data") @Operation(summary = "Exports some query", security = @SecurityRequirement(name = "bearerAuth")) public ResponseEntity<?> export(@NotNull @PathVariable("id") Long containerId, @@ -148,10 +141,6 @@ public class QueryEndpoint extends AbstractEndpoint { QueryMalformedException, DatabaseConnectionException, UserNotFoundException { log.debug("endpoint export query, containerId={}, databaseId={}, queryId={}, accept={}, principal={}", containerId, databaseId, queryId, accept, principal); - if (!hasQueryPermission(containerId, databaseId, queryId, "QUERY_EXPORT", principal)) { - log.error("Missing export query permission"); - throw new NotAllowedException("Missing export query permission"); - } log.trace("checking if query exists in the query store"); final Query query = storeService.findOne(containerId, databaseId, queryId, principal); log.trace("querystore returned query {}", query); diff --git a/fda-query-service/rest-service/src/main/java/at/tuwien/endpoint/StoreEndpoint.java b/fda-query-service/rest-service/src/main/java/at/tuwien/endpoint/StoreEndpoint.java index 54e31bf3d1d0ac4879838919a3b0079f0099ccb3..03c527325cfb2f0d68de2c72a7bf94dca0888b47 100644 --- a/fda-query-service/rest-service/src/main/java/at/tuwien/endpoint/StoreEndpoint.java +++ b/fda-query-service/rest-service/src/main/java/at/tuwien/endpoint/StoreEndpoint.java @@ -19,6 +19,7 @@ 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.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.*; @@ -31,7 +32,7 @@ import java.util.stream.Collectors; @Log4j2 @RestController @RequestMapping("/api/container/{id}/database/{databaseId}/query") -public class StoreEndpoint extends AbstractEndpoint { +public class StoreEndpoint { private final UserMapper userMapper; private final QueryMapper queryMapper; @@ -41,35 +42,30 @@ public class StoreEndpoint extends AbstractEndpoint { private final IdentifierService identifierService; @Autowired - public StoreEndpoint(QueryConfig queryConfig, UserMapper userMapper, QueryMapper queryMapper, - UserService userService, StoreService storeService, DatabaseService databaseService, - IdentifierService identifierService, TableService tableService, AccessService accessService, - IdentifierMapper identifierMapper, IdentifierService identifierService1) { - super(tableService, accessService, databaseService, identifierService, queryConfig); + public StoreEndpoint(UserMapper userMapper, QueryMapper queryMapper, UserService userService, + StoreService storeService, IdentifierMapper identifierMapper, + IdentifierService identifierService) { this.userMapper = userMapper; this.queryMapper = queryMapper; this.userService = userService; this.storeService = storeService; this.identifierMapper = identifierMapper; - this.identifierService = identifierService1; + this.identifierService = identifierService; } @GetMapping @Transactional(readOnly = true) @Timed(value = "store.list", description = "Time needed to list queries from the query store") + @PreAuthorize("hasAuthority('find-queries')") @Operation(summary = "Find queries", security = @SecurityRequirement(name = "bearerAuth")) public ResponseEntity<List<QueryBriefDto>> findAll(@NotNull @PathVariable("id") Long containerId, @NotNull @PathVariable("databaseId") Long databaseId, @RequestParam(value = "persisted", required = false) Boolean persisted, Principal principal) throws QueryStoreException, - DatabaseNotFoundException, ImageNotSupportedException, ContainerNotFoundException, NotAllowedException, + DatabaseNotFoundException, ImageNotSupportedException, ContainerNotFoundException, DatabaseConnectionException, TableMalformedException, UserNotFoundException { log.debug("endpoint list queries, containerId={}, databaseId={}, persisted={}, principal={}", containerId, databaseId, persisted, principal); - if (!hasDatabasePermission(containerId, databaseId, "QUERY_VIEW_ALL", principal)) { - log.error("Missing view all queries permission"); - throw new NotAllowedException("Missing view all queries permission"); - } final List<Query> queries = storeService.findAll(containerId, databaseId, persisted, principal); final List<Identifier> identifiers = identifierService.findAll(); final List<User> users = userService.findAll(); @@ -93,6 +89,7 @@ public class StoreEndpoint extends AbstractEndpoint { @GetMapping("/{queryId}") @Transactional(readOnly = true) + @PreAuthorize("hasAuthority('find-query')") @Timed(value = "store.find", description = "Time needed to find a query from the query store") @Operation(summary = "Find some query", security = @SecurityRequirement(name = "bearerAuth")) public ResponseEntity<QueryDto> find(@NotNull @PathVariable("id") Long containerId, @@ -104,10 +101,6 @@ public class StoreEndpoint extends AbstractEndpoint { DatabaseConnectionException { log.debug("endpoint find query, containerId={}, databaseId={}, queryId={}, principal={}", containerId, databaseId, queryId, principal); - if (!hasQueryPermission(containerId, databaseId, queryId, "QUERY_VIEW", principal)) { - log.error("Missing view query permission"); - throw new NotAllowedException("Missing view query permission"); - } final Query query = storeService.findOne(containerId, databaseId, queryId, principal); final QueryDto dto = queryMapper.queryToQueryDto(query); final User creator = userService.findByUsername(query.getCreatedBy()); @@ -124,6 +117,7 @@ public class StoreEndpoint extends AbstractEndpoint { @PutMapping("/{queryId}") @Transactional(readOnly = true) + @PreAuthorize("hasAuthority('persist-query')") @Timed(value = "store.persist", description = "Time needed to persist a query in the query store") @Operation(summary = "Persist some query", security = @SecurityRequirement(name = "bearerAuth")) public ResponseEntity<QueryDto> persist(@NotNull @PathVariable("id") Long containerId, @@ -131,14 +125,10 @@ public class StoreEndpoint extends AbstractEndpoint { @NotNull @PathVariable("queryId") Long queryId, @NotNull Principal principal) throws QueryStoreException, DatabaseNotFoundException, ImageNotSupportedException, - NotAllowedException, DatabaseConnectionException, UserNotFoundException, QueryNotFoundException, + DatabaseConnectionException, UserNotFoundException, QueryNotFoundException, QueryAlreadyPersistedException { log.debug("endpoint persist query, container, containerId={}, databaseId={}, queryId={}, principal={}", containerId, databaseId, queryId, principal); - if (!hasQueryPermission(containerId, databaseId, queryId, "QUERY_PERSIST", principal)) { - log.error("Missing query persist permission"); - throw new NotAllowedException("Missing query persist permission"); - } final Query check = storeService.findOne(containerId, databaseId, queryId, principal); if (check.getIsPersisted()) { log.error("Failed to persist, is already persisted"); diff --git a/fda-query-service/rest-service/src/main/java/at/tuwien/endpoint/TableDataEndpoint.java b/fda-query-service/rest-service/src/main/java/at/tuwien/endpoint/TableDataEndpoint.java index 277e622b6273f66827b116972b8498e30b20dc43..1edc99ccb0a102d0023e3729ea8fa6bd16b69068 100644 --- a/fda-query-service/rest-service/src/main/java/at/tuwien/endpoint/TableDataEndpoint.java +++ b/fda-query-service/rest-service/src/main/java/at/tuwien/endpoint/TableDataEndpoint.java @@ -6,15 +6,16 @@ import at.tuwien.api.database.query.QueryResultDto; import at.tuwien.api.database.table.TableCsvDeleteDto; import at.tuwien.api.database.table.TableCsvDto; import at.tuwien.api.database.table.TableCsvUpdateDto; -import at.tuwien.config.QueryConfig; import at.tuwien.exception.*; import at.tuwien.service.*; +import at.tuwien.validation.EndpointValidator; import io.micrometer.core.annotation.Timed; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.security.SecurityRequirement; import lombok.extern.log4j.Log4j2; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.*; @@ -27,21 +28,21 @@ import java.time.Instant; @CrossOrigin(origins = "*") @RestController @RequestMapping("/api/container/{id}/database/{databaseId}/table/{tableId}/data") -public class TableDataEndpoint extends AbstractEndpoint { +public class TableDataEndpoint { private final QueryService queryService; + private final EndpointValidator endpointValidator; @Autowired - public TableDataEndpoint(QueryService queryService, DatabaseService databaseService, - IdentifierService identifierService, TableService tableService, - AccessService accessService, QueryConfig queryConfig) { - super(tableService, accessService, databaseService, identifierService, queryConfig); + public TableDataEndpoint(QueryService queryService, EndpointValidator endpointValidator) { this.queryService = queryService; + this.endpointValidator = endpointValidator; } @PostMapping @Transactional @Timed(value = "data.insert", description = "Time needed to insert data into a table") + @PreAuthorize("hasAuthority('modify-data')") @Operation(summary = "Insert data", security = @SecurityRequirement(name = "bearerAuth")) public ResponseEntity<Void> insert(@NotNull @PathVariable("id") Long containerId, @NotNull @PathVariable("databaseId") Long databaseId, @@ -53,10 +54,6 @@ public class TableDataEndpoint extends AbstractEndpoint { UserNotFoundException { log.debug("endpoint insert data, containerId={}, databaseId={}, tableId={}, data={}, principal={}", containerId, databaseId, tableId, data, principal); - if (!hasTablePermission(containerId, databaseId, tableId, "DATA_INSERT", principal)) { - log.error("Missing data insert permission"); - throw new NotAllowedException("Missing data insert permission"); - } queryService.insert(containerId, databaseId, tableId, data, principal); return ResponseEntity.accepted() .build(); @@ -65,6 +62,7 @@ public class TableDataEndpoint extends AbstractEndpoint { @PutMapping @Transactional @Deprecated + @PreAuthorize("hasAuthority('modify-data')") @Timed(value = "data.update", description = "Time needed to update data in a table") @Operation(summary = "Update data", security = @SecurityRequirement(name = "bearerAuth")) public ResponseEntity<Void> update(@NotNull @PathVariable("id") Long containerId, @@ -73,14 +71,10 @@ public class TableDataEndpoint extends AbstractEndpoint { @NotNull @Valid @RequestBody TableCsvUpdateDto data, @NotNull Principal principal) throws TableNotFoundException, DatabaseNotFoundException, TableMalformedException, - ImageNotSupportedException, NotAllowedException, DatabaseConnectionException, QueryMalformedException, + ImageNotSupportedException, DatabaseConnectionException, QueryMalformedException, UserNotFoundException { log.debug("endpoint update data, containerId={}, databaseId={}, tableId={}, data={}, principal={}", containerId, databaseId, tableId, data, principal); - if (!hasTablePermission(containerId, databaseId, tableId, "DATA_UPDATE", principal)) { - log.error("Missing data update permission"); - throw new NotAllowedException("Missing data update permission"); - } queryService.update(containerId, databaseId, tableId, data, principal); return ResponseEntity.accepted() .build(); @@ -88,6 +82,7 @@ public class TableDataEndpoint extends AbstractEndpoint { @DeleteMapping @Transactional + @PreAuthorize("hasAuthority('modify-data')") @Timed(value = "data.delete", description = "Time needed to delete data into a table") @Operation(summary = "Delete data", security = @SecurityRequirement(name = "bearerAuth")) public ResponseEntity<Void> delete(@NotNull @PathVariable("id") Long containerId, @@ -96,14 +91,10 @@ public class TableDataEndpoint extends AbstractEndpoint { @NotNull @Valid @RequestBody TableCsvDeleteDto data, @NotNull Principal principal) throws TableNotFoundException, DatabaseNotFoundException, TableMalformedException, - ImageNotSupportedException, TupleDeleteException, NotAllowedException, ContainerNotFoundException, + ImageNotSupportedException, TupleDeleteException, ContainerNotFoundException, DatabaseConnectionException, QueryMalformedException, UserNotFoundException { log.debug("endpoint delete data, containerId={}, databaseId={}, tableId={}, data={}, principal={}", containerId, databaseId, tableId, data, principal); - if (!hasTablePermission(containerId, databaseId, tableId, "DATA_DELETE", principal)) { - log.error("Missing data delete permission"); - throw new NotAllowedException("Missing data delete permission"); - } queryService.delete(containerId, databaseId, tableId, data, principal); return ResponseEntity.accepted() .build(); @@ -111,6 +102,7 @@ public class TableDataEndpoint extends AbstractEndpoint { @PostMapping("/import") @Transactional + @PreAuthorize("hasAuthority('modify-data')") @Timed(value = "data.insertbulk", description = "Time needed to insert data from .csv into a table") @Operation(summary = "Insert data from csv", security = @SecurityRequirement(name = "bearerAuth")) public ResponseEntity<Void> importCsv(@NotNull @PathVariable("id") Long containerId, @@ -119,14 +111,10 @@ public class TableDataEndpoint extends AbstractEndpoint { @NotNull @Valid @RequestBody ImportDto data, @NotNull Principal principal) throws TableNotFoundException, DatabaseNotFoundException, TableMalformedException, - ImageNotSupportedException, ContainerNotFoundException, NotAllowedException, DatabaseConnectionException, + ImageNotSupportedException, ContainerNotFoundException, DatabaseConnectionException, QueryMalformedException, UserNotFoundException { log.debug("endpoint insert data from csv, containerId={}, databaseId={}, tableId={}, data={}, principal={}", containerId, databaseId, tableId, data, principal); - if (!hasTablePermission(containerId, databaseId, tableId, "DATA_INSERT", principal)) { - log.error("Missing data insert permission"); - throw new NotAllowedException("Missing data insert permission"); - } queryService.insert(containerId, databaseId, tableId, data, principal); return ResponseEntity.accepted() .build(); @@ -134,6 +122,7 @@ public class TableDataEndpoint extends AbstractEndpoint { @RequestMapping(method = {RequestMethod.GET, RequestMethod.HEAD}) @Transactional(readOnly = true) + @PreAuthorize("hasAuthority('get-data')") @Timed(value = "data.all", description = "Time needed to find all data from a table") @Operation(summary = "Find data", security = @SecurityRequirement(name = "bearerAuth")) public ResponseEntity<QueryResultDto> getAll(@NotNull @PathVariable("id") Long containerId, @@ -147,15 +136,11 @@ public class TableDataEndpoint extends AbstractEndpoint { @RequestParam(required = false) String sortColumn) throws TableNotFoundException, DatabaseNotFoundException, DatabaseConnectionException, ImageNotSupportedException, TableMalformedException, PaginationException, ContainerNotFoundException, - NotAllowedException, QueryMalformedException, SortException, UserNotFoundException { + QueryMalformedException, UserNotFoundException, SortException { log.debug("endpoint find table data, containerId={}, databaseId={}, tableId={}, principal={}, timestamp={}, page={}, size={}, sortDirection={}, sortColumn={}", containerId, databaseId, tableId, principal, timestamp, page, size, sortDirection, sortColumn); /* check */ - if (!hasTablePermission(containerId, databaseId, tableId, "DATA_VIEW", principal)) { - log.error("Missing data view permission"); - throw new NotAllowedException("Missing data view permission"); - } - validateDataParams(page, size, sortDirection, sortColumn); + endpointValidator.validateDataParams(page, size, sortDirection, sortColumn); final QueryResultDto response = queryService.tableFindAll(containerId, databaseId, tableId, timestamp, page, size, principal); log.trace("find table data resulted in result {}", response); return ResponseEntity.ok() @@ -163,6 +148,7 @@ public class TableDataEndpoint extends AbstractEndpoint { } @GetMapping("/count") + @PreAuthorize("hasAuthority('get-data')") @Timed(value = "data.all.count", description = "Time needed to get count of all data from a table") @Operation(summary = "Find data", security = @SecurityRequirement(name = "bearerAuth")) public ResponseEntity<Long> getCount(@NotNull @PathVariable("id") Long containerId, @@ -171,15 +157,10 @@ public class TableDataEndpoint extends AbstractEndpoint { @NotNull Principal principal, @RequestParam(required = false) Instant timestamp) throws TableNotFoundException, DatabaseNotFoundException, DatabaseConnectionException, - ImageNotSupportedException, TableMalformedException, PaginationException, ContainerNotFoundException, - QueryStoreException, NotAllowedException, QueryMalformedException, SortException, UserNotFoundException { + ImageNotSupportedException, TableMalformedException, ContainerNotFoundException, + QueryStoreException, QueryMalformedException, UserNotFoundException { log.debug("endpoint find table data, containerId={}, databaseId={}, tableId={}, principal={}, timestamp={}", containerId, databaseId, tableId, principal, timestamp); - /* check */ - if (!hasTablePermission(containerId, databaseId, tableId, "DATA_VIEW", principal)) { - log.error("Missing data view permission"); - throw new NotAllowedException("Missing data view permission"); - } /* find */ final Long count = queryService.tableCount(containerId, databaseId, tableId, timestamp, principal); log.debug("table data count is {} tuples", count); diff --git a/fda-query-service/rest-service/src/main/java/at/tuwien/endpoint/TableHistoryEndpoint.java b/fda-query-service/rest-service/src/main/java/at/tuwien/endpoint/TableHistoryEndpoint.java index 9b1225baee15c7687882329e5c7f7613da0ef218..e2fff813a7d0d913c98c234890c551b561c0c4ae 100644 --- a/fda-query-service/rest-service/src/main/java/at/tuwien/endpoint/TableHistoryEndpoint.java +++ b/fda-query-service/rest-service/src/main/java/at/tuwien/endpoint/TableHistoryEndpoint.java @@ -10,6 +10,7 @@ import io.swagger.v3.oas.annotations.security.SecurityRequirement; import lombok.extern.log4j.Log4j2; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.*; @@ -21,34 +22,28 @@ import java.util.List; @CrossOrigin(origins = "*") @RestController @RequestMapping("/api/container/{id}/database/{databaseId}/table/{tableId}/history") -public class TableHistoryEndpoint extends AbstractEndpoint { +public class TableHistoryEndpoint { private final TableService tableService; @Autowired - public TableHistoryEndpoint(TableService tableService, DatabaseService databaseService, - IdentifierService identifierService, AccessService accessService, - QueryConfig queryConfig) { - super(tableService, accessService, databaseService, identifierService, queryConfig); + public TableHistoryEndpoint(TableService tableService) { this.tableService = tableService; } @RequestMapping(method = {RequestMethod.GET, RequestMethod.HEAD}) @Transactional(readOnly = true) + @PreAuthorize("hasAuthority('data-history')") @Timed(value = "history.list", description = "Time needed to retrieve table history") @Operation(summary = "Find all history", security = @SecurityRequirement(name = "bearerAuth")) public ResponseEntity<List<TableHistoryDto>> getAll(@NotNull @PathVariable("id") Long containerId, @NotNull @PathVariable("databaseId") Long databaseId, @NotNull @PathVariable("tableId") Long tableId, @NotNull Principal principal) - throws TableNotFoundException, QueryMalformedException, DatabaseNotFoundException, NotAllowedException, + throws TableNotFoundException, QueryMalformedException, DatabaseNotFoundException, QueryStoreException, DatabaseConnectionException, UserNotFoundException { log.debug("endpoint find all history, containerId={}, databaseid={}, tableId={}, principal={}", containerId, databaseId, tableId, principal); - if (!hasTablePermission(containerId, databaseId, tableId, "DATA_HISTORY", principal)) { - log.error("Missing data history permission"); - throw new NotAllowedException("Missing data history permission"); - } final List<TableHistoryDto> history = tableService.findHistory(containerId, databaseId, tableId, principal); log.trace("find all history resulted in history {}", history); return ResponseEntity.ok(history); diff --git a/fda-query-service/rest-service/src/main/java/at/tuwien/endpoint/ViewEndpoint.java b/fda-query-service/rest-service/src/main/java/at/tuwien/endpoint/ViewEndpoint.java index b5b2bf6954025a38f46c79de4b489067324b0945..01f35fd3398fb2cb3d20fc12c859198ce3e9bb52 100644 --- a/fda-query-service/rest-service/src/main/java/at/tuwien/endpoint/ViewEndpoint.java +++ b/fda-query-service/rest-service/src/main/java/at/tuwien/endpoint/ViewEndpoint.java @@ -3,15 +3,13 @@ package at.tuwien.endpoint; import at.tuwien.api.database.ViewBriefDto; import at.tuwien.api.database.ViewCreateDto; import at.tuwien.api.database.ViewDto; -import at.tuwien.api.database.query.ExecuteStatementDto; import at.tuwien.api.database.query.QueryResultDto; -import at.tuwien.api.database.query.QueryTypeDto; -import at.tuwien.config.QueryConfig; import at.tuwien.entities.database.Database; import at.tuwien.entities.database.View; import at.tuwien.exception.*; import at.tuwien.mapper.ViewMapper; import at.tuwien.service.*; +import at.tuwien.validation.EndpointValidator; import io.micrometer.core.annotation.Timed; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.security.SecurityRequirement; @@ -19,6 +17,7 @@ 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.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.*; @@ -32,38 +31,35 @@ import java.util.stream.Collectors; @CrossOrigin(origins = "*") @RestController @RequestMapping("/api/container/{id}/database/{databaseId}/view") -public class ViewEndpoint extends AbstractEndpoint { +public class ViewEndpoint { private final ViewMapper viewMapper; private final ViewService viewService; private final QueryService queryService; private final DatabaseService databaseService; + private final EndpointValidator endpointValidator; @Autowired - public ViewEndpoint(ViewService viewService, DatabaseService databaseService, IdentifierService identifierService, - ViewMapper viewMapper, QueryService queryService, TableService tableService, - AccessService accessService, QueryConfig queryConfig) { - super(tableService, accessService, databaseService, identifierService, queryConfig); + public ViewEndpoint(ViewService viewService, DatabaseService databaseService, + ViewMapper viewMapper, QueryService queryService, EndpointValidator endpointValidator) { this.viewService = viewService; this.databaseService = databaseService; this.viewMapper = viewMapper; this.queryService = queryService; + this.endpointValidator = endpointValidator; } @GetMapping @Transactional(readOnly = true) + @PreAuthorize("hasAuthority('find-views')") @Timed(value = "view.list", description = "Time needed to list all views in a database") @Operation(summary = "Find all views", security = @SecurityRequirement(name = "bearerAuth")) public ResponseEntity<List<ViewBriefDto>> findAll(@NotNull @PathVariable("id") Long containerId, @NotNull @PathVariable("databaseId") Long databaseId, Principal principal) throws DatabaseNotFoundException, - NotAllowedException, UserNotFoundException { + UserNotFoundException { log.debug("endpoint find all views, containerId={}, databaseId={}, principal={}", containerId, databaseId, principal); - if (!hasDatabasePermission(containerId, databaseId, "LIST_VIEWS", principal)) { - log.error("Missing list views permission"); - throw new NotAllowedException("Missing list views permission"); - } final Database database = databaseService.find(containerId, databaseId); log.trace("find all views for database {}", database); final List<ViewBriefDto> views = viewService.findAll(databaseId, principal) @@ -76,6 +72,7 @@ public class ViewEndpoint extends AbstractEndpoint { @PostMapping @Transactional + @PreAuthorize("hasAuthority('create-view')") @Timed(value = "view.create", description = "Time needed to create a view") @Operation(summary = "Create a view", security = @SecurityRequirement(name = "bearerAuth")) public ResponseEntity<ViewBriefDto> create(@NotNull @PathVariable("id") Long containerId, @@ -86,10 +83,6 @@ public class ViewEndpoint extends AbstractEndpoint { UserNotFoundException { log.debug("endpoint create view, containerId={}, databaseId={}, data={}, principal={}", containerId, databaseId, data, principal); - if (!hasDatabasePermission(containerId, databaseId, "CREATE_VIEW", principal)) { - log.error("Missing create view permission"); - throw new NotAllowedException("Missing create view permission"); - } final Database database = databaseService.find(containerId, databaseId); log.trace("create view for database {}", database); final View view; @@ -102,6 +95,7 @@ public class ViewEndpoint extends AbstractEndpoint { @GetMapping("/{viewId}") @Transactional(readOnly = true) + @PreAuthorize("hasAuthority('find-view')") @Timed(value = "view.find", description = "Time needed to find a view") @Operation(summary = "Find one view", security = @SecurityRequirement(name = "bearerAuth")) public ResponseEntity<ViewDto> find(@NotNull @PathVariable("id") Long containerId, @@ -111,10 +105,6 @@ public class ViewEndpoint extends AbstractEndpoint { NotAllowedException, ViewNotFoundException, UserNotFoundException { log.debug("endpoint find view, containerId={}, databaseId={}, viewId={}, principal={}", containerId, databaseId, viewId, principal); - if (!hasDatabasePermission(containerId, databaseId, "FIND_VIEW", principal)) { - log.error("Missing find views permission"); - throw new NotAllowedException("Missing find views permission"); - } final Database database = databaseService.find(containerId, databaseId); log.trace("find view for database {}", database); final ViewDto view = viewMapper.viewToViewDto(viewService.findById(databaseId, viewId, principal)); @@ -124,20 +114,17 @@ public class ViewEndpoint extends AbstractEndpoint { @DeleteMapping("/{viewId}") @Transactional + @PreAuthorize("hasAuthority('delete-view')") @Timed(value = "view.delete", description = "Time needed to delete a view") @Operation(summary = "Delete one view", security = @SecurityRequirement(name = "bearerAuth")) public ResponseEntity<?> delete(@NotNull @PathVariable("id") Long containerId, @NotNull @PathVariable("databaseId") Long databaseId, @NotNull @PathVariable("viewId") Long viewId, @NotNull Principal principal) throws DatabaseNotFoundException, - NotAllowedException, ViewNotFoundException, UserNotFoundException, DatabaseConnectionException, + ViewNotFoundException, UserNotFoundException, DatabaseConnectionException, ViewMalformedException, QueryMalformedException { log.debug("endpoint delete view, containerId={}, databaseId={}, viewId={}, principal={}", containerId, databaseId, viewId, principal); - if (!hasDatabasePermission(containerId, databaseId, "DELETE_VIEW", principal)) { - log.error("Missing delete view permission"); - throw new NotAllowedException("Missing delete view permission"); - } viewService.delete(containerId, databaseId, viewId, principal); return ResponseEntity.accepted() .build(); @@ -145,6 +132,7 @@ public class ViewEndpoint extends AbstractEndpoint { @GetMapping("/{viewId}/data") @Transactional(readOnly = true) + @PreAuthorize("hasAuthority('view-view')") @Timed(value = "view.data", description = "Time needed to retrieve data from a view") @Operation(summary = "Find view data", security = @SecurityRequirement(name = "bearerAuth")) public ResponseEntity<QueryResultDto> data(@NotNull @PathVariable("id") Long containerId, @@ -159,11 +147,7 @@ public class ViewEndpoint extends AbstractEndpoint { log.debug("endpoint find view data, containerId={}, databaseId={}, viewId={}, principal={}, page={}, size={}", containerId, databaseId, viewId, principal, page, size); /* check */ - if (!hasDatabasePermission(containerId, databaseId, "DATA_VIEW", principal)) { - log.error("Missing view data in view permission"); - throw new NotAllowedException("Missing view data in view permission"); - } - validateDataParams(page, size); + endpointValidator.validateDataParams(page, size); /* find */ final Database database = databaseService.find(containerId, databaseId); log.trace("find view data for database {}", database); @@ -177,6 +161,7 @@ public class ViewEndpoint extends AbstractEndpoint { @GetMapping("/{viewId}/data/count") @Transactional(readOnly = true) + @PreAuthorize("hasAuthority('view-view')") @Timed(value = "view.data.count", description = "Time needed to retrieve data count from a view") @Operation(summary = "Find view data count", security = @SecurityRequirement(name = "bearerAuth")) public ResponseEntity<Long> count(@NotNull @PathVariable("id") Long containerId, @@ -188,11 +173,6 @@ public class ViewEndpoint extends AbstractEndpoint { ImageNotSupportedException, ColumnParseException, UserNotFoundException, ContainerNotFoundException, ViewMalformedException { log.debug("endpoint find view data count, containerId={}, databaseId={}, viewId={}, principal={}", containerId, databaseId, viewId, principal); - /* check */ - if (!hasDatabasePermission(containerId, databaseId, "DATA_VIEW", principal)) { - log.error("Missing view data in view permission"); - throw new NotAllowedException("Missing view data in view permission"); - } /* find */ final Database database = databaseService.find(containerId, databaseId); log.trace("find view data for database {}", database); diff --git a/fda-query-service/rest-service/src/main/java/at/tuwien/validation/EndpointValidator.java b/fda-query-service/rest-service/src/main/java/at/tuwien/validation/EndpointValidator.java new file mode 100644 index 0000000000000000000000000000000000000000..61775a087420a25a9ff0b7dc6de167b67e4e3497 --- /dev/null +++ b/fda-query-service/rest-service/src/main/java/at/tuwien/validation/EndpointValidator.java @@ -0,0 +1,77 @@ +package at.tuwien.validation; + +import at.tuwien.SortType; +import at.tuwien.api.database.query.ExecuteStatementDto; +import at.tuwien.config.QueryConfig; +import at.tuwien.exception.PaginationException; +import at.tuwien.exception.QueryMalformedException; +import at.tuwien.exception.SortException; +import lombok.extern.log4j.Log4j2; +import org.springframework.stereotype.Component; + +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +@Log4j2 +@Component +public class EndpointValidator { + + private final QueryConfig queryConfig; + + public EndpointValidator(QueryConfig queryConfig) { + this.queryConfig = queryConfig; + } + + public void validateDataParams(Long page, Long size) throws PaginationException { + log.trace("validate data params, page={}, size={}", page, size); + if ((page == null && size != null) || (page != null && size == null)) { + log.error("Failed to validate page and/or size number, either both are present or none"); + throw new PaginationException("Failed to validate page and/or size number"); + } + if (page != null && page < 0) { + log.error("Failed to validate page number, is lower than zero"); + throw new PaginationException("Failed to validate page number"); + } + if (size != null && size <= 0) { + log.error("Failed to validate size number, is lower or equal than zero"); + throw new PaginationException("Failed to validate size number"); + } + } + + public void validateDataParams(Long page, Long size, SortType sortDirection, String sortColumn) + throws PaginationException, SortException { + log.trace("validate data params, page={}, size={}, sortDirection={}, sortColumn={}", page, size, + sortDirection, sortColumn); + validateDataParams(page, size); + if ((sortDirection == null && sortColumn != null) || (sortDirection != null && sortColumn == null)) { + log.error("Failed to validate sort direction and/or sort column, either both are present or none"); + throw new SortException("Failed to validate sort direction and/or sort column"); + } + } + + /** + * Do not allow aggregate functions and comments + * https://mariadb.com/kb/en/aggregate-functions/ + */ + public void validateForbiddenStatements(ExecuteStatementDto data) throws QueryMalformedException { + final List<String> words = new LinkedList<>(); + Arrays.stream(queryConfig.getNotSupportedKeywords()) + .forEach(keyword -> { + final Pattern pattern = Pattern.compile(keyword); + final Matcher matcher = pattern.matcher(data.getStatement()); + final boolean found = matcher.find(); + if (found) { + words.add(keyword); + } + }); + if (words.size() == 0) { + return; + } + log.error("Query contains forbidden keyword(s): {}", words); + throw new QueryMalformedException("Query contains forbidden keyword(s): " + Arrays.toString(words.toArray())); + } + +} diff --git a/fda-query-service/rest-service/src/test/java/at/tuwien/BaseUnitTest.java b/fda-query-service/rest-service/src/test/java/at/tuwien/BaseUnitTest.java index 43ed8c4414f3c9eb8a51d7303150cb6890c2334f..c62dae93fd85df9e890c560b430cd64cee746402 100644 --- a/fda-query-service/rest-service/src/test/java/at/tuwien/BaseUnitTest.java +++ b/fda-query-service/rest-service/src/test/java/at/tuwien/BaseUnitTest.java @@ -77,11 +77,7 @@ public abstract class BaseUnitTest { .username(USER_1_USERNAME) .email(USER_1_EMAIL) .emailVerified(true) - .themeDark(false) - .password(USER_1_PASSWORD) .databasePassword(USER_1_DATABASE_PASSWORD) - .created(USER_1_CREATED) - .lastModified(USER_1_CREATED) .build(); public final static UserDto USER_1_DTO = UserDto.builder() @@ -89,8 +85,6 @@ public abstract class BaseUnitTest { .username(USER_1_USERNAME) .email(USER_1_EMAIL) .emailVerified(true) - .themeDark(false) - .password(USER_1_PASSWORD) .build(); public final static UserDetails USER_1_DETAILS = UserDetailsDto.builder() @@ -115,11 +109,7 @@ public abstract class BaseUnitTest { .username(USER_2_USERNAME) .email(USER_2_EMAIL) .emailVerified(true) - .themeDark(false) - .password(USER_2_PASSWORD) .databasePassword(USER_2_DATABASE_PASSWORD) - .created(USER_2_CREATED) - .lastModified(USER_2_CREATED) .build(); public final static UserDetails USER_2_DETAILS = UserDetailsDto.builder() @@ -144,11 +134,7 @@ public abstract class BaseUnitTest { .username(USER_3_USERNAME) .email(USER_3_EMAIL) .emailVerified(true) - .themeDark(false) - .password(USER_3_PASSWORD) .databasePassword(USER_3_DATABASE_PASSWORD) - .created(USER_3_CREATED) - .lastModified(USER_3_CREATED) .build(); public final static UserDetails USER_3_DETAILS = UserDetailsDto.builder() @@ -174,8 +160,6 @@ public abstract class BaseUnitTest { .username(USER_4_USERNAME) .email(USER_4_EMAIL) .emailVerified(USER_4_EMAIL_VERIFIED) - .themeDark(USER_4_THEME_DARK) - .password(USER_4_PASSWORD) .databasePassword(USER_4_DATABASE_PASSWORD) .build(); diff --git a/fda-table-service/rest-service/src/main/java/at/tuwien/endpoints/AbstractEndpoint.java b/fda-table-service/rest-service/src/main/java/at/tuwien/endpoints/AbstractEndpoint.java deleted file mode 100644 index 57243400eebc92003549278b15f8eb3047e35c13..0000000000000000000000000000000000000000 --- a/fda-table-service/rest-service/src/main/java/at/tuwien/endpoints/AbstractEndpoint.java +++ /dev/null @@ -1,110 +0,0 @@ -package at.tuwien.endpoints; - -import at.tuwien.entities.database.AccessType; -import at.tuwien.entities.database.Database; -import at.tuwien.entities.database.DatabaseAccess; -import at.tuwien.exception.AccessDeniedException; -import at.tuwien.exception.DatabaseNotFoundException; -import at.tuwien.service.AccessService; -import at.tuwien.service.DatabaseService; -import at.tuwien.service.TableService; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.core.Authentication; - -import java.security.Principal; -import java.util.List; - - -@Slf4j -public abstract class AbstractEndpoint { - - private final AccessService accessService; - private final DatabaseService databaseService; - - @Autowired - protected AbstractEndpoint(AccessService accessService, DatabaseService databaseService) { - this.accessService = accessService; - this.databaseService = databaseService; - } - - protected Boolean hasDatabasePermission(Long containerId, Long databaseId, String permissionCode, - Principal principal) { - log.debug("validate has database permission, containerId={}, databaseId={}, permissionCode={}, principal={}", - containerId, databaseId, permissionCode, principal); - final Database database; - try { - database = databaseService.find(containerId, databaseId); - } catch (DatabaseNotFoundException e) { - log.error("Failed to find database with id {}", databaseId); - return false; - } - if (principal != null && database.getCreator().getUsername().equals(principal.getName())) { - log.debug("grant permission {} because user is creator of database with id {}", permissionCode, databaseId); - return true; - } - /* view-only operations are allowed on public databases */ - if (database.getIsPublic() && List.of("TABLE_CREATE", "TABLES_VIEW").contains(permissionCode)) { - log.debug("grant permission {} because database is public", permissionCode); - return true; - } - /* modification operations are limited to the creator */ - if (principal == null) { - log.error("Failed to grant permission {} because principal is null", permissionCode); - return false; - } - final Authentication authentication = (Authentication) principal /* with pre-authorization this always holds */; - if (authentication.getAuthorities().stream().noneMatch(a -> a.getAuthority().equals("ROLE_RESEARCHER"))) { - log.error("Failed to grant permission {} because current user misses authority 'ROLE_RESEARCHER'", - permissionCode); - return false; - } - log.debug("grant permission {} because user is creator", permissionCode); - return true; - } - - protected Boolean hasTablePermission(Long containerId, Long databaseId, Long tableId, String permissionCode, - Principal principal) throws AccessDeniedException { - log.debug("validate has table permissions, containerId={}, databaseId={}, tableId={}, permissionCode={}, principal={}", - containerId, databaseId, tableId, permissionCode, principal); - final Database database; - try { - database = databaseService.find(containerId, databaseId); - } catch (DatabaseNotFoundException e) { - log.error("Failed to find database with id {}", databaseId); - return false; - } - if (principal != null && database.getCreator().getUsername().equals(principal.getName())) { - log.debug("grant permission {} because user is creator of database with id {}", permissionCode, databaseId); - return true; - } - /* view-only operations are allowed on public databases */ - if (database.getIsPublic() && List.of("TABLE_INFO").contains(permissionCode)) { - log.debug("grant permission {} because database is public", permissionCode); - return true; - } - /* modification operations are limited to the creator */ - if (principal == null) { - log.error("Failed to grant permission {} because principal is null", permissionCode); - return false; - } - final Authentication authentication = (Authentication) principal /* with pre-authorization this always holds */; - if (authentication.getAuthorities().stream().noneMatch(a -> a.getAuthority().equals("ROLE_RESEARCHER"))) { - log.error("Failed to grant permission {} because current user misses authority 'ROLE_RESEARCHER'", - permissionCode); - return false; - } - final DatabaseAccess access = accessService.hasAccess(databaseId, tableId, principal.getName()); - if (hasReadAccess(access) && List.of("TABLE_INFO", "CHECK_ACCESS").contains(permissionCode)) { - log.debug("grant permission {} because user {} has at least read access", permissionCode, principal.getName()); - return true; - } - log.error("Failed to grant permission {} because user {} has insufficient access {} or is not creator", permissionCode, principal.getName(), access.getType()); - return false; - } - - private boolean hasReadAccess(DatabaseAccess access) { - return List.of(AccessType.READ, AccessType.WRITE_OWN, AccessType.WRITE_ALL).contains(access.getType()); - } - -} diff --git a/fda-table-service/rest-service/src/main/java/at/tuwien/endpoints/AccessEndpoint.java b/fda-table-service/rest-service/src/main/java/at/tuwien/endpoints/AccessEndpoint.java index f48226219187863f667117080faf333f60b08614..cee900fc5a5cd458e03962cd8509d75a719ef24f 100644 --- a/fda-table-service/rest-service/src/main/java/at/tuwien/endpoints/AccessEndpoint.java +++ b/fda-table-service/rest-service/src/main/java/at/tuwien/endpoints/AccessEndpoint.java @@ -25,14 +25,13 @@ import java.security.Principal; @CrossOrigin(origins = "*") @RestController @RequestMapping("/api/container/{id}/database/{databaseId}/table/{tableId}/access") -public class AccessEndpoint extends AbstractEndpoint { +public class AccessEndpoint { private final AccessService accessService; private final DatabaseMapper databaseMapper; @Autowired public AccessEndpoint(DatabaseService databaseService, AccessService accessService, DatabaseMapper databaseMapper) { - super(accessService, databaseService); this.accessService = accessService; this.databaseMapper = databaseMapper; } @@ -40,19 +39,15 @@ public class AccessEndpoint extends AbstractEndpoint { @GetMapping @Transactional @Timed(value = "access.check", description = "Time needed to check access to a table") - @PreAuthorize("hasRole('ROLE_RESEARCHER')") + @PreAuthorize("hasAuthority('check-access')") @Operation(summary = "Check access to some table", security = @SecurityRequirement(name = "bearerAuth")) public ResponseEntity<DatabaseAccessDto> checkAccess(@NotBlank @PathVariable("id") Long containerId, @NotBlank @PathVariable("databaseId") Long databaseId, @NotBlank @PathVariable("tableId") Long tableId, @NotNull Principal principal) - throws NotAllowedException, AccessDeniedException { + throws AccessDeniedException { log.debug("endpoint check access to database, containerId={}, databaseId={}, principal={}", containerId, databaseId, principal); - if (!hasTablePermission(containerId, databaseId, tableId, "CHECK_ACCESS", principal)) { - log.error("Missing check access permission"); - throw new NotAllowedException("Missing check access permission"); - } final DatabaseAccess access = accessService.hasAccess(databaseId, tableId, principal.getName()); final DatabaseAccessDto dto = databaseMapper.databaseAccessToDatabaseAccessDto(access); log.trace("check access resulted in dto {}", dto); diff --git a/fda-table-service/rest-service/src/main/java/at/tuwien/endpoints/TableColumnEndpoint.java b/fda-table-service/rest-service/src/main/java/at/tuwien/endpoints/TableColumnEndpoint.java index 73b170ae4ae4151c11bbbf62ffb9abf08d62fba4..d5b44de563f3a387b183b5b515a847eb45d30547 100644 --- a/fda-table-service/rest-service/src/main/java/at/tuwien/endpoints/TableColumnEndpoint.java +++ b/fda-table-service/rest-service/src/main/java/at/tuwien/endpoints/TableColumnEndpoint.java @@ -5,8 +5,6 @@ import at.tuwien.api.database.table.columns.concepts.ColumnSemanticsUpdateDto; import at.tuwien.entities.database.table.columns.TableColumn; import at.tuwien.exception.*; import at.tuwien.mapper.TableMapper; -import at.tuwien.service.AccessService; -import at.tuwien.service.DatabaseService; import at.tuwien.service.TableService; import io.micrometer.core.annotation.Timed; import io.swagger.v3.oas.annotations.Operation; @@ -25,15 +23,13 @@ import java.security.Principal; @CrossOrigin(origins = "*") @RestController @RequestMapping("/api/container/{id}/database/{databaseId}/table/{tableId}/column/{columnId}") -public class TableColumnEndpoint extends AbstractEndpoint { +public class TableColumnEndpoint { private final TableMapper tableMapper; private final TableService tableService; @Autowired - public TableColumnEndpoint(TableMapper tableMapper, TableService tableService, DatabaseService databaseService, - AccessService accessService) { - super(accessService, databaseService); + public TableColumnEndpoint(TableMapper tableMapper, TableService tableService) { this.tableMapper = tableMapper; this.tableService = tableService; } @@ -48,14 +44,10 @@ public class TableColumnEndpoint extends AbstractEndpoint { @NotNull @PathVariable("columnId") Long columnId, @NotNull @Valid @RequestBody ColumnSemanticsUpdateDto updateDto, @NotNull Principal principal) throws NotAllowedException, - AccessDeniedException, TableNotFoundException, TableMalformedException, DatabaseNotFoundException, + TableNotFoundException, TableMalformedException, DatabaseNotFoundException, ContainerNotFoundException, UnitNotFoundException, ConceptNotFoundException { log.debug("endpoint update table, containerId={}, databaseId={}, tableId={}, principal={}", containerId, databaseId, tableId, principal); - if (!hasTablePermission(containerId, databaseId, tableId, "TABLE_UPDATE", principal)) { - log.error("Missing table update permission"); - throw new NotAllowedException("Missing table update permission"); - } final TableColumn column = tableService.update(containerId, databaseId, tableId, columnId, updateDto, principal); log.info("Updated table semantics of table with id {} and database with id {}", tableId, databaseId); final ColumnDto dto = tableMapper.tableColumnToColumnDto(column); diff --git a/fda-table-service/rest-service/src/main/java/at/tuwien/endpoints/TableEndpoint.java b/fda-table-service/rest-service/src/main/java/at/tuwien/endpoints/TableEndpoint.java index d1a6784f83ce35c698f84b78de6cd1d981106945..6c6c1c4577f3b2d2668a4f85c2014f483988ec75 100644 --- a/fda-table-service/rest-service/src/main/java/at/tuwien/endpoints/TableEndpoint.java +++ b/fda-table-service/rest-service/src/main/java/at/tuwien/endpoints/TableEndpoint.java @@ -15,6 +15,7 @@ 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.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.*; @@ -28,16 +29,14 @@ import java.util.stream.Collectors; @CrossOrigin(origins = "*") @RestController @RequestMapping("/api/container/{id}/database/{databaseId}/table") -public class TableEndpoint extends AbstractEndpoint { +public class TableEndpoint { private final TableMapper tableMapper; private final TableService tableService; private final MessageQueueService amqpService; @Autowired - public TableEndpoint(TableMapper tableMapper, TableService tableService, MessageQueueService amqpService, - DatabaseService databaseService, AccessService accessService) { - super(accessService, databaseService); + public TableEndpoint(TableMapper tableMapper, TableService tableService, MessageQueueService amqpService) { this.tableMapper = tableMapper; this.amqpService = amqpService; this.tableService = tableService; @@ -46,17 +45,14 @@ public class TableEndpoint extends AbstractEndpoint { @GetMapping @Transactional(readOnly = true) @Timed(value = "table.list", description = "Time needed to list the tables") + @PreAuthorize("hasAuthority('find-tables')") @Operation(summary = "List all tables", security = @SecurityRequirement(name = "bearerAuth")) public ResponseEntity<List<TableBriefDto>> list(@NotNull @PathVariable("id") Long containerId, @NotNull @PathVariable("databaseId") Long databaseId, Principal principal) - throws DatabaseNotFoundException, NotAllowedException { + throws DatabaseNotFoundException { log.debug("endpoint list tables, containerId={}, databaseId={}, principal={}", containerId, databaseId, principal); - if (!hasDatabasePermission(containerId, databaseId, "TABLES_VIEW", principal)) { - log.error("Missing table view permission"); - throw new NotAllowedException("Missing table view permission"); - } final List<TableBriefDto> dto = tableService.findAll(containerId, databaseId) .stream() .map(tableMapper::tableToTableBriefDto) @@ -67,6 +63,7 @@ public class TableEndpoint extends AbstractEndpoint { @PostMapping @Transactional + @PreAuthorize("hasAuthority('create-table')") @Timed(value = "table.create", description = "Time needed to create a table") @Operation(summary = "Create a table", security = @SecurityRequirement(name = "bearerAuth")) public ResponseEntity<TableBriefDto> create(@NotNull @PathVariable("id") Long containerId, @@ -78,10 +75,6 @@ public class TableEndpoint extends AbstractEndpoint { NotAllowedException { log.debug("endpoint create table, containerId={}, databaseId={}, createDto={}, principal={}", containerId, databaseId, createDto, principal); - if (!hasDatabasePermission(containerId, databaseId, "TABLE_CREATE", principal)) { - log.error("Missing table create permission"); - throw new NotAllowedException("Missing table create permission"); - } final Table table = tableService.createTable(containerId, databaseId, createDto, principal); amqpService.create(table); final TableBriefDto dto = tableMapper.tableToTableBriefDto(table); @@ -94,19 +87,15 @@ public class TableEndpoint extends AbstractEndpoint { @GetMapping("/{tableId}") @Transactional(readOnly = true) @Timed(value = "table.find", description = "Time needed to find a table") + @PreAuthorize("hasAuthority('find-table')") @Operation(summary = "Get information about table", security = @SecurityRequirement(name = "bearerAuth")) public ResponseEntity<TableDto> findById(@NotNull @PathVariable("id") Long containerId, @NotNull @PathVariable("databaseId") Long databaseId, @NotNull @PathVariable("tableId") Long tableId, Principal principal) - throws TableNotFoundException, DatabaseNotFoundException, ContainerNotFoundException, NotAllowedException, - AccessDeniedException { + throws TableNotFoundException, DatabaseNotFoundException, ContainerNotFoundException { log.debug("endpoint find table, containerId={}, databaseId={}, tableId={}, principal={}", containerId, databaseId, tableId, principal); - if (!hasTablePermission(containerId, databaseId, tableId, "TABLE_INFO", principal)) { - log.error("Missing table view permission"); - throw new NotAllowedException("Missing table view permission"); - } final Table table = tableService.findById(containerId, databaseId, tableId); final TableDto dto = tableMapper.tableToTableDto(table); log.trace("find table resulted in table {}", dto); @@ -115,6 +104,7 @@ public class TableEndpoint extends AbstractEndpoint { @DeleteMapping("/{tableId}") @Transactional + @PreAuthorize("hasAuthority('delete-table')") @Timed(value = "table.delete", description = "Time needed to delete a table") @Operation(summary = "Delete a table", security = @SecurityRequirement(name = "bearerAuth")) @ResponseStatus(HttpStatus.OK) @@ -123,14 +113,9 @@ public class TableEndpoint extends AbstractEndpoint { @NotNull @PathVariable("tableId") Long tableId, @NotNull Principal principal) throws TableNotFoundException, DatabaseNotFoundException, ImageNotSupportedException, - DataProcessingException, ContainerNotFoundException, TableMalformedException, QueryMalformedException, - NotAllowedException, AccessDeniedException { + DataProcessingException, ContainerNotFoundException, TableMalformedException, QueryMalformedException { log.debug("endpoint delete table, containerId={}, databaseId={}, tableId={}, principal={}", containerId, databaseId, tableId, principal); - if (!hasTablePermission(containerId, databaseId, tableId, "TABLE_DELETE", principal)) { - log.error("Missing table delete permission"); - throw new NotAllowedException("Missing table delete permission"); - } tableService.deleteTable(containerId, databaseId, tableId); } diff --git a/fda-table-service/rest-service/src/test/java/at/tuwien/BaseUnitTest.java b/fda-table-service/rest-service/src/test/java/at/tuwien/BaseUnitTest.java index 522047601a5efe16e82a2b1c148fa192588308b9..d7b8d42bf48e98351cbb94bb7bb0a47648f0db30 100644 --- a/fda-table-service/rest-service/src/test/java/at/tuwien/BaseUnitTest.java +++ b/fda-table-service/rest-service/src/test/java/at/tuwien/BaseUnitTest.java @@ -38,19 +38,13 @@ public abstract class BaseUnitTest { .username(USER_1_USERNAME) .email(USER_1_EMAIL) .emailVerified(true) - .themeDark(false) - .password(USER_1_PASSWORD) .databasePassword(USER_1_DATABASE_PASSWORD) - .created(USER_1_CREATED) - .lastModified(USER_1_CREATED) .build(); public final static UserDto USER_1_DTO = UserDto.builder() .id(USER_1_ID) .username(USER_1_USERNAME) .email(USER_1_EMAIL) .emailVerified(true) - .themeDark(false) - .password("password") .build(); public final static String USER_2_ID = "0153f998-bd4c-4154-993e-75c355499044"; @@ -64,19 +58,13 @@ public abstract class BaseUnitTest { .username(USER_2_USERNAME) .email(USER_2_EMAIL) .emailVerified(true) - .themeDark(false) - .password(USER_2_PASSWORD) .databasePassword(USER_2_DATABASE_PASSWORD) - .created(USER_2_CREATED) - .lastModified(USER_2_CREATED) .build(); public final static UserDto USER_2_DTO = UserDto.builder() .id(USER_2_ID) .username(USER_2_USERNAME) .email(USER_2_EMAIL) .emailVerified(true) - .themeDark(false) - .password("password") .build(); public final static String DATABASE_NET = "fda-userdb"; diff --git a/fda-ui/api/container/index.js b/fda-ui/api/container/index.js new file mode 100644 index 0000000000000000000000000000000000000000..3cd8e7a2d83c6cbb086937b55b5f3343e7002acc --- /dev/null +++ b/fda-ui/api/container/index.js @@ -0,0 +1,16 @@ +const axios = require('axios/dist/browser/axios.cjs') + +export function listContainers (limit) { + return axios.get(`/api/container?limit=${limit}`) +} + +export function startContainer (token, containerId) { + const payload = { + action: 'start' + } + return axios.put(`/api/container/${containerId}`, payload, { + headers: { + Authorization: `Bearer ${token}` + } + }) +} diff --git a/fda-ui/api/database/index.js b/fda-ui/api/database/index.js new file mode 100644 index 0000000000000000000000000000000000000000..7b61f97544d21309d765565765f4df380890c206 --- /dev/null +++ b/fda-ui/api/database/index.js @@ -0,0 +1,13 @@ +const axios = require('axios/dist/browser/axios.cjs') + +export function createDatabase (token, containerId) { + const payload = { + name: null, + is_public: true + } + return axios.post(`/api/container/${containerId}/database`, payload, { + headers: { + Authorization: `Bearer ${token}` + } + }) +} diff --git a/fda-ui/components/DatabaseList.vue b/fda-ui/components/DatabaseList.vue index 2f85f6a72ed63ff7c776abf354e839b5fc4198c9..f3ffd4196bc6c17580a86a5d5a9a2cfcd4af503c 100644 --- a/fda-ui/components/DatabaseList.vue +++ b/fda-ui/components/DatabaseList.vue @@ -5,7 +5,6 @@ v-for="(container, idx) in containers" :key="idx" :to="link(container)" - :disabled="!container.database" flat tile> <v-divider class="mx-4" /> @@ -32,7 +31,7 @@ <v-btn small secondary - :loading="container.database.loading" + :loading="container?.loading" @click.stop="initDatabase(container)"> Start </v-btn> @@ -53,6 +52,8 @@ <script> import { formatCreators, formatUser, formatYearUTC, isResearcher } from '@/utils' +import { listContainers, startContainer } from '@/api/container' +import { createDatabase } from '@/api/database' export default { data () { @@ -64,10 +65,6 @@ export default { containers: [], searchQuery: null, limit: 100, - createDatabaseDto: { - name: null, - is_public: true - }, items: [ { text: 'Databases', to: '/container', activeClass: '' } ], @@ -109,13 +106,13 @@ export default { return creators || this.formatCreator(container.creator) }, canInit (container) { - if (!this.token) { + if (!this.user) { return false } - if (container.creator.username !== this.user.username) { + if (container.creator.sub !== this.user.id) { return false } - return !container.database.id && !this.loadingDatabases + return !container.database }, hasDatabase (container) { return container.database @@ -128,13 +125,13 @@ export default { .then(() => this.createDatabase(container)) }, identifierCreated (container) { - if (!container.database.identifier) { + if (!container || !container.database || !container.database.identifier) { return null } return formatYearUTC(container.database.identifier.created) }, identifierDescription (container) { - if (!container.database.identifier) { + if (!container || !container.database || !container.database.identifier) { return null } return container.database.identifier.description @@ -143,7 +140,7 @@ export default { this.createDbDialog = false try { this.loadingContainers = true - const res = await this.$axios.get(`/api/container?limit=${this.limit}`) + const res = await listContainers(this.limit) this.containers = res.data console.debug('containers', this.containers) this.error = false @@ -157,18 +154,35 @@ export default { }, async startContainer (container) { try { - container.database.loading = true - const res = await this.$axios.put(`/api/container/${container.id}`, { action: 'start' }, this.config) + container.loading = true + const res = await startContainer(this.token, container.id) console.debug('started container', res.data) this.error = false } catch (error) { + console.error('start container', error) const { status } = error.response if (status !== 409) { this.error = true this.$toast.error('Failed to start container') } } - container.database.loading = false + container.loading = false + }, + async createDatabase (container) { + try { + container.loading = true + const res = await createDatabase(this.token, container.id) + container.database = res.data + console.debug('created database', container.database) + this.error = false + } catch (error) { + console.error('create database', error) + const { message } = error.response + this.error = true + console.error('Failed to create database', error) + this.$toast.error(`${message}`) + } + container.loading = false }, link (container) { if (!container.database || !container.database.id) {