From dae67f3681bf34f0aeb352c9498bb8e272e142ee Mon Sep 17 00:00:00 2001 From: Martin Weise <martin.weise@tuwien.ac.at> Date: Tue, 9 Jul 2024 14:58:10 +0000 Subject: [PATCH] Dev --- .docs/api/identity-service.md | 14 +- .gitlab-ci.yml | 10 +- CONTRIBUTING.md | 9 +- dbrepo-analyse-service/app.py | 4 - dbrepo-auth-service/dbrepo-realm.json | 135 +- dbrepo-data-db/README.md | 6 +- dbrepo-data-db/enable_history_insert.cnf | 1 + dbrepo-data-db/sidecar/app.py | 8 +- dbrepo-data-service/Dockerfile | 4 +- dbrepo-data-service/metrics.md | 24 +- dbrepo-data-service/pom.xml | 6 + .../at/tuwien/endpoints/AccessEndpoint.java | 12 +- .../at/tuwien/endpoints/DatabaseEndpoint.java | 8 +- .../at/tuwien/endpoints/SubsetEndpoint.java | 10 +- .../at/tuwien/endpoints/TableEndpoint.java | 28 +- .../at/tuwien/endpoints/ViewEndpoint.java | 14 +- .../tuwien/handlers/ApiExceptionHandler.java | 235 - .../src/main/resources/application-local.yml | 7 +- .../src/main/resources/application.yml | 6 +- .../java/at/tuwien/config/MariaDbConfig.java | 23 +- .../endpoint/AccessEndpointUnitTest.java | 40 +- .../endpoint/DatabaseEndpointUnitTest.java | 28 +- .../endpoint/SubsetEndpointUnitTest.java | 40 +- .../endpoint/TableEndpointUnitTest.java | 80 +- .../tuwien/endpoint/ViewEndpointUnitTest.java | 28 +- .../gateway/DataDatabaseGatewayUnitTest.java | 10 +- .../tuwien/gateway/InterceptorUnitTest.java | 11 - .../KeycloakSidecarGatewayUnitTest.java | 101 - .../MetadataServiceGatewayUnitTest.java | 71 +- .../handlers/ApiExceptionHandlerTest.java | 48 - .../DefaultListenerIntegrationTest.java | 6 +- .../listener/DefaultListenerUnitTest.java | 6 +- .../DatabaseServiceIntegrationTest.java | 6 + .../service/QueueServiceIntegrationTest.java | 22 +- .../service/SubsetServiceIntegrationTest.java | 153 +- .../service/TableServiceIntegrationTest.java | 92 +- .../java/at/tuwien/utils/UserUtilTest.java | 40 + .../validation/EndpointValidatorUnitTest.java | 101 + .../src/test/resources/application.properties | 2 +- .../auth/BasicAuthenticationProvider.java | 23 +- .../auth/InternalRequestInterceptor.java | 45 + .../java/at/tuwien/config/GatewayConfig.java | 37 +- .../java/at/tuwien/config/KeycloakConfig.java | 14 +- .../at/tuwien/config/WebSecurityConfig.java | 7 +- .../exception/ContainerNotFoundException.java | 21 - .../exception/DatabaseNotFoundException.java | 21 - .../FormatNotAvailableException.java | 23 - .../tuwien/exception/NotAllowedException.java | 21 - .../tuwien/exception/PaginationException.java | 22 - .../exception/QueryNotFoundException.java | 21 - .../exception/StorageNotFoundException.java | 21 - .../exception/TableExistsException.java | 21 - .../exception/TableNotFoundException.java | 21 - .../exception/UserNotFoundException.java | 21 - .../exception/ViewNotFoundException.java | 21 - .../gateway/DataDatabaseSidecarGateway.java | 23 +- .../at/tuwien/gateway/KeycloakGateway.java | 11 +- .../gateway/MetadataServiceGateway.java | 40 +- .../impl/DataDatabaseSidecarGatewayImpl.java | 12 +- .../gateway/impl/KeycloakGatewayImpl.java | 48 +- .../impl/MetadataServiceGatewayImpl.java | 66 +- .../at/tuwien/listener/DefaultListener.java | 2 +- .../java/at/tuwien/mapper/MariaDbMapper.java | 24 +- .../java/at/tuwien/service/SubsetService.java | 13 +- .../java/at/tuwien/service/TableService.java | 4 +- .../java/at/tuwien/service/ViewService.java | 2 +- .../service/impl/StorageServiceS3Impl.java | 5 +- .../impl/SubsetServiceMariaDbImpl.java | 27 +- .../service/impl/TableServiceMariaDbImpl.java | 12 +- .../service/impl/ViewServiceMariaDbImpl.java | 6 +- dbrepo-metadata-service/Dockerfile | 4 +- .../at/tuwien/api/auth/KeycloakErrorDto.java | 6 +- .../api/keycloak/RoleRepresentationDto.java | 34 + .../at/tuwien/api/keycloak/UserCreateDto.java | 4 + .../database/table/columns/TableColumn.java | 4 +- dbrepo-metadata-service/pom.xml | 6 + .../BrokerServiceConnectionException.java | 21 + .../exception/BrokerServiceException.java | 10 +- ...va => DataServiceConnectionException.java} | 8 +- ...ception.java => DataServiceException.java} | 8 +- .../exception/DatabaseMalformedException.java | 0 .../DatabaseUnavailableException.java | 0 .../exception/ExternalServiceException.java | 21 + .../MetadataServiceConnectionException.java | 8 +- .../exception/MetadataServiceException.java | 8 +- .../exception/QueryMalformedException.java | 0 .../exception/QueryNotSupportedException.java | 0 .../exception/QueryStoreCreateException.java | 0 .../exception/QueryStoreGCException.java | 0 .../exception/QueryStoreInsertException.java | 0 .../exception/QueryStorePersistException.java | 0 .../exception/RemoteUnavailableException.java | 0 .../exception/SidecarExportException.java | 0 .../exception/SidecarImportException.java | 0 .../exception/TableMalformedException.java | 0 .../exception/TableSchemaException.java | 0 .../exception/ViewMalformedException.java | 0 .../tuwien/exception/ViewSchemaException.java | 0 .../java/at/tuwien/mapper/MetadataMapper.java | 4 +- .../main/java/at/tuwien/utils/UserUtil.java | 10 + .../at/tuwien/endpoints/AccessEndpoint.java | 16 +- .../tuwien/endpoints/ContainerEndpoint.java | 14 +- .../at/tuwien/endpoints/DatabaseEndpoint.java | 36 +- .../tuwien/endpoints/IdentifierEndpoint.java | 22 +- .../at/tuwien/endpoints/TableEndpoint.java | 39 +- .../at/tuwien/endpoints/UserEndpoint.java | 18 +- .../at/tuwien/endpoints/ViewEndpoint.java | 28 +- .../tuwien/handlers/ApiExceptionHandler.java | 178 +- .../src/main/resources/application-local.yml | 8 +- .../src/main/resources/application.yml | 12 +- .../java/at/tuwien/config/RabbitConfig.java | 41 + .../IdentifierTypeConverterUnitTest.java | 41 + .../endpoints/AccessEndpointUnitTest.java | 18 +- .../endpoints/ContainerEndpointUnitTest.java | 25 +- .../endpoints/DatabaseEndpointUnitTest.java | 24 +- .../endpoints/IdentifierEndpointUnitTest.java | 126 +- .../endpoints/TableEndpointUnitTest.java | 53 +- .../endpoints/UserEndpointUnitTest.java | 4 +- .../endpoints/ViewEndpointUnitTest.java | 12 +- .../gateway/BrokerServiceGatewayUnitTest.java | 73 +- .../gateway/CrossrefGatewayUnitTest.java | 11 +- .../gateway/DataServiceGatewayUnitTest.java | 1105 ++++- .../gateway/KeycloakGatewayUnitTest.java | 350 +- .../tuwien/gateway/OrcidGatewayUnitTest.java | 11 +- .../at/tuwien/gateway/RorGatewayUnitTest.java | 13 +- .../gateway/SearchServiceGatewayUnitTest.java | 14 + .../tuwien/mapper/MetadataMapperUnitTest.java | 56 +- .../mvc/AuthenticationIntegrationTest.java | 293 ++ .../tuwien/mvc/PrometheusEndpointMvcTest.java | 2 +- .../tuwien/service/AccessServiceUnitTest.java | 22 +- .../AuthenticationServiceIntegrationTest.java | 4 +- .../service/BrokerServiceIntegrationTest.java | 43 +- .../service/ConceptServiceUnitTest.java | 1 + ...aCiteIdentifierServicePersistenceTest.java | 204 +- .../DatabaseServicePersistenceTest.java | 48 +- .../service/DatabaseServiceUnitTest.java | 18 +- .../IdentifierServicePersistenceTest.java | 74 +- .../StorageServiceIntegrationTest.java | 139 + .../service/TableServicePersistenceTest.java | 2 +- .../tuwien/service/TableServiceUnitTest.java | 57 +- .../tuwien/service/UserServiceUnitTest.java | 2 +- .../service/ViewServicePersistenceTest.java | 2 +- .../tuwien/service/ViewServiceUnitTest.java | 24 +- .../test/java/at/tuwien/utils/AmqpUtils.java | 22 +- .../at/tuwien/{config => utils}/H2Utils.java | 2 +- .../java/at/tuwien/utils/KeycloakUtils.java | 40 + .../validator/EndpointValidatorUnitTest.java | 21 +- .../src/test/resources/application.properties | 16 +- .../src/test/resources/init/dbrepo-realm.json | 4345 +++++++++++------ .../auth/BasicAuthenticationProvider.java | 20 +- .../auth/InternalRequestInterceptor.java | 47 + .../java/at/tuwien/config/GatewayConfig.java | 59 +- .../java/at/tuwien/config/KeycloakConfig.java | 15 +- .../java/at/tuwien/config/RabbitConfig.java | 3 + .../at/tuwien/config/WebSecurityConfig.java | 8 +- .../tuwien/gateway/BrokerServiceGateway.java | 22 +- .../at/tuwien/gateway/DataServiceGateway.java | 184 +- .../java/at/tuwien/gateway/OrcidGateway.java | 6 + .../impl/BrokerServiceGatewayImpl.java | 33 +- .../gateway/impl/CrossrefGatewayImpl.java | 8 +- .../gateway/impl/DataServiceGatewayImpl.java | 138 +- .../gateway/impl/KeycloakGatewayImpl.java | 49 +- .../tuwien/gateway/impl/OrcidGatewayImpl.java | 8 +- .../tuwien/gateway/impl/RorGatewayImpl.java | 8 +- .../java/at/tuwien/service/AccessService.java | 22 +- .../java/at/tuwien/service/BrokerService.java | 4 +- .../at/tuwien/service/DatabaseService.java | 26 +- .../at/tuwien/service/IdentifierService.java | 58 +- .../at/tuwien/service/StorageService.java | 3 + .../java/at/tuwien/service/TableService.java | 14 +- .../java/at/tuwien/service/ViewService.java | 6 +- .../service/impl/AccessServiceImpl.java | 12 +- .../impl/BrokerServiceRabbitMqImpl.java | 4 +- .../impl/DataCiteIdentifierServiceImpl.java | 40 +- .../service/impl/DatabaseServiceImpl.java | 21 +- .../service/impl/IdentifierServiceImpl.java | 16 +- .../service/impl/StorageServiceS3Impl.java | 3 + .../tuwien/service/impl/TableServiceImpl.java | 15 +- .../tuwien/service/impl/ViewServiceImpl.java | 6 +- .../main/java/at/tuwien/utils/FileUtil.java | 38 - .../main/java/at/tuwien/test/BaseTest.java | 66 +- dbrepo-search-service/app.py | 8 +- .../clients/opensearch_client.py | 73 +- .../test/test_opensearch_client.py | 278 +- dbrepo-ui/Dockerfile | 9 +- dbrepo-ui/bun.lockb | Bin 441269 -> 0 bytes dbrepo-ui/locales/de-AT.json | 7 + dbrepo-ui/locales/en-US.json | 7 + docker-compose.yml | 17 +- helm/dbrepo/Chart.yaml | 6 +- helm/dbrepo/README.md | 217 +- helm/dbrepo/templates/_compatibility.tpl | 42 + helm/dbrepo/templates/analyse-deployment.yaml | 24 +- helm/dbrepo/templates/data-deployment.yaml | 24 +- .../dbrepo/templates/metadata-deployment.yaml | 24 +- helm/dbrepo/templates/search-deployment.yaml | 24 +- helm/dbrepo/templates/ui-deployment.yaml | 21 +- helm/dbrepo/values.schema.json | 460 +- helm/dbrepo/values.yaml | 229 +- make/gen.mk | 2 +- make/test.mk | 27 - 201 files changed, 8375 insertions(+), 4075 deletions(-) create mode 100644 dbrepo-data-db/enable_history_insert.cnf delete mode 100644 dbrepo-data-service/rest-service/src/main/java/at/tuwien/handlers/ApiExceptionHandler.java delete mode 100644 dbrepo-data-service/rest-service/src/test/java/at/tuwien/gateway/KeycloakSidecarGatewayUnitTest.java delete mode 100644 dbrepo-data-service/rest-service/src/test/java/at/tuwien/handlers/ApiExceptionHandlerTest.java create mode 100644 dbrepo-data-service/rest-service/src/test/java/at/tuwien/utils/UserUtilTest.java create mode 100644 dbrepo-data-service/rest-service/src/test/java/at/tuwien/validation/EndpointValidatorUnitTest.java create mode 100644 dbrepo-data-service/services/src/main/java/at/tuwien/auth/InternalRequestInterceptor.java delete mode 100644 dbrepo-data-service/services/src/main/java/at/tuwien/exception/ContainerNotFoundException.java delete mode 100644 dbrepo-data-service/services/src/main/java/at/tuwien/exception/DatabaseNotFoundException.java delete mode 100644 dbrepo-data-service/services/src/main/java/at/tuwien/exception/FormatNotAvailableException.java delete mode 100644 dbrepo-data-service/services/src/main/java/at/tuwien/exception/NotAllowedException.java delete mode 100644 dbrepo-data-service/services/src/main/java/at/tuwien/exception/PaginationException.java delete mode 100644 dbrepo-data-service/services/src/main/java/at/tuwien/exception/QueryNotFoundException.java delete mode 100644 dbrepo-data-service/services/src/main/java/at/tuwien/exception/StorageNotFoundException.java delete mode 100644 dbrepo-data-service/services/src/main/java/at/tuwien/exception/TableExistsException.java delete mode 100644 dbrepo-data-service/services/src/main/java/at/tuwien/exception/TableNotFoundException.java delete mode 100644 dbrepo-data-service/services/src/main/java/at/tuwien/exception/UserNotFoundException.java delete mode 100644 dbrepo-data-service/services/src/main/java/at/tuwien/exception/ViewNotFoundException.java create mode 100644 dbrepo-metadata-service/api/src/main/java/at/tuwien/api/keycloak/RoleRepresentationDto.java create mode 100644 dbrepo-metadata-service/repositories/src/main/java/at/tuwien/exception/BrokerServiceConnectionException.java rename dbrepo-data-service/services/src/main/java/at/tuwien/exception/StorageUnavailableException.java => dbrepo-metadata-service/repositories/src/main/java/at/tuwien/exception/BrokerServiceException.java (52%) rename dbrepo-metadata-service/repositories/src/main/java/at/tuwien/exception/{ServiceConnectionException.java => DataServiceConnectionException.java} (57%) rename dbrepo-metadata-service/repositories/src/main/java/at/tuwien/exception/{ServiceException.java => DataServiceException.java} (59%) rename {dbrepo-data-service/services => dbrepo-metadata-service/repositories}/src/main/java/at/tuwien/exception/DatabaseMalformedException.java (100%) rename {dbrepo-data-service/services => dbrepo-metadata-service/repositories}/src/main/java/at/tuwien/exception/DatabaseUnavailableException.java (100%) create mode 100644 dbrepo-metadata-service/repositories/src/main/java/at/tuwien/exception/ExternalServiceException.java rename dbrepo-data-service/services/src/main/java/at/tuwien/exception/ServiceConnectionException.java => dbrepo-metadata-service/repositories/src/main/java/at/tuwien/exception/MetadataServiceConnectionException.java (56%) rename dbrepo-data-service/services/src/main/java/at/tuwien/exception/ServiceException.java => dbrepo-metadata-service/repositories/src/main/java/at/tuwien/exception/MetadataServiceException.java (58%) rename {dbrepo-data-service/services => dbrepo-metadata-service/repositories}/src/main/java/at/tuwien/exception/QueryMalformedException.java (100%) rename {dbrepo-data-service/services => dbrepo-metadata-service/repositories}/src/main/java/at/tuwien/exception/QueryNotSupportedException.java (100%) rename {dbrepo-data-service/services => dbrepo-metadata-service/repositories}/src/main/java/at/tuwien/exception/QueryStoreCreateException.java (100%) rename {dbrepo-data-service/services => dbrepo-metadata-service/repositories}/src/main/java/at/tuwien/exception/QueryStoreGCException.java (100%) rename {dbrepo-data-service/services => dbrepo-metadata-service/repositories}/src/main/java/at/tuwien/exception/QueryStoreInsertException.java (100%) rename {dbrepo-data-service/services => dbrepo-metadata-service/repositories}/src/main/java/at/tuwien/exception/QueryStorePersistException.java (100%) rename {dbrepo-data-service/services => dbrepo-metadata-service/repositories}/src/main/java/at/tuwien/exception/RemoteUnavailableException.java (100%) rename {dbrepo-data-service/services => dbrepo-metadata-service/repositories}/src/main/java/at/tuwien/exception/SidecarExportException.java (100%) rename {dbrepo-data-service/services => dbrepo-metadata-service/repositories}/src/main/java/at/tuwien/exception/SidecarImportException.java (100%) rename {dbrepo-data-service/services => dbrepo-metadata-service/repositories}/src/main/java/at/tuwien/exception/TableMalformedException.java (100%) rename {dbrepo-data-service/services => dbrepo-metadata-service/repositories}/src/main/java/at/tuwien/exception/TableSchemaException.java (100%) rename {dbrepo-data-service/services => dbrepo-metadata-service/repositories}/src/main/java/at/tuwien/exception/ViewMalformedException.java (100%) rename {dbrepo-data-service/services => dbrepo-metadata-service/repositories}/src/main/java/at/tuwien/exception/ViewSchemaException.java (100%) create mode 100644 dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/config/RabbitConfig.java create mode 100644 dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/converters/IdentifierTypeConverterUnitTest.java create mode 100644 dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/mvc/AuthenticationIntegrationTest.java create mode 100644 dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/StorageServiceIntegrationTest.java rename dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/{config => utils}/H2Utils.java (97%) create mode 100644 dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/utils/KeycloakUtils.java create mode 100644 dbrepo-metadata-service/services/src/main/java/at/tuwien/auth/InternalRequestInterceptor.java delete mode 100644 dbrepo-metadata-service/services/src/main/java/at/tuwien/utils/FileUtil.java delete mode 100755 dbrepo-ui/bun.lockb create mode 100644 helm/dbrepo/templates/_compatibility.tpl diff --git a/.docs/api/identity-service.md b/.docs/api/identity-service.md index 1fe3454956..e01ad63fc2 100644 --- a/.docs/api/identity-service.md +++ b/.docs/api/identity-service.md @@ -12,10 +12,13 @@ author: Martin Weise ## Overview -This service holds the user identities which we simply call identities in the following. It is integrated into the -[Auth Service](../auth-service) through an LDAP federation, allowing any identity to authenticate through the Auth -Service. The LDAP protocol is not used for authentication. You can use your own identity provider, e.g. Active -Directory. +This optional service holds the user identities which we simply call identities in the following. It is integrated into +the [Auth Service](../auth-service) through an LDAP federation, allowing any identity to authenticate through the Auth +Service. The LDAP protocol is not used for authentication. + +The Identity Service can be optionally replaced with your existing LDAP solution. Your LDAP solution should store +users using the RFC 2798 [`InetOrgPerson`](https://datatracker.ietf.org/doc/html/rfc2798) schema which is standard +to most LDAP solutions. ## Identities @@ -23,6 +26,9 @@ Any identity is identified by its `entryUUID` by default in the Auth Service. No the Auth Service) may assign a different UUID to a user. DBRepo **always** uses the UUID provided through the Identity Service. +The field `uid` is the username and is used for bind/unbind operations. The fields `cn` and `sn` are ignored by the +Auth Service and can be empty `""`. + ## Limitations * Limited support for scaling in Kubernetes, see the diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index c934e6a459..605b27a39c 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -100,6 +100,7 @@ build-data-service: dependencies: - build-metadata-service script: + - "mvn -f ./dbrepo-metadata-service/pom.xml clean install $MAVEN_OPTS -DskipTests" - "mvn -f ./dbrepo-data-service/pom.xml clean package $MAVEN_OPTS -DskipTests" # Compiled classes are needed for SonarQube in later stages artifacts: @@ -169,8 +170,8 @@ verify-install-script: - bash install.sh - exit 0 -lint-helm: - image: docker.io/docker:24-dind +lint-helm-chart: + image: docker.io/alpine:3.20 stage: lint except: refs: @@ -179,8 +180,9 @@ lint-helm: - build-metadata-service dependencies: - build-metadata-service + before_script: + - apk add helm script: - - apk add sed helm curl - helm lint ./helm/dbrepo test-metadata-service: @@ -271,7 +273,7 @@ test-search-service: script: - "pip install pipenv" - "pipenv install gunicorn && pipenv install --dev --system --deploy" - - cd ./dbrepo-search-service/ && coverage run -m pytest test/test_opensearch_client.py --junitxml=report.xml && coverage html --omit="test/*" && coverage report --omit="test/*" > ./coverage.txt + - cd ./dbrepo-search-service/ && coverage run -m pytest test/test_opensearch_client.py --junitxml=report.xml && coverage html --omit="test/*,omlib/*" && coverage report --omit="test/*,omlib/*" > ./coverage.txt - "cat ./coverage.txt | grep -o 'TOTAL[^%]*%'" artifacts: when: always diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e6a6e8d951..4e9c81e563 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -35,4 +35,11 @@ a couple of days at maximum, one could go directly for a PR. It's fine. - [ ] Change the maven version in the metadata & data services: - `mvn -f ./dbrepo-metadata-service/pom.xml versions:set -DnewVersion=VERSION` - `mvn -f ./dbrepo-data-service/pom.xml versions:set -DnewVersion=VERSION` -- [ ] Change the versions in `versions.json` for the generated website \ No newline at end of file +- [ ] Change the versions in `versions.json` for the generated website + +Then generate the REST API-, Python Library- and Helm Chart documentation: + +```bash +# optional: pip install -r ./requirements.txt +make gen-swagger-doc gen-lib-doc gen-helm-doc +``` \ No newline at end of file diff --git a/dbrepo-analyse-service/app.py b/dbrepo-analyse-service/app.py index bfc8212864..c006e5777b 100644 --- a/dbrepo-analyse-service/app.py +++ b/dbrepo-analyse-service/app.py @@ -185,8 +185,6 @@ app.config["JWT_PUBKEY"] = '-----BEGIN PUBLIC KEY-----\n' + os.getenv("JWT_PUBKE app.config["AUTH_SERVICE_ENDPOINT"] = os.getenv("AUTH_SERVICE_ENDPOINT", "http://localhost/api/auth") app.config["AUTH_SERVICE_CLIENT"] = os.getenv("AUTH_SERVICE_CLIENT", "dbrepo-client") app.config["AUTH_SERVICE_CLIENT_SECRET"] = os.getenv("AUTH_SERVICE_CLIENT_SECRET", "MUwRc7yfXSJwX8AdRMWaQC3Nep1VjwgG") -app.config["ADMIN_USERNAME"] = os.getenv('ADMIN_USERNAME', 'admin') -app.config["ADMIN_PASSWORD"] = os.getenv('ADMIN_PASSWORD', 'admin') app.config["S3_ACCESS_KEY_ID"] = os.getenv('S3_ACCESS_KEY_ID', 'seaweedfsadmin') app.config["S3_ENDPOINT"] = os.getenv('S3_ENDPOINT', 'http://localhost:9000') app.config["S3_EXPORT_BUCKET"] = os.getenv('S3_EXPORT_BUCKET', 'dbrepo-download') @@ -211,8 +209,6 @@ def verify_token(token: str): def verify_password(username: str, password: str) -> Any: if username is None or username == "" or password is None or password == "": return False - if username == app.config["ADMIN_USERNAME"] and password == app.config["ADMIN_PASSWORD"]: - return User(username=username, roles=["admin"]) client = KeycloakClient() try: return client.verify_jwt(access_token=client.obtain_user_token(username=username, password=password)) diff --git a/dbrepo-auth-service/dbrepo-realm.json b/dbrepo-auth-service/dbrepo-realm.json index 270ca00a1f..87eb81e777 100644 --- a/dbrepo-auth-service/dbrepo-realm.json +++ b/dbrepo-auth-service/dbrepo-realm.json @@ -66,6 +66,17 @@ "clientRole" : false, "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", "attributes" : { } + }, { + "id" : "7ee1c424-11b0-46a9-b0ed-725e9b7fc40c", + "name" : "default-system-roles", + "description" : "${default-system-roles}", + "composite" : true, + "composites" : { + "realm" : [ "delete-database-view", "update-semantic-unit", "export-query-data", "check-foreign-database-access", "default-data-steward-roles", "execute-query", "default-user-handling", "delete-table-data", "find-query", "list-database-views", "persist-query", "update-search-index", "delete-database-access", "view-table-history", "create-ontology", "update-ontology", "modify-user-theme", "default-system-roles", "create-semantic-concept", "default-container-handling", "create-container", "create-table", "default-broker-handling", "default-maintenance-handling", "execute-semantic-query", "uma_authorization", "table-semantic-analyse", "list-containers", "check-database-access", "escalated-query-handling", "delete-identifier", "modify-database-owner", "list-tables", "export-table-data", "create-database-access", "delete-container", "re-execute-query", "create-semantic-unit", "escalated-identifier-handling", "system", "update-table-statistic", "escalated-semantics-handling", "default-database-handling", "delete-ontology", "find-database", "find-database-view", "update-semantic-concept", "find-user", "import-database-data", "publish-identifier", "default-roles-dbrepo", "find-foreign-user", "create-database", "create-maintenance-message", "find-maintenance-message", "escalated-container-handling", "default-researcher-roles", "default-identifier-handling", "escalated-user-handling", "modify-user-information", "create-database-view", "update-maintenance-message", "delete-foreign-table", "offline_access", "modify-foreign-table-column-semantics", "delete-maintenance-message", "find-container", "insert-table-data", "modify-identifier-metadata", "modify-database-image", "escalated-broker-handling", "modify-table-column-semantics", "escalated-database-handling", "default-semantics-handling", "update-database-access", "default-query-handling", "find-table", "list-queries", "default-developer-roles", "create-identifier", "escalated-table-handling", "find-identifier", "view-database-view-data", "view-table-data", "list-licenses", "default-table-handling", "list-identifiers", "create-foreign-identifier", "list-databases", "list-ontologies", "modify-database-visibility", "list-maintenance-messages", "delete-table" ] + }, + "clientRole" : false, + "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", + "attributes" : { } }, { "id" : "143ba359-5fa2-451e-8296-43ecf20bb251", "name" : "update-semantic-concept", @@ -104,6 +115,14 @@ "clientRole" : false, "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", "attributes" : { } + }, { + "id" : "74648f9a-777e-4ef9-b97b-4c5d749d862f", + "name" : "update-search-index", + "description" : "${update-search-index}", + "composite" : false, + "clientRole" : false, + "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", + "attributes" : { } }, { "id" : "22492b64-c633-48a0-9678-b28669f2885b", "name" : "execute-semantic-query", @@ -131,14 +150,6 @@ "clientRole" : false, "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", "attributes" : { } - }, { - "id" : "79534da1-4c85-409e-810e-a7ce6d632b09", - "name" : "system", - "description" : "${system}", - "composite" : false, - "clientRole" : false, - "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", - "attributes" : { } }, { "id" : "b0d66d3d-59b4-4aae-aa66-e3d5a49f28e3", "name" : "view-database-view-data", @@ -389,6 +400,14 @@ "clientRole" : false, "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", "attributes" : { } + }, { + "id" : "b05e9b2b-748d-490b-949b-e78655bf7805", + "name" : "check-foreign-database-access", + "description" : "${check-foreign-database-access}", + "composite" : false, + "clientRole" : false, + "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", + "attributes" : { } }, { "id" : "c047d521-cec3-4444-86c4-aef098489b7b", "name" : "delete-maintenance-message", @@ -397,6 +416,14 @@ "clientRole" : false, "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", "attributes" : { } + }, { + "id" : "88f82262-be80-4d18-9fb4-5529da031f33", + "name" : "system", + "description" : "${system}", + "composite" : false, + "clientRole" : false, + "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", + "attributes" : { } }, { "id" : "e14ab76b-1c24-484d-ae2d-478b8457edea", "name" : "list-licenses", @@ -646,6 +673,14 @@ "clientRole" : false, "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", "attributes" : { } + }, { + "id" : "0c487c93-448f-4a82-8b9f-ebd8a0904bf8", + "name" : "find-foreign-user", + "description" : "${find-foreign-user}", + "composite" : false, + "clientRole" : false, + "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", + "attributes" : { } }, { "id" : "cf9735a9-fb70-4cc5-b5f4-75afc4e5654b", "name" : "modify-identifier-metadata", @@ -1089,6 +1124,14 @@ "realmRoles" : [ "default-researcher-roles" ], "clientRoles" : { }, "subGroups" : [ ] + }, { + "id" : "2b9f94b4-d434-4a98-8eab-25678cfee983", + "name" : "system", + "path" : "/system", + "attributes" : { }, + "realmRoles" : [ "default-system-roles" ], + "clientRoles" : { }, + "subGroups" : [ ] } ], "defaultRole" : { "id" : "abd2d9ee-ebc4-4d0a-839e-6b588a6d442a", @@ -1107,7 +1150,7 @@ "otpPolicyLookAheadWindow" : 1, "otpPolicyPeriod" : 30, "otpPolicyCodeReusable" : false, - "otpSupportedApplications" : [ "totpAppMicrosoftAuthenticatorName", "totpAppFreeOTPName", "totpAppGoogleName" ], + "otpSupportedApplications" : [ "totpAppFreeOTPName", "totpAppGoogleName", "totpAppMicrosoftAuthenticatorName" ], "webAuthnPolicyRpEntityName" : "keycloak", "webAuthnPolicySignatureAlgorithms" : [ "ES256" ], "webAuthnPolicyRpId" : "", @@ -2095,7 +2138,7 @@ "subType" : "anonymous", "subComponents" : { }, "config" : { - "allowed-protocol-mapper-types" : [ "oidc-usermodel-property-mapper", "oidc-full-name-mapper", "oidc-address-mapper", "oidc-sha256-pairwise-sub-mapper", "oidc-usermodel-attribute-mapper", "saml-user-property-mapper", "saml-user-attribute-mapper", "saml-role-list-mapper" ] + "allowed-protocol-mapper-types" : [ "oidc-usermodel-property-mapper", "oidc-address-mapper", "saml-user-attribute-mapper", "oidc-usermodel-attribute-mapper", "saml-role-list-mapper", "oidc-sha256-pairwise-sub-mapper", "oidc-full-name-mapper", "saml-user-property-mapper" ] } }, { "id" : "1849e52a-b8c9-44a8-af3d-ee19376a1ed1", @@ -2121,7 +2164,7 @@ "subType" : "authenticated", "subComponents" : { }, "config" : { - "allowed-protocol-mapper-types" : [ "oidc-usermodel-property-mapper", "saml-user-attribute-mapper", "oidc-full-name-mapper", "saml-user-property-mapper", "oidc-usermodel-attribute-mapper", "oidc-sha256-pairwise-sub-mapper", "oidc-address-mapper", "saml-role-list-mapper" ] + "allowed-protocol-mapper-types" : [ "oidc-full-name-mapper", "saml-role-list-mapper", "oidc-address-mapper", "oidc-usermodel-property-mapper", "saml-user-property-mapper", "oidc-usermodel-attribute-mapper", "oidc-sha256-pairwise-sub-mapper", "saml-user-attribute-mapper" ] } } ], "org.keycloak.storage.UserStorageProvider" : [ { @@ -2149,8 +2192,8 @@ "config" : { "ldap.attribute" : [ "sn" ], "is.mandatory.in.ldap" : [ "true" ], - "always.read.value.from.ldap" : [ "true" ], "read.only" : [ "false" ], + "always.read.value.from.ldap" : [ "true" ], "user.model.attribute" : [ "lastName" ] } }, { @@ -2185,17 +2228,17 @@ "config" : { "membership.attribute.type" : [ "DN" ], "group.name.ldap.attribute" : [ "cn" ], - "preserve.group.inheritance" : [ "false" ], "membership.user.ldap.attribute" : [ "uid" ], - "groups.dn" : [ "cn=system,ou=users,dc=dbrepo,dc=at" ], + "preserve.group.inheritance" : [ "false" ], + "groups.dn" : [ "ou=users,dc=dbrepo,dc=at" ], "mode" : [ "LDAP_ONLY" ], "user.roles.retrieve.strategy" : [ "LOAD_GROUPS_BY_MEMBER_ATTRIBUTE" ], - "ignore.missing.groups" : [ "false" ], "membership.ldap.attribute" : [ "member" ], - "memberof.ldap.attribute" : [ "memberOf" ], + "ignore.missing.groups" : [ "false" ], "group.object.classes" : [ "groupOfNames" ], - "groups.path" : [ "/" ], - "drop.non.existing.groups.during.sync" : [ "false" ] + "memberof.ldap.attribute" : [ "memberOf" ], + "drop.non.existing.groups.during.sync" : [ "false" ], + "groups.path" : [ "/" ] } }, { "id" : "b6ff3285-35af-4e86-8bb4-d94b8e0d70bb", @@ -2205,8 +2248,8 @@ "config" : { "ldap.attribute" : [ "modifyTimestamp" ], "is.mandatory.in.ldap" : [ "false" ], - "always.read.value.from.ldap" : [ "true" ], "read.only" : [ "true" ], + "always.read.value.from.ldap" : [ "true" ], "user.model.attribute" : [ "modifyTimestamp" ] } }, { @@ -2219,8 +2262,8 @@ "attribute.force.default" : [ "false" ], "is.mandatory.in.ldap" : [ "true" ], "is.binary.attribute" : [ "false" ], - "always.read.value.from.ldap" : [ "false" ], "read.only" : [ "false" ], + "always.read.value.from.ldap" : [ "false" ], "user.model.attribute" : [ "username" ] } } ] @@ -2229,16 +2272,16 @@ "fullSyncPeriod" : [ "-1" ], "pagination" : [ "false" ], "startTls" : [ "false" ], - "usersDn" : [ "ou=users,dc=dbrepo,dc=at" ], "connectionPooling" : [ "true" ], + "usersDn" : [ "ou=users,dc=dbrepo,dc=at" ], "cachePolicy" : [ "DEFAULT" ], "useKerberosForPasswordAuthentication" : [ "false" ], "importEnabled" : [ "true" ], "enabled" : [ "true" ], - "usernameLDAPAttribute" : [ "uid" ], - "bindDn" : [ "cn=admin,dc=dbrepo,dc=at" ], - "bindCredential" : [ "adminpassword" ], "changedSyncPeriod" : [ "-1" ], + "bindDn" : [ "cn=admin,dc=dbrepo,dc=at" ], + "usernameLDAPAttribute" : [ "uid" ], + "bindCredential" : [ "admin" ], "lastSync" : [ "1719252666" ], "vendor" : [ "other" ], "uuidLDAPAttribute" : [ "entryUUID" ], @@ -2304,7 +2347,7 @@ "internationalizationEnabled" : false, "supportedLocales" : [ ], "authenticationFlows" : [ { - "id" : "df1ebc5f-2037-43f5-9915-71eb4cd0ed7e", + "id" : "259dd7b6-01b7-433a-bda4-028857151ecd", "alias" : "Account verification options", "description" : "Method with which to verity the existing account", "providerId" : "basic-flow", @@ -2326,7 +2369,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "76ef2d26-2756-4ce1-904b-4be58e99b576", + "id" : "f94a4b6d-deaa-4505-be0f-544828436fa1", "alias" : "Authentication Options", "description" : "Authentication options.", "providerId" : "basic-flow", @@ -2355,7 +2398,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "b0d74a54-cea7-48f2-a4c9-f35204488da6", + "id" : "542ca1d7-9627-4102-b843-98837ce433fb", "alias" : "Browser - Conditional OTP", "description" : "Flow to determine if the OTP is required for the authentication", "providerId" : "basic-flow", @@ -2377,7 +2420,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "07b964c7-4527-4071-9f7a-e50d6321d951", + "id" : "4f153b98-6851-440b-a022-0a14e67a9b2f", "alias" : "Direct Grant - Conditional OTP", "description" : "Flow to determine if the OTP is required for the authentication", "providerId" : "basic-flow", @@ -2399,7 +2442,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "be69fd2d-1bf5-429e-9833-a76232a23904", + "id" : "3d791b35-d35c-40b2-bb3e-e806d72b27ee", "alias" : "First broker login - Conditional OTP", "description" : "Flow to determine if the OTP is required for the authentication", "providerId" : "basic-flow", @@ -2421,7 +2464,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "e9d23d2a-d857-4547-a419-2fd850ed58e5", + "id" : "9b746104-9371-4c3f-b69f-9322cead1b08", "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", @@ -2443,7 +2486,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "75e1f120-8a28-4cc0-af60-26fa9d865975", + "id" : "7a164efe-c97b-4fbb-950d-7745359ba9a4", "alias" : "Reset - Conditional OTP", "description" : "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.", "providerId" : "basic-flow", @@ -2465,7 +2508,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "eeb37a0b-2f2f-47f5-9ee6-3da2c8b48ec0", + "id" : "4fdc5e1b-1b55-4662-8360-67d75fa22677", "alias" : "User creation or linking", "description" : "Flow for the existing/non-existing user alternatives", "providerId" : "basic-flow", @@ -2488,7 +2531,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "8637f64c-8b45-48b0-b3ba-c6e93225cce4", + "id" : "75893341-c338-44d8-ae27-a3fc7bfe8f2d", "alias" : "Verify Existing Account by Re-authentication", "description" : "Reauthentication of existing account", "providerId" : "basic-flow", @@ -2510,7 +2553,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "7ad56360-b344-4f26-9dea-1a718ed99d4e", + "id" : "89626b76-f4cf-4c46-934c-4408c225a44b", "alias" : "browser", "description" : "browser based authentication", "providerId" : "basic-flow", @@ -2546,7 +2589,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "c6817917-1d21-4693-9171-b2e3dfde9582", + "id" : "4112115a-e7a7-44c2-9af5-65d538e4ba0d", "alias" : "clients", "description" : "Base authentication for clients", "providerId" : "client-flow", @@ -2582,7 +2625,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "7cd02437-6d05-486d-a7fe-4d1762895ded", + "id" : "f82a9b0a-2c0a-4cb1-96b2-6c78b0b1f14f", "alias" : "direct grant", "description" : "OpenID Connect Resource Owner Grant", "providerId" : "basic-flow", @@ -2611,7 +2654,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "eb1d9721-b4a0-40a5-9236-b4fd95ca9024", + "id" : "3614e155-e8ce-4958-98fb-a27e4706cc70", "alias" : "docker auth", "description" : "Used by Docker clients to authenticate against the IDP", "providerId" : "basic-flow", @@ -2626,7 +2669,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "219415d8-3bab-47a6-9d0c-8c1061ffb68a", + "id" : "506f9b96-5002-47c0-96e3-3830a0fcfa26", "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", @@ -2649,7 +2692,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "ccbf8944-bd32-4aa1-a6f8-93513a3fa5a4", + "id" : "4b7a7e91-36db-4b27-8e2d-01a04a822980", "alias" : "forms", "description" : "Username, password, otp and other auth forms.", "providerId" : "basic-flow", @@ -2671,7 +2714,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "805f972b-75ca-48c0-a390-752b32c0688a", + "id" : "f8ba3c2e-3952-4434-98e8-b892eea90e9e", "alias" : "http challenge", "description" : "An authentication flow based on challenge-response HTTP Authentication Schemes", "providerId" : "basic-flow", @@ -2693,7 +2736,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "2b15383f-ded1-4fb6-afdc-0c19f65dacc7", + "id" : "04c2fe01-5076-4aa4-9596-4efb4004195f", "alias" : "registration", "description" : "registration flow", "providerId" : "basic-flow", @@ -2709,7 +2752,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "1c18c3c7-a191-426b-84a4-1ffec96562cc", + "id" : "d12f77e1-7733-44a2-98ff-fd75c784d721", "alias" : "registration form", "description" : "registration form", "providerId" : "form-flow", @@ -2745,7 +2788,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "cab07ead-2a48-4b0c-8916-2f89abe55720", + "id" : "91f6048c-a376-4809-8f37-c8d7a517830c", "alias" : "reset credentials", "description" : "Reset credentials for a user if they forgot their password or something", "providerId" : "basic-flow", @@ -2781,7 +2824,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "6e158077-d221-4695-b0d3-9528c5ba6bfd", + "id" : "7b8fb487-53b8-4533-a696-76bc05256cb1", "alias" : "saml ecp", "description" : "SAML ECP Profile Authentication Flow", "providerId" : "basic-flow", @@ -2797,13 +2840,13 @@ } ] } ], "authenticatorConfig" : [ { - "id" : "fcb6cb09-fec5-4390-800c-00a3d49525ec", + "id" : "48372696-0579-45e5-b074-5e8dbdbbe7d6", "alias" : "create unique user config", "config" : { "require.password.update.after.registration" : "false" } }, { - "id" : "68f9e765-81d4-47cd-b111-94d6723883c5", + "id" : "08df3b83-e522-42a7-9e24-9028b960bf39", "alias" : "review profile config", "config" : { "update.profile.on.first.login" : "missing" diff --git a/dbrepo-data-db/README.md b/dbrepo-data-db/README.md index 94eb341d84..c2dfb1b0c6 100644 --- a/dbrepo-data-db/README.md +++ b/dbrepo-data-db/README.md @@ -1 +1,5 @@ -# Data Database \ No newline at end of file +# Data Database + +S3 Import + +https://mariadb.com/kb/en/s3-storage-engine-system-variables/ \ No newline at end of file diff --git a/dbrepo-data-db/enable_history_insert.cnf b/dbrepo-data-db/enable_history_insert.cnf new file mode 100644 index 0000000000..7bced156c8 --- /dev/null +++ b/dbrepo-data-db/enable_history_insert.cnf @@ -0,0 +1 @@ +secure_timestamp="SUPER" \ No newline at end of file diff --git a/dbrepo-data-db/sidecar/app.py b/dbrepo-data-db/sidecar/app.py index c88966bb00..40cb9fa4aa 100644 --- a/dbrepo-data-db/sidecar/app.py +++ b/dbrepo-data-db/sidecar/app.py @@ -119,8 +119,6 @@ app.config["JWT_PUBKEY"] = '-----BEGIN PUBLIC KEY-----\n' + os.getenv("JWT_PUBKE app.config["AUTH_SERVICE_ENDPOINT"] = os.getenv("AUTH_SERVICE_ENDPOINT", "http://localhost/api/auth") app.config["AUTH_SERVICE_CLIENT"] = os.getenv("AUTH_SERVICE_CLIENT", "dbrepo-client") app.config["AUTH_SERVICE_CLIENT_SECRET"] = os.getenv("AUTH_SERVICE_CLIENT_SECRET", "MUwRc7yfXSJwX8AdRMWaQC3Nep1VjwgG") -app.config["ADMIN_USERNAME"] = os.getenv('ADMIN_USERNAME', 'admin') -app.config["ADMIN_PASSWORD"] = os.getenv('ADMIN_PASSWORD', 'admin') app.config["S3_ACCESS_KEY_ID"] = os.getenv('S3_ACCESS_KEY_ID', 'seaweedfsadmin') app.config["S3_ENDPOINT"] = os.getenv('S3_ENDPOINT', 'http://localhost:9000') app.config["S3_EXPORT_BUCKET"] = os.getenv('S3_EXPORT_BUCKET', 'dbrepo-download') @@ -146,8 +144,6 @@ def verify_token(token: str): def verify_password(username: str, password: str) -> Any: if username is None or username == "" or password is None or password == "": return False - if username == app.config["ADMIN_USERNAME"] and password == app.config["ADMIN_PASSWORD"]: - return User(username=username, roles=["admin"]) client = KeycloakClient() try: return client.verify_jwt(access_token=client.obtain_user_token(username=username, password=password)) @@ -178,7 +174,7 @@ def health(): @app.route("/sidecar/import/<string:filename>", methods=["POST"], endpoint="sidecar_import") @metrics.gauge(name='dbrepo_sidecar_import_dataset', description='Time needed to import dataset from S3') -@auth.login_required(role=['admin', 'import-database-data']) +@auth.login_required(role=['import-database-data']) @swag_from("ds-yml/import.yml") def import_csv(filename): auth.current_user() @@ -192,7 +188,7 @@ def import_csv(filename): @app.route("/sidecar/export/<string:filename>", methods=["POST"], endpoint="sidecar_export") @metrics.gauge(name='dbrepo_sidecar_export_dataset', description='Time needed to export dataset to S3') -@auth.login_required(role=['admin', 'export-query-data', 'export-table-data']) +@auth.login_required(role=['export-query-data', 'export-table-data']) @swag_from("ds-yml/export.yml") def import_csv(filename): logging.debug('endpoint export csv, filename=%s, body=%s', filename, request) diff --git a/dbrepo-data-service/Dockerfile b/dbrepo-data-service/Dockerfile index d4016836d9..806908a8af 100644 --- a/dbrepo-data-service/Dockerfile +++ b/dbrepo-data-service/Dockerfile @@ -28,9 +28,9 @@ RUN apk add --no-cache curl bash jq WORKDIR /app -USER 65534 +USER 1001 -COPY --from=build --chown=65534 ./rest-service/target/rest-service-*.jar ./data-service.jar +COPY --from=build --chown=1001 ./rest-service/target/rest-service-*.jar ./data-service.jar # non-root port EXPOSE 8080 diff --git a/dbrepo-data-service/metrics.md b/dbrepo-data-service/metrics.md index 425b58ad17..8219898284 100644 --- a/dbrepo-data-service/metrics.md +++ b/dbrepo-data-service/metrics.md @@ -2,18 +2,18 @@ |-----------------------------|-------------------------------------------| | `dbrepo_message_receive` | Received AMQP message from Broker Service | | `dbrepo_subset_create` | Create subset | -| `dbrepo_subset_data` | Retrieved subset data | +| `dbrepo_subset_data` | Get subset data | | `dbrepo_subset_find` | Find subset | | `dbrepo_subset_list` | Find subsets | | `dbrepo_subset_persist` | Persist subset | -| `dbrepo_table_data_create` | Insert a raw data tuple | -| `dbrepo_table_data_delete` | Delete table data | -| `dbrepo_table_data_export` | Export table data | -| `dbrepo_table_data_history` | Find table history | -| `dbrepo_table_data_import` | Import data from a dataset | -| `dbrepo_table_data_list` | Retrieve table data | -| `dbrepo_table_data_update` | Update a raw data tuple | -| `dbrepo_table_schema_list` | Find table schemas | -| `dbrepo_table_statistic` | Generate table statistic | -| `dbrepo_view_data` | Retrieve view data | -| `dbrepo_view_schema_list` | Find view schemas | +| `dbrepo_table_data_create` | Insert tuple | +| `dbrepo_table_data_delete` | Delete tuple | +| `dbrepo_table_data_export` | Get table data | +| `dbrepo_table_data_history` | Get history | +| `dbrepo_table_data_import` | Import dataset | +| `dbrepo_table_data_list` | Get table data | +| `dbrepo_table_data_update` | Update tuple | +| `dbrepo_table_schema_list` | Find tables | +| `dbrepo_table_statistic` | Get table statistic | +| `dbrepo_view_data` | Get view data | +| `dbrepo_view_schema_list` | Find views | diff --git a/dbrepo-data-service/pom.xml b/dbrepo-data-service/pom.xml index 76ead57517..fa6f32a02f 100644 --- a/dbrepo-data-service/pom.xml +++ b/dbrepo-data-service/pom.xml @@ -173,6 +173,12 @@ <artifactId>dbrepo-metadata-service-api</artifactId> <version>${project.version}</version> </dependency> + <!-- Exceptions --> + <dependency> + <groupId>at.tuwien</groupId> + <artifactId>dbrepo-metadata-service-repositories</artifactId> + <version>${project.version}</version> + </dependency> <!-- AMPQ --> <dependency> <groupId>org.springframework.amqp</groupId> diff --git a/dbrepo-data-service/rest-service/src/main/java/at/tuwien/endpoints/AccessEndpoint.java b/dbrepo-data-service/rest-service/src/main/java/at/tuwien/endpoints/AccessEndpoint.java index 4966e00842..133bee769c 100644 --- a/dbrepo-data-service/rest-service/src/main/java/at/tuwien/endpoints/AccessEndpoint.java +++ b/dbrepo-data-service/rest-service/src/main/java/at/tuwien/endpoints/AccessEndpoint.java @@ -41,7 +41,7 @@ public class AccessEndpoint { } @PostMapping("/{userId}") - @PreAuthorize("hasAuthority('admin')") + @PreAuthorize("hasAuthority('system')") @Operation(summary = "Give access", security = {@SecurityRequirement(name = "basicAuth")}, hidden = true) @@ -78,7 +78,7 @@ public class AccessEndpoint { @NotBlank @PathVariable("userId") UUID userId, @Valid @RequestBody UpdateDatabaseAccessDto data) throws NotAllowedException, DatabaseUnavailableException, DatabaseNotFoundException, - RemoteUnavailableException, UserNotFoundException, DatabaseMalformedException, ServiceException { + RemoteUnavailableException, UserNotFoundException, DatabaseMalformedException, MetadataServiceException { log.debug("endpoint give access to database, databaseId={}, userId={}", databaseId, userId); final PrivilegedDatabaseDto database = metadataServiceGateway.getDatabaseById(databaseId); final PrivilegedUserDto user = metadataServiceGateway.getPrivilegedUserById(userId); @@ -97,7 +97,7 @@ public class AccessEndpoint { } @PutMapping("/{userId}") - @PreAuthorize("hasAuthority('admin')") + @PreAuthorize("hasAuthority('system')") @Operation(summary = "Update access", security = {@SecurityRequirement(name = "basicAuth")}, hidden = true) @@ -134,7 +134,7 @@ public class AccessEndpoint { @NotBlank @PathVariable("userId") UUID userId, @Valid @RequestBody UpdateDatabaseAccessDto access) throws NotAllowedException, DatabaseUnavailableException, DatabaseNotFoundException, RemoteUnavailableException, UserNotFoundException, - DatabaseMalformedException, ServiceException { + DatabaseMalformedException, MetadataServiceException { log.debug("endpoint modify access to database, databaseId={}, userId={}, access.type={}", databaseId, userId, access.getType()); final PrivilegedDatabaseDto database = metadataServiceGateway.getDatabaseById(databaseId); @@ -154,7 +154,7 @@ public class AccessEndpoint { } @DeleteMapping("/{userId}") - @PreAuthorize("hasAuthority('admin')") + @PreAuthorize("hasAuthority('system')") @Operation(summary = "Revoke access", security = {@SecurityRequirement(name = "basicAuth")}, hidden = true) @@ -190,7 +190,7 @@ public class AccessEndpoint { public ResponseEntity<Void> revoke(@NotBlank @PathVariable("databaseId") Long databaseId, @NotBlank @PathVariable("userId") UUID userId) throws NotAllowedException, DatabaseUnavailableException, DatabaseNotFoundException, RemoteUnavailableException, UserNotFoundException, - DatabaseMalformedException, ServiceException { + DatabaseMalformedException, MetadataServiceException { log.debug("endpoint revoke access to database, databaseId={}, userId={}", databaseId, userId); final PrivilegedDatabaseDto database = metadataServiceGateway.getDatabaseById(databaseId); final UserDto user = metadataServiceGateway.getUserById(userId); diff --git a/dbrepo-data-service/rest-service/src/main/java/at/tuwien/endpoints/DatabaseEndpoint.java b/dbrepo-data-service/rest-service/src/main/java/at/tuwien/endpoints/DatabaseEndpoint.java index 5397ba1584..9cefc57fa2 100644 --- a/dbrepo-data-service/rest-service/src/main/java/at/tuwien/endpoints/DatabaseEndpoint.java +++ b/dbrepo-data-service/rest-service/src/main/java/at/tuwien/endpoints/DatabaseEndpoint.java @@ -53,7 +53,7 @@ public class DatabaseEndpoint { } @PostMapping - @PreAuthorize("hasAuthority('admin')") + @PreAuthorize("hasAuthority('system')") @Operation(summary = "Create database", security = {@SecurityRequirement(name = "basicAuth")}, hidden = true) @@ -86,7 +86,7 @@ public class DatabaseEndpoint { }) public ResponseEntity<DatabaseDto> create(@Valid @RequestBody CreateDatabaseDto data) throws DatabaseUnavailableException, RemoteUnavailableException, ContainerNotFoundException, - DatabaseMalformedException, QueryStoreCreateException, ServiceException { + DatabaseMalformedException, QueryStoreCreateException, MetadataServiceException { log.debug("endpoint create database, data.containerId={}, data.internalName={}, data.username={}", data.getContainerId(), data.getInternalName(), data.getUsername()); final PrivilegedContainerDto container = metadataServiceGateway.getContainerById(data.getContainerId()); @@ -108,7 +108,7 @@ public class DatabaseEndpoint { } @PutMapping("/{databaseId}") - @PreAuthorize("hasAuthority('admin')") + @PreAuthorize("hasAuthority('system')") @Operation(summary = "Update user password", security = {@SecurityRequirement(name = "basicAuth")}, hidden = true) @@ -134,7 +134,7 @@ public class DatabaseEndpoint { public ResponseEntity<Void> update(@NotBlank @PathVariable("databaseId") Long databaseId, @Valid @RequestBody UpdateUserPasswordDto data) throws DatabaseUnavailableException, DatabaseNotFoundException, RemoteUnavailableException, - DatabaseMalformedException, ServiceException { + DatabaseMalformedException, MetadataServiceException { log.debug("endpoint update user password in database, databaseId={}, data.username={}", databaseId, data.getUsername()); final PrivilegedDatabaseDto database = metadataServiceGateway.getDatabaseById(databaseId); diff --git a/dbrepo-data-service/rest-service/src/main/java/at/tuwien/endpoints/SubsetEndpoint.java b/dbrepo-data-service/rest-service/src/main/java/at/tuwien/endpoints/SubsetEndpoint.java index e75cd571eb..f4626deb93 100644 --- a/dbrepo-data-service/rest-service/src/main/java/at/tuwien/endpoints/SubsetEndpoint.java +++ b/dbrepo-data-service/rest-service/src/main/java/at/tuwien/endpoints/SubsetEndpoint.java @@ -89,7 +89,7 @@ public class SubsetEndpoint { @RequestParam(name = "persisted", required = false) Boolean filterPersisted, Principal principal) throws DatabaseUnavailableException, DatabaseNotFoundException, RemoteUnavailableException, - QueryNotFoundException, NotAllowedException, ServiceException { + QueryNotFoundException, NotAllowedException, MetadataServiceException { log.debug("endpoint find subsets in database, databaseId={}, filterPersisted={}", databaseId, filterPersisted); final PrivilegedDatabaseDto database = metadataServiceGateway.getDatabaseById(databaseId); if (!database.getIsPublic()) { @@ -156,7 +156,7 @@ public class SubsetEndpoint { throws DatabaseUnavailableException, DatabaseNotFoundException, RemoteUnavailableException, QueryNotFoundException, FormatNotAvailableException, StorageUnavailableException, QueryMalformedException, SidecarExportException, StorageNotFoundException, NotAllowedException, UserNotFoundException, - ServiceException { + MetadataServiceException { String accept = httpServletRequest.getHeader("Accept"); log.debug("endpoint find subset in database, databaseId={}, subsetId={}, accept={}, timestamp={}", databaseId, subsetId, accept, timestamp); @@ -260,7 +260,7 @@ public class SubsetEndpoint { throws DatabaseUnavailableException, DatabaseNotFoundException, RemoteUnavailableException, QueryNotFoundException, StorageUnavailableException, QueryMalformedException, SidecarExportException, StorageNotFoundException, QueryStoreInsertException, TableMalformedException, PaginationException, - QueryNotSupportedException, NotAllowedException, UserNotFoundException, ServiceException { + QueryNotSupportedException, NotAllowedException, UserNotFoundException, MetadataServiceException { log.debug("endpoint create subset in database, databaseId={}, data.statement={}, principal.name={}, page={}, " + "size={}, timestamp={}", databaseId, data.getStatement(), principal.getName(), page, size, timestamp); /* check */ @@ -337,7 +337,7 @@ public class SubsetEndpoint { @RequestParam(required = false) Long size) throws PaginationException, DatabaseNotFoundException, RemoteUnavailableException, NotAllowedException, QueryNotFoundException, DatabaseUnavailableException, TableMalformedException, QueryMalformedException, UserNotFoundException, - ServiceException { + MetadataServiceException { log.debug("endpoint re-execute query, databaseId={}, subsetId={}, principal.name={} page={}, size={}", databaseId, subsetId, principal != null ? principal.getName() : null, page, size); endpointValidator.validateDataParams(page, size); @@ -424,7 +424,7 @@ public class SubsetEndpoint { @NotNull @Valid @RequestBody QueryPersistDto data, @NotNull Principal principal) throws NotAllowedException, RemoteUnavailableException, DatabaseNotFoundException, QueryStorePersistException, - DatabaseUnavailableException, QueryNotFoundException, UserNotFoundException, ServiceException { + DatabaseUnavailableException, QueryNotFoundException, UserNotFoundException, MetadataServiceException { log.debug("endpoint persist query, databaseId={}, queryId={}, data.persist={}, principal.name={}", databaseId, queryId, data.getPersist(), principal.getName()); metadataServiceGateway.getAccess(databaseId, UserUtil.getId(principal)); diff --git a/dbrepo-data-service/rest-service/src/main/java/at/tuwien/endpoints/TableEndpoint.java b/dbrepo-data-service/rest-service/src/main/java/at/tuwien/endpoints/TableEndpoint.java index a0200609a6..7e95c7cfa9 100644 --- a/dbrepo-data-service/rest-service/src/main/java/at/tuwien/endpoints/TableEndpoint.java +++ b/dbrepo-data-service/rest-service/src/main/java/at/tuwien/endpoints/TableEndpoint.java @@ -61,7 +61,7 @@ public class TableEndpoint { } @PostMapping - @PreAuthorize("hasAuthority('admin')") + @PreAuthorize("hasAuthority('system')") @Operation(summary = "Create table", security = {@SecurityRequirement(name = "basicAuth")}, hidden = true) @@ -95,7 +95,7 @@ public class TableEndpoint { public ResponseEntity<TableDto> create(@NotNull @PathVariable("databaseId") Long databaseId, @Valid @RequestBody TableCreateDto data) throws DatabaseNotFoundException, RemoteUnavailableException, TableMalformedException, DatabaseUnavailableException, TableExistsException, - TableNotFoundException, QueryMalformedException, ServiceException { + TableNotFoundException, QueryMalformedException, MetadataServiceException { log.debug("endpoint create table, databaseId={}, data.name={}", databaseId, data.getName()); final PrivilegedDatabaseDto database = metadataServiceGateway.getDatabaseById(databaseId); try { @@ -108,7 +108,7 @@ public class TableEndpoint { } @DeleteMapping("/{tableId}") - @PreAuthorize("hasAuthority('admin')") + @PreAuthorize("hasAuthority('system')") @Operation(summary = "Delete table", security = {@SecurityRequirement(name = "basicAuth")}, hidden = true) @@ -137,7 +137,7 @@ public class TableEndpoint { public ResponseEntity<Void> delete(@NotBlank @PathVariable("databaseId") Long databaseId, @NotBlank @PathVariable("tableId") Long tableId) throws DatabaseUnavailableException, RemoteUnavailableException, TableNotFoundException, - QueryMalformedException, ServiceException { + QueryMalformedException, MetadataServiceException { log.debug("endpoint delete table, databaseId={}, tableId={}", databaseId, tableId); final PrivilegedTableDto table = metadataServiceGateway.getTableById(databaseId, tableId); try { @@ -191,7 +191,7 @@ public class TableEndpoint { @RequestParam(required = false) Long size, Principal principal) throws DatabaseUnavailableException, RemoteUnavailableException, TableNotFoundException, - TableMalformedException, PaginationException, QueryMalformedException, ServiceException, + TableMalformedException, PaginationException, QueryMalformedException, MetadataServiceException, NotAllowedException { log.debug("endpoint find table data, databaseId={}, tableId={}, timestamp={}, page={}, size={}", databaseId, tableId, timestamp, page, size); @@ -268,7 +268,7 @@ public class TableEndpoint { @NotNull Principal principal) throws DatabaseUnavailableException, RemoteUnavailableException, TableNotFoundException, TableMalformedException, QueryMalformedException, NotAllowedException, StorageUnavailableException, - StorageNotFoundException, ServiceException { + StorageNotFoundException, MetadataServiceException { log.debug("endpoint insert raw table data, databaseId={}, tableId={}", databaseId, tableId); final PrivilegedTableDto table = metadataServiceGateway.getTableById(databaseId, tableId); final DatabaseAccessDto access = metadataServiceGateway.getAccess(databaseId, UserUtil.getId(principal)); @@ -319,7 +319,7 @@ public class TableEndpoint { @Valid @RequestBody TupleUpdateDto data, @NotNull Principal principal) throws DatabaseUnavailableException, RemoteUnavailableException, TableNotFoundException, - TableMalformedException, QueryMalformedException, NotAllowedException, ServiceException { + TableMalformedException, QueryMalformedException, NotAllowedException, MetadataServiceException { log.debug("endpoint update raw table data, databaseId={}, tableId={}, data.keys={}", databaseId, tableId, data.getKeys()); final PrivilegedTableDto table = metadataServiceGateway.getTableById(databaseId, tableId); @@ -371,7 +371,7 @@ public class TableEndpoint { @Valid @RequestBody TupleDeleteDto data, @NotNull Principal principal) throws DatabaseUnavailableException, RemoteUnavailableException, TableNotFoundException, - TableMalformedException, QueryMalformedException, NotAllowedException, ServiceException { + TableMalformedException, QueryMalformedException, NotAllowedException, MetadataServiceException { log.debug("endpoint delete raw table data, databaseId={}, tableId={}, data.keys={}", databaseId, tableId, data.getKeys()); final PrivilegedTableDto table = metadataServiceGateway.getTableById(databaseId, tableId); @@ -424,7 +424,7 @@ public class TableEndpoint { @NotNull @PathVariable("tableId") Long tableId, @RequestParam(value = "size", required = false) Long size, Principal principal) throws DatabaseUnavailableException, - RemoteUnavailableException, TableNotFoundException, NotAllowedException, ServiceException, + RemoteUnavailableException, TableNotFoundException, NotAllowedException, MetadataServiceException, PaginationException { log.debug("endpoint find table history, databaseId={}, tableId={}", databaseId, tableId); if (size != null && size <= 0) { @@ -453,7 +453,7 @@ public class TableEndpoint { } @GetMapping - @PreAuthorize("hasAuthority('admin')") + @PreAuthorize("hasAuthority('system')") @Observed(name = "dbrepo_table_schema_list") @Operation(summary = "Find tables", hidden = true) @@ -491,7 +491,7 @@ public class TableEndpoint { }) public ResponseEntity<List<TableDto>> getSchema(@NotBlank @PathVariable("databaseId") Long databaseId) throws DatabaseUnavailableException, DatabaseNotFoundException, RemoteUnavailableException, - DatabaseMalformedException, TableNotFoundException, QueryMalformedException, ServiceException { + DatabaseMalformedException, TableNotFoundException, MetadataServiceException { log.debug("endpoint inspect table schemas, databaseId={}", databaseId); final PrivilegedDatabaseDto database = metadataServiceGateway.getDatabaseById(databaseId); try { @@ -540,7 +540,7 @@ public class TableEndpoint { Principal principal) throws DatabaseUnavailableException, RemoteUnavailableException, TableNotFoundException, NotAllowedException, StorageUnavailableException, QueryMalformedException, SidecarExportException, - StorageNotFoundException, ServiceException { + StorageNotFoundException, MetadataServiceException { log.debug("endpoint find table history, databaseId={}, tableId={}, timestamp={}", databaseId, tableId, timestamp); /* parameters */ if (timestamp == null) { @@ -606,7 +606,7 @@ public class TableEndpoint { @NotNull Principal principal) throws DatabaseUnavailableException, RemoteUnavailableException, TableNotFoundException, QueryMalformedException, StorageNotFoundException, SidecarImportException, NotAllowedException, - ServiceException { + MetadataServiceException { log.debug("endpoint insert table data, databaseId={}, tableId={}, data.location={}", databaseId, tableId, data.getLocation()); final PrivilegedTableDto table = metadataServiceGateway.getTableById(databaseId, tableId); final DatabaseAccessDto access = metadataServiceGateway.getAccess(databaseId, UserUtil.getId(principal)); @@ -659,7 +659,7 @@ public class TableEndpoint { public ResponseEntity<TableStatisticDto> statistic(@NotBlank @PathVariable("databaseId") Long databaseId, @NotBlank @PathVariable("tableId") Long tableId) throws DatabaseUnavailableException, RemoteUnavailableException, TableNotFoundException, - ServiceException, TableMalformedException, QueryMalformedException { + MetadataServiceException, TableMalformedException, QueryMalformedException { log.debug("endpoint generate table statistic, databaseId={}, tableId={}", databaseId, tableId); final PrivilegedTableDto table = metadataServiceGateway.getTableById(databaseId, tableId); try { diff --git a/dbrepo-data-service/rest-service/src/main/java/at/tuwien/endpoints/ViewEndpoint.java b/dbrepo-data-service/rest-service/src/main/java/at/tuwien/endpoints/ViewEndpoint.java index 3212b699a2..e9dbfd5c3a 100644 --- a/dbrepo-data-service/rest-service/src/main/java/at/tuwien/endpoints/ViewEndpoint.java +++ b/dbrepo-data-service/rest-service/src/main/java/at/tuwien/endpoints/ViewEndpoint.java @@ -54,7 +54,7 @@ public class ViewEndpoint { } @GetMapping - @PreAuthorize("hasAuthority('admin')") + @PreAuthorize("hasAuthority('system')") @Observed(name = "dbrepo_view_schema_list") @Operation(summary = "Find views", hidden = true) @@ -93,7 +93,7 @@ public class ViewEndpoint { public ResponseEntity<List<ViewDto>> getSchema(@NotBlank @PathVariable("databaseId") Long databaseId) throws DatabaseUnavailableException, DatabaseNotFoundException, RemoteUnavailableException, ViewMalformedException, ViewNotFoundException, DatabaseMalformedException, ViewSchemaException, - ServiceException { + MetadataServiceException { log.debug("endpoint inspect view schemas, databaseId={}", databaseId); final PrivilegedDatabaseDto database = metadataServiceGateway.getDatabaseById(databaseId); try { @@ -105,7 +105,7 @@ public class ViewEndpoint { } @PostMapping - @PreAuthorize("hasAuthority('admin')") + @PreAuthorize("hasAuthority('system')") @Operation(summary = "Create view", security = {@SecurityRequirement(name = "basicAuth"), @SecurityRequirement(name = "bearerAuth")}, hidden = true) @@ -138,7 +138,7 @@ public class ViewEndpoint { }) public ResponseEntity<ViewDto> create(@NotNull @PathVariable("databaseId") Long databaseId, @Valid @RequestBody ViewCreateDto data) throws DatabaseUnavailableException, - DatabaseNotFoundException, RemoteUnavailableException, ViewMalformedException, ServiceException { + DatabaseNotFoundException, RemoteUnavailableException, ViewMalformedException, MetadataServiceException { log.debug("endpoint create view, databaseId={}, data.name={}", databaseId, data.getName()); final PrivilegedDatabaseDto database = metadataServiceGateway.getDatabaseById(databaseId); try { @@ -151,7 +151,7 @@ public class ViewEndpoint { } @DeleteMapping("/{viewId}") - @PreAuthorize("hasAuthority('admin')") + @PreAuthorize("hasAuthority('system')") @Operation(summary = "Delete view", security = {@SecurityRequirement(name = "basicAuth"), @SecurityRequirement(name = "bearerAuth")}, hidden = true) @@ -182,7 +182,7 @@ public class ViewEndpoint { public ResponseEntity<Void> delete(@NotBlank @PathVariable("databaseId") Long databaseId, @NotBlank @PathVariable("viewId") Long viewId) throws DatabaseUnavailableException, RemoteUnavailableException, ViewNotFoundException, - ViewMalformedException, ServiceException { + ViewMalformedException, MetadataServiceException { log.debug("endpoint delete view, databaseId={}, viewId={}", databaseId, viewId); final PrivilegedViewDto view = metadataServiceGateway.getViewById(databaseId, viewId); try { @@ -243,7 +243,7 @@ public class ViewEndpoint { Principal principal) throws DatabaseUnavailableException, RemoteUnavailableException, ViewNotFoundException, QueryMalformedException, ViewMalformedException, PaginationException, NotAllowedException, - ServiceException { + MetadataServiceException { log.debug("endpoint get view data, databaseId={}, viewId={}, page={}, size={}, timestamp={}", databaseId, viewId, page, size, timestamp); endpointValidator.validateDataParams(page, size); diff --git a/dbrepo-data-service/rest-service/src/main/java/at/tuwien/handlers/ApiExceptionHandler.java b/dbrepo-data-service/rest-service/src/main/java/at/tuwien/handlers/ApiExceptionHandler.java deleted file mode 100644 index 18373423c5..0000000000 --- a/dbrepo-data-service/rest-service/src/main/java/at/tuwien/handlers/ApiExceptionHandler.java +++ /dev/null @@ -1,235 +0,0 @@ -package at.tuwien.handlers; - -import at.tuwien.api.error.ApiErrorDto; -import at.tuwien.exception.*; -import io.swagger.v3.oas.annotations.Hidden; -import lombok.extern.log4j.Log4j2; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.ControllerAdvice; -import org.springframework.web.bind.annotation.ExceptionHandler; -import org.springframework.web.bind.annotation.ResponseStatus; -import org.springframework.web.context.request.WebRequest; -import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; - -@Log4j2 -@ControllerAdvice -public class ApiExceptionHandler extends ResponseEntityExceptionHandler { - - @Hidden - @ResponseStatus(code = HttpStatus.NOT_FOUND) - @ExceptionHandler(ContainerNotFoundException.class) - public ResponseEntity<ApiErrorDto> handle(ContainerNotFoundException e) { - return generic_handle(e.getClass(), e.getLocalizedMessage()); - } - - @Hidden - @ResponseStatus(code = HttpStatus.EXPECTATION_FAILED) - @ExceptionHandler(DatabaseMalformedException.class) - public ResponseEntity<ApiErrorDto> handle(DatabaseMalformedException e) { - return generic_handle(e.getClass(), e.getLocalizedMessage()); - } - - @Hidden - @ResponseStatus(code = HttpStatus.NOT_FOUND) - @ExceptionHandler(DatabaseNotFoundException.class) - public ResponseEntity<ApiErrorDto> handle(DatabaseNotFoundException e) { - return generic_handle(e.getClass(), e.getLocalizedMessage()); - } - - @Hidden - @ResponseStatus(code = HttpStatus.SERVICE_UNAVAILABLE) - @ExceptionHandler(DatabaseUnavailableException.class) - public ResponseEntity<ApiErrorDto> handle(DatabaseUnavailableException e) { - return generic_handle(e.getClass(), e.getLocalizedMessage()); - } - - @Hidden - @ResponseStatus(code = HttpStatus.NOT_ACCEPTABLE) - @ExceptionHandler(FormatNotAvailableException.class) - public ResponseEntity<ApiErrorDto> handle(FormatNotAvailableException e) { - return generic_handle(e.getClass(), e.getLocalizedMessage()); - } - - @Hidden - @ResponseStatus(code = HttpStatus.FORBIDDEN) - @ExceptionHandler(NotAllowedException.class) - public ResponseEntity<ApiErrorDto> handle(NotAllowedException e) { - return generic_handle(e.getClass(), e.getLocalizedMessage()); - } - - @Hidden - @ResponseStatus(code = HttpStatus.BAD_REQUEST) - @ExceptionHandler(PaginationException.class) - public ResponseEntity<ApiErrorDto> handle(PaginationException e) { - return generic_handle(e.getClass(), e.getLocalizedMessage()); - } - - @Hidden - @ResponseStatus(code = HttpStatus.BAD_REQUEST) - @ExceptionHandler(QueryMalformedException.class) - public ResponseEntity<ApiErrorDto> handle(QueryMalformedException e) { - return generic_handle(e.getClass(), e.getLocalizedMessage()); - } - - @Hidden - @ResponseStatus(code = HttpStatus.NOT_FOUND) - @ExceptionHandler(QueryNotFoundException.class) - public ResponseEntity<ApiErrorDto> handle(QueryNotFoundException e) { - return generic_handle(e.getClass(), e.getLocalizedMessage()); - } - - @Hidden - @ResponseStatus(code = HttpStatus.NOT_IMPLEMENTED) - @ExceptionHandler(QueryNotSupportedException.class) - public ResponseEntity<ApiErrorDto> handle(QueryNotSupportedException e) { - return generic_handle(e.getClass(), e.getLocalizedMessage()); - } - - @Hidden - @ResponseStatus(code = HttpStatus.EXPECTATION_FAILED) - @ExceptionHandler(QueryStoreCreateException.class) - public ResponseEntity<ApiErrorDto> handle(QueryStoreCreateException e) { - return generic_handle(e.getClass(), e.getLocalizedMessage()); - } - - @Hidden - @ResponseStatus(code = HttpStatus.BAD_REQUEST) - @ExceptionHandler(QueryStoreGCException.class) - public ResponseEntity<ApiErrorDto> handle(QueryStoreGCException e) { - return generic_handle(e.getClass(), e.getLocalizedMessage()); - } - - @Hidden - @ResponseStatus(code = HttpStatus.EXPECTATION_FAILED) - @ExceptionHandler(QueryStoreInsertException.class) - public ResponseEntity<ApiErrorDto> handle(QueryStoreInsertException e) { - return generic_handle(e.getClass(), e.getLocalizedMessage()); - } - - @Hidden - @ResponseStatus(code = HttpStatus.EXPECTATION_FAILED) - @ExceptionHandler(QueryStorePersistException.class) - public ResponseEntity<ApiErrorDto> handle(QueryStorePersistException e) { - return generic_handle(e.getClass(), e.getLocalizedMessage()); - } - - @Hidden - @ResponseStatus(code = HttpStatus.SERVICE_UNAVAILABLE) - @ExceptionHandler(RemoteUnavailableException.class) - public ResponseEntity<ApiErrorDto> handle(RemoteUnavailableException e) { - return generic_handle(e.getClass(), e.getLocalizedMessage()); - } - - @Hidden - @ResponseStatus(code = HttpStatus.BAD_GATEWAY) - @ExceptionHandler(ServiceConnectionException.class) - public ResponseEntity<ApiErrorDto> handle(ServiceConnectionException e) { - return generic_handle(e.getClass(), e.getLocalizedMessage()); - } - - @Hidden - @ResponseStatus(code = HttpStatus.SERVICE_UNAVAILABLE) - @ExceptionHandler(ServiceException.class) - public ResponseEntity<ApiErrorDto> handle(ServiceException e) { - return generic_handle(e.getClass(), e.getLocalizedMessage()); - } - - @Hidden - @ResponseStatus(code = HttpStatus.SERVICE_UNAVAILABLE) - @ExceptionHandler(SidecarExportException.class) - public ResponseEntity<ApiErrorDto> handle(SidecarExportException e) { - return generic_handle(e.getClass(), e.getLocalizedMessage()); - } - - @Hidden - @ResponseStatus(code = HttpStatus.SERVICE_UNAVAILABLE) - @ExceptionHandler(SidecarImportException.class) - public ResponseEntity<ApiErrorDto> handle(SidecarImportException e) { - return generic_handle(e.getClass(), e.getLocalizedMessage()); - } - - @Hidden - @ResponseStatus(code = HttpStatus.NOT_FOUND) - @ExceptionHandler(StorageNotFoundException.class) - public ResponseEntity<ApiErrorDto> handle(StorageNotFoundException e) { - return generic_handle(e.getClass(), e.getLocalizedMessage()); - } - - @Hidden - @ResponseStatus(code = HttpStatus.SERVICE_UNAVAILABLE) - @ExceptionHandler(StorageUnavailableException.class) - public ResponseEntity<ApiErrorDto> handle(StorageUnavailableException e) { - return generic_handle(e.getClass(), e.getLocalizedMessage()); - } - - @Hidden - @ResponseStatus(code = HttpStatus.CONFLICT) - @ExceptionHandler(TableExistsException.class) - public ResponseEntity<ApiErrorDto> handle(TableExistsException e) { - return generic_handle(e.getClass(), e.getLocalizedMessage()); - } - - @Hidden - @ResponseStatus(code = HttpStatus.BAD_REQUEST) - @ExceptionHandler(TableMalformedException.class) - public ResponseEntity<ApiErrorDto> handle(TableMalformedException e) { - return generic_handle(e.getClass(), e.getLocalizedMessage()); - } - - @Hidden - @ResponseStatus(code = HttpStatus.NOT_FOUND) - @ExceptionHandler(TableNotFoundException.class) - public ResponseEntity<ApiErrorDto> handle(TableNotFoundException e) { - return generic_handle(e.getClass(), e.getLocalizedMessage()); - } - - @Hidden - @ResponseStatus(code = HttpStatus.CONFLICT) - @ExceptionHandler(TableSchemaException.class) - public ResponseEntity<ApiErrorDto> handle(TableSchemaException e) { - return generic_handle(e.getClass(), e.getLocalizedMessage()); - } - - @Hidden - @ResponseStatus(code = HttpStatus.NOT_FOUND) - @ExceptionHandler(UserNotFoundException.class) - public ResponseEntity<ApiErrorDto> handle(UserNotFoundException e) { - return generic_handle(e.getClass(), e.getLocalizedMessage()); - } - - @Hidden - @ResponseStatus(code = HttpStatus.BAD_REQUEST) - @ExceptionHandler(ViewMalformedException.class) - public ResponseEntity<ApiErrorDto> handle(ViewMalformedException e) { - return generic_handle(e.getClass(), e.getLocalizedMessage()); - } - - @Hidden - @ResponseStatus(code = HttpStatus.NOT_FOUND) - @ExceptionHandler(ViewNotFoundException.class) - public ResponseEntity<ApiErrorDto> handle(ViewNotFoundException e) { - return generic_handle(e.getClass(), e.getLocalizedMessage()); - } - - @Hidden - @ResponseStatus(code = HttpStatus.CONFLICT) - @ExceptionHandler(ViewSchemaException.class) - public ResponseEntity<ApiErrorDto> handle(ViewSchemaException e) { - return generic_handle(e.getClass(), e.getLocalizedMessage()); - } - - private ResponseEntity<ApiErrorDto> generic_handle(Class<?> exceptionClass, String message) { - final HttpHeaders headers = new HttpHeaders(); - headers.set("Content-Type", "application/problem+json"); - final ResponseStatus annotation = exceptionClass.getAnnotation(ResponseStatus.class); - final ApiErrorDto response = ApiErrorDto.builder() - .status(annotation.code()) - .message(message) - .code(annotation.reason()) - .build(); - return new ResponseEntity<>(response, headers, response.getStatus()); - } - -} diff --git a/dbrepo-data-service/rest-service/src/main/resources/application-local.yml b/dbrepo-data-service/rest-service/src/main/resources/application-local.yml index 5a6dc187e8..fc5445ce78 100644 --- a/dbrepo-data-service/rest-service/src/main/resources/application-local.yml +++ b/dbrepo-data-service/rest-service/src/main/resources/application-local.yml @@ -57,15 +57,14 @@ dbrepo: secretAccessKey: seaweedfsadmin importBucket: dbrepo-upload exportBucket: dbrepo-download - filePath: /tmp - admin: + system: username: admin password: admin jwt: public_key: MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqqnHQ2BWWW9vDNLRCcxD++xZg/16oqMo/c1l+lcFEjjAIJjJp/HqrPYU/U9GvquGE6PbVFtTzW1KcKawOW+FJNOA3CGo8Q1TFEfz43B8rZpKsFbJKvQGVv1Z4HaKPvLUm7iMm8Hv91cLduuoWx6Q3DPe2vg13GKKEZe7UFghF+0T9u8EKzA/XqQ0OiICmsmYPbwvf9N3bCKsB/Y10EYmZRb8IhCoV9mmO5TxgWgiuNeCTtNCv2ePYqL/U0WvyGFW0reasIK8eg3KrAUj8DpyOgPOVBn3lBGf+3KFSYi+0bwZbJZWqbC/Xlk20Go1YfeJPRIt7ImxD27R/lNjgDO/MwIDAQAB keycloak: - username: fda - password: fda + username: admin + password: admin client: dbrepo-client clientSecret: MUwRc7yfXSJwX8AdRMWaQC3Nep1VjwgG defaultDateFormatId: 1 diff --git a/dbrepo-data-service/rest-service/src/main/resources/application.yml b/dbrepo-data-service/rest-service/src/main/resources/application.yml index 93f31ab5ae..2043395f30 100644 --- a/dbrepo-data-service/rest-service/src/main/resources/application.yml +++ b/dbrepo-data-service/rest-service/src/main/resources/application.yml @@ -59,9 +59,9 @@ dbrepo: importBucket: "${S3_IMPORT_BUCKET:dbrepo-upload}" exportBucket: "${S3_EXPORT_BUCKET:dbrepo-download}" filePath: "${S3_FILE_PATH:/tmp}" - admin: - username: "${ADMIN_USERNAME:admin}" - password: "${ADMIN_PASSWORD:admin}" + system: + username: "${SYSTEM_USERNAME:admin}" + password: "${SYSTEM_PASSWORD:admin}" jwt: public_key: "${JWT_PUBKEY:MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqqnHQ2BWWW9vDNLRCcxD++xZg/16oqMo/c1l+lcFEjjAIJjJp/HqrPYU/U9GvquGE6PbVFtTzW1KcKawOW+FJNOA3CGo8Q1TFEfz43B8rZpKsFbJKvQGVv1Z4HaKPvLUm7iMm8Hv91cLduuoWx6Q3DPe2vg13GKKEZe7UFghF+0T9u8EKzA/XqQ0OiICmsmYPbwvf9N3bCKsB/Y10EYmZRb8IhCoV9mmO5TxgWgiuNeCTtNCv2ePYqL/U0WvyGFW0reasIK8eg3KrAUj8DpyOgPOVBn3lBGf+3KFSYi+0bwZbJZWqbC/Xlk20Go1YfeJPRIt7ImxD27R/lNjgDO/MwIDAQAB}" keycloak: diff --git a/dbrepo-data-service/rest-service/src/test/java/at/tuwien/config/MariaDbConfig.java b/dbrepo-data-service/rest-service/src/test/java/at/tuwien/config/MariaDbConfig.java index 54af799db3..8f73fa1b53 100644 --- a/dbrepo-data-service/rest-service/src/test/java/at/tuwien/config/MariaDbConfig.java +++ b/dbrepo-data-service/rest-service/src/test/java/at/tuwien/config/MariaDbConfig.java @@ -177,7 +177,8 @@ public class MariaDbConfig { if (set.next()) { final Matcher matcher = Pattern.compile("GRANT (.*) ON.*").matcher(set.getString(1)); if (matcher.find()) { - final List<String> privileges = Arrays.asList(matcher.group(1).split(","));; + final List<String> privileges = Arrays.asList(matcher.group(1).split(",")); + ; log.trace("found privileges: {}", privileges); return privileges; } @@ -224,7 +225,7 @@ public class MariaDbConfig { public static Long mockUserQueryInsert(PrivilegedDatabaseDto database, String query, String username, String password) throws SQLException { final String jdbc = "jdbc:mariadb://" + database.getContainer().getHost() + ":" + database.getContainer().getPort() + "/" + database.getInternalName(); - log.trace("connect to database {}", jdbc); + log.trace("connect to database: {}", jdbc); try (Connection connection = DriverManager.getConnection(jdbc, username, password)) { final String call = "{call store_query(?,?,?)}"; log.trace("prepare procedure '{}'", call); @@ -255,7 +256,7 @@ public class MariaDbConfig { public static void insertQueryStore(PrivilegedDatabaseDto database, QueryDto query, UUID userId) throws SQLException { final String jdbc = "jdbc:mariadb://" + database.getContainer().getHost() + ":" + database.getContainer().getPort() + "/" + database.getInternalName(); - log.trace("connect to database {}", jdbc); + log.trace("connect to database: {}", jdbc); try (Connection connection = DriverManager.getConnection(jdbc, database.getContainer().getUsername(), database.getContainer().getPassword())) { final PreparedStatement prepareStatement = connection.prepareStatement( "INSERT INTO qs_queries (created_by, query, query_normalized, is_persisted, query_hash, result_hash, result_number, created, executed) VALUES (?,?,?,?,?,?,?,?,?)"); @@ -332,13 +333,27 @@ public class MariaDbConfig { public static void execute(PrivilegedContainerDto container, String query) throws SQLException { final String jdbc = "jdbc:mariadb://" + container.getHost() + ":" + container.getPort(); - log.trace("connect to database {}", jdbc); + log.trace("connect to database: {}", jdbc); try (Connection connection = DriverManager.getConnection(jdbc, container.getUsername(), container.getPassword())) { final Statement statement = connection.createStatement(); statement.executeUpdate(query); } } + public static void dropQueryStore(PrivilegedDatabaseDto database) + throws SQLException { + final String jdbc = "jdbc:mariadb://" + database.getContainer().getHost() + ":" + database.getContainer().getPort() + "/" + database.getInternalName(); + log.trace("connect to database: {}", jdbc); + try (Connection connection = DriverManager.getConnection(jdbc, database.getContainer().getUsername(), database.getContainer().getPassword())) { + final Statement statement = connection.createStatement(); + statement.executeUpdate("DROP SEQUENCE IF EXISTS `qs_queries_seq`;"); + statement.executeUpdate("DROP TABLE IF EXISTS `qs_queries`;"); + statement.executeUpdate("DROP PROCEDURE IF EXISTS `hash_table`;"); + statement.executeUpdate("DROP PROCEDURE IF EXISTS `store_query`;"); + statement.executeUpdate("DROP PROCEDURE IF EXISTS `_store_query`;"); + } + } + public static Map<String, List<Object>> describeTableSchema(PrivilegedTableDto table, String username, String password) throws SQLException { final String jdbc = "jdbc:mariadb://" + table.getDatabase().getContainer().getHost() + ":" + table.getDatabase().getContainer().getPort() + "/" + table.getDatabase().getInternalName(); diff --git a/dbrepo-data-service/rest-service/src/test/java/at/tuwien/endpoint/AccessEndpointUnitTest.java b/dbrepo-data-service/rest-service/src/test/java/at/tuwien/endpoint/AccessEndpointUnitTest.java index 0a69255f10..1dc008dbeb 100644 --- a/dbrepo-data-service/rest-service/src/test/java/at/tuwien/endpoint/AccessEndpointUnitTest.java +++ b/dbrepo-data-service/rest-service/src/test/java/at/tuwien/endpoint/AccessEndpointUnitTest.java @@ -42,9 +42,9 @@ public class AccessEndpointUnitTest extends AbstractUnitTest { } @Test - @WithMockUser(username = USER_LOCAL_ADMIN_USERNAME, authorities = {"admin"}) + @WithMockUser(username = USER_LOCAL_ADMIN_USERNAME, authorities = {"system"}) public void create_succeeds() throws UserNotFoundException, NotAllowedException, DatabaseUnavailableException, - DatabaseNotFoundException, RemoteUnavailableException, DatabaseMalformedException, ServiceException { + DatabaseNotFoundException, RemoteUnavailableException, DatabaseMalformedException, MetadataServiceException { /* mock */ when(metadataServiceGateway.getDatabaseById(DATABASE_1_ID)) @@ -57,9 +57,9 @@ public class AccessEndpointUnitTest extends AbstractUnitTest { } @Test - @WithMockUser(username = USER_LOCAL_ADMIN_USERNAME, authorities = {"admin"}) + @WithMockUser(username = USER_LOCAL_ADMIN_USERNAME, authorities = {"system"}) public void create_alreadyAccess_fails() throws UserNotFoundException, DatabaseNotFoundException, - RemoteUnavailableException, ServiceException { + RemoteUnavailableException, MetadataServiceException { /* mock */ when(metadataServiceGateway.getDatabaseById(DATABASE_1_ID)) @@ -74,9 +74,9 @@ public class AccessEndpointUnitTest extends AbstractUnitTest { } @Test - @WithMockUser(username = USER_LOCAL_ADMIN_USERNAME, authorities = {"admin"}) + @WithMockUser(username = USER_LOCAL_ADMIN_USERNAME, authorities = {"system"}) public void create_databaseNotFound_fails() throws DatabaseNotFoundException, RemoteUnavailableException, - ServiceException { + MetadataServiceException { /* mock */ doThrow(DatabaseNotFoundException.class) @@ -90,9 +90,9 @@ public class AccessEndpointUnitTest extends AbstractUnitTest { } @Test - @WithMockUser(username = USER_LOCAL_ADMIN_USERNAME, authorities = {"admin"}) + @WithMockUser(username = USER_LOCAL_ADMIN_USERNAME, authorities = {"system"}) public void create_userNotFound_fails() throws UserNotFoundException, DatabaseNotFoundException, - RemoteUnavailableException, ServiceException { + RemoteUnavailableException, MetadataServiceException { /* mock */ when(metadataServiceGateway.getDatabaseById(DATABASE_1_ID)) @@ -118,9 +118,9 @@ public class AccessEndpointUnitTest extends AbstractUnitTest { } @Test - @WithMockUser(username = USER_LOCAL_ADMIN_USERNAME, authorities = {"admin"}) + @WithMockUser(username = USER_LOCAL_ADMIN_USERNAME, authorities = {"system"}) public void update_succeeds() throws DatabaseNotFoundException, RemoteUnavailableException, UserNotFoundException, - NotAllowedException, DatabaseUnavailableException, DatabaseMalformedException, ServiceException { + NotAllowedException, DatabaseUnavailableException, DatabaseMalformedException, MetadataServiceException { /* mock */ when(metadataServiceGateway.getDatabaseById(DATABASE_1_ID)) @@ -143,9 +143,9 @@ public class AccessEndpointUnitTest extends AbstractUnitTest { } @Test - @WithMockUser(username = USER_LOCAL_ADMIN_USERNAME, authorities = {"admin"}) + @WithMockUser(username = USER_LOCAL_ADMIN_USERNAME, authorities = {"system"}) public void update_databaseNotFound_fails() throws DatabaseNotFoundException, RemoteUnavailableException, - ServiceException { + MetadataServiceException { /* mock */ doThrow(DatabaseNotFoundException.class) @@ -159,9 +159,9 @@ public class AccessEndpointUnitTest extends AbstractUnitTest { } @Test - @WithMockUser(username = USER_LOCAL_ADMIN_USERNAME, authorities = {"admin"}) + @WithMockUser(username = USER_LOCAL_ADMIN_USERNAME, authorities = {"system"}) public void update_userNotFound_fails() throws DatabaseNotFoundException, RemoteUnavailableException, - UserNotFoundException, ServiceException { + UserNotFoundException, MetadataServiceException { /* mock */ when(metadataServiceGateway.getDatabaseById(DATABASE_1_ID)) @@ -177,9 +177,9 @@ public class AccessEndpointUnitTest extends AbstractUnitTest { } @Test - @WithMockUser(username = USER_LOCAL_ADMIN_USERNAME, authorities = {"admin"}) + @WithMockUser(username = USER_LOCAL_ADMIN_USERNAME, authorities = {"system"}) public void revoke_succeeds() throws UserNotFoundException, NotAllowedException, DatabaseUnavailableException, - DatabaseNotFoundException, RemoteUnavailableException, DatabaseMalformedException, ServiceException, + DatabaseNotFoundException, RemoteUnavailableException, DatabaseMalformedException, MetadataServiceException, SQLException { /* mock */ @@ -206,9 +206,9 @@ public class AccessEndpointUnitTest extends AbstractUnitTest { } @Test - @WithMockUser(username = USER_LOCAL_ADMIN_USERNAME, authorities = {"admin"}) + @WithMockUser(username = USER_LOCAL_ADMIN_USERNAME, authorities = {"system"}) public void revoke_databaseNotFound_fails() throws DatabaseNotFoundException, RemoteUnavailableException, - ServiceException { + MetadataServiceException { /* mock */ doThrow(DatabaseNotFoundException.class) @@ -222,9 +222,9 @@ public class AccessEndpointUnitTest extends AbstractUnitTest { } @Test - @WithMockUser(username = USER_LOCAL_ADMIN_USERNAME, authorities = {"admin"}) + @WithMockUser(username = USER_LOCAL_ADMIN_USERNAME, authorities = {"system"}) public void revoke_userNotFound_fails() throws DatabaseNotFoundException, RemoteUnavailableException, - UserNotFoundException, ServiceException { + UserNotFoundException, MetadataServiceException { /* mock */ when(metadataServiceGateway.getDatabaseById(DATABASE_1_ID)) diff --git a/dbrepo-data-service/rest-service/src/test/java/at/tuwien/endpoint/DatabaseEndpointUnitTest.java b/dbrepo-data-service/rest-service/src/test/java/at/tuwien/endpoint/DatabaseEndpointUnitTest.java index aa424e3aa6..c2b04d5aa9 100644 --- a/dbrepo-data-service/rest-service/src/test/java/at/tuwien/endpoint/DatabaseEndpointUnitTest.java +++ b/dbrepo-data-service/rest-service/src/test/java/at/tuwien/endpoint/DatabaseEndpointUnitTest.java @@ -52,9 +52,9 @@ public class DatabaseEndpointUnitTest extends AbstractUnitTest { } @Test - @WithMockUser(username = USER_LOCAL_ADMIN_USERNAME, authorities = {"admin"}) + @WithMockUser(username = USER_LOCAL_ADMIN_USERNAME, authorities = {"system"}) public void create_succeeds() throws DatabaseUnavailableException, RemoteUnavailableException, - QueryStoreCreateException, ContainerNotFoundException, DatabaseMalformedException, ServiceException { + QueryStoreCreateException, ContainerNotFoundException, DatabaseMalformedException, MetadataServiceException { /* test */ databaseEndpoint.create(DATABASE_1_CREATE_INTERNAL); @@ -63,7 +63,7 @@ public class DatabaseEndpointUnitTest extends AbstractUnitTest { @Test @WithMockUser(username = USER_LOCAL_ADMIN_USERNAME) public void create_noRole_fails() throws RemoteUnavailableException, ContainerNotFoundException, - SQLException, QueryStoreCreateException, DatabaseMalformedException, ServiceException { + SQLException, QueryStoreCreateException, DatabaseMalformedException, MetadataServiceException { /* mock */ when(metadataServiceGateway.getContainerById(CONTAINER_1_ID)) @@ -84,9 +84,9 @@ public class DatabaseEndpointUnitTest extends AbstractUnitTest { } @Test - @WithMockUser(username = USER_LOCAL_ADMIN_USERNAME, authorities = {"admin"}) + @WithMockUser(username = USER_LOCAL_ADMIN_USERNAME, authorities = {"system"}) public void create_containerNotFound_fails() throws RemoteUnavailableException, ContainerNotFoundException, - ServiceException { + MetadataServiceException { /* mock */ doThrow(ContainerNotFoundException.class) @@ -100,9 +100,9 @@ public class DatabaseEndpointUnitTest extends AbstractUnitTest { } @Test - @WithMockUser(username = USER_LOCAL_ADMIN_USERNAME, authorities = {"admin"}) + @WithMockUser(username = USER_LOCAL_ADMIN_USERNAME, authorities = {"system"}) public void create_queryStore_fails() throws RemoteUnavailableException, ContainerNotFoundException, SQLException, - DatabaseMalformedException, QueryStoreCreateException, ServiceException { + DatabaseMalformedException, QueryStoreCreateException, MetadataServiceException { /* mock */ doThrow(ContainerNotFoundException.class) @@ -121,9 +121,9 @@ public class DatabaseEndpointUnitTest extends AbstractUnitTest { } @Test - @WithMockUser(username = USER_LOCAL_ADMIN_USERNAME, authorities = {"admin"}) + @WithMockUser(username = USER_LOCAL_ADMIN_USERNAME, authorities = {"system"}) public void update_succeeds() throws DatabaseUnavailableException, RemoteUnavailableException, - DatabaseMalformedException, DatabaseNotFoundException, ServiceException { + DatabaseMalformedException, DatabaseNotFoundException, MetadataServiceException { /* test */ databaseEndpoint.update(DATABASE_1_ID, USER_1_UPDATE_PASSWORD_DTO); @@ -131,7 +131,7 @@ public class DatabaseEndpointUnitTest extends AbstractUnitTest { @Test @WithMockUser(username = USER_LOCAL_ADMIN_USERNAME) - public void update_noRole_fails() throws RemoteUnavailableException, DatabaseNotFoundException, ServiceException { + public void update_noRole_fails() throws RemoteUnavailableException, DatabaseNotFoundException, MetadataServiceException { /* mock */ when(metadataServiceGateway.getDatabaseById(DATABASE_1_ID)) @@ -144,9 +144,9 @@ public class DatabaseEndpointUnitTest extends AbstractUnitTest { } @Test - @WithMockUser(username = USER_LOCAL_ADMIN_USERNAME, authorities = {"admin"}) + @WithMockUser(username = USER_LOCAL_ADMIN_USERNAME, authorities = {"system"}) public void update_databaseNotFound_fails() throws RemoteUnavailableException, DatabaseNotFoundException, - ServiceException { + MetadataServiceException { /* mock */ doThrow(DatabaseNotFoundException.class) @@ -160,9 +160,9 @@ public class DatabaseEndpointUnitTest extends AbstractUnitTest { } @Test - @WithMockUser(username = USER_LOCAL_ADMIN_USERNAME, authorities = {"admin"}) + @WithMockUser(username = USER_LOCAL_ADMIN_USERNAME, authorities = {"system"}) public void update_password_fails() throws RemoteUnavailableException, DatabaseNotFoundException, SQLException, - DatabaseMalformedException, ServiceException { + DatabaseMalformedException, MetadataServiceException { /* mock */ when(metadataServiceGateway.getDatabaseById(DATABASE_1_ID)) diff --git a/dbrepo-data-service/rest-service/src/test/java/at/tuwien/endpoint/SubsetEndpointUnitTest.java b/dbrepo-data-service/rest-service/src/test/java/at/tuwien/endpoint/SubsetEndpointUnitTest.java index 6cdb0c6753..c45454b8f3 100644 --- a/dbrepo-data-service/rest-service/src/test/java/at/tuwien/endpoint/SubsetEndpointUnitTest.java +++ b/dbrepo-data-service/rest-service/src/test/java/at/tuwien/endpoint/SubsetEndpointUnitTest.java @@ -66,7 +66,7 @@ public class SubsetEndpointUnitTest extends AbstractUnitTest { @Test @WithAnonymousUser public void findAllById_succeeds() throws DatabaseUnavailableException, NotAllowedException, QueryNotFoundException, - DatabaseNotFoundException, RemoteUnavailableException, SQLException, ServiceException { + DatabaseNotFoundException, RemoteUnavailableException, SQLException, MetadataServiceException { /* test */ final List<QueryDto> response = generic_findAllById(DATABASE_3_ID, DATABASE_3_PRIVILEGED_DTO, null); @@ -98,7 +98,7 @@ public class SubsetEndpointUnitTest extends AbstractUnitTest { public void findById_succeeds() throws DatabaseNotFoundException, RemoteUnavailableException, UserNotFoundException, DatabaseUnavailableException, StorageUnavailableException, NotAllowedException, QueryMalformedException, QueryNotFoundException, SidecarExportException, FormatNotAvailableException, StorageNotFoundException, - SQLException, ServiceException { + SQLException, MetadataServiceException { /* mock */ when(metadataServiceGateway.getDatabaseById(DATABASE_3_ID)) @@ -113,7 +113,7 @@ public class SubsetEndpointUnitTest extends AbstractUnitTest { public void findById_acceptCsv_succeeds() throws DatabaseNotFoundException, RemoteUnavailableException, UserNotFoundException, DatabaseUnavailableException, StorageUnavailableException, NotAllowedException, QueryMalformedException, QueryNotFoundException, SidecarExportException, FormatNotAvailableException, - StorageNotFoundException, SQLException, ServiceException { + StorageNotFoundException, SQLException, MetadataServiceException { final ExportResourceDto mock = ExportResourceDto.builder() .filename("deadbeef") .resource(new InputStreamResource(InputStream.nullInputStream())) @@ -134,7 +134,7 @@ public class SubsetEndpointUnitTest extends AbstractUnitTest { public void findById_timestamp_succeeds() throws DatabaseNotFoundException, RemoteUnavailableException, UserNotFoundException, DatabaseUnavailableException, StorageUnavailableException, NotAllowedException, QueryMalformedException, QueryNotFoundException, SidecarExportException, FormatNotAvailableException, - StorageNotFoundException, SQLException, ServiceException { + StorageNotFoundException, SQLException, MetadataServiceException { final ExportResourceDto mock = ExportResourceDto.builder() .filename("deadbeef") .resource(new InputStreamResource(InputStream.nullInputStream())) @@ -152,7 +152,7 @@ public class SubsetEndpointUnitTest extends AbstractUnitTest { @Test @WithAnonymousUser - public void findById_fails() throws DatabaseNotFoundException, RemoteUnavailableException, ServiceException { + public void findById_fails() throws DatabaseNotFoundException, RemoteUnavailableException, MetadataServiceException { /* mock */ doThrow(DatabaseNotFoundException.class) @@ -171,7 +171,7 @@ public class SubsetEndpointUnitTest extends AbstractUnitTest { NotAllowedException, SidecarExportException, QueryNotSupportedException, PaginationException, StorageNotFoundException, DatabaseUnavailableException, StorageUnavailableException, QueryMalformedException, QueryNotFoundException, DatabaseNotFoundException, RemoteUnavailableException, - SQLException, ServiceException { + SQLException, MetadataServiceException { final ExecuteStatementDto request = ExecuteStatementDto.builder() .statement(QUERY_5_STATEMENT) .build(); @@ -207,7 +207,7 @@ public class SubsetEndpointUnitTest extends AbstractUnitTest { TableMalformedException, NotAllowedException, SidecarExportException, QueryNotSupportedException, PaginationException, StorageNotFoundException, DatabaseUnavailableException, StorageUnavailableException, QueryMalformedException, QueryNotFoundException, DatabaseNotFoundException, RemoteUnavailableException, - SQLException, ServiceException { + SQLException, MetadataServiceException { final ExecuteStatementDto request = ExecuteStatementDto.builder() .statement(QUERY_5_STATEMENT) .build(); @@ -227,7 +227,7 @@ public class SubsetEndpointUnitTest extends AbstractUnitTest { @Test @WithMockUser(username = USER_1_USERNAME, authorities = {"execute-query"}) public void create_databaseNotFound_fails() throws NotAllowedException, RemoteUnavailableException, - DatabaseNotFoundException, ServiceException { + DatabaseNotFoundException, MetadataServiceException { final ExecuteStatementDto request = ExecuteStatementDto.builder() .statement(QUERY_5_STATEMENT) .build(); @@ -260,7 +260,7 @@ public class SubsetEndpointUnitTest extends AbstractUnitTest { @Test @WithMockUser(username = USER_4_USERNAME, authorities = {"execute-query"}) - public void create_noAccess_fails() throws NotAllowedException, RemoteUnavailableException, ServiceException { + public void create_noAccess_fails() throws NotAllowedException, RemoteUnavailableException, MetadataServiceException { final ExecuteStatementDto request = ExecuteStatementDto.builder() .statement(QUERY_5_STATEMENT) .build(); @@ -279,7 +279,7 @@ public class SubsetEndpointUnitTest extends AbstractUnitTest { @Test public void getData_succeeds() throws DatabaseNotFoundException, RemoteUnavailableException, UserNotFoundException, NotAllowedException, SQLException, QueryNotFoundException, TableMalformedException, QueryMalformedException, - DatabaseUnavailableException, PaginationException, ServiceException { + DatabaseUnavailableException, PaginationException, MetadataServiceException { /* mock */ when(metadataServiceGateway.getDatabaseById(DATABASE_3_ID)) @@ -308,7 +308,7 @@ public class SubsetEndpointUnitTest extends AbstractUnitTest { @Test public void getData_onlyHead_succeeds() throws DatabaseNotFoundException, RemoteUnavailableException, UserNotFoundException, NotAllowedException, SQLException, QueryNotFoundException, TableMalformedException, QueryMalformedException, - DatabaseUnavailableException, PaginationException, ServiceException { + DatabaseUnavailableException, PaginationException, MetadataServiceException { /* mock */ when(metadataServiceGateway.getDatabaseById(DATABASE_3_ID)) @@ -332,7 +332,7 @@ public class SubsetEndpointUnitTest extends AbstractUnitTest { @WithMockUser(username = USER_1_USERNAME) public void getData_private_succeeds() throws DatabaseNotFoundException, RemoteUnavailableException, UserNotFoundException, DatabaseUnavailableException, NotAllowedException, TableMalformedException, - QueryMalformedException, QueryNotFoundException, PaginationException, SQLException, ServiceException { + QueryMalformedException, QueryNotFoundException, PaginationException, SQLException, MetadataServiceException { /* mock */ when(metadataServiceGateway.getDatabaseById(DATABASE_1_ID)) @@ -358,7 +358,7 @@ public class SubsetEndpointUnitTest extends AbstractUnitTest { @Test @WithAnonymousUser public void getData_privateAnonymous_succeeds() throws DatabaseNotFoundException, RemoteUnavailableException, - ServiceException { + MetadataServiceException { /* mock */ when(metadataServiceGateway.getDatabaseById(DATABASE_1_ID)) @@ -373,7 +373,7 @@ public class SubsetEndpointUnitTest extends AbstractUnitTest { @Test @WithMockUser(username = USER_1_USERNAME) public void getData_privateNoAccess_fails() throws DatabaseNotFoundException, RemoteUnavailableException, - NotAllowedException, ServiceException { + NotAllowedException, MetadataServiceException { /* mock */ when(metadataServiceGateway.getDatabaseById(DATABASE_1_ID)) @@ -392,7 +392,7 @@ public class SubsetEndpointUnitTest extends AbstractUnitTest { @WithMockUser(username = USER_1_USERNAME) public void getData_privateOnlyHead_succeeds() throws DatabaseNotFoundException, RemoteUnavailableException, UserNotFoundException, DatabaseUnavailableException, NotAllowedException, TableMalformedException, - QueryMalformedException, QueryNotFoundException, PaginationException, SQLException, ServiceException { + QueryMalformedException, QueryNotFoundException, PaginationException, SQLException, MetadataServiceException { /* mock */ when(metadataServiceGateway.getDatabaseById(DATABASE_1_ID)) @@ -416,7 +416,7 @@ public class SubsetEndpointUnitTest extends AbstractUnitTest { @WithMockUser(username = USER_3_USERNAME, authorities = {"persist-query"}) public void persist_succeeds() throws NotAllowedException, RemoteUnavailableException, DatabaseNotFoundException, QueryStorePersistException, SQLException, UserNotFoundException, QueryNotFoundException, - DatabaseUnavailableException, ServiceException { + DatabaseUnavailableException, MetadataServiceException { final QueryPersistDto request = QueryPersistDto.builder() .persist(true) .build(); @@ -451,7 +451,7 @@ public class SubsetEndpointUnitTest extends AbstractUnitTest { @Test @WithMockUser(username = USER_3_USERNAME, authorities = {"persist-query"}) - public void persist_noAccess_fails() throws NotAllowedException, RemoteUnavailableException, ServiceException { + public void persist_noAccess_fails() throws NotAllowedException, RemoteUnavailableException, MetadataServiceException { final QueryPersistDto request = QueryPersistDto.builder() .persist(true) .build(); @@ -470,7 +470,7 @@ public class SubsetEndpointUnitTest extends AbstractUnitTest { @Test @WithMockUser(username = USER_3_USERNAME, authorities = {"persist-query"}) public void persist_databaseNotFound_fails() throws NotAllowedException, RemoteUnavailableException, - DatabaseNotFoundException, ServiceException { + DatabaseNotFoundException, MetadataServiceException { final QueryPersistDto request = QueryPersistDto.builder() .persist(true) .build(); @@ -490,7 +490,7 @@ public class SubsetEndpointUnitTest extends AbstractUnitTest { protected List<QueryDto> generic_findAllById(Long databaseId, PrivilegedDatabaseDto database, Principal principal) throws DatabaseUnavailableException, NotAllowedException, QueryNotFoundException, DatabaseNotFoundException, - RemoteUnavailableException, SQLException, ServiceException { + RemoteUnavailableException, SQLException, MetadataServiceException { /* mock */ if (database != null) { @@ -514,7 +514,7 @@ public class SubsetEndpointUnitTest extends AbstractUnitTest { Principal principal) throws UserNotFoundException, DatabaseUnavailableException, StorageUnavailableException, NotAllowedException, QueryMalformedException, QueryNotFoundException, DatabaseNotFoundException, SidecarExportException, RemoteUnavailableException, FormatNotAvailableException, - StorageNotFoundException, SQLException, ServiceException { + StorageNotFoundException, SQLException, MetadataServiceException { /* mock */ when(queryService.findById(DATABASE_3_PRIVILEGED_DTO, subsetId)) diff --git a/dbrepo-data-service/rest-service/src/test/java/at/tuwien/endpoint/TableEndpointUnitTest.java b/dbrepo-data-service/rest-service/src/test/java/at/tuwien/endpoint/TableEndpointUnitTest.java index 022b043caa..29a33d1d9c 100644 --- a/dbrepo-data-service/rest-service/src/test/java/at/tuwien/endpoint/TableEndpointUnitTest.java +++ b/dbrepo-data-service/rest-service/src/test/java/at/tuwien/endpoint/TableEndpointUnitTest.java @@ -52,10 +52,10 @@ public class TableEndpointUnitTest extends AbstractUnitTest { } @Test - @WithMockUser(username = USER_LOCAL_ADMIN_USERNAME, authorities = {"admin"}) + @WithMockUser(username = USER_LOCAL_ADMIN_USERNAME, authorities = {"system"}) public void create_succeeds() throws DatabaseUnavailableException, TableMalformedException, DatabaseNotFoundException, TableExistsException, RemoteUnavailableException, SQLException, - TableNotFoundException, QueryMalformedException, ServiceException { + TableNotFoundException, QueryMalformedException, MetadataServiceException { /* mock */ when(metadataServiceGateway.getDatabaseById(DATABASE_1_ID)) @@ -79,9 +79,9 @@ public class TableEndpointUnitTest extends AbstractUnitTest { } @Test - @WithMockUser(username = USER_LOCAL_ADMIN_USERNAME, authorities = {"admin"}) + @WithMockUser(username = USER_LOCAL_ADMIN_USERNAME, authorities = {"system"}) public void create_databaseNotFound_fails() throws DatabaseNotFoundException, RemoteUnavailableException, - ServiceException { + MetadataServiceException { /* mock */ doThrow(DatabaseNotFoundException.class) @@ -95,9 +95,9 @@ public class TableEndpointUnitTest extends AbstractUnitTest { } @Test - @WithMockUser(username = USER_LOCAL_ADMIN_USERNAME, authorities = {"admin"}) + @WithMockUser(username = USER_LOCAL_ADMIN_USERNAME, authorities = {"system"}) public void delete_succeeds() throws RemoteUnavailableException, DatabaseUnavailableException, - TableNotFoundException, QueryMalformedException, SQLException, ServiceException { + TableNotFoundException, QueryMalformedException, SQLException, MetadataServiceException { /* mock */ when(metadataServiceGateway.getTableById(DATABASE_1_ID, TABLE_1_ID)) @@ -122,9 +122,9 @@ public class TableEndpointUnitTest extends AbstractUnitTest { } @Test - @WithMockUser(username = USER_LOCAL_ADMIN_USERNAME, authorities = {"admin"}) + @WithMockUser(username = USER_LOCAL_ADMIN_USERNAME, authorities = {"system"}) public void delete_tableNotFound_fails() throws RemoteUnavailableException, TableNotFoundException, - ServiceException { + MetadataServiceException { /* mock */ doThrow(TableNotFoundException.class) @@ -140,7 +140,7 @@ public class TableEndpointUnitTest extends AbstractUnitTest { @Test @WithAnonymousUser public void getData_succeeds() throws DatabaseUnavailableException, TableNotFoundException, TableMalformedException, - SQLException, QueryMalformedException, RemoteUnavailableException, PaginationException, ServiceException, + SQLException, QueryMalformedException, RemoteUnavailableException, PaginationException, MetadataServiceException, NotAllowedException { /* mock */ @@ -166,7 +166,7 @@ public class TableEndpointUnitTest extends AbstractUnitTest { @Test @WithAnonymousUser public void getData_tableNotFound_fails() throws TableNotFoundException, RemoteUnavailableException, - ServiceException { + MetadataServiceException { /* mock */ doThrow(TableNotFoundException.class) @@ -183,7 +183,7 @@ public class TableEndpointUnitTest extends AbstractUnitTest { @WithMockUser(username = USER_1_USERNAME, authorities = {"insert-table-data"}) public void createTuple_succeeds() throws DatabaseUnavailableException, TableNotFoundException, TableMalformedException, NotAllowedException, QueryMalformedException, RemoteUnavailableException, - SQLException, StorageUnavailableException, StorageNotFoundException, ServiceException { + SQLException, StorageUnavailableException, StorageNotFoundException, MetadataServiceException { final TupleDto request = TupleDto.builder() .data(new HashMap<>() {{ put(COLUMN_8_1_INTERNAL_NAME, 7L); @@ -227,7 +227,7 @@ public class TableEndpointUnitTest extends AbstractUnitTest { @Test @WithMockUser(username = USER_3_USERNAME, authorities = {"insert-table-data"}) public void createTuple_tableNotFound_fails() throws TableNotFoundException, RemoteUnavailableException, - ServiceException { + MetadataServiceException { final TupleDto request = TupleDto.builder() .data(new HashMap<>() {{ put(COLUMN_8_1_INTERNAL_NAME, 7L); @@ -249,7 +249,7 @@ public class TableEndpointUnitTest extends AbstractUnitTest { @Test @WithMockUser(username = USER_3_USERNAME, authorities = {"insert-table-data"}) public void createTuple_readAccess_fails() throws TableNotFoundException, RemoteUnavailableException, - NotAllowedException, ServiceException { + NotAllowedException, MetadataServiceException { final TupleDto request = TupleDto.builder() .data(new HashMap<>() {{ put(COLUMN_8_1_INTERNAL_NAME, 7L); @@ -273,7 +273,7 @@ public class TableEndpointUnitTest extends AbstractUnitTest { @WithMockUser(username = USER_1_USERNAME, authorities = {"insert-table-data"}) public void createTuple_writeOwnAccess_succeeds() throws TableNotFoundException, RemoteUnavailableException, NotAllowedException, DatabaseUnavailableException, TableMalformedException, QueryMalformedException, - StorageUnavailableException, StorageNotFoundException, ServiceException { + StorageUnavailableException, StorageNotFoundException, MetadataServiceException { final TupleDto request = TupleDto.builder() .data(new HashMap<>() {{ put(COLUMN_8_1_INTERNAL_NAME, 7L); @@ -294,7 +294,7 @@ public class TableEndpointUnitTest extends AbstractUnitTest { @Test @WithMockUser(username = USER_3_USERNAME, authorities = {"insert-table-data"}) public void createTuple_writeOwnAccessForeign_fails() throws TableNotFoundException, RemoteUnavailableException, - NotAllowedException, ServiceException { + NotAllowedException, MetadataServiceException { final TupleDto request = TupleDto.builder() .data(new HashMap<>() {{ put(COLUMN_8_1_INTERNAL_NAME, 7L); @@ -318,7 +318,7 @@ public class TableEndpointUnitTest extends AbstractUnitTest { @WithMockUser(username = USER_3_USERNAME, authorities = {"insert-table-data"}) public void createTuple_writeAllAccessForeign_succeeds() throws TableNotFoundException, RemoteUnavailableException, NotAllowedException, DatabaseUnavailableException, TableMalformedException, QueryMalformedException, - StorageUnavailableException, StorageNotFoundException, ServiceException { + StorageUnavailableException, StorageNotFoundException, MetadataServiceException { final TupleDto request = TupleDto.builder() .data(new HashMap<>() {{ put(COLUMN_8_1_INTERNAL_NAME, 7L); @@ -340,7 +340,7 @@ public class TableEndpointUnitTest extends AbstractUnitTest { @WithMockUser(username = USER_1_USERNAME, authorities = {"insert-table-data"}) public void updateTuple_succeeds() throws DatabaseUnavailableException, TableNotFoundException, TableMalformedException, NotAllowedException, QueryMalformedException, RemoteUnavailableException, - SQLException, ServiceException { + SQLException, MetadataServiceException { final TupleUpdateDto request = TupleUpdateDto.builder() .keys(new HashMap<>() {{ put(COLUMN_8_1_INTERNAL_NAME, 6L); @@ -390,7 +390,7 @@ public class TableEndpointUnitTest extends AbstractUnitTest { @Test @WithMockUser(username = USER_3_USERNAME, authorities = {"insert-table-data"}) public void updateTuple_tableNotFound_fails() throws TableNotFoundException, RemoteUnavailableException, - ServiceException { + MetadataServiceException { final TupleUpdateDto request = TupleUpdateDto.builder() .keys(new HashMap<>() {{ put(COLUMN_8_1_INTERNAL_NAME, 6L); @@ -415,7 +415,7 @@ public class TableEndpointUnitTest extends AbstractUnitTest { @Test @WithMockUser(username = USER_3_USERNAME, authorities = {"insert-table-data"}) public void updateTuple_readAccess_fails() throws TableNotFoundException, RemoteUnavailableException, - NotAllowedException, ServiceException { + NotAllowedException, MetadataServiceException { final TupleUpdateDto request = TupleUpdateDto.builder() .keys(new HashMap<>() {{ put(COLUMN_8_1_INTERNAL_NAME, 6L); @@ -442,7 +442,7 @@ public class TableEndpointUnitTest extends AbstractUnitTest { @WithMockUser(username = USER_1_USERNAME, authorities = {"insert-table-data"}) public void updateTuple_writeOwnAccess_succeeds() throws DatabaseUnavailableException, TableNotFoundException, TableMalformedException, NotAllowedException, QueryMalformedException, RemoteUnavailableException, - SQLException, ServiceException { + SQLException, MetadataServiceException { final TupleUpdateDto request = TupleUpdateDto.builder() .keys(new HashMap<>() {{ put(COLUMN_8_1_INTERNAL_NAME, 6L); @@ -473,7 +473,7 @@ public class TableEndpointUnitTest extends AbstractUnitTest { @Test @WithMockUser(username = USER_3_USERNAME, authorities = {"insert-table-data"}) public void updateTuple_writeOwnAccessForeign_fails() throws TableNotFoundException, RemoteUnavailableException, - NotAllowedException, ServiceException { + NotAllowedException, MetadataServiceException { final TupleUpdateDto request = TupleUpdateDto.builder() .keys(new HashMap<>() {{ put(COLUMN_8_1_INTERNAL_NAME, 6L); @@ -500,7 +500,7 @@ public class TableEndpointUnitTest extends AbstractUnitTest { @WithMockUser(username = USER_3_USERNAME, authorities = {"insert-table-data"}) public void updateTuple_writeAllAccessForeign_succeeds() throws TableNotFoundException, RemoteUnavailableException, NotAllowedException, DatabaseUnavailableException, TableMalformedException, QueryMalformedException, - SQLException, ServiceException { + SQLException, MetadataServiceException { final TupleUpdateDto request = TupleUpdateDto.builder() .keys(new HashMap<>() {{ put(COLUMN_8_1_INTERNAL_NAME, 6L); @@ -532,7 +532,7 @@ public class TableEndpointUnitTest extends AbstractUnitTest { @WithMockUser(username = USER_1_USERNAME, authorities = {"delete-table-data"}) public void deleteTuple_succeeds() throws DatabaseUnavailableException, TableNotFoundException, TableMalformedException, NotAllowedException, QueryMalformedException, RemoteUnavailableException, - SQLException, ServiceException { + SQLException, MetadataServiceException { final TupleDeleteDto request = TupleDeleteDto.builder() .keys(new HashMap<>() {{ put(COLUMN_8_1_INTERNAL_NAME, 6L); @@ -574,7 +574,7 @@ public class TableEndpointUnitTest extends AbstractUnitTest { @Test @WithMockUser(username = USER_3_USERNAME, authorities = {"delete-table-data"}) public void deleteTuple_tableNotFound_fails() throws TableNotFoundException, RemoteUnavailableException, - ServiceException { + MetadataServiceException { final TupleDeleteDto request = TupleDeleteDto.builder() .keys(new HashMap<>() {{ put(COLUMN_8_1_INTERNAL_NAME, 6L); @@ -595,7 +595,7 @@ public class TableEndpointUnitTest extends AbstractUnitTest { @Test @WithMockUser(username = USER_1_USERNAME, authorities = {"delete-table-data"}) public void deleteTuple_readAccess_fails() throws TableNotFoundException, RemoteUnavailableException, - NotAllowedException, ServiceException { + NotAllowedException, MetadataServiceException { final TupleDeleteDto request = TupleDeleteDto.builder() .keys(new HashMap<>() {{ put(COLUMN_8_1_INTERNAL_NAME, 6L); @@ -618,7 +618,7 @@ public class TableEndpointUnitTest extends AbstractUnitTest { @WithMockUser(username = USER_1_USERNAME, authorities = {"delete-table-data"}) public void deleteTuple_writeOwnAccess_succeeds() throws TableNotFoundException, RemoteUnavailableException, NotAllowedException, TableMalformedException, SQLException, QueryMalformedException, - DatabaseUnavailableException, ServiceException { + DatabaseUnavailableException, MetadataServiceException { final TupleDeleteDto request = TupleDeleteDto.builder() .keys(new HashMap<>() {{ put(COLUMN_8_1_INTERNAL_NAME, 6L); @@ -645,7 +645,7 @@ public class TableEndpointUnitTest extends AbstractUnitTest { @Test @WithMockUser(username = USER_3_USERNAME, authorities = {"delete-table-data"}) public void deleteTuple_writeOwnAccessForeign_fails() throws TableNotFoundException, RemoteUnavailableException, - NotAllowedException, ServiceException { + NotAllowedException, MetadataServiceException { final TupleDeleteDto request = TupleDeleteDto.builder() .keys(new HashMap<>() {{ put(COLUMN_8_1_INTERNAL_NAME, 6L); @@ -668,7 +668,7 @@ public class TableEndpointUnitTest extends AbstractUnitTest { @WithMockUser(username = USER_3_USERNAME, authorities = {"delete-table-data"}) public void deleteTuple_writeAllAccessForeign_succeeds() throws TableNotFoundException, RemoteUnavailableException, NotAllowedException, DatabaseUnavailableException, TableMalformedException, QueryMalformedException, - SQLException, ServiceException { + SQLException, MetadataServiceException { final TupleDeleteDto request = TupleDeleteDto.builder() .keys(new HashMap<>() {{ put(COLUMN_8_1_INTERNAL_NAME, 6L); @@ -695,7 +695,7 @@ public class TableEndpointUnitTest extends AbstractUnitTest { @Test @WithAnonymousUser public void getHistory_succeeds() throws DatabaseUnavailableException, TableNotFoundException, - RemoteUnavailableException, SQLException, NotAllowedException, ServiceException, PaginationException { + RemoteUnavailableException, SQLException, NotAllowedException, MetadataServiceException, PaginationException { /* mock */ when(metadataServiceGateway.getTableById(DATABASE_3_ID, TABLE_8_ID)) @@ -711,7 +711,7 @@ public class TableEndpointUnitTest extends AbstractUnitTest { @Test @WithAnonymousUser public void getHistory_privateNoRole_fails() throws TableNotFoundException, RemoteUnavailableException, - ServiceException { + MetadataServiceException { /* mock */ when(metadataServiceGateway.getTableById(DATABASE_1_ID, TABLE_1_ID)) @@ -726,7 +726,7 @@ public class TableEndpointUnitTest extends AbstractUnitTest { @Test @WithMockUser(username = USER_4_USERNAME) public void getHistory_privateNoAccess_fails() throws NotAllowedException, RemoteUnavailableException, - TableNotFoundException, ServiceException { + TableNotFoundException, MetadataServiceException { /* mock */ when(metadataServiceGateway.getTableById(DATABASE_1_ID, TABLE_1_ID)) @@ -744,7 +744,7 @@ public class TableEndpointUnitTest extends AbstractUnitTest { @Test @WithAnonymousUser public void getHistory_tableNotFound_fails() throws TableNotFoundException, RemoteUnavailableException, - ServiceException { + MetadataServiceException { /* mock */ doThrow(TableNotFoundException.class) @@ -761,7 +761,7 @@ public class TableEndpointUnitTest extends AbstractUnitTest { @WithAnonymousUser public void exportData_succeeds() throws DatabaseUnavailableException, TableNotFoundException, NotAllowedException, StorageUnavailableException, QueryMalformedException, SidecarExportException, RemoteUnavailableException, - StorageNotFoundException, SQLException, ServiceException { + StorageNotFoundException, SQLException, MetadataServiceException { final ExportResourceDto mock = ExportResourceDto.builder() .filename("deadbeef") .resource(new InputStreamResource(InputStream.nullInputStream())) @@ -782,7 +782,7 @@ public class TableEndpointUnitTest extends AbstractUnitTest { @Test @WithMockUser(username = USER_4_USERNAME) public void exportData_privateNoAccess_fails() throws TableNotFoundException, NotAllowedException, - RemoteUnavailableException, ServiceException { + RemoteUnavailableException, MetadataServiceException { /* mock */ when(metadataServiceGateway.getTableById(DATABASE_1_ID, TABLE_1_ID)) @@ -802,7 +802,7 @@ public class TableEndpointUnitTest extends AbstractUnitTest { @WithMockUser(username = USER_1_USERNAME, authorities = {"insert-table-data"}) public void importData_succeeds() throws DatabaseUnavailableException, TableNotFoundException, SidecarImportException, NotAllowedException, QueryMalformedException, RemoteUnavailableException, - StorageNotFoundException, SQLException, ServiceException { + StorageNotFoundException, SQLException, MetadataServiceException { final ImportCsvDto request = ImportCsvDto.builder() .skipLines(1L) .lineTermination("\\n") @@ -844,7 +844,7 @@ public class TableEndpointUnitTest extends AbstractUnitTest { @Test @WithMockUser(username = USER_3_USERNAME, authorities = {"insert-table-data"}) public void importData_tableNotFound_fails() throws TableNotFoundException, RemoteUnavailableException, - ServiceException { + MetadataServiceException { final ImportCsvDto request = ImportCsvDto.builder() .skipLines(1L) .lineTermination("\\n") @@ -865,7 +865,7 @@ public class TableEndpointUnitTest extends AbstractUnitTest { @Test @WithMockUser(username = USER_3_USERNAME, authorities = {"insert-table-data"}) public void importData_readAccess_fails() throws TableNotFoundException, RemoteUnavailableException, - NotAllowedException, ServiceException { + NotAllowedException, MetadataServiceException { final ImportCsvDto request = ImportCsvDto.builder() .skipLines(1L) .lineTermination("\\n") @@ -888,7 +888,7 @@ public class TableEndpointUnitTest extends AbstractUnitTest { @WithMockUser(username = USER_1_USERNAME, authorities = {"insert-table-data"}) public void importData_writeOwnAccess_succeeds() throws TableNotFoundException, RemoteUnavailableException, NotAllowedException, DatabaseUnavailableException, SidecarImportException, QueryMalformedException, - StorageNotFoundException, ServiceException { + StorageNotFoundException, MetadataServiceException { final ImportCsvDto request = ImportCsvDto.builder() .skipLines(1L) .lineTermination("\\n") @@ -908,7 +908,7 @@ public class TableEndpointUnitTest extends AbstractUnitTest { @Test @WithMockUser(username = USER_3_USERNAME, authorities = {"insert-table-data"}) public void importData_writeOwnAccessForeign_fails() throws TableNotFoundException, RemoteUnavailableException, - NotAllowedException, ServiceException { + NotAllowedException, MetadataServiceException { final ImportCsvDto request = ImportCsvDto.builder() .skipLines(1L) .lineTermination("\\n") @@ -931,7 +931,7 @@ public class TableEndpointUnitTest extends AbstractUnitTest { @WithMockUser(username = USER_3_USERNAME, authorities = {"insert-table-data"}) public void importData_writeAllAccessForeign_succeeds() throws TableNotFoundException, RemoteUnavailableException, NotAllowedException, DatabaseUnavailableException, SidecarImportException, QueryMalformedException, - StorageNotFoundException, ServiceException { + StorageNotFoundException, MetadataServiceException { final ImportCsvDto request = ImportCsvDto.builder() .skipLines(1L) .lineTermination("\\n") diff --git a/dbrepo-data-service/rest-service/src/test/java/at/tuwien/endpoint/ViewEndpointUnitTest.java b/dbrepo-data-service/rest-service/src/test/java/at/tuwien/endpoint/ViewEndpointUnitTest.java index 9f7ad136a8..b9b814378e 100644 --- a/dbrepo-data-service/rest-service/src/test/java/at/tuwien/endpoint/ViewEndpointUnitTest.java +++ b/dbrepo-data-service/rest-service/src/test/java/at/tuwien/endpoint/ViewEndpointUnitTest.java @@ -49,9 +49,9 @@ public class ViewEndpointUnitTest extends AbstractUnitTest { } @Test - @WithMockUser(username = USER_LOCAL_ADMIN_USERNAME, authorities = {"admin"}) + @WithMockUser(username = USER_LOCAL_ADMIN_USERNAME, authorities = {"system"}) public void create_succeeds() throws DatabaseNotFoundException, RemoteUnavailableException, ViewMalformedException, - SQLException, DatabaseUnavailableException, ServiceException { + SQLException, DatabaseUnavailableException, MetadataServiceException { /* mock */ when(metadataServiceGateway.getDatabaseById(DATABASE_1_ID)) @@ -67,7 +67,7 @@ public class ViewEndpointUnitTest extends AbstractUnitTest { @Test @WithMockUser(username = USER_LOCAL_ADMIN_USERNAME) public void create_noRole_fails() throws DatabaseNotFoundException, RemoteUnavailableException, ViewMalformedException, - SQLException, ServiceException { + SQLException, MetadataServiceException { /* mock */ when(metadataServiceGateway.getDatabaseById(DATABASE_1_ID)) @@ -82,9 +82,9 @@ public class ViewEndpointUnitTest extends AbstractUnitTest { } @Test - @WithMockUser(username = USER_LOCAL_ADMIN_USERNAME, authorities = {"admin"}) + @WithMockUser(username = USER_LOCAL_ADMIN_USERNAME, authorities = {"system"}) public void create_databaseNotFound_fails() throws DatabaseNotFoundException, RemoteUnavailableException, - ServiceException { + MetadataServiceException { /* mock */ doThrow(DatabaseNotFoundException.class) @@ -98,9 +98,9 @@ public class ViewEndpointUnitTest extends AbstractUnitTest { } @Test - @WithMockUser(username = USER_LOCAL_ADMIN_USERNAME, authorities = {"admin"}) + @WithMockUser(username = USER_LOCAL_ADMIN_USERNAME, authorities = {"system"}) public void delete_succeeds() throws DatabaseNotFoundException, RemoteUnavailableException, ViewMalformedException, - SQLException, DatabaseUnavailableException, ViewNotFoundException, ServiceException { + SQLException, DatabaseUnavailableException, ViewNotFoundException, MetadataServiceException { /* mock */ when(metadataServiceGateway.getDatabaseById(DATABASE_1_ID)) @@ -117,7 +117,7 @@ public class ViewEndpointUnitTest extends AbstractUnitTest { @Test @WithMockUser(username = USER_LOCAL_ADMIN_USERNAME) public void delete_noRole_fails() throws DatabaseNotFoundException, RemoteUnavailableException, ViewMalformedException, - SQLException, ServiceException { + SQLException, MetadataServiceException { /* mock */ when(metadataServiceGateway.getDatabaseById(DATABASE_1_ID)) @@ -133,9 +133,9 @@ public class ViewEndpointUnitTest extends AbstractUnitTest { } @Test - @WithMockUser(username = USER_LOCAL_ADMIN_USERNAME, authorities = {"admin"}) + @WithMockUser(username = USER_LOCAL_ADMIN_USERNAME, authorities = {"system"}) public void delete_databaseNotFound_fails() throws RemoteUnavailableException, ViewNotFoundException, - ServiceException { + MetadataServiceException { /* mock */ doThrow(ViewNotFoundException.class) @@ -152,7 +152,7 @@ public class ViewEndpointUnitTest extends AbstractUnitTest { @WithMockUser(username = USER_1_USERNAME, authorities = {"view-database-view-data"}) public void getData_succeeds() throws RemoteUnavailableException, ViewNotFoundException, ViewMalformedException, SQLException, DatabaseUnavailableException, QueryMalformedException, PaginationException, - NotAllowedException, ServiceException { + NotAllowedException, MetadataServiceException { /* mock */ when(metadataServiceGateway.getViewById(DATABASE_1_ID, VIEW_1_ID)) @@ -182,7 +182,7 @@ public class ViewEndpointUnitTest extends AbstractUnitTest { @WithMockUser(username = USER_1_USERNAME, authorities = {"view-database-view-data"}) public void getData_onlyHead_succeeds() throws RemoteUnavailableException, ViewNotFoundException, ViewMalformedException, SQLException, DatabaseUnavailableException, QueryMalformedException, - PaginationException, NotAllowedException, ServiceException { + PaginationException, NotAllowedException, MetadataServiceException { /* mock */ when(metadataServiceGateway.getViewById(DATABASE_1_ID, VIEW_1_ID)) @@ -209,7 +209,7 @@ public class ViewEndpointUnitTest extends AbstractUnitTest { @Test @WithMockUser(username = USER_1_USERNAME, authorities = {"view-database-view-data"}) public void getData_viewNotFound_fails() throws RemoteUnavailableException, ViewNotFoundException, - ServiceException { + MetadataServiceException { /* mock */ doThrow(ViewNotFoundException.class) @@ -225,7 +225,7 @@ public class ViewEndpointUnitTest extends AbstractUnitTest { @Test @WithMockUser(username = USER_3_USERNAME, authorities = {"view-database-view-data"}) public void getData_privateNoAccess_fails() throws RemoteUnavailableException, ViewNotFoundException, - NotAllowedException, ServiceException { + NotAllowedException, MetadataServiceException { /* mock */ when(metadataServiceGateway.getViewById(DATABASE_1_ID, VIEW_3_ID)) diff --git a/dbrepo-data-service/rest-service/src/test/java/at/tuwien/gateway/DataDatabaseGatewayUnitTest.java b/dbrepo-data-service/rest-service/src/test/java/at/tuwien/gateway/DataDatabaseGatewayUnitTest.java index 30a4d31cfb..b00f871d5c 100644 --- a/dbrepo-data-service/rest-service/src/test/java/at/tuwien/gateway/DataDatabaseGatewayUnitTest.java +++ b/dbrepo-data-service/rest-service/src/test/java/at/tuwien/gateway/DataDatabaseGatewayUnitTest.java @@ -41,7 +41,8 @@ public class DataDatabaseGatewayUnitTest extends AbstractUnitTest { } @Test - public void importFile_succeeds() throws RemoteUnavailableException, StorageNotFoundException, ServiceException { + public void importFile_succeeds() throws RemoteUnavailableException, StorageNotFoundException, + SidecarImportException { /* mock */ when(restTemplate.exchange(anyString(), eq(HttpMethod.POST), eq(HttpEntity.EMPTY), eq(Void.class))) @@ -75,7 +76,7 @@ public class DataDatabaseGatewayUnitTest extends AbstractUnitTest { .build()); /* test */ - assertThrows(ServiceException.class, () -> { + assertThrows(SidecarImportException.class, () -> { dataDatabaseSidecarGateway.importFile(CONTAINER_1_HOST, CONTAINER_1_PORT, "filename"); }); } @@ -95,7 +96,8 @@ public class DataDatabaseGatewayUnitTest extends AbstractUnitTest { } @Test - public void exportFile_succeeds() throws RemoteUnavailableException, StorageNotFoundException, ServiceException { + public void exportFile_succeeds() throws RemoteUnavailableException, StorageNotFoundException, + SidecarExportException { /* mock */ when(restTemplate.exchange(anyString(), eq(HttpMethod.POST), eq(HttpEntity.EMPTY), eq(Void.class))) @@ -129,7 +131,7 @@ public class DataDatabaseGatewayUnitTest extends AbstractUnitTest { .build()); /* test */ - assertThrows(ServiceException.class, () -> { + assertThrows(SidecarExportException.class, () -> { dataDatabaseSidecarGateway.exportFile(CONTAINER_1_HOST, CONTAINER_1_PORT, "filename"); }); } diff --git a/dbrepo-data-service/rest-service/src/test/java/at/tuwien/gateway/InterceptorUnitTest.java b/dbrepo-data-service/rest-service/src/test/java/at/tuwien/gateway/InterceptorUnitTest.java index 0fd20a8025..b87793a8bc 100644 --- a/dbrepo-data-service/rest-service/src/test/java/at/tuwien/gateway/InterceptorUnitTest.java +++ b/dbrepo-data-service/rest-service/src/test/java/at/tuwien/gateway/InterceptorUnitTest.java @@ -1,9 +1,6 @@ package at.tuwien.gateway; import at.tuwien.api.keycloak.TokenDto; -import at.tuwien.exception.RemoteUnavailableException; -import at.tuwien.exception.ServiceException; -import at.tuwien.exception.StorageNotFoundException; import at.tuwien.test.AbstractUnitTest; import lombok.extern.log4j.Log4j2; import org.junit.jupiter.api.BeforeEach; @@ -15,16 +12,11 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.HttpEntity; import org.springframework.http.HttpMethod; -import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.test.context.junit.jupiter.SpringExtension; -import org.springframework.web.client.HttpClientErrorException; -import org.springframework.web.client.HttpServerErrorException; import org.springframework.web.client.RestTemplate; -import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.*; -import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.when; @Log4j2 @@ -36,9 +28,6 @@ public class InterceptorUnitTest extends AbstractUnitTest { @Qualifier("keycloakRestTemplate") private RestTemplate restTemplate; - @Autowired - private DataDatabaseSidecarGateway dataDatabaseSidecarGateway; - @BeforeEach public void beforeEach() { genesis(); diff --git a/dbrepo-data-service/rest-service/src/test/java/at/tuwien/gateway/KeycloakSidecarGatewayUnitTest.java b/dbrepo-data-service/rest-service/src/test/java/at/tuwien/gateway/KeycloakSidecarGatewayUnitTest.java deleted file mode 100644 index 2a02e03466..0000000000 --- a/dbrepo-data-service/rest-service/src/test/java/at/tuwien/gateway/KeycloakSidecarGatewayUnitTest.java +++ /dev/null @@ -1,101 +0,0 @@ -package at.tuwien.gateway; - -import at.tuwien.api.keycloak.TokenDto; -import at.tuwien.exception.RemoteUnavailableException; -import at.tuwien.exception.ServiceConnectionException; -import at.tuwien.exception.ServiceException; -import at.tuwien.test.AbstractUnitTest; -import lombok.extern.log4j.Log4j2; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.http.HttpEntity; -import org.springframework.http.HttpMethod; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.test.context.junit.jupiter.SpringExtension; -import org.springframework.web.client.HttpClientErrorException; -import org.springframework.web.client.HttpServerErrorException; -import org.springframework.web.client.RestTemplate; - -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.ArgumentMatchers.*; -import static org.mockito.Mockito.doThrow; -import static org.mockito.Mockito.when; - -@Log4j2 -@SpringBootTest -@ExtendWith(SpringExtension.class) -public class KeycloakSidecarGatewayUnitTest extends AbstractUnitTest { - - @MockBean - @Qualifier("restTemplate") - private RestTemplate restTemplate; - - @Autowired - private KeycloakGateway keycloakGateway; - - @BeforeEach - public void beforeEach() { - genesis(); - } - - @Test - public void obtainUserToken_succeeds() throws ServiceException, RemoteUnavailableException { - - /* mock */ - when(restTemplate.exchange(anyString(), eq(HttpMethod.POST), any(HttpEntity.class), eq(TokenDto.class))) - .thenReturn(ResponseEntity.ok() - .build()); - - /* test */ - final TokenDto response = keycloakGateway.obtainUserToken(USER_1_USERNAME, USER_1_PASSWORD); - } - - @Test - public void obtainUserToken_unavailable_fails() { - - /* mock */ - doThrow(HttpServerErrorException.class) - .when(restTemplate) - .exchange(anyString(), eq(HttpMethod.POST), any(HttpEntity.class), eq(TokenDto.class)); - - /* test */ - assertThrows(RemoteUnavailableException.class, () -> { - keycloakGateway.obtainUserToken(USER_1_USERNAME, USER_1_PASSWORD); - }); - } - - @Test - public void obtainUserToken_badRequest_fails() { - - /* mock */ - doThrow(HttpClientErrorException.BadRequest.class) - .when(restTemplate) - .exchange(anyString(), eq(HttpMethod.POST), any(HttpEntity.class), eq(TokenDto.class)); - - /* test */ - assertThrows(ServiceException.class, () -> { - keycloakGateway.obtainUserToken(USER_1_USERNAME, USER_1_PASSWORD); - }); - } - - @Test - public void obtainUserToken_statusCode_fails() { - - /* mock */ - when(restTemplate.exchange(anyString(), eq(HttpMethod.POST), any(HttpEntity.class), eq(TokenDto.class))) - .thenReturn(ResponseEntity.status(HttpStatus.NO_CONTENT) - .build()); - - /* test */ - assertThrows(ServiceException.class, () -> { - keycloakGateway.obtainUserToken(USER_1_USERNAME, USER_1_PASSWORD); - }); - } - -} diff --git a/dbrepo-data-service/rest-service/src/test/java/at/tuwien/gateway/MetadataServiceGatewayUnitTest.java b/dbrepo-data-service/rest-service/src/test/java/at/tuwien/gateway/MetadataServiceGatewayUnitTest.java index 1ba4978788..c224af4cb2 100644 --- a/dbrepo-data-service/rest-service/src/test/java/at/tuwien/gateway/MetadataServiceGatewayUnitTest.java +++ b/dbrepo-data-service/rest-service/src/test/java/at/tuwien/gateway/MetadataServiceGatewayUnitTest.java @@ -53,7 +53,8 @@ public class MetadataServiceGatewayUnitTest extends AbstractUnitTest { } @Test - public void getTableById_succeeds() throws TableNotFoundException, RemoteUnavailableException, ServiceException { + public void getTableById_succeeds() throws TableNotFoundException, RemoteUnavailableException, + MetadataServiceException { final HttpHeaders headers = new HttpHeaders(); headers.set("X-Type", IMAGE_1_JDBC); headers.set("X-Host", CONTAINER_1_HOST); @@ -119,7 +120,7 @@ public class MetadataServiceGatewayUnitTest extends AbstractUnitTest { .body(TABLE_1_DTO)); /* test */ - assertThrows(ServiceException.class, () -> { + assertThrows(MetadataServiceException.class, () -> { metadataServiceGateway.getTableById(DATABASE_1_ID, TABLE_1_ID); }); } @@ -139,7 +140,7 @@ public class MetadataServiceGatewayUnitTest extends AbstractUnitTest { .headers(headers) .body(TABLE_1_DTO)); /* test */ - assertThrows(ServiceException.class, () -> { + assertThrows(MetadataServiceException.class, () -> { metadataServiceGateway.getTableById(DATABASE_1_ID, TABLE_1_ID); }); } @@ -164,14 +165,14 @@ public class MetadataServiceGatewayUnitTest extends AbstractUnitTest { .build()); /* test */ - assertThrows(ServiceException.class, () -> { + assertThrows(MetadataServiceException.class, () -> { metadataServiceGateway.getTableById(DATABASE_1_ID, TABLE_1_ID); }); } @Test public void getDatabaseByInternalName_succeeds() throws DatabaseNotFoundException, RemoteUnavailableException, - ServiceException { + MetadataServiceException { /* mock */ when(restTemplate.exchange(anyString(), eq(HttpMethod.GET), eq(HttpEntity.EMPTY), eq(PrivilegedDatabaseDto[].class))) @@ -206,7 +207,7 @@ public class MetadataServiceGatewayUnitTest extends AbstractUnitTest { .body(new PrivilegedDatabaseDto[]{DATABASE_1_PRIVILEGED_DTO})); /* test */ - assertThrows(ServiceException.class, () -> { + assertThrows(MetadataServiceException.class, () -> { metadataServiceGateway.getDatabaseByInternalName(DATABASE_1_INTERNALNAME); }); } @@ -220,7 +221,7 @@ public class MetadataServiceGatewayUnitTest extends AbstractUnitTest { .build()); /* test */ - assertThrows(ServiceException.class, () -> { + assertThrows(MetadataServiceException.class, () -> { metadataServiceGateway.getDatabaseByInternalName(DATABASE_1_INTERNALNAME); }); } @@ -240,7 +241,7 @@ public class MetadataServiceGatewayUnitTest extends AbstractUnitTest { } @Test - public void getDatabaseById_succeeds() throws RemoteUnavailableException, ServiceException, + public void getDatabaseById_succeeds() throws RemoteUnavailableException, MetadataServiceException, DatabaseNotFoundException { final HttpHeaders headers = new HttpHeaders(); headers.set("X-Username", CONTAINER_1_PRIVILEGED_USERNAME); @@ -294,7 +295,7 @@ public class MetadataServiceGatewayUnitTest extends AbstractUnitTest { .build()); /* test */ - assertThrows(ServiceException.class, () -> { + assertThrows(MetadataServiceException.class, () -> { metadataServiceGateway.getDatabaseById(DATABASE_1_ID); }); } @@ -312,7 +313,7 @@ public class MetadataServiceGatewayUnitTest extends AbstractUnitTest { .build()); /* test */ - assertThrows(ServiceException.class, () -> { + assertThrows(MetadataServiceException.class, () -> { metadataServiceGateway.getDatabaseById(DATABASE_1_ID); }); } @@ -332,14 +333,14 @@ public class MetadataServiceGatewayUnitTest extends AbstractUnitTest { .headers(headers) .build()); /* test */ - assertThrows(ServiceException.class, () -> { + assertThrows(MetadataServiceException.class, () -> { metadataServiceGateway.getDatabaseById(DATABASE_1_ID); }); } } @Test - public void getContainerById_succeeds() throws RemoteUnavailableException, ContainerNotFoundException, ServiceException { + public void getContainerById_succeeds() throws RemoteUnavailableException, ContainerNotFoundException, MetadataServiceException { final HttpHeaders headers = new HttpHeaders(); headers.set("X-Username", CONTAINER_1_PRIVILEGED_USERNAME); headers.set("X-Password", CONTAINER_1_PRIVILEGED_PASSWORD); @@ -392,7 +393,7 @@ public class MetadataServiceGatewayUnitTest extends AbstractUnitTest { .build()); /* test */ - assertThrows(ServiceException.class, () -> { + assertThrows(MetadataServiceException.class, () -> { metadataServiceGateway.getContainerById(CONTAINER_1_ID); }); } @@ -412,7 +413,7 @@ public class MetadataServiceGatewayUnitTest extends AbstractUnitTest { .build()); /* test */ - assertThrows(ServiceException.class, () -> { + assertThrows(MetadataServiceException.class, () -> { metadataServiceGateway.getContainerById(CONTAINER_1_ID); }); } @@ -431,13 +432,13 @@ public class MetadataServiceGatewayUnitTest extends AbstractUnitTest { .build()); /* test */ - assertThrows(ServiceException.class, () -> { + assertThrows(MetadataServiceException.class, () -> { metadataServiceGateway.getContainerById(CONTAINER_1_ID); }); } @Test - public void getViewById_succeeds() throws RemoteUnavailableException, ViewNotFoundException, ServiceException { + public void getViewById_succeeds() throws RemoteUnavailableException, ViewNotFoundException, MetadataServiceException { final HttpHeaders headers = new HttpHeaders(); headers.set("X-Type", IMAGE_1_JDBC); headers.set("X-Host", CONTAINER_1_HOST); @@ -494,7 +495,7 @@ public class MetadataServiceGatewayUnitTest extends AbstractUnitTest { .build()); /* test */ - assertThrows(ServiceException.class, () -> { + assertThrows(MetadataServiceException.class, () -> { metadataServiceGateway.getViewById(CONTAINER_1_ID, VIEW_1_ID); }); } @@ -514,7 +515,7 @@ public class MetadataServiceGatewayUnitTest extends AbstractUnitTest { .build()); /* test */ - assertThrows(ServiceException.class, () -> { + assertThrows(MetadataServiceException.class, () -> { metadataServiceGateway.getViewById(CONTAINER_1_ID, VIEW_1_ID); }); } @@ -537,13 +538,13 @@ public class MetadataServiceGatewayUnitTest extends AbstractUnitTest { .build()); /* test */ - assertThrows(ServiceException.class, () -> { + assertThrows(MetadataServiceException.class, () -> { metadataServiceGateway.getViewById(CONTAINER_1_ID, VIEW_1_ID); }); } @Test - public void getUserById_succeeds() throws RemoteUnavailableException, UserNotFoundException, ServiceException { + public void getUserById_succeeds() throws RemoteUnavailableException, UserNotFoundException, MetadataServiceException { /* mock */ when(restTemplate.exchange(anyString(), eq(HttpMethod.GET), eq(HttpEntity.EMPTY), eq(UserDto.class))) @@ -592,7 +593,7 @@ public class MetadataServiceGatewayUnitTest extends AbstractUnitTest { .build()); /* test */ - assertThrows(ServiceException.class, () -> { + assertThrows(MetadataServiceException.class, () -> { metadataServiceGateway.getUserById(USER_1_ID); }); } @@ -606,14 +607,14 @@ public class MetadataServiceGatewayUnitTest extends AbstractUnitTest { .build()); /* test */ - assertThrows(ServiceException.class, () -> { + assertThrows(MetadataServiceException.class, () -> { metadataServiceGateway.getUserById(USER_1_ID); }); } @Test public void getPrivilegedUserById_succeeds() throws RemoteUnavailableException, UserNotFoundException, - ServiceException { + MetadataServiceException { final HttpHeaders headers = new HttpHeaders(); headers.set("X-Username", CONTAINER_1_PRIVILEGED_USERNAME); headers.set("X-Password", CONTAINER_1_PRIVILEGED_PASSWORD); @@ -668,7 +669,7 @@ public class MetadataServiceGatewayUnitTest extends AbstractUnitTest { .build()); /* test */ - assertThrows(ServiceException.class, () -> { + assertThrows(MetadataServiceException.class, () -> { metadataServiceGateway.getPrivilegedUserById(USER_1_ID); }); } @@ -688,7 +689,7 @@ public class MetadataServiceGatewayUnitTest extends AbstractUnitTest { .build()); /* test */ - assertThrows(ServiceException.class, () -> { + assertThrows(MetadataServiceException.class, () -> { metadataServiceGateway.getPrivilegedUserById(USER_1_ID); }); } @@ -707,13 +708,13 @@ public class MetadataServiceGatewayUnitTest extends AbstractUnitTest { .build()); /* test */ - assertThrows(ServiceException.class, () -> { + assertThrows(MetadataServiceException.class, () -> { metadataServiceGateway.getPrivilegedUserById(USER_1_ID); }); } @Test - public void getAccess_succeeds() throws RemoteUnavailableException, NotAllowedException, ServiceException { + public void getAccess_succeeds() throws RemoteUnavailableException, NotAllowedException, MetadataServiceException { /* mock */ when(restTemplate.exchange(anyString(), eq(HttpMethod.GET), eq(HttpEntity.EMPTY), eq(DatabaseAccessDto.class))) @@ -775,7 +776,7 @@ public class MetadataServiceGatewayUnitTest extends AbstractUnitTest { .build()); /* test */ - assertThrows(ServiceException.class, () -> { + assertThrows(MetadataServiceException.class, () -> { metadataServiceGateway.getAccess(DATABASE_1_ID, USER_1_ID); }); } @@ -789,13 +790,13 @@ public class MetadataServiceGatewayUnitTest extends AbstractUnitTest { .build()); /* test */ - assertThrows(ServiceException.class, () -> { + assertThrows(MetadataServiceException.class, () -> { metadataServiceGateway.getAccess(DATABASE_1_ID, USER_1_ID); }); } @Test - public void getIdentifiers_witSubset_succeeds() throws RemoteUnavailableException, DatabaseNotFoundException, ServiceException { + public void getIdentifiers_witSubset_succeeds() throws RemoteUnavailableException, DatabaseNotFoundException, MetadataServiceException { /* mock */ when(restTemplate.exchange(anyString(), eq(HttpMethod.GET), eq(HttpEntity.EMPTY), eq(IdentifierDto[].class))) @@ -808,7 +809,7 @@ public class MetadataServiceGatewayUnitTest extends AbstractUnitTest { } @Test - public void getIdentifiers_succeeds() throws RemoteUnavailableException, DatabaseNotFoundException, ServiceException { + public void getIdentifiers_succeeds() throws RemoteUnavailableException, DatabaseNotFoundException, MetadataServiceException { /* mock */ when(restTemplate.exchange(anyString(), eq(HttpMethod.GET), eq(HttpEntity.EMPTY), eq(IdentifierDto[].class))) @@ -857,7 +858,7 @@ public class MetadataServiceGatewayUnitTest extends AbstractUnitTest { .build()); /* test */ - assertThrows(ServiceException.class, () -> { + assertThrows(MetadataServiceException.class, () -> { metadataServiceGateway.getIdentifiers(DATABASE_1_ID, QUERY_1_ID); }); } @@ -871,13 +872,13 @@ public class MetadataServiceGatewayUnitTest extends AbstractUnitTest { .build()); /* test */ - assertThrows(ServiceException.class, () -> { + assertThrows(MetadataServiceException.class, () -> { metadataServiceGateway.getIdentifiers(DATABASE_1_ID, QUERY_1_ID); }); } @Test - public void updateTableStatistics_succeeds() throws RemoteUnavailableException, ServiceException, TableNotFoundException { + public void updateTableStatistics_succeeds() throws RemoteUnavailableException, MetadataServiceException, TableNotFoundException { /* mock */ when(restTemplate.exchange(anyString(), eq(HttpMethod.PUT), eq(HttpEntity.EMPTY), eq(Void.class))) @@ -925,7 +926,7 @@ public class MetadataServiceGatewayUnitTest extends AbstractUnitTest { .build()); /* test */ - assertThrows(ServiceException.class, () -> { + assertThrows(MetadataServiceException.class, () -> { metadataServiceGateway.updateTableStatistics(DATABASE_1_ID, TABLE_1_ID); }); } diff --git a/dbrepo-data-service/rest-service/src/test/java/at/tuwien/handlers/ApiExceptionHandlerTest.java b/dbrepo-data-service/rest-service/src/test/java/at/tuwien/handlers/ApiExceptionHandlerTest.java deleted file mode 100644 index 9075ec2a02..0000000000 --- a/dbrepo-data-service/rest-service/src/test/java/at/tuwien/handlers/ApiExceptionHandlerTest.java +++ /dev/null @@ -1,48 +0,0 @@ -package at.tuwien.handlers; - -import at.tuwien.test.AbstractUnitTest; -import lombok.extern.log4j.Log4j2; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.http.HttpStatus; -import org.springframework.test.context.junit.jupiter.SpringExtension; -import org.springframework.web.bind.annotation.ResponseStatus; - -import java.io.IOException; -import java.lang.reflect.Method; -import java.util.Arrays; -import java.util.List; -import java.util.Optional; - -import static at.tuwien.test.utils.EndpointUtils.getErrorCodes; -import static at.tuwien.test.utils.EndpointUtils.getExceptions; - -@Log4j2 -@ExtendWith(SpringExtension.class) -@SpringBootTest -public class ApiExceptionHandlerTest extends AbstractUnitTest { - - @Test - public void handle_succeeds() throws ClassNotFoundException, IOException { - final List<Method> handlers = Arrays.asList(ApiExceptionHandler.class.getMethods()); - final List<String> errorCodes = getErrorCodes(); - - /* test */ - for (Class<?> exception : getExceptions()) { - final Optional<Method> optional = handlers.stream().filter(h -> Arrays.asList(h.getParameterTypes()).contains(exception)).findFirst(); - if (optional.isEmpty()) { - Assertions.fail("Exception " + exception.getName() + " does not have a corresponding handle method in the endpoint"); - } - final Method method = optional.get(); - /* exception */ - Assertions.assertNotNull(exception.getDeclaredAnnotation(ResponseStatus.class).code()); - Assertions.assertNotEquals(exception.getDeclaredAnnotation(ResponseStatus.class).code(), HttpStatus.INTERNAL_SERVER_ERROR); - Assertions.assertNotNull(exception.getDeclaredAnnotation(ResponseStatus.class).reason(), "Exception " + exception.getName() + " does not provide a reason code"); - Assertions.assertTrue(errorCodes.contains(exception.getDeclaredAnnotation(ResponseStatus.class).reason()), "Exception code " + exception.getDeclaredAnnotation(ResponseStatus.class).reason() + " does have a reason code mapped in localized ui error messages"); - /* handler method */ - Assertions.assertEquals(method.getDeclaredAnnotation(ResponseStatus.class).code(), exception.getDeclaredAnnotation(ResponseStatus.class).code()); - } - } -} diff --git a/dbrepo-data-service/rest-service/src/test/java/at/tuwien/listener/DefaultListenerIntegrationTest.java b/dbrepo-data-service/rest-service/src/test/java/at/tuwien/listener/DefaultListenerIntegrationTest.java index dec1fcc028..88bad7b061 100644 --- a/dbrepo-data-service/rest-service/src/test/java/at/tuwien/listener/DefaultListenerIntegrationTest.java +++ b/dbrepo-data-service/rest-service/src/test/java/at/tuwien/listener/DefaultListenerIntegrationTest.java @@ -2,8 +2,8 @@ package at.tuwien.listener; import at.tuwien.config.MariaDbConfig; import at.tuwien.config.MariaDbContainerConfig; +import at.tuwien.exception.MetadataServiceException; import at.tuwien.exception.RemoteUnavailableException; -import at.tuwien.exception.ServiceException; import at.tuwien.exception.TableNotFoundException; import at.tuwien.gateway.MetadataServiceGateway; import at.tuwien.test.AbstractUnitTest; @@ -63,7 +63,7 @@ public class DefaultListenerIntegrationTest extends AbstractUnitTest { @Test public void onMessage_succeeds(CapturedOutput output) throws TableNotFoundException, RemoteUnavailableException, - ServiceException { + MetadataServiceException { final Message request = buildMessage("dbrepo." + DATABASE_1_ID + "." + TABLE_1_ID, "{\"id\":4,\"date\":\"2023-10-03\",\"mintemp\":15.0,\"rainfall\":0.2}", new HashMap<>()); /* mock */ @@ -78,7 +78,7 @@ public class DefaultListenerIntegrationTest extends AbstractUnitTest { @Test @Disabled public void onMessage_tableNotFound_fails(CapturedOutput output) throws TableNotFoundException, - RemoteUnavailableException, ServiceException { + RemoteUnavailableException, MetadataServiceException { final Message request = buildMessage("dbrepo." + DATABASE_1_ID + "." + TABLE_1_ID, "{\"id\":4,\"date\":\"2023-10-03\",\"mintemp\":15.0,\"rainfall\":0.2}", new HashMap<>()); /* mock */ diff --git a/dbrepo-data-service/rest-service/src/test/java/at/tuwien/listener/DefaultListenerUnitTest.java b/dbrepo-data-service/rest-service/src/test/java/at/tuwien/listener/DefaultListenerUnitTest.java index ab4a171c89..648e36caa9 100644 --- a/dbrepo-data-service/rest-service/src/test/java/at/tuwien/listener/DefaultListenerUnitTest.java +++ b/dbrepo-data-service/rest-service/src/test/java/at/tuwien/listener/DefaultListenerUnitTest.java @@ -2,8 +2,8 @@ package at.tuwien.listener; import at.tuwien.config.MariaDbConfig; import at.tuwien.config.MariaDbContainerConfig; +import at.tuwien.exception.MetadataServiceException; import at.tuwien.exception.RemoteUnavailableException; -import at.tuwien.exception.ServiceException; import at.tuwien.exception.TableNotFoundException; import at.tuwien.gateway.MetadataServiceGateway; import at.tuwien.test.AbstractUnitTest; @@ -76,7 +76,7 @@ public class DefaultListenerUnitTest extends AbstractUnitTest { @Test public void onMessage_messageMalformed_fails(CapturedOutput output) throws TableNotFoundException, - RemoteUnavailableException, ServiceException { + RemoteUnavailableException, MetadataServiceException { final Message request = buildMessage("dbrepo.1.1", "{,}", new HashMap<>()); /* mock */ @@ -90,7 +90,7 @@ public class DefaultListenerUnitTest extends AbstractUnitTest { @Test public void onMessage_tableNotFound_fails(CapturedOutput output) throws TableNotFoundException, - RemoteUnavailableException, ServiceException { + RemoteUnavailableException, MetadataServiceException { final Message request = buildMessage("dbrepo.1.1", "{\"id\":1}", new HashMap<>()); /* mock */ diff --git a/dbrepo-data-service/rest-service/src/test/java/at/tuwien/service/DatabaseServiceIntegrationTest.java b/dbrepo-data-service/rest-service/src/test/java/at/tuwien/service/DatabaseServiceIntegrationTest.java index e53f6ad88f..c89d53282c 100644 --- a/dbrepo-data-service/rest-service/src/test/java/at/tuwien/service/DatabaseServiceIntegrationTest.java +++ b/dbrepo-data-service/rest-service/src/test/java/at/tuwien/service/DatabaseServiceIntegrationTest.java @@ -9,6 +9,7 @@ import at.tuwien.mapper.MariaDbMapper; import at.tuwien.mapper.MariaDbMapperImpl; import at.tuwien.test.AbstractUnitTest; import lombok.extern.log4j.Log4j2; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -38,6 +39,11 @@ public class DatabaseServiceIntegrationTest extends AbstractUnitTest { @Autowired private MariaDbMapper mariaDbMapper; + @BeforeAll + public static void beforeAll() throws InterruptedException { + Thread.sleep(1000) /* wait for test container some more */; + } + @BeforeEach public void beforeEach() throws SQLException { genesis(); diff --git a/dbrepo-data-service/rest-service/src/test/java/at/tuwien/service/QueueServiceIntegrationTest.java b/dbrepo-data-service/rest-service/src/test/java/at/tuwien/service/QueueServiceIntegrationTest.java index 4bfa5b443a..bd57eafacc 100644 --- a/dbrepo-data-service/rest-service/src/test/java/at/tuwien/service/QueueServiceIntegrationTest.java +++ b/dbrepo-data-service/rest-service/src/test/java/at/tuwien/service/QueueServiceIntegrationTest.java @@ -3,13 +3,14 @@ package at.tuwien.service; import at.tuwien.config.MariaDbConfig; import at.tuwien.config.MariaDbContainerConfig; import at.tuwien.exception.ContainerNotFoundException; +import at.tuwien.exception.MetadataServiceException; import at.tuwien.exception.RemoteUnavailableException; -import at.tuwien.exception.ServiceException; import at.tuwien.exception.TableNotFoundException; import at.tuwien.gateway.MetadataServiceGateway; import at.tuwien.service.impl.QueueServiceRabbitMqImpl; import at.tuwien.test.AbstractUnitTest; import lombok.extern.log4j.Log4j2; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -42,6 +43,11 @@ public class QueueServiceIntegrationTest extends AbstractUnitTest { @Container private static MariaDBContainer<?> mariaDBContainer = MariaDbContainerConfig.getContainer(); + @BeforeAll + public static void beforeAll() throws InterruptedException { + Thread.sleep(1000) /* wait for test container some more */; + } + @BeforeEach public void beforeEach() throws SQLException { genesis(); @@ -51,8 +57,8 @@ public class QueueServiceIntegrationTest extends AbstractUnitTest { } @Test - public void insert_succeeds() throws InterruptedException, SQLException, RemoteUnavailableException, - ContainerNotFoundException, TableNotFoundException, ServiceException { + public void insert_succeeds() throws SQLException, RemoteUnavailableException, ContainerNotFoundException, + TableNotFoundException, MetadataServiceException { final Map<String, Object> request = new HashMap<>() {{ put("id", 4L); put("date", "2023-10-03"); @@ -61,9 +67,6 @@ public class QueueServiceIntegrationTest extends AbstractUnitTest { put("rainfall", 0.2); }}; - /* pre-condition */ - Thread.sleep(1000) /* wait for test container some more */; - /* mock */ when(metadataServiceGateway.getContainerById(CONTAINER_1_ID)) .thenReturn(CONTAINER_1_PRIVILEGED_DTO); @@ -75,16 +78,13 @@ public class QueueServiceIntegrationTest extends AbstractUnitTest { } @Test - public void insert_onlyMandatoryFields_succeeds() throws InterruptedException, SQLException, - RemoteUnavailableException, TableNotFoundException, ServiceException { + public void insert_onlyMandatoryFields_succeeds() throws SQLException, RemoteUnavailableException, + TableNotFoundException, MetadataServiceException { final Map<String, Object> request = new HashMap<>() {{ put("id", 5L); put("date", "2023-10-04"); }}; - /* pre-condition */ - Thread.sleep(1000) /* wait for test container some more */; - /* mock */ when(metadataServiceGateway.getTableById(DATABASE_1_ID, TABLE_1_ID)) .thenReturn(TABLE_1_PRIVILEGED_DTO); diff --git a/dbrepo-data-service/rest-service/src/test/java/at/tuwien/service/SubsetServiceIntegrationTest.java b/dbrepo-data-service/rest-service/src/test/java/at/tuwien/service/SubsetServiceIntegrationTest.java index d13a37531b..aeaae0ecf2 100644 --- a/dbrepo-data-service/rest-service/src/test/java/at/tuwien/service/SubsetServiceIntegrationTest.java +++ b/dbrepo-data-service/rest-service/src/test/java/at/tuwien/service/SubsetServiceIntegrationTest.java @@ -1,14 +1,21 @@ package at.tuwien.service; +import at.tuwien.ExportResourceDto; import at.tuwien.api.database.query.QueryDto; import at.tuwien.api.database.query.QueryResultDto; import at.tuwien.api.identifier.IdentifierDto; import at.tuwien.config.MariaDbConfig; import at.tuwien.config.MariaDbContainerConfig; +import at.tuwien.config.S3Config; import at.tuwien.exception.*; +import at.tuwien.gateway.DataDatabaseSidecarGateway; import at.tuwien.gateway.MetadataServiceGateway; import at.tuwien.test.AbstractUnitTest; +import com.google.common.hash.Hashing; import lombok.extern.log4j.Log4j2; +import org.apache.commons.io.FileUtils; +import org.apache.commons.lang3.RandomUtils; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -20,15 +27,19 @@ import org.testcontainers.containers.MariaDBContainer; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; import java.math.BigInteger; +import java.nio.charset.Charset; import java.sql.SQLException; import java.time.Instant; import java.util.List; import java.util.Map; import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.when; @Log4j2 @@ -43,9 +54,18 @@ public class SubsetServiceIntegrationTest extends AbstractUnitTest { @MockBean private MetadataServiceGateway metadataServiceGateway; + @MockBean + private DataDatabaseSidecarGateway dataDatabaseSidecarGateway; + + @MockBean + private StorageService storageService; + @Container private static MariaDBContainer<?> mariaDBContainer = MariaDbContainerConfig.getContainer(); + @Autowired + private S3Config s3Config; + @BeforeEach public void beforeEach() throws SQLException { genesis(); @@ -56,8 +76,8 @@ public class SubsetServiceIntegrationTest extends AbstractUnitTest { @Test public void execute_succeeds() throws QueryStoreInsertException, TableMalformedException, SQLException, - QueryNotFoundException, InterruptedException, UserNotFoundException, NotAllowedException, - RemoteUnavailableException, ServiceException, DatabaseNotFoundException { + QueryNotFoundException, UserNotFoundException, NotAllowedException, RemoteUnavailableException, + MetadataServiceException, DatabaseNotFoundException, InterruptedException { /* pre-condition */ Thread.sleep(1000) /* wait for test container some more */; @@ -96,9 +116,9 @@ public class SubsetServiceIntegrationTest extends AbstractUnitTest { } @Test - public void execute_joinWithAlias_succeeds() throws QueryStoreInsertException, TableMalformedException, SQLException, - QueryNotFoundException, InterruptedException, UserNotFoundException, NotAllowedException, - RemoteUnavailableException, ServiceException, DatabaseNotFoundException { + public void execute_joinWithAlias_succeeds() throws QueryStoreInsertException, TableMalformedException, + SQLException, QueryNotFoundException, UserNotFoundException, NotAllowedException, + RemoteUnavailableException, MetadataServiceException, DatabaseNotFoundException, InterruptedException { /* pre-condition */ Thread.sleep(1000) /* wait for test container some more */; @@ -126,8 +146,8 @@ public class SubsetServiceIntegrationTest extends AbstractUnitTest { @Test public void execute_oneResult_succeeds() throws QueryStoreInsertException, TableMalformedException, SQLException, - QueryNotFoundException, InterruptedException, UserNotFoundException, NotAllowedException, - RemoteUnavailableException, ServiceException, DatabaseNotFoundException { + QueryNotFoundException, UserNotFoundException, NotAllowedException, RemoteUnavailableException, + MetadataServiceException, DatabaseNotFoundException, InterruptedException { /* pre-condition */ Thread.sleep(1000) /* wait for test container some more */; @@ -157,8 +177,8 @@ public class SubsetServiceIntegrationTest extends AbstractUnitTest { @Test public void execute_oneResultPagination_succeeds() throws QueryStoreInsertException, TableMalformedException, - SQLException, QueryNotFoundException, InterruptedException, UserNotFoundException, NotAllowedException, - RemoteUnavailableException, ServiceException, DatabaseNotFoundException { + SQLException, QueryNotFoundException, UserNotFoundException, NotAllowedException, + RemoteUnavailableException, MetadataServiceException, DatabaseNotFoundException, InterruptedException { /* pre-condition */ Thread.sleep(1000) /* wait for test container some more */; @@ -187,8 +207,8 @@ public class SubsetServiceIntegrationTest extends AbstractUnitTest { } @Test - public void findAll_succeeds() throws SQLException, QueryNotFoundException, InterruptedException, - NotAllowedException, RemoteUnavailableException, ServiceException, DatabaseNotFoundException { + public void findAll_succeeds() throws SQLException, QueryNotFoundException, NotAllowedException, + RemoteUnavailableException, MetadataServiceException, DatabaseNotFoundException, InterruptedException { /* test */ final List<QueryDto> response = findAll_generic(null); @@ -198,8 +218,8 @@ public class SubsetServiceIntegrationTest extends AbstractUnitTest { } @Test - public void findAll_onlyPersisted_succeeds() throws SQLException, QueryNotFoundException, InterruptedException, - NotAllowedException, RemoteUnavailableException, ServiceException, DatabaseNotFoundException { + public void findAll_onlyPersisted_succeeds() throws SQLException, QueryNotFoundException, NotAllowedException, + RemoteUnavailableException, MetadataServiceException, DatabaseNotFoundException, InterruptedException { /* test */ final List<QueryDto> response = findAll_generic(true); @@ -208,8 +228,8 @@ public class SubsetServiceIntegrationTest extends AbstractUnitTest { } @Test - public void findAll_onlyNonPersisted_succeeds() throws SQLException, QueryNotFoundException, InterruptedException, - NotAllowedException, RemoteUnavailableException, ServiceException, DatabaseNotFoundException { + public void findAll_onlyNonPersisted_succeeds() throws SQLException, QueryNotFoundException, NotAllowedException, + RemoteUnavailableException, MetadataServiceException, DatabaseNotFoundException, InterruptedException { /* test */ final List<QueryDto> response = findAll_generic(false); @@ -218,9 +238,9 @@ public class SubsetServiceIntegrationTest extends AbstractUnitTest { } @Test - public void findById_succeeds() throws SQLException, QueryNotFoundException, InterruptedException, - UserNotFoundException, NotAllowedException, RemoteUnavailableException, ServiceException, - DatabaseNotFoundException { + public void findById_succeeds() throws SQLException, QueryNotFoundException, UserNotFoundException, + NotAllowedException, RemoteUnavailableException, MetadataServiceException, DatabaseNotFoundException, + InterruptedException { /* test */ findById_generic(QUERY_1_ID); @@ -236,9 +256,9 @@ public class SubsetServiceIntegrationTest extends AbstractUnitTest { } @Test - public void persist_succeeds() throws SQLException, InterruptedException, QueryStorePersistException, - QueryNotFoundException, UserNotFoundException, NotAllowedException, RemoteUnavailableException, - ServiceException, DatabaseNotFoundException { + public void persist_succeeds() throws SQLException, QueryStorePersistException, QueryNotFoundException, + UserNotFoundException, NotAllowedException, RemoteUnavailableException, MetadataServiceException, + DatabaseNotFoundException, InterruptedException { /* mock */ when(metadataServiceGateway.getUserById(QUERY_2_CREATED_BY)) @@ -252,9 +272,9 @@ public class SubsetServiceIntegrationTest extends AbstractUnitTest { } @Test - public void persist_unPersist_succeeds() throws SQLException, InterruptedException, QueryStorePersistException, - QueryNotFoundException, UserNotFoundException, NotAllowedException, RemoteUnavailableException, - ServiceException, DatabaseNotFoundException { + public void persist_unPersist_succeeds() throws SQLException, QueryStorePersistException, QueryNotFoundException, + UserNotFoundException, NotAllowedException, RemoteUnavailableException, MetadataServiceException, + DatabaseNotFoundException, InterruptedException { /* mock */ when(metadataServiceGateway.getUserById(QUERY_1_CREATED_BY)) @@ -267,9 +287,40 @@ public class SubsetServiceIntegrationTest extends AbstractUnitTest { assertFalse(response.getIsPersisted()); } - protected void findById_generic(Long queryId) throws InterruptedException, NotAllowedException, - RemoteUnavailableException, SQLException, UserNotFoundException, QueryNotFoundException, ServiceException, - DatabaseNotFoundException { + @Test + public void createQueryStore_succeeds() throws SQLException, QueryStoreCreateException, InterruptedException { + + /* mock */ + MariaDbConfig.dropQueryStore(DATABASE_1_PRIVILEGED_DTO); + + /* test */ + createQueryStore_generic(DATABASE_1_INTERNALNAME); + } + + @Test + public void createQueryStore_fails() { + + /* test */ + assertThrows(QueryStoreCreateException.class, () -> { + createQueryStore_generic(DATABASE_1_INTERNALNAME); + }); + } + + @Test + public void export_succeeds() throws SQLException, StorageUnavailableException, QueryMalformedException, + SidecarExportException, MetadataServiceException, RemoteUnavailableException, IOException, + StorageNotFoundException, InterruptedException { + + /* mock */ + MariaDbConfig.dropQueryStore(DATABASE_1_PRIVILEGED_DTO); + + /* test */ + export_generic(); + } + + protected void findById_generic(Long queryId) throws NotAllowedException, RemoteUnavailableException, SQLException, + UserNotFoundException, QueryNotFoundException, MetadataServiceException, DatabaseNotFoundException, + InterruptedException { /* pre-condition */ Thread.sleep(1000) /* wait for test container some more */; @@ -287,9 +338,9 @@ public class SubsetServiceIntegrationTest extends AbstractUnitTest { assertEquals(DATABASE_1_ID, response.getDatabaseId()); } - protected List<QueryDto> findAll_generic(Boolean filterPersisted) throws InterruptedException, SQLException, - QueryNotFoundException, NotAllowedException, RemoteUnavailableException, ServiceException, - DatabaseNotFoundException { + protected List<QueryDto> findAll_generic(Boolean filterPersisted) throws SQLException, QueryNotFoundException, + NotAllowedException, RemoteUnavailableException, MetadataServiceException, DatabaseNotFoundException, + InterruptedException { /* pre-condition */ Thread.sleep(1000) /* wait for test container some more */; @@ -305,8 +356,8 @@ public class SubsetServiceIntegrationTest extends AbstractUnitTest { } protected void persist_generic(Long queryId, List<IdentifierDto> identifiers, Boolean persist) - throws InterruptedException, RemoteUnavailableException, SQLException, QueryStorePersistException, - ServiceException, DatabaseNotFoundException { + throws RemoteUnavailableException, SQLException, QueryStorePersistException, MetadataServiceException, + DatabaseNotFoundException, InterruptedException { /* pre-condition */ Thread.sleep(1000) /* wait for test container some more */; @@ -321,4 +372,38 @@ public class SubsetServiceIntegrationTest extends AbstractUnitTest { queryService.persist(DATABASE_1_PRIVILEGED_DTO, queryId, persist); } + protected void createQueryStore_generic(String databaseName) throws SQLException, QueryStoreCreateException, + InterruptedException { + + /* pre-condition */ + Thread.sleep(1000) /* wait for test container some more */; + + /* test */ + queryService.createQueryStore(CONTAINER_1_PRIVILEGED_DTO, databaseName); + final List<Map<String, Object>> response = MariaDbConfig.listQueryStore(DATABASE_1_PRIVILEGED_DTO); + assertEquals(0, response.size()); + } + + protected void export_generic() throws StorageUnavailableException, SQLException, + QueryMalformedException, SidecarExportException, MetadataServiceException, RemoteUnavailableException, + StorageNotFoundException, IOException, InterruptedException { + final String filename = "68b329da9893e34099c7d8ad5cb9c940"; + + /* pre-condition */ + Thread.sleep(1000) /* wait for test container some more */; + + /* mock */ + FileUtils.deleteQuietly(new File(s3Config.getS3FilePath() + "/" + filename)); + doNothing() + .when(dataDatabaseSidecarGateway) + .exportFile(anyString(), anyInt(), eq(filename)); + when(storageService.getResource(anyString())) + .thenReturn(EXPORT_RESOURCE_DTO); + + /* test */ + final ExportResourceDto response = queryService.export(DATABASE_1_PRIVILEGED_DTO, QUERY_1_DTO, Instant.now(), filename); + assertEquals(filename, response.getFilename()); + assertNotNull(response.getResource().getInputStream()); + } + } diff --git a/dbrepo-data-service/rest-service/src/test/java/at/tuwien/service/TableServiceIntegrationTest.java b/dbrepo-data-service/rest-service/src/test/java/at/tuwien/service/TableServiceIntegrationTest.java index 86a0442ef6..65ec32c0d5 100644 --- a/dbrepo-data-service/rest-service/src/test/java/at/tuwien/service/TableServiceIntegrationTest.java +++ b/dbrepo-data-service/rest-service/src/test/java/at/tuwien/service/TableServiceIntegrationTest.java @@ -25,6 +25,7 @@ import at.tuwien.test.AbstractUnitTest; import lombok.extern.log4j.Log4j2; import org.apache.commons.io.FileUtils; import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -74,6 +75,11 @@ public class TableServiceIntegrationTest extends AbstractUnitTest { @Container private static MariaDBContainer<?> mariaDBContainer = MariaDbContainerConfig.getContainer(); + @BeforeAll + public static void beforeAll() throws InterruptedException { + Thread.sleep(1000) /* wait for test container some more */; + } + @BeforeEach public void beforeEach() throws SQLException { genesis(); @@ -84,9 +90,8 @@ public class TableServiceIntegrationTest extends AbstractUnitTest { } @Test - public void updateTuple_succeeds() throws InterruptedException, SQLException, RemoteUnavailableException, - ContainerNotFoundException, TableNotFoundException, TableMalformedException, QueryMalformedException, - ServiceException { + public void updateTuple_succeeds() throws SQLException, RemoteUnavailableException, ContainerNotFoundException, + TableNotFoundException, TableMalformedException, QueryMalformedException, MetadataServiceException { /* modify row based on primary key */ final TupleUpdateDto request = TupleUpdateDto.builder() .data(new HashMap<>() {{ @@ -100,9 +105,6 @@ public class TableServiceIntegrationTest extends AbstractUnitTest { }}) .build(); - /* pre-condition */ - Thread.sleep(1000) /* wait for test container some more */; - /* mock */ when(metadataServiceGateway.getContainerById(CONTAINER_1_ID)) .thenReturn(CONTAINER_1_PRIVILEGED_DTO); @@ -120,9 +122,9 @@ public class TableServiceIntegrationTest extends AbstractUnitTest { } @Test - public void updateTuple_modifyPrimaryKey_succeeds() throws InterruptedException, SQLException, - RemoteUnavailableException, ContainerNotFoundException, TableNotFoundException, TableMalformedException, - QueryMalformedException, ServiceException { + public void updateTuple_modifyPrimaryKey_succeeds() throws SQLException, RemoteUnavailableException, + ContainerNotFoundException, TableNotFoundException, TableMalformedException, QueryMalformedException, + MetadataServiceException { /* modify row primary key based on primary key */ final TupleUpdateDto request = TupleUpdateDto.builder() .data(new HashMap<>() {{ @@ -137,9 +139,6 @@ public class TableServiceIntegrationTest extends AbstractUnitTest { }}) .build(); - /* pre-condition */ - Thread.sleep(1000) /* wait for test container some more */; - /* mock */ when(metadataServiceGateway.getContainerById(CONTAINER_1_ID)) .thenReturn(CONTAINER_1_PRIVILEGED_DTO); @@ -157,9 +156,9 @@ public class TableServiceIntegrationTest extends AbstractUnitTest { } @Test - public void updateTuple_missingPrimaryKey_succeeds() throws InterruptedException, SQLException, - RemoteUnavailableException, ContainerNotFoundException, TableNotFoundException, TableMalformedException, - QueryMalformedException, ServiceException { + public void updateTuple_missingPrimaryKey_succeeds() throws SQLException, RemoteUnavailableException, + ContainerNotFoundException, TableNotFoundException, TableMalformedException, QueryMalformedException, + MetadataServiceException { /* modify row based on non-primary key column */ final TupleUpdateDto request = TupleUpdateDto.builder() .data(new HashMap<>() {{ @@ -173,9 +172,6 @@ public class TableServiceIntegrationTest extends AbstractUnitTest { }}) .build(); - /* pre-condition */ - Thread.sleep(1000) /* wait for test container some more */; - /* mock */ when(metadataServiceGateway.getContainerById(CONTAINER_1_ID)) .thenReturn(CONTAINER_1_PRIVILEGED_DTO); @@ -193,9 +189,9 @@ public class TableServiceIntegrationTest extends AbstractUnitTest { } @Test - public void updateTuple_notInOrder_succeeds() throws InterruptedException, SQLException, RemoteUnavailableException, + public void updateTuple_notInOrder_succeeds() throws SQLException, RemoteUnavailableException, ContainerNotFoundException, TableNotFoundException, TableMalformedException, QueryMalformedException, - ServiceException { + MetadataServiceException { /* modify row based on non-primary key column */ final TupleUpdateDto request = TupleUpdateDto.builder() .data(new HashMap<>() {{ @@ -209,9 +205,6 @@ public class TableServiceIntegrationTest extends AbstractUnitTest { }}) .build(); - /* pre-condition */ - Thread.sleep(1000) /* wait for test container some more */; - /* mock */ when(metadataServiceGateway.getContainerById(CONTAINER_1_ID)) .thenReturn(CONTAINER_1_PRIVILEGED_DTO); @@ -229,9 +222,9 @@ public class TableServiceIntegrationTest extends AbstractUnitTest { } @Test - public void createTuple_succeeds() throws InterruptedException, SQLException, RemoteUnavailableException, - ContainerNotFoundException, TableNotFoundException, TableMalformedException, QueryMalformedException, - StorageUnavailableException, StorageNotFoundException, ServiceException { + public void createTuple_succeeds() throws SQLException, RemoteUnavailableException, ContainerNotFoundException, + TableNotFoundException, TableMalformedException, QueryMalformedException, StorageUnavailableException, + StorageNotFoundException, MetadataServiceException { /* add row with primary key */ final TupleDto request = TupleDto.builder() .data(new HashMap<>() {{ @@ -243,9 +236,6 @@ public class TableServiceIntegrationTest extends AbstractUnitTest { }}) .build(); - /* pre-condition */ - Thread.sleep(1000) /* wait for test container some more */; - /* mock */ when(metadataServiceGateway.getContainerById(CONTAINER_1_ID)) .thenReturn(CONTAINER_1_PRIVILEGED_DTO); @@ -263,9 +253,9 @@ public class TableServiceIntegrationTest extends AbstractUnitTest { } @Test - public void createTuple_notInOrder_succeeds() throws InterruptedException, SQLException, RemoteUnavailableException, + public void createTuple_notInOrder_succeeds() throws SQLException, RemoteUnavailableException, ContainerNotFoundException, TableNotFoundException, TableMalformedException, QueryMalformedException, - StorageUnavailableException, StorageNotFoundException, ServiceException { + StorageUnavailableException, StorageNotFoundException, MetadataServiceException { /* add row with primary key */ final TupleDto request = TupleDto.builder() .data(new HashMap<>() {{ @@ -277,9 +267,6 @@ public class TableServiceIntegrationTest extends AbstractUnitTest { }}) .build(); - /* pre-condition */ - Thread.sleep(1000) /* wait for test container some more */; - /* mock */ when(metadataServiceGateway.getContainerById(CONTAINER_1_ID)) .thenReturn(CONTAINER_1_PRIVILEGED_DTO); @@ -297,9 +284,8 @@ public class TableServiceIntegrationTest extends AbstractUnitTest { } @Test - public void deleteTuple_succeeds() throws InterruptedException, SQLException, RemoteUnavailableException, - ContainerNotFoundException, TableNotFoundException, TableMalformedException, QueryMalformedException, - ServiceException { + public void deleteTuple_succeeds() throws SQLException, RemoteUnavailableException, ContainerNotFoundException, + TableNotFoundException, TableMalformedException, QueryMalformedException, MetadataServiceException { /* delete row based on primary key */ final TupleDeleteDto request = TupleDeleteDto.builder() .keys(new HashMap<>() {{ @@ -307,9 +293,6 @@ public class TableServiceIntegrationTest extends AbstractUnitTest { }}) .build(); - /* pre-condition */ - Thread.sleep(1000) /* wait for test container some more */; - /* mock */ when(metadataServiceGateway.getContainerById(CONTAINER_1_ID)) .thenReturn(CONTAINER_1_PRIVILEGED_DTO); @@ -323,9 +306,9 @@ public class TableServiceIntegrationTest extends AbstractUnitTest { } @Test - public void deleteTuple_withoutPrimaryKey_succeeds() throws InterruptedException, SQLException, - RemoteUnavailableException, ContainerNotFoundException, TableNotFoundException, TableMalformedException, - QueryMalformedException, ServiceException { + public void deleteTuple_withoutPrimaryKey_succeeds() throws SQLException, RemoteUnavailableException, + ContainerNotFoundException, TableNotFoundException, TableMalformedException, QueryMalformedException, + MetadataServiceException { /* remove row based on non-primary key */ final TupleDeleteDto request = TupleDeleteDto.builder() .keys(new HashMap<>() {{ @@ -334,9 +317,6 @@ public class TableServiceIntegrationTest extends AbstractUnitTest { }}) .build(); - /* pre-condition */ - Thread.sleep(1000) /* wait for test container some more */; - /* mock */ when(metadataServiceGateway.getContainerById(CONTAINER_1_ID)) .thenReturn(CONTAINER_1_PRIVILEGED_DTO); @@ -350,8 +330,7 @@ public class TableServiceIntegrationTest extends AbstractUnitTest { } @Test - public void getSchemas_succeeds() throws TableNotFoundException, SQLException, QueryMalformedException, - DatabaseMalformedException { + public void getSchemas_succeeds() throws TableNotFoundException, SQLException, DatabaseMalformedException { /* test */ final List<TableDto> response = tableService.getSchemas(DATABASE_1_PRIVILEGED_DTO); @@ -462,7 +441,7 @@ public class TableServiceIntegrationTest extends AbstractUnitTest { @Test public void create_succeeds() throws TableNotFoundException, TableMalformedException, SQLException, - QueryMalformedException, TableExistsException { + TableExistsException { /* test */ final TableDto response = tableService.createTable(DATABASE_1_PRIVILEGED_DTO, TABLE_4_CREATE_INTERNAL_DTO); @@ -538,7 +517,7 @@ public class TableServiceIntegrationTest extends AbstractUnitTest { @Test public void create_needSequence_succeeds() throws TableNotFoundException, TableMalformedException, SQLException, - QueryMalformedException, TableExistsException { + TableExistsException { /* mock */ MariaDbConfig.dropTable(DATABASE_1_PRIVILEGED_DTO, TABLE_1_INTERNALNAME); @@ -665,8 +644,8 @@ public class TableServiceIntegrationTest extends AbstractUnitTest { } @Test - public void importDataset_withSeparatorAndQuoteAndNullElement_succeeds() throws SidecarImportException, ServiceException, SQLException, - QueryMalformedException, RemoteUnavailableException, StorageNotFoundException, IOException { + public void importDataset_withSeparatorAndQuoteAndNullElement_succeeds() throws SidecarImportException, + SQLException, QueryMalformedException, RemoteUnavailableException, StorageNotFoundException, IOException { final ImportCsvDto request = ImportCsvDto.builder() .location("weather_aus.csv") .separator(';') @@ -688,8 +667,8 @@ public class TableServiceIntegrationTest extends AbstractUnitTest { } @Test - public void importDataset_malformedData_fails() throws ServiceException, RemoteUnavailableException, StorageNotFoundException, - IOException { + public void importDataset_malformedData_fails() throws RemoteUnavailableException, StorageNotFoundException, + IOException, SidecarImportException { final ImportCsvDto request = ImportCsvDto.builder() .location("weather_aus.csv") .separator(';') @@ -712,9 +691,8 @@ public class TableServiceIntegrationTest extends AbstractUnitTest { } @Test - public void exportDataset_succeeds() throws ServiceException, SQLException, - QueryMalformedException, RemoteUnavailableException, StorageNotFoundException, StorageUnavailableException, - SidecarExportException { + public void exportDataset_succeeds() throws SQLException, QueryMalformedException, RemoteUnavailableException, + StorageNotFoundException, StorageUnavailableException, SidecarExportException { final ExportResourceDto mock = ExportResourceDto.builder() .filename("weather_aus.csv") .resource(new InputStreamResource(InputStream.nullInputStream())) diff --git a/dbrepo-data-service/rest-service/src/test/java/at/tuwien/utils/UserUtilTest.java b/dbrepo-data-service/rest-service/src/test/java/at/tuwien/utils/UserUtilTest.java new file mode 100644 index 0000000000..13806e93dd --- /dev/null +++ b/dbrepo-data-service/rest-service/src/test/java/at/tuwien/utils/UserUtilTest.java @@ -0,0 +1,40 @@ +package at.tuwien.utils; + +import at.tuwien.test.BaseTest; +import org.junit.jupiter.api.Test; + + +import static org.junit.jupiter.api.Assertions.*; + +public class UserUtilTest extends BaseTest { + + @Test + public void hasRole_succeeds() { + assertTrue(UserUtil.hasRole(USER_1_PRINCIPAL, "find-container")); + } + + @Test + public void hasRole_principalMissing_fails() { + assertFalse(UserUtil.hasRole(null, "find-container")); + } + + @Test + public void hasRole_roleMissing_fails() { + assertFalse(UserUtil.hasRole(USER_1_PRINCIPAL, null)); + } + + @Test + public void getId_succeeds() { + assertEquals(USER_1_ID, UserUtil.getId(USER_1_PRINCIPAL)); + } + + @Test + public void getId_principalMissing_fails() { + assertNull(UserUtil.getId(null)); + } + + @Test + public void getId_roleMissing_fails() { + assertNull(UserUtil.getId(USER_LOCAL_ADMIN_PRINCIPAL)); + } +} diff --git a/dbrepo-data-service/rest-service/src/test/java/at/tuwien/validation/EndpointValidatorUnitTest.java b/dbrepo-data-service/rest-service/src/test/java/at/tuwien/validation/EndpointValidatorUnitTest.java new file mode 100644 index 0000000000..b12313dfe9 --- /dev/null +++ b/dbrepo-data-service/rest-service/src/test/java/at/tuwien/validation/EndpointValidatorUnitTest.java @@ -0,0 +1,101 @@ +package at.tuwien.validation; + +import at.tuwien.annotations.MockAmqp; +import at.tuwien.exception.PaginationException; +import at.tuwien.exception.QueryNotSupportedException; +import at.tuwien.test.AbstractUnitTest; +import lombok.extern.log4j.Log4j2; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.actuate.observability.AutoConfigureObservability; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.test.web.servlet.MockMvc; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@Log4j2 +@ExtendWith(SpringExtension.class) +@AutoConfigureMockMvc +@SpringBootTest +@AutoConfigureObservability +@MockAmqp +public class EndpointValidatorUnitTest extends AbstractUnitTest { + + @Autowired + private EndpointValidator endpointValidator; + + @Test + public void validateDataParams_succeeds() throws Exception { + + /* test */ + endpointValidator.validateDataParams(null, null); + } + + @Test + public void validateDataParams_onlyPage_fails() { + + /* test */ + assertThrows(PaginationException.class, () -> { + endpointValidator.validateDataParams(0L, null); + }); + } + + @Test + public void validateDataParams_negativePage_fails() { + + /* test */ + assertThrows(PaginationException.class, () -> { + endpointValidator.validateDataParams(-1L, 10L); + }); + } + + @Test + public void validateDataParams_onlySize_fails() { + + /* test */ + assertThrows(PaginationException.class, () -> { + endpointValidator.validateDataParams(null, 10L); + }); + } + + @Test + public void validateDataParams_zeroSize_fails() { + + /* test */ + assertThrows(PaginationException.class, () -> { + endpointValidator.validateDataParams(0L, 0L); + }); + } + + @Test + public void validateForbiddenStatements_succeeds() throws QueryNotSupportedException { + + /* test */ + endpointValidator.validateForbiddenStatements("SELECT country FROM some_table"); + } + + @Test + public void validateForbiddenStatements_fails() { + + /* test */ + assertThrows(QueryNotSupportedException.class, () -> { + endpointValidator.validateForbiddenStatements("SELECT COUNT(id) FROM some_table"); + }); + } + + @Test + public void validateForbiddenStatements_lowercase_fails() { + + /* test */ + assertThrows(QueryNotSupportedException.class, () -> { + endpointValidator.validateForbiddenStatements("SELECT COUNT(id) FROM some_table"); + }); + } + +} diff --git a/dbrepo-data-service/rest-service/src/test/resources/application.properties b/dbrepo-data-service/rest-service/src/test/resources/application.properties index 07eb7f642b..764d9609c9 100644 --- a/dbrepo-data-service/rest-service/src/test/resources/application.properties +++ b/dbrepo-data-service/rest-service/src/test/resources/application.properties @@ -29,4 +29,4 @@ spring.rabbitmq.password=guest # s3 dbrepo.s3.accessKeyId=minioadmin -dbrepo.s3.secretAccessKey=minioadmin \ No newline at end of file +dbrepo.s3.secretAccessKey=minioadmin diff --git a/dbrepo-data-service/services/src/main/java/at/tuwien/auth/BasicAuthenticationProvider.java b/dbrepo-data-service/services/src/main/java/at/tuwien/auth/BasicAuthenticationProvider.java index 8aa52308bf..80fdd15f1d 100644 --- a/dbrepo-data-service/services/src/main/java/at/tuwien/auth/BasicAuthenticationProvider.java +++ b/dbrepo-data-service/services/src/main/java/at/tuwien/auth/BasicAuthenticationProvider.java @@ -1,8 +1,6 @@ package at.tuwien.auth; import at.tuwien.api.keycloak.TokenDto; -import at.tuwien.api.user.UserDetailsDto; -import at.tuwien.config.GatewayConfig; import at.tuwien.exception.*; import at.tuwien.gateway.KeycloakGateway; import jakarta.servlet.ServletException; @@ -13,45 +11,30 @@ import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; -import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.stereotype.Component; -import java.util.List; - @Log4j2 @Component public class BasicAuthenticationProvider implements AuthenticationManager { - private final GatewayConfig gatewayConfig; private final AuthTokenFilter authTokenFilter; private final KeycloakGateway keycloakGateway; @Autowired - public BasicAuthenticationProvider(GatewayConfig gatewayConfig, AuthTokenFilter authTokenFilter, - KeycloakGateway keycloakGateway) { - this.gatewayConfig = gatewayConfig; + public BasicAuthenticationProvider(AuthTokenFilter authTokenFilter, KeycloakGateway keycloakGateway) { this.authTokenFilter = authTokenFilter; this.keycloakGateway = keycloakGateway; } @Override public Authentication authenticate(Authentication auth) throws AuthenticationException { - if (auth.getName().equals(gatewayConfig.getAdminUsername()) - && auth.getCredentials().toString().equals(gatewayConfig.getAdminPassword())) { - log.trace("current user is {}: skip authentication", gatewayConfig.getAdminUsername()); - final UserDetails userDetails = UserDetailsDto.builder() - .username(auth.getName()) - .authorities(List.of(new SimpleGrantedAuthority("admin"))) - .build(); - return new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); - } - log.trace("current user is {}: begin authentication", auth.getName()); try { final TokenDto tokenDto = keycloakGateway.obtainUserToken(auth.getName(), auth.getCredentials().toString()); final UserDetails userDetails = authTokenFilter.verifyJwt(tokenDto.getAccessToken()); return new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); - } catch (ServletException | RemoteUnavailableException | ServiceException e) { + } catch (ServletException | CredentialsInvalidException | AccountNotSetupException | + AuthServiceConnectionException e) { throw new BadCredentialsException("Failed to authenticate with authentication service", e); } } diff --git a/dbrepo-data-service/services/src/main/java/at/tuwien/auth/InternalRequestInterceptor.java b/dbrepo-data-service/services/src/main/java/at/tuwien/auth/InternalRequestInterceptor.java new file mode 100644 index 0000000000..20955bcdb3 --- /dev/null +++ b/dbrepo-data-service/services/src/main/java/at/tuwien/auth/InternalRequestInterceptor.java @@ -0,0 +1,45 @@ +package at.tuwien.auth; + +import at.tuwien.api.keycloak.TokenDto; +import at.tuwien.config.GatewayConfig; +import at.tuwien.exception.*; +import at.tuwien.gateway.KeycloakGateway; +import lombok.extern.log4j.Log4j2; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpRequest; +import org.springframework.http.MediaType; +import org.springframework.http.client.ClientHttpRequestExecution; +import org.springframework.http.client.ClientHttpRequestInterceptor; +import org.springframework.http.client.ClientHttpResponse; + +import java.io.IOException; +import java.util.List; + +@Log4j2 +public class InternalRequestInterceptor implements ClientHttpRequestInterceptor { + + private final GatewayConfig gatewayConfig; + private final KeycloakGateway keycloakGateway; + + public InternalRequestInterceptor(GatewayConfig gatewayConfig, KeycloakGateway keycloakGateway) { + this.gatewayConfig = gatewayConfig; + this.keycloakGateway = keycloakGateway; + } + + @Override + public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) + throws IOException { + final HttpHeaders headers = request.getHeaders(); + headers.setAccept(List.of(MediaType.APPLICATION_JSON)); + try { + final TokenDto token = keycloakGateway.obtainUserToken(gatewayConfig.getSystemUsername(), + gatewayConfig.getSystemPassword()); + headers.setBearerAuth(token.getAccessToken()); + log.trace("set bearer token for internal user: {}", gatewayConfig.getSystemUsername()); + } catch (AuthServiceConnectionException | CredentialsInvalidException | AccountNotSetupException e) { + log.error("Failed to obtain token for internal user: {}", gatewayConfig.getSystemUsername()); + throw new IOException("Failed to obtain token for internal user", e); + } + return execution.execute(request, body); + } +} diff --git a/dbrepo-data-service/services/src/main/java/at/tuwien/config/GatewayConfig.java b/dbrepo-data-service/services/src/main/java/at/tuwien/config/GatewayConfig.java index b04aff18ce..6ebd674457 100644 --- a/dbrepo-data-service/services/src/main/java/at/tuwien/config/GatewayConfig.java +++ b/dbrepo-data-service/services/src/main/java/at/tuwien/config/GatewayConfig.java @@ -1,19 +1,16 @@ package at.tuwien.config; +import at.tuwien.auth.InternalRequestInterceptor; +import at.tuwien.gateway.KeycloakGateway; import lombok.Getter; import lombok.extern.log4j.Log4j2; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.http.HttpHeaders; -import org.springframework.http.MediaType; -import org.springframework.http.client.ClientHttpRequestInterceptor; -import org.springframework.http.client.support.BasicAuthenticationInterceptor; import org.springframework.web.client.RestTemplate; import org.springframework.web.util.DefaultUriBuilderFactory; -import java.util.List; - @Log4j2 @Getter @Configuration @@ -22,30 +19,26 @@ public class GatewayConfig { @Value("${dbrepo.endpoints.metadataService}") private String metadataEndpoint; - @Value("${dbrepo.admin.username}") - private String adminUsername; + @Value("${dbrepo.system.username}") + private String systemUsername; + + @Value("${dbrepo.system.password}") + private String systemPassword; - @Value("${dbrepo.admin.password}") - private String adminPassword; + private final KeycloakGateway keycloakGateway; + + @Autowired + public GatewayConfig(KeycloakGateway keycloakGateway) { + this.keycloakGateway = keycloakGateway; + } @Bean public RestTemplate restTemplate() { final RestTemplate restTemplate = new RestTemplate(); restTemplate.setUriTemplateHandler(new DefaultUriBuilderFactory(metadataEndpoint)); - log.debug("add basic authentication for metadata service: username={}, password=(hidden)", adminUsername); restTemplate.getInterceptors() - .addAll(List.of(new BasicAuthenticationInterceptor(adminUsername, adminPassword), - clientHttpRequestInterceptor())); + .add(new InternalRequestInterceptor(this, keycloakGateway)); return restTemplate; } - @Bean - public ClientHttpRequestInterceptor clientHttpRequestInterceptor() { - return (request, body, execution) -> { - final HttpHeaders headers = request.getHeaders(); - headers.setAccept(List.of(MediaType.APPLICATION_JSON)); - return execution.execute(request, body); - }; - } - } diff --git a/dbrepo-data-service/services/src/main/java/at/tuwien/config/KeycloakConfig.java b/dbrepo-data-service/services/src/main/java/at/tuwien/config/KeycloakConfig.java index 4d258d496a..e0d7d03215 100644 --- a/dbrepo-data-service/services/src/main/java/at/tuwien/config/KeycloakConfig.java +++ b/dbrepo-data-service/services/src/main/java/at/tuwien/config/KeycloakConfig.java @@ -2,16 +2,12 @@ package at.tuwien.config; import at.tuwien.interceptor.KeycloakInterceptor; import lombok.Getter; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.http.client.ClientHttpRequestInterceptor; import org.springframework.web.client.RestTemplate; import org.springframework.web.util.DefaultUriBuilderFactory; -import java.util.List; - @Getter @Configuration public class KeycloakConfig { @@ -31,20 +27,12 @@ public class KeycloakConfig { @Value("${dbrepo.keycloak.clientSecret}") private String keycloakClientSecret; - private final ClientHttpRequestInterceptor clientHttpRequestInterceptor; - - @Autowired - public KeycloakConfig(ClientHttpRequestInterceptor clientHttpRequestInterceptor) { - this.clientHttpRequestInterceptor = clientHttpRequestInterceptor; - } - @Bean("keycloakRestTemplate") public RestTemplate brokerRestTemplate() { final RestTemplate restTemplate = new RestTemplate(); restTemplate.setUriTemplateHandler(new DefaultUriBuilderFactory(keycloakEndpoint)); restTemplate.getInterceptors() - .addAll(List.of(new KeycloakInterceptor(keycloakUsername, keycloakPassword, keycloakEndpoint), - clientHttpRequestInterceptor)); + .add(new KeycloakInterceptor(keycloakUsername, keycloakPassword, keycloakEndpoint)); return restTemplate; } } diff --git a/dbrepo-data-service/services/src/main/java/at/tuwien/config/WebSecurityConfig.java b/dbrepo-data-service/services/src/main/java/at/tuwien/config/WebSecurityConfig.java index 2aa39d5d43..1560c14b7a 100644 --- a/dbrepo-data-service/services/src/main/java/at/tuwien/config/WebSecurityConfig.java +++ b/dbrepo-data-service/services/src/main/java/at/tuwien/config/WebSecurityConfig.java @@ -43,8 +43,7 @@ public class WebSecurityConfig { } @Bean - public SecurityFilterChain filterChain(HttpSecurity http, KeycloakGateway keycloakGateway, - GatewayConfig gatewayConfig) + public SecurityFilterChain filterChain(HttpSecurity http, KeycloakGateway keycloakGateway) throws Exception { final OrRequestMatcher internalEndpoints = new OrRequestMatcher( new AntPathRequestMatcher("/actuator/**", "GET"), @@ -86,8 +85,8 @@ public class WebSecurityConfig { http.addFilterBefore(authTokenFilter(), UsernamePasswordAuthenticationFilter.class ); - http.addFilterBefore(new BasicAuthenticationFilter(new BasicAuthenticationProvider(gatewayConfig, - authTokenFilter(), keycloakGateway)), + http.addFilterBefore(new BasicAuthenticationFilter(new BasicAuthenticationProvider(authTokenFilter(), + keycloakGateway)), UsernamePasswordAuthenticationFilter.class ); return http.build(); diff --git a/dbrepo-data-service/services/src/main/java/at/tuwien/exception/ContainerNotFoundException.java b/dbrepo-data-service/services/src/main/java/at/tuwien/exception/ContainerNotFoundException.java deleted file mode 100644 index 8d3b2b2243..0000000000 --- a/dbrepo-data-service/services/src/main/java/at/tuwien/exception/ContainerNotFoundException.java +++ /dev/null @@ -1,21 +0,0 @@ -package at.tuwien.exception; - -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.ResponseStatus; - -@ResponseStatus(code = HttpStatus.NOT_FOUND, reason = "error.container.missing") -public class ContainerNotFoundException extends Exception { - - public ContainerNotFoundException(String message) { - super(message); - } - - public ContainerNotFoundException(String message, Throwable thr) { - super(message, thr); - } - - public ContainerNotFoundException(Throwable thr) { - super(thr); - } - -} diff --git a/dbrepo-data-service/services/src/main/java/at/tuwien/exception/DatabaseNotFoundException.java b/dbrepo-data-service/services/src/main/java/at/tuwien/exception/DatabaseNotFoundException.java deleted file mode 100644 index ff4ce77cf2..0000000000 --- a/dbrepo-data-service/services/src/main/java/at/tuwien/exception/DatabaseNotFoundException.java +++ /dev/null @@ -1,21 +0,0 @@ -package at.tuwien.exception; - -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.ResponseStatus; - -@ResponseStatus(code = HttpStatus.NOT_FOUND, reason = "error.database.missing") -public class DatabaseNotFoundException extends Exception { - - public DatabaseNotFoundException(String message) { - super(message); - } - - public DatabaseNotFoundException(String message, Throwable thr) { - super(message, thr); - } - - public DatabaseNotFoundException(Throwable thr) { - super(thr); - } - -} diff --git a/dbrepo-data-service/services/src/main/java/at/tuwien/exception/FormatNotAvailableException.java b/dbrepo-data-service/services/src/main/java/at/tuwien/exception/FormatNotAvailableException.java deleted file mode 100644 index d46b7b2baa..0000000000 --- a/dbrepo-data-service/services/src/main/java/at/tuwien/exception/FormatNotAvailableException.java +++ /dev/null @@ -1,23 +0,0 @@ -package at.tuwien.exception; - -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.ResponseStatus; - -import java.io.IOException; - -@ResponseStatus(code = HttpStatus.NOT_ACCEPTABLE, reason = "error.subset.format") -public class FormatNotAvailableException extends IOException { - - public FormatNotAvailableException(String msg) { - super(msg); - } - - public FormatNotAvailableException(String msg, Throwable thr) { - super(msg + ": " + thr.getLocalizedMessage(), thr); - } - - public FormatNotAvailableException(Throwable thr) { - super(thr); - } - -} diff --git a/dbrepo-data-service/services/src/main/java/at/tuwien/exception/NotAllowedException.java b/dbrepo-data-service/services/src/main/java/at/tuwien/exception/NotAllowedException.java deleted file mode 100644 index 33b2f7f9e3..0000000000 --- a/dbrepo-data-service/services/src/main/java/at/tuwien/exception/NotAllowedException.java +++ /dev/null @@ -1,21 +0,0 @@ -package at.tuwien.exception; - -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.ResponseStatus; - -@ResponseStatus(code = HttpStatus.FORBIDDEN, reason = "error.request.forbidden") -public class NotAllowedException extends Exception { - - public NotAllowedException(String message) { - super(message); - } - - public NotAllowedException(String message, Throwable thr) { - super(message, thr); - } - - public NotAllowedException(Throwable thr) { - super(thr); - } - -} diff --git a/dbrepo-data-service/services/src/main/java/at/tuwien/exception/PaginationException.java b/dbrepo-data-service/services/src/main/java/at/tuwien/exception/PaginationException.java deleted file mode 100644 index 53446bdb64..0000000000 --- a/dbrepo-data-service/services/src/main/java/at/tuwien/exception/PaginationException.java +++ /dev/null @@ -1,22 +0,0 @@ -package at.tuwien.exception; - -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.ResponseStatus; - -@ResponseStatus(code = HttpStatus.BAD_REQUEST, reason = "error.pagination.malformed") -public class PaginationException extends Exception { - - public PaginationException(String msg) { - super(msg); - } - - public PaginationException(String msg, Throwable thr) { - super(msg + ": " + thr.getLocalizedMessage(), thr); - } - - public PaginationException(Throwable thr) { - super(thr); - } - -} - diff --git a/dbrepo-data-service/services/src/main/java/at/tuwien/exception/QueryNotFoundException.java b/dbrepo-data-service/services/src/main/java/at/tuwien/exception/QueryNotFoundException.java deleted file mode 100644 index d55be584cf..0000000000 --- a/dbrepo-data-service/services/src/main/java/at/tuwien/exception/QueryNotFoundException.java +++ /dev/null @@ -1,21 +0,0 @@ -package at.tuwien.exception; - -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.ResponseStatus; - -@ResponseStatus(code = HttpStatus.NOT_FOUND, reason = "error.query.missing") -public class QueryNotFoundException extends Exception { - - public QueryNotFoundException(String message) { - super(message); - } - - public QueryNotFoundException(String message, Throwable thr) { - super(message, thr); - } - - public QueryNotFoundException(Throwable thr) { - super(thr); - } - -} diff --git a/dbrepo-data-service/services/src/main/java/at/tuwien/exception/StorageNotFoundException.java b/dbrepo-data-service/services/src/main/java/at/tuwien/exception/StorageNotFoundException.java deleted file mode 100644 index bbb780ea91..0000000000 --- a/dbrepo-data-service/services/src/main/java/at/tuwien/exception/StorageNotFoundException.java +++ /dev/null @@ -1,21 +0,0 @@ -package at.tuwien.exception; - -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.ResponseStatus; - -@ResponseStatus(code = HttpStatus.NOT_FOUND, reason = "error.storage.missing") -public class StorageNotFoundException extends Exception { - - public StorageNotFoundException(String message) { - super(message); - } - - public StorageNotFoundException(String message, Throwable thr) { - super(message, thr); - } - - public StorageNotFoundException(Throwable thr) { - super(thr); - } - -} diff --git a/dbrepo-data-service/services/src/main/java/at/tuwien/exception/TableExistsException.java b/dbrepo-data-service/services/src/main/java/at/tuwien/exception/TableExistsException.java deleted file mode 100644 index fdc23ad7d3..0000000000 --- a/dbrepo-data-service/services/src/main/java/at/tuwien/exception/TableExistsException.java +++ /dev/null @@ -1,21 +0,0 @@ -package at.tuwien.exception; - -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.ResponseStatus; - -@ResponseStatus(code = HttpStatus.CONFLICT, reason = "error.table.exists") -public class TableExistsException extends Exception { - - public TableExistsException(String message) { - super(message); - } - - public TableExistsException(String message, Throwable thr) { - super(message, thr); - } - - public TableExistsException(Throwable thr) { - super(thr); - } - -} diff --git a/dbrepo-data-service/services/src/main/java/at/tuwien/exception/TableNotFoundException.java b/dbrepo-data-service/services/src/main/java/at/tuwien/exception/TableNotFoundException.java deleted file mode 100644 index 199ce9c74c..0000000000 --- a/dbrepo-data-service/services/src/main/java/at/tuwien/exception/TableNotFoundException.java +++ /dev/null @@ -1,21 +0,0 @@ -package at.tuwien.exception; - -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.ResponseStatus; - -@ResponseStatus(code = HttpStatus.NOT_FOUND, reason = "error.table.missing") -public class TableNotFoundException extends Exception { - - public TableNotFoundException(String message) { - super(message); - } - - public TableNotFoundException(String message, Throwable thr) { - super(message, thr); - } - - public TableNotFoundException(Throwable thr) { - super(thr); - } - -} diff --git a/dbrepo-data-service/services/src/main/java/at/tuwien/exception/UserNotFoundException.java b/dbrepo-data-service/services/src/main/java/at/tuwien/exception/UserNotFoundException.java deleted file mode 100644 index 5aeabab27d..0000000000 --- a/dbrepo-data-service/services/src/main/java/at/tuwien/exception/UserNotFoundException.java +++ /dev/null @@ -1,21 +0,0 @@ -package at.tuwien.exception; - -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.ResponseStatus; - -@ResponseStatus(code = HttpStatus.NOT_FOUND, reason = "error.user.missing") -public class UserNotFoundException extends Exception { - - public UserNotFoundException(String message) { - super(message); - } - - public UserNotFoundException(String message, Throwable thr) { - super(message, thr); - } - - public UserNotFoundException(Throwable thr) { - super(thr); - } - -} diff --git a/dbrepo-data-service/services/src/main/java/at/tuwien/exception/ViewNotFoundException.java b/dbrepo-data-service/services/src/main/java/at/tuwien/exception/ViewNotFoundException.java deleted file mode 100644 index 7ba64c5e8f..0000000000 --- a/dbrepo-data-service/services/src/main/java/at/tuwien/exception/ViewNotFoundException.java +++ /dev/null @@ -1,21 +0,0 @@ -package at.tuwien.exception; - -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.ResponseStatus; - -@ResponseStatus(code = HttpStatus.NOT_FOUND, reason = "error.view.missing") -public class ViewNotFoundException extends Exception { - - public ViewNotFoundException(String message) { - super(message); - } - - public ViewNotFoundException(String message, Throwable thr) { - super(message, thr); - } - - public ViewNotFoundException(Throwable thr) { - super(thr); - } - -} diff --git a/dbrepo-data-service/services/src/main/java/at/tuwien/gateway/DataDatabaseSidecarGateway.java b/dbrepo-data-service/services/src/main/java/at/tuwien/gateway/DataDatabaseSidecarGateway.java index ecac6865f6..23dc08a55d 100644 --- a/dbrepo-data-service/services/src/main/java/at/tuwien/gateway/DataDatabaseSidecarGateway.java +++ b/dbrepo-data-service/services/src/main/java/at/tuwien/gateway/DataDatabaseSidecarGateway.java @@ -3,9 +3,28 @@ package at.tuwien.gateway; import at.tuwien.exception.*; public interface DataDatabaseSidecarGateway { + + /** + * Imports a given dataset name into the given database. + * @param hostname The database hostname. + * @param port The database port. + * @param filename The dataset name. + * @throws StorageNotFoundException The dataset name was not found in the storage service. + * @throws RemoteUnavailableException Connection to the sidecar could not be established. + * @throws SidecarImportException The sidecar failed to import the dataset. + */ void importFile(String hostname, Integer port, String filename) throws StorageNotFoundException, - RemoteUnavailableException, ServiceException; + RemoteUnavailableException, SidecarImportException; + /** + * Exports a given dataset name from the given database. + * @param hostname The database hostname. + * @param port The database port. + * @param filename The dataset name. + * @throws StorageNotFoundException The dataset name was not found in the storage service. + * @throws RemoteUnavailableException Connection to the sidecar could not be established. + * @throws SidecarExportException The sidecar failed to export the dataset. + */ void exportFile(String hostname, Integer port, String filename) throws StorageNotFoundException, - ServiceException, RemoteUnavailableException; + SidecarExportException, RemoteUnavailableException; } diff --git a/dbrepo-data-service/services/src/main/java/at/tuwien/gateway/KeycloakGateway.java b/dbrepo-data-service/services/src/main/java/at/tuwien/gateway/KeycloakGateway.java index 1058119a25..9e6a5f56bd 100644 --- a/dbrepo-data-service/services/src/main/java/at/tuwien/gateway/KeycloakGateway.java +++ b/dbrepo-data-service/services/src/main/java/at/tuwien/gateway/KeycloakGateway.java @@ -1,14 +1,13 @@ package at.tuwien.gateway; import at.tuwien.api.keycloak.TokenDto; -import at.tuwien.exception.RemoteUnavailableException; -import at.tuwien.exception.ServiceConnectionException; -import at.tuwien.exception.ServiceException; - -import javax.naming.ServiceUnavailableException; +import at.tuwien.exception.AccountNotSetupException; +import at.tuwien.exception.AuthServiceConnectionException; +import at.tuwien.exception.CredentialsInvalidException; public interface KeycloakGateway { - TokenDto obtainUserToken(String username, String password) throws RemoteUnavailableException, ServiceException; + TokenDto obtainUserToken(String username, String password) throws AuthServiceConnectionException, + CredentialsInvalidException, AccountNotSetupException; } diff --git a/dbrepo-data-service/services/src/main/java/at/tuwien/gateway/MetadataServiceGateway.java b/dbrepo-data-service/services/src/main/java/at/tuwien/gateway/MetadataServiceGateway.java index 4c01a40a44..d16c8c8eba 100644 --- a/dbrepo-data-service/services/src/main/java/at/tuwien/gateway/MetadataServiceGateway.java +++ b/dbrepo-data-service/services/src/main/java/at/tuwien/gateway/MetadataServiceGateway.java @@ -24,10 +24,10 @@ public interface MetadataServiceGateway { * @return The container with privileged connection information, if successful. * @throws ContainerNotFoundException The table was not found in the metadata service. * @throws RemoteUnavailableException The remote service is not available. - * @throws ServiceException The remote service returned invalid data. + * @throws MetadataServiceException The remote service returned invalid data. */ PrivilegedContainerDto getContainerById(Long containerId) throws RemoteUnavailableException, - ContainerNotFoundException, ServiceException; + ContainerNotFoundException, MetadataServiceException; /** * Get a database with given id from the metadata service. @@ -36,10 +36,10 @@ public interface MetadataServiceGateway { * @return The database, if successful. * @throws DatabaseNotFoundException The database was not found in the metadata service. * @throws RemoteUnavailableException The remote service is not available. - * @throws ServiceException The remote service returned invalid data. + * @throws MetadataServiceException The remote service returned invalid data. */ PrivilegedDatabaseDto getDatabaseById(Long id) throws DatabaseNotFoundException, RemoteUnavailableException, - ServiceException; + MetadataServiceException; /** * Get a database with given internal name from the metadata service. @@ -48,10 +48,10 @@ public interface MetadataServiceGateway { * @return The database, if successful. * @throws DatabaseNotFoundException The database was not found in the metadata service. * @throws RemoteUnavailableException The remote service is not available. - * @throws ServiceException The remote service returned invalid data. + * @throws MetadataServiceException The remote service returned invalid data. */ PrivilegedDatabaseDto getDatabaseByInternalName(String internalName) throws DatabaseNotFoundException, - RemoteUnavailableException, ServiceException; + RemoteUnavailableException, MetadataServiceException; /** * Get a table with given database id and table id from the metadata service. @@ -61,10 +61,10 @@ public interface MetadataServiceGateway { * @return The table, if successful. * @throws TableNotFoundException The table was not found in the metadata service. * @throws RemoteUnavailableException The remote service is not available. - * @throws ServiceException The remote service returned invalid data. + * @throws MetadataServiceException The remote service returned invalid data. */ PrivilegedTableDto getTableById(Long databaseId, Long id) throws TableNotFoundException, RemoteUnavailableException, - ServiceException; + MetadataServiceException; /** * Get a view with given database id and view id from the metadata service. @@ -73,10 +73,10 @@ public interface MetadataServiceGateway { * @return The view, if successful. * @throws ViewNotFoundException The view was not found in the metadata service. * @throws RemoteUnavailableException The remote service is not available. - * @throws ServiceException The remote service returned invalid data. + * @throws MetadataServiceException The remote service returned invalid data. */ PrivilegedViewDto getViewById(Long databaseId, Long id) throws RemoteUnavailableException, ViewNotFoundException, - ServiceException; + MetadataServiceException; /** * Get a user with given user id from the metadata service. @@ -85,9 +85,9 @@ public interface MetadataServiceGateway { * @return The user, if successful. * @throws RemoteUnavailableException The remote service is not available and invalid data was returned. * @throws UserNotFoundException The user was not found in the metadata service. - * @throws ServiceException The remote service returned invalid data. + * @throws MetadataServiceException The remote service returned invalid data. */ - UserDto getUserById(UUID userId) throws RemoteUnavailableException, UserNotFoundException, ServiceException; + UserDto getUserById(UUID userId) throws RemoteUnavailableException, UserNotFoundException, MetadataServiceException; /** * Get a user with given user id from the metadata service. @@ -96,10 +96,10 @@ public interface MetadataServiceGateway { * @return The user, if successful. * @throws RemoteUnavailableException The remote service is not available and invalid data was returned. * @throws UserNotFoundException The user was not found in the metadata service. - * @throws ServiceException The remote service returned invalid data. + * @throws MetadataServiceException The remote service returned invalid data. */ PrivilegedUserDto getPrivilegedUserById(UUID userId) throws RemoteUnavailableException, UserNotFoundException, - ServiceException; + MetadataServiceException; /** * Get database access for a given user and database id from the metadata service. @@ -108,10 +108,10 @@ public interface MetadataServiceGateway { * @return The database access, if successful. * @throws RemoteUnavailableException The remote service is not available and invalid data was returned. * @throws NotAllowedException The access to this database is denied for the given user. - * @throws ServiceException The remote service returned invalid data. + * @throws MetadataServiceException The remote service returned invalid data. */ DatabaseAccessDto getAccess(Long databaseId, UUID userId) throws RemoteUnavailableException, NotAllowedException, - ServiceException; + MetadataServiceException; /** * Get a list of identifiers for a given database id and optional subset id. @@ -120,9 +120,9 @@ public interface MetadataServiceGateway { * @return The list of identifiers. * @throws RemoteUnavailableException The remote service is not available and invalid data was returned. * @throws DatabaseNotFoundException The database was not found. - * @throws ServiceException The remote service returned invalid data. + * @throws MetadataServiceException The remote service returned invalid data. */ - List<IdentifierDto> getIdentifiers(@NotNull Long databaseId, Long subsetId) throws ServiceException, + List<IdentifierDto> getIdentifiers(@NotNull Long databaseId, Long subsetId) throws MetadataServiceException, RemoteUnavailableException, DatabaseNotFoundException; /** @@ -131,7 +131,7 @@ public interface MetadataServiceGateway { * @param tableId The table id. * @throws RemoteUnavailableException The remote service is not available and invalid data was returned. * @throws TableNotFoundException The table was not found. - * @throws ServiceException The remote service returned invalid data. + * @throws MetadataServiceException The remote service returned invalid data. */ - void updateTableStatistics(Long databaseId, Long tableId) throws TableNotFoundException, ServiceException, RemoteUnavailableException; + void updateTableStatistics(Long databaseId, Long tableId) throws TableNotFoundException, MetadataServiceException, RemoteUnavailableException; } diff --git a/dbrepo-data-service/services/src/main/java/at/tuwien/gateway/impl/DataDatabaseSidecarGatewayImpl.java b/dbrepo-data-service/services/src/main/java/at/tuwien/gateway/impl/DataDatabaseSidecarGatewayImpl.java index b3e7c3bd41..841aace474 100644 --- a/dbrepo-data-service/services/src/main/java/at/tuwien/gateway/impl/DataDatabaseSidecarGatewayImpl.java +++ b/dbrepo-data-service/services/src/main/java/at/tuwien/gateway/impl/DataDatabaseSidecarGatewayImpl.java @@ -24,13 +24,13 @@ public class DataDatabaseSidecarGatewayImpl implements DataDatabaseSidecarGatewa @Override public void importFile(String hostname, Integer port, String filename) throws StorageNotFoundException, - RemoteUnavailableException, ServiceException { + RemoteUnavailableException, SidecarImportException { final ResponseEntity<Void> response; final String url = "http://" + hostname + ":" + port + "/sidecar/import/" + filename; log.debug("import file into data database sidecar"); try { response = restTemplate.exchange(url, HttpMethod.POST, new HttpEntity<>(null), Void.class); - } catch (ResourceAccessException | HttpServerErrorException e) { + } catch (HttpServerErrorException e) { log.error("Failed to import dataset with filename: {}: {}", filename, e.getMessage()); throw new RemoteUnavailableException("Failed to import dataset: " + e.getMessage(), e); } catch (HttpClientErrorException.BadRequest e) { @@ -39,19 +39,19 @@ public class DataDatabaseSidecarGatewayImpl implements DataDatabaseSidecarGatewa } if (!response.getStatusCode().equals(HttpStatus.ACCEPTED)) { log.error("Failed to import dataset with filename: {}: service responded unsuccessful: {}", filename, response.getStatusCode()); - throw new ServiceException("Failed to import dataset: service responded unsuccessful: " + response.getStatusCode()); + throw new SidecarImportException("Failed to import dataset: service responded unsuccessful: " + response.getStatusCode()); } } @Override public void exportFile(String hostname, Integer port, String filename) throws StorageNotFoundException, - RemoteUnavailableException, ServiceException { + RemoteUnavailableException, SidecarExportException { final ResponseEntity<Void> response; final String url = "http://" + hostname + ":" + port + "/sidecar/export/" + filename; log.debug("export file from data database sidecar: {}", url); try { response = restTemplate.exchange(url, HttpMethod.POST, new HttpEntity<>(null), Void.class); - } catch (ResourceAccessException | HttpServerErrorException e) { + } catch (HttpServerErrorException e) { log.error("Failed to export dataset with filename: {}: {}", filename, e.getMessage()); throw new RemoteUnavailableException("Failed to export dataset: " + e.getMessage(), e); } catch (HttpClientErrorException.BadRequest e) { @@ -60,7 +60,7 @@ public class DataDatabaseSidecarGatewayImpl implements DataDatabaseSidecarGatewa } if (!response.getStatusCode().equals(HttpStatus.ACCEPTED)) { log.error("Failed to export dataset with filename: {}: service responded unsuccessful: {}", filename, response.getStatusCode()); - throw new ServiceException("Failed to export dataset: service responded unsuccessful: " + response.getStatusCode()); + throw new SidecarExportException("Failed to export dataset: service responded unsuccessful: " + response.getStatusCode()); } } } diff --git a/dbrepo-data-service/services/src/main/java/at/tuwien/gateway/impl/KeycloakGatewayImpl.java b/dbrepo-data-service/services/src/main/java/at/tuwien/gateway/impl/KeycloakGatewayImpl.java index 545e259097..f95bb3fe76 100644 --- a/dbrepo-data-service/services/src/main/java/at/tuwien/gateway/impl/KeycloakGatewayImpl.java +++ b/dbrepo-data-service/services/src/main/java/at/tuwien/gateway/impl/KeycloakGatewayImpl.java @@ -1,61 +1,69 @@ package at.tuwien.gateway.impl; +import at.tuwien.api.auth.KeycloakErrorDto; import at.tuwien.api.keycloak.TokenDto; import at.tuwien.config.KeycloakConfig; -import at.tuwien.exception.RemoteUnavailableException; -import at.tuwien.exception.ServiceConnectionException; -import at.tuwien.exception.ServiceException; +import at.tuwien.exception.*; import at.tuwien.gateway.KeycloakGateway; import lombok.extern.log4j.Log4j2; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.http.*; import org.springframework.stereotype.Service; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; +import org.springframework.web.client.HttpClientErrorException; import org.springframework.web.client.HttpServerErrorException; -import org.springframework.web.client.ResourceAccessException; import org.springframework.web.client.RestTemplate; @Log4j2 @Service public class KeycloakGatewayImpl implements KeycloakGateway { - private final RestTemplate restTemplate; private final KeycloakConfig keycloakConfig; @Autowired - public KeycloakGatewayImpl(RestTemplate restTemplate, KeycloakConfig keycloakConfig) { - this.restTemplate = restTemplate; + public KeycloakGatewayImpl(KeycloakConfig keycloakConfig) { this.keycloakConfig = keycloakConfig; } @Override - public TokenDto obtainUserToken(String username, String password) throws RemoteUnavailableException, ServiceException { + public TokenDto obtainUserToken(String username, String password) throws AuthServiceConnectionException, + CredentialsInvalidException, AccountNotSetupException { final HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); final MultiValueMap<String, String> payload = new LinkedMultiValueMap<>(); payload.add("username", username); payload.add("password", password); payload.add("grant_type", "password"); - payload.add("scope", "openid roles attributes"); + payload.add("scope", "openid roles"); payload.add("client_id", keycloakConfig.getKeycloakClient()); payload.add("client_secret", keycloakConfig.getKeycloakClientSecret()); final String url = keycloakConfig.getKeycloakEndpoint() + "/realms/dbrepo/protocol/openid-connect/token"; - log.debug("request user token from url {}", url); + log.trace("request user token from url: {}", url); + log.trace("request username: {}", username); + log.trace("request password: {}", password != null ? "(set)" : "(not set)"); + log.trace("request client_id: {}", keycloakConfig.getKeycloakClient()); + log.trace("request client_secret: {}", keycloakConfig.getKeycloakClientSecret()); final ResponseEntity<TokenDto> response; try { - response = restTemplate.exchange(url, HttpMethod.POST, new HttpEntity<>(payload, headers), TokenDto.class); + response = new RestTemplate() + .exchange(url, HttpMethod.POST, new HttpEntity<>(payload, headers), TokenDto.class); } catch (HttpServerErrorException e) { log.error("Failed to obtain user token: {}", e.getMessage()); - throw new RemoteUnavailableException("Failed to obtain user token: " + e.getMessage(), e); - } catch (Exception e) { - log.error("Failed to obtain user token: unexpected response: {}", e.getMessage(), e); - throw new ServiceException("Failed to obtain user token: unexpected response: " + e.getMessage(), e); - } - if (!response.getStatusCode().equals(HttpStatus.OK)) { - log.error("Failed to obtain user token: service responded unsuccessful: {}", response.getStatusCode()); - throw new ServiceException("obtain user token: service responded unsuccessful: " + response.getStatusCode()); + throw new AuthServiceConnectionException("Service unavailable", e); + } catch (HttpClientErrorException.BadRequest e) { + if (e.getResponseBodyAsByteArray() != null && e.getResponseBodyAsByteArray().length > 0) { + final KeycloakErrorDto error = e.getResponseBodyAs(KeycloakErrorDto.class); + if (error != null && error.getError().equals("invalid_grant")) { + log.error("Failed to obtain user token: {}", error.getErrorDescription()); + throw new AccountNotSetupException(error.getErrorDescription()); + } + } + log.error("Failed to obtain user token: bad request"); + throw new CredentialsInvalidException("Bad request", e); + } catch (HttpClientErrorException.Unauthorized e) { + log.error("Failed to obtain user token: invalid credentials"); + throw new CredentialsInvalidException("Invalid credentials", e); } return response.getBody(); } diff --git a/dbrepo-data-service/services/src/main/java/at/tuwien/gateway/impl/MetadataServiceGatewayImpl.java b/dbrepo-data-service/services/src/main/java/at/tuwien/gateway/impl/MetadataServiceGatewayImpl.java index 1fcf3e50ee..a3fda6482c 100644 --- a/dbrepo-data-service/services/src/main/java/at/tuwien/gateway/impl/MetadataServiceGatewayImpl.java +++ b/dbrepo-data-service/services/src/main/java/at/tuwien/gateway/impl/MetadataServiceGatewayImpl.java @@ -46,7 +46,7 @@ public class MetadataServiceGatewayImpl implements MetadataServiceGateway { @Override public PrivilegedContainerDto getContainerById(Long containerId) throws RemoteUnavailableException, - ContainerNotFoundException, ServiceException { + ContainerNotFoundException, MetadataServiceException { final ResponseEntity<ContainerDto> response; try { response = restTemplate.exchange("/api/container/" + containerId, HttpMethod.GET, HttpEntity.EMPTY, @@ -60,15 +60,15 @@ public class MetadataServiceGatewayImpl implements MetadataServiceGateway { } if (response.getStatusCode() != HttpStatus.OK) { log.error("Failed to find container with id {}: service responded unsuccessful: {}", containerId, response.getStatusCode()); - throw new ServiceException("Failed to find container: service responded unsuccessful: " + response.getStatusCode()); + throw new MetadataServiceException("Failed to find container: service responded unsuccessful: " + response.getStatusCode()); } if (!response.getHeaders().keySet().containsAll(List.of("X-Username", "X-Password"))) { log.error("Failed to find all privileged container headers"); - throw new ServiceException("Failed to find all privileged container headers"); + throw new MetadataServiceException("Failed to find all privileged container headers"); } if (response.getBody() == null) { log.error("Failed to find container with id {}: body is empty", containerId); - throw new ServiceException("Failed to find container with id " + containerId + ": body is empty"); + throw new MetadataServiceException("Failed to find container with id " + containerId + ": body is empty"); } final PrivilegedContainerDto container = metadataMapper.containerDtoToPrivilegedContainerDto(response.getBody()); container.setUsername(response.getHeaders().get("X-Username").get(0)); @@ -78,7 +78,7 @@ public class MetadataServiceGatewayImpl implements MetadataServiceGateway { @Override public PrivilegedDatabaseDto getDatabaseById(Long id) throws DatabaseNotFoundException, RemoteUnavailableException, - ServiceException { + MetadataServiceException { final ResponseEntity<PrivilegedDatabaseDto> response; try { response = restTemplate.exchange("/api/database/" + id, HttpMethod.GET, HttpEntity.EMPTY, @@ -92,15 +92,15 @@ public class MetadataServiceGatewayImpl implements MetadataServiceGateway { } if (response.getStatusCode() != HttpStatus.OK) { log.error("Failed to find database with id {}: service responded unsuccessful: {}", id, response.getStatusCode()); - throw new ServiceException("Failed to find database: service responded unsuccessful: " + response.getStatusCode()); + throw new MetadataServiceException("Failed to find database: service responded unsuccessful: " + response.getStatusCode()); } if (!response.getHeaders().keySet().containsAll(List.of("X-Username", "X-Password"))) { log.error("Failed to find all privileged database headers"); - throw new ServiceException("Failed to find all privileged database headers"); + throw new MetadataServiceException("Failed to find all privileged database headers"); } if (response.getBody() == null) { log.error("Failed to find database with id {}: body is empty", id); - throw new ServiceException("Failed to find database with id " + id + ": body is empty"); + throw new MetadataServiceException("Failed to find database with id " + id + ": body is empty"); } final PrivilegedDatabaseDto database = response.getBody(); database.getContainer().setUsername(response.getHeaders().get("X-Username").get(0)); @@ -111,7 +111,7 @@ public class MetadataServiceGatewayImpl implements MetadataServiceGateway { @Override public PrivilegedDatabaseDto getDatabaseByInternalName(String internalName) throws DatabaseNotFoundException, - RemoteUnavailableException, ServiceException { + RemoteUnavailableException, MetadataServiceException { final ResponseEntity<PrivilegedDatabaseDto[]> response; try { response = restTemplate.exchange("/api/database/", HttpMethod.GET, HttpEntity.EMPTY, PrivilegedDatabaseDto[].class); @@ -121,7 +121,7 @@ public class MetadataServiceGatewayImpl implements MetadataServiceGateway { } if (!response.getStatusCode().equals(HttpStatus.OK) || response.getBody() == null) { log.error("Failed to find database with internal name {}: service responded unsuccessful: {}", internalName, response.getStatusCode()); - throw new ServiceException("Failed to find database: service responded unsuccessful: " + response.getStatusCode()); + throw new MetadataServiceException("Failed to find database: service responded unsuccessful: " + response.getStatusCode()); } if (response.getBody().length != 1) { log.error("Failed to find database with internal name {}: body is empty", internalName); @@ -132,7 +132,7 @@ public class MetadataServiceGatewayImpl implements MetadataServiceGateway { @Override public PrivilegedTableDto getTableById(Long databaseId, Long id) throws TableNotFoundException, - RemoteUnavailableException, ServiceException { + RemoteUnavailableException, MetadataServiceException { final ResponseEntity<TableDto> response; try { response = restTemplate.exchange("/api/database/" + databaseId + "/table/" + id, HttpMethod.GET, HttpEntity.EMPTY, TableDto.class); @@ -145,15 +145,15 @@ public class MetadataServiceGatewayImpl implements MetadataServiceGateway { } if (!response.getStatusCode().equals(HttpStatus.OK)) { log.error("Failed to find table with id {}: service responded unsuccessful: {}", id, response.getStatusCode()); - throw new ServiceException("Failed to find table: service responded unsuccessful: " + response.getStatusCode()); + throw new MetadataServiceException("Failed to find table: service responded unsuccessful: " + response.getStatusCode()); } if (!response.getHeaders().keySet().containsAll(List.of("X-Type", "X-Host", "X-Port", "X-Username", "X-Password", "X-Database", "X-Sidecar-Host", "X-Sidecar-Port"))) { log.error("Failed to find all privileged table headers"); - throw new ServiceException("Failed to find all privileged table headers"); + throw new MetadataServiceException("Failed to find all privileged table headers"); } if (response.getBody() == null) { log.error("Failed to find table with id {}: body is empty", id); - throw new ServiceException("Failed to find table with id " + id + ": body is empty"); + throw new MetadataServiceException("Failed to find table with id " + id + ": body is empty"); } final PrivilegedTableDto table = metadataMapper.tableDtoToPrivilegedTableDto(response.getBody()); table.getDatabase().getContainer().getImage().setJdbcMethod(response.getHeaders().get("X-Type").get(0)); @@ -170,7 +170,7 @@ public class MetadataServiceGatewayImpl implements MetadataServiceGateway { @Override public PrivilegedViewDto getViewById(Long databaseId, Long id) throws RemoteUnavailableException, - ViewNotFoundException, ServiceException { + ViewNotFoundException, MetadataServiceException { final ResponseEntity<ViewDto> response; try { response = restTemplate.exchange("/api/database/" + databaseId + "/view/" + id, HttpMethod.GET, HttpEntity.EMPTY, ViewDto.class); @@ -183,15 +183,15 @@ public class MetadataServiceGatewayImpl implements MetadataServiceGateway { } if (!response.getStatusCode().equals(HttpStatus.OK)) { log.error("Failed to find view with id {}: service responded unsuccessful: {}", id, response.getStatusCode()); - throw new ServiceException("Failed to find view: service responded unsuccessful: " + response.getStatusCode()); + throw new MetadataServiceException("Failed to find view: service responded unsuccessful: " + response.getStatusCode()); } if (!response.getHeaders().keySet().containsAll(List.of("X-Type", "X-Host", "X-Port", "X-Username", "X-Password", "X-Database"))) { log.error("Failed to find all privileged view headers"); - throw new ServiceException("Failed to find all privileged view headers"); + throw new MetadataServiceException("Failed to find all privileged view headers"); } if (response.getBody() == null) { log.error("Failed to find view with id {}: body is empty", id); - throw new ServiceException("Failed to find view with id " + id + ": body is empty"); + throw new MetadataServiceException("Failed to find view with id " + id + ": body is empty"); } final PrivilegedViewDto table = metadataMapper.viewDtoToPrivilegedViewDto(response.getBody()); table.getDatabase().getContainer().getImage().setJdbcMethod(response.getHeaders().get("X-Type").get(0)); @@ -205,7 +205,7 @@ public class MetadataServiceGatewayImpl implements MetadataServiceGateway { @Override public UserDto getUserById(UUID userId) throws RemoteUnavailableException, UserNotFoundException, - ServiceException { + MetadataServiceException { final ResponseEntity<UserDto> response; try { response = restTemplate.exchange("/api/user/" + userId, HttpMethod.GET, HttpEntity.EMPTY, UserDto.class); @@ -218,18 +218,18 @@ public class MetadataServiceGatewayImpl implements MetadataServiceGateway { } if (!response.getStatusCode().equals(HttpStatus.OK)) { log.error("Failed to find user with id {}: service responded unsuccessful: {}", userId, response.getStatusCode()); - throw new ServiceException("Failed to find user: service responded unsuccessful: " + response.getStatusCode()); + throw new MetadataServiceException("Failed to find user: service responded unsuccessful: " + response.getStatusCode()); } if (response.getBody() == null) { log.error("Failed to find user with id {}: body is empty", userId); - throw new ServiceException("Failed to find user with id " + userId + ": body is empty"); + throw new MetadataServiceException("Failed to find user with id " + userId + ": body is empty"); } return response.getBody(); } @Override public PrivilegedUserDto getPrivilegedUserById(UUID userId) throws RemoteUnavailableException, UserNotFoundException, - ServiceException { + MetadataServiceException { final ResponseEntity<UserDto> response; try { response = restTemplate.exchange("/api/user/" + userId, HttpMethod.GET, HttpEntity.EMPTY, UserDto.class); @@ -242,15 +242,15 @@ public class MetadataServiceGatewayImpl implements MetadataServiceGateway { } if (!response.getStatusCode().equals(HttpStatus.OK)) { log.error("Failed to find user with id {}: service responded unsuccessful: {}", userId, response.getStatusCode()); - throw new ServiceException("Failed to find user: service responded unsuccessful: " + response.getStatusCode()); + throw new MetadataServiceException("Failed to find user: service responded unsuccessful: " + response.getStatusCode()); } if (!response.getHeaders().keySet().containsAll(List.of("X-Username", "X-Password"))) { log.error("Failed to find all privileged user headers"); - throw new ServiceException("Failed to find all privileged user headers"); + throw new MetadataServiceException("Failed to find all privileged user headers"); } if (response.getBody() == null) { log.error("Failed to find user with id {}: body is empty", userId); - throw new ServiceException("Failed to find user with id " + userId + ": body is empty"); + throw new MetadataServiceException("Failed to find user with id " + userId + ": body is empty"); } final PrivilegedUserDto user = metadataMapper.userDtoToPrivilegedUserDto(response.getBody()); user.setUsername(response.getHeaders().get("X-Username").get(0)); @@ -260,7 +260,7 @@ public class MetadataServiceGatewayImpl implements MetadataServiceGateway { @Override public DatabaseAccessDto getAccess(Long databaseId, UUID userId) throws RemoteUnavailableException, - NotAllowedException, ServiceException { + NotAllowedException, MetadataServiceException { final ResponseEntity<DatabaseAccessDto> response; try { response = restTemplate.exchange("/api/database/" + databaseId + "/access/" + userId, HttpMethod.GET, HttpEntity.EMPTY, DatabaseAccessDto.class); @@ -273,17 +273,17 @@ public class MetadataServiceGatewayImpl implements MetadataServiceGateway { } if (!response.getStatusCode().equals(HttpStatus.OK)) { log.error("Failed to find database access for user with id {}: service responded unsuccessful: {}", userId, response.getStatusCode()); - throw new ServiceException("Failed to find database access: service responded unsuccessful: " + response.getStatusCode()); + throw new MetadataServiceException("Failed to find database access: service responded unsuccessful: " + response.getStatusCode()); } if (response.getBody() == null) { log.error("Failed to find database access: body is empty"); - throw new ServiceException("Failed to find database access: body is empty"); + throw new MetadataServiceException("Failed to find database access: body is empty"); } return response.getBody(); } @Override - public List<IdentifierDto> getIdentifiers(@NotNull Long databaseId, Long subsetId) throws ServiceException, + public List<IdentifierDto> getIdentifiers(@NotNull Long databaseId, Long subsetId) throws MetadataServiceException, RemoteUnavailableException, DatabaseNotFoundException { final ResponseEntity<IdentifierDto[]> response; final String url = "/api/identifier?dbid=" + databaseId + (subsetId != null ? ("&qid=" + subsetId) : ""); @@ -299,17 +299,17 @@ public class MetadataServiceGatewayImpl implements MetadataServiceGateway { } if (!response.getStatusCode().equals(HttpStatus.OK)) { log.error("Failed to find identifiers for database with id {} and subset with id {}: service responded unsuccessful: {}", databaseId, subsetId, response.getStatusCode()); - throw new ServiceException("Failed to find identifiers for database: service responded unsuccessful: " + response.getStatusCode()); + throw new MetadataServiceException("Failed to find identifiers for database: service responded unsuccessful: " + response.getStatusCode()); } if (response.getBody() == null) { log.error("Failed to find identifiers: body is null"); - throw new ServiceException("Failed to find identifiers: body is null"); + throw new MetadataServiceException("Failed to find identifiers: body is null"); } return List.of(response.getBody()); } @Override - public void updateTableStatistics(Long databaseId, Long tableId) throws TableNotFoundException, ServiceException, + public void updateTableStatistics(Long databaseId, Long tableId) throws TableNotFoundException, MetadataServiceException, RemoteUnavailableException { final ResponseEntity<Void> response; final String url = "/api/database/" + databaseId + "/table/" + tableId; @@ -324,7 +324,7 @@ public class MetadataServiceGatewayImpl implements MetadataServiceGateway { } if (!response.getStatusCode().equals(HttpStatus.ACCEPTED)) { log.error("Failed to update table statistic for table with id {}: service responded unsuccessful: {}", tableId, response.getStatusCode()); - throw new ServiceException("Failed to update table statistic for database: service responded unsuccessful: " + response.getStatusCode()); + throw new MetadataServiceException("Failed to update table statistic for database: service responded unsuccessful: " + response.getStatusCode()); } } diff --git a/dbrepo-data-service/services/src/main/java/at/tuwien/listener/DefaultListener.java b/dbrepo-data-service/services/src/main/java/at/tuwien/listener/DefaultListener.java index 192fc30a61..fac47a3d80 100644 --- a/dbrepo-data-service/services/src/main/java/at/tuwien/listener/DefaultListener.java +++ b/dbrepo-data-service/services/src/main/java/at/tuwien/listener/DefaultListener.java @@ -64,7 +64,7 @@ public class DefaultListener implements MessageListener { log.error("Failed to read object: {}", e.getMessage()); } catch (SQLException | RemoteUnavailableException e) { log.error("Failed to insert tuple: {}", e.getMessage()); - } catch (TableNotFoundException | ServiceException e) { + } catch (TableNotFoundException | MetadataServiceException e) { log.error("Failed to find table: {}", e.getMessage()); } } diff --git a/dbrepo-data-service/services/src/main/java/at/tuwien/mapper/MariaDbMapper.java b/dbrepo-data-service/services/src/main/java/at/tuwien/mapper/MariaDbMapper.java index 8cadf146b1..b6b782a04c 100644 --- a/dbrepo-data-service/services/src/main/java/at/tuwien/mapper/MariaDbMapper.java +++ b/dbrepo-data-service/services/src/main/java/at/tuwien/mapper/MariaDbMapper.java @@ -94,6 +94,13 @@ public interface MariaDbMapper { return statement; } + @Named("dropView") + default String dropViewRawQuery(String viewName) { + final String statement = "DROP VIEW IF EXISTS `" + viewName + "`;"; + log.trace("mapped drop view statement: {}", statement); + return statement; + } + default String databaseViewSelectRawQuery() { final String statement = "SELECT t.`TABLE_NAME`, t.`TABLE_TYPE`, t.`TABLE_ROWS`, t.`AVG_ROW_LENGTH`, t.`DATA_LENGTH`, t.`MAX_DATA_LENGTH`, COALESCE(t.`CREATE_TIME`, NOW()) as `CREATE_TIME`, t.`UPDATE_TIME`, v.`VIEW_DEFINITION` FROM information_schema.TABLES t LEFT JOIN information_schema.VIEWS v ON t.`TABLE_NAME` = v.`TABLE_NAME` WHERE t.`TABLE_SCHEMA` = ? AND t.`TABLE_TYPE` = 'VIEW' AND t.`TABLE_NAME` != 'qs_queries' AND t.`TABLE_NAME` = ?"; log.trace("mapped select view statement: {}", statement); @@ -439,9 +446,20 @@ public interface MariaDbMapper { return statement.toString(); } - default String subsetToRawExportQuery(String query, Instant timestamp, String filePath) { - final StringBuilder statement = new StringBuilder(query.replaceAll(";", "")) - .append(" FOR SYSTEM_TIME AS OF TIMESTAMP'") + default String subsetToRawTemporaryViewQuery(String viewName, String query) { + final StringBuilder statement = new StringBuilder("CREATE VIEW `") + .append(viewName) + .append("` AS (") + .append(query) + .append(");"); + log.debug("mapped temporary view query: {}", statement); + return statement.toString(); + } + + default String subsetToRawExportQuery(String viewName, Instant timestamp, String filePath) { + final StringBuilder statement = new StringBuilder("SELECT * FROM `") + .append(viewName) + .append("` FOR SYSTEM_TIME AS OF TIMESTAMP'") .append(mariaDbFormatter.format(timestamp)) .append("'") .append(" INTO OUTFILE '") diff --git a/dbrepo-data-service/services/src/main/java/at/tuwien/service/SubsetService.java b/dbrepo-data-service/services/src/main/java/at/tuwien/service/SubsetService.java index 7c2575b9cc..3c3ff101fe 100644 --- a/dbrepo-data-service/services/src/main/java/at/tuwien/service/SubsetService.java +++ b/dbrepo-data-service/services/src/main/java/at/tuwien/service/SubsetService.java @@ -28,7 +28,9 @@ public interface SubsetService { QueryResultDto execute(PrivilegedDatabaseDto database, String statement, Instant timestamp, UUID userId, Long page, Long size, SortTypeDto sortDirection, String sortColumn) - throws QueryStoreInsertException, SQLException, QueryNotFoundException, TableMalformedException, UserNotFoundException, NotAllowedException, RemoteUnavailableException, ServiceException, DatabaseNotFoundException; + throws QueryStoreInsertException, SQLException, QueryNotFoundException, TableMalformedException, + UserNotFoundException, NotAllowedException, RemoteUnavailableException, DatabaseNotFoundException, + MetadataServiceException; QueryResultDto reExecute(PrivilegedDatabaseDto database, QueryDto query, Long page, Long size, SortTypeDto sortDirection, String sortColumn) throws TableMalformedException, @@ -45,11 +47,12 @@ public interface SubsetService { * @return The list of queries. */ List<QueryDto> findAll(PrivilegedDatabaseDto database, Boolean filterPersisted) throws SQLException, - QueryNotFoundException, NotAllowedException, RemoteUnavailableException, ServiceException, DatabaseNotFoundException; + QueryNotFoundException, NotAllowedException, RemoteUnavailableException, DatabaseNotFoundException, + MetadataServiceException; ExportResourceDto export(PrivilegedDatabaseDto database, QueryDto query, Instant timestamp, String filename) throws SQLException, QueryMalformedException, SidecarExportException, StorageNotFoundException, - StorageUnavailableException, ServiceException, RemoteUnavailableException; + StorageUnavailableException, MetadataServiceException, RemoteUnavailableException; Long executeCountNonPersistent(PrivilegedDatabaseDto database, String statement, Instant timestamp) throws SQLException, QueryMalformedException, TableMalformedException; @@ -62,7 +65,9 @@ public interface SubsetService { * @return The query. * @throws QueryNotFoundException The query store did not return a query */ - QueryDto findById(PrivilegedDatabaseDto database, Long queryId) throws QueryNotFoundException, SQLException, NotAllowedException, RemoteUnavailableException, UserNotFoundException, ServiceException, DatabaseNotFoundException; + QueryDto findById(PrivilegedDatabaseDto database, Long queryId) throws QueryNotFoundException, SQLException, + NotAllowedException, RemoteUnavailableException, UserNotFoundException, DatabaseNotFoundException, + MetadataServiceException; /** * Inserts a query and metadata to the query store of a given database id. diff --git a/dbrepo-data-service/services/src/main/java/at/tuwien/service/TableService.java b/dbrepo-data-service/services/src/main/java/at/tuwien/service/TableService.java index 98ed0ec7ae..50894eb77a 100644 --- a/dbrepo-data-service/services/src/main/java/at/tuwien/service/TableService.java +++ b/dbrepo-data-service/services/src/main/java/at/tuwien/service/TableService.java @@ -105,7 +105,7 @@ public interface TableService { QueryMalformedException; void importDataset(PrivilegedTableDto table, ImportCsvDto data) throws SidecarImportException, - StorageNotFoundException, SQLException, QueryMalformedException, ServiceException, RemoteUnavailableException; + StorageNotFoundException, SQLException, QueryMalformedException, RemoteUnavailableException; void deleteTuple(PrivilegedTableDto table, TupleDeleteDto data) throws SQLException, TableMalformedException, QueryMalformedException; @@ -118,5 +118,5 @@ public interface TableService { ExportResourceDto exportDataset(PrivilegedTableDto table, Instant timestamp) throws SQLException, SidecarExportException, StorageNotFoundException, StorageUnavailableException, - QueryMalformedException, ServiceException, RemoteUnavailableException; + QueryMalformedException, RemoteUnavailableException; } diff --git a/dbrepo-data-service/services/src/main/java/at/tuwien/service/ViewService.java b/dbrepo-data-service/services/src/main/java/at/tuwien/service/ViewService.java index 3455c320cd..5acca0018d 100644 --- a/dbrepo-data-service/services/src/main/java/at/tuwien/service/ViewService.java +++ b/dbrepo-data-service/services/src/main/java/at/tuwien/service/ViewService.java @@ -58,5 +58,5 @@ public interface ViewService { ExportResourceDto exportDataset(PrivilegedDatabaseDto database, ViewDto view, Instant timestamp) throws SQLException, QueryMalformedException, SidecarExportException, StorageNotFoundException, - StorageUnavailableException, ServiceException, RemoteUnavailableException; + StorageUnavailableException, RemoteUnavailableException; } diff --git a/dbrepo-data-service/services/src/main/java/at/tuwien/service/impl/StorageServiceS3Impl.java b/dbrepo-data-service/services/src/main/java/at/tuwien/service/impl/StorageServiceS3Impl.java index b2d3f1b550..c8b49fd4cb 100644 --- a/dbrepo-data-service/services/src/main/java/at/tuwien/service/impl/StorageServiceS3Impl.java +++ b/dbrepo-data-service/services/src/main/java/at/tuwien/service/impl/StorageServiceS3Impl.java @@ -72,9 +72,10 @@ public class StorageServiceS3Impl implements StorageService { @Override public ExportResourceDto getResource(String bucket, String key) throws StorageNotFoundException, StorageUnavailableException { - final InputStream stream = getObject(bucket, key); + final InputStreamResource resource = new InputStreamResource(getObject(bucket, key)); + log.trace("return export resource with filename: {}", key); return ExportResourceDto.builder() - .resource(new InputStreamResource(stream)) + .resource(resource) .filename(key) .build(); } diff --git a/dbrepo-data-service/services/src/main/java/at/tuwien/service/impl/SubsetServiceMariaDbImpl.java b/dbrepo-data-service/services/src/main/java/at/tuwien/service/impl/SubsetServiceMariaDbImpl.java index 2ab2f7b349..3d19276196 100644 --- a/dbrepo-data-service/services/src/main/java/at/tuwien/service/impl/SubsetServiceMariaDbImpl.java +++ b/dbrepo-data-service/services/src/main/java/at/tuwien/service/impl/SubsetServiceMariaDbImpl.java @@ -19,12 +19,16 @@ import at.tuwien.mapper.MariaDbMapper; import at.tuwien.mapper.MetadataMapper; import at.tuwien.service.SubsetService; import at.tuwien.service.StorageService; +import com.google.common.hash.Hashing; import com.mchange.v2.c3p0.ComboPooledDataSource; import lombok.extern.log4j.Log4j2; import net.sf.jsqlparser.JSQLParserException; +import org.apache.commons.lang3.RandomUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import java.sql.*; import java.time.Instant; import java.util.LinkedList; @@ -58,7 +62,8 @@ public class SubsetServiceMariaDbImpl extends HibernateConnector implements Subs } @Override - public void createQueryStore(PrivilegedContainerDto container, String databaseName) throws SQLException, QueryStoreCreateException { + public void createQueryStore(PrivilegedContainerDto container, String databaseName) throws SQLException, + QueryStoreCreateException { final ComboPooledDataSource dataSource = getPrivilegedDataSource(container, databaseName); final Connection connection = dataSource.getConnection(); try { @@ -88,8 +93,8 @@ public class SubsetServiceMariaDbImpl extends HibernateConnector implements Subs public QueryResultDto execute(PrivilegedDatabaseDto database, String statement, Instant timestamp, UUID userId, Long page, Long size, SortTypeDto sortDirection, String sortColumn) throws QueryStoreInsertException, SQLException, QueryNotFoundException, TableMalformedException, - UserNotFoundException, NotAllowedException, RemoteUnavailableException, ServiceException, - DatabaseNotFoundException { + UserNotFoundException, NotAllowedException, RemoteUnavailableException, DatabaseNotFoundException, + MetadataServiceException { final Long queryId = storeQuery(database, statement, timestamp, userId); final QueryDto query = findById(database, queryId); return reExecute(database, query, page, size, sortDirection, sortColumn); @@ -120,7 +125,7 @@ public class SubsetServiceMariaDbImpl extends HibernateConnector implements Subs @Override public List<QueryDto> findAll(PrivilegedDatabaseDto database, Boolean filterPersisted) throws SQLException, - QueryNotFoundException, RemoteUnavailableException, ServiceException, DatabaseNotFoundException { + QueryNotFoundException, RemoteUnavailableException, DatabaseNotFoundException, MetadataServiceException { final List<IdentifierDto> identifiers = metadataServiceGateway.getIdentifiers(database.getId(), null); final ComboPooledDataSource dataSource = getPrivilegedDataSource(database); final Connection connection = dataSource.getConnection(); @@ -153,13 +158,21 @@ public class SubsetServiceMariaDbImpl extends HibernateConnector implements Subs @Override public ExportResourceDto export(PrivilegedDatabaseDto database, QueryDto query, Instant timestamp, String filename) throws SQLException, QueryMalformedException, SidecarExportException, StorageNotFoundException, - StorageUnavailableException, ServiceException, RemoteUnavailableException { + StorageUnavailableException, RemoteUnavailableException { final String filePath = s3Config.getS3FilePath() + "/" + filename; + final String viewName = "ex_" + Hashing.sha512() + .hashString(new String(RandomUtils.nextBytes(256), Charset.defaultCharset()), Charset.defaultCharset()) + .toString() + .substring(0, 60); final ComboPooledDataSource dataSource = getPrivilegedDataSource(database); final Connection connection = dataSource.getConnection(); try { /* export to data database sidecar */ - connection.prepareStatement(mariaDbMapper.subsetToRawExportQuery(query.getQuery(), timestamp, filePath)) + connection.prepareStatement(mariaDbMapper.subsetToRawTemporaryViewQuery(viewName, query.getQuery())) + .executeUpdate(); + connection.prepareStatement(mariaDbMapper.subsetToRawExportQuery(viewName, timestamp, filePath)) + .executeUpdate(); + connection.prepareStatement(mariaDbMapper.dropViewRawQuery(viewName)) .executeUpdate(); connection.commit(); } catch (SQLException e) { @@ -208,7 +221,7 @@ public class SubsetServiceMariaDbImpl extends HibernateConnector implements Subs @Override public QueryDto findById(PrivilegedDatabaseDto database, Long queryId) throws QueryNotFoundException, SQLException, - RemoteUnavailableException, UserNotFoundException, ServiceException, DatabaseNotFoundException { + RemoteUnavailableException, UserNotFoundException, DatabaseNotFoundException, MetadataServiceException { final ComboPooledDataSource dataSource = getPrivilegedDataSource(database); final Connection connection = dataSource.getConnection(); try { diff --git a/dbrepo-data-service/services/src/main/java/at/tuwien/service/impl/TableServiceMariaDbImpl.java b/dbrepo-data-service/services/src/main/java/at/tuwien/service/impl/TableServiceMariaDbImpl.java index be15d46895..e913c0cb82 100644 --- a/dbrepo-data-service/services/src/main/java/at/tuwien/service/impl/TableServiceMariaDbImpl.java +++ b/dbrepo-data-service/services/src/main/java/at/tuwien/service/impl/TableServiceMariaDbImpl.java @@ -245,8 +245,8 @@ public class TableServiceMariaDbImpl extends HibernateConnector implements Table } @Override - public void importDataset(PrivilegedTableDto table, ImportCsvDto data) - throws StorageNotFoundException, SQLException, QueryMalformedException, ServiceException, RemoteUnavailableException { + public void importDataset(PrivilegedTableDto table, ImportCsvDto data) throws StorageNotFoundException, + SQLException, QueryMalformedException, RemoteUnavailableException, SidecarImportException { /* import .csv from blob storage to sidecar */ dataDatabaseSidecarGateway.importFile(table.getDatabase().getContainer().getSidecarHost(), table.getDatabase().getContainer().getSidecarPort(), data.getLocation()); /* import .csv from sidecar to database */ @@ -297,8 +297,8 @@ public class TableServiceMariaDbImpl extends HibernateConnector implements Table } @Override - public void createTuple(PrivilegedTableDto table, TupleDto data) throws SQLException, - QueryMalformedException, TableMalformedException, StorageUnavailableException, StorageNotFoundException { + public void createTuple(PrivilegedTableDto table, TupleDto data) throws SQLException, QueryMalformedException, + TableMalformedException, StorageUnavailableException, StorageNotFoundException { log.trace("create tuple: {}", data); /* for each LOB-like data-column, retrieve the bytes and replace the value */ for (String key : data.getData().keySet()) { @@ -385,8 +385,8 @@ public class TableServiceMariaDbImpl extends HibernateConnector implements Table @Override public ExportResourceDto exportDataset(PrivilegedTableDto table, Instant timestamp) throws SQLException, - StorageNotFoundException, StorageUnavailableException, QueryMalformedException, ServiceException, - RemoteUnavailableException { + StorageNotFoundException, StorageUnavailableException, QueryMalformedException, RemoteUnavailableException, + SidecarExportException { final String fileName = RandomStringUtils.randomAlphabetic(40) + ".csv"; final String filePath = s3Config.getS3FilePath() + "/" + fileName; final ComboPooledDataSource dataSource = getPrivilegedDataSource(table.getDatabase()); diff --git a/dbrepo-data-service/services/src/main/java/at/tuwien/service/impl/ViewServiceMariaDbImpl.java b/dbrepo-data-service/services/src/main/java/at/tuwien/service/impl/ViewServiceMariaDbImpl.java index 6f88c40973..3cdba35f08 100644 --- a/dbrepo-data-service/services/src/main/java/at/tuwien/service/impl/ViewServiceMariaDbImpl.java +++ b/dbrepo-data-service/services/src/main/java/at/tuwien/service/impl/ViewServiceMariaDbImpl.java @@ -174,7 +174,7 @@ public class ViewServiceMariaDbImpl extends HibernateConnector implements ViewSe final Connection connection = dataSource.getConnection(); try { /* drop view if exists */ - connection.prepareStatement("DROP VIEW IF EXISTS `" + view.getInternalName() + "`;") + connection.prepareStatement(mariaDbMapper.dropViewRawQuery(view.getInternalName())) .execute(); connection.commit(); } catch (SQLException e) { @@ -214,8 +214,8 @@ public class ViewServiceMariaDbImpl extends HibernateConnector implements ViewSe @Override public ExportResourceDto exportDataset(PrivilegedDatabaseDto database, ViewDto view, Instant timestamp) - throws SQLException, QueryMalformedException, StorageNotFoundException, - StorageUnavailableException, ServiceException, RemoteUnavailableException { + throws SQLException, QueryMalformedException, StorageNotFoundException, StorageUnavailableException, + RemoteUnavailableException, SidecarExportException { final String fileName = RandomStringUtils.randomAlphabetic(40) + ".csv"; final String filePath = s3Config.getS3FilePath() + "/" + fileName; final ComboPooledDataSource dataSource = getPrivilegedDataSource(database); diff --git a/dbrepo-metadata-service/Dockerfile b/dbrepo-metadata-service/Dockerfile index 75fe485c16..1a37bf7e7e 100644 --- a/dbrepo-metadata-service/Dockerfile +++ b/dbrepo-metadata-service/Dockerfile @@ -34,9 +34,9 @@ RUN apk add --no-cache curl bash jq WORKDIR /app -USER 65534 +USER 1001 -COPY --from=build --chown=65534 ./rest-service/target/dbrepo-metadata-service-rest-service-*.jar ./metadata-service.jar +COPY --from=build --chown=1001 ./rest-service/target/dbrepo-metadata-service-rest-service-*.jar ./metadata-service.jar # non-root port EXPOSE 8080 diff --git a/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/auth/KeycloakErrorDto.java b/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/auth/KeycloakErrorDto.java index 4b9eefa16d..9b8ad90ea5 100644 --- a/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/auth/KeycloakErrorDto.java +++ b/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/auth/KeycloakErrorDto.java @@ -6,6 +6,8 @@ import jakarta.validation.constraints.NotNull; import lombok.*; import lombok.extern.jackson.Jacksonized; +import java.io.Serializable; + @Getter @Setter @Builder @@ -13,7 +15,7 @@ import lombok.extern.jackson.Jacksonized; @AllArgsConstructor @Jacksonized @ToString -public class KeycloakErrorDto { +public class KeycloakErrorDto implements Serializable { @NotNull @Schema(example = "invalid_grant") @@ -23,4 +25,6 @@ public class KeycloakErrorDto { @JsonProperty("error_description") private String errorDescription; + private String errorMessage; + } diff --git a/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/keycloak/RoleRepresentationDto.java b/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/keycloak/RoleRepresentationDto.java new file mode 100644 index 0000000000..8f7d795fdb --- /dev/null +++ b/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/keycloak/RoleRepresentationDto.java @@ -0,0 +1,34 @@ +package at.tuwien.api.keycloak; + +import lombok.*; +import lombok.extern.jackson.Jacksonized; + +import java.util.UUID; + +/** + * https://www.keycloak.org/docs-api/22.0.1/rest-api/index.html#RoleRepresentation + */ +@Getter +@Setter +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Jacksonized +@ToString +public class RoleRepresentationDto { + + private UUID id; + + private String name; + + private String description; + + private Boolean scopeParamRequired; + + private Boolean composite; + + private Boolean clientRole; + + private UUID containerId; + +} diff --git a/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/keycloak/UserCreateDto.java b/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/keycloak/UserCreateDto.java index 0ebaffff10..fe4b695502 100644 --- a/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/keycloak/UserCreateDto.java +++ b/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/keycloak/UserCreateDto.java @@ -32,4 +32,8 @@ public class UserCreateDto { @NotNull private List<CredentialDto> credentials; + private List<String> realmRoles; + + private List<String> groups; + } diff --git a/dbrepo-metadata-service/entities/src/main/java/at/tuwien/entities/database/table/columns/TableColumn.java b/dbrepo-metadata-service/entities/src/main/java/at/tuwien/entities/database/table/columns/TableColumn.java index 9a50f5c054..c869e41637 100644 --- a/dbrepo-metadata-service/entities/src/main/java/at/tuwien/entities/database/table/columns/TableColumn.java +++ b/dbrepo-metadata-service/entities/src/main/java/at/tuwien/entities/database/table/columns/TableColumn.java @@ -86,13 +86,13 @@ public class TableColumn implements Comparable<TableColumn> { @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX", timezone = "UTC") private Instant created; - @ManyToOne(fetch = FetchType.LAZY, cascade = {CascadeType.ALL, CascadeType.PERSIST}) + @ManyToOne(fetch = FetchType.LAZY, cascade = {CascadeType.MERGE, CascadeType.PERSIST}) @JoinTable(name = "mdb_columns_concepts", joinColumns = @JoinColumn(name = "cid", referencedColumnName = "id", nullable = false), inverseJoinColumns = @JoinColumn(name = "id", referencedColumnName = "id")) private TableColumnConcept concept; - @ManyToOne(fetch = FetchType.LAZY, cascade = {CascadeType.ALL, CascadeType.PERSIST}) + @ManyToOne(fetch = FetchType.LAZY, cascade = {CascadeType.MERGE, CascadeType.PERSIST}) @JoinTable(name = "mdb_columns_units", joinColumns = @JoinColumn(name = "cid", referencedColumnName = "id", nullable = false), inverseJoinColumns = @JoinColumn(name = "id", referencedColumnName = "id")) diff --git a/dbrepo-metadata-service/pom.xml b/dbrepo-metadata-service/pom.xml index ef3bee2637..0fdc80b428 100644 --- a/dbrepo-metadata-service/pom.xml +++ b/dbrepo-metadata-service/pom.xml @@ -58,6 +58,7 @@ <keycloak-testcontainer.version>3.2.0</keycloak-testcontainer.version> <aws-s3.version>2.25.23</aws-s3.version> <jackson.version>2.15.2</jackson.version> + <minio.version>8.5.7</minio.version> </properties> <dependencies> @@ -248,6 +249,11 @@ <version>${testcontainers.version}</version> <scope>test</scope> </dependency> + <dependency> + <groupId>org.testcontainers</groupId> + <artifactId>minio</artifactId> + <version>${testcontainers.version}</version> + </dependency> <dependency> <groupId>com.github.dasniko</groupId> <artifactId>testcontainers-keycloak</artifactId> diff --git a/dbrepo-metadata-service/repositories/src/main/java/at/tuwien/exception/BrokerServiceConnectionException.java b/dbrepo-metadata-service/repositories/src/main/java/at/tuwien/exception/BrokerServiceConnectionException.java new file mode 100644 index 0000000000..6efa16fa87 --- /dev/null +++ b/dbrepo-metadata-service/repositories/src/main/java/at/tuwien/exception/BrokerServiceConnectionException.java @@ -0,0 +1,21 @@ +package at.tuwien.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(code = HttpStatus.BAD_GATEWAY, reason = "error.broker.connection") +public class BrokerServiceConnectionException extends Exception { + + public BrokerServiceConnectionException(String msg) { + super(msg); + } + + public BrokerServiceConnectionException(String msg, Throwable thr) { + super(msg + ": " + thr.getLocalizedMessage(), thr); + } + + public BrokerServiceConnectionException(Throwable thr) { + super(thr); + } + +} diff --git a/dbrepo-data-service/services/src/main/java/at/tuwien/exception/StorageUnavailableException.java b/dbrepo-metadata-service/repositories/src/main/java/at/tuwien/exception/BrokerServiceException.java similarity index 52% rename from dbrepo-data-service/services/src/main/java/at/tuwien/exception/StorageUnavailableException.java rename to dbrepo-metadata-service/repositories/src/main/java/at/tuwien/exception/BrokerServiceException.java index b25bac260e..86201c5d69 100644 --- a/dbrepo-data-service/services/src/main/java/at/tuwien/exception/StorageUnavailableException.java +++ b/dbrepo-metadata-service/repositories/src/main/java/at/tuwien/exception/BrokerServiceException.java @@ -3,18 +3,18 @@ package at.tuwien.exception; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.ResponseStatus; -@ResponseStatus(code = HttpStatus.SERVICE_UNAVAILABLE, reason = "error.storage.missing") -public class StorageUnavailableException extends Exception { +@ResponseStatus(code = HttpStatus.SERVICE_UNAVAILABLE, reason = "error.broker.invalid") +public class BrokerServiceException extends Exception { - public StorageUnavailableException(String message) { + public BrokerServiceException(String message) { super(message); } - public StorageUnavailableException(String message, Throwable thr) { + public BrokerServiceException(String message, Throwable thr) { super(message, thr); } - public StorageUnavailableException(Throwable thr) { + public BrokerServiceException(Throwable thr) { super(thr); } diff --git a/dbrepo-metadata-service/repositories/src/main/java/at/tuwien/exception/ServiceConnectionException.java b/dbrepo-metadata-service/repositories/src/main/java/at/tuwien/exception/DataServiceConnectionException.java similarity index 57% rename from dbrepo-metadata-service/repositories/src/main/java/at/tuwien/exception/ServiceConnectionException.java rename to dbrepo-metadata-service/repositories/src/main/java/at/tuwien/exception/DataServiceConnectionException.java index 069e1d774a..0125a781ad 100644 --- a/dbrepo-metadata-service/repositories/src/main/java/at/tuwien/exception/ServiceConnectionException.java +++ b/dbrepo-metadata-service/repositories/src/main/java/at/tuwien/exception/DataServiceConnectionException.java @@ -4,17 +4,17 @@ import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.ResponseStatus; @ResponseStatus(code = HttpStatus.BAD_GATEWAY, reason = "error.data.connection") -public class ServiceConnectionException extends Exception { +public class DataServiceConnectionException extends Exception { - public ServiceConnectionException(String msg) { + public DataServiceConnectionException(String msg) { super(msg); } - public ServiceConnectionException(String msg, Throwable thr) { + public DataServiceConnectionException(String msg, Throwable thr) { super(msg + ": " + thr.getLocalizedMessage(), thr); } - public ServiceConnectionException(Throwable thr) { + public DataServiceConnectionException(Throwable thr) { super(thr); } diff --git a/dbrepo-metadata-service/repositories/src/main/java/at/tuwien/exception/ServiceException.java b/dbrepo-metadata-service/repositories/src/main/java/at/tuwien/exception/DataServiceException.java similarity index 59% rename from dbrepo-metadata-service/repositories/src/main/java/at/tuwien/exception/ServiceException.java rename to dbrepo-metadata-service/repositories/src/main/java/at/tuwien/exception/DataServiceException.java index 70bef91528..f76e662a65 100644 --- a/dbrepo-metadata-service/repositories/src/main/java/at/tuwien/exception/ServiceException.java +++ b/dbrepo-metadata-service/repositories/src/main/java/at/tuwien/exception/DataServiceException.java @@ -4,17 +4,17 @@ import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.ResponseStatus; @ResponseStatus(code = HttpStatus.SERVICE_UNAVAILABLE, reason = "error.data.invalid") -public class ServiceException extends Exception { +public class DataServiceException extends Exception { - public ServiceException(String message) { + public DataServiceException(String message) { super(message); } - public ServiceException(String message, Throwable thr) { + public DataServiceException(String message, Throwable thr) { super(message, thr); } - public ServiceException(Throwable thr) { + public DataServiceException(Throwable thr) { super(thr); } diff --git a/dbrepo-data-service/services/src/main/java/at/tuwien/exception/DatabaseMalformedException.java b/dbrepo-metadata-service/repositories/src/main/java/at/tuwien/exception/DatabaseMalformedException.java similarity index 100% rename from dbrepo-data-service/services/src/main/java/at/tuwien/exception/DatabaseMalformedException.java rename to dbrepo-metadata-service/repositories/src/main/java/at/tuwien/exception/DatabaseMalformedException.java diff --git a/dbrepo-data-service/services/src/main/java/at/tuwien/exception/DatabaseUnavailableException.java b/dbrepo-metadata-service/repositories/src/main/java/at/tuwien/exception/DatabaseUnavailableException.java similarity index 100% rename from dbrepo-data-service/services/src/main/java/at/tuwien/exception/DatabaseUnavailableException.java rename to dbrepo-metadata-service/repositories/src/main/java/at/tuwien/exception/DatabaseUnavailableException.java diff --git a/dbrepo-metadata-service/repositories/src/main/java/at/tuwien/exception/ExternalServiceException.java b/dbrepo-metadata-service/repositories/src/main/java/at/tuwien/exception/ExternalServiceException.java new file mode 100644 index 0000000000..d5f399c402 --- /dev/null +++ b/dbrepo-metadata-service/repositories/src/main/java/at/tuwien/exception/ExternalServiceException.java @@ -0,0 +1,21 @@ +package at.tuwien.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(code = HttpStatus.SERVICE_UNAVAILABLE, reason = "error.external.invalid") +public class ExternalServiceException extends Exception { + + public ExternalServiceException(String message) { + super(message); + } + + public ExternalServiceException(String message, Throwable thr) { + super(message, thr); + } + + public ExternalServiceException(Throwable thr) { + super(thr); + } + +} diff --git a/dbrepo-data-service/services/src/main/java/at/tuwien/exception/ServiceConnectionException.java b/dbrepo-metadata-service/repositories/src/main/java/at/tuwien/exception/MetadataServiceConnectionException.java similarity index 56% rename from dbrepo-data-service/services/src/main/java/at/tuwien/exception/ServiceConnectionException.java rename to dbrepo-metadata-service/repositories/src/main/java/at/tuwien/exception/MetadataServiceConnectionException.java index 6a91dac23a..329de6ffc4 100644 --- a/dbrepo-data-service/services/src/main/java/at/tuwien/exception/ServiceConnectionException.java +++ b/dbrepo-metadata-service/repositories/src/main/java/at/tuwien/exception/MetadataServiceConnectionException.java @@ -4,17 +4,17 @@ import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.ResponseStatus; @ResponseStatus(code = HttpStatus.BAD_GATEWAY, reason = "error.metadata.connection") -public class ServiceConnectionException extends Exception { +public class MetadataServiceConnectionException extends Exception { - public ServiceConnectionException(String msg) { + public MetadataServiceConnectionException(String msg) { super(msg); } - public ServiceConnectionException(String msg, Throwable thr) { + public MetadataServiceConnectionException(String msg, Throwable thr) { super(msg + ": " + thr.getLocalizedMessage(), thr); } - public ServiceConnectionException(Throwable thr) { + public MetadataServiceConnectionException(Throwable thr) { super(thr); } diff --git a/dbrepo-data-service/services/src/main/java/at/tuwien/exception/ServiceException.java b/dbrepo-metadata-service/repositories/src/main/java/at/tuwien/exception/MetadataServiceException.java similarity index 58% rename from dbrepo-data-service/services/src/main/java/at/tuwien/exception/ServiceException.java rename to dbrepo-metadata-service/repositories/src/main/java/at/tuwien/exception/MetadataServiceException.java index a543d02c9a..a6784d6dd0 100644 --- a/dbrepo-data-service/services/src/main/java/at/tuwien/exception/ServiceException.java +++ b/dbrepo-metadata-service/repositories/src/main/java/at/tuwien/exception/MetadataServiceException.java @@ -4,17 +4,17 @@ import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.ResponseStatus; @ResponseStatus(code = HttpStatus.SERVICE_UNAVAILABLE, reason = "error.metadata.invalid") -public class ServiceException extends Exception { +public class MetadataServiceException extends Exception { - public ServiceException(String message) { + public MetadataServiceException(String message) { super(message); } - public ServiceException(String message, Throwable thr) { + public MetadataServiceException(String message, Throwable thr) { super(message, thr); } - public ServiceException(Throwable thr) { + public MetadataServiceException(Throwable thr) { super(thr); } diff --git a/dbrepo-data-service/services/src/main/java/at/tuwien/exception/QueryMalformedException.java b/dbrepo-metadata-service/repositories/src/main/java/at/tuwien/exception/QueryMalformedException.java similarity index 100% rename from dbrepo-data-service/services/src/main/java/at/tuwien/exception/QueryMalformedException.java rename to dbrepo-metadata-service/repositories/src/main/java/at/tuwien/exception/QueryMalformedException.java diff --git a/dbrepo-data-service/services/src/main/java/at/tuwien/exception/QueryNotSupportedException.java b/dbrepo-metadata-service/repositories/src/main/java/at/tuwien/exception/QueryNotSupportedException.java similarity index 100% rename from dbrepo-data-service/services/src/main/java/at/tuwien/exception/QueryNotSupportedException.java rename to dbrepo-metadata-service/repositories/src/main/java/at/tuwien/exception/QueryNotSupportedException.java diff --git a/dbrepo-data-service/services/src/main/java/at/tuwien/exception/QueryStoreCreateException.java b/dbrepo-metadata-service/repositories/src/main/java/at/tuwien/exception/QueryStoreCreateException.java similarity index 100% rename from dbrepo-data-service/services/src/main/java/at/tuwien/exception/QueryStoreCreateException.java rename to dbrepo-metadata-service/repositories/src/main/java/at/tuwien/exception/QueryStoreCreateException.java diff --git a/dbrepo-data-service/services/src/main/java/at/tuwien/exception/QueryStoreGCException.java b/dbrepo-metadata-service/repositories/src/main/java/at/tuwien/exception/QueryStoreGCException.java similarity index 100% rename from dbrepo-data-service/services/src/main/java/at/tuwien/exception/QueryStoreGCException.java rename to dbrepo-metadata-service/repositories/src/main/java/at/tuwien/exception/QueryStoreGCException.java diff --git a/dbrepo-data-service/services/src/main/java/at/tuwien/exception/QueryStoreInsertException.java b/dbrepo-metadata-service/repositories/src/main/java/at/tuwien/exception/QueryStoreInsertException.java similarity index 100% rename from dbrepo-data-service/services/src/main/java/at/tuwien/exception/QueryStoreInsertException.java rename to dbrepo-metadata-service/repositories/src/main/java/at/tuwien/exception/QueryStoreInsertException.java diff --git a/dbrepo-data-service/services/src/main/java/at/tuwien/exception/QueryStorePersistException.java b/dbrepo-metadata-service/repositories/src/main/java/at/tuwien/exception/QueryStorePersistException.java similarity index 100% rename from dbrepo-data-service/services/src/main/java/at/tuwien/exception/QueryStorePersistException.java rename to dbrepo-metadata-service/repositories/src/main/java/at/tuwien/exception/QueryStorePersistException.java diff --git a/dbrepo-data-service/services/src/main/java/at/tuwien/exception/RemoteUnavailableException.java b/dbrepo-metadata-service/repositories/src/main/java/at/tuwien/exception/RemoteUnavailableException.java similarity index 100% rename from dbrepo-data-service/services/src/main/java/at/tuwien/exception/RemoteUnavailableException.java rename to dbrepo-metadata-service/repositories/src/main/java/at/tuwien/exception/RemoteUnavailableException.java diff --git a/dbrepo-data-service/services/src/main/java/at/tuwien/exception/SidecarExportException.java b/dbrepo-metadata-service/repositories/src/main/java/at/tuwien/exception/SidecarExportException.java similarity index 100% rename from dbrepo-data-service/services/src/main/java/at/tuwien/exception/SidecarExportException.java rename to dbrepo-metadata-service/repositories/src/main/java/at/tuwien/exception/SidecarExportException.java diff --git a/dbrepo-data-service/services/src/main/java/at/tuwien/exception/SidecarImportException.java b/dbrepo-metadata-service/repositories/src/main/java/at/tuwien/exception/SidecarImportException.java similarity index 100% rename from dbrepo-data-service/services/src/main/java/at/tuwien/exception/SidecarImportException.java rename to dbrepo-metadata-service/repositories/src/main/java/at/tuwien/exception/SidecarImportException.java diff --git a/dbrepo-data-service/services/src/main/java/at/tuwien/exception/TableMalformedException.java b/dbrepo-metadata-service/repositories/src/main/java/at/tuwien/exception/TableMalformedException.java similarity index 100% rename from dbrepo-data-service/services/src/main/java/at/tuwien/exception/TableMalformedException.java rename to dbrepo-metadata-service/repositories/src/main/java/at/tuwien/exception/TableMalformedException.java diff --git a/dbrepo-data-service/services/src/main/java/at/tuwien/exception/TableSchemaException.java b/dbrepo-metadata-service/repositories/src/main/java/at/tuwien/exception/TableSchemaException.java similarity index 100% rename from dbrepo-data-service/services/src/main/java/at/tuwien/exception/TableSchemaException.java rename to dbrepo-metadata-service/repositories/src/main/java/at/tuwien/exception/TableSchemaException.java diff --git a/dbrepo-data-service/services/src/main/java/at/tuwien/exception/ViewMalformedException.java b/dbrepo-metadata-service/repositories/src/main/java/at/tuwien/exception/ViewMalformedException.java similarity index 100% rename from dbrepo-data-service/services/src/main/java/at/tuwien/exception/ViewMalformedException.java rename to dbrepo-metadata-service/repositories/src/main/java/at/tuwien/exception/ViewMalformedException.java diff --git a/dbrepo-data-service/services/src/main/java/at/tuwien/exception/ViewSchemaException.java b/dbrepo-metadata-service/repositories/src/main/java/at/tuwien/exception/ViewSchemaException.java similarity index 100% rename from dbrepo-data-service/services/src/main/java/at/tuwien/exception/ViewSchemaException.java rename to dbrepo-metadata-service/repositories/src/main/java/at/tuwien/exception/ViewSchemaException.java diff --git a/dbrepo-metadata-service/repositories/src/main/java/at/tuwien/mapper/MetadataMapper.java b/dbrepo-metadata-service/repositories/src/main/java/at/tuwien/mapper/MetadataMapper.java index fb7c80e011..557d1e49ab 100644 --- a/dbrepo-metadata-service/repositories/src/main/java/at/tuwien/mapper/MetadataMapper.java +++ b/dbrepo-metadata-service/repositories/src/main/java/at/tuwien/mapper/MetadataMapper.java @@ -467,8 +467,8 @@ public interface MetadataMapper { TableBriefDto tableToTableBriefDto(Table data); default UniqueDto uniqueToUniqueDto(Unique data) { - data.getTable().setOwner(null); /* loop */ - data.getTable().setCreator(null); /* loop */ +// data.getTable().setOwner(null); /* loop */ +// data.getTable().setCreator(null); /* loop */ return UniqueDto.builder() .id(data.getId()) .name(data.getName()) diff --git a/dbrepo-metadata-service/repositories/src/main/java/at/tuwien/utils/UserUtil.java b/dbrepo-metadata-service/repositories/src/main/java/at/tuwien/utils/UserUtil.java index 7a99e839ed..4e517625ed 100644 --- a/dbrepo-metadata-service/repositories/src/main/java/at/tuwien/utils/UserUtil.java +++ b/dbrepo-metadata-service/repositories/src/main/java/at/tuwien/utils/UserUtil.java @@ -18,6 +18,16 @@ public class UserUtil { .anyMatch(a -> a.getAuthority().equals(role)); } + public static boolean isSystem(Principal principal) { + if (principal == null) { + return false; + } + final Authentication authentication = (Authentication) principal; + return authentication.getAuthorities() + .stream() + .anyMatch(a -> a.getAuthority().equals("system")); + } + public static UUID getId(Principal principal) { if (principal == null) { return null; diff --git a/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/AccessEndpoint.java b/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/AccessEndpoint.java index 3c33e2b057..9cdcfdedf9 100644 --- a/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/AccessEndpoint.java +++ b/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/AccessEndpoint.java @@ -94,8 +94,8 @@ public class AccessEndpoint { public ResponseEntity<DatabaseAccessDto> create(@NotBlank @PathVariable("databaseId") Long databaseId, @NotBlank @PathVariable("userId") UUID userId, @Valid @RequestBody UpdateDatabaseAccessDto data, - @NotNull Principal principal) throws NotAllowedException, ServiceException, - ServiceConnectionException, DatabaseNotFoundException, UserNotFoundException, AccessNotFoundException, + @NotNull Principal principal) throws NotAllowedException, DataServiceException, + DataServiceConnectionException, DatabaseNotFoundException, UserNotFoundException, AccessNotFoundException, SearchServiceException, SearchServiceConnectionException { log.debug("endpoint give access to database, databaseId={}, userId={}, access.type={}", databaseId, userId, data.getType()); @@ -157,7 +157,7 @@ public class AccessEndpoint { @NotBlank @PathVariable("userId") UUID userId, @Valid @RequestBody UpdateDatabaseAccessDto data, @NotNull Principal principal) throws NotAllowedException, - ServiceException, ServiceConnectionException, DatabaseNotFoundException, UserNotFoundException, + DataServiceException, DataServiceConnectionException, DatabaseNotFoundException, UserNotFoundException, AccessNotFoundException, SearchServiceException, SearchServiceConnectionException { log.debug("endpoint modify database access, databaseId={}, userId={}, access.type={}", databaseId, userId, data.getType()); @@ -176,9 +176,9 @@ public class AccessEndpoint { @RequestMapping(value = "/{userId}", method = {RequestMethod.GET, RequestMethod.HEAD}) @Transactional(readOnly = true) @Observed(name = "dbrepo_access_get") - @PreAuthorize("hasAuthority('check-database-access') or hasAuthority('admin')") + @PreAuthorize("hasAuthority('check-database-access') or hasAuthority('check-foreign-database-access')") @Operation(summary = "Find/Check access", - description = "Finds or checks access of a user with given id to a database with given id. Requests with HTTP method **GET** return the access object, requests with HTTP method **HEAD** only the status. When the user has at least *READ* access, the status 200 is returned, 403 otherwise. Requires role `check-database-access` or `admin`.", + description = "Finds or checks access of a user with given id to a database with given id. Requests with HTTP method **GET** return the access object, requests with HTTP method **HEAD** only the status. When the user has at least *READ* access, the status 200 is returned, 403 otherwise. Requires role `check-database-access` or `check-foreign-database-access`.", security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) @ApiResponses(value = { @ApiResponse(responseCode = "200", @@ -204,7 +204,7 @@ public class AccessEndpoint { log.debug("endpoint get database access, databaseId={}, userId={}, principal.name={}", databaseId, userId, principal.getName()); if (!userId.equals(UserUtil.getId(principal))) { - if (!UserUtil.hasRole(principal, "admin")) { + if (!UserUtil.hasRole(principal, "check-foreign-database-access")) { log.error("Failed to find access: foreign user"); throw new NotAllowedException("Failed to find access: foreign user"); } @@ -256,8 +256,8 @@ public class AccessEndpoint { }) public ResponseEntity<Void> revoke(@NotBlank @PathVariable("databaseId") Long databaseId, @NotBlank @PathVariable("userId") UUID userId, - @NotNull Principal principal) throws NotAllowedException, ServiceException, - ServiceConnectionException, DatabaseNotFoundException, UserNotFoundException, AccessNotFoundException, + @NotNull Principal principal) throws NotAllowedException, DataServiceException, + DataServiceConnectionException, DatabaseNotFoundException, UserNotFoundException, AccessNotFoundException, SearchServiceException, SearchServiceConnectionException { log.debug("endpoint revoke database access, databaseId={}, userId={}", databaseId, userId); final Database database = databaseService.findById(databaseId); diff --git a/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/ContainerEndpoint.java b/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/ContainerEndpoint.java index 77d35ec498..294d471e8f 100644 --- a/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/ContainerEndpoint.java +++ b/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/ContainerEndpoint.java @@ -10,6 +10,7 @@ import at.tuwien.exception.ContainerNotFoundException; import at.tuwien.exception.ImageNotFoundException; import at.tuwien.mapper.MetadataMapper; import at.tuwien.service.ContainerService; +import at.tuwien.utils.UserUtil; import io.micrometer.observation.annotation.Observed; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.ArraySchema; @@ -143,14 +144,11 @@ public class ContainerEndpoint { final ContainerDto dto = metadataMapper.containerToContainerDto(container); log.trace("find container resulted in container {}", dto); final HttpHeaders headers = new HttpHeaders(); - if (principal != null) { - final Authentication authentication = (Authentication) principal; - if (authentication.isAuthenticated() && authentication.getAuthorities().stream().anyMatch(a -> a.getAuthority().equals("admin"))) { - log.trace("attach privileged credential information"); - headers.set("X-Username", container.getPrivilegedUsername()); - headers.set("X-Password", container.getPrivilegedPassword()); - headers.set("Access-Control-Expose-Headers", "X-Username X-Password"); - } + if (UserUtil.isSystem(principal)) { + log.trace("attach privileged credential information"); + headers.set("X-Username", container.getPrivilegedUsername()); + headers.set("X-Password", container.getPrivilegedPassword()); + headers.set("Access-Control-Expose-Headers", "X-Username X-Password"); } return ResponseEntity.ok() .headers(headers) diff --git a/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/DatabaseEndpoint.java b/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/DatabaseEndpoint.java index d974de4276..8be62ea5c4 100644 --- a/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/DatabaseEndpoint.java +++ b/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/DatabaseEndpoint.java @@ -8,6 +8,7 @@ import at.tuwien.entities.user.User; import at.tuwien.exception.*; import at.tuwien.mapper.MetadataMapper; import at.tuwien.service.*; +import at.tuwien.utils.UserUtil; import io.micrometer.observation.annotation.Observed; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.headers.Header; @@ -25,7 +26,6 @@ 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.security.core.Authentication; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.*; @@ -139,8 +139,8 @@ public class DatabaseEndpoint { schema = @Schema(implementation = ApiErrorDto.class))}), }) public ResponseEntity<DatabaseDto> create(@Valid @RequestBody DatabaseCreateDto data, - @NotNull Principal principal) throws ServiceException, - ServiceConnectionException, UserNotFoundException, DatabaseNotFoundException, ContainerNotFoundException, + @NotNull Principal principal) throws DataServiceException, + DataServiceConnectionException, UserNotFoundException, DatabaseNotFoundException, ContainerNotFoundException, SearchServiceException, SearchServiceConnectionException { log.debug("endpoint create database, data.name={}", data.getName()); final User user = userService.findByUsername(principal.getName()); @@ -190,9 +190,10 @@ public class DatabaseEndpoint { schema = @Schema(implementation = ApiErrorDto.class))}), }) public ResponseEntity<DatabaseDto> refreshTableMetadata(@NotNull @PathVariable("databaseId") Long databaseId, - @NotNull Principal principal) throws ServiceException, - ServiceConnectionException, DatabaseNotFoundException, SearchServiceException, - SearchServiceConnectionException, NotAllowedException, QueryNotFoundException, MalformedException { + @NotNull Principal principal) throws DataServiceException, + DataServiceConnectionException, DatabaseNotFoundException, SearchServiceException, + SearchServiceConnectionException, NotAllowedException, QueryNotFoundException, MalformedException, + TableNotFoundException { log.debug("endpoint refresh database metadata, databaseId={}", databaseId); Database database = databaseService.findById(databaseId); if (!database.getOwner().equals(principal)) { @@ -238,9 +239,9 @@ public class DatabaseEndpoint { schema = @Schema(implementation = ApiErrorDto.class))}), }) public ResponseEntity<DatabaseDto> refreshViewMetadata(@NotNull @PathVariable("databaseId") Long databaseId, - @NotNull Principal principal) throws ServiceException, - ServiceConnectionException, DatabaseNotFoundException, SearchServiceException, - SearchServiceConnectionException, NotAllowedException, QueryNotFoundException { + @NotNull Principal principal) throws DataServiceException, + DataServiceConnectionException, DatabaseNotFoundException, SearchServiceException, + SearchServiceConnectionException, NotAllowedException, QueryNotFoundException, ViewNotFoundException { log.debug("endpoint refresh database metadata, databaseId={}", databaseId); Database database = databaseService.findById(databaseId); if (!database.getOwner().equals(principal)) { @@ -347,7 +348,7 @@ public class DatabaseEndpoint { public ResponseEntity<DatabaseDto> transfer(@NotNull @PathVariable("databaseId") Long databaseId, @Valid @RequestBody DatabaseTransferDto data, @NotNull Principal principal) throws NotAllowedException, - ServiceException, ServiceConnectionException, DatabaseNotFoundException, UserNotFoundException, + DataServiceException, DataServiceConnectionException, DatabaseNotFoundException, UserNotFoundException, SearchServiceException, SearchServiceConnectionException { log.debug("endpoint transfer database, databaseId={}, transferDto.id={}", databaseId, data.getId()); final Database database = databaseService.findById(databaseId); @@ -452,8 +453,8 @@ public class DatabaseEndpoint { schema = @Schema(implementation = ApiErrorDto.class))}), }) public ResponseEntity<DatabaseDto> findById(@NotNull @PathVariable("databaseId") Long databaseId, - Principal principal) throws ServiceException, - ServiceConnectionException, DatabaseNotFoundException, ExchangeNotFoundException { + Principal principal) throws DataServiceException, + DataServiceConnectionException, DatabaseNotFoundException, ExchangeNotFoundException { log.debug("endpoint find database, databaseId={}", databaseId); final Database database = databaseService.findById(databaseId); final DatabaseDto dto = databaseMapper.customDatabaseToDatabaseDto(database); @@ -467,13 +468,10 @@ public class DatabaseEndpoint { log.debug("found {} database accesses", accesses.size()); } final HttpHeaders headers = new HttpHeaders(); - if (principal != null) { - final Authentication authentication = (Authentication) principal; - if (authentication.isAuthenticated() && authentication.getAuthorities().stream().anyMatch(a -> a.getAuthority().equals("admin"))) { - headers.set("X-Username", database.getContainer().getPrivilegedUsername()); - headers.set("X-Password", database.getContainer().getPrivilegedPassword()); - headers.set("Access-Control-Expose-Headers", "X-Username X-Password"); - } + if (UserUtil.isSystem(principal)) { + headers.set("X-Username", database.getContainer().getPrivilegedUsername()); + headers.set("X-Password", database.getContainer().getPrivilegedPassword()); + headers.set("Access-Control-Expose-Headers", "X-Username X-Password"); } return ResponseEntity.status(HttpStatus.OK) .headers(headers) diff --git a/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/IdentifierEndpoint.java b/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/IdentifierEndpoint.java index 781e5c44e8..831e9cd28d 100644 --- a/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/IdentifierEndpoint.java +++ b/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/IdentifierEndpoint.java @@ -205,7 +205,7 @@ public class IdentifierEndpoint { }) public ResponseEntity<?> find(@Valid @PathVariable("identifierId") Long identifierId, @RequestHeader(HttpHeaders.ACCEPT) String accept) throws IdentifierNotFoundException, - ServiceException, ServiceConnectionException, MalformedException, FormatNotAvailableException, + DataServiceException, DataServiceConnectionException, MalformedException, FormatNotAvailableException, QueryNotFoundException { log.debug("endpoint find identifier, identifierId={}, accept={}", identifierId, accept); final Identifier identifier = identifierService.find(identifierId); @@ -300,8 +300,9 @@ public class IdentifierEndpoint { schema = @Schema(implementation = ApiErrorDto.class))}), }) public ResponseEntity<Void> delete(@NotNull @PathVariable("identifierId") Long identifierId) - throws IdentifierNotFoundException, NotAllowedException, ServiceException, ServiceConnectionException, - DatabaseNotFoundException, SearchServiceException, SearchServiceConnectionException { + throws IdentifierNotFoundException, NotAllowedException, DataServiceException, + DataServiceConnectionException, DatabaseNotFoundException, SearchServiceException, + SearchServiceConnectionException { log.debug("endpoint delete identifier, identifierId={}", identifierId); final Identifier identifier = identifierService.find(identifierId); if (identifier.getStatus().equals(IdentifierStatusType.PUBLISHED)) { @@ -354,7 +355,7 @@ public class IdentifierEndpoint { }) public ResponseEntity<IdentifierDto> publish(@Valid @PathVariable("identifierId") Long identifierId) throws SearchServiceException, DatabaseNotFoundException, SearchServiceConnectionException, - MalformedException, ServiceConnectionException, IdentifierNotFoundException { + MalformedException, DataServiceConnectionException, IdentifierNotFoundException, ExternalServiceException { log.debug("endpoint publish identifier, identifierId={}", identifierId); identifierService.find(identifierId); return ResponseEntity.status(HttpStatus.CREATED) @@ -403,9 +404,10 @@ public class IdentifierEndpoint { public ResponseEntity<IdentifierDto> save(@NotNull @PathVariable("identifierId") Long identifierId, @NotNull @Valid @RequestBody IdentifierSaveDto data, @NotNull Principal principal) throws UserNotFoundException, - DatabaseNotFoundException, MalformedException, NotAllowedException, ServiceException, - ServiceConnectionException, SearchServiceException, QueryNotFoundException, - SearchServiceConnectionException, IdentifierNotFoundException, ViewNotFoundException, TableNotFoundException { + DatabaseNotFoundException, MalformedException, NotAllowedException, DataServiceException, + DataServiceConnectionException, SearchServiceException, QueryNotFoundException, + SearchServiceConnectionException, IdentifierNotFoundException, ViewNotFoundException, + TableNotFoundException, ExternalServiceException { log.debug("endpoint save identifier, identifierId={}, data.id={}, principal.name={}", identifierId, data.getId(), principal.getName()); final Database database = databaseService.findById(data.getDatabaseId()); @@ -524,9 +526,9 @@ public class IdentifierEndpoint { }) public ResponseEntity<IdentifierDto> create(@NotNull @Valid @RequestBody IdentifierCreateDto data, @NotNull Principal principal) throws DatabaseNotFoundException, - UserNotFoundException, NotAllowedException, MalformedException, ServiceConnectionException, - SearchServiceException, ServiceException, QueryNotFoundException, SearchServiceConnectionException, - IdentifierNotFoundException, ViewNotFoundException { + UserNotFoundException, NotAllowedException, MalformedException, DataServiceConnectionException, + SearchServiceException, DataServiceException, QueryNotFoundException, SearchServiceConnectionException, + IdentifierNotFoundException, ViewNotFoundException, ExternalServiceException { log.debug("endpoint create identifier, data.databaseId={}", data.getDatabaseId()); final Database database = databaseService.findById(data.getDatabaseId()); final User user = userService.findByUsername(principal.getName()); diff --git a/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/TableEndpoint.java b/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/TableEndpoint.java index c8f01bf26a..4fb8240b1d 100644 --- a/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/TableEndpoint.java +++ b/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/TableEndpoint.java @@ -35,7 +35,6 @@ 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.security.core.Authentication; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.*; @@ -152,7 +151,7 @@ public class TableEndpoint { @PutMapping("/{tableId}") @Transactional - @PreAuthorize("hasAuthority('update-table-statistic') or hasAuthority('admin')") + @PreAuthorize("hasAuthority('update-table-statistic')") @Observed(name = "dbrepo_statistic_table_update") @Operation(summary = "Update statistics", description = "Updates basic statistical properties (min, max, mean, median, std.dev) for numerical columns in a table with id. Requires role `update-table-statistic`", @@ -184,7 +183,7 @@ public class TableEndpoint { public ResponseEntity<Void> updateStatistic(@NotNull @PathVariable("databaseId") Long databaseId, @NotNull @PathVariable("tableId") Long tableId) throws TableNotFoundException, DatabaseNotFoundException, SearchServiceException, - SearchServiceConnectionException, MalformedException, ServiceException, ServiceConnectionException { + SearchServiceConnectionException, MalformedException, DataServiceException, DataServiceConnectionException { log.debug("endpoint update table statistics, databaseId={}, tableId={}", databaseId, tableId); final Table table = tableService.findById(databaseId, tableId); tableService.updateStatistics(table); @@ -236,7 +235,7 @@ public class TableEndpoint { @NotNull @PathVariable("columnId") Long columnId, @NotNull @Valid @RequestBody ColumnSemanticsUpdateDto updateDto, @NotNull Principal principal) throws NotAllowedException, - MalformedException, ServiceException, ServiceConnectionException, UserNotFoundException, + MalformedException, DataServiceException, DataServiceConnectionException, UserNotFoundException, TableNotFoundException, DatabaseNotFoundException, AccessNotFoundException, SearchServiceException, SearchServiceConnectionException, OntologyNotFoundException, SemanticEntityNotFoundException { log.debug("endpoint update table, databaseId={}, tableId={}, columnId={}", databaseId, tableId, columnId); @@ -343,7 +342,7 @@ public class TableEndpoint { public ResponseEntity<TableDto> create(@NotNull @PathVariable("databaseId") Long databaseId, @NotNull @Valid @RequestBody TableCreateDto data, @NotNull Principal principal) throws NotAllowedException, MalformedException, - ServiceException, ServiceConnectionException, DatabaseNotFoundException, UserNotFoundException, + DataServiceException, DataServiceConnectionException, DatabaseNotFoundException, UserNotFoundException, AccessNotFoundException, TableNotFoundException, TableExistsException, SearchServiceException, SearchServiceConnectionException, OntologyNotFoundException, SemanticEntityNotFoundException { log.debug("endpoint create table, databaseId={}, data.name={}", databaseId, data.getName()); @@ -401,26 +400,22 @@ public class TableEndpoint { }) public ResponseEntity<TableDto> findById(@NotNull @PathVariable("databaseId") Long databaseId, @NotNull @PathVariable("tableId") Long tableId, - Principal principal) throws ServiceException, - ServiceConnectionException, TableNotFoundException, DatabaseNotFoundException, QueueNotFoundException { + Principal principal) throws DataServiceException, + DataServiceConnectionException, TableNotFoundException, DatabaseNotFoundException, QueueNotFoundException { log.debug("endpoint find table, databaseId={}, tableId={}", databaseId, tableId); final Table table = tableService.findById(databaseId, tableId); final TableDto dto = metadataMapper.customTableToTableDto(table); final HttpHeaders headers = new HttpHeaders(); - if (principal != null) { - /* extra effort only when logged-in */ - final Authentication authentication = (Authentication) principal; - if (authentication.isAuthenticated() && authentication.getAuthorities().stream().anyMatch(a -> a.getAuthority().equals("admin"))) { - headers.set("X-Username", table.getDatabase().getContainer().getPrivilegedUsername()); - headers.set("X-Password", table.getDatabase().getContainer().getPrivilegedPassword()); - headers.set("X-Host", table.getDatabase().getContainer().getHost()); - headers.set("X-Port", "" + table.getDatabase().getContainer().getPort()); - headers.set("X-Type", table.getDatabase().getContainer().getImage().getJdbcMethod()); - headers.set("X-Database", table.getDatabase().getInternalName()); - headers.set("X-Sidecar-Host", table.getDatabase().getContainer().getSidecarHost()); - headers.set("X-Sidecar-Port", "" + table.getDatabase().getContainer().getSidecarPort()); - headers.set("Access-Control-Expose-Headers", "X-Username X-Password X-Host X-Port X-Type X-Database X-Sidecar-Host X-Sidecar-Port"); - } + if (UserUtil.isSystem(principal)) { + headers.set("X-Username", table.getDatabase().getContainer().getPrivilegedUsername()); + headers.set("X-Password", table.getDatabase().getContainer().getPrivilegedPassword()); + headers.set("X-Host", table.getDatabase().getContainer().getHost()); + headers.set("X-Port", "" + table.getDatabase().getContainer().getPort()); + headers.set("X-Type", table.getDatabase().getContainer().getImage().getJdbcMethod()); + headers.set("X-Database", table.getDatabase().getInternalName()); + headers.set("X-Sidecar-Host", table.getDatabase().getContainer().getSidecarHost()); + headers.set("X-Sidecar-Port", "" + table.getDatabase().getContainer().getSidecarPort()); + headers.set("Access-Control-Expose-Headers", "X-Username X-Password X-Host X-Port X-Type X-Database X-Sidecar-Host X-Sidecar-Port"); } return ResponseEntity.status(HttpStatus.OK) .headers(headers) @@ -466,7 +461,7 @@ public class TableEndpoint { public ResponseEntity<Void> delete(@NotNull @PathVariable("databaseId") Long databaseId, @NotNull @PathVariable("tableId") Long tableId, @NotNull Principal principal) throws NotAllowedException, - ServiceException, ServiceConnectionException, TableNotFoundException, DatabaseNotFoundException, + DataServiceException, DataServiceConnectionException, TableNotFoundException, DatabaseNotFoundException, SearchServiceException, SearchServiceConnectionException { log.debug("endpoint delete table, databaseId={}, tableId={}", databaseId, tableId); final Table table = tableService.findById(databaseId, tableId); diff --git a/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/UserEndpoint.java b/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/UserEndpoint.java index 1ee680e016..19e3a1df06 100644 --- a/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/UserEndpoint.java +++ b/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/UserEndpoint.java @@ -6,7 +6,6 @@ import at.tuwien.api.auth.SignupRequestDto; import at.tuwien.api.error.ApiErrorDto; import at.tuwien.api.keycloak.TokenDto; import at.tuwien.api.user.*; -import at.tuwien.config.KeycloakConfig; import at.tuwien.entities.database.Database; import at.tuwien.entities.user.User; import at.tuwien.exception.*; @@ -187,14 +186,19 @@ public class UserEndpoint { userService.findByUsername(data.getUsername()); } catch (UserNotFoundException e) { /* need to sync */ - log.debug("User with username {} does not exist in metadata database yet", data.getUsername()); + log.warn("User with username {} does not exist in metadata database yet", data.getUsername()); final SignupRequestDto request = SignupRequestDto.builder() .username(data.getUsername()) .email("noreply@example.com") .password(data.getPassword()) .build(); - userService.create(request, authenticationService.findByUsername(data.getUsername()).getId()); - log.info("Fetched user information from auth service and stored it into metadata database"); + final at.tuwien.api.keycloak.UserDto user = authenticationService.findByUsername(data.getUsername()); + if (user.getAttributes().getLdapId().length != 1) { + log.error("Failed to map ldap id for user with username: {}", data.getUsername()); + throw new UserNotFoundException("Failed to map ldap id"); + } + userService.create(request, user.getAttributes().getLdapId()[0]); + log.info("Patched missing user information for user with username: {}", data.getUsername()); } return ResponseEntity.accepted() .body(token); @@ -266,7 +270,7 @@ public class UserEndpoint { /* check */ final User user = userService.findById(userId); if (!user.equals(principal)) { - if (!UserUtil.hasRole(principal, "admin")) { + if (!UserUtil.hasRole(principal, "find-foreign-user")) { log.error("Failed to find user: foreign user"); throw new NotAllowedException("Failed to find user: foreign user"); } @@ -360,8 +364,8 @@ public class UserEndpoint { public ResponseEntity<Void> password(@NotNull @PathVariable("userId") UUID userId, @NotNull @Valid @RequestBody UserPasswordDto data, @NotNull Principal principal) throws NotAllowedException, AuthServiceException, - AuthServiceConnectionException, UserNotFoundException, DatabaseNotFoundException, ServiceException, - ServiceConnectionException, CredentialsInvalidException { + AuthServiceConnectionException, UserNotFoundException, DatabaseNotFoundException, DataServiceException, + DataServiceConnectionException, CredentialsInvalidException { log.debug("endpoint modify a user password, userId={}, data.password=(hidden)", userId); User user = userService.findById(userId); if (!user.equals(principal)) { diff --git a/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/ViewEndpoint.java b/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/ViewEndpoint.java index a22bdff267..79981ee6d1 100644 --- a/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/ViewEndpoint.java +++ b/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/ViewEndpoint.java @@ -4,7 +4,6 @@ import at.tuwien.api.database.ViewBriefDto; import at.tuwien.api.database.ViewCreateDto; import at.tuwien.api.database.ViewDto; import at.tuwien.api.error.ApiErrorDto; -import at.tuwien.config.KeycloakConfig; import at.tuwien.entities.database.Database; import at.tuwien.entities.database.View; import at.tuwien.entities.user.User; @@ -13,6 +12,7 @@ import at.tuwien.mapper.MetadataMapper; import at.tuwien.service.DatabaseService; import at.tuwien.service.UserService; import at.tuwien.service.ViewService; +import at.tuwien.utils.UserUtil; import io.micrometer.observation.annotation.Observed; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.ArraySchema; @@ -29,7 +29,6 @@ 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.security.core.Authentication; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.*; @@ -136,7 +135,7 @@ public class ViewEndpoint { public ResponseEntity<ViewBriefDto> create(@NotNull @PathVariable("databaseId") Long databaseId, @NotNull @Valid @RequestBody ViewCreateDto data, @NotNull Principal principal) throws NotAllowedException, - MalformedException, ServiceException, ServiceConnectionException, DatabaseNotFoundException, + MalformedException, DataServiceException, DataServiceConnectionException, DatabaseNotFoundException, UserNotFoundException, SearchServiceException, SearchServiceConnectionException { log.debug("endpoint create view, databaseId={}, data={}", databaseId, data); final Database database = databaseService.findById(databaseId); @@ -184,17 +183,14 @@ public class ViewEndpoint { final Database database = databaseService.findById(databaseId); final View view = viewService.findById(database, viewId); final HttpHeaders headers = new HttpHeaders(); - if (principal != null) { - final Authentication authentication = (Authentication) principal; - if (authentication.isAuthenticated() && authentication.getAuthorities().stream().anyMatch(a -> a.getAuthority().equals("admin"))) { - headers.set("X-Username", view.getDatabase().getContainer().getPrivilegedUsername()); - headers.set("X-Password", view.getDatabase().getContainer().getPrivilegedPassword()); - headers.set("X-Host", view.getDatabase().getContainer().getHost()); - headers.set("X-Port", "" + view.getDatabase().getContainer().getPort()); - headers.set("X-Type", view.getDatabase().getContainer().getImage().getJdbcMethod()); - headers.set("X-Database", view.getDatabase().getInternalName()); - headers.set("Access-Control-Expose-Headers", "X-Username X-Password X-Host X-Port X-Type X-Database"); - } + if (UserUtil.isSystem(principal)) { + headers.set("X-Username", view.getDatabase().getContainer().getPrivilegedUsername()); + headers.set("X-Password", view.getDatabase().getContainer().getPrivilegedPassword()); + headers.set("X-Host", view.getDatabase().getContainer().getHost()); + headers.set("X-Port", "" + view.getDatabase().getContainer().getPort()); + headers.set("X-Type", view.getDatabase().getContainer().getImage().getJdbcMethod()); + headers.set("X-Database", view.getDatabase().getInternalName()); + headers.set("Access-Control-Expose-Headers", "X-Username X-Password X-Host X-Port X-Type X-Database"); } return ResponseEntity.status(HttpStatus.OK) .headers(headers) @@ -244,8 +240,8 @@ public class ViewEndpoint { }) public ResponseEntity<View> delete(@NotNull @PathVariable("databaseId") Long databaseId, @NotNull @PathVariable("viewId") Long viewId, - @NotNull Principal principal) throws NotAllowedException, ServiceException, - ServiceConnectionException, DatabaseNotFoundException, ViewNotFoundException, SearchServiceException, + @NotNull Principal principal) throws NotAllowedException, DataServiceException, + DataServiceConnectionException, DatabaseNotFoundException, ViewNotFoundException, SearchServiceException, SearchServiceConnectionException { log.debug("endpoint delete view, databaseId={}, viewId={}", databaseId, viewId); final Database database = databaseService.findById(databaseId); diff --git a/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/handlers/ApiExceptionHandler.java b/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/handlers/ApiExceptionHandler.java index 6b981eb62f..f676489555 100644 --- a/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/handlers/ApiExceptionHandler.java +++ b/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/handlers/ApiExceptionHandler.java @@ -44,6 +44,20 @@ public class ApiExceptionHandler extends ResponseEntityExceptionHandler { return generic_handle(e.getClass(), e.getLocalizedMessage()); } + @Hidden + @ResponseStatus(code = HttpStatus.BAD_GATEWAY) + @ExceptionHandler(BrokerServiceConnectionException.class) + public ResponseEntity<ApiErrorDto> handle(BrokerServiceConnectionException e) { + return generic_handle(e.getClass(), e.getLocalizedMessage()); + } + + @Hidden + @ResponseStatus(code = HttpStatus.SERVICE_UNAVAILABLE) + @ExceptionHandler(BrokerServiceException.class) + public ResponseEntity<ApiErrorDto> handle(BrokerServiceException e) { + return generic_handle(e.getClass(), e.getLocalizedMessage()); + } + @Hidden @ResponseStatus(code = HttpStatus.NOT_FOUND) @ExceptionHandler(ConceptNotFoundException.class) @@ -72,6 +86,13 @@ public class ApiExceptionHandler extends ResponseEntityExceptionHandler { return generic_handle(e.getClass(), e.getLocalizedMessage()); } + @Hidden + @ResponseStatus(code = HttpStatus.EXPECTATION_FAILED) + @ExceptionHandler(DatabaseMalformedException.class) + public ResponseEntity<ApiErrorDto> handle(DatabaseMalformedException e) { + return generic_handle(e.getClass(), e.getLocalizedMessage()); + } + @Hidden @ResponseStatus(code = HttpStatus.NOT_FOUND) @ExceptionHandler(DatabaseNotFoundException.class) @@ -79,6 +100,13 @@ public class ApiExceptionHandler extends ResponseEntityExceptionHandler { return generic_handle(e.getClass(), e.getLocalizedMessage()); } + @Hidden + @ResponseStatus(code = HttpStatus.SERVICE_UNAVAILABLE) + @ExceptionHandler(DatabaseUnavailableException.class) + public ResponseEntity<ApiErrorDto> handle(DatabaseUnavailableException e) { + return generic_handle(e.getClass(), e.getLocalizedMessage()); + } + @Hidden @ResponseStatus(code = HttpStatus.NOT_FOUND) @ExceptionHandler(DoiNotFoundException.class) @@ -100,30 +128,37 @@ public class ApiExceptionHandler extends ResponseEntityExceptionHandler { return generic_handle(e.getClass(), e.getLocalizedMessage()); } + @Hidden + @ResponseStatus(code = HttpStatus.SERVICE_UNAVAILABLE) + @ExceptionHandler(ExternalServiceException.class) + public ResponseEntity<ApiErrorDto> handle(ExternalServiceException e) { + return generic_handle(e.getClass(), e.getLocalizedMessage()); + } + @Hidden @ResponseStatus(code = HttpStatus.BAD_REQUEST) - @ExceptionHandler({FilterBadRequestException.class}) + @ExceptionHandler(FilterBadRequestException.class) public ResponseEntity<ApiErrorDto> handle(FilterBadRequestException e) { return generic_handle(e.getClass(), e.getLocalizedMessage()); } @Hidden @ResponseStatus(code = HttpStatus.NOT_ACCEPTABLE) - @ExceptionHandler({FormatNotAvailableException.class}) + @ExceptionHandler(FormatNotAvailableException.class) public ResponseEntity<ApiErrorDto> handle(FormatNotAvailableException e) { return generic_handle(e.getClass(), e.getLocalizedMessage()); } @Hidden @ResponseStatus(code = HttpStatus.NOT_FOUND) - @ExceptionHandler({IdentifierNotFoundException.class}) + @ExceptionHandler(IdentifierNotFoundException.class) public ResponseEntity<ApiErrorDto> handle(IdentifierNotFoundException e) { return generic_handle(e.getClass(), e.getLocalizedMessage()); } @Hidden @ResponseStatus(code = HttpStatus.NOT_FOUND) - @ExceptionHandler({IdentifierNotSupportedException.class}) + @ExceptionHandler(IdentifierNotSupportedException.class) public ResponseEntity<ApiErrorDto> handle(IdentifierNotSupportedException e) { return generic_handle(e.getClass(), e.getLocalizedMessage()); } @@ -158,18 +193,32 @@ public class ApiExceptionHandler extends ResponseEntityExceptionHandler { @Hidden @ResponseStatus(code = HttpStatus.BAD_REQUEST) - @ExceptionHandler({MalformedException.class}) + @ExceptionHandler(MalformedException.class) public ResponseEntity<ApiErrorDto> handle(MalformedException e) { return generic_handle(e.getClass(), e.getLocalizedMessage()); } @Hidden @ResponseStatus(code = HttpStatus.NOT_FOUND) - @ExceptionHandler({MessageNotFoundException.class}) + @ExceptionHandler(MessageNotFoundException.class) public ResponseEntity<ApiErrorDto> handle(MessageNotFoundException e) { return generic_handle(e.getClass(), e.getLocalizedMessage()); } + @Hidden + @ResponseStatus(code = HttpStatus.BAD_GATEWAY) + @ExceptionHandler(MetadataServiceConnectionException.class) + public ResponseEntity<ApiErrorDto> handle(MetadataServiceConnectionException e) { + return generic_handle(e.getClass(), e.getLocalizedMessage()); + } + + @Hidden + @ResponseStatus(code = HttpStatus.SERVICE_UNAVAILABLE) + @ExceptionHandler(MetadataServiceException.class) + public ResponseEntity<ApiErrorDto> handle(MetadataServiceException e) { + return generic_handle(e.getClass(), e.getLocalizedMessage()); + } + @Hidden @ResponseStatus(code = HttpStatus.FORBIDDEN) @ExceptionHandler(NotAllowedException.class) @@ -193,11 +242,18 @@ public class ApiExceptionHandler extends ResponseEntityExceptionHandler { @Hidden @ResponseStatus(code = HttpStatus.BAD_REQUEST) - @ExceptionHandler({PaginationException.class}) + @ExceptionHandler(PaginationException.class) public ResponseEntity<ApiErrorDto> handle(PaginationException e) { return generic_handle(e.getClass(), e.getLocalizedMessage()); } + @Hidden + @ResponseStatus(code = HttpStatus.BAD_REQUEST) + @ExceptionHandler(QueryMalformedException.class) + public ResponseEntity<ApiErrorDto> handle(QueryMalformedException e) { + return generic_handle(e.getClass(), e.getLocalizedMessage()); + } + @Hidden @ResponseStatus(code = HttpStatus.NOT_FOUND) @ExceptionHandler(QueryNotFoundException.class) @@ -205,52 +261,108 @@ public class ApiExceptionHandler extends ResponseEntityExceptionHandler { return generic_handle(e.getClass(), e.getLocalizedMessage()); } + @Hidden + @ResponseStatus(code = HttpStatus.NOT_IMPLEMENTED) + @ExceptionHandler(QueryNotSupportedException.class) + public ResponseEntity<ApiErrorDto> handle(QueryNotSupportedException e) { + return generic_handle(e.getClass(), e.getLocalizedMessage()); + } + @Hidden @ResponseStatus(code = HttpStatus.NOT_FOUND) - @ExceptionHandler({QueueNotFoundException.class}) + @ExceptionHandler(QueueNotFoundException.class) public ResponseEntity<ApiErrorDto> handle(QueueNotFoundException e) { return generic_handle(e.getClass(), e.getLocalizedMessage()); } + @Hidden + @ResponseStatus(code = HttpStatus.EXPECTATION_FAILED) + @ExceptionHandler(QueryStoreCreateException.class) + public ResponseEntity<ApiErrorDto> handle(QueryStoreCreateException e) { + return generic_handle(e.getClass(), e.getLocalizedMessage()); + } + + @Hidden + @ResponseStatus(code = HttpStatus.BAD_REQUEST) + @ExceptionHandler(QueryStoreGCException.class) + public ResponseEntity<ApiErrorDto> handle(QueryStoreGCException e) { + return generic_handle(e.getClass(), e.getLocalizedMessage()); + } + + @Hidden + @ResponseStatus(code = HttpStatus.EXPECTATION_FAILED) + @ExceptionHandler(QueryStoreInsertException.class) + public ResponseEntity<ApiErrorDto> handle(QueryStoreInsertException e) { + return generic_handle(e.getClass(), e.getLocalizedMessage()); + } + + @Hidden + @ResponseStatus(code = HttpStatus.EXPECTATION_FAILED) + @ExceptionHandler(QueryStorePersistException.class) + public ResponseEntity<ApiErrorDto> handle(QueryStorePersistException e) { + return generic_handle(e.getClass(), e.getLocalizedMessage()); + } + + @Hidden + @ResponseStatus(code = HttpStatus.SERVICE_UNAVAILABLE) + @ExceptionHandler(RemoteUnavailableException.class) + public ResponseEntity<ApiErrorDto> handle(RemoteUnavailableException e) { + return generic_handle(e.getClass(), e.getLocalizedMessage()); + } + @Hidden @ResponseStatus(code = HttpStatus.NOT_FOUND) - @ExceptionHandler({RorNotFoundException.class}) + @ExceptionHandler(RorNotFoundException.class) public ResponseEntity<ApiErrorDto> handle(RorNotFoundException e) { return generic_handle(e.getClass(), e.getLocalizedMessage()); } @Hidden @ResponseStatus(code = HttpStatus.BAD_GATEWAY) - @ExceptionHandler({SearchServiceConnectionException.class}) + @ExceptionHandler(SearchServiceConnectionException.class) public ResponseEntity<ApiErrorDto> handle(SearchServiceConnectionException e) { return generic_handle(e.getClass(), e.getLocalizedMessage()); } @Hidden @ResponseStatus(code = HttpStatus.SERVICE_UNAVAILABLE) - @ExceptionHandler({SearchServiceException.class}) + @ExceptionHandler(SearchServiceException.class) public ResponseEntity<ApiErrorDto> handle(SearchServiceException e) { return generic_handle(e.getClass(), e.getLocalizedMessage()); } @Hidden @ResponseStatus(code = HttpStatus.NOT_FOUND) - @ExceptionHandler({SemanticEntityNotFoundException.class}) + @ExceptionHandler(SemanticEntityNotFoundException.class) public ResponseEntity<ApiErrorDto> handle(SemanticEntityNotFoundException e) { return generic_handle(e.getClass(), e.getLocalizedMessage()); } @Hidden @ResponseStatus(code = HttpStatus.BAD_GATEWAY) - @ExceptionHandler({ServiceConnectionException.class}) - public ResponseEntity<ApiErrorDto> handle(ServiceConnectionException e) { + @ExceptionHandler(DataServiceConnectionException.class) + public ResponseEntity<ApiErrorDto> handle(DataServiceConnectionException e) { return generic_handle(e.getClass(), e.getLocalizedMessage()); } @Hidden @ResponseStatus(code = HttpStatus.SERVICE_UNAVAILABLE) - @ExceptionHandler({ServiceException.class}) - public ResponseEntity<ApiErrorDto> handle(ServiceException e) { + @ExceptionHandler(DataServiceException.class) + public ResponseEntity<ApiErrorDto> handle(DataServiceException e) { + return generic_handle(e.getClass(), e.getLocalizedMessage()); + } + + @Hidden + @ResponseStatus(code = HttpStatus.SERVICE_UNAVAILABLE) + @ExceptionHandler(SidecarExportException.class) + public ResponseEntity<ApiErrorDto> handle(SidecarExportException e) { + return generic_handle(e.getClass(), e.getLocalizedMessage()); + } + + @Hidden + @ResponseStatus(code = HttpStatus.SERVICE_UNAVAILABLE) + @ExceptionHandler(SidecarImportException.class) + public ResponseEntity<ApiErrorDto> handle(SidecarImportException e) { return generic_handle(e.getClass(), e.getLocalizedMessage()); } @@ -282,23 +394,37 @@ public class ApiExceptionHandler extends ResponseEntityExceptionHandler { return generic_handle(e.getClass(), e.getLocalizedMessage()); } + @Hidden + @ResponseStatus(code = HttpStatus.BAD_REQUEST) + @ExceptionHandler(TableMalformedException.class) + public ResponseEntity<ApiErrorDto> handle(TableMalformedException e) { + return generic_handle(e.getClass(), e.getLocalizedMessage()); + } + + @Hidden + @ResponseStatus(code = HttpStatus.CONFLICT) + @ExceptionHandler(TableSchemaException.class) + public ResponseEntity<ApiErrorDto> handle(TableSchemaException e) { + return generic_handle(e.getClass(), e.getLocalizedMessage()); + } + @Hidden @ResponseStatus(code = HttpStatus.NOT_FOUND) - @ExceptionHandler({TableNotFoundException.class}) + @ExceptionHandler(TableNotFoundException.class) public ResponseEntity<ApiErrorDto> handle(TableNotFoundException e) { return generic_handle(e.getClass(), e.getLocalizedMessage()); } @Hidden @ResponseStatus(code = HttpStatus.NOT_FOUND) - @ExceptionHandler({UnitNotFoundException.class}) + @ExceptionHandler(UnitNotFoundException.class) public ResponseEntity<ApiErrorDto> handle(UnitNotFoundException e) { return generic_handle(e.getClass(), e.getLocalizedMessage()); } @Hidden @ResponseStatus(code = HttpStatus.EXPECTATION_FAILED) - @ExceptionHandler({UriMalformedException.class}) + @ExceptionHandler(UriMalformedException.class) public ResponseEntity<ApiErrorDto> handle(UriMalformedException e) { return generic_handle(e.getClass(), e.getLocalizedMessage()); } @@ -317,6 +443,13 @@ public class ApiExceptionHandler extends ResponseEntityExceptionHandler { return generic_handle(e.getClass(), e.getLocalizedMessage()); } + @Hidden + @ResponseStatus(code = HttpStatus.BAD_REQUEST) + @ExceptionHandler(ViewMalformedException.class) + public ResponseEntity<ApiErrorDto> handle(ViewMalformedException e) { + return generic_handle(e.getClass(), e.getLocalizedMessage()); + } + @Hidden @ResponseStatus(code = HttpStatus.NOT_FOUND) @ExceptionHandler(ViewNotFoundException.class) @@ -324,6 +457,13 @@ public class ApiExceptionHandler extends ResponseEntityExceptionHandler { return generic_handle(e.getClass(), e.getLocalizedMessage()); } + @Hidden + @ResponseStatus(code = HttpStatus.CONFLICT) + @ExceptionHandler(ViewSchemaException.class) + public ResponseEntity<ApiErrorDto> handle(ViewSchemaException e) { + return generic_handle(e.getClass(), e.getLocalizedMessage()); + } + private ResponseEntity<ApiErrorDto> generic_handle(Class<?> exceptionClass, String message) { final HttpHeaders headers = new HttpHeaders(); headers.set("Content-Type", "application/problem+json"); diff --git a/dbrepo-metadata-service/rest-service/src/main/resources/application-local.yml b/dbrepo-metadata-service/rest-service/src/main/resources/application-local.yml index fd44a809ca..63675da565 100644 --- a/dbrepo-metadata-service/rest-service/src/main/resources/application-local.yml +++ b/dbrepo-metadata-service/rest-service/src/main/resources/application-local.yml @@ -1,10 +1,4 @@ spring: - ldap: - urls: ldap://localhost:1389 - userDn: cn=admin,dc=dbrepo,dc=at - password: adminpassword - base: dc=dbrepo,dc=at - adminDn: cn=admins,ou=users,dc=dbrepo,dc=at datasource: url: jdbc:mariadb://localhost:3306/dbrepo driver-class-name: org.mariadb.jdbc.Driver @@ -66,7 +60,7 @@ dbrepo: secretAccessKey: seaweedfsadmin importBucket: dbrepo-upload exportBucket: dbrepo-download - admin: + system: username: admin password: admin endpoints: diff --git a/dbrepo-metadata-service/rest-service/src/main/resources/application.yml b/dbrepo-metadata-service/rest-service/src/main/resources/application.yml index 0552cce9cb..9398db2b54 100644 --- a/dbrepo-metadata-service/rest-service/src/main/resources/application.yml +++ b/dbrepo-metadata-service/rest-service/src/main/resources/application.yml @@ -2,12 +2,6 @@ application: title: DBRepo version: '@project.version@' spring: - ldap: - urls: "${IDENTITY_SERVICE_URLS:ldap://identity-service:1389}" - userDn: "${IDENTITY_SERVICE_USERNAME:cn=admin,dc=dbrepo,dc=at}" - password: "${IDENTITY_SERVICE_PASSWORD:adminpassword}" - base: "${IDENTITY_SERVICE_BASE:dc=dbrepo,dc=at}" - adminDn: "${IDENTITY_SERVICE_ADMIN_GROUP:cn=admins,ou=users,dc=dbrepo,dc=at}" datasource: url: "jdbc:mariadb://${METADATA_HOST:metadata-db}:3306/${METADATA_DB:dbrepo}${METADATA_JDBC_EXTRA_ARGS}" driver-class-name: org.mariadb.jdbc.Driver @@ -68,9 +62,9 @@ dbrepo: secretAccessKey: "${S3_SECRET_ACCESS_KEY:seaweedfsadmin}" importBucket: "${S3_IMPORT_BUCKET:dbrepo-upload}" exportBucket: "${S3_EXPORT_BUCKET:dbrepo-download}" - admin: - username: "${ADMIN_USERNAME:admin}" - password: "${ADMIN_PASSWORD:admin}" + system: + username: "${SYSTEM_USERNAME:admin}" + password: "${SYSTEM_PASSWORD:admin}" endpoints: searchService: "${SEARCH_SERVICE_ENDPOINT:http://gateway-service}" analyseService: "${ANALYSE_SERVICE_ENDPOINT:http://gateway-service}" diff --git a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/config/RabbitConfig.java b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/config/RabbitConfig.java new file mode 100644 index 0000000000..f8a83baf85 --- /dev/null +++ b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/config/RabbitConfig.java @@ -0,0 +1,41 @@ +package at.tuwien.config; + +import at.tuwien.test.BaseTest; +import lombok.Getter; +import lombok.extern.log4j.Log4j2; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; +import org.springframework.http.client.support.BasicAuthenticationInterceptor; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.DefaultUriBuilderFactory; + +@Getter +@Log4j2 +@Configuration +public class RabbitConfig extends BaseTest { + + @Value("${dbrepo.exchangeName}") + private String exchangeName; + + @Value("${dbrepo.queueName}") + private String queueName; + + @Value("${spring.rabbitmq.virtual-host}") + private String virtualHost; + + @Value("${dbrepo.endpoints.brokerService}") + private String brokerEndpoint; + + @Bean + @Primary + public RestTemplate brokerRestTemplate() { + final RestTemplate restTemplate = new RestTemplate(); + restTemplate.setUriTemplateHandler(new DefaultUriBuilderFactory(brokerEndpoint)); + restTemplate.getInterceptors() + .add(new BasicAuthenticationInterceptor(USER_1_USERNAME, USER_1_PASSWORD)); + return restTemplate; + } + +} diff --git a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/converters/IdentifierTypeConverterUnitTest.java b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/converters/IdentifierTypeConverterUnitTest.java new file mode 100644 index 0000000000..7215e5db91 --- /dev/null +++ b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/converters/IdentifierTypeConverterUnitTest.java @@ -0,0 +1,41 @@ +package at.tuwien.converters; + +import at.tuwien.api.identifier.IdentifierTypeDto; +import at.tuwien.test.AbstractUnitTest; +import lombok.extern.log4j.Log4j2; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import static org.junit.jupiter.api.Assertions.*; + +@Log4j2 +@SpringBootTest +public class IdentifierTypeConverterUnitTest extends AbstractUnitTest { + + @Autowired + private IdentifierTypeConverter identifierTypeConverter; + + @BeforeEach + public void beforeEach() { + genesis(); + } + + @Test + public void identifierTypeConverter_succeeds() { + + /* test */ + final IdentifierTypeDto response = identifierTypeConverter.convert(IdentifierTypeDto.DATABASE.getName()); + assertEquals(IdentifierTypeDto.DATABASE, response); + } + + @Test + public void identifierTypeConverter_fails() { + + /* test */ + assertThrows(IllegalArgumentException.class, () -> { + identifierTypeConverter.convert("i_do_not_exist"); + }); + } +} diff --git a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/endpoints/AccessEndpointUnitTest.java b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/endpoints/AccessEndpointUnitTest.java index 16c212a546..69d817afb7 100644 --- a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/endpoints/AccessEndpointUnitTest.java +++ b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/endpoints/AccessEndpointUnitTest.java @@ -73,7 +73,7 @@ public class AccessEndpointUnitTest extends AbstractUnitTest { @Test @WithMockUser(username = USER_1_USERNAME, authorities = {"create-database-access"}) - public void create_succeeds() throws ServiceException, ServiceConnectionException, NotAllowedException, + public void create_succeeds() throws DataServiceException, DataServiceConnectionException, NotAllowedException, DatabaseNotFoundException, UserNotFoundException, AccessNotFoundException, SearchServiceException, SearchServiceConnectionException { @@ -115,12 +115,12 @@ public class AccessEndpointUnitTest extends AbstractUnitTest { } @Test - @WithMockUser(username = USER_1_USERNAME, authorities = {"admin"}) + @WithMockUser(username = USER_1_USERNAME, authorities = {"check-foreign-database-access"}) public void find_hasRoleHasAccessForeign_succeeds() throws UserNotFoundException, NotAllowedException, DatabaseNotFoundException, AccessNotFoundException { /* test */ - generic_find(DATABASE_1_ID, DATABASE_1, DATABASE_1_USER_1_READ_ACCESS, USER_LOCAL_ADMIN_PRINCIPAL, USER_1_ID, USER_1); + generic_find(DATABASE_1_ID, DATABASE_1, DATABASE_1_USER_1_READ_ACCESS, USER_1_PRINCIPAL, USER_1_ID, USER_1); } @Test @@ -155,7 +155,7 @@ public class AccessEndpointUnitTest extends AbstractUnitTest { @Test @WithMockUser(username = USER_1_USERNAME, authorities = {"update-database-access"}) - public void update_succeeds() throws NotAllowedException, ServiceException, ServiceConnectionException, + public void update_succeeds() throws NotAllowedException, DataServiceException, DataServiceConnectionException, AccessNotFoundException, DatabaseNotFoundException, UserNotFoundException, SearchServiceException, SearchServiceConnectionException { @@ -190,7 +190,7 @@ public class AccessEndpointUnitTest extends AbstractUnitTest { @Test @WithMockUser(username = USER_1_USERNAME, authorities = {"delete-database-access"}) - public void revoke_succeeds() throws NotAllowedException, ServiceException, ServiceConnectionException, + public void revoke_succeeds() throws NotAllowedException, DataServiceException, DataServiceConnectionException, UserNotFoundException, DatabaseNotFoundException, AccessNotFoundException, SearchServiceException, SearchServiceConnectionException { @@ -208,7 +208,7 @@ public class AccessEndpointUnitTest extends AbstractUnitTest { /* ################################################################################################### */ protected void generic_create(Principal principal, UUID userId, String username, User user) - throws NotAllowedException, ServiceException, ServiceConnectionException, UserNotFoundException, + throws NotAllowedException, DataServiceException, DataServiceConnectionException, UserNotFoundException, DatabaseNotFoundException, AccessNotFoundException, SearchServiceException, SearchServiceConnectionException { @@ -269,7 +269,7 @@ public class AccessEndpointUnitTest extends AbstractUnitTest { } protected void generic_update(DatabaseAccess access, String otherUsername, User otherUser, Principal principal, - User user) throws NotAllowedException, ServiceException, ServiceConnectionException, + User user) throws NotAllowedException, DataServiceException, DataServiceConnectionException, AccessNotFoundException, UserNotFoundException, DatabaseNotFoundException, SearchServiceException, SearchServiceConnectionException { @@ -307,8 +307,8 @@ public class AccessEndpointUnitTest extends AbstractUnitTest { assertNull(response.getBody()); } - protected void generic_revoke(Principal principal, User user) throws ServiceConnectionException, - NotAllowedException, ServiceException, UserNotFoundException, DatabaseNotFoundException, + protected void generic_revoke(Principal principal, User user) throws DataServiceConnectionException, + NotAllowedException, DataServiceException, UserNotFoundException, DatabaseNotFoundException, AccessNotFoundException, SearchServiceException, SearchServiceConnectionException { /* mock */ diff --git a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/endpoints/ContainerEndpointUnitTest.java b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/endpoints/ContainerEndpointUnitTest.java index cb230377f3..7706e185bd 100644 --- a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/endpoints/ContainerEndpointUnitTest.java +++ b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/endpoints/ContainerEndpointUnitTest.java @@ -42,7 +42,7 @@ public class ContainerEndpointUnitTest extends AbstractUnitTest { public void findById_anonymous_succeeds() throws ContainerNotFoundException { /* test */ - findById_generic(CONTAINER_1_ID, CONTAINER_1, null, false); + findById_generic(CONTAINER_1_ID, CONTAINER_1, null); } @Test @@ -50,7 +50,7 @@ public class ContainerEndpointUnitTest extends AbstractUnitTest { public void findById_hasRole_succeeds() throws ContainerNotFoundException { /* test */ - findById_generic(CONTAINER_1_ID, CONTAINER_1, USER_1_PRINCIPAL, false); + findById_generic(CONTAINER_1_ID, CONTAINER_1, USER_1_PRINCIPAL); } @Test @@ -58,15 +58,7 @@ public class ContainerEndpointUnitTest extends AbstractUnitTest { public void findById_noRole_succeeds() throws ContainerNotFoundException { /* test */ - findById_generic(CONTAINER_1_ID, CONTAINER_1, USER_4_PRINCIPAL, false); - } - - @Test - @WithMockUser(username = USER_LOCAL_ADMIN_USERNAME, authorities = {"admin"}) - public void findById_admin_succeeds() throws ContainerNotFoundException { - - /* test */ - findById_generic(CONTAINER_1_ID, CONTAINER_1, USER_LOCAL_ADMIN_PRINCIPAL, true); + findById_generic(CONTAINER_1_ID, CONTAINER_1, USER_4_PRINCIPAL); } @Test @@ -173,7 +165,7 @@ public class ContainerEndpointUnitTest extends AbstractUnitTest { /* ## GENERIC TEST CASES ## */ /* ################################################################################################### */ - public void findById_generic(Long containerId, Container container, Principal principal, Boolean isAdmin) + public void findById_generic(Long containerId, Container container, Principal principal) throws ContainerNotFoundException { /* mock */ @@ -184,15 +176,6 @@ public class ContainerEndpointUnitTest extends AbstractUnitTest { final ResponseEntity<ContainerDto> response = containerEndpoint.findById(containerId, principal); assertEquals(HttpStatus.OK, response.getStatusCode()); assertNotNull(response.getBody()); - if (isAdmin) { - assertNotNull(response.getHeaders()); - final List<String> xUsername = response.getHeaders().get("X-Username"); - assertNotNull(xUsername); - assertEquals(CONTAINER_1_PRIVILEGED_USERNAME, xUsername.get(0)); - final List<String> xPassword = response.getHeaders().get("X-Password"); - assertNotNull(xPassword); - assertEquals(CONTAINER_1_PRIVILEGED_PASSWORD, xPassword.get(0)); - } } public void delete_generic(Long containerId, Container container) throws ContainerNotFoundException { diff --git a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/endpoints/DatabaseEndpointUnitTest.java b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/endpoints/DatabaseEndpointUnitTest.java index 7e53274f50..02ba52ecaa 100644 --- a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/endpoints/DatabaseEndpointUnitTest.java +++ b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/endpoints/DatabaseEndpointUnitTest.java @@ -94,10 +94,10 @@ public class DatabaseEndpointUnitTest extends AbstractUnitTest { @Test @WithMockUser(username = USER_1_USERNAME, authorities = {"create-database"}) - public void create_succeeds() throws ServiceException, ServiceConnectionException, UserNotFoundException, + public void create_succeeds() throws DataServiceException, DataServiceConnectionException, UserNotFoundException, DatabaseNotFoundException, ContainerNotFoundException, SearchServiceException, SearchServiceConnectionException, AuthServiceException, AuthServiceConnectionException, - CredentialsInvalidException { + CredentialsInvalidException, BrokerServiceException, BrokerServiceConnectionException { final DatabaseCreateDto request = DatabaseCreateDto.builder() .cid(CONTAINER_1_ID) .name(DATABASE_1_NAME) @@ -302,7 +302,7 @@ public class DatabaseEndpointUnitTest extends AbstractUnitTest { @Test @WithMockUser(username = USER_1_USERNAME, authorities = {"modify-database-owner"}) - public void transfer_hasRole_succeeds() throws ServiceConnectionException, ServiceException, + public void transfer_hasRole_succeeds() throws DataServiceConnectionException, DataServiceException, NotAllowedException, UserNotFoundException, DatabaseNotFoundException, SearchServiceException, SearchServiceConnectionException, AuthServiceException, AuthServiceConnectionException, CredentialsInvalidException { @@ -345,7 +345,7 @@ public class DatabaseEndpointUnitTest extends AbstractUnitTest { @Test @WithAnonymousUser - public void findById_anonymous_succeeds() throws ServiceException, ServiceConnectionException, + public void findById_anonymous_succeeds() throws DataServiceException, DataServiceConnectionException, DatabaseNotFoundException, ExchangeNotFoundException { /* test */ @@ -364,7 +364,7 @@ public class DatabaseEndpointUnitTest extends AbstractUnitTest { @Test @WithMockUser(username = USER_1_USERNAME, authorities = {"find-database"}) - public void findById_hasRole_succeeds() throws ServiceException, ServiceConnectionException, + public void findById_hasRole_succeeds() throws DataServiceException, DataServiceConnectionException, DatabaseNotFoundException, ExchangeNotFoundException { /* pre-condition */ @@ -376,7 +376,7 @@ public class DatabaseEndpointUnitTest extends AbstractUnitTest { @Test @WithMockUser(username = USER_1_USERNAME, authorities = {"find-database"}) - public void findById_hasRoleForeign_succeeds() throws ServiceException, ServiceConnectionException, + public void findById_hasRoleForeign_succeeds() throws DataServiceException, DataServiceConnectionException, DatabaseNotFoundException, ExchangeNotFoundException { /* pre-condition */ @@ -388,7 +388,7 @@ public class DatabaseEndpointUnitTest extends AbstractUnitTest { @Test @WithMockUser(username = USER_1_USERNAME, authorities = {"find-database"}) - public void findById_ownerSeesAccessRights_succeeds() throws ServiceException, ServiceConnectionException, + public void findById_ownerSeesAccessRights_succeeds() throws DataServiceException, DataServiceConnectionException, DatabaseNotFoundException, ExchangeNotFoundException { /* mock */ @@ -430,9 +430,10 @@ public class DatabaseEndpointUnitTest extends AbstractUnitTest { assertEquals(databases.size(), body.size()); } - public void create_generic(DatabaseCreateDto data, Principal principal, User user) throws ServiceException, - ServiceConnectionException, UserNotFoundException, DatabaseNotFoundException, ContainerNotFoundException, - SearchServiceException, SearchServiceConnectionException { + public void create_generic(DatabaseCreateDto data, Principal principal, User user) throws DataServiceException, + DataServiceConnectionException, UserNotFoundException, DatabaseNotFoundException, + ContainerNotFoundException, SearchServiceException, SearchServiceConnectionException, + BrokerServiceException, BrokerServiceConnectionException { /* mock */ doNothing() @@ -468,7 +469,8 @@ public class DatabaseEndpointUnitTest extends AbstractUnitTest { } public DatabaseDto findById_generic(Long databaseId, Database database, Principal principal) - throws ServiceException, ServiceConnectionException, DatabaseNotFoundException, ExchangeNotFoundException { + throws DataServiceConnectionException, DatabaseNotFoundException, ExchangeNotFoundException, + DataServiceException { /* mock */ if (database != null) { diff --git a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/endpoints/IdentifierEndpointUnitTest.java b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/endpoints/IdentifierEndpointUnitTest.java index 7a83d2558f..af98f87e57 100644 --- a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/endpoints/IdentifierEndpointUnitTest.java +++ b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/endpoints/IdentifierEndpointUnitTest.java @@ -78,8 +78,9 @@ public class IdentifierEndpointUnitTest extends AbstractUnitTest { @Test @WithAnonymousUser - public void find_json0_succeeds() throws IOException, MalformedException, ServiceException, - ServiceConnectionException, QueryNotFoundException, IdentifierNotFoundException, FormatNotAvailableException { + public void find_json0_succeeds() throws IOException, MalformedException, DataServiceException, + DataServiceConnectionException, QueryNotFoundException, IdentifierNotFoundException, + FormatNotAvailableException { final String accept = "application/json"; final IdentifierDto compare = objectMapper.readValue(FileUtils.readFileToString(new File("src/test/resources/json/metadata0.json"), StandardCharsets.UTF_8), IdentifierDto.class); @@ -109,8 +110,9 @@ public class IdentifierEndpointUnitTest extends AbstractUnitTest { @Test @WithAnonymousUser - public void find_json1_succeeds() throws IOException, MalformedException, ServiceException, - ServiceConnectionException, QueryNotFoundException, IdentifierNotFoundException, FormatNotAvailableException { + public void find_json1_succeeds() throws IOException, MalformedException, DataServiceException, + DataServiceConnectionException, QueryNotFoundException, IdentifierNotFoundException, + FormatNotAvailableException { final String accept = "application/json"; final IdentifierDto compare = objectMapper.readValue(FileUtils.readFileToString(new File("src/test/resources/json/metadata1.json"), StandardCharsets.UTF_8), IdentifierDto.class); @@ -160,8 +162,9 @@ public class IdentifierEndpointUnitTest extends AbstractUnitTest { @Test @WithAnonymousUser - public void find_csv_succeeds() throws IOException, MalformedException, ServiceException, - ServiceConnectionException, QueryNotFoundException, IdentifierNotFoundException, FormatNotAvailableException { + public void find_csv_succeeds() throws IOException, MalformedException, DataServiceException, + DataServiceConnectionException, QueryNotFoundException, IdentifierNotFoundException, + FormatNotAvailableException { final String accept = "text/csv"; final InputStreamResource compare = new InputStreamResource(FileUtils.openInputStream(new File("src/test/resources/csv/keyboard.csv"))); final InputStreamResource mock = new InputStreamResource(FileUtils.openInputStream(new File("src/test/resources/csv/keyboard.csv"))); @@ -182,7 +185,9 @@ public class IdentifierEndpointUnitTest extends AbstractUnitTest { @Test @Disabled("not testable with xml") - public void find_xml0_succeeds() throws IOException, MalformedException, ServiceException, ServiceConnectionException, IdentifierNotFoundException, QueryNotFoundException, FormatNotAvailableException { + public void find_xml0_succeeds() throws IOException, MalformedException, DataServiceException, + DataServiceConnectionException, IdentifierNotFoundException, QueryNotFoundException, + FormatNotAvailableException { final String accept = "text/xml"; final InputStreamResource compare = new InputStreamResource(FileUtils.openInputStream(new File("src/test/resources/xml/metadata0.xml"))); @@ -200,8 +205,9 @@ public class IdentifierEndpointUnitTest extends AbstractUnitTest { @Test @Disabled("not testable with xml") - public void find_xml1_succeeds() throws IOException, MalformedException, ServiceException, - ServiceConnectionException, QueryNotFoundException, IdentifierNotFoundException, FormatNotAvailableException { + public void find_xml1_succeeds() throws IOException, MalformedException, DataServiceException, + DataServiceConnectionException, QueryNotFoundException, IdentifierNotFoundException, + FormatNotAvailableException { final String accept = "text/xml"; final InputStreamResource compare = new InputStreamResource(FileUtils.openInputStream(new File("src/test/resources/xml/metadata1.xml"))); @@ -220,8 +226,9 @@ public class IdentifierEndpointUnitTest extends AbstractUnitTest { @Test @WithAnonymousUser - public void find_bibliography_succeeds() throws IOException, MalformedException, ServiceException, - ServiceConnectionException, QueryNotFoundException, IdentifierNotFoundException, FormatNotAvailableException { + public void find_bibliography_succeeds() throws IOException, MalformedException, DataServiceException, + DataServiceConnectionException, QueryNotFoundException, IdentifierNotFoundException, + FormatNotAvailableException { final String accept = "text/bibliography"; final String compare = FileUtils.readFileToString(new File("src/test/resources/bibliography/style_apa1.txt"), StandardCharsets.UTF_8); @@ -242,8 +249,9 @@ public class IdentifierEndpointUnitTest extends AbstractUnitTest { @Test @WithAnonymousUser - public void find_bibliographyApa0_succeeds() throws IOException, MalformedException, ServiceException, - ServiceConnectionException, QueryNotFoundException, IdentifierNotFoundException, FormatNotAvailableException { + public void find_bibliographyApa0_succeeds() throws IOException, MalformedException, DataServiceException, + DataServiceConnectionException, QueryNotFoundException, IdentifierNotFoundException, + FormatNotAvailableException { final String accept = "text/bibliography; style=apa"; final String compare = FileUtils.readFileToString(new File("src/test/resources/bibliography/style_apa0.txt"), StandardCharsets.UTF_8); @@ -264,8 +272,9 @@ public class IdentifierEndpointUnitTest extends AbstractUnitTest { @Test @WithAnonymousUser - public void find_bibliographyApa1_succeeds() throws IOException, MalformedException, ServiceException, - ServiceConnectionException, QueryNotFoundException, IdentifierNotFoundException, FormatNotAvailableException { + public void find_bibliographyApa1_succeeds() throws IOException, MalformedException, DataServiceException, + DataServiceConnectionException, QueryNotFoundException, IdentifierNotFoundException, + FormatNotAvailableException { final String accept = "text/bibliography; style=apa"; final String compare = FileUtils.readFileToString(new File("src/test/resources/bibliography/style_apa1.txt"), StandardCharsets.UTF_8); @@ -286,8 +295,9 @@ public class IdentifierEndpointUnitTest extends AbstractUnitTest { @Test @WithAnonymousUser - public void find_bibliographyApa2_succeeds() throws IOException, MalformedException, ServiceException, - ServiceConnectionException, QueryNotFoundException, IdentifierNotFoundException, FormatNotAvailableException { + public void find_bibliographyApa2_succeeds() throws IOException, MalformedException, DataServiceException, + DataServiceConnectionException, QueryNotFoundException, IdentifierNotFoundException, + FormatNotAvailableException { final String accept = "text/bibliography; style=apa"; final String compare = FileUtils.readFileToString(new File("src/test/resources/bibliography/style_apa2.txt"), StandardCharsets.UTF_8); @@ -308,8 +318,9 @@ public class IdentifierEndpointUnitTest extends AbstractUnitTest { @Test @WithAnonymousUser - public void find_bibliographyApa3_succeeds() throws IOException, MalformedException, ServiceException, - ServiceConnectionException, QueryNotFoundException, IdentifierNotFoundException, FormatNotAvailableException { + public void find_bibliographyApa3_succeeds() throws IOException, MalformedException, DataServiceException, + DataServiceConnectionException, QueryNotFoundException, IdentifierNotFoundException, + FormatNotAvailableException { final String accept = "text/bibliography; style=apa"; final String compare = FileUtils.readFileToString(new File("src/test/resources/bibliography/style_apa3.txt"), StandardCharsets.UTF_8); @@ -330,8 +341,9 @@ public class IdentifierEndpointUnitTest extends AbstractUnitTest { @Test @WithAnonymousUser - public void find_bibliographyApa4_succeeds() throws IOException, MalformedException, ServiceException, - ServiceConnectionException, QueryNotFoundException, IdentifierNotFoundException, FormatNotAvailableException { + public void find_bibliographyApa4_succeeds() throws IOException, MalformedException, DataServiceException, + DataServiceConnectionException, QueryNotFoundException, IdentifierNotFoundException, + FormatNotAvailableException { final String accept = "text/bibliography; style=apa"; final String compare = FileUtils.readFileToString(new File("src/test/resources/bibliography/style_apa4.txt"), StandardCharsets.UTF_8); @@ -352,8 +364,9 @@ public class IdentifierEndpointUnitTest extends AbstractUnitTest { @Test @WithAnonymousUser - public void find_bibliographyIeee0_succeeds() throws IOException, MalformedException, ServiceException, - ServiceConnectionException, QueryNotFoundException, IdentifierNotFoundException, FormatNotAvailableException { + public void find_bibliographyIeee0_succeeds() throws IOException, MalformedException, DataServiceException, + DataServiceConnectionException, QueryNotFoundException, IdentifierNotFoundException, + FormatNotAvailableException { final String accept = "text/bibliography; style=ieee"; final String compare = FileUtils.readFileToString(new File("src/test/resources/bibliography/style_ieee0.txt"), StandardCharsets.UTF_8); @@ -374,8 +387,9 @@ public class IdentifierEndpointUnitTest extends AbstractUnitTest { @Test @WithAnonymousUser - public void find_bibliographyIeee1_succeeds() throws IOException, MalformedException, ServiceException, - ServiceConnectionException, QueryNotFoundException, IdentifierNotFoundException, FormatNotAvailableException { + public void find_bibliographyIeee1_succeeds() throws IOException, MalformedException, DataServiceException, + DataServiceConnectionException, QueryNotFoundException, IdentifierNotFoundException, + FormatNotAvailableException { final String accept = "text/bibliography; style=ieee"; final String compare = FileUtils.readFileToString(new File("src/test/resources/bibliography/style_ieee1.txt"), StandardCharsets.UTF_8); @@ -396,8 +410,9 @@ public class IdentifierEndpointUnitTest extends AbstractUnitTest { @Test @WithAnonymousUser - public void find_bibliographyIeee2_succeeds() throws IOException, MalformedException, ServiceException, - ServiceConnectionException, QueryNotFoundException, IdentifierNotFoundException, FormatNotAvailableException { + public void find_bibliographyIeee2_succeeds() throws IOException, MalformedException, DataServiceException, + DataServiceConnectionException, QueryNotFoundException, IdentifierNotFoundException, + FormatNotAvailableException { final String accept = "text/bibliography; style=ieee"; final String compare = FileUtils.readFileToString(new File("src/test/resources/bibliography/style_ieee2.txt"), StandardCharsets.UTF_8); @@ -418,8 +433,9 @@ public class IdentifierEndpointUnitTest extends AbstractUnitTest { @Test @WithAnonymousUser - public void find_bibliographyIeee3_succeeds() throws IOException, MalformedException, ServiceException, - ServiceConnectionException, QueryNotFoundException, IdentifierNotFoundException, FormatNotAvailableException { + public void find_bibliographyIeee3_succeeds() throws IOException, MalformedException, DataServiceException, + DataServiceConnectionException, QueryNotFoundException, IdentifierNotFoundException, + FormatNotAvailableException { final String accept = "text/bibliography; style=ieee"; final String compare = FileUtils.readFileToString(new File("src/test/resources/bibliography/style_ieee3.txt"), StandardCharsets.UTF_8); @@ -440,8 +456,9 @@ public class IdentifierEndpointUnitTest extends AbstractUnitTest { @Test @WithAnonymousUser - public void find_bibliographyBibtex0_succeeds() throws IOException, MalformedException, ServiceException, - ServiceConnectionException, QueryNotFoundException, IdentifierNotFoundException, FormatNotAvailableException { + public void find_bibliographyBibtex0_succeeds() throws IOException, MalformedException, DataServiceException, + DataServiceConnectionException, QueryNotFoundException, IdentifierNotFoundException, + FormatNotAvailableException { final String accept = "text/bibliography; style=bibtex"; final String compare = FileUtils.readFileToString(new File("src/test/resources/bibliography/style_bibtex0.txt"), StandardCharsets.UTF_8); @@ -462,8 +479,9 @@ public class IdentifierEndpointUnitTest extends AbstractUnitTest { @Test @WithAnonymousUser - public void find_bibliographyBibtex1_succeeds() throws MalformedException, IOException, ServiceException, - ServiceConnectionException, QueryNotFoundException, IdentifierNotFoundException, FormatNotAvailableException { + public void find_bibliographyBibtex1_succeeds() throws MalformedException, IOException, DataServiceException, + DataServiceConnectionException, QueryNotFoundException, IdentifierNotFoundException, + FormatNotAvailableException { final String accept = "text/bibliography; style=bibtex"; final String compare = FileUtils.readFileToString(new File("src/test/resources/bibliography/style_bibtex1.txt"), StandardCharsets.UTF_8); @@ -484,8 +502,9 @@ public class IdentifierEndpointUnitTest extends AbstractUnitTest { @Test @WithAnonymousUser - public void find_bibliographyBibtex2_succeeds() throws MalformedException, ServiceException, IOException, - ServiceConnectionException, QueryNotFoundException, IdentifierNotFoundException, FormatNotAvailableException { + public void find_bibliographyBibtex2_succeeds() throws MalformedException, DataServiceException, IOException, + DataServiceConnectionException, QueryNotFoundException, IdentifierNotFoundException, + FormatNotAvailableException { final String accept = "text/bibliography; style=bibtex"; final String compare = FileUtils.readFileToString(new File("src/test/resources/bibliography/style_bibtex2.txt"), StandardCharsets.UTF_8); @@ -506,8 +525,8 @@ public class IdentifierEndpointUnitTest extends AbstractUnitTest { @Test @WithAnonymousUser - public void find_bibliographyBibtex3_succeeds() throws MalformedException, ServiceException, - ServiceConnectionException, IOException, QueryNotFoundException, IdentifierNotFoundException, + public void find_bibliographyBibtex3_succeeds() throws MalformedException, DataServiceException, + DataServiceConnectionException, IOException, QueryNotFoundException, IdentifierNotFoundException, FormatNotAvailableException { final String accept = "text/bibliography; style=bibtex"; final String compare = FileUtils.readFileToString(new File("src/test/resources/bibliography/style_bibtex3.txt"), @@ -545,9 +564,9 @@ public class IdentifierEndpointUnitTest extends AbstractUnitTest { @Test @WithMockUser(username = USER_2_USERNAME, authorities = {"delete-identifier"}) - public void delete_hasRole_succeeds() throws NotAllowedException, ServiceException, ServiceConnectionException, - DatabaseNotFoundException, IdentifierNotFoundException, SearchServiceException, - SearchServiceConnectionException { + public void delete_hasRole_succeeds() throws NotAllowedException, DataServiceException, + DataServiceConnectionException, DatabaseNotFoundException, IdentifierNotFoundException, + SearchServiceException, SearchServiceConnectionException { /* test */ this.generic_delete(); @@ -555,7 +574,7 @@ public class IdentifierEndpointUnitTest extends AbstractUnitTest { @Test @WithAnonymousUser - public void find_json_succeeds() throws MalformedException, ServiceException, ServiceConnectionException, + public void find_json_succeeds() throws MalformedException, DataServiceException, DataServiceConnectionException, FormatNotAvailableException, QueryNotFoundException, IdentifierNotFoundException { final String accept = "application/json"; @@ -582,7 +601,7 @@ public class IdentifierEndpointUnitTest extends AbstractUnitTest { @Test @WithAnonymousUser - public void find_xml_succeeds() throws MalformedException, ServiceException, ServiceConnectionException, + public void find_xml_succeeds() throws MalformedException, DataServiceException, DataServiceConnectionException, IOException, QueryNotFoundException, IdentifierNotFoundException, FormatNotAvailableException { final InputStreamResource resource = new InputStreamResource(FileUtils.openInputStream( new File("src/test/resources/xml/datacite-example-dataset-v4.xml"))); @@ -598,8 +617,9 @@ public class IdentifierEndpointUnitTest extends AbstractUnitTest { @Test @WithAnonymousUser - public void find_httpRedirect_succeeds() throws MalformedException, ServiceException, ServiceConnectionException, - FormatNotAvailableException, QueryNotFoundException, IdentifierNotFoundException { + public void find_httpRedirect_succeeds() throws MalformedException, DataServiceException, + DataServiceConnectionException, FormatNotAvailableException, QueryNotFoundException, + IdentifierNotFoundException { /* test */ final ResponseEntity<?> response = generic_find(null, null); @@ -611,10 +631,10 @@ public class IdentifierEndpointUnitTest extends AbstractUnitTest { @Test @WithMockUser(username = USER_1_USERNAME, authorities = {"create-identifier"}) - public void save_hasRoleDatabase_succeeds() throws MalformedException, NotAllowedException, ServiceException, - ServiceConnectionException, UserNotFoundException, DatabaseNotFoundException, AccessNotFoundException, + public void save_hasRoleDatabase_succeeds() throws MalformedException, NotAllowedException, DataServiceException, + DataServiceConnectionException, UserNotFoundException, DatabaseNotFoundException, AccessNotFoundException, QueryNotFoundException, IdentifierNotFoundException, ViewNotFoundException, SearchServiceException, - SearchServiceConnectionException, TableNotFoundException { + SearchServiceConnectionException, TableNotFoundException, ExternalServiceException { /* test */ generic_save(DATABASE_1_ID, DATABASE_1, DATABASE_1_USER_1_READ_ACCESS, IDENTIFIER_1, IDENTIFIER_1_SAVE_DTO, USER_1_PRINCIPAL, USER_1); @@ -633,9 +653,9 @@ public class IdentifierEndpointUnitTest extends AbstractUnitTest { @Test @WithMockUser(username = USER_2_USERNAME, authorities = {"create-identifier"}) public void save_hasRoleReadAccessQuery_succeeds() throws MalformedException, NotAllowedException, - ServiceException, ServiceConnectionException, UserNotFoundException, DatabaseNotFoundException, + DataServiceException, DataServiceConnectionException, UserNotFoundException, DatabaseNotFoundException, AccessNotFoundException, QueryNotFoundException, IdentifierNotFoundException, ViewNotFoundException, - SearchServiceException, SearchServiceConnectionException, TableNotFoundException { + SearchServiceException, SearchServiceConnectionException, TableNotFoundException, ExternalServiceException { /* mock */ when(dataServiceGateway.findQuery(DATABASE_2_ID, IDENTIFIER_5_QUERY_ID)) @@ -798,10 +818,10 @@ public class IdentifierEndpointUnitTest extends AbstractUnitTest { protected void generic_save(Long databaseId, Database database, DatabaseAccess access, Identifier identifier, IdentifierSaveDto data, Principal principal, User user) throws MalformedException, - NotAllowedException, ServiceException, ServiceConnectionException, UserNotFoundException, + NotAllowedException, DataServiceException, DataServiceConnectionException, UserNotFoundException, DatabaseNotFoundException, AccessNotFoundException, QueryNotFoundException, IdentifierNotFoundException, ViewNotFoundException, SearchServiceException, - SearchServiceConnectionException, TableNotFoundException { + SearchServiceConnectionException, TableNotFoundException, ExternalServiceException { /* mock */ if (access != null) { @@ -842,7 +862,7 @@ public class IdentifierEndpointUnitTest extends AbstractUnitTest { } protected ResponseEntity<?> generic_find(String accept, InputStreamResource resource) - throws MalformedException, ServiceException, ServiceConnectionException, FormatNotAvailableException, + throws MalformedException, DataServiceException, DataServiceConnectionException, FormatNotAvailableException, QueryNotFoundException, IdentifierNotFoundException { /* mock */ @@ -863,7 +883,7 @@ public class IdentifierEndpointUnitTest extends AbstractUnitTest { return IOUtils.toString(inputStream, StandardCharsets.UTF_8); } - protected void generic_delete() throws NotAllowedException, ServiceException, ServiceConnectionException, + protected void generic_delete() throws NotAllowedException, DataServiceException, DataServiceConnectionException, DatabaseNotFoundException, IdentifierNotFoundException, SearchServiceException, SearchServiceConnectionException { diff --git a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/endpoints/TableEndpointUnitTest.java b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/endpoints/TableEndpointUnitTest.java index b9a67d9911..154ebda86c 100644 --- a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/endpoints/TableEndpointUnitTest.java +++ b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/endpoints/TableEndpointUnitTest.java @@ -348,7 +348,7 @@ public class TableEndpointUnitTest extends AbstractUnitTest { @Test @WithAnonymousUser - public void findById_publicAnonymous_succeeds() throws ServiceException, ServiceConnectionException, + public void findById_publicAnonymous_succeeds() throws DataServiceException, DataServiceConnectionException, TableNotFoundException, DatabaseNotFoundException, AccessNotFoundException, QueueNotFoundException { /* test */ @@ -367,7 +367,7 @@ public class TableEndpointUnitTest extends AbstractUnitTest { @Test @WithMockUser(username = USER_1_USERNAME, authorities = "find-table") - public void findById_publicHasRoleDatabaseNotFound_succeeds() throws ServiceException, ServiceConnectionException, + public void findById_publicHasRoleDatabaseNotFound_succeeds() throws DataServiceException, DataServiceConnectionException, TableNotFoundException, DatabaseNotFoundException, AccessNotFoundException, QueueNotFoundException { /* test */ @@ -376,7 +376,7 @@ public class TableEndpointUnitTest extends AbstractUnitTest { @Test @WithMockUser(username = USER_1_USERNAME, authorities = "find-table") - public void findById_publicHasRole_succeeds() throws ServiceException, ServiceConnectionException, + public void findById_publicHasRole_succeeds() throws DataServiceException, DataServiceConnectionException, TableNotFoundException, DatabaseNotFoundException, AccessNotFoundException, QueueNotFoundException { /* test */ @@ -388,7 +388,7 @@ public class TableEndpointUnitTest extends AbstractUnitTest { @Test @WithMockUser(username = USER_4_USERNAME) - public void findById_publicNoRole_succeeds() throws ServiceException, ServiceConnectionException, + public void findById_publicNoRole_succeeds() throws DataServiceException, DataServiceConnectionException, TableNotFoundException, DatabaseNotFoundException, AccessNotFoundException, QueueNotFoundException { /* test */ @@ -497,8 +497,8 @@ public class TableEndpointUnitTest extends AbstractUnitTest { @Test @WithMockUser(username = USER_1_USERNAME, authorities = {"modify-table-column-semantics"}) - public void update_publicHasRoleHasOwnWriteAccess_succeeds() throws MalformedException, ServiceException, - NotAllowedException, ServiceConnectionException, UserNotFoundException, TableNotFoundException, + public void update_publicHasRoleHasOwnWriteAccess_succeeds() throws MalformedException, DataServiceException, + NotAllowedException, DataServiceConnectionException, UserNotFoundException, TableNotFoundException, DatabaseNotFoundException, AccessNotFoundException, SearchServiceException, SearchServiceConnectionException, OntologyNotFoundException, SemanticEntityNotFoundException { final ColumnSemanticsUpdateDto request = ColumnSemanticsUpdateDto.builder() @@ -540,8 +540,8 @@ public class TableEndpointUnitTest extends AbstractUnitTest { @Test @WithMockUser(username = USER_2_USERNAME, authorities = {"modify-table-column-semantics"}) - public void update_publicHasRoleForeignHasAllWriteAccess_succeeds() throws MalformedException, ServiceException, - NotAllowedException, ServiceConnectionException, UserNotFoundException, TableNotFoundException, + public void update_publicHasRoleForeignHasAllWriteAccess_succeeds() throws MalformedException, DataServiceException, + NotAllowedException, DataServiceConnectionException, UserNotFoundException, TableNotFoundException, DatabaseNotFoundException, AccessNotFoundException, SearchServiceException, SearchServiceConnectionException, OntologyNotFoundException, SemanticEntityNotFoundException { final ColumnSemanticsUpdateDto request = ColumnSemanticsUpdateDto.builder() @@ -601,8 +601,8 @@ public class TableEndpointUnitTest extends AbstractUnitTest { @Test @WithMockUser(username = USER_1_USERNAME, authorities = {"modify-table-column-semantics"}) - public void update_privateHasRoleHasOwnWriteAccess_succeeds() throws MalformedException, ServiceException, - NotAllowedException, ServiceConnectionException, UserNotFoundException, TableNotFoundException, + public void update_privateHasRoleHasOwnWriteAccess_succeeds() throws MalformedException, DataServiceException, + NotAllowedException, DataServiceConnectionException, UserNotFoundException, TableNotFoundException, DatabaseNotFoundException, AccessNotFoundException, SearchServiceException, SearchServiceConnectionException, OntologyNotFoundException, SemanticEntityNotFoundException { final ColumnSemanticsUpdateDto request = ColumnSemanticsUpdateDto.builder() @@ -644,8 +644,8 @@ public class TableEndpointUnitTest extends AbstractUnitTest { @Test @WithMockUser(username = USER_2_USERNAME, authorities = {"modify-table-column-semantics"}) - public void update_privateHasRoleForeignHasAllWriteAccess_succeeds() throws MalformedException, ServiceException, - NotAllowedException, ServiceConnectionException, UserNotFoundException, TableNotFoundException, + public void update_privateHasRoleForeignHasAllWriteAccess_succeeds() throws MalformedException, DataServiceException, + NotAllowedException, DataServiceConnectionException, UserNotFoundException, TableNotFoundException, DatabaseNotFoundException, AccessNotFoundException, SearchServiceException, SearchServiceConnectionException, OntologyNotFoundException, SemanticEntityNotFoundException { final ColumnSemanticsUpdateDto request = ColumnSemanticsUpdateDto.builder() @@ -731,7 +731,7 @@ public class TableEndpointUnitTest extends AbstractUnitTest { @Test @WithAnonymousUser - public void findById_privateAnonymous_succeeds() throws ServiceException, ServiceConnectionException, + public void findById_privateAnonymous_succeeds() throws DataServiceException, DataServiceConnectionException, TableNotFoundException, DatabaseNotFoundException, AccessNotFoundException, QueueNotFoundException { /* test */ @@ -750,7 +750,7 @@ public class TableEndpointUnitTest extends AbstractUnitTest { @Test @WithMockUser(username = USER_1_USERNAME, authorities = "find-table") - public void findById_privateHasRoleDatabaseNotFound_succeeds() throws ServiceException, ServiceConnectionException, + public void findById_privateHasRoleDatabaseNotFound_succeeds() throws DataServiceException, DataServiceConnectionException, TableNotFoundException, DatabaseNotFoundException, AccessNotFoundException, QueueNotFoundException { /* test */ @@ -759,7 +759,7 @@ public class TableEndpointUnitTest extends AbstractUnitTest { @Test @WithMockUser(username = USER_1_USERNAME, authorities = "find-table") - public void findById_privateHasRole_succeeds() throws ServiceException, ServiceConnectionException, + public void findById_privateHasRole_succeeds() throws DataServiceException, DataServiceConnectionException, TableNotFoundException, DatabaseNotFoundException, AccessNotFoundException, QueueNotFoundException { /* test */ final ResponseEntity<TableDto> response = generic_findById(DATABASE_1_ID, DATABASE_1, TABLE_1_ID, TABLE_1, @@ -771,7 +771,7 @@ public class TableEndpointUnitTest extends AbstractUnitTest { @Test @WithMockUser(username = USER_4_USERNAME) - public void findById_privateNoRole_succeeds() throws ServiceException, ServiceConnectionException, + public void findById_privateNoRole_succeeds() throws DataServiceException, DataServiceConnectionException, TableNotFoundException, DatabaseNotFoundException, AccessNotFoundException, QueueNotFoundException { /* test */ @@ -790,7 +790,7 @@ public class TableEndpointUnitTest extends AbstractUnitTest { @Test @WithMockUser(username = USER_1_USERNAME, authorities = {"delete-table"}) - public void delete_succeeds() throws NotAllowedException, ServiceException, ServiceConnectionException, + public void delete_succeeds() throws NotAllowedException, DataServiceException, DataServiceConnectionException, TableNotFoundException, DatabaseNotFoundException, SearchServiceException, SearchServiceConnectionException { @@ -810,9 +810,9 @@ public class TableEndpointUnitTest extends AbstractUnitTest { @Test @WithMockUser(username = USER_2_USERNAME, authorities = {"delete-foreign-table"}) - public void delete_foreign_succeeds() throws NotAllowedException, ServiceException, ServiceConnectionException, + public void delete_foreign_succeeds() throws NotAllowedException, DataServiceConnectionException, TableNotFoundException, DatabaseNotFoundException, SearchServiceException, - SearchServiceConnectionException { + SearchServiceConnectionException, DataServiceException { /* test */ generic_delete(USER_2_PRINCIPAL, TABLE_1); @@ -895,9 +895,10 @@ public class TableEndpointUnitTest extends AbstractUnitTest { protected ResponseEntity<TableDto> generic_create(Long databaseId, Database database, TableCreateDto data, Principal principal, User user, DatabaseAccess access) - throws MalformedException, NotAllowedException, ServiceException, ServiceConnectionException, + throws MalformedException, NotAllowedException, DataServiceException, DataServiceConnectionException, UserNotFoundException, DatabaseNotFoundException, AccessNotFoundException, TableNotFoundException, - TableExistsException, SearchServiceException, SearchServiceConnectionException, OntologyNotFoundException, SemanticEntityNotFoundException { + TableExistsException, SearchServiceException, SearchServiceConnectionException, OntologyNotFoundException, + SemanticEntityNotFoundException { /* mock */ if (principal != null) { @@ -927,8 +928,8 @@ public class TableEndpointUnitTest extends AbstractUnitTest { protected ResponseEntity<TableDto> generic_findById(Long databaseId, Database database, Long tableId, Table table, Principal principal, User user, - DatabaseAccess access) throws ServiceException, - ServiceConnectionException, TableNotFoundException, DatabaseNotFoundException, AccessNotFoundException, + DatabaseAccess access) throws DataServiceException, + DataServiceConnectionException, TableNotFoundException, DatabaseNotFoundException, AccessNotFoundException, QueueNotFoundException { /* mock */ @@ -964,7 +965,7 @@ public class TableEndpointUnitTest extends AbstractUnitTest { } protected ResponseEntity<?> generic_delete(Principal principal, Table table) throws NotAllowedException, - ServiceException, ServiceConnectionException, TableNotFoundException, DatabaseNotFoundException, + DataServiceException, DataServiceConnectionException, TableNotFoundException, DatabaseNotFoundException, SearchServiceException, SearchServiceConnectionException { /* mock */ @@ -978,7 +979,7 @@ public class TableEndpointUnitTest extends AbstractUnitTest { protected ResponseEntity<ColumnDto> generic_update(Long databaseId, Database database, Long tableId, Table table, Long columnId, TableColumn column, Principal principal, User user, ColumnSemanticsUpdateDto data, DatabaseAccess access) - throws ServiceException, ServiceConnectionException, MalformedException, NotAllowedException, + throws DataServiceException, DataServiceConnectionException, MalformedException, NotAllowedException, UserNotFoundException, TableNotFoundException, DatabaseNotFoundException, AccessNotFoundException, SearchServiceException, SearchServiceConnectionException, OntologyNotFoundException, SemanticEntityNotFoundException { @@ -990,7 +991,7 @@ public class TableEndpointUnitTest extends AbstractUnitTest { when(tableService.update(column, data)) .thenReturn(column); } else { - doThrow(ServiceException.class) + doThrow(DataServiceException.class) .when(tableService) .update(column, data); doThrow(TableNotFoundException.class) diff --git a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/endpoints/UserEndpointUnitTest.java b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/endpoints/UserEndpointUnitTest.java index f21e13711b..f5e413e0e9 100644 --- a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/endpoints/UserEndpointUnitTest.java +++ b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/endpoints/UserEndpointUnitTest.java @@ -216,7 +216,7 @@ public class UserEndpointUnitTest extends AbstractUnitTest { @Test @WithMockUser(username = USER_1_USERNAME) - public void password_succeeds() throws NotAllowedException, ServiceException, ServiceConnectionException, + public void password_succeeds() throws NotAllowedException, DataServiceException, DataServiceConnectionException, UserNotFoundException, DatabaseNotFoundException, AuthServiceException, AuthServiceConnectionException, CredentialsInvalidException { final UserPasswordDto request = UserPasswordDto.builder() @@ -302,7 +302,7 @@ public class UserEndpointUnitTest extends AbstractUnitTest { } protected void password_generic(Principal principal, UserPasswordDto data) throws NotAllowedException, - ServiceException, ServiceConnectionException, UserNotFoundException, DatabaseNotFoundException, + DataServiceException, DataServiceConnectionException, UserNotFoundException, DatabaseNotFoundException, AuthServiceException, AuthServiceConnectionException, CredentialsInvalidException { /* mock */ diff --git a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/endpoints/ViewEndpointUnitTest.java b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/endpoints/ViewEndpointUnitTest.java index 724d43ca16..74e418f42b 100644 --- a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/endpoints/ViewEndpointUnitTest.java +++ b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/endpoints/ViewEndpointUnitTest.java @@ -196,8 +196,8 @@ public class ViewEndpointUnitTest extends AbstractUnitTest { @Test @WithMockUser(username = USER_3_USERNAME, authorities = {"delete-database-view"}) - public void delete_publicOwner_succeeds() throws NotAllowedException, ServiceException, - ServiceConnectionException, ViewNotFoundException, DatabaseNotFoundException, AccessNotFoundException, + public void delete_publicOwner_succeeds() throws NotAllowedException, DataServiceException, + DataServiceConnectionException, ViewNotFoundException, DatabaseNotFoundException, AccessNotFoundException, SearchServiceException, SearchServiceConnectionException { /* test */ @@ -352,7 +352,7 @@ public class ViewEndpointUnitTest extends AbstractUnitTest { @Test @WithMockUser(username = USER_1_USERNAME, authorities = {"delete-database-view"}) - public void delete_privateOwner_succeeds() throws NotAllowedException, ServiceException, ServiceConnectionException, + public void delete_privateOwner_succeeds() throws NotAllowedException, DataServiceException, DataServiceConnectionException, DatabaseNotFoundException, AccessNotFoundException, ViewNotFoundException, SearchServiceException, SearchServiceConnectionException { @@ -401,8 +401,8 @@ public class ViewEndpointUnitTest extends AbstractUnitTest { } protected void create_generic(Long databaseId, Database database, Principal principal, UUID userId, User user, - DatabaseAccess access) throws MalformedException, ServiceException, - ServiceConnectionException, NotAllowedException, UserNotFoundException, DatabaseNotFoundException, + DatabaseAccess access) throws MalformedException, DataServiceException, + DataServiceConnectionException, NotAllowedException, UserNotFoundException, DatabaseNotFoundException, AccessNotFoundException, SearchServiceException, SearchServiceConnectionException { final ViewCreateDto request = ViewCreateDto.builder() .name(VIEW_1_NAME) @@ -469,7 +469,7 @@ public class ViewEndpointUnitTest extends AbstractUnitTest { protected void delete_generic(Long databaseId, Database database, Long viewId, View view, Principal principal, UUID userId, User user, DatabaseAccess access) throws NotAllowedException, - ServiceException, ServiceConnectionException, DatabaseNotFoundException, AccessNotFoundException, + DataServiceException, DataServiceConnectionException, DatabaseNotFoundException, AccessNotFoundException, ViewNotFoundException, SearchServiceException, SearchServiceConnectionException { /* mock */ diff --git a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/gateway/BrokerServiceGatewayUnitTest.java b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/gateway/BrokerServiceGatewayUnitTest.java index ffa2ff6c1b..a812bd5de4 100644 --- a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/gateway/BrokerServiceGatewayUnitTest.java +++ b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/gateway/BrokerServiceGatewayUnitTest.java @@ -14,6 +14,7 @@ import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.web.client.HttpServerErrorException; import org.springframework.web.client.RestClientException; import org.springframework.web.client.RestTemplate; @@ -33,7 +34,8 @@ public class BrokerServiceGatewayUnitTest extends AbstractUnitTest { private BrokerServiceGateway brokerServiceGateway; @Test - public void grantTopicPermission_exchangeNoRightsBefore_succeeds() throws ServiceException, ServiceConnectionException { + public void grantTopicPermission_exchangeNoRightsBefore_succeeds() throws BrokerServiceException, + BrokerServiceConnectionException { final ResponseEntity<Void> mock = ResponseEntity.status(HttpStatus.CREATED) .build(); @@ -46,7 +48,8 @@ public class BrokerServiceGatewayUnitTest extends AbstractUnitTest { } @Test - public void grantTopicPermission_exchangeRightsSame_succeeds() throws ServiceException, ServiceConnectionException { + public void grantTopicPermission_exchangeRightsSame_succeeds() throws BrokerServiceException, + BrokerServiceConnectionException { final ResponseEntity<Void> mock = ResponseEntity.status(HttpStatus.NO_CONTENT) .build(); @@ -68,13 +71,14 @@ public class BrokerServiceGatewayUnitTest extends AbstractUnitTest { .thenReturn(mock); /* test */ - assertThrows(ServiceException.class, () -> { + assertThrows(BrokerServiceException.class, () -> { brokerServiceGateway.grantTopicPermission(USER_1_USERNAME, VIRTUAL_HOST_EXCHANGE_UPDATE_DTO); }); } @Test - public void grantVirtualHostPermission_virtualHostNoRightsBefore_succeeds() throws ServiceConnectionException, ServiceException { + public void grantVirtualHostPermission_virtualHostNoRightsBefore_succeeds() throws BrokerServiceException, + BrokerServiceConnectionException { final ResponseEntity<Void> mock = ResponseEntity.status(HttpStatus.CREATED) .build(); @@ -87,7 +91,8 @@ public class BrokerServiceGatewayUnitTest extends AbstractUnitTest { } @Test - public void grantVirtualHostPermission_virtualHostRightsSame_succeeds() throws ServiceConnectionException, ServiceException { + public void grantVirtualHostPermission_virtualHostRightsSame_succeeds() throws BrokerServiceException, + BrokerServiceConnectionException { final ResponseEntity<Void> mock = ResponseEntity.status(HttpStatus.NO_CONTENT) .build(); @@ -109,7 +114,7 @@ public class BrokerServiceGatewayUnitTest extends AbstractUnitTest { .thenReturn(mock); /* test */ - assertThrows(ServiceException.class, () -> { + assertThrows(BrokerServiceException.class, () -> { brokerServiceGateway.grantVirtualHostPermission(USER_1_USERNAME, VIRTUAL_HOST_GRANT_DTO); }); } @@ -123,7 +128,21 @@ public class BrokerServiceGatewayUnitTest extends AbstractUnitTest { .exchange(anyString(), eq(HttpMethod.PUT), any(HttpEntity.class), eq(Void.class)); /* test */ - assertThrows(ServiceException.class, () -> { + assertThrows(BrokerServiceException.class, () -> { + brokerServiceGateway.grantVirtualHostPermission(USER_1_USERNAME, VIRTUAL_HOST_GRANT_DTO); + }); + } + + @Test + public void grantVirtualHostPermission_connection_fails() { + + /* mock */ + doThrow(HttpServerErrorException.class) + .when(restTemplate) + .exchange(anyString(), eq(HttpMethod.PUT), any(HttpEntity.class), eq(Void.class)); + + /* test */ + assertThrows(BrokerServiceConnectionException.class, () -> { brokerServiceGateway.grantVirtualHostPermission(USER_1_USERNAME, VIRTUAL_HOST_GRANT_DTO); }); } @@ -137,13 +156,28 @@ public class BrokerServiceGatewayUnitTest extends AbstractUnitTest { .exchange(anyString(), eq(HttpMethod.PUT), any(HttpEntity.class), eq(Void.class)); /* test */ - assertThrows(ServiceException.class, () -> { + assertThrows(BrokerServiceException.class, () -> { brokerServiceGateway.grantTopicPermission(USER_1_USERNAME, VIRTUAL_HOST_EXCHANGE_UPDATE_DTO); }); } @Test - public void grantExchangePermission_succeeds() throws ServiceConnectionException, ServiceException { + public void grantTopicPermission_connection_fails() { + + /* mock */ + doThrow(HttpServerErrorException.class) + .when(restTemplate) + .exchange(anyString(), eq(HttpMethod.PUT), any(HttpEntity.class), eq(Void.class)); + + /* test */ + assertThrows(BrokerServiceConnectionException.class, () -> { + brokerServiceGateway.grantTopicPermission(USER_1_USERNAME, VIRTUAL_HOST_EXCHANGE_UPDATE_DTO); + }); + } + + @Test + public void grantExchangePermission_succeeds() throws BrokerServiceException, + BrokerServiceConnectionException { final ResponseEntity<Void> mock = ResponseEntity.status(HttpStatus.CREATED) .build(); @@ -156,7 +190,8 @@ public class BrokerServiceGatewayUnitTest extends AbstractUnitTest { } @Test - public void grantExchangePermission_exists_succeeds() throws ServiceConnectionException, ServiceException { + public void grantExchangePermission_exists_succeeds() throws BrokerServiceException, + BrokerServiceConnectionException { final ResponseEntity<Void> mock = ResponseEntity.status(HttpStatus.NO_CONTENT) .build(); @@ -178,7 +213,21 @@ public class BrokerServiceGatewayUnitTest extends AbstractUnitTest { .thenReturn(mock); /* test */ - assertThrows(ServiceException.class, () -> { + assertThrows(BrokerServiceException.class, () -> { + brokerServiceGateway.grantExchangePermission(USER_1_USERNAME, USER_1_RABBITMQ_GRANT_TOPIC_DTO); + }); + } + + @Test + public void grantExchangePermission_connection_fails() { + + /* mock */ + doThrow(HttpServerErrorException.class) + .when(restTemplate) + .exchange(anyString(), eq(HttpMethod.PUT), any(HttpEntity.class), eq(Void.class)); + + /* test */ + assertThrows(BrokerServiceConnectionException.class, () -> { brokerServiceGateway.grantExchangePermission(USER_1_USERNAME, USER_1_RABBITMQ_GRANT_TOPIC_DTO); }); } @@ -192,7 +241,7 @@ public class BrokerServiceGatewayUnitTest extends AbstractUnitTest { .exchange(anyString(), eq(HttpMethod.PUT), any(HttpEntity.class), eq(Void.class)); /* test */ - assertThrows(ServiceException.class, () -> { + assertThrows(BrokerServiceException.class, () -> { brokerServiceGateway.grantExchangePermission(USER_1_USERNAME, USER_1_RABBITMQ_GRANT_TOPIC_DTO); }); } diff --git a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/gateway/CrossrefGatewayUnitTest.java b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/gateway/CrossrefGatewayUnitTest.java index 8d056ad48d..881f29ed04 100644 --- a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/gateway/CrossrefGatewayUnitTest.java +++ b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/gateway/CrossrefGatewayUnitTest.java @@ -15,9 +15,11 @@ import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.web.client.HttpServerErrorException; import org.springframework.web.client.ResourceAccessException; import org.springframework.web.client.RestTemplate; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.Mockito.*; @Log4j2 @@ -26,7 +28,6 @@ import static org.mockito.Mockito.*; public class CrossrefGatewayUnitTest extends AbstractUnitTest { @MockBean - @Qualifier("keycloakRestTemplate") private RestTemplate restTemplate; @Autowired @@ -45,15 +46,17 @@ public class CrossrefGatewayUnitTest extends AbstractUnitTest { } @Test - public void findById_fails() throws DoiNotFoundException { + public void findById_fails() { /* mock */ - doThrow(ResourceAccessException.class) + doThrow(HttpServerErrorException.class) .when(restTemplate) .exchange(anyString(), eq(HttpMethod.GET), any(HttpEntity.class), eq(CrossrefDto.class)); /* test */ - crossrefGateway.findById("501100004729"); + assertThrows(DoiNotFoundException.class, () -> { + crossrefGateway.findById("501100004729"); + }); } } diff --git a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/gateway/DataServiceGatewayUnitTest.java b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/gateway/DataServiceGatewayUnitTest.java index d8369bb6da..8183ea9080 100644 --- a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/gateway/DataServiceGatewayUnitTest.java +++ b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/gateway/DataServiceGatewayUnitTest.java @@ -1,16 +1,1119 @@ package at.tuwien.gateway; +import at.tuwien.ExportResourceDto; +import at.tuwien.api.database.AccessTypeDto; +import at.tuwien.api.database.DatabaseDto; +import at.tuwien.api.database.ViewDto; +import at.tuwien.api.database.query.QueryDto; +import at.tuwien.api.database.table.TableDto; +import at.tuwien.api.database.table.TableStatisticDto; +import at.tuwien.exception.*; import at.tuwien.test.AbstractUnitTest; import lombok.extern.log4j.Log4j2; +import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.web.client.HttpClientErrorException; +import org.springframework.web.client.HttpServerErrorException; +import org.springframework.web.client.RestTemplate; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.when; @Log4j2 @SpringBootTest @ExtendWith(SpringExtension.class) public class DataServiceGatewayUnitTest extends AbstractUnitTest { - // TODO check mapping of databaseService too!! + @MockBean + @Qualifier("dataServiceRestTemplate") + private RestTemplate dataServiceRestTemplate; + + @Autowired + private DataServiceGateway dataServiceGateway; + + @Test + public void createAccess_succeeds() throws DataServiceException, DataServiceConnectionException, DatabaseNotFoundException { + + /* mock */ + when(dataServiceRestTemplate.exchange(anyString(), eq(HttpMethod.POST), any(HttpEntity.class), eq(Void.class))) + .thenReturn(ResponseEntity.status(HttpStatus.CREATED) + .build()); + + /* test */ + dataServiceGateway.createAccess(DATABASE_1_ID, USER_1_ID, AccessTypeDto.READ); + } + + @Test + public void createAccess_connection_fails() { + + /* mock */ + doThrow(HttpServerErrorException.class) + .when(dataServiceRestTemplate) + .exchange(anyString(), eq(HttpMethod.POST), any(HttpEntity.class), eq(Void.class)); + + /* test */ + assertThrows(DataServiceConnectionException.class, () -> { + dataServiceGateway.createAccess(DATABASE_1_ID, USER_1_ID, AccessTypeDto.READ); + }); + } + + @Test + public void createAccess_notFound_fails() { + + /* mock */ + doThrow(HttpClientErrorException.NotFound.class) + .when(dataServiceRestTemplate) + .exchange(anyString(), eq(HttpMethod.POST), any(HttpEntity.class), eq(Void.class)); + + /* test */ + assertThrows(DatabaseNotFoundException.class, () -> { + dataServiceGateway.createAccess(DATABASE_1_ID, USER_1_ID, AccessTypeDto.READ); + }); + } + + @Test + public void createAccess_unexpected_fails() { + + /* mock */ + doThrow(HttpClientErrorException.BadRequest.class) + .when(dataServiceRestTemplate) + .exchange(anyString(), eq(HttpMethod.POST), any(HttpEntity.class), eq(Void.class)); + + /* test */ + assertThrows(DataServiceException.class, () -> { + dataServiceGateway.createAccess(DATABASE_1_ID, USER_1_ID, AccessTypeDto.READ); + }); + } + + @Test + public void createAccess_responseCode_fails() { + + /* mock */ + when(dataServiceRestTemplate.exchange(anyString(), eq(HttpMethod.POST), any(HttpEntity.class), eq(Void.class))) + .thenReturn(ResponseEntity.status(HttpStatus.NO_CONTENT) + .build()); + + /* test */ + assertThrows(DataServiceException.class, () -> { + dataServiceGateway.createAccess(DATABASE_1_ID, USER_1_ID, AccessTypeDto.READ); + }); + } + + @Test + public void updateAccess_succeeds() throws DataServiceException, DataServiceConnectionException, AccessNotFoundException { + + /* mock */ + when(dataServiceRestTemplate.exchange(anyString(), eq(HttpMethod.PUT), any(HttpEntity.class), eq(Void.class))) + .thenReturn(ResponseEntity.status(HttpStatus.ACCEPTED) + .build()); + + /* test */ + dataServiceGateway.updateAccess(DATABASE_1_ID, USER_1_ID, AccessTypeDto.READ); + } + + @Test + public void updateAccess_connection_fails() { + + /* mock */ + doThrow(HttpServerErrorException.class) + .when(dataServiceRestTemplate) + .exchange(anyString(), eq(HttpMethod.PUT), any(HttpEntity.class), eq(Void.class)); + + /* test */ + assertThrows(DataServiceConnectionException.class, () -> { + dataServiceGateway.updateAccess(DATABASE_1_ID, USER_1_ID, AccessTypeDto.READ); + }); + } + + @Test + public void updateAccess_notFound_fails() { + + /* mock */ + doThrow(HttpClientErrorException.NotFound.class) + .when(dataServiceRestTemplate) + .exchange(anyString(), eq(HttpMethod.PUT), any(HttpEntity.class), eq(Void.class)); + + /* test */ + assertThrows(AccessNotFoundException.class, () -> { + dataServiceGateway.updateAccess(DATABASE_1_ID, USER_1_ID, AccessTypeDto.READ); + }); + } + + @Test + public void updateAccess_unexpected_fails() { + + /* mock */ + doThrow(HttpClientErrorException.BadRequest.class) + .when(dataServiceRestTemplate) + .exchange(anyString(), eq(HttpMethod.PUT), any(HttpEntity.class), eq(Void.class)); + + /* test */ + assertThrows(DataServiceException.class, () -> { + dataServiceGateway.updateAccess(DATABASE_1_ID, USER_1_ID, AccessTypeDto.READ); + }); + } + + @Test + public void updateAccess_responseCode_fails() { + + /* mock */ + when(dataServiceRestTemplate.exchange(anyString(), eq(HttpMethod.PUT), any(HttpEntity.class), eq(Void.class))) + .thenReturn(ResponseEntity.status(HttpStatus.NO_CONTENT) + .build()); + + /* test */ + assertThrows(DataServiceException.class, () -> { + dataServiceGateway.updateAccess(DATABASE_1_ID, USER_1_ID, AccessTypeDto.READ); + }); + } + + @Test + public void deleteAccess_succeeds() throws DataServiceException, DataServiceConnectionException, AccessNotFoundException { + + /* mock */ + when(dataServiceRestTemplate.exchange(anyString(), eq(HttpMethod.DELETE), eq(HttpEntity.EMPTY), eq(Void.class))) + .thenReturn(ResponseEntity.status(HttpStatus.ACCEPTED) + .build()); + + /* test */ + dataServiceGateway.deleteAccess(DATABASE_1_ID, USER_1_ID); + } + + @Test + public void deleteAccess_connection_fails() { + + /* mock */ + doThrow(HttpServerErrorException.class) + .when(dataServiceRestTemplate) + .exchange(anyString(), eq(HttpMethod.DELETE), eq(HttpEntity.EMPTY), eq(Void.class)); + + /* test */ + assertThrows(DataServiceConnectionException.class, () -> { + dataServiceGateway.deleteAccess(DATABASE_1_ID, USER_1_ID); + }); + } + + @Test + public void deleteAccess_notFound_fails() { + + /* mock */ + doThrow(HttpClientErrorException.NotFound.class) + .when(dataServiceRestTemplate) + .exchange(anyString(), eq(HttpMethod.DELETE), eq(HttpEntity.EMPTY), eq(Void.class)); + + /* test */ + assertThrows(AccessNotFoundException.class, () -> { + dataServiceGateway.deleteAccess(DATABASE_1_ID, USER_1_ID); + }); + } + + @Test + public void deleteAccess_unauthorized_fails() { + + /* mock */ + doThrow(HttpClientErrorException.Unauthorized.class) + .when(dataServiceRestTemplate) + .exchange(anyString(), eq(HttpMethod.DELETE), eq(HttpEntity.EMPTY), eq(Void.class)); + + /* test */ + assertThrows(DataServiceException.class, () -> { + dataServiceGateway.deleteAccess(DATABASE_1_ID, USER_1_ID); + }); + } + + @Test + public void deleteAccess_responseCode_fails() { + + /* mock */ + when(dataServiceRestTemplate.exchange(anyString(), eq(HttpMethod.DELETE), eq(HttpEntity.EMPTY), eq(Void.class))) + .thenReturn(ResponseEntity.status(HttpStatus.NO_CONTENT) + .build()); + + /* test */ + assertThrows(DataServiceException.class, () -> { + dataServiceGateway.deleteAccess(DATABASE_1_ID, USER_1_ID); + }); + } + + @Test + public void createDatabase_succeeds() throws DataServiceException, DataServiceConnectionException, + DatabaseNotFoundException { + + /* mock */ + when(dataServiceRestTemplate.exchange(anyString(), eq(HttpMethod.POST), any(HttpEntity.class), eq(DatabaseDto.class))) + .thenReturn(ResponseEntity.status(HttpStatus.CREATED) + .body(DATABASE_1_DTO)); + + /* test */ + dataServiceGateway.createDatabase(DATABASE_1_CREATE_INTERNAL); + } + + @Test + public void createDatabase_connection_fails() { + + /* mock */ + doThrow(HttpServerErrorException.class) + .when(dataServiceRestTemplate) + .exchange(anyString(), eq(HttpMethod.POST), any(HttpEntity.class), eq(DatabaseDto.class)); + + /* test */ + assertThrows(DataServiceConnectionException.class, () -> { + dataServiceGateway.createDatabase(DATABASE_1_CREATE_INTERNAL); + }); + } + + @Test + public void createDatabase_unauthorized_fails() { + + /* mock */ + doThrow(HttpClientErrorException.Unauthorized.class) + .when(dataServiceRestTemplate) + .exchange(anyString(), eq(HttpMethod.POST), any(HttpEntity.class), eq(DatabaseDto.class)); + + /* test */ + assertThrows(DataServiceException.class, () -> { + dataServiceGateway.createDatabase(DATABASE_1_CREATE_INTERNAL); + }); + } + + @Test + public void createDatabase_unexpected_fails() { + + /* mock */ + doThrow(HttpClientErrorException.BadRequest.class) + .when(dataServiceRestTemplate) + .exchange(anyString(), eq(HttpMethod.POST), any(HttpEntity.class), eq(DatabaseDto.class)); + + /* test */ + assertThrows(DataServiceException.class, () -> { + dataServiceGateway.createDatabase(DATABASE_1_CREATE_INTERNAL); + }); + } + + @Test + public void createDatabase_responseCode_fails() { + + /* mock */ + when(dataServiceRestTemplate.exchange(anyString(), eq(HttpMethod.POST), any(HttpEntity.class), eq(DatabaseDto.class))) + .thenReturn(ResponseEntity.status(HttpStatus.NO_CONTENT) + .build()); + + /* test */ + assertThrows(DataServiceException.class, () -> { + dataServiceGateway.createDatabase(DATABASE_1_CREATE_INTERNAL); + }); + } + + @Test + public void updateDatabase_succeeds() throws DataServiceException, DataServiceConnectionException, + DatabaseNotFoundException { + + /* mock */ + when(dataServiceRestTemplate.exchange(anyString(), eq(HttpMethod.PUT), any(HttpEntity.class), eq(Void.class))) + .thenReturn(ResponseEntity.status(HttpStatus.ACCEPTED) + .build()); + + /* test */ + dataServiceGateway.updateDatabase(DATABASE_1_ID, USER_1_UPDATE_PASSWORD_DTO); + } + + @Test + public void updateDatabase_connection_fails() { + + /* mock */ + doThrow(HttpServerErrorException.class) + .when(dataServiceRestTemplate) + .exchange(anyString(), eq(HttpMethod.PUT), any(HttpEntity.class), eq(Void.class)); + + /* test */ + assertThrows(DataServiceConnectionException.class, () -> { + dataServiceGateway.updateDatabase(DATABASE_1_ID, USER_1_UPDATE_PASSWORD_DTO); + }); + } + + @Test + public void updateDatabase_unauthorized_fails() { + + /* mock */ + doThrow(HttpClientErrorException.Unauthorized.class) + .when(dataServiceRestTemplate) + .exchange(anyString(), eq(HttpMethod.PUT), any(HttpEntity.class), eq(Void.class)); + + /* test */ + assertThrows(DataServiceException.class, () -> { + dataServiceGateway.updateDatabase(DATABASE_1_ID, USER_1_UPDATE_PASSWORD_DTO); + }); + } + + @Test + public void updateDatabase_notFound_fails() { + + /* mock */ + doThrow(HttpClientErrorException.NotFound.class) + .when(dataServiceRestTemplate) + .exchange(anyString(), eq(HttpMethod.PUT), any(HttpEntity.class), eq(Void.class)); + + /* test */ + assertThrows(DatabaseNotFoundException.class, () -> { + dataServiceGateway.updateDatabase(DATABASE_1_ID, USER_1_UPDATE_PASSWORD_DTO); + }); + } + + @Test + public void updateDatabase_unexpected_fails() { + + /* mock */ + doThrow(HttpClientErrorException.BadRequest.class) + .when(dataServiceRestTemplate) + .exchange(anyString(), eq(HttpMethod.PUT), any(HttpEntity.class), eq(Void.class)); + + /* test */ + assertThrows(DataServiceException.class, () -> { + dataServiceGateway.updateDatabase(DATABASE_1_ID, USER_1_UPDATE_PASSWORD_DTO); + }); + } + + @Test + public void updateDatabase_responseCode_fails() { + + /* mock */ + when(dataServiceRestTemplate.exchange(anyString(), eq(HttpMethod.PUT), any(HttpEntity.class), eq(Void.class))) + .thenReturn(ResponseEntity.status(HttpStatus.NO_CONTENT) + .build()); + + /* test */ + assertThrows(DataServiceException.class, () -> { + dataServiceGateway.updateDatabase(DATABASE_1_ID, USER_1_UPDATE_PASSWORD_DTO); + }); + } + + @Test + public void createTable_succeeds() throws DataServiceException, DataServiceConnectionException, + DatabaseNotFoundException, TableExistsException { + + /* mock */ + when(dataServiceRestTemplate.exchange(anyString(), eq(HttpMethod.POST), any(HttpEntity.class), eq(Void.class))) + .thenReturn(ResponseEntity.status(HttpStatus.CREATED) + .build()); + + /* test */ + dataServiceGateway.createTable(DATABASE_1_ID, TABLE_1_CREATE_DTO); + } + + @Test + public void createTable_connection_fails() { + + /* mock */ + doThrow(HttpServerErrorException.class) + .when(dataServiceRestTemplate) + .exchange(anyString(), eq(HttpMethod.POST), any(HttpEntity.class), eq(Void.class)); + + /* test */ + assertThrows(DataServiceConnectionException.class, () -> { + dataServiceGateway.createTable(DATABASE_1_ID, TABLE_1_CREATE_DTO); + }); + } + + @Test + public void createTable_unauthorized_fails() { + + /* mock */ + doThrow(HttpClientErrorException.Unauthorized.class) + .when(dataServiceRestTemplate) + .exchange(anyString(), eq(HttpMethod.POST), any(HttpEntity.class), eq(Void.class)); + + /* test */ + assertThrows(DataServiceException.class, () -> { + dataServiceGateway.createTable(DATABASE_1_ID, TABLE_1_CREATE_DTO); + }); + } + + @Test + public void createTable_notFound_fails() { + + /* mock */ + doThrow(HttpClientErrorException.NotFound.class) + .when(dataServiceRestTemplate) + .exchange(anyString(), eq(HttpMethod.POST), any(HttpEntity.class), eq(Void.class)); + + /* test */ + assertThrows(DatabaseNotFoundException.class, () -> { + dataServiceGateway.createTable(DATABASE_1_ID, TABLE_1_CREATE_DTO); + }); + } + + @Test + public void createTable_exists_fails() { + + /* mock */ + doThrow(HttpClientErrorException.Conflict.class) + .when(dataServiceRestTemplate) + .exchange(anyString(), eq(HttpMethod.POST), any(HttpEntity.class), eq(Void.class)); + + /* test */ + assertThrows(TableExistsException.class, () -> { + dataServiceGateway.createTable(DATABASE_1_ID, TABLE_1_CREATE_DTO); + }); + } + + @Test + public void createTable_unexpected_fails() { + + /* mock */ + doThrow(HttpClientErrorException.BadRequest.class) + .when(dataServiceRestTemplate) + .exchange(anyString(), eq(HttpMethod.POST), any(HttpEntity.class), eq(Void.class)); + + /* test */ + assertThrows(DataServiceException.class, () -> { + dataServiceGateway.createTable(DATABASE_1_ID, TABLE_1_CREATE_DTO); + }); + } + + @Test + public void createTable_responseCode_fails() { + + /* mock */ + when(dataServiceRestTemplate.exchange(anyString(), eq(HttpMethod.POST), any(HttpEntity.class), eq(Void.class))) + .thenReturn(ResponseEntity.status(HttpStatus.NO_CONTENT) + .build()); + + /* test */ + assertThrows(DataServiceException.class, () -> { + dataServiceGateway.createTable(DATABASE_1_ID, TABLE_1_CREATE_DTO); + }); + } + + @Test + public void deleteTable_succeeds() throws DataServiceException, DataServiceConnectionException, + TableNotFoundException { + + /* mock */ + when(dataServiceRestTemplate.exchange(anyString(), eq(HttpMethod.DELETE), any(HttpEntity.class), eq(Void.class))) + .thenReturn(ResponseEntity.status(HttpStatus.ACCEPTED) + .build()); + + /* test */ + dataServiceGateway.deleteTable(DATABASE_1_ID, TABLE_1_ID); + } + + @Test + public void deleteTable_connection_fails() { + + /* mock */ + doThrow(HttpServerErrorException.class) + .when(dataServiceRestTemplate) + .exchange(anyString(), eq(HttpMethod.DELETE), any(HttpEntity.class), eq(Void.class)); + + /* test */ + assertThrows(DataServiceConnectionException.class, () -> { + dataServiceGateway.deleteTable(DATABASE_1_ID, TABLE_1_ID); + }); + } + + @Test + public void deleteTable_unauthorized_fails() { + + /* mock */ + doThrow(HttpClientErrorException.Unauthorized.class) + .when(dataServiceRestTemplate) + .exchange(anyString(), eq(HttpMethod.DELETE), any(HttpEntity.class), eq(Void.class)); + + /* test */ + assertThrows(DataServiceException.class, () -> { + dataServiceGateway.deleteTable(DATABASE_1_ID, TABLE_1_ID); + }); + } + + @Test + public void deleteTable_unexpected_fails() { + + /* mock */ + doThrow(HttpClientErrorException.NotFound.class) + .when(dataServiceRestTemplate) + .exchange(anyString(), eq(HttpMethod.DELETE), any(HttpEntity.class), eq(Void.class)); + + /* test */ + assertThrows(TableNotFoundException.class, () -> { + dataServiceGateway.deleteTable(DATABASE_1_ID, TABLE_1_ID); + }); + } + + @Test + public void deleteTable_responseCode_fails() { + + /* mock */ + when(dataServiceRestTemplate.exchange(anyString(), eq(HttpMethod.DELETE), any(HttpEntity.class), eq(Void.class))) + .thenReturn(ResponseEntity.status(HttpStatus.NO_CONTENT) + .build()); + + /* test */ + assertThrows(DataServiceException.class, () -> { + dataServiceGateway.deleteTable(DATABASE_1_ID, TABLE_1_ID); + }); + } + + @Test + public void createView_succeeds() throws DataServiceException, DataServiceConnectionException { + + /* mock */ + when(dataServiceRestTemplate.exchange(anyString(), eq(HttpMethod.POST), any(HttpEntity.class), eq(ViewDto.class))) + .thenReturn(ResponseEntity.status(HttpStatus.CREATED) + .body(VIEW_1_DTO)); + + /* test */ + dataServiceGateway.createView(DATABASE_1_ID, VIEW_1_CREATE_DTO); + } + + @Test + public void createView_connection_fails() { + + /* mock */ + doThrow(HttpServerErrorException.class) + .when(dataServiceRestTemplate) + .exchange(anyString(), eq(HttpMethod.POST), any(HttpEntity.class), eq(ViewDto.class)); + + /* test */ + assertThrows(DataServiceConnectionException.class, () -> { + dataServiceGateway.createView(DATABASE_1_ID, VIEW_1_CREATE_DTO); + }); + } + + @Test + public void createView_unauthorized_fails() { + + /* mock */ + doThrow(HttpClientErrorException.Unauthorized.class) + .when(dataServiceRestTemplate) + .exchange(anyString(), eq(HttpMethod.POST), any(HttpEntity.class), eq(ViewDto.class)); + + /* test */ + assertThrows(DataServiceException.class, () -> { + dataServiceGateway.createView(DATABASE_1_ID, VIEW_1_CREATE_DTO); + }); + } + + @Test + public void createView_unexpected_fails() { + + /* mock */ + doThrow(HttpClientErrorException.BadRequest.class) + .when(dataServiceRestTemplate) + .exchange(anyString(), eq(HttpMethod.POST), any(HttpEntity.class), eq(ViewDto.class)); + + /* test */ + assertThrows(DataServiceException.class, () -> { + dataServiceGateway.createView(DATABASE_1_ID, VIEW_1_CREATE_DTO); + }); + } + + @Test + public void createView_responseCode_fails() { + + /* mock */ + when(dataServiceRestTemplate.exchange(anyString(), eq(HttpMethod.POST), any(HttpEntity.class), eq(ViewDto.class))) + .thenReturn(ResponseEntity.status(HttpStatus.NO_CONTENT) + .build()); + + /* test */ + assertThrows(DataServiceException.class, () -> { + dataServiceGateway.createView(DATABASE_1_ID, VIEW_1_CREATE_DTO); + }); + } + + @Test + public void createView_emptyBody_fails() { + + /* mock */ + when(dataServiceRestTemplate.exchange(anyString(), eq(HttpMethod.POST), any(HttpEntity.class), eq(ViewDto.class))) + .thenReturn(ResponseEntity.status(HttpStatus.CREATED) + .build()); + + /* test */ + assertThrows(DataServiceException.class, () -> { + dataServiceGateway.createView(DATABASE_1_ID, VIEW_1_CREATE_DTO); + }); + } + + @Test + public void deleteView_succeeds() throws DataServiceException, DataServiceConnectionException, + ViewNotFoundException { + + /* mock */ + when(dataServiceRestTemplate.exchange(anyString(), eq(HttpMethod.DELETE), eq(HttpEntity.EMPTY), eq(Void.class))) + .thenReturn(ResponseEntity.status(HttpStatus.ACCEPTED) + .build()); + + /* test */ + dataServiceGateway.deleteView(DATABASE_1_ID, VIEW_1_ID); + } + + @Test + public void deleteView_connection_fails() { + + /* mock */ + doThrow(HttpServerErrorException.class) + .when(dataServiceRestTemplate) + .exchange(anyString(), eq(HttpMethod.DELETE), eq(HttpEntity.EMPTY), eq(Void.class)); + + /* test */ + assertThrows(DataServiceConnectionException.class, () -> { + dataServiceGateway.deleteView(DATABASE_1_ID, VIEW_1_ID); + }); + } + + @Test + public void deleteView_unauthorized_fails() { + + /* mock */ + doThrow(HttpClientErrorException.Unauthorized.class) + .when(dataServiceRestTemplate) + .exchange(anyString(), eq(HttpMethod.DELETE), eq(HttpEntity.EMPTY), eq(Void.class)); + + /* test */ + assertThrows(DataServiceException.class, () -> { + dataServiceGateway.deleteView(DATABASE_1_ID, VIEW_1_ID); + }); + } + + @Test + public void deleteView_notFound_fails() { + + /* mock */ + doThrow(HttpClientErrorException.NotFound.class) + .when(dataServiceRestTemplate) + .exchange(anyString(), eq(HttpMethod.DELETE), eq(HttpEntity.EMPTY), eq(Void.class)); + + /* test */ + assertThrows(ViewNotFoundException.class, () -> { + dataServiceGateway.deleteView(DATABASE_1_ID, VIEW_1_ID); + }); + } + + @Test + public void deleteView_responseCode_fails() { + + /* mock */ + when(dataServiceRestTemplate.exchange(anyString(), eq(HttpMethod.DELETE), eq(HttpEntity.EMPTY), eq(Void.class))) + .thenReturn(ResponseEntity.status(HttpStatus.NO_CONTENT) + .build()); + + /* test */ + assertThrows(DataServiceException.class, () -> { + dataServiceGateway.deleteView(DATABASE_1_ID, VIEW_1_ID); + }); + } + + @Test + public void findQuery_succeeds() throws DataServiceException, DataServiceConnectionException, + QueryNotFoundException { + + /* mock */ + when(dataServiceRestTemplate.exchange(anyString(), eq(HttpMethod.GET), eq(HttpEntity.EMPTY), eq(QueryDto.class))) + .thenReturn(ResponseEntity.status(HttpStatus.OK) + .body(QUERY_1_DTO)); + + /* test */ + dataServiceGateway.findQuery(DATABASE_1_ID, QUERY_1_ID); + } + + @Test + public void findQuery_connection_fails() { + + /* mock */ + doThrow(HttpServerErrorException.class) + .when(dataServiceRestTemplate) + .exchange(anyString(), eq(HttpMethod.GET), eq(HttpEntity.EMPTY), eq(QueryDto.class)); + + /* test */ + assertThrows(DataServiceConnectionException.class, () -> { + dataServiceGateway.findQuery(DATABASE_1_ID, QUERY_1_ID); + }); + } + + @Test + public void findQuery_unauthorized_fails() { + + /* mock */ + doThrow(HttpClientErrorException.Unauthorized.class) + .when(dataServiceRestTemplate) + .exchange(anyString(), eq(HttpMethod.GET), eq(HttpEntity.EMPTY), eq(QueryDto.class)); + + /* test */ + assertThrows(DataServiceException.class, () -> { + dataServiceGateway.findQuery(DATABASE_1_ID, QUERY_1_ID); + }); + } + + @Test + public void findQuery_notFound_fails() { + + /* mock */ + doThrow(HttpClientErrorException.NotFound.class) + .when(dataServiceRestTemplate) + .exchange(anyString(), eq(HttpMethod.GET), eq(HttpEntity.EMPTY), eq(QueryDto.class)); + + /* test */ + assertThrows(QueryNotFoundException.class, () -> { + dataServiceGateway.findQuery(DATABASE_1_ID, QUERY_1_ID); + }); + } + + @Test + public void findQuery_notAcceptable_fails() { + + /* mock */ + doThrow(HttpClientErrorException.NotAcceptable.class) + .when(dataServiceRestTemplate) + .exchange(anyString(), eq(HttpMethod.GET), eq(HttpEntity.EMPTY), eq(QueryDto.class)); + + /* test */ + assertThrows(DataServiceException.class, () -> { + dataServiceGateway.findQuery(DATABASE_1_ID, QUERY_1_ID); + }); + } + + @Test + public void findQuery_responseCode_fails() { + + /* mock */ + when(dataServiceRestTemplate.exchange(anyString(), eq(HttpMethod.GET), eq(HttpEntity.EMPTY), eq(QueryDto.class))) + .thenReturn(ResponseEntity.status(HttpStatus.NO_CONTENT) + .build()); + + /* test */ + assertThrows(DataServiceException.class, () -> { + dataServiceGateway.findQuery(DATABASE_1_ID, QUERY_1_ID); + }); + } + + @Test + public void exportQuery_succeeds() throws DataServiceException, DataServiceConnectionException, + QueryNotFoundException { + + /* mock */ + when(dataServiceRestTemplate.exchange(anyString(), eq(HttpMethod.GET), eq(HttpEntity.EMPTY), eq(ExportResourceDto.class))) + .thenReturn(ResponseEntity.status(HttpStatus.OK) + .build()); + + /* test */ + dataServiceGateway.exportQuery(DATABASE_1_ID, QUERY_1_ID); + } + + @Test + public void exportQuery_connection_fails() { + + /* mock */ + doThrow(HttpServerErrorException.class) + .when(dataServiceRestTemplate) + .exchange(anyString(), eq(HttpMethod.GET), eq(HttpEntity.EMPTY), eq(ExportResourceDto.class)); + + /* test */ + assertThrows(DataServiceConnectionException.class, () -> { + dataServiceGateway.exportQuery(DATABASE_1_ID, QUERY_1_ID); + }); + } + + @Test + public void exportQuery_unauthorized_fails() { + + /* mock */ + doThrow(HttpClientErrorException.Unauthorized.class) + .when(dataServiceRestTemplate) + .exchange(anyString(), eq(HttpMethod.GET), eq(HttpEntity.EMPTY), eq(ExportResourceDto.class)); + + /* test */ + assertThrows(DataServiceException.class, () -> { + dataServiceGateway.exportQuery(DATABASE_1_ID, QUERY_1_ID); + }); + } + + @Test + public void exportQuery_notFound_fails() { + + /* mock */ + doThrow(HttpClientErrorException.NotFound.class) + .when(dataServiceRestTemplate) + .exchange(anyString(), eq(HttpMethod.GET), eq(HttpEntity.EMPTY), eq(ExportResourceDto.class)); + + /* test */ + assertThrows(QueryNotFoundException.class, () -> { + dataServiceGateway.exportQuery(DATABASE_1_ID, QUERY_1_ID); + }); + } + + @Test + public void exportQuery_responseCode_fails() { + + /* mock */ + when(dataServiceRestTemplate.exchange(anyString(), eq(HttpMethod.GET), eq(HttpEntity.EMPTY), eq(ExportResourceDto.class))) + .thenReturn(ResponseEntity.status(HttpStatus.NO_CONTENT) + .build()); + + /* test */ + assertThrows(DataServiceException.class, () -> { + dataServiceGateway.exportQuery(DATABASE_1_ID, QUERY_1_ID); + }); + } + + @Test + public void getTableSchemas_succeeds() throws DataServiceException, DataServiceConnectionException, TableNotFoundException { + + /* mock */ + when(dataServiceRestTemplate.exchange(anyString(), eq(HttpMethod.GET), eq(HttpEntity.EMPTY), eq(TableDto[].class))) + .thenReturn(ResponseEntity.status(HttpStatus.OK) + .body(new TableDto[]{})); + + /* test */ + dataServiceGateway.getTableSchemas(DATABASE_1_ID); + } + + @Test + public void getTableSchemas_connection_fails() { + + /* mock */ + doThrow(HttpServerErrorException.class) + .when(dataServiceRestTemplate) + .exchange(anyString(), eq(HttpMethod.GET), eq(HttpEntity.EMPTY), eq(TableDto[].class)); + + /* test */ + assertThrows(DataServiceConnectionException.class, () -> { + dataServiceGateway.getTableSchemas(DATABASE_1_ID); + }); + } + + @Test + public void getTableSchemas_unauthorized_fails() { + + /* mock */ + doThrow(HttpClientErrorException.Unauthorized.class) + .when(dataServiceRestTemplate) + .exchange(anyString(), eq(HttpMethod.GET), eq(HttpEntity.EMPTY), eq(TableDto[].class)); + + /* test */ + assertThrows(DataServiceException.class, () -> { + dataServiceGateway.getTableSchemas(DATABASE_1_ID); + }); + } + + @Test + public void getTableSchemas_notFound_fails() { + + /* mock */ + doThrow(HttpClientErrorException.NotFound.class) + .when(dataServiceRestTemplate) + .exchange(anyString(), eq(HttpMethod.GET), eq(HttpEntity.EMPTY), eq(TableDto[].class)); + + /* test */ + assertThrows(TableNotFoundException.class, () -> { + dataServiceGateway.getTableSchemas(DATABASE_1_ID); + }); + } + + @Test + public void getTableSchemas_responseCode_fails() { + + /* mock */ + when(dataServiceRestTemplate.exchange(anyString(), eq(HttpMethod.GET), eq(HttpEntity.EMPTY), eq(TableDto[].class))) + .thenReturn(ResponseEntity.status(HttpStatus.NO_CONTENT) + .build()); + + /* test */ + assertThrows(DataServiceException.class, () -> { + dataServiceGateway.getTableSchemas(DATABASE_1_ID); + }); + } + + @Test + public void getTableSchemas_emptyBody_fails() { + + /* mock */ + when(dataServiceRestTemplate.exchange(anyString(), eq(HttpMethod.GET), eq(HttpEntity.EMPTY), eq(TableDto[].class))) + .thenReturn(ResponseEntity.status(HttpStatus.OK) + .build()); + + /* test */ + assertThrows(DataServiceException.class, () -> { + dataServiceGateway.getTableSchemas(DATABASE_1_ID); + }); + } + + @Test + public void getViewSchemas_succeeds() throws DataServiceException, DataServiceConnectionException, + ViewNotFoundException { + + /* mock */ + when(dataServiceRestTemplate.exchange(anyString(), eq(HttpMethod.GET), eq(HttpEntity.EMPTY), eq(ViewDto[].class))) + .thenReturn(ResponseEntity.status(HttpStatus.OK) + .body(new ViewDto[]{})); + + /* test */ + dataServiceGateway.getViewSchemas(DATABASE_1_ID); + } + + @Test + public void getViewSchemas_connection_fails() { + + /* mock */ + doThrow(HttpServerErrorException.class) + .when(dataServiceRestTemplate) + .exchange(anyString(), eq(HttpMethod.GET), eq(HttpEntity.EMPTY), eq(ViewDto[].class)); + + /* test */ + assertThrows(DataServiceConnectionException.class, () -> { + dataServiceGateway.getViewSchemas(DATABASE_1_ID); + }); + } + + @Test + public void getViewSchemas_unauthorized_fails() { + + /* mock */ + doThrow(HttpClientErrorException.Unauthorized.class) + .when(dataServiceRestTemplate) + .exchange(anyString(), eq(HttpMethod.GET), eq(HttpEntity.EMPTY), eq(ViewDto[].class)); + + /* test */ + assertThrows(DataServiceException.class, () -> { + dataServiceGateway.getViewSchemas(DATABASE_1_ID); + }); + } + + @Test + public void getViewSchemas_notFound_fails() { + + /* mock */ + doThrow(HttpClientErrorException.NotFound.class) + .when(dataServiceRestTemplate) + .exchange(anyString(), eq(HttpMethod.GET), eq(HttpEntity.EMPTY), eq(ViewDto[].class)); + + /* test */ + assertThrows(ViewNotFoundException.class, () -> { + dataServiceGateway.getViewSchemas(DATABASE_1_ID); + }); + } + + @Test + public void getViewSchemas_responseCode_fails() { + + /* mock */ + when(dataServiceRestTemplate.exchange(anyString(), eq(HttpMethod.GET), eq(HttpEntity.EMPTY), eq(ViewDto[].class))) + .thenReturn(ResponseEntity.status(HttpStatus.NO_CONTENT) + .build()); + + /* test */ + assertThrows(DataServiceException.class, () -> { + dataServiceGateway.getViewSchemas(DATABASE_1_ID); + }); + } + + @Test + public void getViewSchemas_emptyBody_fails() { + + /* mock */ + when(dataServiceRestTemplate.exchange(anyString(), eq(HttpMethod.GET), eq(HttpEntity.EMPTY), eq(ViewDto[].class))) + .thenReturn(ResponseEntity.status(HttpStatus.OK) + .build()); + + /* test */ + assertThrows(DataServiceException.class, () -> { + dataServiceGateway.getViewSchemas(DATABASE_1_ID); + }); + } + + @Test + public void getTableStatistics_succeeds() throws DataServiceException, DataServiceConnectionException, + TableNotFoundException { + + /* mock */ + when(dataServiceRestTemplate.exchange(anyString(), eq(HttpMethod.GET), eq(HttpEntity.EMPTY), eq(TableStatisticDto.class))) + .thenReturn(ResponseEntity.status(HttpStatus.OK) + .body(TABLE_8_STATISTIC_DTO)); + + /* test */ + dataServiceGateway.getTableStatistics(DATABASE_3_ID, TABLE_8_ID); + } + + @Test + public void getTableStatistics_connection_fails() { + + /* mock */ + doThrow(HttpServerErrorException.class) + .when(dataServiceRestTemplate) + .exchange(anyString(), eq(HttpMethod.GET), eq(HttpEntity.EMPTY), eq(TableStatisticDto.class)); + + /* test */ + assertThrows(DataServiceConnectionException.class, () -> { + dataServiceGateway.getTableStatistics(DATABASE_3_ID, TABLE_8_ID); + }); + } + + @Test + public void getTableStatistics_unauthorized_fails() { + + /* mock */ + doThrow(HttpClientErrorException.Unauthorized.class) + .when(dataServiceRestTemplate) + .exchange(anyString(), eq(HttpMethod.GET), eq(HttpEntity.EMPTY), eq(TableStatisticDto.class)); + + /* test */ + assertThrows(DataServiceException.class, () -> { + dataServiceGateway.getTableStatistics(DATABASE_3_ID, TABLE_8_ID); + }); + } + + @Test + public void getTableStatistics_notFound_fails() { + + /* mock */ + doThrow(HttpClientErrorException.NotFound.class) + .when(dataServiceRestTemplate) + .exchange(anyString(), eq(HttpMethod.GET), eq(HttpEntity.EMPTY), eq(TableStatisticDto.class)); + + /* test */ + assertThrows(TableNotFoundException.class, () -> { + dataServiceGateway.getTableStatistics(DATABASE_3_ID, TABLE_8_ID); + }); + } + + @Test + public void getTableStatistics_responseCode_fails() { + + /* mock */ + when(dataServiceRestTemplate.exchange(anyString(), eq(HttpMethod.GET), eq(HttpEntity.EMPTY), eq(TableStatisticDto.class))) + .thenReturn(ResponseEntity.status(HttpStatus.NO_CONTENT) + .build()); + + /* test */ + assertThrows(DataServiceException.class, () -> { + dataServiceGateway.getTableStatistics(DATABASE_3_ID, TABLE_8_ID); + }); + } + + @Test + public void getTableStatistics_emptyBody_fails() { + + /* mock */ + when(dataServiceRestTemplate.exchange(anyString(), eq(HttpMethod.GET), eq(HttpEntity.EMPTY), eq(TableStatisticDto.class))) + .thenReturn(ResponseEntity.status(HttpStatus.OK) + .build()); + + /* test */ + assertThrows(DataServiceException.class, () -> { + dataServiceGateway.getTableStatistics(DATABASE_3_ID, TABLE_8_ID); + }); + } } diff --git a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/gateway/KeycloakGatewayUnitTest.java b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/gateway/KeycloakGatewayUnitTest.java index 06e75c62fe..f60211d590 100644 --- a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/gateway/KeycloakGatewayUnitTest.java +++ b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/gateway/KeycloakGatewayUnitTest.java @@ -16,12 +16,11 @@ import org.springframework.http.*; import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.web.client.HttpClientErrorException; import org.springframework.web.client.HttpServerErrorException; -import org.springframework.web.client.ResourceAccessException; import org.springframework.web.client.RestTemplate; import java.nio.charset.Charset; -import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.*; @Log4j2 @@ -31,6 +30,10 @@ public class KeycloakGatewayUnitTest extends AbstractUnitTest { @MockBean @Qualifier("keycloakRestTemplate") + private RestTemplate keycloakRestTemplate; + + @MockBean + @Qualifier("restTemplate") private RestTemplate restTemplate; @Autowired @@ -41,7 +44,7 @@ public class KeycloakGatewayUnitTest extends AbstractUnitTest { CredentialsInvalidException { /* mock */ - when(restTemplate.exchange(anyString(), eq(HttpMethod.POST), any(HttpEntity.class), eq(TokenDto.class))) + when(keycloakRestTemplate.exchange(anyString(), eq(HttpMethod.POST), any(HttpEntity.class), eq(TokenDto.class))) .thenReturn(ResponseEntity.status(HttpStatus.OK) .body(TOKEN_DTO)); @@ -50,11 +53,11 @@ public class KeycloakGatewayUnitTest extends AbstractUnitTest { } @Test - public void obtainToken_fails() { + public void obtainToken_connection_fails() { /* mock */ - doThrow(HttpServerErrorException.BadGateway.create(HttpStatus.BAD_GATEWAY, "", new HttpHeaders(), new byte[]{}, Charset.defaultCharset())) - .when(restTemplate) + doThrow(HttpServerErrorException.BadGateway.class) + .when(keycloakRestTemplate) .exchange(anyString(), eq(HttpMethod.POST), any(HttpEntity.class), eq(TokenDto.class)); /* test */ @@ -63,15 +66,43 @@ public class KeycloakGatewayUnitTest extends AbstractUnitTest { }); } + @Test + public void obtainToken_unauthorized_fails() { + + /* mock */ + doThrow(HttpClientErrorException.Unauthorized.class) + .when(keycloakRestTemplate) + .exchange(anyString(), eq(HttpMethod.POST), any(HttpEntity.class), eq(TokenDto.class)); + + /* test */ + assertThrows(CredentialsInvalidException.class, () -> { + keycloakGateway.obtainToken(); + }); + } + + @Test + public void obtainToken_unexpected_fails() { + + /* mock */ + doThrow(HttpClientErrorException.BadRequest.class) + .when(keycloakRestTemplate) + .exchange(anyString(), eq(HttpMethod.POST), any(HttpEntity.class), eq(TokenDto.class)); + + /* test */ + assertThrows(AuthServiceException.class, () -> { + keycloakGateway.obtainToken(); + }); + } + @Test public void createUser_succeeds() throws UserExistsException, EmailExistsException, AuthServiceException, AuthServiceConnectionException, CredentialsInvalidException { /* mock */ - when(restTemplate.exchange(anyString(), eq(HttpMethod.POST), any(HttpEntity.class), eq(TokenDto.class))) + when(keycloakRestTemplate.exchange(anyString(), eq(HttpMethod.POST), any(HttpEntity.class), eq(TokenDto.class))) .thenReturn(ResponseEntity.status(HttpStatus.OK) .body(TOKEN_DTO)); - when(restTemplate.exchange(anyString(), eq(HttpMethod.POST), any(HttpEntity.class), eq(Void.class))) + when(keycloakRestTemplate.exchange(anyString(), eq(HttpMethod.POST), any(HttpEntity.class), eq(Void.class))) .thenReturn(ResponseEntity.status(HttpStatus.CREATED) .build()); @@ -83,10 +114,10 @@ public class KeycloakGatewayUnitTest extends AbstractUnitTest { public void createUser_fails() { /* mock */ - when(restTemplate.exchange(anyString(), eq(HttpMethod.POST), any(HttpEntity.class), eq(TokenDto.class))) + when(keycloakRestTemplate.exchange(anyString(), eq(HttpMethod.POST), any(HttpEntity.class), eq(TokenDto.class))) .thenReturn(ResponseEntity.status(HttpStatus.OK) .body(TOKEN_DTO)); - when(restTemplate.exchange(anyString(), eq(HttpMethod.POST), any(HttpEntity.class), eq(Void.class))) + when(keycloakRestTemplate.exchange(anyString(), eq(HttpMethod.POST), any(HttpEntity.class), eq(Void.class))) .thenReturn(ResponseEntity.status(HttpStatus.NO_CONTENT) .build()); @@ -96,32 +127,15 @@ public class KeycloakGatewayUnitTest extends AbstractUnitTest { }); } - @Test - public void createUser_sameEMail_fails() { - - /* mock */ - when(restTemplate.exchange(anyString(), eq(HttpMethod.POST), any(HttpEntity.class), eq(TokenDto.class))) - .thenReturn(ResponseEntity.status(HttpStatus.OK) - .body(TOKEN_DTO)); - doThrow(HttpClientErrorException.Conflict.create(HttpStatus.CONFLICT, "same email", new HttpHeaders(), new byte[]{}, null)) - .when(restTemplate) - .exchange(anyString(), eq(HttpMethod.POST), any(HttpEntity.class), eq(Void.class)); - - /* test */ - assertThrows(EmailExistsException.class, () -> { - keycloakGateway.createUser(USER_1_KEYCLOAK_SIGNUP_REQUEST); - }); - } - @Test public void createUser_sameUsername_fails() { /* mock */ - when(restTemplate.exchange(anyString(), eq(HttpMethod.POST), any(HttpEntity.class), eq(TokenDto.class))) + when(keycloakRestTemplate.exchange(anyString(), eq(HttpMethod.POST), any(HttpEntity.class), eq(TokenDto.class))) .thenReturn(ResponseEntity.status(HttpStatus.OK) .body(TOKEN_DTO)); - doThrow(HttpClientErrorException.Conflict.create(HttpStatus.CONFLICT, "same username", new HttpHeaders(), new byte[]{}, null)) - .when(restTemplate) + doThrow(HttpClientErrorException.Conflict.class) + .when(keycloakRestTemplate) .exchange(anyString(), eq(HttpMethod.POST), any(HttpEntity.class), eq(Void.class)); /* test */ @@ -131,14 +145,14 @@ public class KeycloakGatewayUnitTest extends AbstractUnitTest { } @Test - public void createUser_unexpected_fails() { + public void createUser_connection_fails() { /* mock */ - when(restTemplate.exchange(anyString(), eq(HttpMethod.POST), any(HttpEntity.class), eq(TokenDto.class))) + when(keycloakRestTemplate.exchange(anyString(), eq(HttpMethod.POST), any(HttpEntity.class), eq(TokenDto.class))) .thenReturn(ResponseEntity.status(HttpStatus.OK) .body(TOKEN_DTO)); - doThrow(HttpServerErrorException.BadGateway.create(HttpStatus.BAD_GATEWAY, "", new HttpHeaders(), new byte[]{}, Charset.defaultCharset())) - .when(restTemplate) + doThrow(HttpServerErrorException.class) + .when(keycloakRestTemplate) .exchange(anyString(), eq(HttpMethod.POST), any(HttpEntity.class), eq(Void.class)); /* test */ @@ -151,10 +165,10 @@ public class KeycloakGatewayUnitTest extends AbstractUnitTest { public void deleteUser_fails() { /* mock */ - when(restTemplate.exchange(anyString(), eq(HttpMethod.POST), any(HttpEntity.class), eq(TokenDto.class))) + when(keycloakRestTemplate.exchange(anyString(), eq(HttpMethod.POST), any(HttpEntity.class), eq(TokenDto.class))) .thenReturn(ResponseEntity.status(HttpStatus.OK) .body(TOKEN_DTO)); - when(restTemplate.exchange(anyString(), eq(HttpMethod.DELETE), any(HttpEntity.class), eq(Void.class))) + when(keycloakRestTemplate.exchange(anyString(), eq(HttpMethod.DELETE), any(HttpEntity.class), eq(Void.class))) .thenReturn(ResponseEntity.status(HttpStatus.OK) .build()); @@ -169,10 +183,10 @@ public class KeycloakGatewayUnitTest extends AbstractUnitTest { AuthServiceConnectionException, CredentialsInvalidException { /* mock */ - when(restTemplate.exchange(anyString(), eq(HttpMethod.POST), any(HttpEntity.class), eq(TokenDto.class))) + when(keycloakRestTemplate.exchange(anyString(), eq(HttpMethod.POST), any(HttpEntity.class), eq(TokenDto.class))) .thenReturn(ResponseEntity.status(HttpStatus.OK) .body(TOKEN_DTO)); - when(restTemplate.exchange(anyString(), eq(HttpMethod.DELETE), any(HttpEntity.class), eq(Void.class))) + when(keycloakRestTemplate.exchange(anyString(), eq(HttpMethod.DELETE), any(HttpEntity.class), eq(Void.class))) .thenReturn(ResponseEntity.status(HttpStatus.NO_CONTENT) .build()); @@ -181,48 +195,48 @@ public class KeycloakGatewayUnitTest extends AbstractUnitTest { } @Test - public void deleteUser_unexpected_fails() { + public void deleteUser_notFound_fails() { /* mock */ - when(restTemplate.exchange(anyString(), eq(HttpMethod.POST), any(HttpEntity.class), eq(TokenDto.class))) + when(keycloakRestTemplate.exchange(anyString(), eq(HttpMethod.POST), any(HttpEntity.class), eq(TokenDto.class))) .thenReturn(ResponseEntity.status(HttpStatus.OK) .body(TOKEN_DTO)); - doThrow(ResourceAccessException.class) - .when(restTemplate) + doThrow(HttpClientErrorException.NotFound.class) + .when(keycloakRestTemplate) .exchange(anyString(), eq(HttpMethod.DELETE), any(HttpEntity.class), eq(Void.class)); /* test */ - assertThrows(AuthServiceException.class, () -> { + assertThrows(UserNotFoundException.class, () -> { keycloakGateway.deleteUser(USER_1_ID); }); } @Test - public void deleteUser_notFound_fails() { + public void deleteUser_unexpected_fails() { /* mock */ - when(restTemplate.exchange(anyString(), eq(HttpMethod.POST), any(HttpEntity.class), eq(TokenDto.class))) + when(keycloakRestTemplate.exchange(anyString(), eq(HttpMethod.POST), any(HttpEntity.class), eq(TokenDto.class))) .thenReturn(ResponseEntity.status(HttpStatus.OK) .body(TOKEN_DTO)); - doThrow(HttpClientErrorException.NotFound.create(HttpStatus.NOT_FOUND, "", new HttpHeaders(), new byte[]{}, Charset.defaultCharset())) - .when(restTemplate) + doThrow(HttpClientErrorException.Conflict.class) + .when(keycloakRestTemplate) .exchange(anyString(), eq(HttpMethod.DELETE), any(HttpEntity.class), eq(Void.class)); /* test */ - assertThrows(UserNotFoundException.class, () -> { + assertThrows(AuthServiceException.class, () -> { keycloakGateway.deleteUser(USER_1_ID); }); } @Test - public void deleteUser_unexpected2_fails() { + public void deleteUser_connection_fails() { /* mock */ - when(restTemplate.exchange(anyString(), eq(HttpMethod.POST), any(HttpEntity.class), eq(TokenDto.class))) + when(keycloakRestTemplate.exchange(anyString(), eq(HttpMethod.POST), any(HttpEntity.class), eq(TokenDto.class))) .thenReturn(ResponseEntity.status(HttpStatus.OK) .body(TOKEN_DTO)); - doThrow(HttpServerErrorException.BadGateway.create(HttpStatus.BAD_GATEWAY, "", new HttpHeaders(), new byte[]{}, Charset.defaultCharset())) - .when(restTemplate) + doThrow(HttpServerErrorException.class) + .when(keycloakRestTemplate) .exchange(anyString(), eq(HttpMethod.DELETE), any(HttpEntity.class), eq(Void.class)); /* test */ @@ -236,10 +250,10 @@ public class KeycloakGatewayUnitTest extends AbstractUnitTest { CredentialsInvalidException { /* mock */ - when(restTemplate.exchange(anyString(), eq(HttpMethod.POST), any(HttpEntity.class), eq(TokenDto.class))) + when(keycloakRestTemplate.exchange(anyString(), eq(HttpMethod.POST), any(HttpEntity.class), eq(TokenDto.class))) .thenReturn(ResponseEntity.status(HttpStatus.OK) .body(TOKEN_DTO)); - when(restTemplate.exchange(anyString(), eq(HttpMethod.PUT), any(HttpEntity.class), eq(Void.class))) + when(keycloakRestTemplate.exchange(anyString(), eq(HttpMethod.PUT), any(HttpEntity.class), eq(Void.class))) .thenReturn(ResponseEntity.status(HttpStatus.NO_CONTENT) .build()); @@ -251,10 +265,10 @@ public class KeycloakGatewayUnitTest extends AbstractUnitTest { public void updateUserCredentials_fails() { /* mock */ - when(restTemplate.exchange(anyString(), eq(HttpMethod.POST), any(HttpEntity.class), eq(TokenDto.class))) + when(keycloakRestTemplate.exchange(anyString(), eq(HttpMethod.POST), any(HttpEntity.class), eq(TokenDto.class))) .thenReturn(ResponseEntity.status(HttpStatus.OK) .body(TOKEN_DTO)); - when(restTemplate.exchange(anyString(), eq(HttpMethod.PUT), any(HttpEntity.class), eq(Void.class))) + when(keycloakRestTemplate.exchange(anyString(), eq(HttpMethod.PUT), any(HttpEntity.class), eq(Void.class))) .thenReturn(ResponseEntity.status(HttpStatus.OK) .build()); @@ -265,14 +279,14 @@ public class KeycloakGatewayUnitTest extends AbstractUnitTest { } @Test - public void updateUserCredentials_unexpected_fails() { + public void updateUserCredentials_connection_fails() { /* mock */ - when(restTemplate.exchange(anyString(), eq(HttpMethod.POST), any(HttpEntity.class), eq(TokenDto.class))) + when(keycloakRestTemplate.exchange(anyString(), eq(HttpMethod.POST), any(HttpEntity.class), eq(TokenDto.class))) .thenReturn(ResponseEntity.status(HttpStatus.OK) .body(TOKEN_DTO)); - doThrow(HttpServerErrorException.BadGateway.create(HttpStatus.BAD_GATEWAY, "", new HttpHeaders(), new byte[]{}, Charset.defaultCharset())) - .when(restTemplate) + doThrow(HttpServerErrorException.class) + .when(keycloakRestTemplate) .exchange(anyString(), eq(HttpMethod.PUT), any(HttpEntity.class), eq(Void.class)); /* test */ @@ -281,14 +295,31 @@ public class KeycloakGatewayUnitTest extends AbstractUnitTest { }); } + @Test + public void updateUserCredentials_unexpected_fails() { + + /* mock */ + when(keycloakRestTemplate.exchange(anyString(), eq(HttpMethod.POST), any(HttpEntity.class), eq(TokenDto.class))) + .thenReturn(ResponseEntity.status(HttpStatus.OK) + .body(TOKEN_DTO)); + doThrow(HttpClientErrorException.Conflict.class) + .when(keycloakRestTemplate) + .exchange(anyString(), eq(HttpMethod.PUT), any(HttpEntity.class), eq(Void.class)); + + /* test */ + assertThrows(AuthServiceException.class, () -> { + keycloakGateway.updateUserCredentials(USER_1_ID, USER_1_PASSWORD_DTO); + }); + } + @Test public void findByUsername_notFound_fails() { /* mock */ - when(restTemplate.exchange(anyString(), eq(HttpMethod.POST), any(HttpEntity.class), eq(TokenDto.class))) + when(keycloakRestTemplate.exchange(anyString(), eq(HttpMethod.POST), any(HttpEntity.class), eq(TokenDto.class))) .thenReturn(ResponseEntity.status(HttpStatus.OK) .body(TOKEN_DTO)); - when(restTemplate.exchange(anyString(), eq(HttpMethod.GET), any(HttpEntity.class), eq(UserDto[].class))) + when(keycloakRestTemplate.exchange(anyString(), eq(HttpMethod.GET), any(HttpEntity.class), eq(UserDto[].class))) .thenReturn(ResponseEntity.status(HttpStatus.OK) .body(new UserDto[]{})); @@ -299,18 +330,18 @@ public class KeycloakGatewayUnitTest extends AbstractUnitTest { } @Test - public void findByUsername_remote_fails() { + public void findByUsername_connection_fails() { /* mock */ - when(restTemplate.exchange(anyString(), eq(HttpMethod.POST), any(HttpEntity.class), eq(TokenDto.class))) + when(keycloakRestTemplate.exchange(anyString(), eq(HttpMethod.POST), any(HttpEntity.class), eq(TokenDto.class))) .thenReturn(ResponseEntity.status(HttpStatus.OK) .body(TOKEN_DTO)); - doThrow(ResourceAccessException.class) - .when(restTemplate) + doThrow(HttpServerErrorException.class) + .when(keycloakRestTemplate) .exchange(anyString(), eq(HttpMethod.GET), any(HttpEntity.class), eq(UserDto[].class)); /* test */ - assertThrows(AuthServiceException.class, () -> { + assertThrows(AuthServiceConnectionException.class, () -> { keycloakGateway.findByUsername(USER_1_USERNAME); }); } @@ -318,17 +349,196 @@ public class KeycloakGatewayUnitTest extends AbstractUnitTest { @Test public void findByUsername_unexpected_fails() { + /* mock */ + when(keycloakRestTemplate.exchange(anyString(), eq(HttpMethod.POST), any(HttpEntity.class), eq(TokenDto.class))) + .thenReturn(ResponseEntity.status(HttpStatus.OK) + .body(TOKEN_DTO)); + doThrow(HttpClientErrorException.Conflict.class) + .when(keycloakRestTemplate) + .exchange(anyString(), eq(HttpMethod.GET), any(HttpEntity.class), eq(UserDto[].class)); + + /* test */ + assertThrows(AuthServiceException.class, () -> { + keycloakGateway.findByUsername(USER_1_USERNAME); + }); + } + + @Test + public void findById_succeeds() throws UserNotFoundException, AuthServiceException, AuthServiceConnectionException, + CredentialsInvalidException { + + /* mock */ + when(keycloakRestTemplate.exchange(anyString(), eq(HttpMethod.POST), any(HttpEntity.class), eq(TokenDto.class))) + .thenReturn(ResponseEntity.status(HttpStatus.OK) + .body(TOKEN_DTO)); + when(keycloakRestTemplate.exchange(anyString(), eq(HttpMethod.GET), any(HttpEntity.class), eq(UserDto.class))) + .thenReturn(ResponseEntity.status(HttpStatus.OK) + .body(USER_1_KEYCLOAK_DTO)); + + /* test */ + final UserDto response = keycloakGateway.findById(USER_1_ID); + assertEquals(USER_1_ID, response.getId()); + } + + @Test + public void findById_notFound_fails() { + + /* mock */ + when(keycloakRestTemplate.exchange(anyString(), eq(HttpMethod.POST), any(HttpEntity.class), eq(TokenDto.class))) + .thenReturn(ResponseEntity.status(HttpStatus.OK) + .body(TOKEN_DTO)); + doThrow(HttpClientErrorException.NotFound.class) + .when(keycloakRestTemplate) + .exchange(anyString(), eq(HttpMethod.GET), any(HttpEntity.class), eq(UserDto.class)); + + /* test */ + assertThrows(UserNotFoundException.class, () -> { + keycloakGateway.findById(USER_1_ID); + }); + } + + @Test + public void findById_connection_fails() { + + /* mock */ + when(keycloakRestTemplate.exchange(anyString(), eq(HttpMethod.POST), any(HttpEntity.class), eq(TokenDto.class))) + .thenReturn(ResponseEntity.status(HttpStatus.OK) + .body(TOKEN_DTO)); + doThrow(HttpServerErrorException.class) + .when(keycloakRestTemplate) + .exchange(anyString(), eq(HttpMethod.GET), any(HttpEntity.class), eq(UserDto.class)); + + /* test */ + assertThrows(AuthServiceConnectionException.class, () -> { + keycloakGateway.findById(USER_1_ID); + }); + } + + @Test + public void findById_unexpected_fails() { + + /* mock */ + when(keycloakRestTemplate.exchange(anyString(), eq(HttpMethod.POST), any(HttpEntity.class), eq(TokenDto.class))) + .thenReturn(ResponseEntity.status(HttpStatus.OK) + .body(TOKEN_DTO)); + doThrow(HttpClientErrorException.Conflict.class) + .when(keycloakRestTemplate) + .exchange(anyString(), eq(HttpMethod.GET), any(HttpEntity.class), eq(UserDto.class)); + + /* test */ + assertThrows(AuthServiceException.class, () -> { + keycloakGateway.findById(USER_1_ID); + }); + } + + @Test + public void refreshUserToken_succeeds() throws AuthServiceConnectionException, CredentialsInvalidException { + /* mock */ when(restTemplate.exchange(anyString(), eq(HttpMethod.POST), any(HttpEntity.class), eq(TokenDto.class))) .thenReturn(ResponseEntity.status(HttpStatus.OK) .body(TOKEN_DTO)); - doThrow(HttpServerErrorException.BadGateway.create(HttpStatus.BAD_GATEWAY, "", new HttpHeaders(), new byte[]{}, Charset.defaultCharset())) + + /* test */ + final TokenDto response = keycloakGateway.refreshUserToken(TOKEN_DTO.getRefreshToken()); + assertNotNull(response.getAccessToken()); + } + + @Test + public void refreshUserToken_connection_fails() { + + /* mock */ + doThrow(HttpServerErrorException.class) .when(restTemplate) - .exchange(anyString(), eq(HttpMethod.GET), any(HttpEntity.class), eq(UserDto[].class)); + .exchange(anyString(), eq(HttpMethod.POST), any(HttpEntity.class), eq(TokenDto.class)); /* test */ assertThrows(AuthServiceConnectionException.class, () -> { - keycloakGateway.findByUsername(USER_1_USERNAME); + keycloakGateway.refreshUserToken(TOKEN_DTO.getRefreshToken()); + }); + } + + @Test + public void refreshUserToken_unauthorized_fails() { + + /* mock */ + doThrow(HttpClientErrorException.Unauthorized.class) + .when(restTemplate) + .exchange(anyString(), eq(HttpMethod.POST), any(HttpEntity.class), eq(TokenDto.class)); + + /* test */ + assertThrows(CredentialsInvalidException.class, () -> { + keycloakGateway.refreshUserToken(TOKEN_DTO.getRefreshToken()); + }); + } + + @Test + public void refreshUserToken_badRequest_fails() { + + /* mock */ + doThrow(HttpClientErrorException.BadRequest.class) + .when(restTemplate) + .exchange(anyString(), eq(HttpMethod.POST), any(HttpEntity.class), eq(TokenDto.class)); + + /* test */ + assertThrows(CredentialsInvalidException.class, () -> { + keycloakGateway.refreshUserToken(TOKEN_DTO.getRefreshToken()); + }); + } + + @Test + public void refreshUserToken_badRequestInactiveSession_fails() { + + /* mock */ + doThrow(HttpClientErrorException.BadRequest.create(HttpStatus.BAD_REQUEST, "Session not active", new HttpHeaders(), new byte[]{}, Charset.defaultCharset())) + .when(restTemplate) + .exchange(anyString(), eq(HttpMethod.POST), any(HttpEntity.class), eq(TokenDto.class)); + + /* test */ + assertThrows(CredentialsInvalidException.class, () -> { + keycloakGateway.refreshUserToken(TOKEN_DTO.getRefreshToken()); + }); + } + + @Test + public void obtainUserToken_succeeds() throws AuthServiceConnectionException, CredentialsInvalidException, + AccountNotSetupException { + + /* mock */ + when(restTemplate.exchange(anyString(), eq(HttpMethod.POST), any(HttpEntity.class), eq(TokenDto.class))) + .thenReturn(ResponseEntity.status(HttpStatus.OK) + .body(TOKEN_DTO)); + + /* test */ + final TokenDto response = keycloakGateway.obtainUserToken(USER_1_USERNAME, USER_1_PASSWORD); + assertNotNull(response.getAccessToken()); + } + + @Test + public void obtainUserToken_connection_fails() { + + /* mock */ + doThrow(HttpServerErrorException.class) + .when(restTemplate) + .exchange(anyString(), eq(HttpMethod.POST), any(HttpEntity.class), eq(TokenDto.class)); + + /* test */ + assertThrows(AuthServiceConnectionException.class, () -> { + keycloakGateway.obtainUserToken(USER_1_USERNAME, USER_1_PASSWORD); + }); + } + + @Test + public void obtainUserToken_unauthorized_fails() { + + /* mock */ + doThrow(HttpClientErrorException.Unauthorized.class) + .when(restTemplate) + .exchange(anyString(), eq(HttpMethod.POST), any(HttpEntity.class), eq(TokenDto.class)); + + /* test */ + assertThrows(CredentialsInvalidException.class, () -> { + keycloakGateway.obtainUserToken(USER_1_USERNAME, USER_1_PASSWORD); }); } diff --git a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/gateway/OrcidGatewayUnitTest.java b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/gateway/OrcidGatewayUnitTest.java index 4572711ed2..bcd27107bc 100644 --- a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/gateway/OrcidGatewayUnitTest.java +++ b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/gateway/OrcidGatewayUnitTest.java @@ -15,9 +15,11 @@ import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.web.client.HttpServerErrorException; import org.springframework.web.client.ResourceAccessException; import org.springframework.web.client.RestTemplate; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.Mockito.*; @Log4j2 @@ -26,7 +28,6 @@ import static org.mockito.Mockito.*; public class OrcidGatewayUnitTest extends AbstractUnitTest { @MockBean - @Qualifier("keycloakRestTemplate") private RestTemplate restTemplate; @Autowired @@ -45,15 +46,17 @@ public class OrcidGatewayUnitTest extends AbstractUnitTest { } @Test - public void findByUrl_fails() throws OrcidNotFoundException { + public void findByUrl_fails() { /* mock */ - doThrow(ResourceAccessException.class) + doThrow(HttpServerErrorException.class) .when(restTemplate) .exchange(anyString(), eq(HttpMethod.GET), any(HttpEntity.class), eq(OrcidDto.class)); /* test */ - orcidGateway.findByUrl(USER_1_ORCID_URL); + assertThrows(OrcidNotFoundException.class, () -> { + orcidGateway.findByUrl(USER_1_ORCID_URL); + }); } } diff --git a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/gateway/RorGatewayUnitTest.java b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/gateway/RorGatewayUnitTest.java index 384cd290b3..ff9d4f741c 100644 --- a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/gateway/RorGatewayUnitTest.java +++ b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/gateway/RorGatewayUnitTest.java @@ -7,14 +7,14 @@ import lombok.extern.log4j.Log4j2; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.*; import org.springframework.test.context.junit.jupiter.SpringExtension; -import org.springframework.web.client.ResourceAccessException; +import org.springframework.web.client.HttpServerErrorException; import org.springframework.web.client.RestTemplate; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.Mockito.*; @Log4j2 @@ -23,7 +23,6 @@ import static org.mockito.Mockito.*; public class RorGatewayUnitTest extends AbstractUnitTest { @MockBean - @Qualifier("keycloakRestTemplate") private RestTemplate restTemplate; @Autowired @@ -42,15 +41,17 @@ public class RorGatewayUnitTest extends AbstractUnitTest { } @Test - public void findById_fails() throws RorNotFoundException { + public void findById_fails() { /* mock */ - doThrow(ResourceAccessException.class) + doThrow(HttpServerErrorException.class) .when(restTemplate) .exchange(anyString(), eq(HttpMethod.GET), any(HttpEntity.class), eq(RorDto.class)); /* test */ - rorGateway.findById("04d836q62"); + assertThrows(RorNotFoundException.class, () -> { + rorGateway.findById("04d836q62"); + }); } } diff --git a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/gateway/SearchServiceGatewayUnitTest.java b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/gateway/SearchServiceGatewayUnitTest.java index b4b205d4f4..aa1c9d4f05 100644 --- a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/gateway/SearchServiceGatewayUnitTest.java +++ b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/gateway/SearchServiceGatewayUnitTest.java @@ -135,6 +135,20 @@ public class SearchServiceGatewayUnitTest extends AbstractUnitTest { }); } + @Test + public void delete_unauthorized_fails() { + + /* mock */ + doThrow(HttpClientErrorException.Unauthorized.class) + .when(restTemplate) + .exchange(anyString(), eq(HttpMethod.DELETE), any(HttpEntity.class), eq(Void.class)); + + /* test */ + assertThrows(SearchServiceException.class, () -> { + searchServiceGateway.delete(DATABASE_1_ID); + }); + } + @Test public void delete_unexpectedResponse_fails() { final ResponseEntity<Void> mock = ResponseEntity.status(HttpStatus.OK) diff --git a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/mapper/MetadataMapperUnitTest.java b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/mapper/MetadataMapperUnitTest.java index 732c11124f..8a72f2cabb 100644 --- a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/mapper/MetadataMapperUnitTest.java +++ b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/mapper/MetadataMapperUnitTest.java @@ -15,9 +15,7 @@ import at.tuwien.api.user.UserBriefDto; import at.tuwien.api.user.UserDto; import at.tuwien.entities.container.Container; import at.tuwien.entities.database.Database; -import at.tuwien.entities.database.table.Table; -import at.tuwien.entities.identifier.Identifier; -import at.tuwien.entities.identifier.IdentifierType; +import at.tuwien.entities.identifier.*; import at.tuwien.test.AbstractUnitTest; import lombok.extern.log4j.Log4j2; import org.junit.jupiter.api.BeforeEach; @@ -104,12 +102,42 @@ public class MetadataMapperUnitTest extends AbstractUnitTest { /* test */ final Identifier response = metadataMapper.identifierCreateDtoToIdentifier(IDENTIFIER_1_CREATE_DTO); - assertNull(response.getDatabase()); - assertNull(response.getViewId()); - assertNull(response.getQueryId()); - assertNull(response.getTableId()); - assertNull(response.getDoi()); - assertEquals(IDENTIFIER_1_TYPE, response.getType()); + assertNotNull(response.getTitles()); + final List<IdentifierTitle> titles = response.getTitles(); + assertEquals(2, titles.size()); + final IdentifierTitle title0 = titles.get(0); + assertEquals(IDENTIFIER_1_TITLE_1_TITLE, title0.getTitle()); + assertEquals(IDENTIFIER_1_TITLE_1_LANG, title0.getLanguage()); + assertEquals(IDENTIFIER_1_TITLE_1_TYPE, title0.getTitleType()); + final IdentifierTitle title1 = titles.get(1); + assertEquals(IDENTIFIER_1_TITLE_2_TITLE, title1.getTitle()); + assertEquals(IDENTIFIER_1_TITLE_2_LANG, title1.getLanguage()); + assertEquals(IDENTIFIER_1_TITLE_2_TYPE, title1.getTitleType()); + assertNotNull(response.getDescriptions()); + assertEquals(1, response.getDescriptions().size()); + final List<IdentifierDescription> descriptions = response.getDescriptions(); + final IdentifierDescription description0 = descriptions.get(0); + assertNull(description0.getId()); + assertEquals(IDENTIFIER_1_DESCRIPTION_1_DESCRIPTION, description0.getDescription()); + assertEquals(IDENTIFIER_1_DESCRIPTION_1_LANG, description0.getLanguage()); + assertEquals(IDENTIFIER_1_DESCRIPTION_1_TYPE, description0.getDescriptionType()); + assertNotNull(response.getCreators()); + assertEquals(1, response.getCreators().size()); + final Creator creator0 = response.getCreators().get(0); + assertNotNull(creator0); + assertNull(creator0.getId()); + assertEquals(IDENTIFIER_1_CREATOR_1_FIRSTNAME, creator0.getFirstname()); + assertEquals(IDENTIFIER_1_CREATOR_1_LASTNAME, creator0.getLastname()); + assertEquals(IDENTIFIER_1_CREATOR_1_NAME, creator0.getCreatorName()); + assertEquals(IDENTIFIER_1_CREATOR_1_ORCID, creator0.getNameIdentifier()); + assertEquals(IDENTIFIER_1_CREATOR_1_IDENTIFIER_SCHEME_TYPE, creator0.getNameIdentifierScheme()); + assertEquals(IDENTIFIER_1_CREATOR_1_AFFILIATION, creator0.getAffiliation()); + assertEquals(IDENTIFIER_1_CREATOR_1_AFFILIATION_IDENTIFIER, creator0.getAffiliationIdentifier()); + assertEquals(IDENTIFIER_1_CREATOR_1_AFFILIATION_IDENTIFIER_SCHEME, creator0.getAffiliationIdentifierScheme()); + assertEquals(IDENTIFIER_1_CREATOR_1_AFFILIATION_IDENTIFIER_SCHEME_URI, creator0.getAffiliationIdentifierSchemeUri()); + assertNotNull(response.getFunders()); + assertEquals(1, response.getFunders().size()); + assertNull(response.getRelatedIdentifiers()); /* mapstruct strategy for empty values is to set null */ } @Test @@ -154,6 +182,8 @@ public class MetadataMapperUnitTest extends AbstractUnitTest { @Test public void customDatabaseToDatabaseDto_succeeds() { + final Database debug = DATABASE_1; + /* test */ final DatabaseDto response = metadataMapper.customDatabaseToDatabaseDto(DATABASE_1); assertEquals(DATABASE_1_ID, response.getId()); @@ -193,7 +223,9 @@ public class MetadataMapperUnitTest extends AbstractUnitTest { assertEquals(TABLE_1_DESCRIPTION, table0.getDescription()); assertEquals(DATABASE_1_ID, table0.getTdbid()); assertEquals(USER_1_ID, table0.getCreatedBy()); + assertNotNull(table0.getOwner()); assertEquals(USER_1_ID, table0.getOwner().getId()); + assertNotNull(table0.getCreator()); assertEquals(USER_1_ID, table0.getCreator().getId()); assertEquals(TABLE_1_AVG_ROW_LENGTH, table0.getAvgRowLength()); assertEquals(TABLE_1_NUM_ROWS, table0.getNumRows()); @@ -244,7 +276,9 @@ public class MetadataMapperUnitTest extends AbstractUnitTest { assertEquals(TABLE_2_DESCRIPTION, table1.getDescription()); assertEquals(DATABASE_1_ID, table1.getTdbid()); assertEquals(USER_2_ID, table1.getCreatedBy()); + assertNotNull(table1.getOwner()); assertEquals(USER_2_ID, table1.getOwner().getId()); + assertNotNull(table1.getCreator()); assertEquals(USER_2_ID, table1.getCreator().getId()); assertEquals(TABLE_2_AVG_ROW_LENGTH, table1.getAvgRowLength()); assertEquals(TABLE_2_NUM_ROWS, table1.getNumRows()); @@ -316,7 +350,9 @@ public class MetadataMapperUnitTest extends AbstractUnitTest { assertEquals(TABLE_3_DESCRIPTION, table2.getDescription()); assertEquals(DATABASE_1_ID, table2.getTdbid()); assertEquals(USER_3_ID, table2.getCreatedBy()); + assertNotNull(table2.getOwner()); assertEquals(USER_3_ID, table2.getOwner().getId()); + assertNotNull(table2.getCreator()); assertEquals(USER_3_ID, table2.getCreator().getId()); assertEquals(TABLE_3_AVG_ROW_LENGTH, table2.getAvgRowLength()); assertEquals(TABLE_3_NUM_ROWS, table2.getNumRows()); @@ -359,7 +395,9 @@ public class MetadataMapperUnitTest extends AbstractUnitTest { assertEquals(TABLE_4_DESCRIPTION, table3.getDescription()); assertEquals(DATABASE_1_ID, table3.getTdbid()); assertEquals(USER_1_ID, table3.getCreatedBy()); + assertNotNull(table3.getOwner()); assertEquals(USER_1_ID, table3.getOwner().getId()); + assertNotNull(table3.getCreator()); assertEquals(USER_1_ID, table3.getCreator().getId()); assertEquals(TABLE_4_AVG_ROW_LENGTH, table3.getAvgRowLength()); assertEquals(TABLE_4_NUM_ROWS, table3.getNumRows()); diff --git a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/mvc/AuthenticationIntegrationTest.java b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/mvc/AuthenticationIntegrationTest.java new file mode 100644 index 0000000000..c1ca4ab2f2 --- /dev/null +++ b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/mvc/AuthenticationIntegrationTest.java @@ -0,0 +1,293 @@ +package at.tuwien.mvc; + +import at.tuwien.exception.AuthServiceConnectionException; +import at.tuwien.exception.AuthServiceException; +import at.tuwien.exception.CredentialsInvalidException; +import at.tuwien.gateway.KeycloakGateway; +import at.tuwien.repository.ContainerRepository; +import at.tuwien.repository.DatabaseRepository; +import at.tuwien.repository.LicenseRepository; +import at.tuwien.repository.UserRepository; +import at.tuwien.test.AbstractUnitTest; +import at.tuwien.utils.KeycloakUtils; +import dasniko.testcontainers.keycloak.KeycloakContainer; +import lombok.extern.log4j.Log4j2; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.test.web.servlet.MockMvc; +import org.testcontainers.images.PullPolicy; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +import java.util.List; + +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@Log4j2 +@ExtendWith(SpringExtension.class) +@AutoConfigureMockMvc +@Testcontainers +@SpringBootTest +public class AuthenticationIntegrationTest extends AbstractUnitTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private KeycloakUtils keycloakUtils; + + @Autowired + private KeycloakGateway keycloakGateway; + + @Autowired + private UserRepository userRepository; + + @Autowired + private LicenseRepository licenseRepository; + + @Autowired + private ContainerRepository containerRepository; + + @Autowired + private DatabaseRepository databaseRepository; + + @Container + private static KeycloakContainer keycloakContainer = new KeycloakContainer("quay.io/keycloak/keycloak:21.0") + .withImagePullPolicy(PullPolicy.alwaysPull()) + .withAdminUsername("admin") + .withAdminPassword("admin") + .withRealmImportFile("./init/dbrepo-realm.json") + .withEnv("KC_HOSTNAME_STRICT_HTTPS", "false"); + + @DynamicPropertySource + static void keycloakProperties(DynamicPropertyRegistry registry) { + registry.add("dbrepo.endpoints.authService", () -> "http://localhost:" + keycloakContainer.getMappedPort(8080)); + } + + @BeforeEach + public void beforeEach() throws AuthServiceException, AuthServiceConnectionException, CredentialsInvalidException { + genesis(); + /* metadata database */ + licenseRepository.save(LICENSE_1); + containerRepository.save(CONTAINER_1); + userRepository.saveAll(List.of(USER_1, USER_2, USER_3, USER_4)); + databaseRepository.save(DATABASE_1); + /* keycloak */ + keycloakUtils.deleteUser(USER_1_USERNAME); + } + + @Test + public void findById_database_basicUser_fails() throws Exception { + + /* mock */ + keycloakGateway.createUser(USER_1_KEYCLOAK_SIGNUP_REQUEST); + + /* test */ + this.mockMvc.perform(get("/api/database/1").with(httpBasic(USER_1_USERNAME, USER_1_PASSWORD))) + .andDo(print()) + .andExpect(header().doesNotExist("X-Username")) + .andExpect(header().doesNotExist("X-Password")) + .andExpect(header().doesNotExist("Access-Control-Expose-Headers")) + .andExpect(status().isOk()); + } + + @Test + public void findById_database_basicAdmin_succeeds() throws Exception { + + /* mock */ + keycloakGateway.createUser(USER_1_KEYCLOAK_SYSTEM_SIGNUP_REQUEST); + + /* test */ + this.mockMvc.perform(get("/api/database/1").with(httpBasic(USER_1_USERNAME, USER_1_PASSWORD))) + .andDo(print()) + .andExpect(header().string("X-Username", CONTAINER_1_PRIVILEGED_USERNAME)) + .andExpect(header().string("X-Password", CONTAINER_1_PRIVILEGED_PASSWORD)) + .andExpect(header().string("Access-Control-Expose-Headers", "X-Username X-Password")) + .andExpect(status().isOk()); + } + + @Test + @WithMockUser(username = "admin", authorities = {"system"}) + public void findById_database_bearerAdmin_succeeds() throws Exception { + + /* test */ + this.mockMvc.perform(get("/api/database/1")) + .andDo(print()) + .andExpect(header().string("X-Username", CONTAINER_1_PRIVILEGED_USERNAME)) + .andExpect(header().string("X-Password", CONTAINER_1_PRIVILEGED_PASSWORD)) + .andExpect(header().string("Access-Control-Expose-Headers", "X-Username X-Password")) + .andExpect(status().isOk()); + } + + @Test + public void findById_table_basicUser_fails() throws Exception { + + /* mock */ + keycloakGateway.createUser(USER_1_KEYCLOAK_SIGNUP_REQUEST); + + /* test */ + this.mockMvc.perform(get("/api/database/1/table/1").with(httpBasic(USER_1_USERNAME, USER_1_PASSWORD))) + .andDo(print()) + .andExpect(header().doesNotExist("X-Username")) + .andExpect(header().doesNotExist("X-Password")) + .andExpect(header().doesNotExist("X-Host")) + .andExpect(header().doesNotExist("X-Port")) + .andExpect(header().doesNotExist("X-Type")) + .andExpect(header().doesNotExist("X-Database")) + .andExpect(header().doesNotExist("X-Sidecar-Host")) + .andExpect(header().doesNotExist("X-Sidecar-Port")) + .andExpect(header().doesNotExist("Access-Control-Expose-Headers")) + .andExpect(status().isOk()); + } + + @Test + public void findById_table_basicAdmin_succeeds() throws Exception { + + /* mock */ + keycloakGateway.createUser(USER_1_KEYCLOAK_SYSTEM_SIGNUP_REQUEST); + + /* test */ + this.mockMvc.perform(get("/api/database/1/table/1").with(httpBasic(USER_1_USERNAME, USER_1_PASSWORD))) + .andDo(print()) + .andExpect(header().string("X-Username", CONTAINER_1_PRIVILEGED_USERNAME)) + .andExpect(header().string("X-Password", CONTAINER_1_PRIVILEGED_PASSWORD)) + .andExpect(header().string("X-Host", CONTAINER_1_HOST)) + .andExpect(header().string("X-Port", "" + CONTAINER_1_PORT)) + .andExpect(header().string("X-Type", IMAGE_1_JDBC)) + .andExpect(header().string("X-Database", DATABASE_1_INTERNALNAME)) + .andExpect(header().string("X-Sidecar-Host", CONTAINER_1_SIDECAR_HOST)) + .andExpect(header().string("X-Sidecar-Port", "" + CONTAINER_1_SIDECAR_PORT)) + .andExpect(header().string("Access-Control-Expose-Headers", "X-Username X-Password X-Host X-Port X-Type X-Database X-Sidecar-Host X-Sidecar-Port")) + .andExpect(status().isOk()); + } + + @Test + @WithMockUser(username = "admin", authorities = {"system"}) + public void findById_table_bearerAdmin_succeeds() throws Exception { + + /* test */ + this.mockMvc.perform(get("/api/database/1/table/1")) + .andDo(print()) + .andExpect(header().string("X-Username", CONTAINER_1_PRIVILEGED_USERNAME)) + .andExpect(header().string("X-Password", CONTAINER_1_PRIVILEGED_PASSWORD)) + .andExpect(header().string("X-Host", CONTAINER_1_HOST)) + .andExpect(header().string("X-Port", "" + CONTAINER_1_PORT)) + .andExpect(header().string("X-Type", IMAGE_1_JDBC)) + .andExpect(header().string("X-Database", DATABASE_1_INTERNALNAME)) + .andExpect(header().string("X-Sidecar-Host", CONTAINER_1_SIDECAR_HOST)) + .andExpect(header().string("X-Sidecar-Port", "" + CONTAINER_1_SIDECAR_PORT)) + .andExpect(header().string("Access-Control-Expose-Headers", "X-Username X-Password X-Host X-Port X-Type X-Database X-Sidecar-Host X-Sidecar-Port")) + .andExpect(status().isOk()); + } + + @Test + public void findById_view_basicUser_fails() throws Exception { + + /* mock */ + keycloakGateway.createUser(USER_1_KEYCLOAK_SIGNUP_REQUEST); + + /* test */ + this.mockMvc.perform(get("/api/database/1/view/1").with(httpBasic(USER_1_USERNAME, USER_1_PASSWORD))) + .andDo(print()) + .andExpect(header().doesNotExist("X-Username")) + .andExpect(header().doesNotExist("X-Password")) + .andExpect(header().doesNotExist("X-Host")) + .andExpect(header().doesNotExist("X-Port")) + .andExpect(header().doesNotExist("X-Type")) + .andExpect(header().doesNotExist("X-Database")) + .andExpect(header().doesNotExist("Access-Control-Expose-Headers")) + .andExpect(status().isOk()); + } + + @Test + public void findById_view_basicAdmin_succeeds() throws Exception { + + /* mock */ + keycloakGateway.createUser(USER_1_KEYCLOAK_SYSTEM_SIGNUP_REQUEST); + + /* test */ + this.mockMvc.perform(get("/api/database/1/view/1").with(httpBasic(USER_1_USERNAME, USER_1_PASSWORD))) + .andDo(print()) + .andExpect(header().string("X-Username", CONTAINER_1_PRIVILEGED_USERNAME)) + .andExpect(header().string("X-Password", CONTAINER_1_PRIVILEGED_PASSWORD)) + .andExpect(header().string("X-Host", CONTAINER_1_HOST)) + .andExpect(header().string("X-Port", "" + CONTAINER_1_PORT)) + .andExpect(header().string("X-Type", IMAGE_1_JDBC)) + .andExpect(header().string("X-Database", DATABASE_1_INTERNALNAME)) + .andExpect(header().string("Access-Control-Expose-Headers", "X-Username X-Password X-Host X-Port X-Type X-Database")) + .andExpect(status().isOk()); + } + + @Test + @WithMockUser(username = "admin", authorities = {"system"}) + public void findById_view_bearerAdmin_succeeds() throws Exception { + + /* test */ + this.mockMvc.perform(get("/api/database/1/view/1")) + .andDo(print()) + .andExpect(header().string("X-Username", CONTAINER_1_PRIVILEGED_USERNAME)) + .andExpect(header().string("X-Password", CONTAINER_1_PRIVILEGED_PASSWORD)) + .andExpect(header().string("X-Host", CONTAINER_1_HOST)) + .andExpect(header().string("X-Port", "" + CONTAINER_1_PORT)) + .andExpect(header().string("X-Type", IMAGE_1_JDBC)) + .andExpect(header().string("X-Database", DATABASE_1_INTERNALNAME)) + .andExpect(header().string("Access-Control-Expose-Headers", "X-Username X-Password X-Host X-Port X-Type X-Database")) + .andExpect(status().isOk()); + } + + @Test + public void findById_container_basicUser_fails() throws Exception { + + /* mock */ + keycloakGateway.createUser(USER_1_KEYCLOAK_SIGNUP_REQUEST); + + /* test */ + this.mockMvc.perform(get("/api/container/1").with(httpBasic(USER_1_USERNAME, USER_1_PASSWORD))) + .andDo(print()) + .andExpect(header().doesNotExist("X-Username")) + .andExpect(header().doesNotExist("X-Password")) + .andExpect(header().doesNotExist("Access-Control-Expose-Headers")) + .andExpect(status().isOk()); + } + + @Test + public void findById_container_basicAdmin_succeeds() throws Exception { + + /* mock */ + keycloakGateway.createUser(USER_1_KEYCLOAK_SYSTEM_SIGNUP_REQUEST); + + /* test */ + this.mockMvc.perform(get("/api/container/1").with(httpBasic(USER_1_USERNAME, USER_1_PASSWORD))) + .andDo(print()) + .andExpect(header().string("X-Username", CONTAINER_1_PRIVILEGED_USERNAME)) + .andExpect(header().string("X-Password", CONTAINER_1_PRIVILEGED_PASSWORD)) + .andExpect(header().string("Access-Control-Expose-Headers", "X-Username X-Password")) + .andExpect(status().isOk()); + } + + @Test + @WithMockUser(username = "admin", authorities = {"system"}) + public void findById_container_bearerAdmin_succeeds() throws Exception { + + /* test */ + this.mockMvc.perform(get("/api/container/1")) + .andDo(print()) + .andExpect(header().string("X-Username", CONTAINER_1_PRIVILEGED_USERNAME)) + .andExpect(header().string("X-Password", CONTAINER_1_PRIVILEGED_PASSWORD)) + .andExpect(header().string("Access-Control-Expose-Headers", "X-Username X-Password")) + .andExpect(status().isOk()); + } + +} diff --git a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/mvc/PrometheusEndpointMvcTest.java b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/mvc/PrometheusEndpointMvcTest.java index 2b2df73909..d3f75d7060 100644 --- a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/mvc/PrometheusEndpointMvcTest.java +++ b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/mvc/PrometheusEndpointMvcTest.java @@ -500,7 +500,7 @@ public class PrometheusEndpointMvcTest extends AbstractUnitTest { @Test @WithMockUser(username = USER_1_USERNAME, authorities = {"create-table", "delete-table", - "modify-table-column-semantics", "modify-foreign-table-column-semantics", "admin", + "modify-table-column-semantics", "modify-foreign-table-column-semantics", "update-table-statistic", "table-semantic-analyse"}) public void prometheusTableEndpoint_succeeds() { final ColumnSemanticsUpdateDto request = ColumnSemanticsUpdateDto.builder() diff --git a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/AccessServiceUnitTest.java b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/AccessServiceUnitTest.java index 75a08540b0..fa939b0cb1 100644 --- a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/AccessServiceUnitTest.java +++ b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/AccessServiceUnitTest.java @@ -73,7 +73,7 @@ public class AccessServiceUnitTest extends AbstractUnitTest { } @Test - public void create_succeeds() throws ServiceException, ServiceConnectionException, + public void create_succeeds() throws DataServiceException, DataServiceConnectionException, DatabaseNotFoundException, SearchServiceException, SearchServiceConnectionException { /* mock */ @@ -99,7 +99,7 @@ public class AccessServiceUnitTest extends AbstractUnitTest { .exchange(anyString(), eq(HttpMethod.POST), any(HttpEntity.class), eq(Void.class)); /* test */ - assertThrows(ServiceException.class, () -> { + assertThrows(DataServiceException.class, () -> { accessService.create(DATABASE_1, USER_1, AccessTypeDto.WRITE_ALL); }); } @@ -113,7 +113,7 @@ public class AccessServiceUnitTest extends AbstractUnitTest { .exchange(anyString(), eq(HttpMethod.POST), any(HttpEntity.class), eq(Void.class)); /* test */ - assertThrows(ServiceException.class, () -> { + assertThrows(DataServiceException.class, () -> { accessService.create(DATABASE_1, USER_1, AccessTypeDto.WRITE_ALL); }); } @@ -141,7 +141,7 @@ public class AccessServiceUnitTest extends AbstractUnitTest { .exchange(anyString(), eq(HttpMethod.POST), any(HttpEntity.class), eq(Void.class)); /* test */ - assertThrows(ServiceConnectionException.class, () -> { + assertThrows(DataServiceConnectionException.class, () -> { accessService.create(DATABASE_1, USER_1, AccessTypeDto.WRITE_ALL); }); } @@ -223,7 +223,7 @@ public class AccessServiceUnitTest extends AbstractUnitTest { } @Test - public void update_succeeds() throws ServiceException, ServiceConnectionException, AccessNotFoundException, + public void update_succeeds() throws DataServiceException, DataServiceConnectionException, AccessNotFoundException, DatabaseNotFoundException, SearchServiceException, SearchServiceConnectionException { /* mock */ @@ -249,7 +249,7 @@ public class AccessServiceUnitTest extends AbstractUnitTest { .exchange(anyString(), eq(HttpMethod.PUT), any(HttpEntity.class), eq(Void.class)); /* test */ - assertThrows(ServiceException.class, () -> { + assertThrows(DataServiceException.class, () -> { accessService.update(DATABASE_1, USER_1, AccessTypeDto.WRITE_ALL); }); } @@ -263,7 +263,7 @@ public class AccessServiceUnitTest extends AbstractUnitTest { .exchange(anyString(), eq(HttpMethod.PUT), any(HttpEntity.class), eq(Void.class)); /* test */ - assertThrows(ServiceException.class, () -> { + assertThrows(DataServiceException.class, () -> { accessService.update(DATABASE_1, USER_1, AccessTypeDto.WRITE_ALL); }); } @@ -291,7 +291,7 @@ public class AccessServiceUnitTest extends AbstractUnitTest { .exchange(anyString(), eq(HttpMethod.PUT), any(HttpEntity.class), eq(Void.class)); /* test */ - assertThrows(ServiceConnectionException.class, () -> { + assertThrows(DataServiceConnectionException.class, () -> { accessService.update(DATABASE_1, USER_1, AccessTypeDto.WRITE_ALL); }); } @@ -373,7 +373,7 @@ public class AccessServiceUnitTest extends AbstractUnitTest { } @Test - public void delete_succeeds() throws ServiceException, ServiceConnectionException, AccessNotFoundException, + public void delete_succeeds() throws DataServiceException, DataServiceConnectionException, AccessNotFoundException, DatabaseNotFoundException, SearchServiceException, SearchServiceConnectionException { /* mock */ @@ -401,7 +401,7 @@ public class AccessServiceUnitTest extends AbstractUnitTest { .exchange(anyString(), eq(HttpMethod.DELETE), any(HttpEntity.class), eq(Void.class)); /* test */ - assertThrows(ServiceException.class, () -> { + assertThrows(DataServiceException.class, () -> { accessService.delete(DATABASE_1, USER_1); }); } @@ -429,7 +429,7 @@ public class AccessServiceUnitTest extends AbstractUnitTest { .exchange(anyString(), eq(HttpMethod.DELETE), any(HttpEntity.class), eq(Void.class)); /* test */ - assertThrows(ServiceConnectionException.class, () -> { + assertThrows(DataServiceConnectionException.class, () -> { accessService.delete(DATABASE_1, USER_1); }); } diff --git a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/AuthenticationServiceIntegrationTest.java b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/AuthenticationServiceIntegrationTest.java index ba560f0e47..8b7be04cb6 100644 --- a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/AuthenticationServiceIntegrationTest.java +++ b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/AuthenticationServiceIntegrationTest.java @@ -64,8 +64,8 @@ public class AuthenticationServiceIntegrationTest extends AbstractUnitTest { } @Test - public void create_succeeds() throws EmailExistsException, UserExistsException, ServiceException, - ServiceConnectionException, AuthServiceException, AuthServiceConnectionException, + public void create_succeeds() throws EmailExistsException, UserExistsException, + DataServiceConnectionException, AuthServiceException, AuthServiceConnectionException, CredentialsInvalidException { /* mock */ diff --git a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/BrokerServiceIntegrationTest.java b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/BrokerServiceIntegrationTest.java index a340e29345..c9a2ad62b6 100644 --- a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/BrokerServiceIntegrationTest.java +++ b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/BrokerServiceIntegrationTest.java @@ -1,15 +1,13 @@ package at.tuwien.service; import at.tuwien.config.RabbitConfig; +import at.tuwien.exception.*; import at.tuwien.test.AbstractUnitTest; import at.tuwien.api.amqp.GrantExchangePermissionsDto; import at.tuwien.api.amqp.TopicPermissionDto; import at.tuwien.api.amqp.VirtualHostPermissionDto; import at.tuwien.entities.database.DatabaseAccess; import at.tuwien.entities.user.User; -import at.tuwien.exception.ServiceConnectionException; -import at.tuwien.exception.ServiceException; -import at.tuwien.repository.*; import at.tuwien.utils.AmqpUtils; import lombok.extern.log4j.Log4j2; import org.junit.jupiter.api.BeforeEach; @@ -44,9 +42,6 @@ public class BrokerServiceIntegrationTest extends AbstractUnitTest { @Autowired private BrokerService brokerService; - @Autowired - private AmqpUtils amqpUtils; - @Container private static final RabbitMQContainer rabbitContainer = new RabbitMQContainer("rabbitmq:3-management") .withUser(USER_1_USERNAME, USER_1_PASSWORD, Set.of("administrator")) @@ -55,10 +50,6 @@ public class BrokerServiceIntegrationTest extends AbstractUnitTest { @DynamicPropertySource static void rabbitProperties(DynamicPropertyRegistry registry) { registry.add("dbrepo.endpoints.brokerService", rabbitContainer::getHttpUrl); - registry.add("spring.rabbitmq.host", rabbitContainer::getHost); - registry.add("spring.rabbitmq.port", rabbitContainer::getAmqpPort); - registry.add("spring.rabbitmq.username", rabbitContainer::getAdminUsername); - registry.add("spring.rabbitmq.password", rabbitContainer::getAdminPassword); } @BeforeEach @@ -67,7 +58,7 @@ public class BrokerServiceIntegrationTest extends AbstractUnitTest { } @Test - public void updatePermissions_empty_succeeds() throws ServiceException, ServiceConnectionException { + public void updatePermissions_empty_succeeds() throws BrokerServiceException, BrokerServiceConnectionException { /* test */ final VirtualHostPermissionDto permissions = setVirtualHostPermissions_generic(); @@ -79,7 +70,7 @@ public class BrokerServiceIntegrationTest extends AbstractUnitTest { } @Test - public void updatePermissions_writeAll_succeeds() throws ServiceException, ServiceConnectionException { + public void updatePermissions_writeAll_succeeds() throws BrokerServiceException, BrokerServiceConnectionException { /* test */ final VirtualHostPermissionDto permissions = setVirtualHostPermissions_generic(); @@ -91,7 +82,7 @@ public class BrokerServiceIntegrationTest extends AbstractUnitTest { } @Test - public void updatePermissions_writeOwn_succeeds() throws ServiceException, ServiceConnectionException { + public void updatePermissions_writeOwn_succeeds() throws BrokerServiceException, BrokerServiceConnectionException { /* test */ final VirtualHostPermissionDto permissions = setVirtualHostPermissions_generic(); @@ -103,7 +94,7 @@ public class BrokerServiceIntegrationTest extends AbstractUnitTest { } @Test - public void updatePermissions_read_succeeds() throws ServiceException, ServiceConnectionException { + public void updatePermissions_read_succeeds() throws BrokerServiceException, BrokerServiceConnectionException { /* test */ final VirtualHostPermissionDto permissions = setVirtualHostPermissions_generic(); @@ -116,7 +107,8 @@ public class BrokerServiceIntegrationTest extends AbstractUnitTest { @Test @Transactional(readOnly = true) - public void setTopicExchangePermissions_empty_succeeds() throws ServiceException, ServiceConnectionException { + public void setTopicExchangePermissions_empty_succeeds() throws BrokerServiceException, + BrokerServiceConnectionException { /* test */ final TopicPermissionDto permissions = setTopicExchangePermissions_generic(List.of()); @@ -129,7 +121,8 @@ public class BrokerServiceIntegrationTest extends AbstractUnitTest { @Test @Transactional(readOnly = true) - public void setTopicExchangePermissions_writeAll_succeeds() throws ServiceException, ServiceConnectionException { + public void setTopicExchangePermissions_writeAll_succeeds() throws BrokerServiceException, + BrokerServiceConnectionException { /* test */ final TopicPermissionDto permissions = setTopicExchangePermissions_generic(List.of(DATABASE_1_USER_1_WRITE_ALL_ACCESS)); @@ -142,7 +135,8 @@ public class BrokerServiceIntegrationTest extends AbstractUnitTest { @Test @Transactional(readOnly = true) - public void setTopicExchangePermissions_writeOwn_succeeds() throws ServiceException, ServiceConnectionException { + public void setTopicExchangePermissions_writeOwn_succeeds() throws BrokerServiceException, + BrokerServiceConnectionException { /* test */ final TopicPermissionDto permissions = setTopicExchangePermissions_generic(List.of(DATABASE_1_USER_1_WRITE_OWN_ACCESS)); @@ -155,7 +149,8 @@ public class BrokerServiceIntegrationTest extends AbstractUnitTest { @Test @Transactional(readOnly = true) - public void setTopicExchangePermissions_read_succeeds() throws ServiceException, ServiceConnectionException { + public void setTopicExchangePermissions_read_succeeds() throws BrokerServiceException, + BrokerServiceConnectionException { /* test */ final TopicPermissionDto permissions = setTopicExchangePermissions_generic(List.of(DATABASE_1_USER_1_READ_ACCESS)); @@ -170,8 +165,9 @@ public class BrokerServiceIntegrationTest extends AbstractUnitTest { /* ## GENERIC TEST CASES ## */ /* ################################################################################################### */ - protected VirtualHostPermissionDto setVirtualHostPermissions_generic() throws ServiceException, - ServiceConnectionException { + protected VirtualHostPermissionDto setVirtualHostPermissions_generic() throws BrokerServiceException, + BrokerServiceConnectionException { + final AmqpUtils amqpUtils = new AmqpUtils(rabbitContainer.getHttpUrl()); /* mock */ amqpUtils.setVirtualHostPermissions(REALM_DBREPO_NAME, USER_1_USERNAME, USER_1_RABBITMQ_GRANT_DTO); @@ -183,13 +179,14 @@ public class BrokerServiceIntegrationTest extends AbstractUnitTest { @Transactional(readOnly = true) protected TopicPermissionDto setTopicExchangePermissions_generic(List<DatabaseAccess> accesses) - throws ServiceException, ServiceConnectionException { + throws BrokerServiceException, BrokerServiceConnectionException { + final AmqpUtils amqpUtils = new AmqpUtils(rabbitContainer.getHttpUrl()); final GrantExchangePermissionsDto request = GrantExchangePermissionsDto.builder() .exchange(rabbitConfig.getExchangeName()) .read("") .write("") .build(); - final User user1 = User.builder() + final User user = User.builder() .id(USER_1_ID) .username(USER_1_USERNAME) .accesses(accesses) @@ -200,7 +197,7 @@ public class BrokerServiceIntegrationTest extends AbstractUnitTest { amqpUtils.setTopicPermissions(REALM_DBREPO_NAME, USER_1_USERNAME, request); /* test */ - brokerService.setTopicExchangePermissions(user1); + brokerService.setTopicExchangePermissions(user); return amqpUtils.getTopicPermissions(USER_1_USERNAME); } diff --git a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/ConceptServiceUnitTest.java b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/ConceptServiceUnitTest.java index 164f335eb7..7cec5e6241 100644 --- a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/ConceptServiceUnitTest.java +++ b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/ConceptServiceUnitTest.java @@ -6,6 +6,7 @@ import at.tuwien.test.AbstractUnitTest; import at.tuwien.entities.database.table.columns.TableColumnConcept; import lombok.extern.log4j.Log4j2; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; diff --git a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/DataCiteIdentifierServicePersistenceTest.java b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/DataCiteIdentifierServicePersistenceTest.java index 7443439608..0c0cf075c4 100644 --- a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/DataCiteIdentifierServicePersistenceTest.java +++ b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/DataCiteIdentifierServicePersistenceTest.java @@ -1,6 +1,10 @@ package at.tuwien.service; +import at.tuwien.api.identifier.BibliographyTypeDto; +import at.tuwien.entities.identifier.Creator; import at.tuwien.entities.identifier.Identifier; +import at.tuwien.entities.identifier.IdentifierStatusType; +import at.tuwien.entities.identifier.NameIdentifierSchemeType; import at.tuwien.repository.ContainerRepository; import at.tuwien.repository.DatabaseRepository; import at.tuwien.repository.LicenseRepository; @@ -19,6 +23,7 @@ import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.core.ParameterizedTypeReference; +import org.springframework.core.io.InputStreamResource; import org.springframework.http.HttpEntity; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; @@ -38,7 +43,7 @@ import static org.mockito.Mockito.when; @ExtendWith(SpringExtension.class) @DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD) -@SpringBootTest(properties = "spring.profiles.active:local,doi") +@SpringBootTest(properties = "spring.profiles.active:local,junit,doi") public class DataCiteIdentifierServicePersistenceTest extends AbstractUnitTest { @MockBean @@ -71,15 +76,62 @@ public class DataCiteIdentifierServicePersistenceTest extends AbstractUnitTest { genesis(); /* metadata database */ licenseRepository.save(LICENSE_1); - containerRepository.save(CONTAINER_1); - userRepository.saveAll(List.of(USER_1, USER_2, USER_3, USER_4)); - databaseRepository.save(DATABASE_1); + containerRepository.saveAll(List.of(CONTAINER_1, CONTAINER_2, CONTAINER_3, CONTAINER_4)); + userRepository.saveAll(List.of(USER_1, USER_2, USER_3, USER_4, USER_5)); + databaseRepository.saveAll(List.of(DATABASE_1, DATABASE_2, DATABASE_3, DATABASE_4)); } @Test - public void save_database_succeeds() throws ServiceException, ServiceConnectionException, + public void findAll_succeeds() { + + /* test */ + final List<Identifier> response = dataCiteIdentifierService.findAll(null, null, null, null, null); + assertEquals(7, response.size()); + for (Long id : List.of(IDENTIFIER_1_ID, IDENTIFIER_2_ID, IDENTIFIER_3_ID, IDENTIFIER_4_ID, IDENTIFIER_5_ID, IDENTIFIER_6_ID, IDENTIFIER_7_ID)) { + assertTrue(response.stream().map(Identifier::getId).toList().contains(id)); + } + } + + @Test + public void findAll_databaseId_succeeds() { + + /* test */ + final List<Identifier> response = dataCiteIdentifierService.findAll(null, DATABASE_1_ID, null, null, null); + assertEquals(4, response.size()); + assertTrue(response.stream().map(Identifier::getId).toList().contains(IDENTIFIER_1_ID)); + assertTrue(response.stream().map(Identifier::getId).toList().contains(IDENTIFIER_2_ID)); + assertTrue(response.stream().map(Identifier::getId).toList().contains(IDENTIFIER_3_ID)); + assertTrue(response.stream().map(Identifier::getId).toList().contains(IDENTIFIER_4_ID)); + } + + @Test + public void findAll_queryId_succeeds() { + + /* test */ + final List<Identifier> response = dataCiteIdentifierService.findAll(null, null, QUERY_1_ID, null, null); + assertEquals(1, response.size()); + } + + @Test + public void findAll_empty_succeeds() { + + /* test */ + final List<Identifier> response = dataCiteIdentifierService.findAll(null, DATABASE_2_ID, QUERY_1_ID, null, null); + assertEquals(0, response.size()); + } + + @Test + public void find_succeeds() throws IdentifierNotFoundException { + + /* test */ + final Identifier response = dataCiteIdentifierService.find(IDENTIFIER_1_ID); + assertEquals(IDENTIFIER_1, response); + } + + @Test + public void save_database_succeeds() throws DataServiceException, DataServiceConnectionException, DatabaseNotFoundException, MalformedException, IdentifierNotFoundException, ViewNotFoundException, - QueryNotFoundException, SearchServiceException, SearchServiceConnectionException { + QueryNotFoundException, SearchServiceException, SearchServiceConnectionException, ExternalServiceException { final ResponseEntity<DataCiteBody<DataCiteDoi>> mock = ResponseEntity.status(HttpStatus.CREATED) .body(IDENTIFIER_1_DATA_CITE); @@ -122,15 +174,16 @@ public class DataCiteIdentifierServicePersistenceTest extends AbstractUnitTest { .thenReturn(DATABASE_1_DTO); /* test */ - assertThrows(ServiceConnectionException.class, () -> { + assertThrows(DataServiceConnectionException.class, () -> { dataCiteIdentifierService.save(DATABASE_1, USER_1, IDENTIFIER_1_SAVE_DTO); }); } @Test - public void create_succeeds() throws SearchServiceException, MalformedException, ServiceException, - QueryNotFoundException, ServiceConnectionException, DatabaseNotFoundException, - SearchServiceConnectionException, IdentifierNotFoundException, ViewNotFoundException { + public void create_succeeds() throws SearchServiceException, MalformedException, DataServiceException, + QueryNotFoundException, DataServiceConnectionException, DatabaseNotFoundException, + SearchServiceConnectionException, IdentifierNotFoundException, ViewNotFoundException, + ExternalServiceException { final ResponseEntity<DataCiteBody<DataCiteDoi>> mock = ResponseEntity.status(HttpStatus.CREATED) .body(IDENTIFIER_1_DATA_CITE); @@ -144,9 +197,10 @@ public class DataCiteIdentifierServicePersistenceTest extends AbstractUnitTest { } @Test - public void create_hasDoi_succeeds() throws SearchServiceException, MalformedException, ServiceException, - QueryNotFoundException, ServiceConnectionException, DatabaseNotFoundException, - SearchServiceConnectionException, IdentifierNotFoundException, ViewNotFoundException { + public void create_hasDoi_succeeds() throws SearchServiceException, MalformedException, DataServiceException, + QueryNotFoundException, DataServiceConnectionException, DatabaseNotFoundException, + SearchServiceConnectionException, IdentifierNotFoundException, ViewNotFoundException, + ExternalServiceException { final ResponseEntity<DataCiteBody<DataCiteDoi>> mock = ResponseEntity.status(HttpStatus.CREATED) .body(IDENTIFIER_1_DATA_CITE); @@ -159,4 +213,128 @@ public class DataCiteIdentifierServicePersistenceTest extends AbstractUnitTest { assertEquals(IDENTIFIER_1_DOI_NOT_NULL, response.getDoi()); } + @Test + public void publish_succeeds() throws MalformedException, DataServiceConnectionException, SearchServiceException, + DatabaseNotFoundException, SearchServiceConnectionException, IdentifierNotFoundException, + ExternalServiceException { + final ResponseEntity<DataCiteBody<DataCiteDoi>> mock = ResponseEntity.status(HttpStatus.CREATED) + .body(IDENTIFIER_7_DATA_CITE); + + /* mock */ + when(restTemplate.exchange(anyString(), eq(HttpMethod.POST), any(HttpEntity.class), eq(dataCiteBodyParameterizedTypeReference))) + .thenReturn(mock); + + /* test */ + final Identifier response = dataCiteIdentifierService.publish(IDENTIFIER_7_ID); + assertEquals(IDENTIFIER_7_ID, response.getId()); + assertEquals(IdentifierStatusType.PUBLISHED, response.getStatus()); + } + + @Test + public void exportMetadata_succeeds() { + + /* test */ + final InputStreamResource response = dataCiteIdentifierService.exportMetadata(IDENTIFIER_1); + assertNotNull(response); + } + + @Test + public void exportBibliography_apa_succeeds() throws MalformedException { + + /* test */ + final String response = dataCiteIdentifierService.exportBibliography(IDENTIFIER_1, BibliographyTypeDto.APA); + assertTrue(response.contains(IDENTIFIER_1_TITLE_1.getTitle())); + assertTrue(response.contains("" + IDENTIFIER_1_PUBLICATION_YEAR)); + assertTrue(response.contains(IDENTIFIER_1_CREATOR_1.getLastname())); + } + + @Test + public void exportBibliography_apaMixedPersonAndOrg_succeeds() throws MalformedException { + final Creator org = Creator.builder() + .id(CREATOR_2_ID) + .creatorName("Institute of Science and Technology Austria") + .nameIdentifier("https://ror.org/03gnh5541") + .nameIdentifierScheme(NameIdentifierSchemeType.ROR) + .build(); + final Identifier identifier = IDENTIFIER_1.toBuilder() + .creators(List.of(IDENTIFIER_1_CREATOR_1, org)) + .build(); + + /* test */ + final String response = dataCiteIdentifierService.exportBibliography(identifier, BibliographyTypeDto.APA); + final String title = IDENTIFIER_1_CREATOR_1.getFirstname().charAt(0) + "., " + IDENTIFIER_1_CREATOR_1.getLastname() + " & Institute of Science and Technology Austria"; + assertTrue(response.contains(title), "expected title not found: " + title); + assertTrue(response.contains("" + IDENTIFIER_1_PUBLICATION_YEAR), "expected publication year not found: " + IDENTIFIER_1_PUBLICATION_YEAR); + } + + @Test + public void exportBibliography_bibtex_succeeds() throws MalformedException { + + /* test */ + final String response = dataCiteIdentifierService.exportBibliography(IDENTIFIER_1, BibliographyTypeDto.BIBTEX); + assertTrue(response.contains(IDENTIFIER_1_TITLE_1.getTitle())); + assertTrue(response.contains("" + IDENTIFIER_1_PUBLICATION_YEAR)); + assertTrue(response.contains(IDENTIFIER_1_CREATOR_1.getLastname())); + } + + @Test + public void exportBibliography_bibtexMixedPersonAndOrg_succeeds() throws MalformedException { + final Creator org = Creator.builder() + .id(CREATOR_2_ID) + .creatorName("Institute of Science and Technology Austria") + .nameIdentifier("https://ror.org/03gnh5541") + .nameIdentifierScheme(NameIdentifierSchemeType.ROR) + .build(); + final Identifier identifier = IDENTIFIER_1.toBuilder() + .creators(List.of(IDENTIFIER_1_CREATOR_1, org)) + .build(); + + /* test */ + final String response = dataCiteIdentifierService.exportBibliography(identifier, BibliographyTypeDto.BIBTEX); + final String title = IDENTIFIER_5_CREATOR_1.getLastname() + ", " + IDENTIFIER_1_CREATOR_1.getFirstname() + " and Institute of Science and Technology Austria"; + assertTrue(response.contains(title), "expected title not found: " + title); + assertTrue(response.contains("" + IDENTIFIER_1_PUBLICATION_YEAR), "expected publication year not found: " + IDENTIFIER_1_PUBLICATION_YEAR); + } + + @Test + public void exportBibliography_ieee_succeeds() throws MalformedException { + + /* test */ + final String response = dataCiteIdentifierService.exportBibliography(IDENTIFIER_1, BibliographyTypeDto.IEEE); + assertTrue(response.contains(IDENTIFIER_1_TITLE_1.getTitle())); + assertTrue(response.contains("" + IDENTIFIER_1_PUBLICATION_YEAR)); + assertTrue(response.contains(IDENTIFIER_1_CREATOR_1.getLastname())); + } + + @Test + public void exportBibliography_ieeeMixedPersonAndOrg_succeeds() throws MalformedException { + final Creator org = Creator.builder() + .id(CREATOR_2_ID) + .creatorName("Institute of Science and Technology Austria") + .nameIdentifier("https://ror.org/03gnh5541") + .nameIdentifierScheme(NameIdentifierSchemeType.ROR) + .build(); + final Identifier identifier = IDENTIFIER_1.toBuilder() + .creators(List.of(IDENTIFIER_1_CREATOR_1, org)) + .build(); + + /* test */ + final String response = dataCiteIdentifierService.exportBibliography(identifier, BibliographyTypeDto.IEEE); + final String title = IDENTIFIER_1_CREATOR_1.getFirstname().charAt(0) + ". " + IDENTIFIER_1_CREATOR_1.getLastname() + ", Institute of Science and Technology Austria"; + assertTrue(response.contains(title), "expected title not found: " + title); + assertTrue(response.contains("" + IDENTIFIER_1_PUBLICATION_YEAR), "expected publication year not found: " + IDENTIFIER_1_PUBLICATION_YEAR); + } + + @Test + public void delete_succeeds() throws DataServiceException, DataServiceConnectionException, DatabaseNotFoundException, + IdentifierNotFoundException, SearchServiceException, SearchServiceConnectionException { + + /* mock */ + when(searchServiceGateway.update(any(Database.class))) + .thenReturn(DATABASE_1_DTO); + + /* test */ + dataCiteIdentifierService.delete(IDENTIFIER_1); + } + } diff --git a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/DatabaseServicePersistenceTest.java b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/DatabaseServicePersistenceTest.java index 8ccb486638..283450cc25 100644 --- a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/DatabaseServicePersistenceTest.java +++ b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/DatabaseServicePersistenceTest.java @@ -47,9 +47,9 @@ public class DatabaseServicePersistenceTest extends AbstractUnitTest { genesis(); /* metadata database */ licenseRepository.save(LICENSE_1); - containerRepository.save(CONTAINER_1); - userRepository.saveAll(List.of(USER_1, USER_2, USER_3)); - databaseRepository.save(DATABASE_1); + containerRepository.saveAll(List.of(CONTAINER_1, CONTAINER_2, CONTAINER_3, CONTAINER_4)); + userRepository.saveAll(List.of(USER_1, USER_2, USER_3, USER_4, USER_5)); + databaseRepository.saveAll(List.of(DATABASE_1, DATABASE_2, DATABASE_3, DATABASE_4)); } @Test @@ -94,4 +94,46 @@ public class DatabaseServicePersistenceTest extends AbstractUnitTest { assertNotNull(response.getCreator().getAccesses()); } + @Test + @Transactional + public void findByInternalName_succeeds() throws DatabaseNotFoundException { + + /* test */ + final Database response = databaseService.findByInternalName(DATABASE_1_INTERNALNAME); + assertEquals(DATABASE_1_ID, response.getId()); + assertEquals(CONTAINER_1_ID, response.getCid()); + /* container */ + assertNotNull(response.getContainer()); + assertEquals(CONTAINER_1_ID, response.getContainer().getId()); + assertEquals(CONTAINER_1_NAME, response.getContainer().getName()); + assertEquals(CONTAINER_1_INTERNALNAME, response.getContainer().getInternalName()); + assertEquals(CONTAINER_1_HOST, response.getContainer().getHost()); + assertEquals(CONTAINER_1_PORT, response.getContainer().getPort()); + assertEquals(CONTAINER_1_UI_HOST, response.getContainer().getUiHost()); + assertEquals(CONTAINER_1_UI_PORT, response.getContainer().getUiPort()); + assertEquals(CONTAINER_1_UI_ADDITIONAL_FLAGS, response.getContainer().getUiAdditionalFlags()); + assertEquals(CONTAINER_1_SIDECAR_HOST, response.getContainer().getSidecarHost()); + assertEquals(CONTAINER_1_SIDECAR_PORT, response.getContainer().getSidecarPort()); + assertEquals(CONTAINER_1_PRIVILEGED_USERNAME, response.getContainer().getPrivilegedUsername()); + assertEquals(CONTAINER_1_PRIVILEGED_PASSWORD, response.getContainer().getPrivilegedPassword()); + assertNotNull(response.getContainer().getImage()); + assertEquals(IMAGE_1_NAME, response.getContainer().getImage().getName()); + assertEquals(IMAGE_1_VERSION, response.getContainer().getImage().getVersion()); + assertEquals(IMAGE_1_DIALECT, response.getContainer().getImage().getDialect()); + assertEquals(IMAGE_1_JDBC, response.getContainer().getImage().getJdbcMethod()); + assertEquals(IMAGE_1_DRIVER, response.getContainer().getImage().getDriverClass()); + assertEquals(IMAGE_1_REGISTRY, response.getContainer().getImage().getRegistry()); + assertEquals(IMAGE_1_PORT, response.getContainer().getImage().getDefaultPort()); + assertNotNull(response.getContainer().getImage().getDateFormats()); + assertEquals(4, response.getContainer().getImage().getDateFormats().size()); + /* creator */ + assertNotNull(response.getCreator()); + assertEquals(USER_1_ID, response.getCreator().getId()); + assertEquals(USER_1_USERNAME, response.getCreator().getUsername()); + assertEquals(USER_1_EMAIL, response.getCreator().getEmail()); + assertEquals(USER_1_THEME, response.getCreator().getTheme()); + assertEquals(USER_1_LANGUAGE, response.getCreator().getLanguage()); + assertNotNull(response.getCreator().getAccesses()); + } + } diff --git a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/DatabaseServiceUnitTest.java b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/DatabaseServiceUnitTest.java index ede1738381..ea58ae16e4 100644 --- a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/DatabaseServiceUnitTest.java +++ b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/DatabaseServiceUnitTest.java @@ -161,33 +161,35 @@ public class DatabaseServiceUnitTest extends AbstractUnitTest { } @Test - public void create_dataServiceError_fails() throws ServiceException, ServiceConnectionException { + public void create_dataServiceError_fails() throws DataServiceException, DataServiceConnectionException, + DatabaseNotFoundException { /* mock */ when(containerRepository.findById(DATABASE_1.getCid())) .thenReturn(Optional.of(CONTAINER_1)); - doThrow(ServiceException.class) + doThrow(DataServiceException.class) .when(dataServiceGateway) .createDatabase(any(CreateDatabaseDto.class)); /* test */ - assertThrows(ServiceException.class, () -> { + assertThrows(DataServiceException.class, () -> { generic_create(DATABASE_1_CREATE, DATABASE_1); }); } @Test - public void create_dataServiceConnection_fails() throws ServiceException, ServiceConnectionException { + public void create_dataServiceConnection_fails() throws DataServiceException, DataServiceConnectionException, + DatabaseNotFoundException { /* mock */ when(containerRepository.findById(DATABASE_1.getCid())) .thenReturn(Optional.of(CONTAINER_1)); - doThrow(ServiceConnectionException.class) + doThrow(DataServiceConnectionException.class) .when(dataServiceGateway) .createDatabase(any(CreateDatabaseDto.class)); /* test */ - assertThrows(ServiceConnectionException.class, () -> { + assertThrows(DataServiceConnectionException.class, () -> { generic_create(DATABASE_1_CREATE, DATABASE_1); }); } @@ -302,8 +304,8 @@ public class DatabaseServiceUnitTest extends AbstractUnitTest { /* ## GENERIC TEST CASES ## */ /* ################################################################################################### */ - protected Database generic_create(DatabaseCreateDto createDto, Database database) throws ServiceException, - ServiceConnectionException, UserNotFoundException, DatabaseNotFoundException, + protected Database generic_create(DatabaseCreateDto createDto, Database database) throws DataServiceException, + DataServiceConnectionException, UserNotFoundException, DatabaseNotFoundException, ContainerNotFoundException, SearchServiceException, SearchServiceConnectionException { /* mock */ diff --git a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/IdentifierServicePersistenceTest.java b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/IdentifierServicePersistenceTest.java index e664abd516..6b3ff624b0 100644 --- a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/IdentifierServicePersistenceTest.java +++ b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/IdentifierServicePersistenceTest.java @@ -39,7 +39,6 @@ import static org.mockito.Mockito.when; @Log4j2 @SpringBootTest -@Disabled("keep failing on CI but works locally") @ExtendWith(SpringExtension.class) @DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD) public class IdentifierServicePersistenceTest extends AbstractUnitTest { @@ -154,9 +153,9 @@ public class IdentifierServicePersistenceTest extends AbstractUnitTest { } @Test - public void save_database_succeeds() throws ServiceException, ServiceConnectionException, MalformedException, - DatabaseNotFoundException, IdentifierNotFoundException, ViewNotFoundException, QueryNotFoundException, - SearchServiceException, SearchServiceConnectionException { + public void save_database_succeeds() throws DataServiceException, DataServiceConnectionException, + MalformedException, DatabaseNotFoundException, IdentifierNotFoundException, ViewNotFoundException, + QueryNotFoundException, SearchServiceException, SearchServiceConnectionException, ExternalServiceException { /* mock */ when(restTemplate.exchange(anyString(), any(HttpMethod.class), any(HttpEntity.class), eq(QueryDto.class))) @@ -168,9 +167,9 @@ public class IdentifierServicePersistenceTest extends AbstractUnitTest { } @Test - public void save_existsSubset_succeeds() throws ServiceException, ServiceConnectionException, MalformedException, - DatabaseNotFoundException, IdentifierNotFoundException, ViewNotFoundException, QueryNotFoundException, - SearchServiceException, SearchServiceConnectionException { + public void save_existsSubset_succeeds() throws DataServiceException, DataServiceConnectionException, + MalformedException, DatabaseNotFoundException, IdentifierNotFoundException, ViewNotFoundException, + QueryNotFoundException, SearchServiceException, SearchServiceConnectionException, ExternalServiceException { /* mock */ when(dataServiceGateway.findQuery(IDENTIFIER_5_DATABASE_ID, IDENTIFIER_5_QUERY_ID)) @@ -183,9 +182,10 @@ public class IdentifierServicePersistenceTest extends AbstractUnitTest { } @Test - public void save_existsDatabase_succeeds() throws MalformedException, ServiceException, - ServiceConnectionException, DatabaseNotFoundException, IdentifierNotFoundException, ViewNotFoundException, - QueryNotFoundException, SearchServiceException, SearchServiceConnectionException { + public void save_existsDatabase_succeeds() throws MalformedException, DataServiceException, + DataServiceConnectionException, DatabaseNotFoundException, IdentifierNotFoundException, + ViewNotFoundException, QueryNotFoundException, SearchServiceException, SearchServiceConnectionException, + ExternalServiceException { /* test */ identifierService.save(DATABASE_1, USER_1, IDENTIFIER_1_SAVE_DTO); @@ -279,8 +279,9 @@ public class IdentifierServicePersistenceTest extends AbstractUnitTest { } @Test - public void delete_succeeds() throws ServiceException, ServiceConnectionException, DatabaseNotFoundException, - IdentifierNotFoundException, SearchServiceException, SearchServiceConnectionException { + public void delete_succeeds() throws DataServiceException, DataServiceConnectionException, + DatabaseNotFoundException, IdentifierNotFoundException, SearchServiceException, + SearchServiceConnectionException { /* mock */ when(searchServiceGateway.update(any(Database.class))) @@ -300,9 +301,9 @@ public class IdentifierServicePersistenceTest extends AbstractUnitTest { @Test @Transactional - public void save_subsetRelatedIdentifiers_succeeds() throws ServiceException, ServiceConnectionException, + public void save_subsetRelatedIdentifiers_succeeds() throws DataServiceException, DataServiceConnectionException, MalformedException, DatabaseNotFoundException, IdentifierNotFoundException, ViewNotFoundException, - QueryNotFoundException, SearchServiceException, SearchServiceConnectionException { + QueryNotFoundException, SearchServiceException, SearchServiceConnectionException, ExternalServiceException { /* mock */ when(dataServiceGateway.findQuery(IDENTIFIER_5_DATABASE_ID, IDENTIFIER_5_QUERY_ID)) @@ -339,9 +340,9 @@ public class IdentifierServicePersistenceTest extends AbstractUnitTest { } @Test - public void save_succeeds() throws MalformedException, ServiceException, ServiceConnectionException, + public void save_succeeds() throws MalformedException, DataServiceException, DataServiceConnectionException, DatabaseNotFoundException, IdentifierNotFoundException, ViewNotFoundException, QueryNotFoundException, - SearchServiceException, SearchServiceConnectionException { + SearchServiceException, SearchServiceConnectionException, ExternalServiceException { /* test */ final Identifier response = identifierService.save(DATABASE_1, USER_1, IDENTIFIER_1_SAVE_DTO); @@ -384,9 +385,10 @@ public class IdentifierServicePersistenceTest extends AbstractUnitTest { } @Test - public void save_repeatedRemoveChildren_succeeds() throws MalformedException, ServiceException, ServiceConnectionException, - DatabaseNotFoundException, IdentifierNotFoundException, ViewNotFoundException, QueryNotFoundException, - SearchServiceException, SearchServiceConnectionException { + public void save_repeatedRemoveChildren_succeeds() throws MalformedException, DataServiceException, + DataServiceConnectionException, DatabaseNotFoundException, IdentifierNotFoundException, + ViewNotFoundException, QueryNotFoundException, SearchServiceException, SearchServiceConnectionException, + ExternalServiceException { /* test */ final Identifier response = identifierService.save(DATABASE_1, USER_1, IDENTIFIER_1_SAVE_MODIFY_DTO); @@ -413,9 +415,9 @@ public class IdentifierServicePersistenceTest extends AbstractUnitTest { } @Test - public void save_noRelatedTitleDescription_succeeds() throws ServiceException, ServiceConnectionException, + public void save_noRelatedTitleDescription_succeeds() throws DataServiceException, DataServiceConnectionException, MalformedException, DatabaseNotFoundException, IdentifierNotFoundException, ViewNotFoundException, - QueryNotFoundException, SearchServiceException, SearchServiceConnectionException { + QueryNotFoundException, SearchServiceException, SearchServiceConnectionException, ExternalServiceException { /* test */ final Identifier response = identifierService.save(DATABASE_4, USER_4, IDENTIFIER_7_SAVE_DTO); @@ -430,9 +432,10 @@ public class IdentifierServicePersistenceTest extends AbstractUnitTest { } @Test - public void save_subsetHasDatabaseIdentifier_succeeds() throws ServiceException, ServiceConnectionException, + public void save_subsetHasDatabaseIdentifier_succeeds() throws DataServiceException, DataServiceConnectionException, DatabaseNotFoundException, QueryNotFoundException, SearchServiceException, ViewNotFoundException, - SearchServiceConnectionException, MalformedException, IdentifierNotFoundException { + SearchServiceConnectionException, MalformedException, IdentifierNotFoundException, + ExternalServiceException { /* mock */ when(dataServiceGateway.findQuery(IDENTIFIER_2_DATABASE_ID, IDENTIFIER_2_QUERY_ID)) @@ -449,9 +452,10 @@ public class IdentifierServicePersistenceTest extends AbstractUnitTest { } @Test - public void save_viewIdentifier_succeeds() throws SearchServiceException, MalformedException, ServiceException, - QueryNotFoundException, ServiceConnectionException, DatabaseNotFoundException, - SearchServiceConnectionException, IdentifierNotFoundException, ViewNotFoundException { + public void save_viewIdentifier_succeeds() throws SearchServiceException, MalformedException, DataServiceException, + QueryNotFoundException, DataServiceConnectionException, DatabaseNotFoundException, + SearchServiceConnectionException, IdentifierNotFoundException, ViewNotFoundException, + ExternalServiceException { /* test */ final Identifier response = identifierService.save(DATABASE_1, USER_1, IDENTIFIER_3_SAVE_DTO); @@ -465,9 +469,9 @@ public class IdentifierServicePersistenceTest extends AbstractUnitTest { } @Test - public void create_succeeds() throws MalformedException, ServiceConnectionException, SearchServiceException, - ServiceException, QueryNotFoundException, DatabaseNotFoundException, SearchServiceConnectionException, - IdentifierNotFoundException, ViewNotFoundException { + public void create_succeeds() throws MalformedException, DataServiceConnectionException, SearchServiceException, + DataServiceException, QueryNotFoundException, DatabaseNotFoundException, SearchServiceConnectionException, + IdentifierNotFoundException, ViewNotFoundException, ExternalServiceException { /* test */ final Identifier response = identifierService.create(DATABASE_1, USER_1, IDENTIFIER_1_CREATE_DTO); @@ -475,9 +479,10 @@ public class IdentifierServicePersistenceTest extends AbstractUnitTest { } @Test - public void create_hasDoi_succeeds() throws SearchServiceException, MalformedException, ServiceException, - QueryNotFoundException, ServiceConnectionException, DatabaseNotFoundException, - SearchServiceConnectionException, IdentifierNotFoundException, ViewNotFoundException { + public void create_hasDoi_succeeds() throws SearchServiceException, MalformedException, DataServiceException, + QueryNotFoundException, DataServiceConnectionException, DatabaseNotFoundException, + SearchServiceConnectionException, IdentifierNotFoundException, ViewNotFoundException, + ExternalServiceException { /* test */ final Identifier response = identifierService.create(DATABASE_1, USER_1, IDENTIFIER_1_CREATE_WITH_DOI_DTO); @@ -486,8 +491,9 @@ public class IdentifierServicePersistenceTest extends AbstractUnitTest { } @Test - public void publish_succeeds() throws MalformedException, ServiceConnectionException, SearchServiceException, - DatabaseNotFoundException, SearchServiceConnectionException, IdentifierNotFoundException { + public void publish_succeeds() throws MalformedException, DataServiceConnectionException, SearchServiceException, + DatabaseNotFoundException, SearchServiceConnectionException, IdentifierNotFoundException, + ExternalServiceException { /* test */ final Identifier response = identifierService.publish(IDENTIFIER_7_ID); diff --git a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/StorageServiceIntegrationTest.java b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/StorageServiceIntegrationTest.java new file mode 100644 index 0000000000..aa3a460b99 --- /dev/null +++ b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/StorageServiceIntegrationTest.java @@ -0,0 +1,139 @@ +package at.tuwien.service; + +import at.tuwien.config.S3Config; +import at.tuwien.exception.*; +import at.tuwien.test.AbstractUnitTest; +import lombok.extern.log4j.Log4j2; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.testcontainers.containers.MinIOContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import software.amazon.awssdk.core.sync.RequestBody; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.CreateBucketRequest; +import software.amazon.awssdk.services.s3.model.PutObjectRequest; + +import java.io.File; +import java.io.InputStream; +import java.sql.SQLException; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +@Log4j2 +@Testcontainers +@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD) +@SpringBootTest +@ExtendWith(SpringExtension.class) +public class StorageServiceIntegrationTest extends AbstractUnitTest { + + @Autowired + private StorageService storageService; + + @Autowired + private S3Client s3Client; + + @Autowired + private S3Config s3Config; + + @Container + private static final MinIOContainer minIOContainer = new MinIOContainer("minio/minio:RELEASE.2024-06-06T09-36-42Z"); + + @DynamicPropertySource + static void dynamicProperties(DynamicPropertyRegistry registry) { + registry.add("dbrepo.endpoints.storageService", minIOContainer::getS3URL); + } + + @BeforeEach + public void beforeEach() throws SQLException { + genesis(); + /* s3 */ + if (s3Client.listBuckets().buckets().stream().noneMatch(b -> b.name().equals(s3Config.getS3ImportBucket()))) { + s3Client.createBucket(CreateBucketRequest.builder() + .bucket(s3Config.getS3ImportBucket()) + .build()); + } + if (s3Client.listBuckets().buckets().stream().noneMatch(b -> b.name().equals(s3Config.getS3ExportBucket()))) { + s3Client.createBucket(CreateBucketRequest.builder() + .bucket(s3Config.getS3ExportBucket()) + .build()); + } + } + + @Test + public void getObject_succeeds() throws StorageUnavailableException, StorageNotFoundException { + final String key = "s3key"; + + /* mock */ + log.trace("mock object with key {} to bucket {}", key, s3Config.getS3ImportBucket()); + s3Client.putObject(PutObjectRequest.builder() + .key(key) + .bucket(s3Config.getS3ImportBucket()) + .build(), RequestBody.fromFile(new File("src/test/resources/csv/keyboard.csv"))); + + /* test */ + final InputStream response = storageService.getObject(s3Config.getS3ImportBucket(), key); + assertNotNull(response); + } + + @Test + public void getObject_notFound_fails() { + + /* test */ + assertThrows(StorageNotFoundException.class, () -> { + storageService.getObject(s3Config.getS3ImportBucket(), "i_do_not_exist"); + }); + } + + @Test + public void getObject_bucketNotFound_fails() { + + /* test */ + assertThrows(StorageUnavailableException.class, () -> { + storageService.getObject("i_do_not_exist", "i_do_neither"); + }); + } + + @Test + public void getBytes_succeeds() throws StorageUnavailableException, StorageNotFoundException { + final String key = "s3key"; + + /* mock */ + log.trace("mock object with key {} to bucket {}", key, s3Config.getS3ImportBucket()); + s3Client.putObject(PutObjectRequest.builder() + .key(key) + .bucket(s3Config.getS3ImportBucket()) + .build(), RequestBody.fromFile(new File("src/test/resources/csv/keyboard.csv"))); + + /* test */ + final byte[] response = storageService.getBytes(key); + assertNotNull(response); + } + + @Test + public void getBytes_notExists_fails() { + + /* test */ + assertThrows(StorageNotFoundException.class, () -> { + storageService.getBytes("i_do_not_exist"); + }); + } + + @Test + public void getBytes_bucketNotExists_fails() { + + /* test */ + assertThrows(StorageUnavailableException.class, () -> { + storageService.getBytes("i_do_not_exist", "i_do_neither"); + }); + } + +} diff --git a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/TableServicePersistenceTest.java b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/TableServicePersistenceTest.java index f30bf485f3..e66d35d8ea 100644 --- a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/TableServicePersistenceTest.java +++ b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/TableServicePersistenceTest.java @@ -78,7 +78,7 @@ public class TableServicePersistenceTest extends AbstractUnitTest { @Test @Transactional - public void create_succeeds() throws MalformedException, ServiceException, ServiceConnectionException, + public void create_succeeds() throws MalformedException, DataServiceException, DataServiceConnectionException, UserNotFoundException, TableNotFoundException, DatabaseNotFoundException, TableExistsException, SearchServiceException, SearchServiceConnectionException, OntologyNotFoundException, SemanticEntityNotFoundException { final TableCreateDto request = TableCreateDto.builder() .name("New Table") diff --git a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/TableServiceUnitTest.java b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/TableServiceUnitTest.java index d32189f944..33b4594424 100644 --- a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/TableServiceUnitTest.java +++ b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/TableServiceUnitTest.java @@ -8,11 +8,9 @@ import at.tuwien.api.database.table.constraints.foreign.ForeignKeyCreateDto; import at.tuwien.entities.database.table.columns.TableColumnType; import at.tuwien.entities.database.table.constraints.Constraints; import at.tuwien.test.AbstractUnitTest; -import at.tuwien.api.database.table.columns.concepts.ColumnSemanticsUpdateDto; import at.tuwien.entities.database.Database; import at.tuwien.entities.database.table.Table; import at.tuwien.entities.database.table.columns.TableColumn; -import at.tuwien.entities.database.table.columns.TableColumnConcept; import at.tuwien.exception.*; import at.tuwien.gateway.DataServiceGateway; import at.tuwien.gateway.SearchServiceGateway; @@ -117,9 +115,9 @@ public class TableServiceUnitTest extends AbstractUnitTest { } @Test - public void createTable_succeeds() throws ServiceException, ServiceConnectionException, UserNotFoundException, - TableNotFoundException, DatabaseNotFoundException, TableExistsException, SearchServiceException, - SearchServiceConnectionException, MalformedException, OntologyNotFoundException, + public void createTable_succeeds() throws DataServiceException, DataServiceConnectionException, + UserNotFoundException, TableNotFoundException, DatabaseNotFoundException, TableExistsException, + SearchServiceException, SearchServiceConnectionException, MalformedException, OntologyNotFoundException, SemanticEntityNotFoundException { /* mock */ @@ -139,10 +137,10 @@ public class TableServiceUnitTest extends AbstractUnitTest { } @Test - public void createTable_nonStandardColumnNames_succeeds() throws ServiceException, ServiceConnectionException, - UserNotFoundException, TableNotFoundException, DatabaseNotFoundException, TableExistsException, - SearchServiceException, SearchServiceConnectionException, MalformedException, OntologyNotFoundException, - SemanticEntityNotFoundException { + public void createTable_nonStandardColumnNames_succeeds() throws DataServiceException, + DataServiceConnectionException, UserNotFoundException, TableNotFoundException, DatabaseNotFoundException, + TableExistsException, SearchServiceException, SearchServiceConnectionException, MalformedException, + OntologyNotFoundException, SemanticEntityNotFoundException { final TableCreateDto request = TableCreateDto.builder() .name("New Table") .description("A wonderful table") @@ -202,7 +200,7 @@ public class TableServiceUnitTest extends AbstractUnitTest { } @Test - public void createTable_dateFormatNotFound_fails() throws ServiceException, ServiceConnectionException, + public void createTable_dateFormatNotFound_fails() throws DataServiceException, DataServiceConnectionException, UserNotFoundException, DatabaseNotFoundException, TableExistsException, SearchServiceException, SearchServiceConnectionException { final TableCreateDto request = TableCreateDto.builder() @@ -240,7 +238,7 @@ public class TableServiceUnitTest extends AbstractUnitTest { } @Test - public void create_succeeds() throws MalformedException, ServiceException, ServiceConnectionException, + public void create_succeeds() throws MalformedException, DataServiceException, DataServiceConnectionException, UserNotFoundException, TableNotFoundException, DatabaseNotFoundException, TableExistsException, SearchServiceException, SearchServiceConnectionException, OntologyNotFoundException, SemanticEntityNotFoundException { @@ -262,31 +260,7 @@ public class TableServiceUnitTest extends AbstractUnitTest { } @Test - @Transactional - public void update_succeeds() throws ServiceException, ServiceConnectionException, DatabaseNotFoundException, - SearchServiceException, SearchServiceConnectionException, MalformedException, OntologyNotFoundException, - SemanticEntityNotFoundException { - final ColumnSemanticsUpdateDto request = ColumnSemanticsUpdateDto.builder() - .conceptUri(CONCEPT_1_URI) - .build(); - - /* mock */ - when(ontologyService.find(anyString())) - .thenReturn(ONTOLOGY_2); - when(ontologyService.findAll()) - .thenReturn(List.of(ONTOLOGY_1, ONTOLOGY_2, ONTOLOGY_3, ONTOLOGY_4, ONTOLOGY_5)); - when(searchServiceGateway.update(any(Database.class))) - .thenReturn(DATABASE_1_DTO); - - /* test */ - final TableColumn response = tableService.update(TABLE_1_COLUMNS.get(0), request); - assertNotNull(response.getConcept()); - final TableColumnConcept concept = response.getConcept(); - assertEquals(CONCEPT_1_URI, concept.getUri()); - } - - @Test - public void create_dataServiceError_fails() throws ServiceException, ServiceConnectionException, + public void create_dataServiceError_fails() throws DataServiceException, DataServiceConnectionException, UserNotFoundException, DatabaseNotFoundException, TableExistsException, SearchServiceException, SearchServiceConnectionException { @@ -295,14 +269,14 @@ public class TableServiceUnitTest extends AbstractUnitTest { .thenReturn(USER_1); when(databaseRepository.save(any(Database.class))) .thenReturn(DATABASE_1); - doThrow(ServiceException.class) + doThrow(DataServiceException.class) .when(dataServiceGateway) .createTable(DATABASE_1_ID, TABLE_5_CREATE_DTO); when(searchServiceGateway.update(any(Database.class))) .thenReturn(DATABASE_1_DTO); /* test */ - assertThrows(ServiceException.class, () -> { + assertThrows(DataServiceException.class, () -> { tableService.createTable(DATABASE_1, TABLE_5_CREATE_DTO, USER_1_PRINCIPAL); }); } @@ -388,8 +362,9 @@ public class TableServiceUnitTest extends AbstractUnitTest { @Test @Transactional - public void delete_succeeds() throws ServiceException, ServiceConnectionException, DatabaseNotFoundException, - TableNotFoundException, SearchServiceException, SearchServiceConnectionException { + public void delete_succeeds() throws DataServiceException, DataServiceConnectionException, + DatabaseNotFoundException, TableNotFoundException, SearchServiceException, + SearchServiceConnectionException { /* mock */ doNothing() @@ -404,7 +379,7 @@ public class TableServiceUnitTest extends AbstractUnitTest { @Test @Transactional - public void delete_hasIdentifier_succeeds() throws ServiceException, ServiceConnectionException, + public void delete_hasIdentifier_succeeds() throws DataServiceException, DataServiceConnectionException, DatabaseNotFoundException, TableNotFoundException, SearchServiceException, SearchServiceConnectionException { /* mock */ diff --git a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/UserServiceUnitTest.java b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/UserServiceUnitTest.java index f85d466370..5a4690892f 100644 --- a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/UserServiceUnitTest.java +++ b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/UserServiceUnitTest.java @@ -72,7 +72,7 @@ public class UserServiceUnitTest extends AbstractUnitTest { @Test public void create_succeeds() throws UserNotFoundException, UserExistsException, EmailExistsException, - ServiceException, ServiceConnectionException, AuthServiceException, AuthServiceConnectionException, + DataServiceException, DataServiceConnectionException, AuthServiceException, AuthServiceConnectionException, CredentialsInvalidException { /* mock */ diff --git a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/ViewServicePersistenceTest.java b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/ViewServicePersistenceTest.java index fc83d3a650..e23320017c 100644 --- a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/ViewServicePersistenceTest.java +++ b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/ViewServicePersistenceTest.java @@ -79,7 +79,7 @@ public class ViewServicePersistenceTest extends AbstractUnitTest { } @Test - public void delete_succeeds() throws SearchServiceException, ServiceException, ServiceConnectionException, + public void delete_succeeds() throws SearchServiceException, DataServiceException, DataServiceConnectionException, DatabaseNotFoundException, SearchServiceConnectionException, ViewNotFoundException { /* mock */ diff --git a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/ViewServiceUnitTest.java b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/ViewServiceUnitTest.java index 517661ce1a..cd9fe03c65 100644 --- a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/ViewServiceUnitTest.java +++ b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/ViewServiceUnitTest.java @@ -48,7 +48,7 @@ public class ViewServiceUnitTest extends AbstractUnitTest { } @Test - public void create_succeeds() throws MalformedException, ServiceException, ServiceConnectionException, + public void create_succeeds() throws MalformedException, DataServiceException, DataServiceConnectionException, DatabaseNotFoundException, SearchServiceException, SearchServiceConnectionException { final ViewCreateDto request = ViewCreateDto.builder() .name(VIEW_1_NAME) @@ -107,8 +107,8 @@ public class ViewServiceUnitTest extends AbstractUnitTest { } @Test - public void delete_succeeds() throws ServiceException, ServiceConnectionException, DatabaseNotFoundException, - ViewNotFoundException, SearchServiceException, SearchServiceConnectionException { + public void delete_succeeds() throws DataServiceException, DataServiceConnectionException, + DatabaseNotFoundException, ViewNotFoundException, SearchServiceException, SearchServiceConnectionException { /* mock */ doNothing() @@ -124,37 +124,37 @@ public class ViewServiceUnitTest extends AbstractUnitTest { } @Test - public void delete_dataServiceException_fails() throws ServiceException, ServiceConnectionException, + public void delete_dataServiceException_fails() throws DataServiceException, DataServiceConnectionException, ViewNotFoundException { /* mock */ - doThrow(ServiceException.class) + doThrow(DataServiceException.class) .when(dataServiceGateway) .deleteView(DATABASE_1_ID, VIEW_1_ID); /* test */ - assertThrows(ServiceException.class, () -> { + assertThrows(DataServiceException.class, () -> { viewService.delete(VIEW_1); }); } @Test - public void delete_dataServiceConnection_fails() throws ServiceException, ServiceConnectionException, + public void delete_dataServiceConnection_fails() throws DataServiceException, DataServiceConnectionException, ViewNotFoundException { /* mock */ - doThrow(ServiceConnectionException.class) + doThrow(DataServiceConnectionException.class) .when(dataServiceGateway) .deleteView(DATABASE_1_ID, VIEW_1_ID); /* test */ - assertThrows(ServiceConnectionException.class, () -> { + assertThrows(DataServiceConnectionException.class, () -> { viewService.delete(VIEW_1); }); } @Test - public void delete_searchServiceError_fails() throws ServiceException, ServiceConnectionException, + public void delete_searchServiceError_fails() throws DataServiceException, DataServiceConnectionException, ViewNotFoundException, DatabaseNotFoundException, SearchServiceException, SearchServiceConnectionException { /* mock */ @@ -174,7 +174,7 @@ public class ViewServiceUnitTest extends AbstractUnitTest { } @Test - public void delete_searchServiceConnection_fails() throws ServiceException, ServiceConnectionException, + public void delete_searchServiceConnection_fails() throws DataServiceException, DataServiceConnectionException, DatabaseNotFoundException, ViewNotFoundException, SearchServiceException, SearchServiceConnectionException { /* mock */ @@ -194,7 +194,7 @@ public class ViewServiceUnitTest extends AbstractUnitTest { } @Test - public void delete_searchServiceNotFound_fails() throws ServiceException, ServiceConnectionException, + public void delete_searchServiceNotFound_fails() throws DataServiceException, DataServiceConnectionException, ViewNotFoundException, DatabaseNotFoundException, SearchServiceException, SearchServiceConnectionException { /* mock */ diff --git a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/utils/AmqpUtils.java b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/utils/AmqpUtils.java index a0588a6e1b..df86598714 100644 --- a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/utils/AmqpUtils.java +++ b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/utils/AmqpUtils.java @@ -1,18 +1,22 @@ package at.tuwien.utils; import at.tuwien.api.amqp.*; +import at.tuwien.test.BaseTest; import lombok.extern.log4j.Log4j2; import org.apache.commons.codec.binary.Base64; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpEntity; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.http.client.support.BasicAuthenticationInterceptor; import org.springframework.stereotype.Service; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.DefaultUriBuilderFactory; import java.nio.charset.Charset; import java.util.Arrays; @@ -20,13 +24,17 @@ import java.util.List; import java.util.stream.Collectors; @Log4j2 -@Service -public class AmqpUtils { +public class AmqpUtils extends BaseTest { + + private static final String BASIC_AUTH = new String(Base64.encodeBase64((USER_1_USERNAME + ":" + USER_1_PASSWORD).getBytes(Charset.defaultCharset()))); private final RestTemplate restTemplate; - @Autowired - public AmqpUtils(@Qualifier("brokerRestTemplate") RestTemplate restTemplate) { + public AmqpUtils(String endpoint) { + final RestTemplate restTemplate = new RestTemplate(); + restTemplate.setUriTemplateHandler(new DefaultUriBuilderFactory(endpoint)); + restTemplate.getInterceptors() + .add(new BasicAuthenticationInterceptor(USER_1_USERNAME, USER_1_PASSWORD)); this.restTemplate = restTemplate; } @@ -97,10 +105,10 @@ public class AmqpUtils { public void setVirtualHostPermissions(String vhost, String username, GrantVirtualHostPermissionsDto data) { final String url = "/api/permissions/" + vhost + "/" + username; - log.debug("set virtual host permissions: {}", url); log.trace("body: {}", data); final MultiValueMap<String, String> headers = new LinkedMultiValueMap<>(); - headers.add("Authentication", "Basic " + new String(Base64.encodeBase64("guest:guest".getBytes(Charset.defaultCharset())))); + headers.add("Authentication", "Basic " + BASIC_AUTH); + log.trace("set virtual host permissions: {}", url); final ResponseEntity<Void> response = restTemplate.exchange(url, HttpMethod.PUT, new HttpEntity<>(data, headers), Void.class); if (!response.getStatusCode().equals(HttpStatus.CREATED) && !response.getStatusCode().equals(HttpStatus.NO_CONTENT)) { log.error("Failed to set virtual host permissions: {}", response.getStatusCode()); @@ -113,7 +121,7 @@ public class AmqpUtils { log.debug("set topic permissions: {}", url); log.trace("body: {}", data); final MultiValueMap<String, String> headers = new LinkedMultiValueMap<>(); - headers.add("Authentication", "Basic " + new String(Base64.encodeBase64("guest:guest".getBytes(Charset.defaultCharset())))); + headers.add("Authentication", "Basic " + BASIC_AUTH); final ResponseEntity<Void> response = restTemplate.exchange(url, HttpMethod.PUT, new HttpEntity<>(data, headers), Void.class); if (!response.getStatusCode().equals(HttpStatus.CREATED) && !response.getStatusCode().equals(HttpStatus.NO_CONTENT)) { log.error("Failed to set topic permissions: {}", response.getStatusCode()); diff --git a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/config/H2Utils.java b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/utils/H2Utils.java similarity index 97% rename from dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/config/H2Utils.java rename to dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/utils/H2Utils.java index 033d1ba280..7c80d5274a 100644 --- a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/config/H2Utils.java +++ b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/utils/H2Utils.java @@ -1,4 +1,4 @@ -package at.tuwien.config; +package at.tuwien.utils; import jakarta.persistence.EntityManager; import lombok.extern.log4j.Log4j2; diff --git a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/utils/KeycloakUtils.java b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/utils/KeycloakUtils.java new file mode 100644 index 0000000000..49694e6fd1 --- /dev/null +++ b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/utils/KeycloakUtils.java @@ -0,0 +1,40 @@ +package at.tuwien.utils; + +import at.tuwien.api.keycloak.RoleRepresentationDto; +import at.tuwien.exception.*; +import at.tuwien.gateway.KeycloakGateway; +import at.tuwien.gateway.impl.KeycloakGatewayImpl; +import lombok.extern.log4j.Log4j2; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.*; +import org.springframework.stereotype.Component; +import org.springframework.web.client.HttpClientErrorException; +import org.springframework.web.client.HttpServerErrorException; +import org.springframework.web.client.RestTemplate; + +import java.util.List; +import java.util.UUID; + +@Log4j2 +@Component +public class KeycloakUtils { + + final static UUID realmId = UUID.fromString("82c39861-d877-4667-a0f3-4daa2ee230e0"); + + private final KeycloakGateway keycloakGateway; + + @Autowired + public KeycloakUtils(KeycloakGateway keycloakGateway) { + this.keycloakGateway = keycloakGateway; + } + + public void deleteUser(String username) throws AuthServiceException, AuthServiceConnectionException, + CredentialsInvalidException { + try { + final UUID userId = keycloakGateway.findByUsername(username).getId(); + keycloakGateway.deleteUser(userId); + } catch (UserNotFoundException e) { + /* ignore */ + } + } +} diff --git a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/validator/EndpointValidatorUnitTest.java b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/validator/EndpointValidatorUnitTest.java index 1e5aa8227a..f6cf4e887d 100644 --- a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/validator/EndpointValidatorUnitTest.java +++ b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/validator/EndpointValidatorUnitTest.java @@ -30,8 +30,7 @@ import java.util.List; import java.util.stream.Stream; import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.when; @@ -173,7 +172,7 @@ public class EndpointValidatorUnitTest extends AbstractUnitTest { DatabaseNotFoundException, AccessNotFoundException { /* mock */ - when(databaseService.findById(DATABASE_3_ID)) + when(databaseService.findById(anyLong())) .thenReturn(DATABASE_3); /* test */ @@ -181,11 +180,11 @@ public class EndpointValidatorUnitTest extends AbstractUnitTest { } @Test - @Disabled("keep failing on CI but works locally") + @Disabled public void validateOnlyAccessOrPublic_privateAnonymous_fails() throws DatabaseNotFoundException { /* mock */ - when(databaseService.findById(DATABASE_1_ID)) + when(databaseService.findById(anyLong())) .thenReturn(DATABASE_1); /* test */ @@ -195,7 +194,7 @@ public class EndpointValidatorUnitTest extends AbstractUnitTest { } @Test - @Disabled("keep failing on CI but works locally") + @Disabled public void validateOnlyAccessOrPublic_privateNoAccess_fails() throws DatabaseNotFoundException, AccessNotFoundException { @@ -204,7 +203,7 @@ public class EndpointValidatorUnitTest extends AbstractUnitTest { .thenReturn(DATABASE_1); doThrow(AccessNotFoundException.class) .when(accessService) - .find(eq(DATABASE_1), any(User.class)); + .find(any(Database.class), any(User.class)); /* test */ assertThrows(AccessNotFoundException.class, () -> { @@ -402,11 +401,11 @@ public class EndpointValidatorUnitTest extends AbstractUnitTest { } @Test - @Disabled("keep failing on CI but works locally") + @Disabled public void validateOnlyPrivateHasRole_privatePrincipalMissing_fails() throws DatabaseNotFoundException { /* mock */ - when(databaseService.findById(DATABASE_1_ID)) + when(databaseService.findById(anyLong())) .thenReturn(DATABASE_1); /* test */ @@ -416,11 +415,11 @@ public class EndpointValidatorUnitTest extends AbstractUnitTest { } @Test - @Disabled("keep failing on CI but works locally") + @Disabled public void validateOnlyPrivateHasRole_privateRoleMissing_fails() throws DatabaseNotFoundException { /* mock */ - when(databaseService.findById(DATABASE_1_ID)) + when(databaseService.findById(anyLong())) .thenReturn(DATABASE_1); /* test */ diff --git a/dbrepo-metadata-service/rest-service/src/test/resources/application.properties b/dbrepo-metadata-service/rest-service/src/test/resources/application.properties index 4243bcf79e..0929175cf7 100644 --- a/dbrepo-metadata-service/rest-service/src/test/resources/application.properties +++ b/dbrepo-metadata-service/rest-service/src/test/resources/application.properties @@ -14,20 +14,20 @@ spring.sql.init.mode=always spring.sql.init.schema-locations=classpath*:init/schema.sql spring.jpa.hibernate.ddl-auto=create -# LDAP -spring.ldap.userDn: cn=admin,dc=dbrepo,dc=at -spring.ldap.password: adminpassword -spring.ldap.base: dc=dbrepo,dc=at - -# admin -dbrepo.system.role: admin - # logging logging.level.root=error logging.level.at.tuwien.=trace +logging.level.org.testcontainers.=info +logging.level.tc.=info +logging.level.com.github.dockerjava.=warn +logging.level.com.github.dockerjava.zerodep.shaded.org.apache.hc.client5.http.wire.=off # datacite dbrepo.datacite.url: https://api.test.datacite.org dbrepo.datacite.prefix: 10.12345 dbrepo.datacite.username: test-user dbrepo.datacite.password: test-password + +# s3 +dbrepo.s3.accessKeyId=minioadmin +dbrepo.s3.secretAccessKey=minioadmin diff --git a/dbrepo-metadata-service/rest-service/src/test/resources/init/dbrepo-realm.json b/dbrepo-metadata-service/rest-service/src/test/resources/init/dbrepo-realm.json index 639d73ea15..588053e15f 100644 --- a/dbrepo-metadata-service/rest-service/src/test/resources/init/dbrepo-realm.json +++ b/dbrepo-metadata-service/rest-service/src/test/resources/init/dbrepo-realm.json @@ -1,1525 +1,2822 @@ -{ - "id": "4ef53018-8322-40ed-a44a-a7c5292a07be", - "realm": "dbrepo", - "displayName": "", - "displayNameHtml": "", - "notBefore": 0, - "defaultSignatureAlgorithm": "RS256", - "revokeRefreshToken": false, - "refreshTokenMaxReuse": 0, - "accessTokenLifespan": 300, - "accessTokenLifespanForImplicitFlow": 900, - "ssoSessionIdleTimeout": 1800, - "ssoSessionMaxLifespan": 36000, - "ssoSessionIdleTimeoutRememberMe": 0, - "ssoSessionMaxLifespanRememberMe": 0, - "offlineSessionIdleTimeout": 2592000, - "offlineSessionMaxLifespanEnabled": false, - "offlineSessionMaxLifespan": 5184000, - "clientSessionIdleTimeout": 0, - "clientSessionMaxLifespan": 0, - "clientOfflineSessionIdleTimeout": 0, - "clientOfflineSessionMaxLifespan": 0, - "accessCodeLifespan": 60, - "accessCodeLifespanUserAction": 300, - "accessCodeLifespanLogin": 1800, - "actionTokenGeneratedByAdminLifespan": 43200, - "actionTokenGeneratedByUserLifespan": 300, - "oauth2DeviceCodeLifespan": 600, - "oauth2DevicePollingInterval": 5, - "enabled": true, - "sslRequired": "none", - "registrationAllowed": false, - "registrationEmailAsUsername": false, - "rememberMe": false, - "verifyEmail": false, - "loginWithEmailAllowed": true, - "duplicateEmailsAllowed": false, - "resetPasswordAllowed": false, - "editUsernameAllowed": false, - "bruteForceProtected": false, - "permanentLockout": false, - "maxFailureWaitSeconds": 900, - "minimumQuickLoginWaitSeconds": 60, - "waitIncrementSeconds": 60, - "quickLoginCheckMilliSeconds": 1000, - "maxDeltaTimeSeconds": 43200, - "failureFactor": 30, - "defaultRole": { - "id": "7a02acbe-09aa-4af7-8e79-fbb0cf5fd0d6", - "name": "default-roles-dbrepo", - "description": "${role_default-roles}", - "composite": true, - "clientRole": false, - "containerId": "4ef53018-8322-40ed-a44a-a7c5292a07be" - }, - "requiredCredentials": [ - "password" - ], - "otpPolicyType": "totp", - "otpPolicyAlgorithm": "HmacSHA1", - "otpPolicyInitialCounter": 0, - "otpPolicyDigits": 6, - "otpPolicyLookAheadWindow": 1, - "otpPolicyPeriod": 30, - "otpPolicyCodeReusable": false, - "otpSupportedApplications": [ - "totpAppGoogleName", - "totpAppMicrosoftAuthenticatorName", - "totpAppFreeOTPName" - ], - "webAuthnPolicyRpEntityName": "keycloak", - "webAuthnPolicySignatureAlgorithms": [ - "ES256" - ], - "webAuthnPolicyRpId": "", - "webAuthnPolicyAttestationConveyancePreference": "not specified", - "webAuthnPolicyAuthenticatorAttachment": "not specified", - "webAuthnPolicyRequireResidentKey": "not specified", - "webAuthnPolicyUserVerificationRequirement": "not specified", - "webAuthnPolicyCreateTimeout": 0, - "webAuthnPolicyAvoidSameAuthenticatorRegister": false, - "webAuthnPolicyAcceptableAaguids": [], - "webAuthnPolicyPasswordlessRpEntityName": "keycloak", - "webAuthnPolicyPasswordlessSignatureAlgorithms": [ - "ES256" - ], - "webAuthnPolicyPasswordlessRpId": "", - "webAuthnPolicyPasswordlessAttestationConveyancePreference": "not specified", - "webAuthnPolicyPasswordlessAuthenticatorAttachment": "not specified", - "webAuthnPolicyPasswordlessRequireResidentKey": "not specified", - "webAuthnPolicyPasswordlessUserVerificationRequirement": "not specified", - "webAuthnPolicyPasswordlessCreateTimeout": 0, - "webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister": false, - "webAuthnPolicyPasswordlessAcceptableAaguids": [], - "scopeMappings": [ - { - "clientScope": "offline_access", - "roles": [ - "offline_access" - ] - } - ], - "clientScopes": [ - { - "id": "4410371f-2840-458a-a496-375bd500f5fc", - "name": "email", - "description": "OpenID Connect built-in scope: email", - "protocol": "openid-connect", - "attributes": { - "include.in.token.scope": "true", - "display.on.consent.screen": "true", - "consent.screen.text": "${emailScopeConsentText}" - }, - "protocolMappers": [ - { - "id": "7001651a-7ded-4a0f-9cb7-816b058af730", - "name": "email", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-property-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "email", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "email", - "jsonType.label": "String" - } - }, - { - "id": "25e4ad82-8b9b-463d-8b35-02fae44ec64c", - "name": "email verified", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-property-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "emailVerified", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "email_verified", - "jsonType.label": "boolean" - } - } - ] - }, - { - "id": "ebe0a886-5df1-48f3-9f00-a45d686b0b02", - "name": "address", - "description": "OpenID Connect built-in scope: address", - "protocol": "openid-connect", - "attributes": { - "include.in.token.scope": "true", - "display.on.consent.screen": "true", - "consent.screen.text": "${addressScopeConsentText}" - }, - "protocolMappers": [ - { - "id": "866be669-9799-45ad-abdd-23f5d1c82293", - "name": "address", - "protocol": "openid-connect", - "protocolMapper": "oidc-address-mapper", - "consentRequired": false, - "config": { - "user.attribute.formatted": "formatted", - "user.attribute.country": "country", - "user.attribute.postal_code": "postal_code", - "userinfo.token.claim": "true", - "user.attribute.street": "street", - "id.token.claim": "true", - "user.attribute.region": "region", - "access.token.claim": "true", - "user.attribute.locality": "locality" - } - } - ] - }, - { - "id": "be0f3000-0106-4edd-a8f5-0618e3a3f7fe", - "name": "microprofile-jwt", - "description": "Microprofile - JWT built-in scope", - "protocol": "openid-connect", - "attributes": { - "include.in.token.scope": "true", - "display.on.consent.screen": "false" - }, - "protocolMappers": [ - { - "id": "093f222d-cee9-495a-81b7-ddae4b730ab2", - "name": "groups", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-realm-role-mapper", - "consentRequired": false, - "config": { - "multivalued": "true", - "user.attribute": "foo", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "groups", - "jsonType.label": "String" - } - }, - { - "id": "2e21b9f5-c428-4a5b-9dd8-8a80bc6d9795", - "name": "upn", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-property-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "username", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "upn", - "jsonType.label": "String" - } - } - ] - }, - { - "id": "2a95813e-053d-4c20-9f2b-f7e1ad1798f5", - "name": "role_list", - "description": "SAML role list", - "protocol": "saml", - "attributes": { - "consent.screen.text": "${samlRoleListScopeConsentText}", - "display.on.consent.screen": "true" - }, - "protocolMappers": [ - { - "id": "9c8a5628-af2a-4ba4-9cad-a8ae58c8526a", - "name": "role list", - "protocol": "saml", - "protocolMapper": "saml-role-list-mapper", - "consentRequired": false, - "config": { - "single": "false", - "attribute.nameformat": "Basic", - "attribute.name": "Role" - } - } - ] - }, - { - "id": "c6375bbe-9820-4781-8828-99020210f5bd", - "name": "roles", - "description": "OpenID Connect scope for add user roles to the access token", - "protocol": "openid-connect", - "attributes": { - "include.in.token.scope": "false", - "display.on.consent.screen": "true", - "consent.screen.text": "${rolesScopeConsentText}" - }, - "protocolMappers": [ - { - "id": "a1db2464-4e12-4696-914d-d252263e4470", - "name": "audience resolve", - "protocol": "openid-connect", - "protocolMapper": "oidc-audience-resolve-mapper", - "consentRequired": false, - "config": {} - }, - { - "id": "c5513105-d577-4ba0-bff8-f96e9272f6fd", - "name": "client roles", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-client-role-mapper", - "consentRequired": false, - "config": { - "user.attribute": "foo", - "access.token.claim": "true", - "claim.name": "resource_access.${client_id}.roles", - "jsonType.label": "String", - "multivalued": "true" - } - }, - { - "id": "66901106-8ea3-4712-8bdf-bc67da4a9d2d", - "name": "realm roles", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-realm-role-mapper", - "consentRequired": false, - "config": { - "user.attribute": "foo", - "access.token.claim": "true", - "claim.name": "realm_access.roles", - "jsonType.label": "String", - "multivalued": "true" - } - } - ] - }, - { - "id": "f0ea8ba1-794d-4b89-9ad3-b7f390c5241e", - "name": "phone", - "description": "OpenID Connect built-in scope: phone", - "protocol": "openid-connect", - "attributes": { - "include.in.token.scope": "true", - "display.on.consent.screen": "true", - "consent.screen.text": "${phoneScopeConsentText}" - }, - "protocolMappers": [ - { - "id": "b3789148-8427-4af8-bc03-4dd3fae4b312", - "name": "phone number", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "phoneNumber", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "phone_number", - "jsonType.label": "String" - } - }, - { - "id": "552ac5e8-6e04-47d5-8ddf-89230ca31b43", - "name": "phone number verified", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "phoneNumberVerified", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "phone_number_verified", - "jsonType.label": "boolean" - } - } - ] - }, - { - "id": "8ff9a2f3-582c-4cda-8ea8-d08850f8a394", - "name": "web-origins", - "description": "OpenID Connect scope for add allowed web origins to the access token", - "protocol": "openid-connect", - "attributes": { - "include.in.token.scope": "false", - "display.on.consent.screen": "false", - "consent.screen.text": "" - }, - "protocolMappers": [ - { - "id": "52af8bfd-44b1-4b69-bfd5-bc92cf1629aa", - "name": "allowed web origins", - "protocol": "openid-connect", - "protocolMapper": "oidc-allowed-origins-mapper", - "consentRequired": false, - "config": {} - } - ] - }, - { - "id": "b3ebaf28-e467-4c70-b993-d7b8459dfb5a", - "name": "profile", - "description": "OpenID Connect built-in scope: profile", - "protocol": "openid-connect", - "attributes": { - "include.in.token.scope": "true", - "display.on.consent.screen": "true", - "consent.screen.text": "${profileScopeConsentText}" - }, - "protocolMappers": [ - { - "id": "1025302a-104e-42e8-acf8-83579c21bfbb", - "name": "family name", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-property-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "lastName", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "family_name", - "jsonType.label": "String" - } - }, - { - "id": "70a9942e-85c1-4f15-9605-6408e0087b74", - "name": "given name", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-property-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "firstName", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "given_name", - "jsonType.label": "String" - } - }, - { - "id": "1e36c899-c472-427e-a024-c12a64d6084c", - "name": "website", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "website", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "website", - "jsonType.label": "String" - } - }, - { - "id": "b5e54114-79cb-488d-9e60-6a804ca3b22a", - "name": "profile", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "profile", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "profile", - "jsonType.label": "String" - } - }, - { - "id": "3e9406b9-a0ae-48d2-98b9-66ab7360b2a2", - "name": "gender", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "gender", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "gender", - "jsonType.label": "String" - } - }, - { - "id": "aec55dcc-59e7-4ea7-9eb5-55954d983fd0", - "name": "locale", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "locale", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "locale", - "jsonType.label": "String" - } - }, - { - "id": "11fd80da-8760-4fa6-9d1b-32da9df170bd", - "name": "picture", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "picture", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "picture", - "jsonType.label": "String" - } - }, - { - "id": "90f93a66-27d0-4890-846b-79d99e02ad76", - "name": "updated at", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "updatedAt", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "updated_at", - "jsonType.label": "long" - } - }, - { - "id": "6c8aa1f6-3aa2-4e48-9f9c-44cffe71e7d2", - "name": "username", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-property-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "username", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "preferred_username", - "jsonType.label": "String" - } - }, - { - "id": "a861da8e-87e7-4b5d-92fa-4ca61671c77f", - "name": "birthdate", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "birthdate", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "birthdate", - "jsonType.label": "String" - } - }, - { - "id": "ae301563-61a7-413b-87a0-036e1af942bb", - "name": "middle name", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "middleName", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "middle_name", - "jsonType.label": "String" - } - }, - { - "id": "c39e0763-48a9-4f69-84f4-4e334365f99b", - "name": "full name", - "protocol": "openid-connect", - "protocolMapper": "oidc-full-name-mapper", - "consentRequired": false, - "config": { - "id.token.claim": "true", - "access.token.claim": "true", - "userinfo.token.claim": "true" - } - }, - { - "id": "a54ae213-5854-4f77-8646-5cc7767fb914", - "name": "nickname", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "nickname", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "nickname", - "jsonType.label": "String" - } - }, - { - "id": "41824638-4a2f-4259-90fe-431a32371256", - "name": "zoneinfo", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "zoneinfo", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "zoneinfo", - "jsonType.label": "String" - } - } - ] - }, - { - "id": "4c7c18fe-2aff-4df3-aeff-4dca9de93a79", - "name": "offline_access", - "description": "OpenID Connect built-in scope: offline_access", - "protocol": "openid-connect", - "attributes": { - "consent.screen.text": "${offlineAccessScopeConsentText}", - "display.on.consent.screen": "true" - } - }, - { - "id": "30b249b2-c788-47ef-a8c7-daa0e3713b7c", - "name": "acr", - "description": "OpenID Connect scope for add acr (authentication context class reference) to the token", - "protocol": "openid-connect", - "attributes": { - "include.in.token.scope": "false", - "display.on.consent.screen": "false" - }, - "protocolMappers": [ - { - "id": "50cf075c-fdc9-43f4-ab85-1e32cafbbb26", - "name": "acr loa level", - "protocol": "openid-connect", - "protocolMapper": "oidc-acr-mapper", - "consentRequired": false, - "config": { - "id.token.claim": "true", - "access.token.claim": "true" - } - } - ] - } - ], - "defaultDefaultClientScopes": [ - "role_list", - "profile", - "email", - "roles", - "web-origins", - "acr" - ], - "defaultOptionalClientScopes": [ - "offline_access", - "address", - "phone", - "microprofile-jwt" - ], - "browserSecurityHeaders": { - "contentSecurityPolicyReportOnly": "", - "xContentTypeOptions": "nosniff", - "xRobotsTag": "none", - "xFrameOptions": "SAMEORIGIN", - "contentSecurityPolicy": "frame-src 'self'; frame-ancestors 'self'; object-src 'none';", - "xXSSProtection": "1; mode=block", - "strictTransportSecurity": "max-age=31536000; includeSubDomains" - }, - "smtpServer": {}, - "eventsEnabled": false, - "eventsListeners": [ - "jboss-logging" - ], - "enabledEventTypes": [], - "adminEventsEnabled": false, - "adminEventsDetailsEnabled": false, - "identityProviders": [], - "identityProviderMappers": [], - "components": { - "org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy": [ - { - "id": "4489b316-ab01-42cf-a224-f853a310c7c5", - "name": "Allowed Protocol Mapper Types", - "providerId": "allowed-protocol-mappers", - "subType": "authenticated", - "subComponents": {}, - "config": { - "allowed-protocol-mapper-types": [ - "oidc-address-mapper", - "saml-user-property-mapper", - "oidc-usermodel-property-mapper", - "oidc-usermodel-attribute-mapper", - "saml-user-attribute-mapper", - "oidc-sha256-pairwise-sub-mapper", - "oidc-full-name-mapper", - "saml-role-list-mapper" - ] - } - }, - { - "id": "d355f2ac-bd0d-4c5c-8e2d-52f14f332354", - "name": "Allowed Protocol Mapper Types", - "providerId": "allowed-protocol-mappers", - "subType": "anonymous", - "subComponents": {}, - "config": { - "allowed-protocol-mapper-types": [ - "oidc-full-name-mapper", - "oidc-usermodel-attribute-mapper", - "oidc-address-mapper", - "oidc-sha256-pairwise-sub-mapper", - "saml-role-list-mapper", - "saml-user-property-mapper", - "saml-user-attribute-mapper", - "oidc-usermodel-property-mapper" - ] - } - }, - { - "id": "cefbb851-d3bd-4b6d-b582-7fc4fa51d9b6", - "name": "Allowed Client Scopes", - "providerId": "allowed-client-templates", - "subType": "anonymous", - "subComponents": {}, - "config": { - "allow-default-scopes": [ - "true" - ] - } - }, - { - "id": "b7fe23c6-f82b-4920-8062-3d9fa6fb838f", - "name": "Full Scope Disabled", - "providerId": "scope", - "subType": "anonymous", - "subComponents": {}, - "config": {} - }, - { - "id": "8332b5b7-5643-4c9f-afdb-4bda506bac55", - "name": "Allowed Client Scopes", - "providerId": "allowed-client-templates", - "subType": "authenticated", - "subComponents": {}, - "config": { - "allow-default-scopes": [ - "true" - ] - } - }, - { - "id": "bb595923-ec43-4cbc-be3c-6d65c250956d", - "name": "Consent Required", - "providerId": "consent-required", - "subType": "anonymous", - "subComponents": {}, - "config": {} - }, - { - "id": "4d9901ea-7d1e-484a-a5de-abb36d8a5cc8", - "name": "Trusted Hosts", - "providerId": "trusted-hosts", - "subType": "anonymous", - "subComponents": {}, - "config": { - "host-sending-registration-request-must-match": [ - "true" - ], - "client-uris-must-match": [ - "true" - ] - } - }, - { - "id": "44bd2392-250b-4b4a-9772-7478fbd68f67", - "name": "Max Clients Limit", - "providerId": "max-clients", - "subType": "anonymous", - "subComponents": {}, - "config": { - "max-clients": [ - "200" - ] - } - } - ], - "org.keycloak.userprofile.UserProfileProvider": [ - { - "id": "0ffb33e9-2f5b-4e54-b188-19b3da816f0f", - "providerId": "declarative-user-profile", - "subComponents": {}, - "config": {} - } - ], - "org.keycloak.keys.KeyProvider": [ - { - "id": "d80cf13c-e402-49e5-b347-0175bdcac7b6", - "name": "rsa-generated", - "providerId": "rsa-generated", - "subComponents": {}, - "config": { - "priority": [ - "100" - ] - } - }, - { - "id": "03442c52-879c-4b3d-89c4-db02a0ad3c52", - "name": "aes-generated", - "providerId": "aes-generated", - "subComponents": {}, - "config": { - "priority": [ - "100" - ] - } - }, - { - "id": "30ce0596-cbce-4633-a422-15873bc363bf", - "name": "hmac-generated", - "providerId": "hmac-generated", - "subComponents": {}, - "config": { - "priority": [ - "100" - ], - "algorithm": [ - "HS256" - ] - } - }, - { - "id": "012d037c-b22a-48f2-a80a-927424d75f00", - "name": "rsa-enc-generated", - "providerId": "rsa-enc-generated", - "subComponents": {}, - "config": { - "priority": [ - "100" - ], - "algorithm": [ - "RSA-OAEP" - ] - } - } - ] - }, - "internationalizationEnabled": false, - "supportedLocales": [], - "authenticationFlows": [ - { - "id": "865c209a-a080-4162-9b49-795345294601", - "alias": "Account verification options", - "description": "Method with which to verity the existing account", - "providerId": "basic-flow", - "topLevel": false, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "idp-email-verification", - "authenticatorFlow": false, - "requirement": "ALTERNATIVE", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticatorFlow": true, - "requirement": "ALTERNATIVE", - "priority": 20, - "autheticatorFlow": true, - "flowAlias": "Verify Existing Account by Re-authentication", - "userSetupAllowed": false - } - ] - }, - { - "id": "85fc1de2-5b82-40ec-bda0-91a4fec38584", - "alias": "Authentication Options", - "description": "Authentication options.", - "providerId": "basic-flow", - "topLevel": false, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "basic-auth", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "basic-auth-otp", - "authenticatorFlow": false, - "requirement": "DISABLED", - "priority": 20, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "auth-spnego", - "authenticatorFlow": false, - "requirement": "DISABLED", - "priority": 30, - "autheticatorFlow": false, - "userSetupAllowed": false - } - ] - }, - { - "id": "2476a340-9459-4541-9e61-226f06ec66c4", - "alias": "Browser - Conditional OTP", - "description": "Flow to determine if the OTP is required for the authentication", - "providerId": "basic-flow", - "topLevel": false, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "conditional-user-configured", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "auth-otp-form", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 20, - "autheticatorFlow": false, - "userSetupAllowed": false - } - ] - }, - { - "id": "2e424297-19dd-4c09-a06a-876ef5183ca6", - "alias": "Direct Grant - Conditional OTP", - "description": "Flow to determine if the OTP is required for the authentication", - "providerId": "basic-flow", - "topLevel": false, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "conditional-user-configured", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "direct-grant-validate-otp", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 20, - "autheticatorFlow": false, - "userSetupAllowed": false - } - ] - }, - { - "id": "3966485f-3455-47d2-86a2-d990b41c7953", - "alias": "First broker login - Conditional OTP", - "description": "Flow to determine if the OTP is required for the authentication", - "providerId": "basic-flow", - "topLevel": false, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "conditional-user-configured", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "auth-otp-form", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 20, - "autheticatorFlow": false, - "userSetupAllowed": false - } - ] - }, - { - "id": "29098fa4-d254-44cb-a6c5-63b36b8bfdc1", - "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", - "topLevel": false, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "idp-confirm-link", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticatorFlow": true, - "requirement": "REQUIRED", - "priority": 20, - "autheticatorFlow": true, - "flowAlias": "Account verification options", - "userSetupAllowed": false - } - ] - }, - { - "id": "03569e80-3083-4147-ad49-445466c191d8", - "alias": "Reset - Conditional OTP", - "description": "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.", - "providerId": "basic-flow", - "topLevel": false, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "conditional-user-configured", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "reset-otp", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 20, - "autheticatorFlow": false, - "userSetupAllowed": false - } - ] - }, - { - "id": "7e660682-5eb3-4527-a5fa-53fc14a0f2f5", - "alias": "User creation or linking", - "description": "Flow for the existing/non-existing user alternatives", - "providerId": "basic-flow", - "topLevel": false, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticatorConfig": "create unique user config", - "authenticator": "idp-create-user-if-unique", - "authenticatorFlow": false, - "requirement": "ALTERNATIVE", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticatorFlow": true, - "requirement": "ALTERNATIVE", - "priority": 20, - "autheticatorFlow": true, - "flowAlias": "Handle Existing Account", - "userSetupAllowed": false - } - ] - }, - { - "id": "4deed0f8-02f9-4581-9a91-05bc58fbffba", - "alias": "Verify Existing Account by Re-authentication", - "description": "Reauthentication of existing account", - "providerId": "basic-flow", - "topLevel": false, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "idp-username-password-form", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticatorFlow": true, - "requirement": "CONDITIONAL", - "priority": 20, - "autheticatorFlow": true, - "flowAlias": "First broker login - Conditional OTP", - "userSetupAllowed": false - } - ] - }, - { - "id": "3c101100-d2dc-4e0d-beea-7bb41ee08849", - "alias": "browser", - "description": "browser based authentication", - "providerId": "basic-flow", - "topLevel": true, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "auth-cookie", - "authenticatorFlow": false, - "requirement": "ALTERNATIVE", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "auth-spnego", - "authenticatorFlow": false, - "requirement": "DISABLED", - "priority": 20, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "identity-provider-redirector", - "authenticatorFlow": false, - "requirement": "ALTERNATIVE", - "priority": 25, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticatorFlow": true, - "requirement": "ALTERNATIVE", - "priority": 30, - "autheticatorFlow": true, - "flowAlias": "forms", - "userSetupAllowed": false - } - ] - }, - { - "id": "f38f76db-5399-4231-82e0-bf31cf748d14", - "alias": "clients", - "description": "Base authentication for clients", - "providerId": "client-flow", - "topLevel": true, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "client-secret", - "authenticatorFlow": false, - "requirement": "ALTERNATIVE", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "client-jwt", - "authenticatorFlow": false, - "requirement": "ALTERNATIVE", - "priority": 20, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "client-secret-jwt", - "authenticatorFlow": false, - "requirement": "ALTERNATIVE", - "priority": 30, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "client-x509", - "authenticatorFlow": false, - "requirement": "ALTERNATIVE", - "priority": 40, - "autheticatorFlow": false, - "userSetupAllowed": false - } - ] - }, - { - "id": "ed29eed7-d490-4985-be20-2444da957182", - "alias": "direct grant", - "description": "OpenID Connect Resource Owner Grant", - "providerId": "basic-flow", - "topLevel": true, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "direct-grant-validate-username", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "direct-grant-validate-password", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 20, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticatorFlow": true, - "requirement": "CONDITIONAL", - "priority": 30, - "autheticatorFlow": true, - "flowAlias": "Direct Grant - Conditional OTP", - "userSetupAllowed": false - } - ] - }, - { - "id": "e41f4bae-aad9-4821-8597-514e6dc5796c", - "alias": "docker auth", - "description": "Used by Docker clients to authenticate against the IDP", - "providerId": "basic-flow", - "topLevel": true, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "docker-http-basic-authenticator", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - } - ] - }, - { - "id": "3149ebc7-481d-43bd-9d09-9e66bd9b2b7f", - "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", - "topLevel": true, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticatorConfig": "review profile config", - "authenticator": "idp-review-profile", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticatorFlow": true, - "requirement": "REQUIRED", - "priority": 20, - "autheticatorFlow": true, - "flowAlias": "User creation or linking", - "userSetupAllowed": false - } - ] - }, - { - "id": "7aaab1d1-5357-4e71-8553-52d5613acf7d", - "alias": "forms", - "description": "Username, password, otp and other auth forms.", - "providerId": "basic-flow", - "topLevel": false, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "auth-username-password-form", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticatorFlow": true, - "requirement": "CONDITIONAL", - "priority": 20, - "autheticatorFlow": true, - "flowAlias": "Browser - Conditional OTP", - "userSetupAllowed": false - } - ] - }, - { - "id": "c2ffc5bb-568a-4648-bc13-8e7d6a4a53b4", - "alias": "http challenge", - "description": "An authentication flow based on challenge-response HTTP Authentication Schemes", - "providerId": "basic-flow", - "topLevel": true, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "no-cookie-redirect", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticatorFlow": true, - "requirement": "REQUIRED", - "priority": 20, - "autheticatorFlow": true, - "flowAlias": "Authentication Options", - "userSetupAllowed": false - } - ] - }, - { - "id": "c6b78806-13ee-4010-b282-a6e527dda66c", - "alias": "registration", - "description": "registration flow", - "providerId": "basic-flow", - "topLevel": true, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "registration-page-form", - "authenticatorFlow": true, - "requirement": "REQUIRED", - "priority": 10, - "autheticatorFlow": true, - "flowAlias": "registration form", - "userSetupAllowed": false - } - ] - }, - { - "id": "b5f00ead-4f6f-4454-a925-154c1e9c2cc5", - "alias": "registration form", - "description": "registration form", - "providerId": "form-flow", - "topLevel": false, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "registration-user-creation", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 20, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "registration-profile-action", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 40, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "registration-password-action", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 50, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "registration-recaptcha-action", - "authenticatorFlow": false, - "requirement": "DISABLED", - "priority": 60, - "autheticatorFlow": false, - "userSetupAllowed": false - } - ] - }, - { - "id": "f8370f63-f996-4039-9fbf-86e87da675dd", - "alias": "reset credentials", - "description": "Reset credentials for a user if they forgot their password or something", - "providerId": "basic-flow", - "topLevel": true, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "reset-credentials-choose-user", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "reset-credential-email", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 20, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticator": "reset-password", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 30, - "autheticatorFlow": false, - "userSetupAllowed": false - }, - { - "authenticatorFlow": true, - "requirement": "CONDITIONAL", - "priority": 40, - "autheticatorFlow": true, - "flowAlias": "Reset - Conditional OTP", - "userSetupAllowed": false - } - ] - }, - { - "id": "7f156260-804d-4848-9ea0-347281aea644", - "alias": "saml ecp", - "description": "SAML ECP Profile Authentication Flow", - "providerId": "basic-flow", - "topLevel": true, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "http-basic-authenticator", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 10, - "autheticatorFlow": false, - "userSetupAllowed": false - } - ] - } - ], - "authenticatorConfig": [ - { - "id": "17f2c0cc-8dd8-4774-adcc-cd294cdadac8", - "alias": "create unique user config", - "config": { - "require.password.update.after.registration": "false" - } - }, - { - "id": "d5be3f6c-d096-4232-88e8-b0f46e608cff", - "alias": "review profile config", - "config": { - "update.profile.on.first.login": "missing" - } - } - ], - "requiredActions": [ - { - "alias": "CONFIGURE_TOTP", - "name": "Configure OTP", - "providerId": "CONFIGURE_TOTP", - "enabled": true, - "defaultAction": false, - "priority": 10, - "config": {} - }, - { - "alias": "TERMS_AND_CONDITIONS", - "name": "Terms and Conditions", - "providerId": "TERMS_AND_CONDITIONS", - "enabled": false, - "defaultAction": false, - "priority": 20, - "config": {} - }, - { - "alias": "UPDATE_PASSWORD", - "name": "Update Password", - "providerId": "UPDATE_PASSWORD", - "enabled": true, - "defaultAction": false, - "priority": 30, - "config": {} - }, - { - "alias": "UPDATE_PROFILE", - "name": "Update Profile", - "providerId": "UPDATE_PROFILE", - "enabled": true, - "defaultAction": false, - "priority": 40, - "config": {} - }, - { - "alias": "VERIFY_EMAIL", - "name": "Verify Email", - "providerId": "VERIFY_EMAIL", - "enabled": true, - "defaultAction": false, - "priority": 50, - "config": {} - }, - { - "alias": "delete_account", - "name": "Delete Account", - "providerId": "delete_account", - "enabled": false, - "defaultAction": false, - "priority": 60, - "config": {} - }, - { - "alias": "webauthn-register", - "name": "Webauthn Register", - "providerId": "webauthn-register", - "enabled": true, - "defaultAction": false, - "priority": 70, - "config": {} - }, - { - "alias": "webauthn-register-passwordless", - "name": "Webauthn Register Passwordless", - "providerId": "webauthn-register-passwordless", - "enabled": true, - "defaultAction": false, - "priority": 80, - "config": {} - }, - { - "alias": "update_user_locale", - "name": "Update User Locale", - "providerId": "update_user_locale", - "enabled": true, - "defaultAction": false, - "priority": 1000, - "config": {} - } - ], - "browserFlow": "browser", - "registrationFlow": "registration", - "directGrantFlow": "direct grant", - "resetCredentialsFlow": "reset credentials", - "clientAuthenticationFlow": "clients", - "dockerAuthenticationFlow": "docker auth", - "attributes": { - "cibaBackchannelTokenDeliveryMode": "poll", - "cibaAuthRequestedUserHint": "login_hint", - "oauth2DevicePollingInterval": "5", - "clientOfflineSessionMaxLifespan": "0", - "clientSessionIdleTimeout": "0", - "clientOfflineSessionIdleTimeout": "0", - "cibaInterval": "5", - "realmReusableOtpCode": "false", - "cibaExpiresIn": "120", - "oauth2DeviceCodeLifespan": "600", - "parRequestUriLifespan": "60", - "clientSessionMaxLifespan": "0", - "frontendUrl": "", - "acr.loa.map": "{}" - }, - "keycloakVersion": "21.0.2", - "userManagedAccessAllowed": false, - "clientProfiles": { - "profiles": [] - }, - "clientPolicies": { - "policies": [] - } +{ + "id" : "82c39861-d877-4667-a0f3-4daa2ee230e0", + "realm" : "dbrepo", + "notBefore" : 0, + "defaultSignatureAlgorithm" : "RS256", + "revokeRefreshToken" : false, + "refreshTokenMaxReuse" : 1, + "accessTokenLifespan" : 900, + "accessTokenLifespanForImplicitFlow" : 900, + "ssoSessionIdleTimeout" : 864000, + "ssoSessionMaxLifespan" : 2592000, + "ssoSessionIdleTimeoutRememberMe" : 0, + "ssoSessionMaxLifespanRememberMe" : 0, + "offlineSessionIdleTimeout" : 2592000, + "offlineSessionMaxLifespanEnabled" : false, + "offlineSessionMaxLifespan" : 5184000, + "clientSessionIdleTimeout" : 0, + "clientSessionMaxLifespan" : 0, + "clientOfflineSessionIdleTimeout" : 0, + "clientOfflineSessionMaxLifespan" : 0, + "accessCodeLifespan" : 60, + "accessCodeLifespanUserAction" : 300, + "accessCodeLifespanLogin" : 1800, + "actionTokenGeneratedByAdminLifespan" : 43200, + "actionTokenGeneratedByUserLifespan" : 1800, + "oauth2DeviceCodeLifespan" : 600, + "oauth2DevicePollingInterval" : 5, + "enabled" : true, + "sslRequired" : "none", + "registrationAllowed" : false, + "registrationEmailAsUsername" : false, + "rememberMe" : false, + "verifyEmail" : true, + "loginWithEmailAllowed" : false, + "duplicateEmailsAllowed" : false, + "resetPasswordAllowed" : false, + "editUsernameAllowed" : false, + "bruteForceProtected" : false, + "permanentLockout" : false, + "maxFailureWaitSeconds" : 900, + "minimumQuickLoginWaitSeconds" : 60, + "waitIncrementSeconds" : 60, + "quickLoginCheckMilliSeconds" : 1000, + "maxDeltaTimeSeconds" : 43200, + "failureFactor" : 30, + "roles" : { + "realm" : [ { + "id" : "48f38342-1e3f-427a-995d-c436eaee65cb", + "name" : "default-user-handling", + "description" : "${default-user-handling}", + "composite" : true, + "composites" : { + "realm" : [ "modify-user-theme", "modify-user-information" ] + }, + "clientRole" : false, + "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", + "attributes" : { } + }, { + "id" : "9bb4a8dc-28e0-4645-b62f-cc94425f0cb0", + "name" : "default-maintenance-handling", + "description" : "${default-maintenance-handling}", + "composite" : true, + "composites" : { + "realm" : [ "create-maintenance-message", "find-maintenance-message", "update-maintenance-message", "delete-maintenance-message", "list-maintenance-messages" ] + }, + "clientRole" : false, + "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", + "attributes" : { } + }, { + "id" : "7ee1c424-11b0-46a9-b0ed-725e9b7fc40c", + "name" : "default-system-roles", + "description" : "${default-system-roles}", + "composite" : true, + "composites" : { + "realm" : [ "delete-database-view", "update-semantic-unit", "export-query-data", "default-data-steward-roles", "execute-query", "default-user-handling", "delete-table-data", "find-query", "list-database-views", "persist-query", "update-search-index", "delete-database-access", "view-table-history", "create-ontology", "update-ontology", "modify-user-theme", "default-system-roles", "create-semantic-concept", "default-container-handling", "create-container", "create-table", "default-broker-handling", "default-maintenance-handling", "execute-semantic-query", "uma_authorization", "table-semantic-analyse", "list-containers", "check-database-access", "escalated-query-handling", "delete-identifier", "modify-database-owner", "list-tables", "export-table-data", "create-database-access", "delete-container", "re-execute-query", "create-semantic-unit", "escalated-identifier-handling", "system", "update-table-statistic", "escalated-semantics-handling", "default-database-handling", "delete-ontology", "find-database", "find-database-view", "update-semantic-concept", "find-user", "import-database-data", "publish-identifier", "default-roles-dbrepo", "find-foreign-user", "create-database", "create-maintenance-message", "find-maintenance-message", "escalated-container-handling", "default-researcher-roles", "default-identifier-handling", "escalated-user-handling", "modify-user-information", "create-database-view", "update-maintenance-message", "delete-foreign-table", "offline_access", "modify-foreign-table-column-semantics", "delete-maintenance-message", "find-container", "insert-table-data", "modify-identifier-metadata", "modify-database-image", "escalated-broker-handling", "modify-table-column-semantics", "escalated-database-handling", "default-semantics-handling", "update-database-access", "default-query-handling", "find-table", "list-queries", "default-developer-roles", "create-identifier", "escalated-table-handling", "find-identifier", "view-database-view-data", "view-table-data", "list-licenses", "default-table-handling", "list-identifiers", "create-foreign-identifier", "list-databases", "list-ontologies", "modify-database-visibility", "list-maintenance-messages", "delete-table" ] + }, + "clientRole" : false, + "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", + "attributes" : { } + }, { + "id" : "143ba359-5fa2-451e-8296-43ecf20bb251", + "name" : "update-semantic-concept", + "description" : "${update-semantic-concept}", + "composite" : false, + "clientRole" : false, + "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", + "attributes" : { } + }, { + "id" : "5136d7a3-e3f0-4585-bacd-15cb8a56095c", + "name" : "escalated-container-handling", + "description" : "${escalated-container-handling}", + "composite" : true, + "composites" : { + "realm" : [ "create-container", "delete-container" ] + }, + "clientRole" : false, + "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", + "attributes" : { } + }, { + "id" : "b0bc8649-7d84-4dd3-84f0-7f174425babe", + "name" : "list-tables", + "description" : "${list-tables}", + "composite" : false, + "clientRole" : false, + "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", + "attributes" : { } + }, { + "id" : "bfd85d9c-2772-4660-a8f0-cdc0cd8252b3", + "name" : "default-database-handling", + "description" : "${default-database-handling}", + "composite" : true, + "composites" : { + "realm" : [ "modify-database-image", "modify-database-owner", "update-database-access", "create-database", "list-databases", "create-database-access", "find-database", "modify-database-visibility", "import-database-data", "delete-database-access", "check-database-access" ] + }, + "clientRole" : false, + "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", + "attributes" : { } + }, { + "id" : "74648f9a-777e-4ef9-b97b-4c5d749d862f", + "name" : "update-search-index", + "description" : "${update-search-index}", + "composite" : false, + "clientRole" : false, + "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", + "attributes" : { } + }, { + "id" : "22492b64-c633-48a0-9678-b28669f2885b", + "name" : "execute-semantic-query", + "description" : "${execute-semantic-query}", + "composite" : false, + "clientRole" : false, + "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", + "attributes" : { } + }, { + "id" : "4ed919fa-edc5-44e5-9411-607786e4a86d", + "name" : "view-table-history", + "description" : "${view-table-history}", + "composite" : false, + "clientRole" : false, + "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", + "attributes" : { } + }, { + "id" : "d89a2881-b642-4abb-b990-196e71372f6b", + "name" : "default-table-handling", + "description" : "${default-table-handling}", + "composite" : true, + "composites" : { + "realm" : [ "modify-table-column-semantics", "list-tables", "update-table-statistic", "find-table", "create-table", "delete-table" ] + }, + "clientRole" : false, + "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", + "attributes" : { } + }, { + "id" : "b0d66d3d-59b4-4aae-aa66-e3d5a49f28e3", + "name" : "view-database-view-data", + "description" : "${view-database-view-data}", + "composite" : false, + "clientRole" : false, + "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", + "attributes" : { } + }, { + "id" : "f5ea431a-9b2c-4195-bcb4-9511f38e4b44", + "name" : "create-database-view", + "description" : "${create-database-view}", + "composite" : false, + "clientRole" : false, + "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", + "attributes" : { } + }, { + "id" : "a5ffc20e-8b11-498c-9f3b-b5740aec24c7", + "name" : "default-semantics-handling", + "description" : "${default-semantics-handling}", + "composite" : true, + "composites" : { + "realm" : [ "create-semantic-unit", "create-semantic-concept", "execute-semantic-query", "table-semantic-analyse" ] + }, + "clientRole" : false, + "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", + "attributes" : { } + }, { + "id" : "fe4a01f3-6590-4df6-9ade-5a9c1fae4736", + "name" : "create-semantic-unit", + "description" : "${create-semantic-unit}", + "composite" : false, + "clientRole" : false, + "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", + "attributes" : { } + }, { + "id" : "0e12eedf-545d-4d32-ac4d-2821dcb118b8", + "name" : "update-table-statistic", + "description" : "${update-table-statistic}", + "composite" : false, + "clientRole" : false, + "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", + "attributes" : { } + }, { + "id" : "e63e61a2-d852-4ad3-bfb5-92d9ceafef6a", + "name" : "escalated-user-handling", + "description" : "${escalated-user-handling}", + "composite" : true, + "composites" : { + "realm" : [ "find-user" ] + }, + "clientRole" : false, + "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", + "attributes" : { } + }, { + "id" : "be4e1aba-e276-4241-b6ea-01dce6c52f8b", + "name" : "find-container", + "description" : "${find-container}", + "composite" : false, + "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-semantics-handling", "default-container-handling", "default-query-handling", "default-user-handling", "default-database-handling", "default-broker-handling", "default-identifier-handling" ] + }, + "clientRole" : false, + "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", + "attributes" : { } + }, { + "id" : "3d8104fb-8307-40f0-b4b2-c3e518957110", + "name" : "view-table-data", + "description" : "${view-table-data}", + "composite" : false, + "clientRole" : false, + "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", + "attributes" : { } + }, { + "id" : "fe71b907-7020-44ab-9964-da2b87264582", + "name" : "create-database", + "description" : "${create-database}", + "composite" : false, + "clientRole" : false, + "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", + "attributes" : { } + }, { + "id" : "e51b63c2-48dd-4bd6-95fb-d257d21b26ba", + "name" : "import-database-data", + "description" : "${import-database-data}", + "composite" : false, + "clientRole" : false, + "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", + "attributes" : { } + }, { + "id" : "1f0a9b13-c2b8-474c-bc08-59dbd71835a6", + "name" : "modify-database-image", + "description" : "${modify-database-image}", + "composite" : false, + "clientRole" : false, + "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", + "attributes" : { } + }, { + "id" : "a7ad038c-5c06-42fc-951c-15ac09d4df66", + "name" : "modify-database-owner", + "description" : "${modify-database-owner}", + "composite" : false, + "clientRole" : false, + "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", + "attributes" : { } + }, { + "id" : "c12c1f4e-186f-4153-a795-26e79fb623d6", + "name" : "create-ontology", + "description" : "${create-ontology}", + "composite" : false, + "clientRole" : false, + "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", + "attributes" : { } + }, { + "id" : "b60a5694-4099-4f7d-a7e9-4c433e0eb9c9", + "name" : "update-semantic-unit", + "description" : "${update-semantic-unit}", + "composite" : false, + "clientRole" : false, + "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", + "attributes" : { } + }, { + "id" : "e9854bbb-4580-4757-b1ae-305934173249", + "name" : "create-database-access", + "description" : "${create-database-access}", + "composite" : false, + "clientRole" : false, + "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", + "attributes" : { } + }, { + "id" : "50c604c1-7c6e-43f3-9c43-2398f5eff66e", + "name" : "list-databases", + "description" : "${list-databases}", + "composite" : false, + "clientRole" : false, + "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", + "attributes" : { } + }, { + "id" : "535f1484-4514-4d24-8d97-e3f6c11a426b", + "name" : "create-container", + "description" : "${create-container}", + "composite" : false, + "clientRole" : false, + "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", + "attributes" : { } + }, { + "id" : "f4116230-8642-4bb7-bbc8-db9c5c07b558", + "name" : "create-maintenance-message", + "description" : "${create-maintenance-message}", + "composite" : false, + "clientRole" : false, + "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", + "attributes" : { } + }, { + "id" : "973f0999-cc70-4b28-9f43-979c470bea8e", + "name" : "default-data-steward-roles", + "description" : "${default-data-steward-roles}", + "composite" : true, + "composites" : { + "realm" : [ "escalated-identifier-handling", "default-semantics-handling", "escalated-semantics-handling", "default-user-handling" ] + }, + "clientRole" : false, + "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", + "attributes" : { } + }, { + "id" : "e1383fb7-d54c-4732-9146-93030eb2ca50", + "name" : "escalated-query-handling", + "description" : "${escalated-query-handling}", + "composite" : false, + "clientRole" : false, + "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", + "attributes" : { } + }, { + "id" : "993b5c69-9eb2-42af-ac28-b4a46c6b61f2", + "name" : "find-user", + "description" : "${find-user}", + "composite" : false, + "clientRole" : false, + "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", + "attributes" : { } + }, { + "id" : "e4cfdc4d-2373-477b-a8df-161db99aba00", + "name" : "create-foreign-identifier", + "description" : "${create-foreign-identifier}", + "composite" : false, + "clientRole" : false, + "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", + "attributes" : { } + }, { + "id" : "6a5872a5-2b51-415d-ae2d-25a6db4a35df", + "name" : "escalated-semantics-handling", + "description" : "${escalated-semantics-handling}", + "composite" : true, + "composites" : { + "realm" : [ "update-semantic-unit", "create-ontology", "update-ontology", "list-ontologies", "delete-ontology", "modify-foreign-table-column-semantics", "update-semantic-concept" ] + }, + "clientRole" : false, + "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", + "attributes" : { } + }, { + "id" : "09147c48-273b-450b-8b11-7ef9b9245244", + "name" : "export-table-data", + "description" : "${export-table-data}", + "composite" : false, + "clientRole" : false, + "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", + "attributes" : { } + }, { + "id" : "d14af590-60a8-4d75-b864-40ee0165bd7f", + "name" : "delete-database-access", + "description" : "${delete-database-access}", + "composite" : false, + "clientRole" : false, + "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", + "attributes" : { } + }, { + "id" : "be051d45-cd74-4b13-8a45-f2d3351bd995", + "name" : "table-semantic-analyse", + "description" : "${table-semantic-analyse}", + "composite" : false, + "clientRole" : false, + "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", + "attributes" : { } + }, { + "id" : "272a79a7-e282-4261-8f7d-5d5d1364243a", + "name" : "update-maintenance-message", + "description" : "${update-maintenance-message}", + "composite" : false, + "clientRole" : false, + "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", + "attributes" : { } + }, { + "id" : "64c16bfb-2015-48ad-a23f-637ff24419cb", + "name" : "default-query-handling", + "description" : "${default-query-handling}", + "composite" : true, + "composites" : { + "realm" : [ "delete-database-view", "export-query-data", "execute-query", "delete-table-data", "export-table-data", "list-queries", "find-query", "list-database-views", "persist-query", "view-database-view-data", "view-table-data", "re-execute-query", "view-table-history", "create-database-view", "find-database-view", "insert-table-data" ] + }, + "clientRole" : false, + "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", + "attributes" : { } + }, { + "id" : "c047d521-cec3-4444-86c4-aef098489b7b", + "name" : "delete-maintenance-message", + "description" : "${delete-maintenance-message}", + "composite" : false, + "clientRole" : false, + "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", + "attributes" : { } + }, { + "id" : "88f82262-be80-4d18-9fb4-5529da031f33", + "name" : "system", + "description" : "${system}", + "composite" : false, + "clientRole" : false, + "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", + "attributes" : { } + }, { + "id" : "e14ab76b-1c24-484d-ae2d-478b8457edea", + "name" : "list-licenses", + "description" : "${list-licenses}", + "composite" : false, + "clientRole" : false, + "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", + "attributes" : { } + }, { + "id" : "d4f29937-3ca0-41e9-9786-2b7b921b6cdd", + "name" : "modify-foreign-table-column-semantics", + "description" : "${modify-foreign-table-column-semantics}", + "composite" : false, + "clientRole" : false, + "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", + "attributes" : { } + }, { + "id" : "8eda9f5c-938c-4915-bed5-6a81a1de15a8", + "name" : "list-database-views", + "description" : "${list-database-views}", + "composite" : false, + "clientRole" : false, + "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", + "attributes" : { } + }, { + "id" : "b372f8f7-d203-4293-b991-ad93fb505917", + "name" : "escalated-database-handling", + "description" : "${escalated-database-handling}", + "composite" : false, + "clientRole" : false, + "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", + "attributes" : { } + }, { + "id" : "abd2d9ee-ebc4-4d0a-839e-6b588a6d442a", + "name" : "default-roles-dbrepo", + "description" : "${role_default-roles}", + "composite" : false, + "clientRole" : false, + "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", + "attributes" : { } + }, { + "id" : "3293799a-82b9-4f47-8f25-1aad2e0222fd", + "name" : "find-identifier", + "description" : "${find-identifier}", + "composite" : false, + "clientRole" : false, + "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", + "attributes" : { } + }, { + "id" : "aaa3f804-38a0-4474-b8e9-f1020c4b3f62", + "name" : "list-queries", + "description" : "${list-queries}", + "composite" : false, + "clientRole" : false, + "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", + "attributes" : { } + }, { + "id" : "76e38f7b-99bf-4d12-8d74-1c7d8812f443", + "name" : "update-ontology", + "description" : "${update-ontology}", + "composite" : false, + "clientRole" : false, + "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", + "attributes" : { } + }, { + "id" : "11f7973e-d1eb-42cb-a35d-c59dfc122775", + "name" : "modify-user-theme", + "description" : "${modify-user-theme}", + "composite" : false, + "clientRole" : false, + "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", + "attributes" : { } + }, { + "id" : "f392bfcb-0be5-4fad-9ce4-8ac6396f176d", + "name" : "export-query-data", + "description" : "${export-query-data}", + "composite" : false, + "clientRole" : false, + "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", + "attributes" : { } + }, { + "id" : "da493b7e-fb9b-43ca-82a5-e274ad2e6b39", + "name" : "find-query", + "description" : "${find-query}", + "composite" : false, + "clientRole" : false, + "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", + "attributes" : { } + }, { + "id" : "a4d4a788-ebcf-4d32-baed-4a85616ca037", + "name" : "escalated-identifier-handling", + "description" : "${escalated-identifier-handling}", + "composite" : true, + "composites" : { + "realm" : [ "create-foreign-identifier", "modify-identifier-metadata" ] + }, + "clientRole" : false, + "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", + "attributes" : { } + }, { + "id" : "ea38d69d-17b8-4c65-95e8-1c3501b83618", + "name" : "default-container-handling", + "description" : "${default-container-handling}", + "composite" : true, + "composites" : { + "realm" : [ "find-container", "list-containers" ] + }, + "clientRole" : false, + "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", + "attributes" : { } + }, { + "id" : "8b8813e0-af07-4d04-a8c1-e3f37192bace", + "name" : "publish-identifier", + "description" : "${publish-identifier}", + "composite" : false, + "clientRole" : false, + "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", + "attributes" : { } + }, { + "id" : "47f5eee7-9821-4bf8-b434-0da1f81c3e5a", + "name" : "default-broker-handling", + "description" : "${default-broker-handling}", + "composite" : false, + "clientRole" : false, + "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", + "attributes" : { } + }, { + "id" : "71874bde-64a5-4a69-8685-d8998303a80c", + "name" : "delete-table-data", + "description" : "${delete-table-data}", + "composite" : false, + "clientRole" : false, + "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", + "attributes" : { } + }, { + "id" : "7c0306fc-3b03-4c64-87d1-9a34f2073977", + "name" : "modify-table-column-semantics", + "description" : "${modify-table-column-semantics}", + "composite" : false, + "clientRole" : false, + "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", + "attributes" : { } + }, { + "id" : "cd0ee04c-4a5e-4035-a11b-f6a1165f7829", + "name" : "delete-container", + "description" : "${delete-container}", + "composite" : false, + "clientRole" : false, + "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", + "attributes" : { } + }, { + "id" : "67ee39c0-d601-4a67-a0fe-c4f0021d557e", + "name" : "list-containers", + "description" : "${list-containers}", + "composite" : false, + "clientRole" : false, + "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", + "attributes" : { } + }, { + "id" : "795c7bb8-3502-414a-a97b-2ba1cfd6a79c", + "name" : "persist-query", + "description" : "${persist-query}", + "composite" : false, + "clientRole" : false, + "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", + "attributes" : { } + }, { + "id" : "d05e7698-ddf5-4f20-9027-771afb2cc3c7", + "name" : "list-identifiers", + "description" : "${list-identifiers}", + "composite" : false, + "clientRole" : false, + "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", + "attributes" : { } + }, { + "id" : "e4bfaf36-9a5d-43e0-9fa3-0f4ea7bad8d0", + "name" : "default-developer-roles", + "description" : "${default-developer-roles}", + "composite" : true, + "composites" : { + "realm" : [ "escalated-query-handling", "escalated-broker-handling", "default-table-handling", "escalated-database-handling", "default-container-handling", "default-query-handling", "default-user-handling", "default-database-handling", "default-maintenance-handling", "escalated-container-handling", "escalated-table-handling", "default-identifier-handling" ] + }, + "clientRole" : false, + "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", + "attributes" : { } + }, { + "id" : "e2cb054e-ea41-4ab0-881b-e6f576f7424e", + "name" : "create-semantic-concept", + "description" : "${create-semantic-concept}", + "composite" : false, + "clientRole" : false, + "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", + "attributes" : { } + }, { + "id" : "feb612cc-96a6-4ed2-aaa5-01f39b25beb5", + "name" : "insert-table-data", + "description" : "${insert-table-data}", + "composite" : false, + "clientRole" : false, + "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", + "attributes" : { } + }, { + "id" : "a0942e33-441b-4343-9f02-4353d03f7bbb", + "name" : "find-database", + "description" : "${find-database}", + "composite" : false, + "clientRole" : false, + "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", + "attributes" : { } + }, { + "id" : "6a0bb740-4448-49be-aee8-6dd183325be5", + "name" : "delete-foreign-table", + "description" : "${delete-foreign-table}", + "composite" : false, + "clientRole" : false, + "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", + "attributes" : { } + }, { + "id" : "7f3652c7-3073-4566-ab63-25385495ebc3", + "name" : "modify-database-visibility", + "description" : "${modify-database-visibility}", + "composite" : false, + "clientRole" : false, + "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", + "attributes" : { } + }, { + "id" : "4a5df51d-f14d-41a2-ad70-6521df5a5b4f", + "name" : "offline_access", + "description" : "${role_offline-access}", + "composite" : false, + "clientRole" : false, + "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", + "attributes" : { } + }, { + "id" : "fd41c4c3-d2f8-4f49-84c7-dba84e9a5575", + "name" : "execute-query", + "description" : "${execute-query}", + "composite" : false, + "clientRole" : false, + "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", + "attributes" : { } + }, { + "id" : "2963c2bb-b129-4224-b98f-c8eeab8e72d1", + "name" : "create-table", + "description" : "${create-table}", + "composite" : false, + "clientRole" : false, + "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", + "attributes" : { } + }, { + "id" : "0c487c93-448f-4a82-8b9f-ebd8a0904bf8", + "name" : "find-foreign-user", + "description" : "${find-foreign-user}", + "composite" : false, + "clientRole" : false, + "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", + "attributes" : { } + }, { + "id" : "cf9735a9-fb70-4cc5-b5f4-75afc4e5654b", + "name" : "modify-identifier-metadata", + "description" : "${modify-identifier-metadata}", + "composite" : false, + "clientRole" : false, + "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", + "attributes" : { } + }, { + "id" : "64c2b8f2-1527-4928-81ea-b2651512d028", + "name" : "delete-ontology", + "description" : "${delete-ontology}", + "composite" : false, + "clientRole" : false, + "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", + "attributes" : { } + }, { + "id" : "d6e38368-b40f-423b-82e4-e8aa595237c9", + "name" : "find-maintenance-message", + "description" : "${find-maintenance-message}", + "composite" : false, + "clientRole" : false, + "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", + "attributes" : { } + }, { + "id" : "fd1cc463-3e67-49d9-81b8-2cd90c1daa9c", + "name" : "check-database-access", + "description" : "${check-database-access}", + "composite" : false, + "clientRole" : false, + "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", + "attributes" : { } + }, { + "id" : "74013867-e426-46cc-ab98-2f4a9225ad1e", + "name" : "find-table", + "description" : "${find-table}", + "composite" : false, + "clientRole" : false, + "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", + "attributes" : { } + }, { + "id" : "a2cc60df-d280-46c5-a539-92e2aa249b4a", + "name" : "modify-user-information", + "description" : "${modify-user-information}", + "composite" : false, + "clientRole" : false, + "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", + "attributes" : { } + }, { + "id" : "c367241f-b5b5-491f-84d5-07fe1bef3877", + "name" : "default-identifier-handling", + "description" : "${default-identifier-handling}", + "composite" : true, + "composites" : { + "realm" : [ "delete-identifier", "list-identifiers", "create-identifier", "find-identifier", "publish-identifier" ] + }, + "clientRole" : false, + "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", + "attributes" : { } + }, { + "id" : "ba1ad8f2-39aa-487d-987f-645e8a459559", + "name" : "delete-table", + "description" : "${delete-table}", + "composite" : false, + "clientRole" : false, + "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", + "attributes" : { } + }, { + "id" : "09f7bdb0-296f-46c8-a3a3-8f9254fb17e4", + "name" : "list-maintenance-messages", + "description" : "${list-maintenance-messages}", + "composite" : false, + "clientRole" : false, + "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", + "attributes" : { } + }, { + "id" : "fe3bc45c-61c2-4ece-bcaf-d410dc7de501", + "name" : "update-database-access", + "description" : "${update-database-access}", + "composite" : false, + "clientRole" : false, + "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", + "attributes" : { } + }, { + "id" : "f43e86ed-76de-4ca8-9b5e-c292c9359bfe", + "name" : "escalated-broker-handling", + "description" : "${escalated-broker-handling}", + "composite" : false, + "clientRole" : false, + "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", + "attributes" : { } + }, { + "id" : "916b1e65-f60c-42cd-96e4-5c98ffc1ba3c", + "name" : "uma_authorization", + "description" : "${role_uma_authorization}", + "composite" : false, + "clientRole" : false, + "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", + "attributes" : { } + }, { + "id" : "d1afa3ed-bf4f-469a-a061-ad7325fb8d9e", + "name" : "delete-database-view", + "description" : "${delete-database-view}", + "composite" : false, + "clientRole" : false, + "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", + "attributes" : { } + }, { + "id" : "6f044bad-6651-4408-bffa-20c2d8f92eee", + "name" : "create-identifier", + "description" : "${create-identifier}", + "composite" : false, + "clientRole" : false, + "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", + "attributes" : { } + }, { + "id" : "be91195a-e30a-4d15-a8da-0aca0a68782f", + "name" : "escalated-table-handling", + "description" : "${escalated-table-handling}", + "composite" : true, + "composites" : { + "realm" : [ "delete-foreign-table" ] + }, + "clientRole" : false, + "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", + "attributes" : { } + }, { + "id" : "98bee7d6-d78c-4e7f-b6a3-3705968b248c", + "name" : "list-ontologies", + "description" : "${list-ontologies}", + "composite" : false, + "clientRole" : false, + "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", + "attributes" : { } + }, { + "id" : "15720c6b-027d-4d53-a0ff-0124bfab7c4c", + "name" : "re-execute-query", + "description" : "${re-execute-query}", + "composite" : false, + "clientRole" : false, + "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", + "attributes" : { } + }, { + "id" : "a9b5181a-8135-41d3-9862-ef80af42211d", + "name" : "delete-identifier", + "description" : "${delete-identifier}", + "composite" : false, + "clientRole" : false, + "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", + "attributes" : { } + }, { + "id" : "469c2e63-cda6-48d4-ab8f-eb59a2c69798", + "name" : "find-database-view", + "description" : "${find-database-view}", + "composite" : false, + "clientRole" : false, + "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", + "attributes" : { } + } ], + "client" : { + "realm-management" : [ { + "id" : "4628f654-f8f3-483b-8f92-2a7fc5930b14", + "name" : "query-realms", + "description" : "${role_query-realms}", + "composite" : false, + "clientRole" : true, + "containerId" : "cfffd5d0-aa19-4057-8ca0-f2c51ca0e930", + "attributes" : { } + }, { + "id" : "95c2cc47-12f5-4d73-8b74-67e270c45ade", + "name" : "manage-authorization", + "description" : "${role_manage-authorization}", + "composite" : false, + "clientRole" : true, + "containerId" : "cfffd5d0-aa19-4057-8ca0-f2c51ca0e930", + "attributes" : { } + }, { + "id" : "824791f3-c345-42f8-b103-b7e6d7e40114", + "name" : "manage-identity-providers", + "description" : "${role_manage-identity-providers}", + "composite" : false, + "clientRole" : true, + "containerId" : "cfffd5d0-aa19-4057-8ca0-f2c51ca0e930", + "attributes" : { } + }, { + "id" : "1f840202-b7e2-4195-bac9-64e64dad2037", + "name" : "view-identity-providers", + "description" : "${role_view-identity-providers}", + "composite" : false, + "clientRole" : true, + "containerId" : "cfffd5d0-aa19-4057-8ca0-f2c51ca0e930", + "attributes" : { } + }, { + "id" : "3c32c096-bb13-44c9-a080-d756a48a9ea3", + "name" : "query-clients", + "description" : "${role_query-clients}", + "composite" : false, + "clientRole" : true, + "containerId" : "cfffd5d0-aa19-4057-8ca0-f2c51ca0e930", + "attributes" : { } + }, { + "id" : "e4b85a68-7f31-4fcf-89a2-f10d7df358e9", + "name" : "view-authorization", + "description" : "${role_view-authorization}", + "composite" : false, + "clientRole" : true, + "containerId" : "cfffd5d0-aa19-4057-8ca0-f2c51ca0e930", + "attributes" : { } + }, { + "id" : "7d317752-ae56-46f2-a2ce-67c64d1b35f6", + "name" : "view-users", + "description" : "${role_view-users}", + "composite" : true, + "composites" : { + "client" : { + "realm-management" : [ "query-users", "query-groups" ] + } + }, + "clientRole" : true, + "containerId" : "cfffd5d0-aa19-4057-8ca0-f2c51ca0e930", + "attributes" : { } + }, { + "id" : "28824208-976e-4622-b4d7-3d18efbb46fa", + "name" : "realm-admin", + "description" : "${role_realm-admin}", + "composite" : true, + "composites" : { + "client" : { + "realm-management" : [ "query-realms", "view-identity-providers", "manage-identity-providers", "manage-authorization", "query-clients", "view-authorization", "view-users", "manage-users", "view-realm", "query-users", "view-clients", "query-groups", "create-client", "manage-clients", "manage-events", "impersonation", "view-events", "manage-realm" ] + } + }, + "clientRole" : true, + "containerId" : "cfffd5d0-aa19-4057-8ca0-f2c51ca0e930", + "attributes" : { } + }, { + "id" : "57e846a2-930d-4621-819d-c35086507146", + "name" : "manage-users", + "description" : "${role_manage-users}", + "composite" : false, + "clientRole" : true, + "containerId" : "cfffd5d0-aa19-4057-8ca0-f2c51ca0e930", + "attributes" : { } + }, { + "id" : "7fad9cde-bf96-475a-9174-14a87da51f95", + "name" : "view-realm", + "description" : "${role_view-realm}", + "composite" : false, + "clientRole" : true, + "containerId" : "cfffd5d0-aa19-4057-8ca0-f2c51ca0e930", + "attributes" : { } + }, { + "id" : "bbcac294-d78a-4ea1-a4bf-0384266d2fe1", + "name" : "query-users", + "description" : "${role_query-users}", + "composite" : false, + "clientRole" : true, + "containerId" : "cfffd5d0-aa19-4057-8ca0-f2c51ca0e930", + "attributes" : { } + }, { + "id" : "480e1437-ab9e-47de-b47a-edc6b6e285de", + "name" : "view-clients", + "description" : "${role_view-clients}", + "composite" : true, + "composites" : { + "client" : { + "realm-management" : [ "query-clients" ] + } + }, + "clientRole" : true, + "containerId" : "cfffd5d0-aa19-4057-8ca0-f2c51ca0e930", + "attributes" : { } + }, { + "id" : "b9a9a8f5-f91e-4e73-9e88-1cdf42bd49f9", + "name" : "create-client", + "description" : "${role_create-client}", + "composite" : false, + "clientRole" : true, + "containerId" : "cfffd5d0-aa19-4057-8ca0-f2c51ca0e930", + "attributes" : { } + }, { + "id" : "4d1397fb-247c-436f-b26f-124cd89afb08", + "name" : "query-groups", + "description" : "${role_query-groups}", + "composite" : false, + "clientRole" : true, + "containerId" : "cfffd5d0-aa19-4057-8ca0-f2c51ca0e930", + "attributes" : { } + }, { + "id" : "e31f522b-b283-4ae1-b875-52afcd98b1d2", + "name" : "impersonation", + "description" : "${role_impersonation}", + "composite" : false, + "clientRole" : true, + "containerId" : "cfffd5d0-aa19-4057-8ca0-f2c51ca0e930", + "attributes" : { } + }, { + "id" : "51822d02-fa28-4a49-89da-bc534719d8a8", + "name" : "manage-clients", + "description" : "${role_manage-clients}", + "composite" : false, + "clientRole" : true, + "containerId" : "cfffd5d0-aa19-4057-8ca0-f2c51ca0e930", + "attributes" : { } + }, { + "id" : "b2743ce5-0ce8-4157-ae00-f693560f0b39", + "name" : "manage-events", + "description" : "${role_manage-events}", + "composite" : false, + "clientRole" : true, + "containerId" : "cfffd5d0-aa19-4057-8ca0-f2c51ca0e930", + "attributes" : { } + }, { + "id" : "7ea3d7e0-9bf4-438a-b773-243daf622aaa", + "name" : "view-events", + "description" : "${role_view-events}", + "composite" : false, + "clientRole" : true, + "containerId" : "cfffd5d0-aa19-4057-8ca0-f2c51ca0e930", + "attributes" : { } + }, { + "id" : "fb73f6f5-0ed5-41d0-852c-0eb3b195b15a", + "name" : "manage-realm", + "description" : "${role_manage-realm}", + "composite" : false, + "clientRole" : true, + "containerId" : "cfffd5d0-aa19-4057-8ca0-f2c51ca0e930", + "attributes" : { } + } ], + "security-admin-console" : [ ], + "dbrepo-client" : [ ], + "admin-cli" : [ ], + "rabbitmq-client" : [ ], + "account-console" : [ ], + "broker" : [ { + "id" : "de0cfd5e-c2fe-4082-ac39-e3b092139a0f", + "name" : "read-token", + "description" : "${role_read-token}", + "composite" : false, + "clientRole" : true, + "containerId" : "88694c91-753d-4c44-9740-ec9ac06bba45", + "attributes" : { } + } ], + "account" : [ { + "id" : "acd78c04-eefc-4344-a5b4-3fc83d848936", + "name" : "delete-account", + "description" : "${role_delete-account}", + "composite" : false, + "clientRole" : true, + "containerId" : "e767a4a6-79e9-4e08-82b7-1076e1a09142", + "attributes" : { } + }, { + "id" : "939be844-8c49-45b3-9ca1-4b10a454b346", + "name" : "view-profile", + "description" : "${role_view-profile}", + "composite" : false, + "clientRole" : true, + "containerId" : "e767a4a6-79e9-4e08-82b7-1076e1a09142", + "attributes" : { } + }, { + "id" : "e52fdf00-3e73-4c17-bc1c-643493710a6b", + "name" : "view-applications", + "description" : "${role_view-applications}", + "composite" : false, + "clientRole" : true, + "containerId" : "e767a4a6-79e9-4e08-82b7-1076e1a09142", + "attributes" : { } + }, { + "id" : "b02a822e-a708-420a-bddc-1a315033fd7c", + "name" : "view-consent", + "description" : "${role_view-consent}", + "composite" : false, + "clientRole" : true, + "containerId" : "e767a4a6-79e9-4e08-82b7-1076e1a09142", + "attributes" : { } + }, { + "id" : "c590e5f5-2cbf-4151-b1dc-96c454f1f654", + "name" : "view-groups", + "description" : "${role_view-groups}", + "composite" : false, + "clientRole" : true, + "containerId" : "e767a4a6-79e9-4e08-82b7-1076e1a09142", + "attributes" : { } + }, { + "id" : "15974151-6c13-426b-8cc3-7683dd1311e1", + "name" : "manage-account-links", + "description" : "${role_manage-account-links}", + "composite" : false, + "clientRole" : true, + "containerId" : "e767a4a6-79e9-4e08-82b7-1076e1a09142", + "attributes" : { } + }, { + "id" : "c12d8d94-c2df-498e-bbe4-2f934a83ae92", + "name" : "manage-consent", + "description" : "${role_manage-consent}", + "composite" : true, + "composites" : { + "client" : { + "account" : [ "view-consent" ] + } + }, + "clientRole" : true, + "containerId" : "e767a4a6-79e9-4e08-82b7-1076e1a09142", + "attributes" : { } + }, { + "id" : "55f85811-bded-4d6b-8f7b-45844b963875", + "name" : "manage-account", + "description" : "${role_manage-account}", + "composite" : true, + "composites" : { + "client" : { + "account" : [ "manage-account-links" ] + } + }, + "clientRole" : true, + "containerId" : "e767a4a6-79e9-4e08-82b7-1076e1a09142", + "attributes" : { } + } ] + } + }, + "groups" : [ { + "id" : "f2ce17fe-7b15-47a4-bbf8-86f415298fa9", + "name" : "data-stewards", + "path" : "/data-stewards", + "attributes" : { }, + "realmRoles" : [ "default-data-steward-roles" ], + "clientRoles" : { }, + "subGroups" : [ ] + }, { + "id" : "124d9888-0b6e-46aa-8225-077dcedaf16e", + "name" : "developers", + "path" : "/developers", + "attributes" : { }, + "realmRoles" : [ "default-developer-roles" ], + "clientRoles" : { }, + "subGroups" : [ ] + }, { + "id" : "f467c38e-9041-4faa-ae0b-39cec65ff4db", + "name" : "researchers", + "path" : "/researchers", + "attributes" : { }, + "realmRoles" : [ "default-researcher-roles" ], + "clientRoles" : { }, + "subGroups" : [ ] + }, { + "id" : "2b9f94b4-d434-4a98-8eab-25678cfee983", + "name" : "system", + "path" : "/system", + "attributes" : { }, + "realmRoles" : [ "default-system-roles" ], + "clientRoles" : { }, + "subGroups" : [ ] + } ], + "defaultRole" : { + "id" : "abd2d9ee-ebc4-4d0a-839e-6b588a6d442a", + "name" : "default-roles-dbrepo", + "description" : "${role_default-roles}", + "composite" : false, + "clientRole" : false, + "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0" + }, + "defaultGroups" : [ "/researchers" ], + "requiredCredentials" : [ "password" ], + "otpPolicyType" : "totp", + "otpPolicyAlgorithm" : "HmacSHA1", + "otpPolicyInitialCounter" : 0, + "otpPolicyDigits" : 6, + "otpPolicyLookAheadWindow" : 1, + "otpPolicyPeriod" : 30, + "otpPolicyCodeReusable" : false, + "otpSupportedApplications" : [ "totpAppGoogleName", "totpAppFreeOTPName", "totpAppMicrosoftAuthenticatorName" ], + "webAuthnPolicyRpEntityName" : "keycloak", + "webAuthnPolicySignatureAlgorithms" : [ "ES256" ], + "webAuthnPolicyRpId" : "", + "webAuthnPolicyAttestationConveyancePreference" : "not specified", + "webAuthnPolicyAuthenticatorAttachment" : "not specified", + "webAuthnPolicyRequireResidentKey" : "not specified", + "webAuthnPolicyUserVerificationRequirement" : "not specified", + "webAuthnPolicyCreateTimeout" : 0, + "webAuthnPolicyAvoidSameAuthenticatorRegister" : false, + "webAuthnPolicyAcceptableAaguids" : [ ], + "webAuthnPolicyPasswordlessRpEntityName" : "keycloak", + "webAuthnPolicyPasswordlessSignatureAlgorithms" : [ "ES256" ], + "webAuthnPolicyPasswordlessRpId" : "", + "webAuthnPolicyPasswordlessAttestationConveyancePreference" : "not specified", + "webAuthnPolicyPasswordlessAuthenticatorAttachment" : "not specified", + "webAuthnPolicyPasswordlessRequireResidentKey" : "not specified", + "webAuthnPolicyPasswordlessUserVerificationRequirement" : "not specified", + "webAuthnPolicyPasswordlessCreateTimeout" : 0, + "webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister" : false, + "webAuthnPolicyPasswordlessAcceptableAaguids" : [ ], + "scopeMappings" : [ { + "clientScope" : "rabbitmq.tag:administrator", + "roles" : [ "escalated-broker-handling" ] + }, { + "clientScope" : "rabbitmq.tag:management", + "roles" : [ "default-broker-handling" ] + } ], + "clientScopeMappings" : { + "account" : [ { + "client" : "account-console", + "roles" : [ "manage-account", "view-groups" ] + } ] + }, + "clients" : [ { + "id" : "e767a4a6-79e9-4e08-82b7-1076e1a09142", + "clientId" : "account", + "name" : "${client_account}", + "rootUrl" : "${authBaseUrl}", + "baseUrl" : "/realms/dbrepo/account/", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "redirectUris" : [ "/realms/dbrepo/account/*" ], + "webOrigins" : [ ], + "notBefore" : 0, + "bearerOnly" : false, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : false, + "serviceAccountsEnabled" : false, + "publicClient" : true, + "frontchannelLogout" : false, + "protocol" : "openid-connect", + "attributes" : { + "post.logout.redirect.uris" : "+" + }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : false, + "nodeReRegistrationTimeout" : 0, + "defaultClientScopes" : [ "web-origins", "acr", "profile", "roles", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + }, { + "id" : "d3c4a04e-39ce-4549-a34a-11e25774cd96", + "clientId" : "account-console", + "name" : "${client_account-console}", + "rootUrl" : "${authBaseUrl}", + "baseUrl" : "/realms/dbrepo/account/", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "redirectUris" : [ "/realms/dbrepo/account/*" ], + "webOrigins" : [ ], + "notBefore" : 0, + "bearerOnly" : false, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : false, + "serviceAccountsEnabled" : false, + "publicClient" : true, + "frontchannelLogout" : false, + "protocol" : "openid-connect", + "attributes" : { + "post.logout.redirect.uris" : "+", + "pkce.code.challenge.method" : "S256" + }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : false, + "nodeReRegistrationTimeout" : 0, + "protocolMappers" : [ { + "id" : "22d90d9c-9881-474c-8dfd-a62c808a9f1c", + "name" : "audience resolve", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-audience-resolve-mapper", + "consentRequired" : false, + "config" : { } + } ], + "defaultClientScopes" : [ "web-origins", "acr", "profile", "roles", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + }, { + "id" : "81ef0f59-a5ca-4be4-a1d1-0c32edf1cfd6", + "clientId" : "admin-cli", + "name" : "${client_admin-cli}", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "redirectUris" : [ ], + "webOrigins" : [ ], + "notBefore" : 0, + "bearerOnly" : false, + "consentRequired" : false, + "standardFlowEnabled" : false, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : true, + "serviceAccountsEnabled" : false, + "publicClient" : true, + "frontchannelLogout" : false, + "protocol" : "openid-connect", + "attributes" : { + "post.logout.redirect.uris" : "+" + }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : false, + "nodeReRegistrationTimeout" : 0, + "defaultClientScopes" : [ "web-origins", "acr", "profile", "roles", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + }, { + "id" : "88694c91-753d-4c44-9740-ec9ac06bba45", + "clientId" : "broker", + "name" : "${client_broker}", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "redirectUris" : [ ], + "webOrigins" : [ ], + "notBefore" : 0, + "bearerOnly" : true, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : false, + "serviceAccountsEnabled" : false, + "publicClient" : false, + "frontchannelLogout" : false, + "protocol" : "openid-connect", + "attributes" : { + "post.logout.redirect.uris" : "+" + }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : false, + "nodeReRegistrationTimeout" : 0, + "defaultClientScopes" : [ "web-origins", "acr", "profile", "roles", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + }, { + "id" : "6b7ef364-4132-4831-b4e2-b6e9e9dc63ee", + "clientId" : "dbrepo-client", + "name" : "${dbrepo-client}", + "description" : "", + "rootUrl" : "", + "adminUrl" : "", + "baseUrl" : "", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : true, + "clientAuthenticatorType" : "client-secret", + "secret" : "MUwRc7yfXSJwX8AdRMWaQC3Nep1VjwgG", + "redirectUris" : [ "*" ], + "webOrigins" : [ "*" ], + "notBefore" : 0, + "bearerOnly" : false, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : true, + "serviceAccountsEnabled" : false, + "publicClient" : false, + "frontchannelLogout" : true, + "protocol" : "openid-connect", + "attributes" : { + "oidc.ciba.grant.enabled" : "false", + "client.secret.creation.time" : "1680085365", + "backchannel.logout.session.required" : "true", + "post.logout.redirect.uris" : "*", + "oauth2.device.authorization.grant.enabled" : "false", + "backchannel.logout.revoke.offline.tokens" : "false" + }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : true, + "nodeReRegistrationTimeout" : -1, + "protocolMappers" : [ { + "id" : "da0b27c1-ae2e-4baa-bf78-db233e15c78d", + "name" : "preferred_username", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-property-mapper", + "consentRequired" : false, + "config" : { + "user.attribute" : "username", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "preferred_username", + "userinfo.token.claim" : "true" + } + }, { + "id" : "7c94de93-f60f-487b-b4b7-1891c67f74cc", + "name" : "aud", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-hardcoded-claim-mapper", + "consentRequired" : false, + "config" : { + "claim.value" : "dbrepo", + "userinfo.token.claim" : "true", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "aud", + "access.tokenResponse.claim" : "false" + } + }, { + "id" : "030a1cd9-53d1-4a62-a375-94d50a2dc6fc", + "name" : "uid", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "aggregate.attrs" : "false", + "multivalued" : "false", + "userinfo.token.claim" : "true", + "user.attribute" : "LDAP_ID", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "uid" + } + } ], + "defaultClientScopes" : [ "roles", "attributes" ], + "optionalClientScopes" : [ "rabbitmq.read:*/*", "web-origins", "acr", "rabbitmq.write:*/*", "address", "phone", "offline_access", "profile", "microprofile-jwt", "email", "rabbitmq.configure:*/*" ] + }, { + "id" : "25741f6b-4867-4138-8238-6345c6ba8702", + "clientId" : "rabbitmq-client", + "name" : "${rabbitmq-client}", + "description" : "", + "rootUrl" : "", + "adminUrl" : "", + "baseUrl" : "", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "secret" : "JEC2FexxrX4N65fLeDGukAl6R3Lc9y0u", + "redirectUris" : [ "*" ], + "webOrigins" : [ ], + "notBefore" : 0, + "bearerOnly" : false, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : true, + "serviceAccountsEnabled" : false, + "publicClient" : false, + "frontchannelLogout" : true, + "protocol" : "openid-connect", + "attributes" : { + "oidc.ciba.grant.enabled" : "false", + "client.secret.creation.time" : "1680000860", + "backchannel.logout.session.required" : "true", + "post.logout.redirect.uris" : "*", + "oauth2.device.authorization.grant.enabled" : "false", + "backchannel.logout.revoke.offline.tokens" : "false" + }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : false, + "nodeReRegistrationTimeout" : -1, + "protocolMappers" : [ { + "id" : "01a937ed-f0e8-4137-80f3-3be3c447f7fb", + "name" : "username", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-property-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "false", + "user.attribute" : "username", + "id.token.claim" : "false", + "access.token.claim" : "true", + "claim.name" : "client_id", + "jsonType.label" : "String" + } + }, { + "id" : "f1afc22d-f595-403b-ba2e-6ab19d98205e", + "name" : "Audience", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-hardcoded-claim-mapper", + "consentRequired" : false, + "config" : { + "claim.value" : "rabbitmq", + "userinfo.token.claim" : "false", + "id.token.claim" : "false", + "access.token.claim" : "true", + "claim.name" : "aud", + "access.tokenResponse.claim" : "false" + } + } ], + "defaultClientScopes" : [ "web-origins", "acr", "rabbitmq.tag:management" ], + "optionalClientScopes" : [ "rabbitmq.read:*/*", "rabbitmq.write:*/*", "address", "phone", "offline_access", "profile", "roles", "microprofile-jwt", "email", "rabbitmq.configure:*/*" ] + }, { + "id" : "cfffd5d0-aa19-4057-8ca0-f2c51ca0e930", + "clientId" : "realm-management", + "name" : "${client_realm-management}", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "redirectUris" : [ ], + "webOrigins" : [ ], + "notBefore" : 0, + "bearerOnly" : true, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : false, + "serviceAccountsEnabled" : false, + "publicClient" : false, + "frontchannelLogout" : false, + "protocol" : "openid-connect", + "attributes" : { + "post.logout.redirect.uris" : "+" + }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : false, + "nodeReRegistrationTimeout" : 0, + "defaultClientScopes" : [ "web-origins", "acr", "profile", "roles", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + }, { + "id" : "f205c451-9524-4380-acc3-947f7ecb6b7c", + "clientId" : "security-admin-console", + "name" : "${client_security-admin-console}", + "rootUrl" : "${authAdminUrl}", + "baseUrl" : "/admin/dbrepo/console/", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "redirectUris" : [ "/admin/dbrepo/console/*" ], + "webOrigins" : [ "+" ], + "notBefore" : 0, + "bearerOnly" : false, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : false, + "serviceAccountsEnabled" : false, + "publicClient" : true, + "frontchannelLogout" : false, + "protocol" : "openid-connect", + "attributes" : { + "post.logout.redirect.uris" : "+", + "pkce.code.challenge.method" : "S256" + }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : false, + "nodeReRegistrationTimeout" : 0, + "protocolMappers" : [ { + "id" : "c4d54410-3f22-4259-9571-94da2c43b752", + "name" : "locale", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "locale", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "locale", + "jsonType.label" : "String" + } + } ], + "defaultClientScopes" : [ "web-origins", "acr", "profile", "roles", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + } ], + "clientScopes" : [ { + "id" : "69f4ecf0-4165-49ab-bf0d-38409b15b706", + "name" : "rabbitmq.tag:administrator", + "description" : "administrator", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "true", + "display.on.consent.screen" : "true", + "gui.order" : "", + "consent.screen.text" : "" + } + }, { + "id" : "7f6e9b44-e2eb-417d-b0fe-db820c9a6564", + "name" : "email", + "description" : "OpenID Connect built-in scope: email", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "true", + "display.on.consent.screen" : "true", + "consent.screen.text" : "${emailScopeConsentText}" + }, + "protocolMappers" : [ { + "id" : "782819fe-ba5d-4ddb-9f95-cabb69d79c8d", + "name" : "email verified", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-property-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "emailVerified", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "email_verified", + "jsonType.label" : "boolean" + } + }, { + "id" : "ca613fc8-bbf2-4240-8b33-a1874f1559f3", + "name" : "email", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-property-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "email", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "email", + "jsonType.label" : "String" + } + } ] + }, { + "id" : "b9da268f-6745-49dc-a764-3c54e385accc", + "name" : "profile", + "description" : "OpenID Connect built-in scope: profile", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "true", + "display.on.consent.screen" : "true", + "consent.screen.text" : "${profileScopeConsentText}" + }, + "protocolMappers" : [ { + "id" : "84f0487a-1d7d-470c-9b8e-5835294ae235", + "name" : "username", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-property-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "username", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "preferred_username", + "jsonType.label" : "String" + } + }, { + "id" : "bbdcdb36-3ec0-443d-b1af-9993d40f0567", + "name" : "gender", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "gender", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "gender", + "jsonType.label" : "String" + } + }, { + "id" : "9faa870b-5491-4ce9-b27d-c9ce07d6a95e", + "name" : "birthdate", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "birthdate", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "birthdate", + "jsonType.label" : "String" + } + }, { + "id" : "f0e3c012-9523-4076-83ae-e466e2d08220", + "name" : "full name", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-full-name-mapper", + "consentRequired" : false, + "config" : { + "id.token.claim" : "true", + "access.token.claim" : "true", + "userinfo.token.claim" : "true" + } + }, { + "id" : "f757d8ec-e181-429c-9287-9ad0600b061f", + "name" : "profile", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "profile", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "profile", + "jsonType.label" : "String" + } + }, { + "id" : "18cfbf4b-0a8e-45c7-a832-c0f72c92f3f3", + "name" : "updated at", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "updatedAt", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "updated_at", + "jsonType.label" : "long" + } + }, { + "id" : "841ea785-26ab-429a-a420-09ce3948924d", + "name" : "family name", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-property-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "lastName", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "family_name", + "jsonType.label" : "String" + } + }, { + "id" : "bfba13ff-f952-4e89-bbb1-a693fdebfae8", + "name" : "website", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "website", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "website", + "jsonType.label" : "String" + } + }, { + "id" : "475f071d-5149-4379-b928-76482f5f519c", + "name" : "zoneinfo", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "zoneinfo", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "zoneinfo", + "jsonType.label" : "String" + } + }, { + "id" : "b8bebfed-b5e9-4604-a0ee-9817f7d439ac", + "name" : "middle name", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "middleName", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "middle_name", + "jsonType.label" : "String" + } + }, { + "id" : "445232c8-6830-476c-a6f1-8bbef167595a", + "name" : "picture", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "picture", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "picture", + "jsonType.label" : "String" + } + }, { + "id" : "65f2e474-6ede-4872-86e4-e49504dd0f2a", + "name" : "locale", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "locale", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "locale", + "jsonType.label" : "String" + } + }, { + "id" : "16cd5a27-ccf3-453c-ae1e-8621813ab73c", + "name" : "given name", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-property-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "firstName", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "given_name", + "jsonType.label" : "String" + } + }, { + "id" : "f9efedfc-3388-457c-b10a-1dff4525ff9b", + "name" : "nickname", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "nickname", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "nickname", + "jsonType.label" : "String" + } + } ] + }, { + "id" : "627fa054-08eb-4206-af71-9e838e984b8b", + "name" : "microprofile-jwt", + "description" : "Microprofile - JWT built-in scope", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "true", + "display.on.consent.screen" : "false" + }, + "protocolMappers" : [ { + "id" : "e6cc53e5-5d7e-468e-88c8-0737dd3dc759", + "name" : "groups", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-realm-role-mapper", + "consentRequired" : false, + "config" : { + "multivalued" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "foo", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "groups", + "jsonType.label" : "String" + } + }, { + "id" : "83b4444c-10fc-44e8-a0c0-0c1da1f9bba3", + "name" : "upn", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-property-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "username", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "upn", + "jsonType.label" : "String" + } + } ] + }, { + "id" : "4122ff9e-ad3c-4142-afc6-9aefdecfc86d", + "name" : "role_list", + "description" : "SAML role list", + "protocol" : "saml", + "attributes" : { + "consent.screen.text" : "${samlRoleListScopeConsentText}", + "display.on.consent.screen" : "true" + }, + "protocolMappers" : [ { + "id" : "bb0747fa-c008-4af3-93be-e7739650ebd5", + "name" : "role list", + "protocol" : "saml", + "protocolMapper" : "saml-role-list-mapper", + "consentRequired" : false, + "config" : { + "single" : "false", + "attribute.nameformat" : "Basic", + "attribute.name" : "Role" + } + } ] + }, { + "id" : "2e76447d-fbe7-4fa7-a16c-54a381b960ae", + "name" : "rabbitmq.configure:*/*", + "description" : "", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "true", + "display.on.consent.screen" : "false", + "gui.order" : "", + "consent.screen.text" : "" + } + }, { + "id" : "52aad832-c6c4-49df-8a04-6ad4a406fdfa", + "name" : "phone", + "description" : "OpenID Connect built-in scope: phone", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "true", + "display.on.consent.screen" : "true", + "consent.screen.text" : "${phoneScopeConsentText}" + }, + "protocolMappers" : [ { + "id" : "dae802fb-9138-408a-b80e-a40eb0f56814", + "name" : "phone number", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "phoneNumber", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "phone_number", + "jsonType.label" : "String" + } + }, { + "id" : "feb06a8d-b0eb-4911-8464-368d93f566fa", + "name" : "phone number verified", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "phoneNumberVerified", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "phone_number_verified", + "jsonType.label" : "boolean" + } + } ] + }, { + "id" : "f64d64e8-57ce-4eb2-b99e-9f02fdbd99f9", + "name" : "web-origins", + "description" : "OpenID Connect scope for add allowed web origins to the access token", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "false", + "display.on.consent.screen" : "false", + "consent.screen.text" : "" + }, + "protocolMappers" : [ { + "id" : "c6411e3b-6478-453d-b530-5fe175a4d786", + "name" : "allowed web origins", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-allowed-origins-mapper", + "consentRequired" : false, + "config" : { } + } ] + }, { + "id" : "55341d34-0086-4173-ae61-d9b175b179d8", + "name" : "acr", + "description" : "OpenID Connect scope for add acr (authentication context class reference) to the token", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "false", + "display.on.consent.screen" : "false" + }, + "protocolMappers" : [ { + "id" : "58ea3217-0fff-4207-9d08-919f5493b629", + "name" : "acr loa level", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-acr-mapper", + "consentRequired" : false, + "config" : { + "id.token.claim" : "true", + "access.token.claim" : "true", + "userinfo.token.claim" : "true" + } + } ] + }, { + "id" : "a02c2c38-923c-46ec-9899-321412b388e5", + "name" : "attributes", + "description" : "User Attributes", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "false", + "display.on.consent.screen" : "false", + "gui.order" : "", + "consent.screen.text" : "" + }, + "protocolMappers" : [ { + "id" : "78c461c1-f3f9-4d10-8835-097f13bdcd60", + "name" : "Theme", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "aggregate.attrs" : "false", + "multivalued" : "false", + "userinfo.token.claim" : "true", + "user.attribute" : "theme_dark", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "attributes.theme_dark" + } + } ] + }, { + "id" : "06062e22-89c0-4e1d-a25b-2483903b02d5", + "name" : "rabbitmq.write:*/*", + "description" : "", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "true", + "display.on.consent.screen" : "false", + "gui.order" : "", + "consent.screen.text" : "" + } + }, { + "id" : "db63e03b-7918-492f-997b-f2dda98f3b39", + "name" : "rabbitmq.tag:management", + "description" : "management", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "true", + "display.on.consent.screen" : "true", + "gui.order" : "", + "consent.screen.text" : "" + } + }, { + "id" : "210cc792-6c07-45a6-a77e-827cdf3b41ba", + "name" : "offline_access", + "description" : "OpenID Connect built-in scope: offline_access", + "protocol" : "openid-connect", + "attributes" : { + "consent.screen.text" : "${offlineAccessScopeConsentText}", + "display.on.consent.screen" : "true" + } + }, { + "id" : "425abf4a-2ee2-431d-aa92-e373a36fe556", + "name" : "address", + "description" : "OpenID Connect built-in scope: address", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "true", + "display.on.consent.screen" : "true", + "consent.screen.text" : "${addressScopeConsentText}" + }, + "protocolMappers" : [ { + "id" : "8d4ffe4d-1d01-4ca1-8ff4-44eacca61b30", + "name" : "address", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-address-mapper", + "consentRequired" : false, + "config" : { + "user.attribute.formatted" : "formatted", + "user.attribute.country" : "country", + "user.attribute.postal_code" : "postal_code", + "userinfo.token.claim" : "true", + "user.attribute.street" : "street", + "id.token.claim" : "true", + "user.attribute.region" : "region", + "access.token.claim" : "true", + "user.attribute.locality" : "locality" + } + } ] + }, { + "id" : "c96f0b73-ea79-4b46-93ef-d1092297f855", + "name" : "rabbitmq.read:*/*", + "description" : "", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "true", + "display.on.consent.screen" : "false", + "gui.order" : "", + "consent.screen.text" : "" + } + }, { + "id" : "37f61543-dad7-4a82-8e10-77acdd1eefdc", + "name" : "roles", + "description" : "OpenID Connect scope for add user roles to the access token", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "false", + "display.on.consent.screen" : "true", + "consent.screen.text" : "${rolesScopeConsentText}" + }, + "protocolMappers" : [ { + "id" : "3b6b6914-8ad1-4a71-88ec-444f754aaacb", + "name" : "audience resolve", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-audience-resolve-mapper", + "consentRequired" : false, + "config" : { } + }, { + "id" : "2defedf5-9af3-4531-822c-a879dedcd29d", + "name" : "realm roles", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-realm-role-mapper", + "consentRequired" : false, + "config" : { + "user.attribute" : "foo", + "access.token.claim" : "true", + "claim.name" : "realm_access.roles", + "jsonType.label" : "String", + "multivalued" : "true" + } + }, { + "id" : "a7bd6723-e58e-47f7-95c0-2925ce99283d", + "name" : "client roles", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-client-role-mapper", + "consentRequired" : false, + "config" : { + "user.attribute" : "foo", + "access.token.claim" : "true", + "claim.name" : "resource_access.${client_id}.roles", + "jsonType.label" : "String", + "multivalued" : "true" + } + } ] + } ], + "defaultDefaultClientScopes" : [ "rabbitmq.tag:administrator", "rabbitmq.tag:management" ], + "defaultOptionalClientScopes" : [ "rabbitmq.write:*/*", "offline_access", "rabbitmq.configure:*/*", "roles", "role_list", "address", "phone", "acr", "microprofile-jwt", "email", "attributes", "profile", "rabbitmq.read:*/*", "web-origins" ], + "browserSecurityHeaders" : { + "contentSecurityPolicyReportOnly" : "", + "xContentTypeOptions" : "nosniff", + "xRobotsTag" : "none", + "xFrameOptions" : "SAMEORIGIN", + "contentSecurityPolicy" : "frame-src 'self'; frame-ancestors 'self'; object-src 'none';", + "xXSSProtection" : "1; mode=block", + "strictTransportSecurity" : "max-age=31536000; includeSubDomains" + }, + "smtpServer" : { }, + "eventsEnabled" : false, + "eventsListeners" : [ "jboss-logging" ], + "enabledEventTypes" : [ "SEND_RESET_PASSWORD", "UPDATE_CONSENT_ERROR", "GRANT_CONSENT", "VERIFY_PROFILE_ERROR", "REMOVE_TOTP", "REVOKE_GRANT", "UPDATE_TOTP", "LOGIN_ERROR", "CLIENT_LOGIN", "RESET_PASSWORD_ERROR", "IMPERSONATE_ERROR", "CODE_TO_TOKEN_ERROR", "CUSTOM_REQUIRED_ACTION", "OAUTH2_DEVICE_CODE_TO_TOKEN_ERROR", "RESTART_AUTHENTICATION", "IMPERSONATE", "UPDATE_PROFILE_ERROR", "LOGIN", "OAUTH2_DEVICE_VERIFY_USER_CODE", "UPDATE_PASSWORD_ERROR", "CLIENT_INITIATED_ACCOUNT_LINKING", "TOKEN_EXCHANGE", "AUTHREQID_TO_TOKEN", "LOGOUT", "REGISTER", "DELETE_ACCOUNT_ERROR", "CLIENT_REGISTER", "IDENTITY_PROVIDER_LINK_ACCOUNT", "DELETE_ACCOUNT", "UPDATE_PASSWORD", "CLIENT_DELETE", "FEDERATED_IDENTITY_LINK_ERROR", "IDENTITY_PROVIDER_FIRST_LOGIN", "CLIENT_DELETE_ERROR", "VERIFY_EMAIL", "CLIENT_LOGIN_ERROR", "RESTART_AUTHENTICATION_ERROR", "EXECUTE_ACTIONS", "REMOVE_FEDERATED_IDENTITY_ERROR", "TOKEN_EXCHANGE_ERROR", "PERMISSION_TOKEN", "SEND_IDENTITY_PROVIDER_LINK_ERROR", "EXECUTE_ACTION_TOKEN_ERROR", "SEND_VERIFY_EMAIL", "OAUTH2_DEVICE_AUTH", "EXECUTE_ACTIONS_ERROR", "REMOVE_FEDERATED_IDENTITY", "OAUTH2_DEVICE_CODE_TO_TOKEN", "IDENTITY_PROVIDER_POST_LOGIN", "IDENTITY_PROVIDER_LINK_ACCOUNT_ERROR", "OAUTH2_DEVICE_VERIFY_USER_CODE_ERROR", "UPDATE_EMAIL", "REGISTER_ERROR", "REVOKE_GRANT_ERROR", "EXECUTE_ACTION_TOKEN", "LOGOUT_ERROR", "UPDATE_EMAIL_ERROR", "CLIENT_UPDATE_ERROR", "AUTHREQID_TO_TOKEN_ERROR", "UPDATE_PROFILE", "CLIENT_REGISTER_ERROR", "FEDERATED_IDENTITY_LINK", "SEND_IDENTITY_PROVIDER_LINK", "SEND_VERIFY_EMAIL_ERROR", "RESET_PASSWORD", "CLIENT_INITIATED_ACCOUNT_LINKING_ERROR", "OAUTH2_DEVICE_AUTH_ERROR", "UPDATE_CONSENT", "REMOVE_TOTP_ERROR", "VERIFY_EMAIL_ERROR", "SEND_RESET_PASSWORD_ERROR", "CLIENT_UPDATE", "CUSTOM_REQUIRED_ACTION_ERROR", "IDENTITY_PROVIDER_POST_LOGIN_ERROR", "UPDATE_TOTP_ERROR", "CODE_TO_TOKEN", "VERIFY_PROFILE", "GRANT_CONSENT_ERROR", "IDENTITY_PROVIDER_FIRST_LOGIN_ERROR" ], + "adminEventsEnabled" : false, + "adminEventsDetailsEnabled" : false, + "identityProviders" : [ ], + "identityProviderMappers" : [ ], + "components" : { + "org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy" : [ { + "id" : "4d3f9f14-f5d2-4b0c-8ea7-e6d078aa2191", + "name" : "Max Clients Limit", + "providerId" : "max-clients", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { + "max-clients" : [ "200" ] + } + }, { + "id" : "f35bce67-1e75-408b-b065-52183368d4fd", + "name" : "Allowed Client Scopes", + "providerId" : "allowed-client-templates", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { + "allow-default-scopes" : [ "true" ] + } + }, { + "id" : "1849e52a-b8c9-44a8-af3d-ee19376a1ed1", + "name" : "Trusted Hosts", + "providerId" : "trusted-hosts", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { + "host-sending-registration-request-must-match" : [ "true" ], + "client-uris-must-match" : [ "true" ] + } + }, { + "id" : "f565cb47-3bcf-4078-8f94-eb4179c375b8", + "name" : "Full Scope Disabled", + "providerId" : "scope", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { } + }, { + "id" : "0efa669d-1017-4b4a-82e1-c2eaf72de2c9", + "name" : "Allowed Client Scopes", + "providerId" : "allowed-client-templates", + "subType" : "authenticated", + "subComponents" : { }, + "config" : { + "allow-default-scopes" : [ "true" ] + } + }, { + "id" : "528fb423-d66e-472e-9120-1f03ba9e0f18", + "name" : "Consent Required", + "providerId" : "consent-required", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { } + }, { + "id" : "104ec5a9-025b-4c44-8ac0-82d22887ca3e", + "name" : "Allowed Protocol Mapper Types", + "providerId" : "allowed-protocol-mappers", + "subType" : "authenticated", + "subComponents" : { }, + "config" : { + "allowed-protocol-mapper-types" : [ "oidc-address-mapper", "saml-user-property-mapper", "saml-role-list-mapper", "oidc-usermodel-property-mapper", "oidc-usermodel-attribute-mapper", "saml-user-attribute-mapper", "oidc-sha256-pairwise-sub-mapper", "oidc-full-name-mapper" ] + } + }, { + "id" : "3ab11d74-5e76-408a-b85a-26bf8950f979", + "name" : "Allowed Protocol Mapper Types", + "providerId" : "allowed-protocol-mappers", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { + "allowed-protocol-mapper-types" : [ "oidc-full-name-mapper", "oidc-sha256-pairwise-sub-mapper", "oidc-usermodel-attribute-mapper", "saml-role-list-mapper", "oidc-address-mapper", "oidc-usermodel-property-mapper", "saml-user-property-mapper", "saml-user-attribute-mapper" ] + } + } ], + "org.keycloak.keys.KeyProvider" : [ { + "id" : "28ca0b6d-b2e2-4785-b04b-2391e6344e30", + "name" : "aes-generated", + "providerId" : "aes-generated", + "subComponents" : { }, + "config" : { + "kid" : [ "6dc4834f-a1de-4cfe-a29d-e84ac8e9b1a8" ], + "secret" : [ "HpuzG_jWYKwypLeoPEMC4A" ], + "priority" : [ "100" ] + } + }, { + "id" : "bd7945cf-6d35-4e03-9c3a-197f2dc76973", + "name" : "hmac-generated", + "providerId" : "hmac-generated", + "subComponents" : { }, + "config" : { + "kid" : [ "c8500166-5cc4-4085-ad0f-853c3b0b0233" ], + "secret" : [ "TI3xg__G2Qy8C47DracpYir2X4ItQZSrhgr5KSlwRNISDbBqZ-ky3OcAyokSXMcpweSOaCPvbivpvzJNklUBvw" ], + "priority" : [ "100" ], + "algorithm" : [ "HS256" ] + } + }, { + "id" : "2f53ccf3-37b0-4d34-83e7-ed497499ee51", + "name" : "rsa-enc-generated", + "providerId" : "rsa-enc-generated", + "subComponents" : { }, + "config" : { + "privateKey" : [ "MIIEowIBAAKCAQEA3b1tNLfcjFLUw9UShVDNf+ZD8sQqb4YBaIXcSJTX/zDQUPiCp176BBGI3s4VplDArnOW+LumozmKogeoHEnGEIDW8ovgK5uMU9tSA2p0qqGBUMOdR8YATTIfCJe7qGiiuGa3WZy3sQLM70SuRzx02YU8gvUcvl2Js4KyqAziOUX/w3Wa59H9jjGNUXYyqaPWJp73eHzbVYWySzyLG22mVlcUtBx5siL5T2/Xu0p9z4l7/bapwwmOVi1ZrcHjbEAwdGEiSMGI/uWqAF+r1BRpmJLR7HNXcL3eK4/56VYLaiwSejfyYeRFMITEn/UxGYhcXZ5xMUUCG0TxjBhLYpTBuwIDAQABAoIBAA4dwebcxkrH99Poa8+WkiE7JgaS9sahx9OBI2xwJANoIU2TpzGuNLQZ76uLgB+rPWZTD9Xm5a1iJjwOyQ9/937TzPCk91D0tpgcusRikb8jx/6TGB9acL4kBjYUVCCHr3BA2G75MKKGtJ2OMvAbCQSosZj+r2VDwYFEPUkV2jheE5JHSBkwyIRrus3JCwu8gu5fyCg9z8ljcxJxI5HIsi4v8Z21aCw/cLj7h5cMt44wCjQz4rOfYNBEFeHDtlfR1QtWKgjm4ZHHJbKrzf9b2kQXclziceEbSM0tYbROEXKi+s0Zc+z3HEG89vv0vfN400clmzzIAijKY6gg3pPRWdECgYEA+lnWYbSlXDMNYx6RBXm1RnlMUYIm4oy4/9ljgnoGJ6WCn3SjFkgaDtiKfGIG1BSB85r04pAPANgcWHf5tWDnq0ARvBVG0BX2bKd++7B3D4d3CRYKCwm88SslJXv9dfHVhq4+zViFPiUWwT20A72jCuUCvL88y5fh/YBecfdh+jECgYEA4r5RD0NB9dMaeg5/jk/GEHIo4Z9KLc6FrSoOFo2xFkPOy1sgDpDOiNtypuWvniO7k7Ose3DS3hlfTMsKzIW/CgQJ20+Y4cvBWDaOsRxfjj7w3d+jH5OSJdKKSzTrgLKc9ZhlRzVXy0J0hipIA6HG5kdVdLXmh85ITmf1CbJhE6sCgYBjPVeBNbXTHZ2x6/z62aslO5IoQVqetb/kE82hfDOSZcao5Ph9Lam+ttH2ynkAevykj4mBgi+gWwqpey2uW7KaLPSaxShj9kDQA3mP1fzsV/u0y1rB02Nlin/YIxVvOqU1FT9p8SwoXVVu1sHUNck62VtDbN9xqUx5S/ikXrclEQKBgQCoTssOwEcK+Vty9KYcdfy4onTUHZBLdjxl8Iyqkxy7QTQUYRznkvesQPDXEDGO+kk3dyx2KKZt9Hl4IFNww2quPZcvcuMx4DQxjbXXpA8OIIxcta95NepLJwA+mRai3nKCH1A2TlNP7pFeMa5o+8IPly3Ix2lKr4Wepa4PN5i1pwKBgCZ1QP6XAOERl9NznNmU0rXVcvYNP4PIIfQWfvGsldZ4QKkmjjAGiS0/oYqdWs+UDRZyCRChaVjDXO9fk0PEG5OGKAj9nyiYCT/M8xtJ3UeP5ffZZvJ/vnye3QdDIo1e38ZzsWwJHmLYw7fRqY9W5Vxo0Vsy22U3CJY70KTxVdTy" ], + "keyUse" : [ "ENC" ], + "certificate" : [ "MIICmzCCAYMCBgGG3GWycDANBgkqhkiG9w0BAQsFADARMQ8wDQYDVQQDDAZkYnJlcG8wHhcNMjMwMzEzMTkxMzE3WhcNMzMwMzEzMTkxNDU3WjARMQ8wDQYDVQQDDAZkYnJlcG8wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDdvW00t9yMUtTD1RKFUM1/5kPyxCpvhgFohdxIlNf/MNBQ+IKnXvoEEYjezhWmUMCuc5b4u6ajOYqiB6gcScYQgNbyi+Arm4xT21IDanSqoYFQw51HxgBNMh8Il7uoaKK4ZrdZnLexAszvRK5HPHTZhTyC9Ry+XYmzgrKoDOI5Rf/DdZrn0f2OMY1RdjKpo9Ymnvd4fNtVhbJLPIsbbaZWVxS0HHmyIvlPb9e7Sn3PiXv9tqnDCY5WLVmtweNsQDB0YSJIwYj+5aoAX6vUFGmYktHsc1dwvd4rj/npVgtqLBJ6N/Jh5EUwhMSf9TEZiFxdnnExRQIbRPGMGEtilMG7AgMBAAEwDQYJKoZIhvcNAQELBQADggEBAK3kQ1VkQrzvSWvmXmazmNoA1ZiPzRDs1XhGUWxgsxzgPylr3dGBuqQbKvgnLUBQLSqlJHpI4fZflHswu1qrvVZYtekPcGef4WhcKAu2i1RwxrKa6RJQ1tRbrLuVYCzPv5p/DWgltWVn88aoLnqQn0SK/0PB/o4a4Cm7Kq2ZzCr1dACBr06LvOHsc7249OySmbG4HH+pLK6jVURhZ9VaObqAHe2FJBVVoIzURbdiRRURqumrIvbnpeaU1aFyg6ED5wTnXvmMPmVPt9F79mcB33bASO5wyu00X8t1hyN2Show2l2vxLACGUzVkTQt15s7uDLKE7qLmKSR3EuSGXWv3wA=" ], + "priority" : [ "100" ], + "algorithm" : [ "RSA-OAEP" ] + } + }, { + "id" : "2293ff99-3c6d-46d1-8635-5e679d5b134a", + "name" : "rsa-generated", + "providerId" : "rsa-generated", + "subComponents" : { }, + "config" : { + "privateKey" : [ "MIIEpAIBAAKCAQEAqqnHQ2BWWW9vDNLRCcxD++xZg/16oqMo/c1l+lcFEjjAIJjJp/HqrPYU/U9GvquGE6PbVFtTzW1KcKawOW+FJNOA3CGo8Q1TFEfz43B8rZpKsFbJKvQGVv1Z4HaKPvLUm7iMm8Hv91cLduuoWx6Q3DPe2vg13GKKEZe7UFghF+0T9u8EKzA/XqQ0OiICmsmYPbwvf9N3bCKsB/Y10EYmZRb8IhCoV9mmO5TxgWgiuNeCTtNCv2ePYqL/U0WvyGFW0reasIK8eg3KrAUj8DpyOgPOVBn3lBGf+3KFSYi+0bwZbJZWqbC/Xlk20Go1YfeJPRIt7ImxD27R/lNjgDO/MwIDAQABAoIBADNcMt6hAHub4JTAYS6Mra0EPRBO2XhWmACBrv3+8ETClXd5475KPLDewgRVtlmtbwU8G8awUXESQgPS9lfiqvQhPreA3cHlm6oP2WMKOEtakr2s8I+frsTBLCo0Ini9RaSzjoVVgS0zofyhASKi+T970MafSj5P3XNb8YBFdXgoYDiA7FXLH6a/+m7LScL+wGcFMAAeYESxZbMQLfH3v8L+4EcTraiwjLG17ZdlF3dpybMyUSse6ZQ/PdlyvBuzzLXhN6Ce2gd9ATfS+YWTzo7Yf+GU+ex5bIpVOfHqtuM/hyq7YGKENClsXwNZIAoFnvGCbvECAfgyapVrD30IfykCgYEA0rgsSZ82pxT40NxwgBD1g9lbNVBKXphRB/3S078qusUzJjT7AldEj4imGPhAbI7bI8gAeWJsp1XJWkjM8ktaVrh+NQl7p8e9OPh0pQF/5Bdg8ajbjXESpjnaU66pVYRQy/d+jNli/YRAHX5RUfsBl+6W4+WSVMGmKBiqJsur+ecCgYEAz1YVXClcmUnyZem5B+2E9noIzjF6ROE+jIb6rawM85P3Xd0lXtECQavtxw+Qk7I32qOwrxl1UpK2foVel3pazi+4OpMfmqtYGenRP1Zk1cZwrDo0cIemTDGjj3kJ8tYn12CGolFQpJZgK6OHzvG0tOxI5VZgjIViWNPe1PGWXtUCgYEAxXGNDe8BZs1f11S2lUlOw5yGug3hoYFXbAWJ5p7Ziuf8ZXB/QlJDC7se54a11wKEk6Jzz0lKRgE8CjzszJuOqnN0zn10QGIIC7nCklo1W6QMUmPGVWH994N976tZP6gbjQL6sT+AYcvpx7j0ubxYYeRNvnz+ACzzY964kGGHY0ECgYEAumlwPPNnMN7+VEjGNm2D7UMdJZ3wi3tkjF5ThdA5uMohTsAk+FG80KSu3RmOaGyEsUwY7+VYyYvlDm4E9PZqLBVVczyR3rMNPAcwPd0EPfvzk7WlLkOX7ct3fehaXH3VRlyfz9KCSeh1wOZ/lT1VtpD2nVOC7PSDzs92+kfXZZ0CgYAnrD1y4skgXkdwolZ3unn3EFyGm2d+X5aMTHwQPdWxqoNIAl/9wdghlzihwnPhhsxq1WzlxuC3V2IMrNPtRx70Mi+FbSmR5m4Xx5RptgMtMlwno+L40PzNJgMjHGjt0wcx3Vel8wuohDtnqMyS7P5nG1/TQx0Cyzwn7QOXlNpgbQ==" ], + "keyUse" : [ "SIG" ], + "certificate" : [ "MIICmzCCAYMCBgGG3GWyBTANBgkqhkiG9w0BAQsFADARMQ8wDQYDVQQDDAZkYnJlcG8wHhcNMjMwMzEzMTkxMzE3WhcNMzMwMzEzMTkxNDU3WjARMQ8wDQYDVQQDDAZkYnJlcG8wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCqqcdDYFZZb28M0tEJzEP77FmD/Xqioyj9zWX6VwUSOMAgmMmn8eqs9hT9T0a+q4YTo9tUW1PNbUpwprA5b4Uk04DcIajxDVMUR/PjcHytmkqwVskq9AZW/Vngdoo+8tSbuIybwe/3Vwt266hbHpDcM97a+DXcYooRl7tQWCEX7RP27wQrMD9epDQ6IgKayZg9vC9/03dsIqwH9jXQRiZlFvwiEKhX2aY7lPGBaCK414JO00K/Z49iov9TRa/IYVbSt5qwgrx6DcqsBSPwOnI6A85UGfeUEZ/7coVJiL7RvBlsllapsL9eWTbQajVh94k9Ei3sibEPbtH+U2OAM78zAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAASnN1Cuif1sdfEK2kWAURSXGJCohCROLWdKFjaeHPRaEfpbFJsgxW0Yj3nwX5O3bUlOWoTyENwnXSsXMQsqnNi+At32CKaKO8+AkhAbgQL9F0B+KeJwmYv3cUj5N/LYkJjBvZBzUZ4Ugu5dcxH0k7AktLAIwimkyEnxTNolOA3UyrGGpREr8MCKWVr10RFuOpF/0CsJNNwbHXzalO9D756EUcRWZ9VSg6QVNso0YYRKTnILWDn9hcTRnqGy3SHo3anFTqQZ+BB57YbgFWy6udC0LYRB3zdp6zNti87eu/VEymiDY/mmo1AB8Tm0b6vxFz4AKcL3ax5qS6YnZ9efSzk=" ], + "priority" : [ "100" ] + } + } ] + }, + "internationalizationEnabled" : false, + "supportedLocales" : [ ], + "authenticationFlows" : [ { + "id" : "88e5d526-2298-413c-a904-133ad839d47f", + "alias" : "Account verification options", + "description" : "Method with which to verity the existing account", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "idp-email-verification", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "ALTERNATIVE", + "priority" : 20, + "autheticatorFlow" : true, + "flowAlias" : "Verify Existing Account by Re-authentication", + "userSetupAllowed" : false + } ] + }, { + "id" : "bc0b483f-4a3f-4c15-bf65-b26f5320e6c9", + "alias" : "Authentication Options", + "description" : "Authentication options.", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "basic-auth", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "basic-auth-otp", + "authenticatorFlow" : false, + "requirement" : "DISABLED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "auth-spnego", + "authenticatorFlow" : false, + "requirement" : "DISABLED", + "priority" : 30, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + }, { + "id" : "a690c715-fbae-4c20-b680-bd4010718761", + "alias" : "Browser - Conditional OTP", + "description" : "Flow to determine if the OTP is required for the authentication", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "conditional-user-configured", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "auth-otp-form", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + }, { + "id" : "ad6d407e-c73e-4439-baf3-d7c99c6cb6ad", + "alias" : "Direct Grant - Conditional OTP", + "description" : "Flow to determine if the OTP is required for the authentication", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "conditional-user-configured", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "direct-grant-validate-otp", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + }, { + "id" : "e5d03405-e10a-408a-adb2-41dbb4f24515", + "alias" : "First broker login - Conditional OTP", + "description" : "Flow to determine if the OTP is required for the authentication", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "conditional-user-configured", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "auth-otp-form", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + }, { + "id" : "96b93843-62d0-44f1-84dd-21cc5f95f523", + "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", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "idp-confirm-link", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : true, + "flowAlias" : "Account verification options", + "userSetupAllowed" : false + } ] + }, { + "id" : "088f4051-36ab-4952-a4f2-4ba53c408083", + "alias" : "Reset - Conditional OTP", + "description" : "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "conditional-user-configured", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "reset-otp", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + }, { + "id" : "05f37bb2-779d-4e3f-ad1b-f6eb33bb3de4", + "alias" : "User creation or linking", + "description" : "Flow for the existing/non-existing user alternatives", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticatorConfig" : "create unique user config", + "authenticator" : "idp-create-user-if-unique", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "ALTERNATIVE", + "priority" : 20, + "autheticatorFlow" : true, + "flowAlias" : "Handle Existing Account", + "userSetupAllowed" : false + } ] + }, { + "id" : "300a5647-7d2c-4348-9f1f-51504bfda1c4", + "alias" : "Verify Existing Account by Re-authentication", + "description" : "Reauthentication of existing account", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "idp-username-password-form", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "CONDITIONAL", + "priority" : 20, + "autheticatorFlow" : true, + "flowAlias" : "First broker login - Conditional OTP", + "userSetupAllowed" : false + } ] + }, { + "id" : "26afc672-314b-4ad9-9711-7aaeafd7c00c", + "alias" : "browser", + "description" : "browser based authentication", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "auth-cookie", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "auth-spnego", + "authenticatorFlow" : false, + "requirement" : "DISABLED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "identity-provider-redirector", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 25, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "ALTERNATIVE", + "priority" : 30, + "autheticatorFlow" : true, + "flowAlias" : "forms", + "userSetupAllowed" : false + } ] + }, { + "id" : "9b301f6c-eda7-4da0-ba09-1a6454ff910d", + "alias" : "clients", + "description" : "Base authentication for clients", + "providerId" : "client-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "client-secret", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "client-jwt", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "client-secret-jwt", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 30, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "client-x509", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 40, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + }, { + "id" : "6e54f1be-dbad-4b6d-8eee-8e048d413c63", + "alias" : "direct grant", + "description" : "OpenID Connect Resource Owner Grant", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "direct-grant-validate-username", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "direct-grant-validate-password", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "CONDITIONAL", + "priority" : 30, + "autheticatorFlow" : true, + "flowAlias" : "Direct Grant - Conditional OTP", + "userSetupAllowed" : false + } ] + }, { + "id" : "31da4b94-03c4-4d79-9ac3-5df1445c0781", + "alias" : "docker auth", + "description" : "Used by Docker clients to authenticate against the IDP", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "docker-http-basic-authenticator", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + }, { + "id" : "2e16651d-681f-4d9b-9dd4-9acdb465cd43", + "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", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticatorConfig" : "review profile config", + "authenticator" : "idp-review-profile", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : true, + "flowAlias" : "User creation or linking", + "userSetupAllowed" : false + } ] + }, { + "id" : "da109a26-fefa-48a4-ae8e-1d49627c2db8", + "alias" : "forms", + "description" : "Username, password, otp and other auth forms.", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "auth-username-password-form", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "CONDITIONAL", + "priority" : 20, + "autheticatorFlow" : true, + "flowAlias" : "Browser - Conditional OTP", + "userSetupAllowed" : false + } ] + }, { + "id" : "b8f1f963-6813-4875-bae8-ce48a813763b", + "alias" : "http challenge", + "description" : "An authentication flow based on challenge-response HTTP Authentication Schemes", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "no-cookie-redirect", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : true, + "flowAlias" : "Authentication Options", + "userSetupAllowed" : false + } ] + }, { + "id" : "4c983c77-241f-41c5-8b8a-e2cd6fc08914", + "alias" : "registration", + "description" : "registration flow", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "registration-page-form", + "authenticatorFlow" : true, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : true, + "flowAlias" : "registration form", + "userSetupAllowed" : false + } ] + }, { + "id" : "d62c8dd6-633c-408a-aa99-43071510efb4", + "alias" : "registration form", + "description" : "registration form", + "providerId" : "form-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "registration-user-creation", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "registration-profile-action", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 40, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "registration-password-action", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 50, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "registration-recaptcha-action", + "authenticatorFlow" : false, + "requirement" : "DISABLED", + "priority" : 60, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + }, { + "id" : "c8ca5be7-e76d-4e16-b5ca-3ced99d92dbb", + "alias" : "reset credentials", + "description" : "Reset credentials for a user if they forgot their password or something", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "reset-credentials-choose-user", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "reset-credential-email", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "reset-password", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 30, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "CONDITIONAL", + "priority" : 40, + "autheticatorFlow" : true, + "flowAlias" : "Reset - Conditional OTP", + "userSetupAllowed" : false + } ] + }, { + "id" : "389c1c37-e8af-4610-a507-e1257f55b954", + "alias" : "saml ecp", + "description" : "SAML ECP Profile Authentication Flow", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "http-basic-authenticator", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + } ], + "authenticatorConfig" : [ { + "id" : "d66ca9d0-1645-4c84-abfe-c0a696f17de4", + "alias" : "create unique user config", + "config" : { + "require.password.update.after.registration" : "false" + } + }, { + "id" : "061cc6b8-90be-4423-9bf9-974ead709b5d", + "alias" : "review profile config", + "config" : { + "update.profile.on.first.login" : "missing" + } + } ], + "requiredActions" : [ { + "alias" : "CONFIGURE_TOTP", + "name" : "Configure OTP", + "providerId" : "CONFIGURE_TOTP", + "enabled" : true, + "defaultAction" : false, + "priority" : 10, + "config" : { } + }, { + "alias" : "TERMS_AND_CONDITIONS", + "name" : "Terms and Conditions", + "providerId" : "TERMS_AND_CONDITIONS", + "enabled" : false, + "defaultAction" : false, + "priority" : 20, + "config" : { } + }, { + "alias" : "UPDATE_PASSWORD", + "name" : "Update Password", + "providerId" : "UPDATE_PASSWORD", + "enabled" : false, + "defaultAction" : false, + "priority" : 30, + "config" : { } + }, { + "alias" : "UPDATE_PROFILE", + "name" : "Update Profile", + "providerId" : "UPDATE_PROFILE", + "enabled" : true, + "defaultAction" : false, + "priority" : 40, + "config" : { } + }, { + "alias" : "VERIFY_EMAIL", + "name" : "Verify Email", + "providerId" : "VERIFY_EMAIL", + "enabled" : false, + "defaultAction" : false, + "priority" : 50, + "config" : { } + }, { + "alias" : "delete_account", + "name" : "Delete Account", + "providerId" : "delete_account", + "enabled" : false, + "defaultAction" : false, + "priority" : 60, + "config" : { } + }, { + "alias" : "webauthn-register", + "name" : "Webauthn Register", + "providerId" : "webauthn-register", + "enabled" : true, + "defaultAction" : false, + "priority" : 70, + "config" : { } + }, { + "alias" : "webauthn-register-passwordless", + "name" : "Webauthn Register Passwordless", + "providerId" : "webauthn-register-passwordless", + "enabled" : true, + "defaultAction" : false, + "priority" : 80, + "config" : { } + }, { + "alias" : "update_user_locale", + "name" : "Update User Locale", + "providerId" : "update_user_locale", + "enabled" : true, + "defaultAction" : false, + "priority" : 1000, + "config" : { } + } ], + "browserFlow" : "browser", + "registrationFlow" : "registration", + "directGrantFlow" : "direct grant", + "resetCredentialsFlow" : "reset credentials", + "clientAuthenticationFlow" : "clients", + "dockerAuthenticationFlow" : "docker auth", + "attributes" : { + "cibaBackchannelTokenDeliveryMode" : "poll", + "cibaAuthRequestedUserHint" : "login_hint", + "clientOfflineSessionMaxLifespan" : "0", + "oauth2DevicePollingInterval" : "5", + "clientSessionIdleTimeout" : "0", + "actionTokenGeneratedByUserLifespan-execute-actions" : "", + "actionTokenGeneratedByUserLifespan-verify-email" : "", + "clientOfflineSessionIdleTimeout" : "0", + "actionTokenGeneratedByUserLifespan-reset-credentials" : "", + "cibaInterval" : "5", + "realmReusableOtpCode" : "false", + "cibaExpiresIn" : "120", + "oauth2DeviceCodeLifespan" : "600", + "actionTokenGeneratedByUserLifespan-idp-verify-account-via-email" : "", + "parRequestUriLifespan" : "60", + "clientSessionMaxLifespan" : "0", + "shortVerificationUri" : "" + }, + "keycloakVersion" : "21.0.2", + "userManagedAccessAllowed" : false, + "clientProfiles" : { + "profiles" : [ ] + }, + "clientPolicies" : { + "policies" : [ ] + } } \ No newline at end of file diff --git a/dbrepo-metadata-service/services/src/main/java/at/tuwien/auth/BasicAuthenticationProvider.java b/dbrepo-metadata-service/services/src/main/java/at/tuwien/auth/BasicAuthenticationProvider.java index 1f8f3ced9d..80fdd15f1d 100644 --- a/dbrepo-metadata-service/services/src/main/java/at/tuwien/auth/BasicAuthenticationProvider.java +++ b/dbrepo-metadata-service/services/src/main/java/at/tuwien/auth/BasicAuthenticationProvider.java @@ -1,8 +1,6 @@ package at.tuwien.auth; import at.tuwien.api.keycloak.TokenDto; -import at.tuwien.api.user.UserDetailsDto; -import at.tuwien.config.GatewayConfig; import at.tuwien.exception.*; import at.tuwien.gateway.KeycloakGateway; import jakarta.servlet.ServletException; @@ -13,40 +11,24 @@ import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; -import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.stereotype.Component; -import java.util.List; - @Log4j2 @Component public class BasicAuthenticationProvider implements AuthenticationManager { - private final GatewayConfig gatewayConfig; private final AuthTokenFilter authTokenFilter; private final KeycloakGateway keycloakGateway; @Autowired - public BasicAuthenticationProvider(GatewayConfig gatewayConfig, AuthTokenFilter authTokenFilter, - KeycloakGateway keycloakGateway) { - this.gatewayConfig = gatewayConfig; + public BasicAuthenticationProvider(AuthTokenFilter authTokenFilter, KeycloakGateway keycloakGateway) { this.authTokenFilter = authTokenFilter; this.keycloakGateway = keycloakGateway; } @Override public Authentication authenticate(Authentication auth) throws AuthenticationException { - if (auth.getName().equals(gatewayConfig.getAdminUsername()) - && auth.getCredentials().toString().equals(gatewayConfig.getAdminPassword())) { - log.trace("current user is {}: skip authentication", gatewayConfig.getAdminUsername()); - final UserDetails userDetails = UserDetailsDto.builder() - .username(auth.getName()) - .authorities(List.of(new SimpleGrantedAuthority("admin"))) - .build(); - return new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); - } - log.trace("current user is {}: begin authentication", auth.getName()); try { final TokenDto tokenDto = keycloakGateway.obtainUserToken(auth.getName(), auth.getCredentials().toString()); final UserDetails userDetails = authTokenFilter.verifyJwt(tokenDto.getAccessToken()); diff --git a/dbrepo-metadata-service/services/src/main/java/at/tuwien/auth/InternalRequestInterceptor.java b/dbrepo-metadata-service/services/src/main/java/at/tuwien/auth/InternalRequestInterceptor.java new file mode 100644 index 0000000000..835b7245d1 --- /dev/null +++ b/dbrepo-metadata-service/services/src/main/java/at/tuwien/auth/InternalRequestInterceptor.java @@ -0,0 +1,47 @@ +package at.tuwien.auth; + +import at.tuwien.api.keycloak.TokenDto; +import at.tuwien.config.GatewayConfig; +import at.tuwien.exception.AccountNotSetupException; +import at.tuwien.exception.AuthServiceConnectionException; +import at.tuwien.exception.CredentialsInvalidException; +import at.tuwien.gateway.KeycloakGateway; +import lombok.extern.log4j.Log4j2; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpRequest; +import org.springframework.http.MediaType; +import org.springframework.http.client.ClientHttpRequestExecution; +import org.springframework.http.client.ClientHttpRequestInterceptor; +import org.springframework.http.client.ClientHttpResponse; + +import java.io.IOException; +import java.util.List; + +@Log4j2 +public class InternalRequestInterceptor implements ClientHttpRequestInterceptor { + + private final GatewayConfig gatewayConfig; + private final KeycloakGateway keycloakGateway; + + public InternalRequestInterceptor(GatewayConfig gatewayConfig, KeycloakGateway keycloakGateway) { + this.gatewayConfig = gatewayConfig; + this.keycloakGateway = keycloakGateway; + } + + @Override + public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) + throws IOException { + final HttpHeaders headers = request.getHeaders(); + headers.setAccept(List.of(MediaType.APPLICATION_JSON)); + try { + final TokenDto token = keycloakGateway.obtainUserToken(gatewayConfig.getSystemUsername(), + gatewayConfig.getSystemPassword()); + headers.setBearerAuth(token.getAccessToken()); + log.trace("set bearer token for internal user: {}", gatewayConfig.getSystemUsername()); + return execution.execute(request, body); + } catch (AuthServiceConnectionException | CredentialsInvalidException | AccountNotSetupException e) { + log.error("Failed to obtain token for internal user: {}", gatewayConfig.getSystemUsername()); + throw new IOException("Failed to obtain token for internal user", e); + } + } +} diff --git a/dbrepo-metadata-service/services/src/main/java/at/tuwien/config/GatewayConfig.java b/dbrepo-metadata-service/services/src/main/java/at/tuwien/config/GatewayConfig.java index c64fc52282..d7fc192bb6 100644 --- a/dbrepo-metadata-service/services/src/main/java/at/tuwien/config/GatewayConfig.java +++ b/dbrepo-metadata-service/services/src/main/java/at/tuwien/config/GatewayConfig.java @@ -1,24 +1,15 @@ package at.tuwien.config; +import at.tuwien.auth.InternalRequestInterceptor; +import at.tuwien.gateway.KeycloakGateway; import lombok.Getter; import lombok.extern.log4j.Log4j2; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Primary; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpRequest; -import org.springframework.http.MediaType; -import org.springframework.http.client.ClientHttpRequestExecution; -import org.springframework.http.client.ClientHttpRequestInterceptor; -import org.springframework.http.client.ClientHttpResponse; -import org.springframework.http.client.support.BasicAuthenticationInterceptor; +import org.springframework.context.annotation.*; import org.springframework.web.client.RestTemplate; import org.springframework.web.util.DefaultUriBuilderFactory; -import java.io.IOException; -import java.util.List; - @Log4j2 @Getter @Configuration @@ -42,25 +33,26 @@ public class GatewayConfig { @Value("${spring.rabbitmq.password}") private String brokerPassword; - @Value("${dbrepo.admin.username}") - private String adminUsername; + @Value("${dbrepo.system.username}") + private String systemUsername; + + @Value("${dbrepo.system.password}") + private String systemPassword; - @Value("${dbrepo.admin.password}") - private String adminPassword; + private final KeycloakGateway keycloakGateway; - @Primary - public RestTemplate restTemplate() { - return new RestTemplate(); + @Autowired + public GatewayConfig(KeycloakGateway keycloakGateway) { + this.keycloakGateway = keycloakGateway; } + @Profile("!junit") @Bean("brokerRestTemplate") public RestTemplate brokerRestTemplate() { final RestTemplate restTemplate = new RestTemplate(); restTemplate.setUriTemplateHandler(new DefaultUriBuilderFactory(brokerEndpoint)); - log.debug("add basic authentication for broker service: username={}, password=(hidden)", brokerUsername); restTemplate.getInterceptors() - .addAll(List.of(new BasicAuthenticationInterceptor(brokerUsername, brokerPassword), - clientHttpRequestInterceptor())); + .add(new InternalRequestInterceptor(this, keycloakGateway)); return restTemplate; } @@ -68,10 +60,8 @@ public class GatewayConfig { public RestTemplate dataServiceRestTemplate() { final RestTemplate restTemplate = new RestTemplate(); restTemplate.setUriTemplateHandler(new DefaultUriBuilderFactory(dataEndpoint)); - log.debug("add basic authentication for data service: username={}, password=(hidden)", adminUsername); restTemplate.getInterceptors() - .addAll(List.of(new BasicAuthenticationInterceptor(adminUsername, adminPassword), - clientHttpRequestInterceptor())); + .add(new InternalRequestInterceptor(this, keycloakGateway)); return restTemplate; } @@ -79,10 +69,8 @@ public class GatewayConfig { public RestTemplate analyseServiceRestTemplate() { final RestTemplate restTemplate = new RestTemplate(); restTemplate.setUriTemplateHandler(new DefaultUriBuilderFactory(analyseEndpoint)); - log.debug("add basic authentication for analyse service: username={}, password=(hidden)", adminUsername); restTemplate.getInterceptors() - .addAll(List.of(new BasicAuthenticationInterceptor(adminUsername, adminPassword), - clientHttpRequestInterceptor())); + .add(new InternalRequestInterceptor(this, keycloakGateway)); return restTemplate; } @@ -90,20 +78,9 @@ public class GatewayConfig { public RestTemplate searchServiceRestTemplate() { final RestTemplate restTemplate = new RestTemplate(); restTemplate.setUriTemplateHandler(new DefaultUriBuilderFactory(searchEndpoint)); - log.debug("add basic authentication for search service: username={}, password=(hidden)", adminUsername); restTemplate.getInterceptors() - .addAll(List.of(new BasicAuthenticationInterceptor(adminUsername, adminPassword), - clientHttpRequestInterceptor())); + .add(new InternalRequestInterceptor(this, keycloakGateway)); return restTemplate; } - @Bean - public ClientHttpRequestInterceptor clientHttpRequestInterceptor() { - return (request, body, execution) -> { - final HttpHeaders headers = request.getHeaders(); - headers.setAccept(List.of(MediaType.APPLICATION_JSON)); - return execution.execute(request, body); - }; - } - } diff --git a/dbrepo-metadata-service/services/src/main/java/at/tuwien/config/KeycloakConfig.java b/dbrepo-metadata-service/services/src/main/java/at/tuwien/config/KeycloakConfig.java index 4d258d496a..e422223e06 100644 --- a/dbrepo-metadata-service/services/src/main/java/at/tuwien/config/KeycloakConfig.java +++ b/dbrepo-metadata-service/services/src/main/java/at/tuwien/config/KeycloakConfig.java @@ -2,16 +2,12 @@ package at.tuwien.config; import at.tuwien.interceptor.KeycloakInterceptor; import lombok.Getter; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.http.client.ClientHttpRequestInterceptor; import org.springframework.web.client.RestTemplate; import org.springframework.web.util.DefaultUriBuilderFactory; -import java.util.List; - @Getter @Configuration public class KeycloakConfig { @@ -31,11 +27,9 @@ public class KeycloakConfig { @Value("${dbrepo.keycloak.clientSecret}") private String keycloakClientSecret; - private final ClientHttpRequestInterceptor clientHttpRequestInterceptor; - - @Autowired - public KeycloakConfig(ClientHttpRequestInterceptor clientHttpRequestInterceptor) { - this.clientHttpRequestInterceptor = clientHttpRequestInterceptor; + @Bean + public RestTemplate restTemplate() { + return new RestTemplate(); } @Bean("keycloakRestTemplate") @@ -43,8 +37,7 @@ public class KeycloakConfig { final RestTemplate restTemplate = new RestTemplate(); restTemplate.setUriTemplateHandler(new DefaultUriBuilderFactory(keycloakEndpoint)); restTemplate.getInterceptors() - .addAll(List.of(new KeycloakInterceptor(keycloakUsername, keycloakPassword, keycloakEndpoint), - clientHttpRequestInterceptor)); + .add(new KeycloakInterceptor(keycloakUsername, keycloakPassword, keycloakEndpoint)); return restTemplate; } } diff --git a/dbrepo-metadata-service/services/src/main/java/at/tuwien/config/RabbitConfig.java b/dbrepo-metadata-service/services/src/main/java/at/tuwien/config/RabbitConfig.java index bef0235006..0ed5001dd4 100644 --- a/dbrepo-metadata-service/services/src/main/java/at/tuwien/config/RabbitConfig.java +++ b/dbrepo-metadata-service/services/src/main/java/at/tuwien/config/RabbitConfig.java @@ -19,4 +19,7 @@ public class RabbitConfig { @Value("${spring.rabbitmq.virtual-host}") private String virtualHost; + @Value("${dbrepo.endpoints.brokerService}") + private String brokerEndpoint; + } diff --git a/dbrepo-metadata-service/services/src/main/java/at/tuwien/config/WebSecurityConfig.java b/dbrepo-metadata-service/services/src/main/java/at/tuwien/config/WebSecurityConfig.java index 769cf00b01..ae15c9df2d 100644 --- a/dbrepo-metadata-service/services/src/main/java/at/tuwien/config/WebSecurityConfig.java +++ b/dbrepo-metadata-service/services/src/main/java/at/tuwien/config/WebSecurityConfig.java @@ -43,8 +43,8 @@ public class WebSecurityConfig { } @Bean - public SecurityFilterChain filterChain(GatewayConfig gatewayConfig, HttpSecurity http, - KeycloakGateway keycloakGateway) throws Exception { + public SecurityFilterChain filterChain(HttpSecurity http, KeycloakGateway keycloakGateway) + throws Exception { final OrRequestMatcher internalEndpoints = new OrRequestMatcher( new AntPathRequestMatcher("/actuator/**", "GET"), new AntPathRequestMatcher("/v3/api-docs.yaml"), @@ -88,8 +88,8 @@ public class WebSecurityConfig { http.addFilterBefore(authTokenFilter(), UsernamePasswordAuthenticationFilter.class ); - http.addFilterBefore(new BasicAuthenticationFilter(new BasicAuthenticationProvider(gatewayConfig, - authTokenFilter(), keycloakGateway)), + http.addFilterBefore(new BasicAuthenticationFilter(new BasicAuthenticationProvider(authTokenFilter(), + keycloakGateway)), UsernamePasswordAuthenticationFilter.class ); return http.build(); diff --git a/dbrepo-metadata-service/services/src/main/java/at/tuwien/gateway/BrokerServiceGateway.java b/dbrepo-metadata-service/services/src/main/java/at/tuwien/gateway/BrokerServiceGateway.java index 0ca0f707e4..42e8912d0c 100644 --- a/dbrepo-metadata-service/services/src/main/java/at/tuwien/gateway/BrokerServiceGateway.java +++ b/dbrepo-metadata-service/services/src/main/java/at/tuwien/gateway/BrokerServiceGateway.java @@ -10,31 +10,31 @@ public interface BrokerServiceGateway { * Create topic exchange permissions at the broker service. * * @param data The topic exchange permissions. - * @throws ServiceConnectionException - * @throws ServiceException + * @throws BrokerServiceConnectionException + * @throws BrokerServiceException */ - void grantExchangePermission(String username, GrantExchangePermissionsDto data) throws ServiceConnectionException, - ServiceException; + void grantExchangePermission(String username, GrantExchangePermissionsDto data) + throws BrokerServiceConnectionException, BrokerServiceException; /** * Grants a user permission at a virtual host in the queue service. * * @param username The username of the user. * @param data The grant data. - * @throws ServiceConnectionException - * @throws ServiceException + * @throws BrokerServiceConnectionException + * @throws BrokerServiceException */ - void grantTopicPermission(String username, ExchangeUpdatePermissionsDto data) throws ServiceConnectionException, - ServiceException; + void grantTopicPermission(String username, ExchangeUpdatePermissionsDto data) + throws BrokerServiceConnectionException, BrokerServiceException; /** * Grants a user permission at a virtual host in the queue service. * * @param username The username of the user. * @param data The grant data. - * @throws ServiceConnectionException - * @throws ServiceException + * @throws BrokerServiceConnectionException + * @throws BrokerServiceException */ void grantVirtualHostPermission(String username, GrantVirtualHostPermissionsDto data) - throws ServiceConnectionException, ServiceException; + throws BrokerServiceConnectionException, BrokerServiceException; } diff --git a/dbrepo-metadata-service/services/src/main/java/at/tuwien/gateway/DataServiceGateway.java b/dbrepo-metadata-service/services/src/main/java/at/tuwien/gateway/DataServiceGateway.java index 9edb9f388d..91fb96e5f9 100644 --- a/dbrepo-metadata-service/services/src/main/java/at/tuwien/gateway/DataServiceGateway.java +++ b/dbrepo-metadata-service/services/src/main/java/at/tuwien/gateway/DataServiceGateway.java @@ -17,31 +17,163 @@ import java.util.List; import java.util.UUID; public interface DataServiceGateway { - void createAccess(Long databaseId, UUID userId, AccessTypeDto access) throws ServiceConnectionException, ServiceException, DatabaseNotFoundException; - void updateAccess(Long databaseId, UUID userId, AccessTypeDto access) throws ServiceConnectionException, ServiceException, AccessNotFoundException; - - void deleteAccess(Long databaseId, UUID userId) throws ServiceConnectionException, ServiceException, AccessNotFoundException; - - DatabaseDto createDatabase(CreateDatabaseDto data) throws ServiceConnectionException, ServiceException; - - void updateDatabase(Long databaseId, UpdateUserPasswordDto data) throws ServiceConnectionException, ServiceException, DatabaseNotFoundException; - - void createTable(Long databaseId, TableCreateDto data) throws ServiceConnectionException, ServiceException, DatabaseNotFoundException, TableExistsException; - - void deleteTable(Long databaseId, Long tableId) throws ServiceConnectionException, ServiceException, TableNotFoundException; - - ViewDto createView(Long databaseId, ViewCreateDto data) throws ServiceConnectionException, ServiceException; - - void deleteView(Long databaseId, Long viewId) throws ServiceConnectionException, ServiceException, ViewNotFoundException; - - QueryDto findQuery(Long databaseId, Long queryId) throws ServiceConnectionException, ServiceException, QueryNotFoundException; - - ExportResourceDto exportQuery(Long databaseId, Long queryId) throws ServiceConnectionException, ServiceException, QueryNotFoundException; - - List<TableDto> getTableSchemas(Long databaseId) throws ServiceConnectionException, ServiceException, QueryNotFoundException; - - List<ViewDto> getViewSchemas(Long databaseId) throws ServiceConnectionException, ServiceException, QueryNotFoundException; - - TableStatisticDto getTableStatistics(Long databaseId, Long tableId) throws ServiceConnectionException, ServiceException, TableNotFoundException; + /** + * Create r/w access for a given user to a given database. + * @param databaseId The database id. + * @param userId The user id. + * @param access The access. + * @throws DataServiceConnectionException The connection to the data service could not be established. + * @throws DataServiceException The data service responded unexpectedly. + * @throws DatabaseNotFoundException Some of the privileged parameters of the given database were not provided by the metadata service. + */ + void createAccess(Long databaseId, UUID userId, AccessTypeDto access) throws DataServiceConnectionException, + DataServiceException, DatabaseNotFoundException; + + /** + * Update r/w access for a given user to a given database. + * @param databaseId The database id. + * @param userId The user id. + * @param access The access. + * @throws DataServiceConnectionException The connection to the data service could not be established. + * @throws DataServiceException The data service responded unexpectedly. + * @throws AccessNotFoundException Some of the privileged parameters of the given database were not provided by the metadata service. + */ + void updateAccess(Long databaseId, UUID userId, AccessTypeDto access) throws DataServiceConnectionException, + DataServiceException, AccessNotFoundException; + + /** + * Deletes access for a given user to a given database. + * @param databaseId The database id. + * @param userId The user id. + * @throws DataServiceConnectionException The connection to the data service could not be established. + * @throws DataServiceException The data service responded unexpectedly. + * @throws AccessNotFoundException Some of the privileged parameters of the given database were not provided by the metadata service. + */ + void deleteAccess(Long databaseId, UUID userId) throws DataServiceConnectionException, DataServiceException, + AccessNotFoundException; + + /** + * Creates a database in the data service. + * @param data The data. + * @return The created database, if successful. + * @throws DataServiceConnectionException The connection to the data service could not be established. + * @throws DataServiceException The data service responded unexpectedly. + * @throws DatabaseNotFoundException Some of the privileged parameters of the given database were not provided by the metadata service. + */ + DatabaseDto createDatabase(CreateDatabaseDto data) throws DataServiceConnectionException, DataServiceException, + DatabaseNotFoundException; + + /** + * Updates the user password in the given database in the data service. + * @param databaseId The database id. + * @param data The user password. + * @throws DataServiceConnectionException The connection to the data service could not be established. + * @throws DataServiceException The data service responded unexpectedly. + * @throws DatabaseNotFoundException Some of the privileged parameters of the given database were not provided by the metadata service. + */ + void updateDatabase(Long databaseId, UpdateUserPasswordDto data) throws DataServiceConnectionException, + DataServiceException, DatabaseNotFoundException; + + /** + * Creates a table in a given database. + * @param databaseId The database id. + * @param data The table data. + * @throws DataServiceConnectionException The connection to the data service could not be established. + * @throws DataServiceException The data service responded unexpectedly. + * @throws DatabaseNotFoundException Some of the privileged parameters of the given database were not provided by the metadata service. + * @throws TableExistsException A table with this internal name exists already in the database. + */ + void createTable(Long databaseId, TableCreateDto data) throws DataServiceConnectionException, DataServiceException, + DatabaseNotFoundException, TableExistsException; + + /** + * Deletes a given table in a given database. + * @param databaseId The database id. + * @param tableId The table id. + * @throws DataServiceConnectionException The connection to the data service could not be established. + * @throws DataServiceException The data service responded unexpectedly. + * @throws TableNotFoundException The given table was not found in the database. + */ + void deleteTable(Long databaseId, Long tableId) throws DataServiceConnectionException, DataServiceException, + TableNotFoundException; + + /** + * Creates a view in the given database. + * @param databaseId The database id. + * @param data The view data. + * @return The created view, if successful. + * @throws DataServiceConnectionException The connection to the data service could not be established. + * @throws DataServiceException The data service responded unexpectedly. + */ + ViewDto createView(Long databaseId, ViewCreateDto data) throws DataServiceConnectionException, DataServiceException; + + /** + * Deletes a given view in the given database. + * @param databaseId The database id. + * @param viewId The view id. + * @throws DataServiceConnectionException The connection to the data service could not be established. + * @throws DataServiceException The data service responded unexpectedly. + * @throws ViewNotFoundException The given view was not found in the database. + */ + void deleteView(Long databaseId, Long viewId) throws DataServiceConnectionException, DataServiceException, + ViewNotFoundException; + + /** + * Finds a given query in a given database. + * @param databaseId The database id. + * @param queryId The query id. + * @return The query, if successful. + * @throws DataServiceConnectionException The connection to the data service could not be established. + * @throws DataServiceException The data service responded unexpectedly. + * @throws QueryNotFoundException The given query was not found in the query store. + */ + QueryDto findQuery(Long databaseId, Long queryId) throws DataServiceConnectionException, DataServiceException, + QueryNotFoundException; + + /** + * Exports a given query. + * @param databaseId The database id. + * @param queryId The query id. + * @return The exported resource, if successful. + * @throws DataServiceConnectionException The connection to the data service could not be established. + * @throws DataServiceException The data service responded unexpectedly. + * @throws QueryNotFoundException The given query was not found in the query store. + */ + ExportResourceDto exportQuery(Long databaseId, Long queryId) throws DataServiceConnectionException, + DataServiceException, QueryNotFoundException; + + /** + * Obtain table schemas from a given database. + * @param databaseId The database id. + * @return The list of tables, if successful. + * @throws DataServiceConnectionException The connection to the data service could not be established. + * @throws DataServiceException The data service responded unexpectedly. + * @throws TableNotFoundException The table was not found in the database. + */ + List<TableDto> getTableSchemas(Long databaseId) throws DataServiceConnectionException, DataServiceException, + TableNotFoundException; + + /** + * Obtain view schemas from a given database. + * @param databaseId The database id. + * @return The list of tables, if successful. + * @throws DataServiceConnectionException The connection to the data service could not be established. + * @throws DataServiceException The data service responded unexpectedly. + * @throws ViewNotFoundException The table was not found in the database. + */ + List<ViewDto> getViewSchemas(Long databaseId) throws DataServiceConnectionException, DataServiceException, + ViewNotFoundException; + + /** + * Obtain table statistics for a given table in a given database. + * @param databaseId The database id. + * @param tableId The table id. + * @return The statistic, if successful. + * @throws DataServiceConnectionException The connection to the data service could not be established. + * @throws DataServiceException The data service responded unexpectedly. + * @throws TableNotFoundException The table was not found in the database. + */ + TableStatisticDto getTableStatistics(Long databaseId, Long tableId) throws DataServiceConnectionException, + DataServiceException, TableNotFoundException; } diff --git a/dbrepo-metadata-service/services/src/main/java/at/tuwien/gateway/OrcidGateway.java b/dbrepo-metadata-service/services/src/main/java/at/tuwien/gateway/OrcidGateway.java index b949ddbadd..2cd5f142e6 100644 --- a/dbrepo-metadata-service/services/src/main/java/at/tuwien/gateway/OrcidGateway.java +++ b/dbrepo-metadata-service/services/src/main/java/at/tuwien/gateway/OrcidGateway.java @@ -7,5 +7,11 @@ import org.springframework.stereotype.Service; @Service public interface OrcidGateway { + /** + * Finds metadata from given ORCID url. + * @param url The ORCID url. + * @return The metadata, if successful. + * @throws OrcidNotFoundException The metadata does not exist to the given ORCID. + */ OrcidDto findByUrl(String url) throws OrcidNotFoundException; } diff --git a/dbrepo-metadata-service/services/src/main/java/at/tuwien/gateway/impl/BrokerServiceGatewayImpl.java b/dbrepo-metadata-service/services/src/main/java/at/tuwien/gateway/impl/BrokerServiceGatewayImpl.java index 9b3bbf4cf2..0313ca26ed 100644 --- a/dbrepo-metadata-service/services/src/main/java/at/tuwien/gateway/impl/BrokerServiceGatewayImpl.java +++ b/dbrepo-metadata-service/services/src/main/java/at/tuwien/gateway/impl/BrokerServiceGatewayImpl.java @@ -10,7 +10,6 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.http.*; import org.springframework.stereotype.Service; -import org.springframework.web.client.HttpClientErrorException; import org.springframework.web.client.HttpServerErrorException; import org.springframework.web.client.ResourceAccessException; import org.springframework.web.client.RestTemplate; @@ -31,59 +30,61 @@ public class BrokerServiceGatewayImpl implements BrokerServiceGateway { @Override public void grantTopicPermission(String username, ExchangeUpdatePermissionsDto data) - throws ServiceConnectionException, ServiceException { + throws BrokerServiceConnectionException, BrokerServiceException { final String url = "/api/topic-permissions/" + rabbitConfig.getVirtualHost() + "/" + username; final ResponseEntity<Void> response; try { response = restTemplate.exchange(url, HttpMethod.PUT, new HttpEntity<>(data), Void.class); - } catch (ResourceAccessException | HttpServerErrorException.ServiceUnavailable e) { + } catch (HttpServerErrorException e) { log.error("Failed to grant topic permissions: {}", e.getMessage()); - throw new ServiceConnectionException("Failed to grant topic permissions: " + e.getMessage()); + throw new BrokerServiceConnectionException("Failed to grant topic permissions: " + e.getMessage()); } catch (Exception e) { log.error("Failed to grant topic permissions: unexpected response: {}", e.getMessage()); - throw new ServiceException("Failed to grant topic permissions: unexpected response: " + e.getMessage(), e); + throw new BrokerServiceException("Failed to grant topic permissions: unexpected response: " + e.getMessage(), e); } if (!response.getStatusCode().equals(HttpStatus.CREATED) && !response.getStatusCode().equals(HttpStatus.NO_CONTENT)) { log.error("Failed to grant topic permissions: unexpected status: {}", response.getStatusCode().value()); - throw new ServiceException("Failed to grant topic permissions: unexpected status: " + response.getStatusCode().value()); + throw new BrokerServiceException("Failed to grant topic permissions: unexpected status: " + response.getStatusCode().value()); } } @Override - public void grantVirtualHostPermission(String username, GrantVirtualHostPermissionsDto data) throws ServiceConnectionException, ServiceException { + public void grantVirtualHostPermission(String username, GrantVirtualHostPermissionsDto data) + throws BrokerServiceConnectionException, BrokerServiceException { final String url = "/api/permissions/" + rabbitConfig.getVirtualHost() + "/" + username; final ResponseEntity<Void> response; try { response = restTemplate.exchange(url, HttpMethod.PUT, new HttpEntity<>(data), Void.class); - } catch (ResourceAccessException | HttpServerErrorException.ServiceUnavailable e) { + } catch (HttpServerErrorException e) { log.error("Failed to grant virtual host permissions: {}", e.getMessage()); - throw new ServiceConnectionException("Failed to grant virtual host permissions: " + e.getMessage()); + throw new BrokerServiceConnectionException("Failed to grant virtual host permissions: " + e.getMessage()); } catch (Exception e) { log.error("Failed to grant virtual host permissions: unexpected response: {}", e.getMessage()); - throw new ServiceException("Failed to grant virtual host permissions: unexpected response: " + e.getMessage(), e); + throw new BrokerServiceException("Failed to grant virtual host permissions: unexpected response: " + e.getMessage(), e); } if (!response.getStatusCode().equals(HttpStatus.CREATED) && !response.getStatusCode().equals(HttpStatus.NO_CONTENT)) { log.error("Failed to grant virtual host permissions: unexpected status: {}", response.getStatusCode().value()); - throw new ServiceException("Failed to grant virtual host permissions: unexpected status: " + response.getStatusCode().value()); + throw new BrokerServiceException("Failed to grant virtual host permissions: unexpected status: " + response.getStatusCode().value()); } } @Override - public void grantExchangePermission(String username, GrantExchangePermissionsDto data) throws ServiceConnectionException, ServiceException { + public void grantExchangePermission(String username, GrantExchangePermissionsDto data) + throws BrokerServiceConnectionException, BrokerServiceException { final String url = "/api/topic-permissions/" + rabbitConfig.getVirtualHost() + "/" + username; final ResponseEntity<Void> response; try { response = restTemplate.exchange(url, HttpMethod.PUT, new HttpEntity<>(data), Void.class); - } catch (ResourceAccessException | HttpServerErrorException.ServiceUnavailable e) { + } catch (HttpServerErrorException e) { log.error("Failed to grant exchange permissions: {}", e.getMessage()); - throw new ServiceConnectionException("Failed to grant exchange permissions: " + e.getMessage()); + throw new BrokerServiceConnectionException("Failed to grant exchange permissions: " + e.getMessage()); } catch (Exception e) { log.error("Failed to grant exchange permissions: unexpected response: {}", e.getMessage()); - throw new ServiceException("Failed to grant exchange permissions: unexpected response: " + e.getMessage(), e); + throw new BrokerServiceException("Failed to grant exchange permissions: unexpected response: " + e.getMessage(), e); } if (!response.getStatusCode().equals(HttpStatus.CREATED) && !response.getStatusCode().equals(HttpStatus.NO_CONTENT)) { log.error("Failed to grant exchange permissions: unexpected status: {}", response.getStatusCode().value()); - throw new ServiceException("Failed to grant exchange permissions: unexpected status: " + response.getStatusCode().value()); + throw new BrokerServiceException("Failed to grant exchange permissions: unexpected status: " + response.getStatusCode().value()); } } diff --git a/dbrepo-metadata-service/services/src/main/java/at/tuwien/gateway/impl/CrossrefGatewayImpl.java b/dbrepo-metadata-service/services/src/main/java/at/tuwien/gateway/impl/CrossrefGatewayImpl.java index 7c5d4b19f5..9b675cba34 100644 --- a/dbrepo-metadata-service/services/src/main/java/at/tuwien/gateway/impl/CrossrefGatewayImpl.java +++ b/dbrepo-metadata-service/services/src/main/java/at/tuwien/gateway/impl/CrossrefGatewayImpl.java @@ -4,6 +4,7 @@ import at.tuwien.api.crossref.CrossrefDto; import at.tuwien.exception.DoiNotFoundException; import at.tuwien.gateway.CrossrefGateway; import lombok.extern.log4j.Log4j2; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; @@ -19,8 +20,9 @@ public class CrossrefGatewayImpl implements CrossrefGateway { private final RestTemplate restTemplate; - public CrossrefGatewayImpl() { - this.restTemplate = new RestTemplate(); + @Autowired + public CrossrefGatewayImpl(RestTemplate restTemplate) { + this.restTemplate = restTemplate; } @Override @@ -32,7 +34,7 @@ public class CrossrefGatewayImpl implements CrossrefGateway { try { log.trace("find crossref doi from url {}", url); response = restTemplate.exchange(url, HttpMethod.GET, new HttpEntity<>(null, headers), CrossrefDto.class); - } catch (ResourceAccessException | HttpServerErrorException.ServiceUnavailable e) { + } catch (HttpServerErrorException e) { log.error("Failed to retrieve CrossRef metadata from URL {}: {}", url, e.getMessage()); throw new DoiNotFoundException("Failed to retrieve CrossRef metadata from URL " + url + ": " + e.getMessage()); } diff --git a/dbrepo-metadata-service/services/src/main/java/at/tuwien/gateway/impl/DataServiceGatewayImpl.java b/dbrepo-metadata-service/services/src/main/java/at/tuwien/gateway/impl/DataServiceGatewayImpl.java index 6c09d6d500..4a3f12f759 100644 --- a/dbrepo-metadata-service/services/src/main/java/at/tuwien/gateway/impl/DataServiceGatewayImpl.java +++ b/dbrepo-metadata-service/services/src/main/java/at/tuwien/gateway/impl/DataServiceGatewayImpl.java @@ -34,7 +34,7 @@ public class DataServiceGatewayImpl implements DataServiceGateway { @Override public void createAccess(Long databaseId, UUID userId, AccessTypeDto access) - throws ServiceConnectionException, ServiceException, DatabaseNotFoundException { + throws DataServiceConnectionException, DataServiceException, DatabaseNotFoundException { final ResponseEntity<Void> response; final String url = "/api/database/" + databaseId + "/access/" + userId; try { @@ -42,23 +42,23 @@ public class DataServiceGatewayImpl implements DataServiceGateway { new HttpEntity<>(UpdateDatabaseAccessDto.builder().type(access).build()), Void.class); } catch (HttpServerErrorException e) { log.error("Failed to create access: {}", e.getMessage()); - throw new ServiceConnectionException("Failed to create access: " + e.getMessage(), e); + throw new DataServiceConnectionException("Failed to create access: " + e.getMessage(), e); } catch (HttpClientErrorException.NotFound e) { log.error("Failed to create access: not found: {}", e.getMessage()); throw new DatabaseNotFoundException("Failed to create access: not found: " + e.getMessage(), e); } catch (HttpClientErrorException.BadRequest | HttpClientErrorException.Unauthorized e) { log.error("Failed to create access: {}", e.getMessage()); - throw new ServiceException("Failed to create access: " + e.getMessage(), e); + throw new DataServiceException("Failed to create access: " + e.getMessage(), e); } if (!response.getStatusCode().equals(HttpStatus.CREATED)) { log.error("Failed to create access: wrong http code: {}", response.getStatusCode()); - throw new ServiceException("Failed to create access: wrong http code: " + response.getStatusCode()); + throw new DataServiceException("Failed to create access: wrong http code: " + response.getStatusCode()); } } @Override public void updateAccess(Long databaseId, UUID userId, AccessTypeDto access) - throws ServiceConnectionException, ServiceException, AccessNotFoundException { + throws DataServiceConnectionException, DataServiceException, AccessNotFoundException { final ResponseEntity<Void> response; final String url = "/api/database/" + databaseId + "/access/" + userId; try { @@ -66,22 +66,22 @@ public class DataServiceGatewayImpl implements DataServiceGateway { new HttpEntity<>(UpdateDatabaseAccessDto.builder().type(access).build()), Void.class); } catch (HttpServerErrorException e) { log.error("Failed to update access: {}", e.getMessage()); - throw new ServiceConnectionException("Failed to update access: " + e.getMessage(), e); + throw new DataServiceConnectionException("Failed to update access: " + e.getMessage(), e); } catch (HttpClientErrorException.NotFound e) { log.error("Failed to update access: not found: {}", e.getMessage()); throw new AccessNotFoundException("Failed to update access: not found: " + e.getMessage(), e); } catch (HttpClientErrorException.BadRequest | HttpClientErrorException.Unauthorized e) { log.error("Failed to update access: {}", e.getMessage()); - throw new ServiceException("Failed to update access: " + e.getMessage(), e); + throw new DataServiceException("Failed to update access: " + e.getMessage(), e); } if (!response.getStatusCode().equals(HttpStatus.ACCEPTED)) { log.error("Failed to update access: wrong http code: {}", response.getStatusCode()); - throw new ServiceException("Failed to update access: wrong http code: " + response.getStatusCode()); + throw new DataServiceException("Failed to update access: wrong http code: " + response.getStatusCode()); } } @Override - public void deleteAccess(Long databaseId, UUID userId) throws ServiceConnectionException, ServiceException, + public void deleteAccess(Long databaseId, UUID userId) throws DataServiceConnectionException, DataServiceException, AccessNotFoundException { final ResponseEntity<Void> response; final String url = "/api/database/" + databaseId + "/access/" + userId; @@ -89,65 +89,69 @@ public class DataServiceGatewayImpl implements DataServiceGateway { response = restTemplate.exchange(url, HttpMethod.DELETE, HttpEntity.EMPTY, Void.class); } catch (HttpServerErrorException e) { log.error("Failed to delete access: {}", e.getMessage()); - throw new ServiceConnectionException("Failed to delete access: " + e.getMessage(), e); + throw new DataServiceConnectionException("Failed to delete access: " + e.getMessage(), e); } catch (HttpClientErrorException.NotFound e) { log.error("Failed to delete access: not found: {}", e.getMessage()); throw new AccessNotFoundException("Failed to delete access: not found: " + e.getMessage(), e); } catch (HttpClientErrorException.Unauthorized e) { log.error("Failed to delete access: {}", e.getMessage()); - throw new ServiceException("Failed to delete access: " + e.getMessage(), e); + throw new DataServiceException("Failed to delete access: " + e.getMessage(), e); } if (!response.getStatusCode().equals(HttpStatus.ACCEPTED)) { log.error("Failed to delete access: wrong http code: {}", response.getStatusCode()); - throw new ServiceException("Failed to delete access: wrong http code: " + response.getStatusCode()); + throw new DataServiceException("Failed to delete access: wrong http code: " + response.getStatusCode()); } } @Override - public DatabaseDto createDatabase(CreateDatabaseDto data) throws ServiceConnectionException, ServiceException { + public DatabaseDto createDatabase(CreateDatabaseDto data) throws DataServiceConnectionException, + DataServiceException, DatabaseNotFoundException { final ResponseEntity<DatabaseDto> response; final String url = "/api/database"; try { response = restTemplate.exchange(url, HttpMethod.POST, new HttpEntity<>(data), DatabaseDto.class); } catch (HttpServerErrorException e) { log.error("Failed to create database: {}", e.getMessage()); - throw new ServiceConnectionException("Failed to create database: " + e.getMessage(), e); + throw new DataServiceConnectionException("Failed to create database: " + e.getMessage(), e); } catch (HttpClientErrorException.BadRequest | HttpClientErrorException.Unauthorized e) { - log.error("Failed to create database: {}", e.getMessage()); - throw new ServiceException("Failed to create database: " + e.getMessage(), e); + log.error("Failed to create database: malformed: {}", e.getMessage()); + throw new DataServiceException("Failed to create database: malformed: " + e.getMessage(), e); + } catch (HttpClientErrorException.NotFound e) { + log.error("Failed to create database: not found: {}", e.getMessage()); + throw new DatabaseNotFoundException("Failed to create database: not found: " + e.getMessage(), e); } if (!response.getStatusCode().equals(HttpStatus.CREATED)) { log.error("Failed to create database: wrong http code: {}", response.getStatusCode()); - throw new ServiceException("Failed to create database: wrong http code: " + response.getStatusCode()); + throw new DataServiceException("Failed to create database: wrong http code: " + response.getStatusCode()); } return response.getBody(); } @Override - public void updateDatabase(Long databaseId, UpdateUserPasswordDto data) throws ServiceConnectionException, - ServiceException, DatabaseNotFoundException { + public void updateDatabase(Long databaseId, UpdateUserPasswordDto data) throws DataServiceConnectionException, + DataServiceException, DatabaseNotFoundException { final ResponseEntity<Void> response; final String url = "/api/database/" + databaseId; try { response = restTemplate.exchange(url, HttpMethod.PUT, new HttpEntity<>(data), Void.class); } catch (HttpServerErrorException e) { log.error("Failed to update user password in database: {}", e.getMessage()); - throw new ServiceConnectionException("Failed to update user password in database: " + e.getMessage(), e); + throw new DataServiceConnectionException("Failed to update user password in database: " + e.getMessage(), e); } catch (HttpClientErrorException.NotFound e) { log.error("Failed to update user password in database: not found: {}", e.getMessage()); throw new DatabaseNotFoundException("Failed to update user password in database: not found: " + e.getMessage(), e); } catch (HttpClientErrorException.BadRequest | HttpClientErrorException.Unauthorized e) { log.error("Failed to update user password in database: {}", e.getMessage()); - throw new ServiceException("Failed to update user password in database: " + e.getMessage(), e); + throw new DataServiceException("Failed to update user password in database: " + e.getMessage(), e); } if (!response.getStatusCode().equals(HttpStatus.ACCEPTED)) { log.error("Failed to update user password in database: wrong http code: {}", response.getStatusCode()); - throw new ServiceException("Failed to update user password in database: wrong http code: " + response.getStatusCode()); + throw new DataServiceException("Failed to update user password in database: wrong http code: " + response.getStatusCode()); } } @Override - public void createTable(Long databaseId, TableCreateDto data) throws ServiceConnectionException, ServiceException, + public void createTable(Long databaseId, TableCreateDto data) throws DataServiceConnectionException, DataServiceException, DatabaseNotFoundException, TableExistsException { final ResponseEntity<Void> response; final String url = "/api/database/" + databaseId + "/table"; @@ -155,7 +159,7 @@ public class DataServiceGatewayImpl implements DataServiceGateway { response = restTemplate.exchange(url, HttpMethod.POST, new HttpEntity<>(data), Void.class); } catch (HttpServerErrorException e) { log.error("Failed to create table: {}", e.getMessage()); - throw new ServiceConnectionException("Failed to create table: " + e.getMessage(), e); + throw new DataServiceConnectionException("Failed to create table: " + e.getMessage(), e); } catch (HttpClientErrorException.NotFound e) { log.error("Failed to create table: not found: {}", e.getMessage()); throw new DatabaseNotFoundException("Failed to create table: not found: " + e.getMessage(), e); @@ -164,16 +168,16 @@ public class DataServiceGatewayImpl implements DataServiceGateway { throw new TableExistsException("Failed to create table: already exists", e); } catch (HttpClientErrorException.BadRequest | HttpClientErrorException.Unauthorized e) { log.error("Failed to create table: {}", e.getMessage()); - throw new ServiceException("Failed to create table: " + e.getMessage(), e); + throw new DataServiceException("Failed to create table: " + e.getMessage(), e); } if (!response.getStatusCode().equals(HttpStatus.CREATED)) { log.error("Failed to create table: wrong http code: {}", response.getStatusCode()); - throw new ServiceException("Failed to create table: wrong http code: " + response.getStatusCode()); + throw new DataServiceException("Failed to create table: wrong http code: " + response.getStatusCode()); } } @Override - public void deleteTable(Long databaseId, Long tableId) throws ServiceConnectionException, ServiceException, + public void deleteTable(Long databaseId, Long tableId) throws DataServiceConnectionException, DataServiceException, TableNotFoundException { final ResponseEntity<Void> response; final String url = "/api/database/" + databaseId + "/table/" + tableId; @@ -181,46 +185,46 @@ public class DataServiceGatewayImpl implements DataServiceGateway { response = restTemplate.exchange(url, HttpMethod.DELETE, HttpEntity.EMPTY, Void.class); } catch (HttpServerErrorException e) { log.error("Failed to delete table: {}", e.getMessage()); - throw new ServiceConnectionException("Failed to delete table: " + e.getMessage(), e); + throw new DataServiceConnectionException("Failed to delete table: " + e.getMessage(), e); } catch (HttpClientErrorException.NotFound e) { log.error("Failed to delete table: not found: {}", e.getMessage()); throw new TableNotFoundException("Failed to delete table: not found: " + e.getMessage(), e); } catch (HttpClientErrorException.Unauthorized e) { log.error("Failed to delete table: {}", e.getMessage()); - throw new ServiceException("Failed to delete table: " + e.getMessage(), e); + throw new DataServiceException("Failed to delete table: " + e.getMessage(), e); } if (!response.getStatusCode().equals(HttpStatus.ACCEPTED)) { log.error("Failed to delete table: wrong http code: {}", response.getStatusCode()); - throw new ServiceException("Failed to delete table: wrong http code: " + response.getStatusCode()); + throw new DataServiceException("Failed to delete table: wrong http code: " + response.getStatusCode()); } } @Override - public ViewDto createView(Long databaseId, ViewCreateDto data) throws ServiceConnectionException, ServiceException { + public ViewDto createView(Long databaseId, ViewCreateDto data) throws DataServiceConnectionException, DataServiceException { final ResponseEntity<ViewDto> response; final String url = "/api/database/" + databaseId + "/view"; try { response = restTemplate.exchange(url, HttpMethod.POST, new HttpEntity<>(data), ViewDto.class); } catch (HttpServerErrorException e) { log.error("Failed to create view: {}", e.getMessage()); - throw new ServiceConnectionException("Failed to create view: " + e.getMessage(), e); + throw new DataServiceConnectionException("Failed to create view: " + e.getMessage(), e); } catch (HttpClientErrorException.BadRequest | HttpClientErrorException.Unauthorized e) { log.error("Failed to create view: {}", e.getMessage()); - throw new ServiceException("Failed to create view: " + e.getMessage(), e); + throw new DataServiceException("Failed to create view: " + e.getMessage(), e); } if (!response.getStatusCode().equals(HttpStatus.CREATED)) { log.error("Failed to create view: wrong http code: {}", response.getStatusCode()); - throw new ServiceException("Failed to create view: wrong http code: " + response.getStatusCode()); + throw new DataServiceException("Failed to create view: wrong http code: " + response.getStatusCode()); } if (response.getBody() == null) { log.error("Failed to create view: empty body: {}", response.getStatusCode()); - throw new ServiceException("Failed to create view: empty body: " + response.getStatusCode()); + throw new DataServiceException("Failed to create view: empty body: " + response.getStatusCode()); } return response.getBody(); } @Override - public void deleteView(Long databaseId, Long viewId) throws ServiceConnectionException, ServiceException, + public void deleteView(Long databaseId, Long viewId) throws DataServiceConnectionException, DataServiceException, ViewNotFoundException { final ResponseEntity<Void> response; final String url = "/api/database/" + databaseId + "/view/" + viewId; @@ -228,22 +232,22 @@ public class DataServiceGatewayImpl implements DataServiceGateway { response = restTemplate.exchange(url, HttpMethod.DELETE, HttpEntity.EMPTY, Void.class); } catch (HttpServerErrorException e) { log.error("Failed to delete view: {}", e.getMessage()); - throw new ServiceConnectionException("Failed to delete view: " + e.getMessage(), e); + throw new DataServiceConnectionException("Failed to delete view: " + e.getMessage(), e); } catch (HttpClientErrorException.NotFound e) { log.error("Failed to delete view: not found: {}", e.getMessage()); throw new ViewNotFoundException("Failed to delete view: not found: " + e.getMessage(), e); } catch (HttpClientErrorException.Unauthorized e) { log.error("Failed to delete view: {}", e.getMessage()); - throw new ServiceException("Failed to delete view: " + e.getMessage(), e); + throw new DataServiceException("Failed to delete view: " + e.getMessage(), e); } if (!response.getStatusCode().equals(HttpStatus.ACCEPTED)) { log.error("Failed to delete view: wrong http code: {}", response.getStatusCode()); - throw new ServiceException("Failed to delete view: wrong http code: " + response.getStatusCode()); + throw new DataServiceException("Failed to delete view: wrong http code: " + response.getStatusCode()); } } @Override - public QueryDto findQuery(Long databaseId, Long queryId) throws ServiceConnectionException, ServiceException, + public QueryDto findQuery(Long databaseId, Long queryId) throws DataServiceConnectionException, DataServiceException, QueryNotFoundException { final ResponseEntity<QueryDto> response; final String url = "/api/database/" + databaseId + "/subset/" + queryId; @@ -251,71 +255,72 @@ public class DataServiceGatewayImpl implements DataServiceGateway { response = restTemplate.exchange(url, HttpMethod.GET, HttpEntity.EMPTY, QueryDto.class); } catch (HttpServerErrorException e) { log.error("Failed to find query: {}", e.getMessage()); - throw new ServiceConnectionException("Failed to find query", e); + throw new DataServiceConnectionException("Failed to find query", e); } catch (HttpClientErrorException.NotFound e) { log.error("Failed to find query: not found: {}", e.getMessage()); throw new QueryNotFoundException("Failed to find query: not found", e); } catch (HttpClientErrorException.Unauthorized e) { log.error("Failed to find query: unauthorized: {}", e.getMessage()); - throw new ServiceException("Failed to find query: unauthorized", e); + throw new DataServiceException("Failed to find query: unauthorized", e); } catch (HttpClientErrorException.NotAcceptable e) { log.error("Failed to find query: format not acccepted: {}", e.getMessage()); - throw new ServiceException("Failed to find query: format not accepted", e); + throw new DataServiceException("Failed to find query: format not accepted", e); } if (!response.getStatusCode().equals(HttpStatus.OK)) { log.error("Failed to find query: wrong http code: {}", response.getStatusCode()); - throw new ServiceException("Failed to find query: wrong http code: " + response.getStatusCode()); + throw new DataServiceException("Failed to find query: wrong http code: " + response.getStatusCode()); } return response.getBody(); } @Override - public ExportResourceDto exportQuery(Long databaseId, Long queryId) throws ServiceConnectionException, - ServiceException, QueryNotFoundException { + public ExportResourceDto exportQuery(Long databaseId, Long queryId) throws DataServiceConnectionException, + DataServiceException, QueryNotFoundException { final ResponseEntity<ExportResourceDto> response; final String url = "/api/database/" + databaseId + "/subset/" + queryId; try { response = restTemplate.exchange(url, HttpMethod.GET, HttpEntity.EMPTY, ExportResourceDto.class); } catch (HttpServerErrorException e) { log.error("Failed to export query: {}", e.getMessage()); - throw new ServiceConnectionException("Failed to export query: " + e.getMessage(), e); + throw new DataServiceConnectionException("Failed to export query: " + e.getMessage(), e); } catch (HttpClientErrorException.NotFound e) { log.error("Failed to export query: not found: {}", e.getMessage()); throw new QueryNotFoundException("Failed to export query: not found: " + e.getMessage(), e); } catch (HttpClientErrorException.Unauthorized e) { log.error("Failed to export query: {}", e.getMessage()); - throw new ServiceException("Failed to export query: " + e.getMessage(), e); + throw new DataServiceException("Failed to export query: " + e.getMessage(), e); } if (!response.getStatusCode().equals(HttpStatus.OK)) { log.error("Failed to export query: wrong http code: {}", response.getStatusCode()); - throw new ServiceException("Failed to export query: wrong http code: " + response.getStatusCode()); + throw new DataServiceException("Failed to export query: wrong http code: " + response.getStatusCode()); } return response.getBody(); } @Override - public List<TableDto> getTableSchemas(Long databaseId) throws ServiceConnectionException, ServiceException, QueryNotFoundException { + public List<TableDto> getTableSchemas(Long databaseId) throws DataServiceConnectionException, DataServiceException, + TableNotFoundException { final ResponseEntity<TableDto[]> response; final String url = "/api/database/" + databaseId + "/table"; try { response = restTemplate.exchange(url, HttpMethod.GET, HttpEntity.EMPTY, TableDto[].class); } catch (HttpServerErrorException e) { log.error("Failed to get table schemas: {}", e.getMessage()); - throw new ServiceConnectionException("Failed to get table schemas: " + e.getMessage(), e); + throw new DataServiceConnectionException("Failed to get table schemas: " + e.getMessage(), e); } catch (HttpClientErrorException.NotFound e) { log.error("Failed to get table schemas: not found: {}", e.getMessage()); - throw new QueryNotFoundException("Failed to get table schemas: not found: " + e.getMessage(), e); + throw new TableNotFoundException("Failed to get table schemas: not found: " + e.getMessage(), e); } catch (HttpClientErrorException.Unauthorized e) { log.error("Failed to get table schemas: {}", e.getMessage()); - throw new ServiceException("Failed to get table schemas: " + e.getMessage(), e); + throw new DataServiceException("Failed to get table schemas: " + e.getMessage(), e); } if (!response.getStatusCode().equals(HttpStatus.OK)) { log.error("Failed to get table schemas: wrong http code: {}", response.getStatusCode()); - throw new ServiceException("Failed to get table schemas: wrong http code: " + response.getStatusCode()); + throw new DataServiceException("Failed to get table schemas: wrong http code: " + response.getStatusCode()); } if (response.getBody() == null) { log.error("Failed to get table schemas: empty body: {}", response.getStatusCode()); - throw new ServiceException("Failed to get table schemas: empty body: " + response.getStatusCode()); + throw new DataServiceException("Failed to get table schemas: empty body: " + response.getStatusCode()); } final List<TableDto> tables = Arrays.asList(response.getBody()); log.debug("found {} table(s) in data service", tables.size()); @@ -323,28 +328,29 @@ public class DataServiceGatewayImpl implements DataServiceGateway { } @Override - public List<ViewDto> getViewSchemas(Long databaseId) throws ServiceConnectionException, ServiceException, QueryNotFoundException { + public List<ViewDto> getViewSchemas(Long databaseId) throws DataServiceConnectionException, DataServiceException, + ViewNotFoundException { final ResponseEntity<ViewDto[]> response; final String url = "/api/database/" + databaseId + "/view"; try { response = restTemplate.exchange(url, HttpMethod.GET, HttpEntity.EMPTY, ViewDto[].class); } catch (HttpServerErrorException e) { log.error("Failed to get view schemas: {}", e.getMessage()); - throw new ServiceConnectionException("Failed to get view schemas: " + e.getMessage(), e); + throw new DataServiceConnectionException("Failed to get view schemas: " + e.getMessage(), e); } catch (HttpClientErrorException.NotFound e) { log.error("Failed to get view schemas: not found: {}", e.getMessage()); - throw new QueryNotFoundException("Failed to get view schemas: not found: " + e.getMessage(), e); + throw new ViewNotFoundException("Failed to get view schemas: not found: " + e.getMessage(), e); } catch (HttpClientErrorException.Unauthorized e) { log.error("Failed to get view schemas: {}", e.getMessage()); - throw new ServiceException("Failed to get view schemas: " + e.getMessage(), e); + throw new DataServiceException("Failed to get view schemas: " + e.getMessage(), e); } if (!response.getStatusCode().equals(HttpStatus.OK)) { log.error("Failed to get view schemas: wrong http code: {}", response.getStatusCode()); - throw new ServiceException("Failed to get view schemas: wrong http code: " + response.getStatusCode()); + throw new DataServiceException("Failed to get view schemas: wrong http code: " + response.getStatusCode()); } if (response.getBody() == null) { log.error("Failed to get view schemas: empty body: {}", response.getStatusCode()); - throw new ServiceException("Failed to get view schemas: empty body: " + response.getStatusCode()); + throw new DataServiceException("Failed to get view schemas: empty body: " + response.getStatusCode()); } final List<ViewDto> views = Arrays.asList(response.getBody()); log.debug("found {} view(s) in data service", views.size()); @@ -352,28 +358,28 @@ public class DataServiceGatewayImpl implements DataServiceGateway { } @Override - public TableStatisticDto getTableStatistics(Long databaseId, Long tableId) throws ServiceConnectionException, ServiceException, TableNotFoundException { + public TableStatisticDto getTableStatistics(Long databaseId, Long tableId) throws DataServiceConnectionException, DataServiceException, TableNotFoundException { final ResponseEntity<TableStatisticDto> response; final String url = "/api/database/" + databaseId + "/table/" + tableId + "/statistic"; try { response = restTemplate.exchange(url, HttpMethod.GET, HttpEntity.EMPTY, TableStatisticDto.class); } catch (HttpServerErrorException e) { log.error("Failed to analyse table statistic: {}", e.getMessage()); - throw new ServiceConnectionException("Failed to analyse table statistic: " + e.getMessage(), e); + throw new DataServiceConnectionException("Failed to analyse table statistic: " + e.getMessage(), e); } catch (HttpClientErrorException.NotFound e) { log.error("Failed to analyse table statistic: not found: {}", e.getMessage()); throw new TableNotFoundException("Failed to analyse table statistic: not found: " + e.getMessage(), e); } catch (HttpClientErrorException.Unauthorized e) { log.error("Failed to analyse table statistic: {}", e.getMessage()); - throw new ServiceException("Failed to analyse table statistic: " + e.getMessage(), e); + throw new DataServiceException("Failed to analyse table statistic: " + e.getMessage(), e); } if (!response.getStatusCode().equals(HttpStatus.OK)) { log.error("Failed to analyse table statistic: wrong http code: {}", response.getStatusCode()); - throw new ServiceException("Failed to analyse table statistic: wrong http code: " + response.getStatusCode()); + throw new DataServiceException("Failed to analyse table statistic: wrong http code: " + response.getStatusCode()); } if (response.getBody() == null) { log.error("Failed to analyse table statistic: empty body: {}", response.getStatusCode()); - throw new ServiceException("Failed to analyse table statistic: empty body: " + response.getStatusCode()); + throw new DataServiceException("Failed to analyse table statistic: empty body: " + response.getStatusCode()); } return response.getBody(); } diff --git a/dbrepo-metadata-service/services/src/main/java/at/tuwien/gateway/impl/KeycloakGatewayImpl.java b/dbrepo-metadata-service/services/src/main/java/at/tuwien/gateway/impl/KeycloakGatewayImpl.java index 91ec52d8e0..d05243f9a5 100644 --- a/dbrepo-metadata-service/services/src/main/java/at/tuwien/gateway/impl/KeycloakGatewayImpl.java +++ b/dbrepo-metadata-service/services/src/main/java/at/tuwien/gateway/impl/KeycloakGatewayImpl.java @@ -24,12 +24,15 @@ import java.util.UUID; public class KeycloakGatewayImpl implements KeycloakGateway { private final RestTemplate restTemplate; + private final RestTemplate keycloakRestTemplate; private final KeycloakConfig keycloakConfig; private final MetadataMapper metadataMapper; - public KeycloakGatewayImpl(@Qualifier("keycloakRestTemplate") RestTemplate restTemplate, + public KeycloakGatewayImpl(@Qualifier("restTemplate") RestTemplate restTemplate, + @Qualifier("keycloakRestTemplate") RestTemplate keycloakRestTemplate, KeycloakConfig keycloakConfig, MetadataMapper metadataMapper) { this.restTemplate = restTemplate; + this.keycloakRestTemplate = keycloakRestTemplate; this.keycloakConfig = keycloakConfig; this.metadataMapper = metadataMapper; } @@ -45,13 +48,9 @@ public class KeycloakGatewayImpl implements KeycloakGateway { payload.add("client_id", "admin-cli"); final String url = keycloakConfig.getKeycloakEndpoint() + "/realms/master/protocol/openid-connect/token"; log.trace("request admin token from url: {}", url); - log.trace("request username: {}", keycloakConfig.getKeycloakUsername()); - log.trace("request password: {}", keycloakConfig.getKeycloakPassword() != null ? "(set)" : "(not set)"); - log.trace("request client_id: admin-cli"); - log.trace("request client_secret: (not set)"); final ResponseEntity<TokenDto> response; try { - response = restTemplate.exchange(url, HttpMethod.POST, new HttpEntity<>(payload, headers), TokenDto.class); + response = keycloakRestTemplate.exchange(url, HttpMethod.POST, new HttpEntity<>(payload, headers), TokenDto.class); } catch (HttpServerErrorException e) { log.error("Failed to obtain admin token: {}", e.getMessage()); throw new AuthServiceConnectionException("Service unavailable", e); @@ -78,15 +77,10 @@ public class KeycloakGatewayImpl implements KeycloakGateway { payload.add("client_id", keycloakConfig.getKeycloakClient()); payload.add("client_secret", keycloakConfig.getKeycloakClientSecret()); final String url = keycloakConfig.getKeycloakEndpoint() + "/realms/dbrepo/protocol/openid-connect/token"; - log.trace("request user token from url: {}", url); - log.trace("request username: {}", username); - log.trace("request password: {}", password != null ? "(set)" : "(not set)"); - log.trace("request client_id: {}", keycloakConfig.getKeycloakClient()); - log.trace("request client_secret: {}", keycloakConfig.getKeycloakClientSecret()); + log.trace("request admin token from url: {}", url); final ResponseEntity<TokenDto> response; try { - response = new RestTemplate() - .exchange(url, HttpMethod.POST, new HttpEntity<>(payload, headers), TokenDto.class); + response = restTemplate.exchange(url, HttpMethod.POST, new HttpEntity<>(payload, headers), TokenDto.class); } catch (HttpServerErrorException e) { log.error("Failed to obtain user token: {}", e.getMessage()); throw new AuthServiceConnectionException("Service unavailable", e); @@ -119,8 +113,7 @@ public class KeycloakGatewayImpl implements KeycloakGateway { log.trace("request user token from url: {}", url); final ResponseEntity<TokenDto> response; try { - response = new RestTemplate() - .exchange(url, HttpMethod.POST, new HttpEntity<>(payload, headers), TokenDto.class); + response = restTemplate.exchange(url, HttpMethod.POST, new HttpEntity<>(payload, headers), TokenDto.class); } catch (HttpServerErrorException e) { log.error("Failed to refresh user token: {}", e.getMessage()); throw new AuthServiceConnectionException("Service unavailable", e); @@ -128,7 +121,7 @@ public class KeycloakGatewayImpl implements KeycloakGateway { log.error("Failed to refresh user token: invalid credentials"); throw new CredentialsInvalidException("Invalid credentials", e); } catch (HttpClientErrorException.BadRequest e) { - if (e.getMessage().contains("Session not active")) { + if (e.getMessage() != null && e.getMessage().contains("Session not active")) { log.error("Failed to refresh user token: inactive session", e); throw new CredentialsInvalidException("Inactive session", e); } @@ -148,18 +141,20 @@ public class KeycloakGatewayImpl implements KeycloakGateway { log.debug("create user at url {}", url); final ResponseEntity<Void> response; try { - response = restTemplate.exchange(url, HttpMethod.POST, new HttpEntity<>(data, headers), Void.class); + response = keycloakRestTemplate.exchange(url, HttpMethod.POST, new HttpEntity<>(data, headers), Void.class); } catch (HttpServerErrorException e) { log.error("Failed to create user: {}", e.getMessage()); throw new AuthServiceConnectionException("Service unavailable", e); } catch (HttpClientErrorException.Conflict e) { - if (e.getMessage().contains("same email")) { - log.error("Failed to create user: email exists: {}", e.getMessage()); - throw new EmailExistsException("E-Mail exists", e); - } else { - log.error("Failed to create user: user exists: {}", e.getMessage()); - throw new UserExistsException("User exists", e); + if (e.getResponseBodyAsByteArray() != null && e.getResponseBodyAsByteArray().length > 0) { + final KeycloakErrorDto error = e.getResponseBodyAs(KeycloakErrorDto.class); + if (error != null && error.getErrorMessage().contains("same email")) { + log.error("Failed to create user: email exists: {}", e.getMessage()); + throw new EmailExistsException("E-Mail exists", e); + } } + log.error("Failed to create user: user exists: {}", e.getMessage()); + throw new UserExistsException("User exists", e); } if (!response.getStatusCode().equals(HttpStatus.CREATED)) { log.error("Failed to create user: unexpected status: {}", response.getStatusCode().value()); @@ -178,7 +173,7 @@ public class KeycloakGatewayImpl implements KeycloakGateway { log.debug("delete user at url {}", url); final ResponseEntity<Void> response; try { - response = restTemplate.exchange(url, HttpMethod.DELETE, new HttpEntity<>(null, headers), Void.class); + response = keycloakRestTemplate.exchange(url, HttpMethod.DELETE, new HttpEntity<>(null, headers), Void.class); } catch (HttpServerErrorException e) { log.error("Failed to delete user: {}", e.getMessage()); throw new AuthServiceConnectionException("Service unavailable", e); @@ -207,7 +202,7 @@ public class KeycloakGatewayImpl implements KeycloakGateway { log.debug("update user credentials at url {}", url); final ResponseEntity<Void> response; try { - response = restTemplate.exchange(url, HttpMethod.PUT, new HttpEntity<>(payload, headers), Void.class); + response = keycloakRestTemplate.exchange(url, HttpMethod.PUT, new HttpEntity<>(payload, headers), Void.class); } catch (HttpServerErrorException e) { log.error("Failed to update user credentials: {}", e.getMessage()); throw new AuthServiceConnectionException("Service unavailable", e); @@ -232,7 +227,7 @@ public class KeycloakGatewayImpl implements KeycloakGateway { log.debug("find user from url {}", url); final ResponseEntity<UserDto[]> response; try { - response = restTemplate.exchange(url, HttpMethod.GET, new HttpEntity<>(null, headers), UserDto[].class); + response = keycloakRestTemplate.exchange(url, HttpMethod.GET, new HttpEntity<>(null, headers), UserDto[].class); } catch (HttpServerErrorException e) { log.error("Failed to find user: {}", e.getMessage()); throw new AuthServiceConnectionException("Service unavailable", e); @@ -258,7 +253,7 @@ public class KeycloakGatewayImpl implements KeycloakGateway { log.debug("find user from url {}", url); final ResponseEntity<UserDto> response; try { - response = restTemplate.exchange(url, HttpMethod.GET, new HttpEntity<>(null, headers), UserDto.class); + response = keycloakRestTemplate.exchange(url, HttpMethod.GET, new HttpEntity<>(null, headers), UserDto.class); } catch (HttpServerErrorException e) { log.error("Failed to find user: {}", e.getMessage()); throw new AuthServiceConnectionException("Service unavailable", e); diff --git a/dbrepo-metadata-service/services/src/main/java/at/tuwien/gateway/impl/OrcidGatewayImpl.java b/dbrepo-metadata-service/services/src/main/java/at/tuwien/gateway/impl/OrcidGatewayImpl.java index debbe4d66c..15b73eb193 100644 --- a/dbrepo-metadata-service/services/src/main/java/at/tuwien/gateway/impl/OrcidGatewayImpl.java +++ b/dbrepo-metadata-service/services/src/main/java/at/tuwien/gateway/impl/OrcidGatewayImpl.java @@ -4,6 +4,7 @@ import at.tuwien.api.orcid.OrcidDto; import at.tuwien.exception.OrcidNotFoundException; import at.tuwien.gateway.OrcidGateway; import lombok.extern.log4j.Log4j2; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; @@ -19,8 +20,9 @@ public class OrcidGatewayImpl implements OrcidGateway { private final RestTemplate restTemplate; - public OrcidGatewayImpl() { - this.restTemplate = new RestTemplate(); + @Autowired + public OrcidGatewayImpl(RestTemplate restTemplate) { + this.restTemplate = restTemplate; } @Override @@ -31,7 +33,7 @@ public class OrcidGatewayImpl implements OrcidGateway { try { log.debug("find orcid from url {}", url); response = restTemplate.exchange(url, HttpMethod.GET, new HttpEntity<>(null, headers), OrcidDto.class); - } catch (ResourceAccessException | HttpServerErrorException.ServiceUnavailable e) { + } catch (HttpServerErrorException e) { log.error("Failed to retrieve ORCID metadata from URL {}: {}", url, e.getMessage()); throw new OrcidNotFoundException("Failed to retrieve ORCID metadata from URL " + url + ": " + e.getMessage()); } diff --git a/dbrepo-metadata-service/services/src/main/java/at/tuwien/gateway/impl/RorGatewayImpl.java b/dbrepo-metadata-service/services/src/main/java/at/tuwien/gateway/impl/RorGatewayImpl.java index 6eeb74c3c0..7a5a64e8e2 100644 --- a/dbrepo-metadata-service/services/src/main/java/at/tuwien/gateway/impl/RorGatewayImpl.java +++ b/dbrepo-metadata-service/services/src/main/java/at/tuwien/gateway/impl/RorGatewayImpl.java @@ -4,6 +4,7 @@ import at.tuwien.api.ror.RorDto; import at.tuwien.exception.RorNotFoundException; import at.tuwien.gateway.RorGateway; import lombok.extern.log4j.Log4j2; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; @@ -19,8 +20,9 @@ public class RorGatewayImpl implements RorGateway { private final RestTemplate restTemplate; - public RorGatewayImpl() { - this.restTemplate = new RestTemplate(); + @Autowired + public RorGatewayImpl(RestTemplate restTemplate) { + this.restTemplate = restTemplate; } @Override @@ -32,7 +34,7 @@ public class RorGatewayImpl implements RorGateway { try { log.trace("find ror from url {}", url); response = restTemplate.exchange(url, HttpMethod.GET, new HttpEntity<>(null, headers), RorDto.class); - } catch (ResourceAccessException | HttpServerErrorException.ServiceUnavailable e) { + } catch (HttpServerErrorException e) { log.error("Failed to retrieve ROR metadata from URL {}: {}", url, e.getMessage()); throw new RorNotFoundException("Failed to retrieve ROR metadata from URL " + url + ": " + e.getMessage(), e); } diff --git a/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/AccessService.java b/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/AccessService.java index d5a4d03092..c47bb8daa3 100644 --- a/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/AccessService.java +++ b/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/AccessService.java @@ -5,10 +5,8 @@ import at.tuwien.entities.database.Database; import at.tuwien.entities.database.DatabaseAccess; import at.tuwien.entities.user.User; import at.tuwien.exception.*; -import org.springframework.transaction.annotation.Transactional; import java.util.List; -import java.util.UUID; public interface AccessService { @@ -37,11 +35,11 @@ public interface AccessService { * @param access The access. * @param user The user. * @return The database access, if successful. - * @throws ServiceException The data service responded with unexpected behavior. - * @throws ServiceConnectionException The connection with the data service could not be established. + * @throws DataServiceException The data service responded with unexpected behavior. + * @throws DataServiceConnectionException The connection with the data service could not be established. * @throws DatabaseNotFoundException The database was not found in the metadata/search database. */ - DatabaseAccess create(Database database, User user, AccessTypeDto access) throws ServiceException, ServiceConnectionException, + DatabaseAccess create(Database database, User user, AccessTypeDto access) throws DataServiceException, DataServiceConnectionException, DatabaseNotFoundException, SearchServiceException, SearchServiceConnectionException; /** @@ -50,11 +48,11 @@ public interface AccessService { * @param database The database. * @param user The user. * @param access The updated access. - * @throws ServiceException The data service responded with unexpected behavior. - * @throws ServiceConnectionException The connection with the data service could not be established. + * @throws DataServiceException The data service responded with unexpected behavior. + * @throws DataServiceConnectionException The connection with the data service could not be established. * @throws DatabaseNotFoundException The database was not found in the metadata/search database. */ - void update(Database database, User user, AccessTypeDto access) throws ServiceException, ServiceConnectionException, + void update(Database database, User user, AccessTypeDto access) throws at.tuwien.exception.DataServiceException, DataServiceConnectionException, AccessNotFoundException, DatabaseNotFoundException, SearchServiceException, SearchServiceConnectionException; /** @@ -62,10 +60,10 @@ public interface AccessService { * * @param database The database. * @param user The user. - * @throws ServiceException The data service responded with unexpected behavior. - * @throws ServiceConnectionException The connection with the data service could not be established. + * @throws DataServiceException The data service responded with unexpected behavior. + * @throws DataServiceConnectionException The connection with the data service could not be established. * @throws DatabaseNotFoundException The database was not found in the search database. */ - void delete(Database database, User user) throws AccessNotFoundException, ServiceException, - ServiceConnectionException, DatabaseNotFoundException, SearchServiceException, SearchServiceConnectionException; + void delete(Database database, User user) throws AccessNotFoundException, DataServiceException, + DataServiceConnectionException, DatabaseNotFoundException, SearchServiceException, SearchServiceConnectionException; } diff --git a/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/BrokerService.java b/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/BrokerService.java index 6c0021b450..f249f7a2cf 100644 --- a/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/BrokerService.java +++ b/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/BrokerService.java @@ -10,12 +10,12 @@ public interface BrokerService { * * @param user The user. */ - void setVirtualHostPermissions(User user) throws ServiceException, ServiceConnectionException; + void setVirtualHostPermissions(User user) throws BrokerServiceException, BrokerServiceConnectionException; /** * Sets topic exchange permissions for a user. * * @param user The user. */ - void setTopicExchangePermissions(User user) throws ServiceException, ServiceConnectionException; + void setTopicExchangePermissions(User user) throws BrokerServiceException, BrokerServiceConnectionException; } diff --git a/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/DatabaseService.java b/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/DatabaseService.java index 32291c8755..aa25ee1362 100644 --- a/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/DatabaseService.java +++ b/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/DatabaseService.java @@ -2,15 +2,11 @@ package at.tuwien.service; import at.tuwien.api.database.DatabaseCreateDto; import at.tuwien.api.database.DatabaseModifyVisibilityDto; -import at.tuwien.api.database.DatabaseTransferDto; import at.tuwien.entities.database.Database; import at.tuwien.entities.user.User; import at.tuwien.exception.*; import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Propagation; -import org.springframework.transaction.annotation.Transactional; -import java.security.Principal; import java.util.List; import java.util.UUID; @@ -55,21 +51,21 @@ public interface DatabaseService { * @param user The user. * @return The database, if successful. * @throws UserNotFoundException If the container/user was not found in the metadata database. - * @throws ServiceException If the data service returned non-successfully. - * @throws ServiceConnectionException If failing to connect to the data service/search service. + * @throws DataServiceException If the data service returned non-successfully. + * @throws DataServiceConnectionException If failing to connect to the data service/search service. */ Database create(DatabaseCreateDto createDto, User user) throws UserNotFoundException, ContainerNotFoundException, - ServiceException, ServiceConnectionException, DatabaseNotFoundException, SearchServiceException, SearchServiceConnectionException; + DataServiceException, DataServiceConnectionException, DatabaseNotFoundException, SearchServiceException, SearchServiceConnectionException; /** * Updates the user's password. * * @param database The database. * @param user The user. - * @throws ServiceException If the data service returned non-successfully. - * @throws ServiceConnectionException If failing to connect to the data service. + * @throws DataServiceException If the data service returned non-successfully. + * @throws DataServiceConnectionException If failing to connect to the data service. */ - void updatePassword(Database database, User user) throws ServiceException, ServiceConnectionException, DatabaseNotFoundException; + void updatePassword(Database database, User user) throws DataServiceException, DataServiceConnectionException, DatabaseNotFoundException; /** * Updates the visibility of the database. @@ -78,7 +74,7 @@ public interface DatabaseService { * @param data The visibility * @return The database, if successful. * @throws NotFoundException The database was not found in the metadata database. - * @throws ServiceConnectionException If failing to connect to the search service. + * @throws DataServiceConnectionException If failing to connect to the search service. */ Database modifyVisibility(Database database, DatabaseModifyVisibilityDto data) throws DatabaseNotFoundException, SearchServiceException, SearchServiceConnectionException; @@ -101,11 +97,11 @@ public interface DatabaseService { */ Database modifyImage(Database database, byte[] image) throws DatabaseNotFoundException, SearchServiceException, SearchServiceConnectionException; - Database updateTableMetadata(Database database) throws DatabaseNotFoundException, ServiceException, + Database updateTableMetadata(Database database) throws DatabaseNotFoundException, DataServiceException, SearchServiceException, SearchServiceConnectionException, QueryNotFoundException, - ServiceConnectionException, MalformedException; + DataServiceConnectionException, MalformedException, TableNotFoundException; - Database updateViewMetadata(Database database) throws DatabaseNotFoundException, ServiceException, + Database updateViewMetadata(Database database) throws DatabaseNotFoundException, DataServiceException, SearchServiceException, SearchServiceConnectionException, QueryNotFoundException, - ServiceConnectionException; + DataServiceConnectionException, ViewNotFoundException; } diff --git a/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/IdentifierService.java b/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/IdentifierService.java index e88f75b52e..4d0228f410 100644 --- a/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/IdentifierService.java +++ b/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/IdentifierService.java @@ -85,8 +85,20 @@ public interface IdentifierService { */ List<Identifier> findAll(IdentifierTypeDto type, Long databaseId, Long queryId, Long viewId, Long tableId); + /** + * Publishes a draft identifier with DataCite. + * @param identifierId The identifier id. + * @return The resulting identifier. + * @throws SearchServiceException + * @throws DatabaseNotFoundException + * @throws SearchServiceConnectionException + * @throws MalformedException + * @throws DataServiceConnectionException + * @throws IdentifierNotFoundException + */ Identifier publish(Long identifierId) throws SearchServiceException, DatabaseNotFoundException, - SearchServiceConnectionException, MalformedException, ServiceConnectionException, IdentifierNotFoundException; + SearchServiceConnectionException, MalformedException, DataServiceConnectionException, + IdentifierNotFoundException, ExternalServiceException; /** * Creates a new identifier in the metadata database for a query or database. @@ -95,10 +107,19 @@ public interface IdentifierService { * @param user The user. * @param data The data. * @return The created identifier from the metadata database if successful. + * @throws DataServiceException + * @throws DataServiceConnectionException + * @throws IdentifierNotFoundException + * @throws MalformedException + * @throws ViewNotFoundException + * @throws DatabaseNotFoundException + * @throws QueryNotFoundException + * @throws SearchServiceException + * @throws SearchServiceConnectionException */ - Identifier save(Database database, User user, IdentifierSaveDto data) throws ServiceException, - ServiceConnectionException, IdentifierNotFoundException, MalformedException, ViewNotFoundException, - DatabaseNotFoundException, QueryNotFoundException, SearchServiceException, SearchServiceConnectionException; + Identifier save(Database database, User user, IdentifierSaveDto data) throws DataServiceException, + DataServiceConnectionException, IdentifierNotFoundException, MalformedException, ViewNotFoundException, + DatabaseNotFoundException, QueryNotFoundException, SearchServiceException, SearchServiceConnectionException, ExternalServiceException; /** * Creates a new identifier in the metadata database for a query or database. @@ -107,10 +128,19 @@ public interface IdentifierService { * @param user The user. * @param data The data. * @return The created identifier from the metadata database if successful. + * @throws DataServiceException + * @throws DataServiceConnectionException + * @throws IdentifierNotFoundException + * @throws MalformedException + * @throws ViewNotFoundException + * @throws DatabaseNotFoundException + * @throws QueryNotFoundException + * @throws SearchServiceException + * @throws SearchServiceConnectionException */ - Identifier create(Database database, User user, IdentifierCreateDto data) throws ServiceException, - ServiceConnectionException, IdentifierNotFoundException, MalformedException, ViewNotFoundException, - DatabaseNotFoundException, QueryNotFoundException, SearchServiceException, SearchServiceConnectionException; + Identifier create(Database database, User user, IdentifierCreateDto data) throws DataServiceException, + DataServiceConnectionException, IdentifierNotFoundException, MalformedException, ViewNotFoundException, + DatabaseNotFoundException, QueryNotFoundException, SearchServiceException, SearchServiceConnectionException, ExternalServiceException; /** * Export metadata for a identifier @@ -135,8 +165,12 @@ public interface IdentifierService { * * @param identifier The identifier. * @return The XML resource, if successful. + * @throws DataServiceException + * @throws DataServiceConnectionException + * @throws IdentifierNotFoundException + * @throws QueryNotFoundException */ - InputStreamResource exportResource(Identifier identifier) throws ServiceException, ServiceConnectionException, + InputStreamResource exportResource(Identifier identifier) throws DataServiceException, DataServiceConnectionException, IdentifierNotFoundException, QueryNotFoundException; /** @@ -144,7 +178,13 @@ public interface IdentifierService { * database, but sets it as deleted. * * @param identifier The identifier. + * @throws DataServiceException + * @throws DataServiceConnectionException + * @throws IdentifierNotFoundException + * @throws DatabaseNotFoundException + * @throws SearchServiceException + * @throws SearchServiceConnectionException */ - void delete(Identifier identifier) throws ServiceException, ServiceConnectionException, IdentifierNotFoundException, + void delete(Identifier identifier) throws DataServiceException, DataServiceConnectionException, IdentifierNotFoundException, DatabaseNotFoundException, SearchServiceException, SearchServiceConnectionException; } diff --git a/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/StorageService.java b/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/StorageService.java index 0bed64884e..7cb195ce4e 100644 --- a/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/StorageService.java +++ b/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/StorageService.java @@ -14,6 +14,7 @@ public interface StorageService { * @param key The object key. * @return The input stream, if successful. * @throws StorageUnavailableException The object failed to be loaded from the Storage Service. + * @throws StorageNotFoundException The object could not be found in the Storage Service. */ InputStream getObject(String bucket, String key) throws StorageNotFoundException, StorageUnavailableException; @@ -24,6 +25,7 @@ public interface StorageService { * @param key The object key. * @return The byte array. * @throws StorageUnavailableException The object failed to be loaded from the Storage Service. + * @throws StorageNotFoundException The object could not be found in the Storage Service. */ byte[] getBytes(String key) throws StorageUnavailableException, StorageNotFoundException; @@ -34,6 +36,7 @@ public interface StorageService { * @param key The object key. * @return The byte array. * @throws StorageUnavailableException The object failed to be loaded from the Storage Service. + * @throws StorageNotFoundException The object could not be found in the Storage Service. */ byte[] getBytes(String bucket, String key) throws StorageNotFoundException, StorageUnavailableException; } diff --git a/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/TableService.java b/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/TableService.java index 0eb228ccd5..e476721906 100644 --- a/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/TableService.java +++ b/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/TableService.java @@ -1,17 +1,13 @@ package at.tuwien.service; import at.tuwien.api.database.table.TableCreateDto; -import at.tuwien.api.database.table.TableHistoryDto; -import at.tuwien.api.database.table.TableStatisticDto; import at.tuwien.api.database.table.columns.concepts.ColumnSemanticsUpdateDto; import at.tuwien.entities.database.Database; import at.tuwien.entities.database.table.Table; import at.tuwien.entities.database.table.columns.TableColumn; import at.tuwien.exception.*; -import org.springframework.transaction.annotation.Transactional; import java.security.Principal; -import java.util.List; public interface TableService { @@ -43,7 +39,7 @@ public interface TableService { * @return The created table. */ Table createTable(Database database, TableCreateDto createDto, Principal principal) - throws TableNotFoundException, ServiceException, ServiceConnectionException, UserNotFoundException, + throws TableNotFoundException, DataServiceException, DataServiceConnectionException, UserNotFoundException, DatabaseNotFoundException, TableExistsException, SearchServiceException, SearchServiceConnectionException, MalformedException, OntologyNotFoundException, SemanticEntityNotFoundException; /** @@ -51,12 +47,12 @@ public interface TableService { * * @param table The table. */ - void deleteTable(Table table) throws ServiceException, ServiceConnectionException, DatabaseNotFoundException, TableNotFoundException, SearchServiceException, SearchServiceConnectionException; + void deleteTable(Table table) throws DataServiceException, DataServiceConnectionException, DatabaseNotFoundException, TableNotFoundException, SearchServiceException, SearchServiceConnectionException; - TableColumn update(TableColumn column, ColumnSemanticsUpdateDto updateDto) throws ServiceException, - ServiceConnectionException, DatabaseNotFoundException, SearchServiceException, SearchServiceConnectionException, MalformedException, OntologyNotFoundException, SemanticEntityNotFoundException; + TableColumn update(TableColumn column, ColumnSemanticsUpdateDto updateDto) throws DataServiceException, + DataServiceConnectionException, DatabaseNotFoundException, SearchServiceException, SearchServiceConnectionException, MalformedException, OntologyNotFoundException, SemanticEntityNotFoundException; TableColumn findColumnById(Table table, Long columnId) throws MalformedException; - void updateStatistics(Table table) throws SearchServiceException, DatabaseNotFoundException, SearchServiceConnectionException, MalformedException, TableNotFoundException, ServiceException, ServiceConnectionException; + void updateStatistics(Table table) throws SearchServiceException, DatabaseNotFoundException, SearchServiceConnectionException, MalformedException, TableNotFoundException, DataServiceException, DataServiceConnectionException; } diff --git a/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/ViewService.java b/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/ViewService.java index d19a3be73b..a090ece3cb 100644 --- a/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/ViewService.java +++ b/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/ViewService.java @@ -33,7 +33,7 @@ public interface ViewService { * * @param view The view. */ - void delete(View view) throws ServiceException, ServiceConnectionException, DatabaseNotFoundException, + void delete(View view) throws DataServiceException, DataServiceConnectionException, DatabaseNotFoundException, ViewNotFoundException, SearchServiceException, SearchServiceConnectionException; /** @@ -44,6 +44,6 @@ public interface ViewService { * @param data The given query. * @return The view that was created. */ - View create(Database database, User user, ViewCreateDto data) throws MalformedException, ServiceException, - ServiceConnectionException, DatabaseNotFoundException, SearchServiceException, SearchServiceConnectionException; + View create(Database database, User user, ViewCreateDto data) throws MalformedException, DataServiceException, + DataServiceConnectionException, DatabaseNotFoundException, SearchServiceException, SearchServiceConnectionException; } diff --git a/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/AccessServiceImpl.java b/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/AccessServiceImpl.java index e1e6924e76..aaa50251c3 100644 --- a/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/AccessServiceImpl.java +++ b/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/AccessServiceImpl.java @@ -62,8 +62,8 @@ public class AccessServiceImpl implements AccessService { @Override @Transactional - public DatabaseAccess create(Database database, User user, AccessTypeDto type) throws ServiceException, - ServiceConnectionException, DatabaseNotFoundException, SearchServiceException, + public DatabaseAccess create(Database database, User user, AccessTypeDto type) throws DataServiceException, + DataServiceConnectionException, DatabaseNotFoundException, SearchServiceException, SearchServiceConnectionException { /* create in data database */ dataServiceGateway.createAccess(database.getId(), user.getId(), type); @@ -85,8 +85,8 @@ public class AccessServiceImpl implements AccessService { @Override @Transactional - public void update(Database database, User user, AccessTypeDto access) throws ServiceException, - ServiceConnectionException, AccessNotFoundException, DatabaseNotFoundException, SearchServiceException, + public void update(Database database, User user, AccessTypeDto access) throws DataServiceException, + DataServiceConnectionException, AccessNotFoundException, DatabaseNotFoundException, SearchServiceException, SearchServiceConnectionException { /* update in data database */ dataServiceGateway.updateAccess(database.getId(), user.getId(), access); @@ -112,8 +112,8 @@ public class AccessServiceImpl implements AccessService { @Override @Transactional - public void delete(Database database, User user) throws AccessNotFoundException, ServiceException, - ServiceConnectionException, DatabaseNotFoundException, SearchServiceException, + public void delete(Database database, User user) throws AccessNotFoundException, DataServiceException, + DataServiceConnectionException, DatabaseNotFoundException, SearchServiceException, SearchServiceConnectionException { /* delete in data database */ dataServiceGateway.deleteAccess(database.getId(), user.getId()); diff --git a/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/BrokerServiceRabbitMqImpl.java b/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/BrokerServiceRabbitMqImpl.java index c0ce71c996..2800c96bc9 100644 --- a/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/BrokerServiceRabbitMqImpl.java +++ b/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/BrokerServiceRabbitMqImpl.java @@ -27,7 +27,7 @@ public class BrokerServiceRabbitMqImpl implements BrokerService { } @Override - public void setVirtualHostPermissions(User user) throws ServiceException, ServiceConnectionException { + public void setVirtualHostPermissions(User user) throws BrokerServiceException, BrokerServiceConnectionException { final GrantVirtualHostPermissionsDto permissions = GrantVirtualHostPermissionsDto.builder() .configure("") .write(".*") @@ -39,7 +39,7 @@ public class BrokerServiceRabbitMqImpl implements BrokerService { @Override @Transactional(readOnly = true) - public void setTopicExchangePermissions(User user) throws ServiceException, ServiceConnectionException { + public void setTopicExchangePermissions(User user) throws BrokerServiceException, BrokerServiceConnectionException { final GrantExchangePermissionsDto permissions = GrantExchangePermissionsDto.builder() .exchange(rabbitConfig.getExchangeName()) .write(userToExchangeWritePermissionString(user)) diff --git a/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/DataCiteIdentifierServiceImpl.java b/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/DataCiteIdentifierServiceImpl.java index 87f178b1c0..ed58148707 100644 --- a/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/DataCiteIdentifierServiceImpl.java +++ b/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/DataCiteIdentifierServiceImpl.java @@ -13,6 +13,7 @@ import at.tuwien.config.DataCiteConfig; import at.tuwien.config.EndpointConfig; import at.tuwien.entities.database.Database; import at.tuwien.entities.identifier.Identifier; +import at.tuwien.entities.identifier.IdentifierStatusType; import at.tuwien.entities.user.User; import at.tuwien.exception.*; import at.tuwien.mapper.MetadataMapper; @@ -69,9 +70,10 @@ public class DataCiteIdentifierServiceImpl implements IdentifierService { @Override @Transactional - public Identifier publish(Long identifierId) throws MalformedException, ServiceConnectionException, - IdentifierNotFoundException { + public Identifier publish(Long identifierId) throws MalformedException, DataServiceConnectionException, + IdentifierNotFoundException, ExternalServiceException { final Identifier identifier = find(identifierId); + identifier.setStatus(IdentifierStatusType.PUBLISHED); identifier.setDoi(remoteSave(identifier, DataCiteDoiEvent.PUBLISH)); return identifierRepository.save(identifier); } @@ -94,19 +96,20 @@ public class DataCiteIdentifierServiceImpl implements IdentifierService { @Override @Transactional(rollbackFor = {Exception.class}) - public Identifier save(Database database, User user, IdentifierSaveDto data) throws ServiceException, - ServiceConnectionException, MalformedException, DatabaseNotFoundException, IdentifierNotFoundException, - ViewNotFoundException, QueryNotFoundException, SearchServiceException, SearchServiceConnectionException { + public Identifier save(Database database, User user, IdentifierSaveDto data) throws DataServiceException, + DataServiceConnectionException, MalformedException, DatabaseNotFoundException, IdentifierNotFoundException, + ViewNotFoundException, QueryNotFoundException, SearchServiceException, SearchServiceConnectionException, + ExternalServiceException { data.setDoi(remoteSave(identifierService.save(database, user, data), DataCiteDoiEvent.REGISTER)); return identifierService.save(database, user, data); } @Override @Transactional(rollbackFor = {Exception.class}) - public Identifier create(Database database, User user, IdentifierCreateDto data) throws ServiceException, - ServiceConnectionException, IdentifierNotFoundException, MalformedException, ViewNotFoundException, + public Identifier create(Database database, User user, IdentifierCreateDto data) throws DataServiceException, + DataServiceConnectionException, IdentifierNotFoundException, MalformedException, ViewNotFoundException, DatabaseNotFoundException, QueryNotFoundException, SearchServiceException, - SearchServiceConnectionException { + SearchServiceConnectionException, ExternalServiceException { data.setDoi(remoteSave(identifierService.create(database, user, data), DataCiteDoiEvent.REGISTER)); return identifierService.create(database, user, data); } @@ -118,14 +121,15 @@ public class DataCiteIdentifierServiceImpl implements IdentifierService { * @param event The PID status event, e.g. publish * @return The DOI for this PID. * @throws MalformedException - * @throws ServiceConnectionException + * @throws DataServiceConnectionException + * @throws ExternalServiceException */ public String remoteSave(Identifier identifier, DataCiteDoiEvent event) throws MalformedException, - ServiceConnectionException { + DataServiceConnectionException, ExternalServiceException { final HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); headers.setBasicAuth(dataCiteConfig.getUsername(), dataCiteConfig.getPassword()); - HttpEntity<DataCiteBody<DataCiteCreateDoi>> request = new HttpEntity<>( + final HttpEntity<DataCiteBody<DataCiteCreateDoi>> request = new HttpEntity<>( DataCiteBody.<DataCiteCreateDoi>builder() .data(DataCiteData.<DataCiteCreateDoi>builder() .type("dois") @@ -139,11 +143,11 @@ public class DataCiteIdentifierServiceImpl implements IdentifierService { final String url = dataCiteConfig.getUrl() + "/dois"; log.trace("request doi from url {}", url); try { - ResponseEntity<DataCiteBody<DataCiteDoi>> response = restTemplate.exchange(url, HttpMethod.POST, + final ResponseEntity<DataCiteBody<DataCiteDoi>> response = restTemplate.exchange(url, HttpMethod.POST, request, dataCiteBodyParameterizedTypeReference); if (response.getStatusCode() != HttpStatus.CREATED || response.getBody() == null) { log.error("Failed to mint doi: {}", response); - throw new ServiceException("Failed to mint doi: " + response.getBody()); + throw new ExternalServiceException("Failed to mint doi: " + response.getBody()); } return response.getBody() .getData() @@ -154,9 +158,7 @@ public class DataCiteIdentifierServiceImpl implements IdentifierService { throw new MalformedException("Failed to mint doi: malformed metadata: " + e.getMessage(), e); } catch (RestClientException e) { log.error("Failed to mint doi: {}", e.getMessage()); - throw new ServiceConnectionException("Failed to mint doi: " + e.getMessage(), e); - } catch (ServiceException e) { - throw new RuntimeException(e); + throw new DataServiceConnectionException("Failed to mint doi: " + e.getMessage(), e); } } @@ -196,14 +198,14 @@ public class DataCiteIdentifierServiceImpl implements IdentifierService { @Override @Transactional(readOnly = true) - public InputStreamResource exportResource(Identifier identifier) throws ServiceException, - ServiceConnectionException, IdentifierNotFoundException, QueryNotFoundException { + public InputStreamResource exportResource(Identifier identifier) throws DataServiceException, + DataServiceConnectionException, IdentifierNotFoundException, QueryNotFoundException { return identifierService.exportResource(identifier); } @Override @Transactional - public void delete(Identifier identifier) throws ServiceException, ServiceConnectionException, + public void delete(Identifier identifier) throws DataServiceException, DataServiceConnectionException, DatabaseNotFoundException, IdentifierNotFoundException, SearchServiceException, SearchServiceConnectionException { identifierService.delete(identifier); diff --git a/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/DatabaseServiceImpl.java b/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/DatabaseServiceImpl.java index f42992781c..8c835864db 100644 --- a/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/DatabaseServiceImpl.java +++ b/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/DatabaseServiceImpl.java @@ -62,6 +62,7 @@ public class DatabaseServiceImpl implements DatabaseService { @Override public Database findByInternalName(String internalName) throws DatabaseNotFoundException { + log.trace("find database by internal name: {}", internalName); final Optional<Database> database = databaseRepository.findByInternalName(internalName); if (database.isEmpty()) { log.error("Failed to find database with internal name {} in metadata database", internalName); @@ -84,7 +85,7 @@ public class DatabaseServiceImpl implements DatabaseService { @Override @Transactional public Database create(DatabaseCreateDto data, User user) throws UserNotFoundException, - ContainerNotFoundException, ServiceException, ServiceConnectionException, DatabaseNotFoundException, + ContainerNotFoundException, DataServiceException, DataServiceConnectionException, DatabaseNotFoundException, SearchServiceException, SearchServiceConnectionException { final Container container = containerService.find(data.getCid()); Database database = Database.builder() @@ -135,7 +136,7 @@ public class DatabaseServiceImpl implements DatabaseService { @Override @Transactional(readOnly = true) - public void updatePassword(Database database, User user) throws ServiceException, ServiceConnectionException, + public void updatePassword(Database database, User user) throws DataServiceException, DataServiceConnectionException, DatabaseNotFoundException { final List<Database> databases = databaseRepository.findReadAccess(user.getId()) .stream() @@ -190,10 +191,10 @@ public class DatabaseServiceImpl implements DatabaseService { } @Override - @Transactional(rollbackFor = {SearchServiceException.class, SearchServiceConnectionException.class, DatabaseNotFoundException.class}) - public Database updateTableMetadata(Database database) throws DatabaseNotFoundException, ServiceException, - SearchServiceException, SearchServiceConnectionException, QueryNotFoundException, - ServiceConnectionException, MalformedException { + @Transactional(rollbackFor = {Exception.class}) + public Database updateTableMetadata(Database database) throws DatabaseNotFoundException, DataServiceException, + SearchServiceException, SearchServiceConnectionException, DataServiceConnectionException, + MalformedException, TableNotFoundException { for (TableDto table : dataServiceGateway.getTableSchemas(database.getId())) { if (database.getTables().stream().anyMatch(t -> t.getInternalName().equals(table.getInternalName()))) { log.debug("fetched known table from data service: {}.{}", database.getInternalName(), table.getInternalName()); @@ -296,10 +297,10 @@ public class DatabaseServiceImpl implements DatabaseService { } @Override - @Transactional(rollbackFor = {SearchServiceException.class, SearchServiceConnectionException.class, DatabaseNotFoundException.class}) - public Database updateViewMetadata(Database database) throws DatabaseNotFoundException, ServiceException, - SearchServiceException, SearchServiceConnectionException, QueryNotFoundException, - ServiceConnectionException { + @Transactional(rollbackFor = {Exception.class}) + public Database updateViewMetadata(Database database) throws DatabaseNotFoundException, DataServiceException, + SearchServiceException, SearchServiceConnectionException, DataServiceConnectionException, + ViewNotFoundException { for (ViewDto view : dataServiceGateway.getViewSchemas(database.getId())) { if (database.getViews().stream().anyMatch(v -> v.getInternalName().equals(view.getInternalName()))) { log.debug("fetched known view from data service: {}.{}", database.getInternalName(), view.getInternalName()); diff --git a/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/IdentifierServiceImpl.java b/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/IdentifierServiceImpl.java index 37215d0787..df0f895f5d 100644 --- a/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/IdentifierServiceImpl.java +++ b/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/IdentifierServiceImpl.java @@ -155,7 +155,7 @@ public class IdentifierServiceImpl implements IdentifierService { @Override @Transactional public Identifier save(Database database, User user, IdentifierSaveDto data) throws SearchServiceException, - ServiceException, QueryNotFoundException, ServiceConnectionException, DatabaseNotFoundException, + DataServiceException, QueryNotFoundException, DataServiceConnectionException, DatabaseNotFoundException, SearchServiceConnectionException, IdentifierNotFoundException, ViewNotFoundException { final Identifier identifier = find(data.getId()); identifier.setDatabase(database); @@ -223,7 +223,7 @@ public class IdentifierServiceImpl implements IdentifierService { @Override @Transactional public Identifier create(Database database, User user, IdentifierCreateDto data) throws SearchServiceException, - ServiceException, QueryNotFoundException, ServiceConnectionException, DatabaseNotFoundException, + DataServiceException, QueryNotFoundException, DataServiceConnectionException, DatabaseNotFoundException, SearchServiceConnectionException, IdentifierNotFoundException, ViewNotFoundException { final Identifier identifier = metadataMapper.identifierCreateDtoToIdentifier(data); identifier.setDatabase(database); @@ -276,9 +276,9 @@ public class IdentifierServiceImpl implements IdentifierService { } @Transactional - public Identifier save(Identifier identifier) throws ServiceException, - ServiceConnectionException, IdentifierNotFoundException, ViewNotFoundException, DatabaseNotFoundException, - QueryNotFoundException, SearchServiceException, SearchServiceConnectionException { + public Identifier save(Identifier identifier) throws DataServiceException, DataServiceConnectionException, + IdentifierNotFoundException, ViewNotFoundException, DatabaseNotFoundException, QueryNotFoundException, + SearchServiceException, SearchServiceConnectionException { /* save identifier */ switch (identifier.getType()) { case SUBSET -> { @@ -356,15 +356,15 @@ public class IdentifierServiceImpl implements IdentifierService { @Override @Transactional(readOnly = true) - public InputStreamResource exportResource(Identifier identifier) throws ServiceException, - ServiceConnectionException, QueryNotFoundException { + public InputStreamResource exportResource(Identifier identifier) throws DataServiceException, + DataServiceConnectionException, QueryNotFoundException { final ExportResourceDto exportResource = dataServiceGateway.exportQuery(identifier.getDatabase().getId(), identifier.getQueryId()); return exportResource.getResource(); } @Override @Transactional - public void delete(Identifier identifier) throws ServiceException, ServiceConnectionException, + public void delete(Identifier identifier) throws DataServiceException, DataServiceConnectionException, IdentifierNotFoundException, DatabaseNotFoundException, SearchServiceException, SearchServiceConnectionException { /* delete in metadata database */ diff --git a/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/StorageServiceS3Impl.java b/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/StorageServiceS3Impl.java index 40eab251c9..9ad86b7f90 100644 --- a/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/StorageServiceS3Impl.java +++ b/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/StorageServiceS3Impl.java @@ -29,6 +29,7 @@ public class StorageServiceS3Impl implements StorageService { @Override public InputStream getObject(String bucket, String key) throws StorageNotFoundException, StorageUnavailableException { + log.trace("get object with key {} from bucket {}", key, bucket); try { return s3Client.getObject(GetObjectRequest.builder() .bucket(bucket) @@ -45,11 +46,13 @@ public class StorageServiceS3Impl implements StorageService { @Override public byte[] getBytes(String key) throws StorageNotFoundException, StorageUnavailableException { + log.trace("get bytes with key {} from bucket {}", key, s3Config.getS3ImportBucket()); return getBytes(s3Config.getS3ImportBucket(), key); } @Override public byte[] getBytes(String bucket, String key) throws StorageNotFoundException, StorageUnavailableException { + log.trace("get bytes with key {} from bucket {}", key, bucket); try { return getObject(bucket, key) .readAllBytes(); diff --git a/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/TableServiceImpl.java b/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/TableServiceImpl.java index 593b612a9c..46a0602c74 100644 --- a/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/TableServiceImpl.java +++ b/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/TableServiceImpl.java @@ -95,9 +95,10 @@ public class TableServiceImpl implements TableService { @Override @Transactional - public Table createTable(Database database, TableCreateDto data, Principal principal) throws ServiceException, - ServiceConnectionException, UserNotFoundException, TableNotFoundException, DatabaseNotFoundException, - TableExistsException, SearchServiceException, SearchServiceConnectionException, MalformedException, OntologyNotFoundException, SemanticEntityNotFoundException { + public Table createTable(Database database, TableCreateDto data, Principal principal) throws DataServiceException, + DataServiceConnectionException, UserNotFoundException, TableNotFoundException, DatabaseNotFoundException, + TableExistsException, SearchServiceException, SearchServiceConnectionException, MalformedException, + OntologyNotFoundException, SemanticEntityNotFoundException { final User owner = userService.findByUsername(principal.getName()); /* check */ if (data.getConstraints().getPrimaryKey().isEmpty()) { @@ -217,7 +218,7 @@ public class TableServiceImpl implements TableService { @Override @Transactional - public void deleteTable(Table table) throws ServiceException, ServiceConnectionException, + public void deleteTable(Table table) throws DataServiceException, DataServiceConnectionException, DatabaseNotFoundException, TableNotFoundException, SearchServiceException, SearchServiceConnectionException { /* delete at data service */ @@ -232,8 +233,8 @@ public class TableServiceImpl implements TableService { @Override @Transactional - public TableColumn update(TableColumn column, ColumnSemanticsUpdateDto data) throws ServiceException, - ServiceConnectionException, DatabaseNotFoundException, SearchServiceException, + public TableColumn update(TableColumn column, ColumnSemanticsUpdateDto data) throws DataServiceException, + DataServiceConnectionException, DatabaseNotFoundException, SearchServiceException, SearchServiceConnectionException, MalformedException, OntologyNotFoundException, SemanticEntityNotFoundException { /* assign */ @@ -288,7 +289,7 @@ public class TableServiceImpl implements TableService { @Transactional public void updateStatistics(Table table) throws SearchServiceException, DatabaseNotFoundException, SearchServiceConnectionException, MalformedException, TableNotFoundException, - ServiceException, ServiceConnectionException { + DataServiceException, DataServiceConnectionException { final TableStatisticDto statistic = dataServiceGateway.getTableStatistics(table.getTdbid(), table.getId()); table.setNumRows(statistic.getRows()); for (Map.Entry<String, ColumnStatisticDto> entry : statistic.getColumns().entrySet()) { diff --git a/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/ViewServiceImpl.java b/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/ViewServiceImpl.java index a91e032844..0826d9dcc8 100644 --- a/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/ViewServiceImpl.java +++ b/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/ViewServiceImpl.java @@ -70,7 +70,7 @@ public class ViewServiceImpl implements ViewService { @Override @Transactional - public void delete(View view) throws ServiceException, ServiceConnectionException, DatabaseNotFoundException, + public void delete(View view) throws DataServiceException, DataServiceConnectionException, DatabaseNotFoundException, ViewNotFoundException, SearchServiceException, SearchServiceConnectionException { /* delete in data service */ dataServiceGateway.deleteView(view.getDatabase().getId(), view.getId()); @@ -84,8 +84,8 @@ public class ViewServiceImpl implements ViewService { @Override @Transactional - public View create(Database database, User creator, ViewCreateDto data) throws MalformedException, ServiceException, - ServiceConnectionException, DatabaseNotFoundException, SearchServiceException, SearchServiceConnectionException { + public View create(Database database, User creator, ViewCreateDto data) throws MalformedException, DataServiceException, + DataServiceConnectionException, DatabaseNotFoundException, SearchServiceException, SearchServiceConnectionException { /* create in metadata database */ final View view = View.builder() .vdbid(database.getId()) diff --git a/dbrepo-metadata-service/services/src/main/java/at/tuwien/utils/FileUtil.java b/dbrepo-metadata-service/services/src/main/java/at/tuwien/utils/FileUtil.java deleted file mode 100644 index 6e8b749c5f..0000000000 --- a/dbrepo-metadata-service/services/src/main/java/at/tuwien/utils/FileUtil.java +++ /dev/null @@ -1,38 +0,0 @@ -package at.tuwien.utils; - -import lombok.extern.log4j.Log4j2; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.util.LinkedList; -import java.util.List; - -@Log4j2 -public class FileUtil { - - /** - * Loads a resource from the resource path when the application is compiled into a .jar and during runtime. - * - * @param resourcePath The path to the resource. - * @return The text contents of the resource. - * @throws IOException The resource could not be loaded. - */ - public static List<String> loadResource(String resourcePath) throws IOException { - final InputStream inputStream = FileUtil.class.getResourceAsStream(resourcePath); - if (inputStream == null) { - log.error("Failed to load query store input stream file {}", resourcePath); - throw new IOException("Failed to load query store input stream file"); - } - final BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); - final List<String> lines = new LinkedList<>(); - while(reader.ready()) { - lines.add(reader.readLine()); - } - inputStream.close(); - reader.close(); - return lines; - } - -} diff --git a/dbrepo-metadata-service/test/src/main/java/at/tuwien/test/BaseTest.java b/dbrepo-metadata-service/test/src/main/java/at/tuwien/test/BaseTest.java index 4a8a66f729..173a3ba9f4 100644 --- a/dbrepo-metadata-service/test/src/main/java/at/tuwien/test/BaseTest.java +++ b/dbrepo-metadata-service/test/src/main/java/at/tuwien/test/BaseTest.java @@ -1,5 +1,6 @@ package at.tuwien.test; +import at.tuwien.ExportResourceDto; import at.tuwien.api.amqp.*; import at.tuwien.api.auth.LoginRequestDto; import at.tuwien.api.auth.SignupRequestDto; @@ -75,6 +76,7 @@ import at.tuwien.entities.user.User; import at.tuwien.test.utils.ArrayUtils; import org.apache.commons.io.FileUtils; import org.apache.commons.lang3.StringUtils; +import org.springframework.core.io.InputStreamResource; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; @@ -82,6 +84,7 @@ import org.springframework.security.core.userdetails.UserDetails; import java.io.File; import java.io.IOException; +import java.io.InputStream; import java.math.BigDecimal; import java.math.BigInteger; import java.nio.charset.Charset; @@ -453,6 +456,14 @@ public abstract class BaseTest { .credentials(List.of(USER_1_KEYCLOAK_CREDENTIAL_1)) .build(); + public final static UserCreateDto USER_1_KEYCLOAK_SYSTEM_SIGNUP_REQUEST = UserCreateDto.builder() + .username(USER_1_USERNAME) + .email(USER_1_EMAIL) + .enabled(USER_1_ENABLED) + .credentials(List.of(USER_1_KEYCLOAK_CREDENTIAL_1)) + .groups(List.of("system")) + .build(); + public final static PrivilegedUserDto USER_1_PRIVILEGED_DTO = PrivilegedUserDto.builder() .id(USER_1_ID) .username(USER_1_USERNAME) @@ -4145,7 +4156,7 @@ public abstract class BaseTest { .autoGenerated(false) .build(), TableColumn.builder() - .id(64L) + .id(65L) .ordinalPosition(20) .table(TABLE_5) .name("Class Type") @@ -4376,7 +4387,7 @@ public abstract class BaseTest { .autoGenerated(false) .build(), ColumnDto.builder() - .id(64L) + .id(65L) .ordinalPosition(20) .tableId(TABLE_5_ID) .table(TABLE_5_DTO) @@ -4525,7 +4536,7 @@ public abstract class BaseTest { .build(); public final static List<TableColumn> TABLE_6_COLUMNS = List.of(TableColumn.builder() - .id(66L) + .id(67L) .ordinalPosition(0) .table(TABLE_6) .name("id") @@ -4535,7 +4546,7 @@ public abstract class BaseTest { .autoGenerated(true) .build(), TableColumn.builder() - .id(67L) + .id(68L) .ordinalPosition(1) .table(TABLE_6) .name("firstname") @@ -4545,7 +4556,7 @@ public abstract class BaseTest { .autoGenerated(false) .build(), TableColumn.builder() - .id(68L) + .id(69L) .ordinalPosition(2) .table(TABLE_6) .name("lastname") @@ -4555,7 +4566,7 @@ public abstract class BaseTest { .autoGenerated(false) .build(), TableColumn.builder() - .id(69L) + .id(70L) .ordinalPosition(3) .table(TABLE_6) .name("birth") @@ -4565,7 +4576,7 @@ public abstract class BaseTest { .autoGenerated(false) .build(), TableColumn.builder() - .id(70L) + .id(71L) .ordinalPosition(4) .table(TABLE_6) .name("reminder") @@ -4576,7 +4587,7 @@ public abstract class BaseTest { .autoGenerated(false) .build(), TableColumn.builder() - .id(71L) + .id(72L) .ordinalPosition(5) .table(TABLE_6) .name("ref_id") @@ -4594,7 +4605,7 @@ public abstract class BaseTest { .build(); public final static List<ColumnDto> TABLE_6_COLUMNS_DTO = List.of(ColumnDto.builder() - .id(66L) + .id(67L) .ordinalPosition(0) .tableId(TABLE_6_ID) .table(TABLE_6_DTO) @@ -4605,7 +4616,7 @@ public abstract class BaseTest { .autoGenerated(true) .build(), ColumnDto.builder() - .id(67L) + .id(68L) .ordinalPosition(1) .tableId(TABLE_6_ID) .table(TABLE_6_DTO) @@ -4616,7 +4627,7 @@ public abstract class BaseTest { .autoGenerated(false) .build(), ColumnDto.builder() - .id(68L) + .id(69L) .ordinalPosition(2) .tableId(TABLE_6_ID) .table(TABLE_6_DTO) @@ -4627,7 +4638,7 @@ public abstract class BaseTest { .autoGenerated(false) .build(), ColumnDto.builder() - .id(69L) + .id(70L) .ordinalPosition(3) .tableId(TABLE_6_ID) .table(TABLE_6_DTO) @@ -4638,7 +4649,7 @@ public abstract class BaseTest { .autoGenerated(false) .build(), ColumnDto.builder() - .id(70L) + .id(71L) .ordinalPosition(4) .tableId(TABLE_6_ID) .table(TABLE_6_DTO) @@ -4650,7 +4661,7 @@ public abstract class BaseTest { .autoGenerated(false) .build(), ColumnDto.builder() - .id(71L) + .id(72L) .ordinalPosition(5) .tableId(TABLE_6_ID) .table(TABLE_6_DTO) @@ -4714,7 +4725,7 @@ public abstract class BaseTest { .build(); public final static List<TableColumn> TABLE_7_COLUMNS = List.of(TableColumn.builder() - .id(26L) + .id(74L) .ordinalPosition(0) .table(TABLE_7) .name("name_id") @@ -4724,7 +4735,7 @@ public abstract class BaseTest { .autoGenerated(false) .build(), TableColumn.builder() - .id(27L) + .id(75L) .ordinalPosition(1) .table(TABLE_7) .name("zoo_id") @@ -4735,7 +4746,7 @@ public abstract class BaseTest { .build()); public final static List<ColumnDto> TABLE_7_COLUMNS_DTO = List.of(ColumnDto.builder() - .id(26L) + .id(74L) .ordinalPosition(0) .tableId(TABLE_7_ID) .table(TABLE_7_DTO) @@ -4746,7 +4757,7 @@ public abstract class BaseTest { .autoGenerated(false) .build(), ColumnDto.builder() - .id(27L) + .id(75L) .ordinalPosition(1) .tableId(TABLE_7_ID) .table(TABLE_7_DTO) @@ -5762,6 +5773,7 @@ public abstract class BaseTest { .build(); public final static IdentifierSaveDescriptionDto IDENTIFIER_1_DESCRIPTION_1_CREATE_DTO = IdentifierSaveDescriptionDto.builder() + .id(null) .description(IDENTIFIER_1_DESCRIPTION_1_DESCRIPTION) .descriptionType(IDENTIFIER_1_DESCRIPTION_1_TYPE_DTO) .language(IDENTIFIER_1_DESCRIPTION_1_LANG_DTO) @@ -5807,6 +5819,7 @@ public abstract class BaseTest { .build(); public final static CreatorSaveDto IDENTIFIER_1_CREATOR_1_CREATE_DTO = CreatorSaveDto.builder() + .id(null) .firstname(IDENTIFIER_1_CREATOR_1_FIRSTNAME) .lastname(IDENTIFIER_1_CREATOR_1_LASTNAME) .creatorName(IDENTIFIER_1_CREATOR_1_NAME) @@ -5990,6 +6003,7 @@ public abstract class BaseTest { .publicationMonth(IDENTIFIER_1_PUBLICATION_MONTH) .publisher(IDENTIFIER_1_PUBLISHER) .type(IDENTIFIER_1_TYPE_DTO) + .doi(IDENTIFIER_1_DOI) .licenses(List.of(LICENSE_1_DTO)) .creators(List.of(IDENTIFIER_1_CREATOR_1_CREATE_DTO)) .funders(List.of(IDENTIFIER_1_FUNDER_1_CREATE_DTO)) @@ -6116,6 +6130,7 @@ public abstract class BaseTest { .build(); public final static IdentifierSaveDescriptionDto IDENTIFIER_5_DESCRIPTION_1_CREATE_DTO = IdentifierSaveDescriptionDto.builder() + .id(null) .description(IDENTIFIER_5_DESCRIPTION_1_DESCRIPTION) .language(IDENTIFIER_5_DESCRIPTION_1_LANG_DTO) .descriptionType(IDENTIFIER_5_DESCRIPTION_1_TYPE_DTO) @@ -6375,6 +6390,7 @@ public abstract class BaseTest { .build(); public final static IdentifierSaveDescriptionDto IDENTIFIER_6_DESCRIPTION_1_CREATE_DTO = IdentifierSaveDescriptionDto.builder() + .id(null) .description(IDENTIFIER_6_DESCRIPTION_1_DESCRIPTION_MODIFY) .language(IDENTIFIER_6_DESCRIPTION_1_LANG_DTO) .build(); @@ -6562,6 +6578,15 @@ public abstract class BaseTest { public final static IdentifierStatusType IDENTIFIER_7_STATUS_TYPE = IdentifierStatusType.DRAFT; public final static IdentifierStatusTypeDto IDENTIFIER_7_STATUS_TYPE_DTO = IdentifierStatusTypeDto.DRAFT; + public final static DataCiteBody<DataCiteDoi> IDENTIFIER_7_DATA_CITE = DataCiteBody.<DataCiteDoi>builder() + .data(DataCiteData.<DataCiteDoi>builder() + .type("dois") + .attributes(DataCiteDoi.builder() + .doi(IDENTIFIER_7_DOI) + .build()) + .build()) + .build(); + private final static Long IDENTIFIER_7_CREATOR_1_ID = 6L; public final static Creator IDENTIFIER_7_CREATOR_1 = Creator.builder() @@ -7823,6 +7848,11 @@ public abstract class BaseTest { .build()))) .build(); + public final static ExportResourceDto EXPORT_RESOURCE_DTO = ExportResourceDto.builder() + .filename("68b329da9893e34099c7d8ad5cb9c940") + .resource(new InputStreamResource(InputStream.nullInputStream())) + .build(); + public static void saveObservedMetrics(Map<String, String> observedMetrics) throws IOException { final int keySize = observedMetrics.keySet().stream().max(Comparator.comparingInt(String::length)).get().length(); final int valueSize = observedMetrics.values().stream().max(Comparator.comparingInt(String::length)).get().length(); diff --git a/dbrepo-search-service/app.py b/dbrepo-search-service/app.py index 47f1f0254c..9b2aba7f02 100644 --- a/dbrepo-search-service/app.py +++ b/dbrepo-search-service/app.py @@ -199,8 +199,6 @@ app.config["JWT_PUBKEY"] = '-----BEGIN PUBLIC KEY-----\n' + os.getenv("JWT_PUBKE app.config["AUTH_SERVICE_ENDPOINT"] = os.getenv("AUTH_SERVICE_ENDPOINT", "http://localhost/api/auth") app.config["AUTH_SERVICE_CLIENT"] = os.getenv("AUTH_SERVICE_CLIENT", "dbrepo-client") app.config["AUTH_SERVICE_CLIENT_SECRET"] = os.getenv("AUTH_SERVICE_CLIENT_SECRET", "MUwRc7yfXSJwX8AdRMWaQC3Nep1VjwgG") -app.config["ADMIN_USERNAME"] = os.getenv('ADMIN_USERNAME', 'admin') -app.config["ADMIN_PASSWORD"] = os.getenv('ADMIN_PASSWORD', 'admin') app.config["OPENSEARCH_HOST"] = os.getenv('OPENSEARCH_HOST', 'localhost') app.config["OPENSEARCH_PORT"] = os.getenv('OPENSEARCH_PORT', '9200') app.config["OPENSEARCH_USERNAME"] = os.getenv('OPENSEARCH_USERNAME', 'admin') @@ -227,8 +225,6 @@ def verify_token(token: str): def verify_password(username: str, password: str) -> Any: if username is None or username == "" or password is None or password == "": return False - if username == app.config["ADMIN_USERNAME"] and password == app.config["ADMIN_PASSWORD"]: - return User(username=username, roles=["admin"]) client = KeycloakClient() try: return client.verify_jwt(access_token=client.obtain_user_token(username=username, password=password)) @@ -374,7 +370,7 @@ def post_general_search(type): if t1 is not None and t2 is not None and "unit.uri" in req_body and "concept.uri" in req_body: response = OpenSearchClient().unit_independent_search(t1, t2, req_body) else: - response = OpenSearchClient().general_search(type, t1, t2, req_body) + response = OpenSearchClient().general_search(type, req_body) # filter by type if type == 'table': tmp = [] @@ -431,7 +427,7 @@ def post_general_search(type): @app.route("/api/search/database/<int:database_id>", methods=["PUT"], endpoint="search_put_database") @metrics.gauge(name='dbrepo_search_update_database', description='Time needed to update a database in the search database') -@auth.login_required(role=['admin']) +@auth.login_required(role=['update-search-index']) def update_database(database_id: int) -> Database | ApiError: logging.debug(f"updating database with id: {database_id}") try: diff --git a/dbrepo-search-service/clients/opensearch_client.py b/dbrepo-search-service/clients/opensearch_client.py index 0af3127793..623da7c2a0 100644 --- a/dbrepo-search-service/clients/opensearch_client.py +++ b/dbrepo-search-service/clients/opensearch_client.py @@ -171,91 +171,38 @@ class OpenSearchClient: logging.info(f"Found {len(response['hits']['hits'])} result(s)") return response - def general_search(self, type=None, t1=None, t2=None, field_value_pairs=None): + def general_search(self, type: str = None, field_value_pairs: dict = None): """ Main method for searching stuff in the opensearch db all parameters are optional :param type: The index to be searched. Optional. - :param t1: The start range value. Optional. - :param t2: The end range value. Optional. :param field_value_pairs: The key-value pair of properties that need to match. Optional. :return: The object of results and HTTP status code. e.g. { "hits": { "hits": [] } }, 200 """ musts = [] if field_value_pairs is not None and len(field_value_pairs) > 0: logging.debug(f'field_value_pairs present: {field_value_pairs}') - is_range_open_end = False - is_range_open_begin = False - is_range_query = False - if t1 is not None and t2 is None: - is_range_open_begin = True - logging.debug(f"query has only start value {t1} present") - if t1 is None and t2 is not None: - is_range_open_end = True - logging.debug(f"query has only end value {t2} present") - if t1 is not None and t2 is not None: - is_range_query = True - logging.debug(f"query has start value {t1} and end value {t2} present") for key, value in field_value_pairs.items(): if field_value_pairs[key] == None: logging.debug(f"skip empty key: {key}") continue logging.debug(f"processing key: {key}") - if is_range_open_end and re.match(f"unit\.", key): - logging.debug(f"omit key={key} because query type=open end range and key is somewhat unit") - logging.info(f"add match-query for range ),{t2}]") + if '.' in key: + logging.debug(f'key {key} is nested: use nested query') musts.append({ - "range": { - "val_max": { - "lte": t2 - } - } - }) - elif is_range_open_begin and re.match(f"unit\.", key): - logging.debug(f"omit key={key} because query type=open begin range and key is somewhat unit") - logging.info(f"add match-query for range [{t1},(") - musts.append({ - "range": { - "val_min": { - "gte": t1 - } - } - }) - elif is_range_query and re.match(f"unit\.", key): - logging.debug( - f"omit key={key} because query type=full range and key is somewhat unit") - logging.info(f"add match-query for range [{t1},{t2}]") - musts.append({ - "range": { - "val_min": { - "gte": t1 - } + "match": { + key: value } }) + else: + logging.debug(f'key {key} is flat: use bool query') musts.append({ - "range": { - "val_max": { - "lte": t2 - } + "match": { + key: {"query": value, "minimum_should_match": "90%"} } }) - else: - if '.' in key: - logging.debug(f'key {key} is nested: use nested query') - musts.append({ - "match": { - key: value - } - }) - else: - logging.debug(f'key {key} is flat: use bool query') - musts.append({ - "match": { - key: {"query": value, "minimum_should_match": "90%"} - } - }) body = { "query": {"bool": {"must": musts}} } @@ -290,7 +237,7 @@ class OpenSearchClient: } } response = self._instance().search( - index="column", + index="database", body=dumps(body) ) unit_uris = [hit["key"] for hit in response["aggregations"]["units"]["buckets"]] diff --git a/dbrepo-search-service/test/test_opensearch_client.py b/dbrepo-search-service/test/test_opensearch_client.py index 906aae0ccc..7fe079d0f9 100644 --- a/dbrepo-search-service/test/test_opensearch_client.py +++ b/dbrepo-search-service/test/test_opensearch_client.py @@ -3,49 +3,77 @@ import unittest import opensearchpy from dbrepo.api.dto import Database, User, UserAttributes, Container, Image, Table, Column, ColumnType, Constraints, \ - PrimaryKey, TableMinimal, ColumnMinimal + PrimaryKey, TableMinimal, ColumnMinimal, Concept, Unit + from app import app from clients.opensearch_client import OpenSearchClient +req = Database(id=1, + name="Test", + internal_name="test_tuw1", + creator=User(id="c6b71ef5-2d2f-48b2-9d79-b8f23a3a0502", + username="foo", + attributes=UserAttributes(theme="dark")), + owner=User(id="c6b71ef5-2d2f-48b2-9d79-b8f23a3a0502", + username="foo", + attributes=UserAttributes(theme="dark")), + contact=User(id="c6b71ef5-2d2f-48b2-9d79-b8f23a3a0502", + username="foo", + attributes=UserAttributes(theme="dark")), + created=datetime.datetime(2024, 3, 25, 16, tzinfo=datetime.timezone.utc), + exchange_name="dbrepo", + is_public=True, + container=Container(id=1, + name="MariaDB", + internal_name="mariadb", + host="data-db", + port="3306", + created=datetime.datetime(2024, 3, 1, 10, tzinfo=datetime.timezone.utc), + sidecar_host="data-db-sidecar", + sidecar_port=3305, + image=Image(id=1, + registry="docker.io", + name="mariadb", + version="11.1.3", + dialect="org.hibernate.dialect.MariaDBDialect", + driver_class="org.mariadb.jdbc.Driver", + jdbc_method="mariadb", + default_port=3306)), + tables=[Table(id=1, database_id=1, name="Data", internal_name="data", + creator=User(id="c6b71ef5-2d2f-48b2-9d79-b8f23a3a0502", + username="foo", + attributes=UserAttributes(theme="dark")), + owner=User(id="c6b71ef5-2d2f-48b2-9d79-b8f23a3a0502", + username="foo", + attributes=UserAttributes(theme="dark")), + created=datetime.datetime(2024, 3, 1, 10, tzinfo=datetime.timezone.utc), + constraints=Constraints(uniques=[], foreign_keys=[], checks=[], primary_key=[]), + is_versioned=False, + created_by="c6b71ef5-2d2f-48b2-9d79-b8f23a3a0502", + queue_name="dbrepo", + routing_key="dbrepo.1.1", + is_public=True, + columns=[Column(id=1, database_id=1, table_id=1, name="ID", internal_name="id", + auto_generated=True, column_type=ColumnType.BIGINT, is_public=True, + is_null_allowed=False, size=20, d=0, + concept=Concept(id=1, uri="http://www.wikidata.org/entity/Q2221906", + created=datetime.datetime(2024, 3, 1, 10, + tzinfo=datetime.timezone.utc)), + unit=Unit(id=1, + uri="http://www.ontology-of-units-of-measure.org/resource/om-2/degreeCelsius", + created=datetime.datetime(2024, 3, 1, 10, + tzinfo=datetime.timezone.utc)), + val_min=0, + val_max=10)] + )]) + class OpenSearchClientTest(unittest.TestCase): def test_update_database_succeeds(self): with app.app_context(): client = OpenSearchClient() - req = Database(id=1, - name="Test", - internal_name="test_tuw1", - creator=User(id="c6b71ef5-2d2f-48b2-9d79-b8f23a3a0502", - username="foo", - attributes=UserAttributes(theme="dark")), - owner=User(id="c6b71ef5-2d2f-48b2-9d79-b8f23a3a0502", - username="foo", - attributes=UserAttributes(theme="dark")), - contact=User(id="c6b71ef5-2d2f-48b2-9d79-b8f23a3a0502", - username="foo", - attributes=UserAttributes(theme="dark")), - created=datetime.datetime(2024, 3, 25, 16, tzinfo=datetime.timezone.utc), - exchange_name="dbrepo", - is_public=True, - container=Container(id=1, - name="MariaDB", - internal_name="mariadb", - host="data-db", - port="3306", - created=datetime.datetime(2024, 3, 1, 10, tzinfo=datetime.timezone.utc), - sidecar_host="data-db-sidecar", - sidecar_port=3305, - image=Image(id=1, - registry="docker.io", - name="mariadb", - version="11.1.3", - dialect="org.hibernate.dialect.MariaDBDialect", - driver_class="org.mariadb.jdbc.Driver", - jdbc_method="mariadb", - default_port=3306)), - tables=[]) # mock client.update_database(database_id=1, data=req) @@ -132,38 +160,6 @@ class OpenSearchClientTest(unittest.TestCase): def test_update_database_create_succeeds(self): with app.app_context(): client = OpenSearchClient() - req = Database(id=1, - name="Test", - internal_name="test_tuw1", - creator=User(id="c6b71ef5-2d2f-48b2-9d79-b8f23a3a0502", - username="foo", - attributes=UserAttributes(theme="dark")), - owner=User(id="c6b71ef5-2d2f-48b2-9d79-b8f23a3a0502", - username="foo", - attributes=UserAttributes(theme="dark")), - contact=User(id="c6b71ef5-2d2f-48b2-9d79-b8f23a3a0502", - username="foo", - attributes=UserAttributes(theme="dark")), - created=datetime.datetime(2024, 3, 25, 16, tzinfo=datetime.timezone.utc), - exchange_name="dbrepo", - is_public=True, - container=Container(id=1, - name="MariaDB", - internal_name="mariadb", - host="data-db", - port="3306", - created=datetime.datetime(2024, 3, 1, 10, tzinfo=datetime.timezone.utc), - sidecar_host="data-db-sidecar", - sidecar_port=3305, - image=Image(id=1, - registry="docker.io", - name="mariadb", - version="11.1.3", - dialect="org.hibernate.dialect.MariaDBDialect", - driver_class="org.mariadb.jdbc.Driver", - jdbc_method="mariadb", - default_port=3306)), - tables=[]) # test database = client.update_database(database_id=1, data=req) @@ -186,7 +182,18 @@ class OpenSearchClientTest(unittest.TestCase): # ... self.assertEqual(1, database.container.image.id) # ... - self.assertEqual(0, len(database.tables)) + self.assertEqual(1, len(database.tables)) + + def test_update_database_malformed_fails(self): + with app.app_context(): + app.config['OPENSEARCH_USERNAME'] = 'i_do_not_exist' + client = OpenSearchClient() + + # test + try: + database = client.update_database(database_id=1, data=req) + except opensearchpy.exceptions.TransportError: + pass def test_delete_database_fails(self): with app.app_context(): @@ -201,38 +208,6 @@ class OpenSearchClientTest(unittest.TestCase): def test_delete_database_succeeds(self): with app.app_context(): client = OpenSearchClient() - req = Database(id=1, - name="Test", - internal_name="test_tuw1", - creator=User(id="c6b71ef5-2d2f-48b2-9d79-b8f23a3a0502", - username="foo", - attributes=UserAttributes(theme="dark")), - owner=User(id="c6b71ef5-2d2f-48b2-9d79-b8f23a3a0502", - username="foo", - attributes=UserAttributes(theme="dark")), - contact=User(id="c6b71ef5-2d2f-48b2-9d79-b8f23a3a0502", - username="foo", - attributes=UserAttributes(theme="dark")), - created=datetime.datetime(2024, 3, 25, 16, 0, tzinfo=datetime.timezone.utc), - exchange_name="dbrepo", - is_public=True, - container=Container(id=1, - name="MariaDB", - internal_name="mariadb", - host="data-db", - port="3306", - created=datetime.datetime(2024, 3, 1, 10, tzinfo=datetime.timezone.utc), - sidecar_host="data-db-sidecar", - sidecar_port=3305, - image=Image(id=1, - registry="docker.io", - name="mariadb", - version="11.1.3", - dialect="org.hibernate.dialect.MariaDBDialect", - driver_class="org.mariadb.jdbc.Driver", - jdbc_method="mariadb", - default_port=3306)), - tables=[]) # mock client.update_database(database_id=req.id, data=req) @@ -243,38 +218,6 @@ class OpenSearchClientTest(unittest.TestCase): def test_find_database_succeeds(self): with app.app_context(): client = OpenSearchClient() - req = Database(id=1, - name="Test", - internal_name="test_tuw1", - creator=User(id="c6b71ef5-2d2f-48b2-9d79-b8f23a3a0502", - username="foo", - attributes=UserAttributes(theme="dark")), - owner=User(id="c6b71ef5-2d2f-48b2-9d79-b8f23a3a0502", - username="foo", - attributes=UserAttributes(theme="dark")), - contact=User(id="c6b71ef5-2d2f-48b2-9d79-b8f23a3a0502", - username="foo", - attributes=UserAttributes(theme="dark")), - created=datetime.datetime(2024, 3, 25, 16, tzinfo=datetime.timezone.utc), - exchange_name="dbrepo", - is_public=True, - container=Container(id=1, - name="MariaDB", - internal_name="mariadb", - host="data-db", - port="3306", - created=datetime.datetime(2024, 3, 1, 10, tzinfo=datetime.timezone.utc), - sidecar_host="data-db-sidecar", - sidecar_port=3305, - image=Image(id=1, - registry="docker.io", - name="mariadb", - version="11.1.3", - dialect="org.hibernate.dialect.MariaDBDialect", - driver_class="org.mariadb.jdbc.Driver", - jdbc_method="mariadb", - default_port=3306)), - tables=[]) # mock client.update_database(database_id=req.id, data=req) @@ -286,8 +229,83 @@ class OpenSearchClientTest(unittest.TestCase): with app.app_context(): client = OpenSearchClient() + # mock + client.update_database(database_id=1, data=req) + # test try: client.get_database(database_id=1) except opensearchpy.exceptions.NotFoundError: pass + + def test_query_index_by_term_opensearch_contains_succeeds(self): + with app.app_context(): + client = OpenSearchClient() + + # mock + client.update_database(database_id=1, data=req) + + # test + response = client.query_index_by_term_opensearch(term="test", mode="contains") + self.assertEqual(1, len(response)) + self.assertEqual(1, response[0]['id']) + self.assertEqual('Test', response[0]['name']) + + def test_query_index_by_term_opensearch_exact_succeeds(self): + with app.app_context(): + client = OpenSearchClient() + + # mock + client.update_database(database_id=1, data=req) + + # test + response = client.query_index_by_term_opensearch(term="test", mode="exact") + self.assertEqual(1, len(response)) + self.assertEqual(1, response[0]['id']) + self.assertEqual('Test', response[0]['name']) + + def test_get_fields_for_index_database_succeeds(self): + with app.app_context(): + client = OpenSearchClient() + + # mock + client.update_database(database_id=1, data=req) + + # test + response = client.get_fields_for_index(type="database") + self.assertTrue(len(response) > 0) + + def test_get_fields_for_index_user_succeeds(self): + with app.app_context(): + client = OpenSearchClient() + + # mock + client.update_database(database_id=1, data=req) + + # test + response = client.get_fields_for_index(type="user") + self.assertTrue(len(response) > 0) + + def test_fuzzy_search_succeeds(self): + with app.app_context(): + client = OpenSearchClient() + + # mock + client.update_database(database_id=1, data=req) + + # test + response = client.fuzzy_search(search_term="test") + self.assertTrue(len(response) > 0) + + def test_general_search_succeeds(self): + with app.app_context(): + client = OpenSearchClient() + + # mock + client.update_database(database_id=1, data=req) + + # test + response = client.general_search(type="database", field_value_pairs={"name": "Test", + "id": None}) + self.assertTrue(len(response) > 0) + diff --git a/dbrepo-ui/Dockerfile b/dbrepo-ui/Dockerfile index 14f1e57c1e..d7b63d8f89 100644 --- a/dbrepo-ui/Dockerfile +++ b/dbrepo-ui/Dockerfile @@ -1,10 +1,8 @@ -FROM oven/bun:1.0.26-alpine as build -MAINTAINER Martin Weise <martin.weise@tuwien.ac.at> +FROM oven/bun:1.0.26-alpine AS build WORKDIR /app COPY ./package.json ./package.json -COPY ./bun.lockb ./bun.lockb RUN bun install @@ -27,7 +25,6 @@ COPY ./nuxt.config.ts ./nuxt.config.ts RUN bun run build FROM oven/bun:1.0.26-alpine as runtime -MAINTAINER Martin Weise <martin.weise@tuwien.ac.at> ARG APP_VERSION="latest" ARG COMMIT="" @@ -36,7 +33,9 @@ USER 1000 WORKDIR /app -COPY --from=build --chown=1000:1000 /app/.output /app/.output +COPY --from=build --chown=1000 /app/.output /app/.output + +RUN chmod -R 755 /app/.output ENV NUXT_PUBLIC_VERSION="${APP_VERSION:-}" ENV NUXT_PUBLIC_COMMIT="${COMMIT:-}" diff --git a/dbrepo-ui/bun.lockb b/dbrepo-ui/bun.lockb deleted file mode 100755 index 2ae1649f86a2cfc2d75d6e45a0c9490ad434ae02..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 441269 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	mrdd+hbA2A(dP4N5Coj%-`HWkIX*x!&^? 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 TF!00)iaJ;u1m1#S7^DCIa1sCn diff --git a/dbrepo-ui/locales/de-AT.json b/dbrepo-ui/locales/de-AT.json index 7c2b2a149f..e32fe38ee0 100644 --- a/dbrepo-ui/locales/de-AT.json +++ b/dbrepo-ui/locales/de-AT.json @@ -1179,6 +1179,13 @@ "create": "Ansicht konnte nicht erstellt werden", "missing": "Die Ansicht konnte in der Metadatendatenbank nicht gefunden werden", "invalid": "Die Ansichtsabfrage konnte den Spalten im Datendienst nicht zugeordnet werden" + }, + "broker": { + "connection": "Es konnte keine Verbindung zum Vermittlungsdienst hergestellt werden", + "invalid": "Es konnten keine Metadaten im Metadatendienst abgerufen werden" + }, + "external": { + "invalid": "Metadaten konnten nicht aus dem Datacite-System abgerufen werden" } }, "success": { diff --git a/dbrepo-ui/locales/en-US.json b/dbrepo-ui/locales/en-US.json index c914fd774c..edf6c66ca1 100644 --- a/dbrepo-ui/locales/en-US.json +++ b/dbrepo-ui/locales/en-US.json @@ -1182,6 +1182,13 @@ "create": "Failed to create view", "missing": "Failed to find view in metadata database", "invalid": "Failed to map view query to columns in data service" + }, + "broker": { + "connection": "Failed to contact broker service", + "invalid": "Failed to obtain metadata in the broker service" + }, + "external": { + "invalid": "Failed to obtain metadata from the datacite system" } }, "success": { diff --git a/docker-compose.yml b/docker-compose.yml index 43c3fbbfb1..b67e23fc62 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -39,6 +39,7 @@ services: hostname: data-db image: docker.io/bitnami/mariadb:11.1.3-debian-11-r6 volumes: + - ./dbrepo-data-db/enable_history_insert.cnf:/opt/bitnami/mariadb/conf.default/enable_history_insert.cnf - "${SHARED_VOLUME:-/tmp}:/tmp" - data-db-data:/bitnami/mariadb ports: @@ -116,8 +117,6 @@ services: - "${SHARED_VOLUME:-/tmp}:/tmp" environment: ADMIN_EMAIL: "${ADMIN_EMAIL:-noreply@localhost}" - ADMIN_PASSWORD: "${ADMIN_PASSWORD:-admin}" - ADMIN_USERNAME: "${ADMIN_USERNAME:-admin}" ANALYSE_SERVICE_ENDPOINT: "${ANALYSE_SERVICE_ENDPOINT:-http://gateway-service}" AUTH_SERVICE_ADMIN: ${AUTH_SERVICE_ADMIN:-admin} AUTH_SERVICE_ADMIN_PASSWORD: ${AUTH_SERVICE_ADMIN_PASSWORD:-admin} @@ -152,6 +151,8 @@ services: S3_IMPORT_BUCKET: "${S3_IMPORT_BUCKET:-dbrepo-upload}" S3_SECRET_ACCESS_KEY: "${S3_SECRET_ACCESS_KEY:-seaweedfsadmin}" SPARQL_CONNECTION_TIMEOUT: "${SPARQL_CONNECTION_TIMEOUT:-10000}" + SYSTEM_USERNAME: "${SYSTEM_USERNAME:-admin}" + SYSTEM_PASSWORD: "${SYSTEM_PASSWORD:-admin}" healthcheck: test: curl -sSL localhost:8080/actuator/health/liveness | grep 'UP' || exit 1 interval: 10s @@ -180,8 +181,6 @@ services: ports: - "5000:8080" environment: - ADMIN_PASSWORD: "${ADMIN_PASSWORD:-admin}" - ADMIN_USERNAME: "${ADMIN_USERNAME:-admin}" AUTH_SERVICE_CLIENT: ${AUTH_SERVICE_CLIENT:-dbrepo-client} AUTH_SERVICE_CLIENT_SECRET: ${AUTH_SERVICE_CLIENT:-MUwRc7yfXSJwX8AdRMWaQC3Nep1VjwgG} AUTH_SERVICE_ENDPOINT: ${AUTH_SERVICE_ENDPOINT:-http://auth-service:8080} @@ -265,8 +264,6 @@ services: ports: - "4000:8080" environment: - ADMIN_PASSWORD: "${ADMIN_PASSWORD:-admin}" - ADMIN_USERNAME: "${ADMIN_USERNAME:-admin}" AUTH_SERVICE_CLIENT: ${AUTH_SERVICE_CLIENT:-dbrepo-client} AUTH_SERVICE_CLIENT_SECRET: ${AUTH_SERVICE_CLIENT:-MUwRc7yfXSJwX8AdRMWaQC3Nep1VjwgG} AUTH_SERVICE_ENDPOINT: ${AUTH_SERVICE_ENDPOINT:-http://auth-service:8080} @@ -382,8 +379,8 @@ services: environment: LDAP_ADMIN_USERNAME: "${IDENTITY_SERVICE_ADMIN_USERNAME:-admin}" LDAP_ADMIN_PASSWORD: "${IDENTITY_SERVICE_ADMIN_PASSWORD:-admin}" - LDAP_USERS: "${ADMIN_USERNAME:-admin}" - LDAP_PASSWORDS: "${ADMIN_PASSWORD:-admin}" + LDAP_USERS: "${IDENTITY_SERVICE_ADMIN_USERNAME:-admin}" + LDAP_PASSWORDS: "${IDENTITY_SERVICE_ADMIN_PASSWORD:-admin}" LDAP_GROUP: "${ADMIN_GROUP:-system}" LDAP_ROOT: "${IDENTITY_SERVICE_ROOT:-dc=dbrepo,dc=at}" LDAP_ADMIN_DN: "${IDENTITY_SERVICE_ADMIN_DN:-cn=admin,dc=dbrepo,dc=at}" @@ -486,8 +483,6 @@ services: volumes: - "${SHARED_VOLUME:-/tmp}:/tmp" environment: - ADMIN_PASSWORD: "${ADMIN_PASSWORD:-admin}" - ADMIN_USERNAME: "${ADMIN_USERNAME:-admin}" AUTH_SERVICE_ADMIN: ${AUTH_SERVICE_ADMIN:-admin} AUTH_SERVICE_ADMIN_PASSWORD: ${AUTH_SERVICE_ADMIN_PASSWORD:-admin} AUTH_SERVICE_CLIENT: ${AUTH_SERVICE_CLIENT:-dbrepo-client} @@ -519,6 +514,8 @@ services: S3_FILE_PATH: "${S3_FILE_PATH:-/tmp}" S3_IMPORT_BUCKET: "${S3_IMPORT_BUCKET:-dbrepo-upload}" S3_SECRET_ACCESS_KEY: "${S3_SECRET_ACCESS_KEY:-seaweedfsadmin}" + SYSTEM_USERNAME: "${SYSTEM_USERNAME:-admin}" + SYSTEM_PASSWORD: "${SYSTEM_PASSWORD:-admin}" healthcheck: test: curl -sSL localhost:8080/actuator/health/liveness | grep 'UP' || exit 1 interval: 10s diff --git a/helm/dbrepo/Chart.yaml b/helm/dbrepo/Chart.yaml index 0e708f4669..28ce12c838 100644 --- a/helm/dbrepo/Chart.yaml +++ b/helm/dbrepo/Chart.yaml @@ -4,13 +4,13 @@ description: Helm Chart for installing DBRepo sources: - https://gitlab.phaidra.org/fair-data-austria-db-repository/fda-services type: application -version: "1.4.4" -appVersion: "1.4.4" +version: "1.4.5" +appVersion: "1.4.5" keywords: - dbrepo maintainers: - name: Martin Weise - email: martin.weise@tuwien.ac.a + email: martin.weise@tuwien.ac.at home: https://www.ifs.tuwien.ac.at/infrastructures/dbrepo/ icon: https://gitlab.phaidra.org/fair-data-austria-db-repository/fda-services/-/raw/master/dbrepo-ui/public/favicon.png dependencies: diff --git a/helm/dbrepo/README.md b/helm/dbrepo/README.md index 48848c588c..b310705b11 100644 --- a/helm/dbrepo/README.md +++ b/helm/dbrepo/README.md @@ -45,6 +45,13 @@ The command removes all the Kubernetes components associated with the chart and ## Parameters +### Global parameters + +| Name | Description | Value | +| ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | +| `global.compatibility.openshift.adaptSecurityContext` | Adapt the securityContext sections of the deployment to make them compatible with Openshift restricted-v2 SCC: remove runAsUser, runAsGroup and fsGroup and let the platform use their allowed default IDs. Possible values: auto (apply if the detected running cluster is Openshift), force (perform the adaptation always), disabled (do not perform adaptation) | `auto` | +| `global.storageClass` | Global StorageClass for Persistent Volume(s) | `""` | + ### Common parameters | Name | Description | Value | @@ -137,67 +144,121 @@ The command removes all the Kubernetes components associated with the chart and ### Analyse Service -| Name | Description | Value | -| ----------------------------- | ----------------------------------------------------------- | ------------------------------- | -| `analyseservice.enabled` | Enable the Broker Service. | `true` | -| `analyseservice.image.debug` | Set the logging level to `trace`. Otherwise, set to `info`. | `false` | -| `analyseservice.endpoint` | The url of the endpoint. | `http://analyse-service` | -| `analyseservice.s3.endpoint` | The S3-capable endpoint the microservice connects to. | `http://storageservice-s3:9000` | -| `analyseservice.replicaCount` | The number of replicas. | `2` | +| Name | Description | Value | +| ------------------------------------------------------------------ | ----------------------------------------------------------- | ------------------------------- | +| `analyseservice.enabled` | Enable the Broker Service. | `true` | +| `analyseservice.image.debug` | Set the logging level to `trace`. Otherwise, set to `info`. | `false` | +| `analyseservice.podSecurityContext.enabled` | Enable pods' Security Context | `true` | +| `analyseservice.podSecurityContext.fsGroupChangePolicy` | Set filesystem group change policy | `Always` | +| `analyseservice.podSecurityContext.sysctls` | Set kernel settings using the sysctl interface | `[]` | +| `analyseservice.podSecurityContext.supplementalGroups` | Set filesystem extra groups | `[]` | +| `analyseservice.podSecurityContext.fsGroup` | Set RabbitMQ pod's Security Context fsGroup | `1001` | +| `analyseservice.containerSecurityContext.enabled` | Enabled containers' Security Context | `true` | +| `analyseservice.containerSecurityContext.seLinuxOptions` | Set SELinux options in container | `""` | +| `analyseservice.containerSecurityContext.runAsUser` | Set RabbitMQ containers' Security Context runAsUser | `1001` | +| `analyseservice.containerSecurityContext.runAsGroup` | Set RabbitMQ containers' Security Context runAsGroup | `1001` | +| `analyseservice.containerSecurityContext.runAsNonRoot` | Set RabbitMQ container's Security Context runAsNonRoot | `true` | +| `analyseservice.containerSecurityContext.allowPrivilegeEscalation` | Set container's privilege escalation | `false` | +| `analyseservice.containerSecurityContext.readOnlyRootFilesystem` | Set container's Security Context readOnlyRootFilesystem | `false` | +| `analyseservice.containerSecurityContext.capabilities.drop` | Set container's Security Context runAsNonRoot | `["ALL"]` | +| `analyseservice.containerSecurityContext.seccompProfile.type` | Set container's Security Context seccomp profile | `RuntimeDefault` | +| `analyseservice.endpoint` | The url of the endpoint. | `http://analyse-service` | +| `analyseservice.s3.endpoint` | The S3-capable endpoint the microservice connects to. | `http://storageservice-s3:9000` | +| `analyseservice.replicaCount` | The number of replicas. | `2` | ### Metadata Service -| Name | Description | Value | -| ------------------------------------------ | ---------------------------------------------------------------------------------- | ------------------------------- | -| `metadataservice.enabled` | Enable the Metadata Service. | `true` | -| `metadataservice.image.debug` | Set the logging level to `trace`. Otherwise, set to `info`. | `false` | -| `metadataservice.endpoint` | The Metadata Service endpoint. | `http://metadata-service` | -| `metadataservice.admin.email` | The OAI-PMH exposed e-mail for contacting the metadata records responsible person. | `noreply@example.com` | -| `metadataservice.deletedRecord` | The OAI-PMH exposed delete policy. | `permanent` | -| `metadataservice.repositoryName` | The OAI-PMH exposed repository name. | `Database Repository` | -| `metadataservice.granularity` | The OAI-PMH exposed record granularity. | `YYYY-MM-DDThh:mm:ssZ` | -| `metadataservice.datacite.enabled` | If set to true, the service mints DOIs instead of local PIDs. | `false` | -| `metadataservice.datacite.url` | The DataCite api endpoint url. | `https://api.datacite.org` | -| `metadataservice.datacite.prefix` | The DataCite prefix. | `""` | -| `metadataservice.datacite.username` | The DataCite api username. | `""` | -| `metadataservice.datacite.password` | The DataCite api user password. | `""` | -| `metadataservice.sparql.connectionTimeout` | The connection timeout for sparql queries fetching remote data in ms. | `10000` | -| `metadataservice.s3.endpoint` | The S3-capable endpoint the microservice connects to. | `http://storageservice-s3:9000` | -| `metadataservice.s3.auth.username` | The S3-capable endpoint username (or access key id). | `seaweedfsadmin` | -| `metadataservice.s3.auth.password` | The S3-capable endpoint user password (or access key secret). | `seaweedfsadmin` | -| `metadataservice.replicaCount` | The number of replicas. | `2` | +| Name | Description | Value | +| ------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | ------------------------------- | +| `metadataservice.enabled` | Enable the Broker Service. | `true` | +| `metadataservice.image.debug` | Set the logging level to `trace`. Otherwise, set to `info`. | `false` | +| `metadataservice.podSecurityContext.enabled` | Enable pods' Security Context | `true` | +| `metadataservice.podSecurityContext.fsGroupChangePolicy` | Set filesystem group change policy | `Always` | +| `metadataservice.podSecurityContext.sysctls` | Set kernel settings using the sysctl interface | `[]` | +| `metadataservice.podSecurityContext.supplementalGroups` | Set filesystem extra groups | `[]` | +| `metadataservice.podSecurityContext.fsGroup` | Set RabbitMQ pod's Security Context fsGroup | `1001` | +| `metadataservice.containerSecurityContext.enabled` | Enabled containers' Security Context | `true` | +| `metadataservice.containerSecurityContext.seLinuxOptions` | Set SELinux options in container | `""` | +| `metadataservice.containerSecurityContext.runAsUser` | Set RabbitMQ containers' Security Context runAsUser | `1001` | +| `metadataservice.containerSecurityContext.runAsGroup` | Set RabbitMQ containers' Security Context runAsGroup | `1001` | +| `metadataservice.containerSecurityContext.runAsNonRoot` | Set RabbitMQ container's Security Context runAsNonRoot | `true` | +| `metadataservice.containerSecurityContext.allowPrivilegeEscalation` | Set container's privilege escalation | `false` | +| `metadataservice.containerSecurityContext.readOnlyRootFilesystem` | Set container's Security Context readOnlyRootFilesystem | `false` | +| `metadataservice.containerSecurityContext.capabilities.drop` | Set container's Security Context runAsNonRoot | `["ALL"]` | +| `metadataservice.containerSecurityContext.seccompProfile.type` | Set container's Security Context seccomp profile | `RuntimeDefault` | +| `metadataservice.endpoint` | The Metadata Service endpoint. | `http://metadata-service` | +| `metadataservice.admin.email` | The OAI-PMH exposed e-mail for contacting the metadata records responsible person. | `noreply@example.com` | +| `metadataservice.deletedRecord` | The OAI-PMH exposed delete policy. | `permanent` | +| `metadataservice.repositoryName` | The OAI-PMH exposed repository name. | `Database Repository` | +| `metadataservice.granularity` | The OAI-PMH exposed record granularity. | `YYYY-MM-DDThh:mm:ssZ` | +| `metadataservice.datacite.enabled` | If set to true, the service mints DOIs instead of local PIDs. | `false` | +| `metadataservice.datacite.url` | The DataCite api endpoint url. | `https://api.datacite.org` | +| `metadataservice.datacite.prefix` | The DataCite prefix. | `""` | +| `metadataservice.datacite.username` | The DataCite api username. | `""` | +| `metadataservice.datacite.password` | The DataCite api user password. | `""` | +| `metadataservice.sparql.connectionTimeout` | The connection timeout for sparql queries fetching remote data in ms. | `10000` | +| `metadataservice.s3.endpoint` | The S3-capable endpoint the microservice connects to. | `http://storageservice-s3:9000` | +| `metadataservice.s3.auth.username` | The S3-capable endpoint username (or access key id). | `seaweedfsadmin` | +| `metadataservice.s3.auth.password` | The S3-capable endpoint user password (or access key secret). | `seaweedfsadmin` | +| `metadataservice.replicaCount` | The number of replicas. | `2` | ### Data Service -| Name | Description | Value | -| -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------- | -| `dataservice.enabled` | Enable the Metadata Service. | `true` | -| `dataservice.endpoint` | The endpoint for the microservices. | `http://data-service` | -| `dataservice.image.debug` | Set the logging level to `trace`. Otherwise, set to `info`. | `false` | -| `dataservice.grant.read` | The default database permissions for users with read access. | `SELECT` | -| `dataservice.grant.write` | The default database permissions for users with write access. | `SELECT, CREATE, CREATE VIEW, CREATE ROUTINE, CREATE TEMPORARY TABLES, LOCK TABLES, INDEX, TRIGGER, INSERT, UPDATE, DELETE` | -| `dataservice.default.date` | The default date format id for dates. Default: YYYY-MM-dd (e.g. 2024-06-15). | `3` | -| `dataservice.default.time` | The default date format id for times. Default: HH:mm:ss (e.g. 14:23:42). | `4` | -| `dataservice.default.timestamp` | The default date format id for timestamps. Default: YYYY-MM-dd HH:mm:ss (e.g. 2024-06-15 14:23:42). | `1` | -| `dataservice.rabbitmq.consumerConcurrentMin` | The minimal number of RabbitMQ consumers. | `2` | -| `dataservice.rabbitmq.consumerConcurrentMax` | The maximal number of RabbitMQ consumers. | `6` | -| `dataservice.rabbitmq.requeueRejected` | If set to true, rejected tuples will be re-queued. | `false` | -| `dataservice.rabbitmq.consumer.username` | The username for the consumer to read tuples from the broker service. In many cases this value is equal to `identityservice.users`. | `admin` | -| `dataservice.rabbitmq.consumer.password` | The user password for the consumer to read tuples from the broker service. In many cases this value is equal to `identityservice.userPasswords`. | `admin` | -| `dataservice.s3.endpoint` | The S3-capable endpoint the microservice connects to. | `http://storageservice-s3:9000` | -| `dataservice.s3.auth.username` | The S3-capable endpoint username (or access key id). | `seaweedfsadmin` | -| `dataservice.s3.auth.password` | The S3-capable endpoint user password (or access key secret). | `seaweedfsadmin` | -| `dataservice.s3.filePath` | The local location to download/upload files from/to S3-capable endpoint. | `/s3` | -| `dataservice.replicaCount` | The number of replicas. | `2` | +| Name | Description | Value | +| --------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------- | +| `dataservice.enabled` | Enable the Broker Service. | `true` | +| `dataservice.image.debug` | Set the logging level to `trace`. Otherwise, set to `info`. | `false` | +| `dataservice.podSecurityContext.enabled` | Enable pods' Security Context | `true` | +| `dataservice.podSecurityContext.fsGroupChangePolicy` | Set filesystem group change policy | `Always` | +| `dataservice.podSecurityContext.sysctls` | Set kernel settings using the sysctl interface | `[]` | +| `dataservice.podSecurityContext.supplementalGroups` | Set filesystem extra groups | `[]` | +| `dataservice.podSecurityContext.fsGroup` | Set RabbitMQ pod's Security Context fsGroup | `1001` | +| `dataservice.containerSecurityContext.enabled` | Enabled containers' Security Context | `true` | +| `dataservice.containerSecurityContext.seLinuxOptions` | Set SELinux options in container | `""` | +| `dataservice.containerSecurityContext.runAsUser` | Set RabbitMQ containers' Security Context runAsUser | `1001` | +| `dataservice.containerSecurityContext.runAsGroup` | Set RabbitMQ containers' Security Context runAsGroup | `1001` | +| `dataservice.containerSecurityContext.runAsNonRoot` | Set RabbitMQ container's Security Context runAsNonRoot | `true` | +| `dataservice.containerSecurityContext.allowPrivilegeEscalation` | Set container's privilege escalation | `false` | +| `dataservice.containerSecurityContext.readOnlyRootFilesystem` | Set container's Security Context readOnlyRootFilesystem | `false` | +| `dataservice.containerSecurityContext.capabilities.drop` | Set container's Security Context runAsNonRoot | `["ALL"]` | +| `dataservice.containerSecurityContext.seccompProfile.type` | Set container's Security Context seccomp profile | `RuntimeDefault` | +| `dataservice.grant.read` | The default database permissions for users with read access. | `SELECT` | +| `dataservice.grant.write` | The default database permissions for users with write access. | `SELECT, CREATE, CREATE VIEW, CREATE ROUTINE, CREATE TEMPORARY TABLES, LOCK TABLES, INDEX, TRIGGER, INSERT, UPDATE, DELETE` | +| `dataservice.default.date` | The default date format id for dates. Default: YYYY-MM-dd (e.g. 2024-06-15). | `3` | +| `dataservice.default.time` | The default date format id for times. Default: HH:mm:ss (e.g. 14:23:42). | `4` | +| `dataservice.default.timestamp` | The default date format id for timestamps. Default: YYYY-MM-dd HH:mm:ss (e.g. 2024-06-15 14:23:42). | `1` | +| `dataservice.rabbitmq.consumerConcurrentMin` | The minimal number of RabbitMQ consumers. | `2` | +| `dataservice.rabbitmq.consumerConcurrentMax` | The maximal number of RabbitMQ consumers. | `6` | +| `dataservice.rabbitmq.requeueRejected` | If set to true, rejected tuples will be re-queued. | `false` | +| `dataservice.rabbitmq.consumer.username` | The username for the consumer to read tuples from the broker service. In many cases this value is equal to `identityservice.users`. | `admin` | +| `dataservice.rabbitmq.consumer.password` | The user password for the consumer to read tuples from the broker service. In many cases this value is equal to `identityservice.userPasswords`. | `admin` | +| `dataservice.s3.endpoint` | The S3-capable endpoint the microservice connects to. | `http://storageservice-s3:9000` | +| `dataservice.s3.auth.username` | The S3-capable endpoint username (or access key id). | `seaweedfsadmin` | +| `dataservice.s3.auth.password` | The S3-capable endpoint user password (or access key secret). | `seaweedfsadmin` | +| `dataservice.s3.filePath` | The local location to download/upload files from/to S3-capable endpoint. | `/s3` | +| `dataservice.replicaCount` | The number of replicas. | `2` | ### Search Service -| Name | Description | Value | -| ---------------------------- | ----------------------------------------------------------- | ----------------------- | -| `searchservice.enabled` | Enable the Search Service. | `true` | -| `searchservice.endpoint` | The endpoint for the microservices. | `http://search-service` | -| `searchservice.image.debug` | Set the logging level to `trace`. Otherwise, set to `info`. | `false` | -| `searchservice.replicaCount` | The number of replicas. | `2` | +| Name | Description | Value | +| ----------------------------------------------------------------- | ----------------------------------------------------------- | ---------------- | +| `searchservice.enabled` | Enable the Broker Service. | `true` | +| `searchservice.image.debug` | Set the logging level to `trace`. Otherwise, set to `info`. | `false` | +| `searchservice.podSecurityContext.enabled` | Enable pods' Security Context | `true` | +| `searchservice.podSecurityContext.fsGroupChangePolicy` | Set filesystem group change policy | `Always` | +| `searchservice.podSecurityContext.sysctls` | Set kernel settings using the sysctl interface | `[]` | +| `searchservice.podSecurityContext.supplementalGroups` | Set filesystem extra groups | `[]` | +| `searchservice.podSecurityContext.fsGroup` | Set RabbitMQ pod's Security Context fsGroup | `1001` | +| `searchservice.containerSecurityContext.enabled` | Enabled containers' Security Context | `true` | +| `searchservice.containerSecurityContext.seLinuxOptions` | Set SELinux options in container | `""` | +| `searchservice.containerSecurityContext.runAsUser` | Set RabbitMQ containers' Security Context runAsUser | `1001` | +| `searchservice.containerSecurityContext.runAsGroup` | Set RabbitMQ containers' Security Context runAsGroup | `1001` | +| `searchservice.containerSecurityContext.runAsNonRoot` | Set RabbitMQ container's Security Context runAsNonRoot | `true` | +| `searchservice.containerSecurityContext.allowPrivilegeEscalation` | Set container's privilege escalation | `false` | +| `searchservice.containerSecurityContext.readOnlyRootFilesystem` | Set container's Security Context readOnlyRootFilesystem | `true` | +| `searchservice.containerSecurityContext.capabilities.drop` | Set container's Security Context runAsNonRoot | `["ALL"]` | +| `searchservice.containerSecurityContext.seccompProfile.type` | Set container's Security Context seccomp profile | `RuntimeDefault` | +| `searchservice.replicaCount` | The number of replicas. | `2` | ### Storage Service @@ -222,25 +283,39 @@ The command removes all the Kubernetes components associated with the chart and ### User Interface -| Name | Description | Value | -| --------------------------------- | ---------------------------------------------------------------------------- | ----------------------- | -| `ui.enabled` | Enable the User Interface. | `true` | -| `ui.image.debug` | Set the logging level to `trace`. Otherwise, set to `info`. | `false` | -| `ui.public.api.client` | The endpoint for the client api. | `""` | -| `ui.public.api.server` | The endpoint for the server api. | `""` | -| `ui.public.title` | The user interface title. | `Database Repository` | -| `ui.public.logo` | The user interface logo. | `/logo.svg` | -| `ui.public.icon` | The user interface icon. | `/favicon.ico` | -| `ui.public.touch` | The user interface apple touch icon. | `/apple-touch-icon.png` | -| `ui.public.broker.host` | The displayed broker hostname. | `example.com` | -| `ui.public.broker.port.5671` | Enable display of the broker 5671 port and mark it as secure (SSL/TLS). | `true` | -| `ui.public.broker.port.5672` | Enable display of the broker 5672 port and mark it as insecure (no SSL/TLS). | `false` | -| `ui.public.broker.extra` | Extra metadata displayed. | `""` | -| `ui.public.database.extra` | Extra metadata displayed. | `128.130.0.0/15` | -| `ui.public.pid.default.publisher` | The default dataset publisher for persisted identifiers. | `Example University` | -| `ui.public.doi.enabled` | Enable the display that DOIs are minted. | `false` | -| `ui.public.doi.endpoint` | The DOI proxy. | `https://doi.org` | -| `ui.replicaCount` | The number of replicas. | `2` | +| Name | Description | Value | +| ------------------------------------------------------ | ---------------------------------------------------------------------------- | ----------------------- | +| `ui.enabled` | Enable the Broker Service. | `true` | +| `ui.image.debug` | Set the logging level to `trace`. Otherwise, set to `info`. | `false` | +| `ui.podSecurityContext.enabled` | Enable pods' Security Context | `true` | +| `ui.podSecurityContext.fsGroupChangePolicy` | Set filesystem group change policy | `Always` | +| `ui.podSecurityContext.sysctls` | Set kernel settings using the sysctl interface | `[]` | +| `ui.podSecurityContext.supplementalGroups` | Set filesystem extra groups | `[]` | +| `ui.podSecurityContext.fsGroup` | Set RabbitMQ pod's Security Context fsGroup | `1001` | +| `ui.containerSecurityContext.enabled` | Enabled containers' Security Context | `true` | +| `ui.containerSecurityContext.seLinuxOptions` | Set SELinux options in container | `""` | +| `ui.containerSecurityContext.runAsUser` | Set RabbitMQ containers' Security Context runAsUser | `1001` | +| `ui.containerSecurityContext.runAsGroup` | Set RabbitMQ containers' Security Context runAsGroup | `1001` | +| `ui.containerSecurityContext.runAsNonRoot` | Set RabbitMQ container's Security Context runAsNonRoot | `true` | +| `ui.containerSecurityContext.allowPrivilegeEscalation` | Set container's privilege escalation | `false` | +| `ui.containerSecurityContext.readOnlyRootFilesystem` | Set container's Security Context readOnlyRootFilesystem | `false` | +| `ui.containerSecurityContext.capabilities.drop` | Set container's Security Context runAsNonRoot | `["ALL"]` | +| `ui.containerSecurityContext.seccompProfile.type` | Set container's Security Context seccomp profile | `RuntimeDefault` | +| `ui.public.api.client` | The endpoint for the client api. | `""` | +| `ui.public.api.server` | The endpoint for the server api. | `""` | +| `ui.public.title` | The user interface title. | `Database Repository` | +| `ui.public.logo` | The user interface logo. | `/logo.svg` | +| `ui.public.icon` | The user interface icon. | `/favicon.ico` | +| `ui.public.touch` | The user interface apple touch icon. | `/apple-touch-icon.png` | +| `ui.public.broker.host` | The displayed broker hostname. | `example.com` | +| `ui.public.broker.port.5671` | Enable display of the broker 5671 port and mark it as secure (SSL/TLS). | `true` | +| `ui.public.broker.port.5672` | Enable display of the broker 5672 port and mark it as insecure (no SSL/TLS). | `false` | +| `ui.public.broker.extra` | Extra metadata displayed. | `""` | +| `ui.public.database.extra` | Extra metadata displayed. | `128.130.0.0/15` | +| `ui.public.pid.default.publisher` | The default dataset publisher for persisted identifiers. | `Example University` | +| `ui.public.doi.enabled` | Enable the display that DOIs are minted. | `false` | +| `ui.public.doi.endpoint` | The DOI proxy. | `https://doi.org` | +| `ui.replicaCount` | The number of replicas. | `2` | ### Ingress diff --git a/helm/dbrepo/templates/_compatibility.tpl b/helm/dbrepo/templates/_compatibility.tpl new file mode 100644 index 0000000000..6fc2aa8fa4 --- /dev/null +++ b/helm/dbrepo/templates/_compatibility.tpl @@ -0,0 +1,42 @@ +{{/* +Copyright Broadcom, Inc. All Rights Reserved. +SPDX-License-Identifier: APACHE-2.0 +*/}} + +{{/* vim: set filetype=mustache: */}} + +{{/* +Return true if the detected platform is Openshift +Usage: +{{- include "common.compatibility.isOpenshift" . -}} +*/}} +{{- define "common.compatibility.isOpenshift" -}} +{{- if .Capabilities.APIVersions.Has "security.openshift.io/v1" -}} +{{- true -}} +{{- end -}} +{{- end -}} + +{{/* +Render a compatible securityContext depending on the platform. By default it is maintained as it is. In other platforms like Openshift we remove default user/group values that do not work out of the box with the restricted-v1 SCC +Usage: +{{- include "common.compatibility.renderSecurityContext" (dict "secContext" .Values.containerSecurityContext "context" $) -}} +*/}} +{{- define "common.compatibility.renderSecurityContext" -}} +{{- $adaptedContext := .secContext -}} + +{{- if (((.context.Values.global).compatibility).openshift) -}} + {{- if or (eq .context.Values.global.compatibility.openshift.adaptSecurityContext "force") (and (eq .context.Values.global.compatibility.openshift.adaptSecurityContext "auto") (include "common.compatibility.isOpenshift" .context)) -}} + {{/* Remove incompatible user/group values that do not work in Openshift out of the box */}} + {{- $adaptedContext = omit $adaptedContext "fsGroup" "runAsUser" "runAsGroup" -}} + {{- if not .secContext.seLinuxOptions -}} + {{/* If it is an empty object, we remove it from the resulting context because it causes validation issues */}} + {{- $adaptedContext = omit $adaptedContext "seLinuxOptions" -}} + {{- end -}} + {{- end -}} +{{- end -}} +{{/* Remove fields that are disregarded when running the container in privileged mode */}} +{{- if $adaptedContext.privileged -}} + {{- $adaptedContext = omit $adaptedContext "capabilities" "seLinuxOptions" -}} +{{- end -}} +{{- omit $adaptedContext "enabled" | toYaml -}} +{{- end -}} \ No newline at end of file diff --git a/helm/dbrepo/templates/analyse-deployment.yaml b/helm/dbrepo/templates/analyse-deployment.yaml index 0cdb067ef7..68d43e9cee 100644 --- a/helm/dbrepo/templates/analyse-deployment.yaml +++ b/helm/dbrepo/templates/analyse-deployment.yaml @@ -22,25 +22,16 @@ spec: app: analyse-service service: analyse-service spec: - securityContext: - runAsNonRoot: true - fsGroup: 1001 - runAsUser: 1001 - runAsGroup: 1001 + {{- if .Values.analyseservice.podSecurityContext.enabled }} + securityContext: {{- include "common.compatibility.renderSecurityContext" (dict "secContext" .Values.analyseservice.podSecurityContext "context" $) | nindent 8 }} + {{- end }} containers: - name: analyse-service image: {{ .Values.analyseservice.image.name }} imagePullPolicy: {{ .Values.analyseservice.image.pullPolicy | default "IfNotPresent" }} - securityContext: - allowPrivilegeEscalation: false - runAsNonRoot: true - runAsUser: 1001 - runAsGroup: 1001 - seccompProfile: - type: {{ .Values.analyseservice.profileType | default "RuntimeDefault" }} - capabilities: - drop: - - ALL + {{- if .Values.analyseservice.containerSecurityContext.enabled }} + securityContext: {{- include "common.compatibility.renderSecurityContext" (dict "secContext" .Values.analyseservice.containerSecurityContext "context" $) | nindent 12 }} + {{- end }} ports: - containerPort: 8080 protocol: TCP @@ -63,4 +54,7 @@ spec: - "curl -sSL localhost:8080/health | grep 'UP' || exit 1" initialDelaySeconds: 10 periodSeconds: 30 + {{- if .Values.analyseservice.resources }} + resources: {{- toYaml .Values.analyseservice.resources | nindent 12 }} + {{- end }} {{- end }} diff --git a/helm/dbrepo/templates/data-deployment.yaml b/helm/dbrepo/templates/data-deployment.yaml index cb8fda0991..1d9e2352bd 100644 --- a/helm/dbrepo/templates/data-deployment.yaml +++ b/helm/dbrepo/templates/data-deployment.yaml @@ -22,25 +22,16 @@ spec: app: data-service service: data-service spec: - securityContext: - runAsNonRoot: true - fsGroup: 65534 - runAsUser: 65534 - runAsGroup: 65534 + {{- if .Values.dataservice.podSecurityContext.enabled }} + securityContext: {{- include "common.compatibility.renderSecurityContext" (dict "secContext" .Values.dataservice.podSecurityContext "context" $) | nindent 8 }} + {{- end }} containers: - name: data-service image: {{ .Values.dataservice.image.name }} imagePullPolicy: {{ .Values.dataservice.image.pullPolicy | default "IfNotPresent" }} - securityContext: - allowPrivilegeEscalation: false - runAsNonRoot: true - runAsUser: 65534 - runAsGroup: 65534 - seccompProfile: - type: {{ .Values.dataservice.profileType | default "RuntimeDefault" }} - capabilities: - drop: - - ALL + {{- if .Values.dataservice.containerSecurityContext.enabled }} + securityContext: {{- include "common.compatibility.renderSecurityContext" (dict "secContext" .Values.dataservice.containerSecurityContext "context" $) | nindent 12 }} + {{- end }} ports: - containerPort: 80 protocol: TCP @@ -63,6 +54,9 @@ spec: - "curl -sSL localhost:8080/actuator/health/liveness | grep 'UP' || exit 1" initialDelaySeconds: 30 periodSeconds: 30 + {{- if .Values.dataservice.resources }} + resources: {{- toYaml .Values.dataservice.resources | nindent 12 }} + {{- end }} volumeMounts: [] volumes: [] {{- end }} diff --git a/helm/dbrepo/templates/metadata-deployment.yaml b/helm/dbrepo/templates/metadata-deployment.yaml index 7c78f853e6..4d16efb68b 100644 --- a/helm/dbrepo/templates/metadata-deployment.yaml +++ b/helm/dbrepo/templates/metadata-deployment.yaml @@ -22,25 +22,16 @@ spec: app: metadata-service service: metadata-service spec: - securityContext: - runAsNonRoot: true - fsGroup: 65534 - runAsUser: 65534 - runAsGroup: 65534 + {{- if .Values.metadataservice.podSecurityContext.enabled }} + securityContext: {{- include "common.compatibility.renderSecurityContext" (dict "secContext" .Values.metadataservice.podSecurityContext "context" $) | nindent 8 }} + {{- end }} containers: - name: metadata-service image: {{ .Values.metadataservice.image.name }} imagePullPolicy: {{ .Values.metadataservice.image.pullPolicy | default "IfNotPresent" }} - securityContext: - allowPrivilegeEscalation: false - runAsNonRoot: true - runAsUser: 65534 - runAsGroup: 65534 - seccompProfile: - type: {{ .Values.metadataservice.profileType | default "RuntimeDefault" }} - capabilities: - drop: - - ALL + {{- if .Values.metadataservice.containerSecurityContext.enabled }} + securityContext: {{- include "common.compatibility.renderSecurityContext" (dict "secContext" .Values.metadataservice.containerSecurityContext "context" $) | nindent 12 }} + {{- end }} ports: - containerPort: 80 protocol: TCP @@ -63,4 +54,7 @@ spec: - "curl -sSL localhost:8080/actuator/health/liveness | grep 'UP' || exit 1" initialDelaySeconds: 30 periodSeconds: 30 + {{- if .Values.metadataservice.resources }} + resources: {{- toYaml .Values.metadataservice.resources | nindent 12 }} + {{- end }} {{- end }} diff --git a/helm/dbrepo/templates/search-deployment.yaml b/helm/dbrepo/templates/search-deployment.yaml index bd937c6650..6ba54abfca 100644 --- a/helm/dbrepo/templates/search-deployment.yaml +++ b/helm/dbrepo/templates/search-deployment.yaml @@ -22,25 +22,16 @@ spec: app: search-service service: search-service spec: - securityContext: - runAsNonRoot: true - fsGroup: 1001 - runAsUser: 1001 - runAsGroup: 1001 + {{- if .Values.searchservice.podSecurityContext.enabled }} + securityContext: {{- include "common.compatibility.renderSecurityContext" (dict "secContext" .Values.searchservice.podSecurityContext "context" $) | nindent 8 }} + {{- end }} initContainers: - name: init image: {{ .Values.searchservice.init.image.name }} imagePullPolicy: {{ .Values.searchservice.init.image.pullPolicy | default "IfNotPresent" }} - securityContext: - allowPrivilegeEscalation: false - runAsNonRoot: true - runAsUser: 1001 - runAsGroup: 1001 - seccompProfile: - type: {{ .Values.searchservice.profileType | default "RuntimeDefault" }} - capabilities: - drop: - - ALL + {{- if .Values.searchservice.containerSecurityContext.enabled }} + securityContext: {{- include "common.compatibility.renderSecurityContext" (dict "secContext" .Values.searchservice.containerSecurityContext "context" $) | nindent 12 }} + {{- end }} envFrom: - secretRef: name: search-service-secret @@ -80,6 +71,9 @@ spec: - "curl -sSL localhost:8080/health | grep 'UP' || exit 1" initialDelaySeconds: 10 periodSeconds: 30 + {{- if .Values.searchservice.resources }} + resources: {{- toYaml .Values.searchservice.resources | nindent 12 }} + {{- end }} volumeMounts: [ ] volumes: [ ] {{- end }} diff --git a/helm/dbrepo/templates/ui-deployment.yaml b/helm/dbrepo/templates/ui-deployment.yaml index 3f8c042579..64cea9bf10 100644 --- a/helm/dbrepo/templates/ui-deployment.yaml +++ b/helm/dbrepo/templates/ui-deployment.yaml @@ -22,22 +22,16 @@ spec: app: ui service: ui spec: - securityContext: - runAsNonRoot: true - fsGroup: 1000 - runAsUser: 1000 - runAsGroup: 1000 + {{- if .Values.ui.podSecurityContext.enabled }} + securityContext: {{- include "common.compatibility.renderSecurityContext" (dict "secContext" .Values.ui.podSecurityContext "context" $) | nindent 8 }} + {{- end }} containers: - name: ui image: {{ .Values.ui.image.name }} imagePullPolicy: {{ .Values.ui.image.pullPolicy | default "IfNotPresent" }} - securityContext: - allowPrivilegeEscalation: false - seccompProfile: - type: {{ .Values.ui.profileType | default "RuntimeDefault" }} - capabilities: - drop: - - ALL + {{- if .Values.ui.containerSecurityContext.enabled }} + securityContext: {{- include "common.compatibility.renderSecurityContext" (dict "secContext" .Values.ui.containerSecurityContext "context" $) | nindent 12 }} + {{- end }} ports: - containerPort: 3000 protocol: TCP @@ -143,6 +137,9 @@ spec: port: 3000 initialDelaySeconds: 30 periodSeconds: 30 + {{- if .Values.ui.resources }} + resources: {{- toYaml .Values.ui.resources | nindent 12 }} + {{- end }} volumes: {{- if .Values.ui.extraVolumes }} {{- .Values.ui.extraVolumes | toYaml | nindent 8 }} diff --git a/helm/dbrepo/values.schema.json b/helm/dbrepo/values.schema.json index 5872dd5e3a..0e1d72462c 100644 --- a/helm/dbrepo/values.schema.json +++ b/helm/dbrepo/values.schema.json @@ -3,6 +3,51 @@ "properties": { "analyseservice": { "properties": { + "containerSecurityContext": { + "properties": { + "allowPrivilegeEscalation": { + "type": "boolean" + }, + "capabilities": { + "properties": { + "drop": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object" + }, + "enabled": { + "type": "boolean" + }, + "readOnlyRootFilesystem": { + "type": "boolean" + }, + "runAsGroup": { + "type": "integer" + }, + "runAsNonRoot": { + "type": "boolean" + }, + "runAsUser": { + "type": "integer" + }, + "seLinuxOptions": { + "type": "string" + }, + "seccompProfile": { + "properties": { + "type": { + "type": "string" + } + }, + "type": "object" + } + }, + "type": "object" + }, "enabled": { "type": "boolean" }, @@ -23,9 +68,56 @@ }, "type": "object" }, + "podSecurityContext": { + "properties": { + "enabled": { + "type": "boolean" + }, + "fsGroup": { + "type": "integer" + }, + "fsGroupChangePolicy": { + "type": "string" + }, + "supplementalGroups": { + "type": "array" + }, + "sysctls": { + "type": "array" + } + }, + "type": "object" + }, "replicaCount": { "type": "integer" }, + "resources": { + "properties": { + "limits": { + "properties": { + "cpu": { + "type": "string" + }, + "memory": { + "type": "string" + } + }, + "type": "object" + }, + "requests": { + "properties": { + "cpu": { + "type": "string" + }, + "memory": { + "type": "string" + } + }, + "type": "object" + } + }, + "type": "object" + }, "s3": { "properties": { "endpoint": { @@ -579,6 +671,51 @@ }, "dataservice": { "properties": { + "containerSecurityContext": { + "properties": { + "allowPrivilegeEscalation": { + "type": "boolean" + }, + "capabilities": { + "properties": { + "drop": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object" + }, + "enabled": { + "type": "boolean" + }, + "readOnlyRootFilesystem": { + "type": "boolean" + }, + "runAsGroup": { + "type": "integer" + }, + "runAsNonRoot": { + "type": "boolean" + }, + "runAsUser": { + "type": "integer" + }, + "seLinuxOptions": { + "type": "string" + }, + "seccompProfile": { + "properties": { + "type": { + "type": "string" + } + }, + "type": "object" + } + }, + "type": "object" + }, "default": { "properties": { "date": { @@ -596,9 +733,6 @@ "enabled": { "type": "boolean" }, - "endpoint": { - "type": "string" - }, "grant": { "properties": { "read": { @@ -624,6 +758,26 @@ }, "type": "object" }, + "podSecurityContext": { + "properties": { + "enabled": { + "type": "boolean" + }, + "fsGroup": { + "type": "integer" + }, + "fsGroupChangePolicy": { + "type": "string" + }, + "supplementalGroups": { + "type": "array" + }, + "sysctls": { + "type": "array" + } + }, + "type": "object" + }, "rabbitmq": { "properties": { "consumer": { @@ -691,6 +845,27 @@ "gateway": { "type": "string" }, + "global": { + "properties": { + "compatibility": { + "properties": { + "openshift": { + "properties": { + "adaptSecurityContext": { + "type": "string" + } + }, + "type": "object" + } + }, + "type": "object" + }, + "storageClass": { + "type": "string" + } + }, + "type": "object" + }, "hostname": { "type": "string" }, @@ -930,6 +1105,51 @@ }, "type": "object" }, + "containerSecurityContext": { + "properties": { + "allowPrivilegeEscalation": { + "type": "boolean" + }, + "capabilities": { + "properties": { + "drop": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object" + }, + "enabled": { + "type": "boolean" + }, + "readOnlyRootFilesystem": { + "type": "boolean" + }, + "runAsGroup": { + "type": "integer" + }, + "runAsNonRoot": { + "type": "boolean" + }, + "runAsUser": { + "type": "integer" + }, + "seLinuxOptions": { + "type": "string" + }, + "seccompProfile": { + "properties": { + "type": { + "type": "string" + } + }, + "type": "object" + } + }, + "type": "object" + }, "datacite": { "properties": { "enabled": { @@ -976,12 +1196,59 @@ }, "type": "object" }, + "podSecurityContext": { + "properties": { + "enabled": { + "type": "boolean" + }, + "fsGroup": { + "type": "integer" + }, + "fsGroupChangePolicy": { + "type": "string" + }, + "supplementalGroups": { + "type": "array" + }, + "sysctls": { + "type": "array" + } + }, + "type": "object" + }, "replicaCount": { "type": "integer" }, "repositoryName": { "type": "string" }, + "resources": { + "properties": { + "limits": { + "properties": { + "cpu": { + "type": "string" + }, + "memory": { + "type": "string" + } + }, + "type": "object" + }, + "requests": { + "properties": { + "cpu": { + "type": "string" + }, + "memory": { + "type": "string" + } + }, + "type": "object" + } + }, + "type": "object" + }, "s3": { "properties": { "auth": { @@ -1065,12 +1332,54 @@ }, "searchservice": { "properties": { + "containerSecurityContext": { + "properties": { + "allowPrivilegeEscalation": { + "type": "boolean" + }, + "capabilities": { + "properties": { + "drop": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object" + }, + "enabled": { + "type": "boolean" + }, + "readOnlyRootFilesystem": { + "type": "boolean" + }, + "runAsGroup": { + "type": "integer" + }, + "runAsNonRoot": { + "type": "boolean" + }, + "runAsUser": { + "type": "integer" + }, + "seLinuxOptions": { + "type": "string" + }, + "seccompProfile": { + "properties": { + "type": { + "type": "string" + } + }, + "type": "object" + } + }, + "type": "object" + }, "enabled": { "type": "boolean" }, - "endpoint": { - "type": "string" - }, "image": { "properties": { "debug": { @@ -1101,8 +1410,55 @@ }, "type": "object" }, + "podSecurityContext": { + "properties": { + "enabled": { + "type": "boolean" + }, + "fsGroup": { + "type": "integer" + }, + "fsGroupChangePolicy": { + "type": "string" + }, + "supplementalGroups": { + "type": "array" + }, + "sysctls": { + "type": "array" + } + }, + "type": "object" + }, "replicaCount": { "type": "integer" + }, + "resources": { + "properties": { + "limits": { + "properties": { + "cpu": { + "type": "string" + }, + "memory": { + "type": "string" + } + }, + "type": "object" + }, + "requests": { + "properties": { + "cpu": { + "type": "string" + }, + "memory": { + "type": "string" + } + }, + "type": "object" + } + }, + "type": "object" } }, "type": "object" @@ -1238,6 +1594,51 @@ }, "ui": { "properties": { + "containerSecurityContext": { + "properties": { + "allowPrivilegeEscalation": { + "type": "boolean" + }, + "capabilities": { + "properties": { + "drop": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object" + }, + "enabled": { + "type": "boolean" + }, + "readOnlyRootFilesystem": { + "type": "boolean" + }, + "runAsGroup": { + "type": "integer" + }, + "runAsNonRoot": { + "type": "boolean" + }, + "runAsUser": { + "type": "integer" + }, + "seLinuxOptions": { + "type": "string" + }, + "seccompProfile": { + "properties": { + "type": { + "type": "string" + } + }, + "type": "object" + } + }, + "type": "object" + }, "enabled": { "type": "boolean" }, @@ -1261,6 +1662,26 @@ }, "type": "object" }, + "podSecurityContext": { + "properties": { + "enabled": { + "type": "boolean" + }, + "fsGroup": { + "type": "integer" + }, + "fsGroupChangePolicy": { + "type": "string" + }, + "supplementalGroups": { + "type": "array" + }, + "sysctls": { + "type": "array" + } + }, + "type": "object" + }, "public": { "properties": { "api": { @@ -1372,6 +1793,33 @@ }, "replicaCount": { "type": "integer" + }, + "resources": { + "properties": { + "limits": { + "properties": { + "cpu": { + "type": "string" + }, + "memory": { + "type": "string" + } + }, + "type": "object" + }, + "requests": { + "properties": { + "cpu": { + "type": "string" + }, + "memory": { + "type": "string" + } + }, + "type": "object" + } + }, + "type": "object" } }, "type": "object" diff --git a/helm/dbrepo/values.yaml b/helm/dbrepo/values.yaml index ba86f30cb7..d910ff084f 100644 --- a/helm/dbrepo/values.yaml +++ b/helm/dbrepo/values.yaml @@ -1,23 +1,29 @@ # Copyright the DBRepo developers # SPDX-License-Identifier: APACHE-2.0 +## @section Global parameters + +global: + ## Compatibility adaptations for Kubernetes platforms + compatibility: + ## Compatibility adaptations for Openshift + openshift: + ## @param global.compatibility.openshift.adaptSecurityContext Adapt the securityContext sections of the deployment to make them compatible with Openshift restricted-v2 SCC: remove runAsUser, runAsGroup and fsGroup and let the platform use their allowed default IDs. Possible values: auto (apply if the detected running cluster is Openshift), force (perform the adaptation always), disabled (do not perform adaptation) + adaptSecurityContext: auto + ## @param global.storageClass Global StorageClass for Persistent Volume(s) + storageClass: "" + ## @section Common parameters -## ## @param namespace The namespace to install the chart -## namespace: dbrepo ## @param hostname The hostname. -## hostname: example.com ## @param gateway The gateway endpoint. -## gateway: https://example.com ## @param strategyType The image pull -## strategyType: RollingUpdate ## @param clusterDomain The cluster domain. -## clusterDomain: cluster.local ## @section Metadata Database @@ -336,6 +342,48 @@ analyseservice: pullPolicy: Always ## @param analyseservice.image.debug Set the logging level to `trace`. Otherwise, set to `info`. debug: false + ## ref: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/#set-the-security-context-for-a-pod + podSecurityContext: + ## @param analyseservice.podSecurityContext.enabled Enable pods' Security Context + enabled: true + ## @param analyseservice.podSecurityContext.fsGroupChangePolicy Set filesystem group change policy + fsGroupChangePolicy: Always + ## @param analyseservice.podSecurityContext.sysctls Set kernel settings using the sysctl interface + sysctls: [ ] + ## @param analyseservice.podSecurityContext.supplementalGroups Set filesystem extra groups + supplementalGroups: [ ] + ## @param analyseservice.podSecurityContext.fsGroup Set RabbitMQ pod's Security Context fsGroup + fsGroup: 1001 + containerSecurityContext: + ## @param analyseservice.containerSecurityContext.enabled Enabled containers' Security Context + enabled: true + ## @param analyseservice.containerSecurityContext.seLinuxOptions Set SELinux options in container + seLinuxOptions: "" + ## @param analyseservice.containerSecurityContext.runAsUser Set RabbitMQ containers' Security Context runAsUser + runAsUser: 1001 + ## @param analyseservice.containerSecurityContext.runAsGroup Set RabbitMQ containers' Security Context runAsGroup + runAsGroup: 1001 + ## @param analyseservice.containerSecurityContext.runAsNonRoot Set RabbitMQ container's Security Context runAsNonRoot + runAsNonRoot: true + ## @param analyseservice.containerSecurityContext.allowPrivilegeEscalation Set container's privilege escalation + allowPrivilegeEscalation: false + ## @param analyseservice.containerSecurityContext.readOnlyRootFilesystem Set container's Security Context readOnlyRootFilesystem + readOnlyRootFilesystem: false + capabilities: + ## @param analyseservice.containerSecurityContext.capabilities.drop Set container's Security Context runAsNonRoot + drop: [ "ALL" ] + seccompProfile: + ## @param analyseservice.containerSecurityContext.seccompProfile.type Set container's Security Context seccomp profile + type: "RuntimeDefault" + ## @skip analyseservice.resources + resources: + requests: + cpu: 250m + memory: 512Mi + limits: + cpu: 500m + memory: 2048Mi + ## @param analyseservice.endpoint The url of the endpoint. endpoint: http://analyse-service s3: @@ -347,7 +395,7 @@ analyseservice: ## @section Metadata Service metadataservice: - ## @param metadataservice.enabled Enable the Metadata Service. + ## @param metadataservice.enabled Enable the Broker Service. enabled: true image: ## @skip metadataservice.image.name @@ -356,6 +404,47 @@ metadataservice: pullPolicy: Always ## @param metadataservice.image.debug Set the logging level to `trace`. Otherwise, set to `info`. debug: false + ## ref: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/#set-the-security-context-for-a-pod + podSecurityContext: + ## @param metadataservice.podSecurityContext.enabled Enable pods' Security Context + enabled: true + ## @param metadataservice.podSecurityContext.fsGroupChangePolicy Set filesystem group change policy + fsGroupChangePolicy: Always + ## @param metadataservice.podSecurityContext.sysctls Set kernel settings using the sysctl interface + sysctls: [ ] + ## @param metadataservice.podSecurityContext.supplementalGroups Set filesystem extra groups + supplementalGroups: [ ] + ## @param metadataservice.podSecurityContext.fsGroup Set RabbitMQ pod's Security Context fsGroup + fsGroup: 1001 + containerSecurityContext: + ## @param metadataservice.containerSecurityContext.enabled Enabled containers' Security Context + enabled: true + ## @param metadataservice.containerSecurityContext.seLinuxOptions Set SELinux options in container + seLinuxOptions: "" + ## @param metadataservice.containerSecurityContext.runAsUser Set RabbitMQ containers' Security Context runAsUser + runAsUser: 1001 + ## @param metadataservice.containerSecurityContext.runAsGroup Set RabbitMQ containers' Security Context runAsGroup + runAsGroup: 1001 + ## @param metadataservice.containerSecurityContext.runAsNonRoot Set RabbitMQ container's Security Context runAsNonRoot + runAsNonRoot: true + ## @param metadataservice.containerSecurityContext.allowPrivilegeEscalation Set container's privilege escalation + allowPrivilegeEscalation: false + ## @param metadataservice.containerSecurityContext.readOnlyRootFilesystem Set container's Security Context readOnlyRootFilesystem + readOnlyRootFilesystem: false + capabilities: + ## @param metadataservice.containerSecurityContext.capabilities.drop Set container's Security Context runAsNonRoot + drop: [ "ALL" ] + seccompProfile: + ## @param metadataservice.containerSecurityContext.seccompProfile.type Set container's Security Context seccomp profile + type: "RuntimeDefault" + ## @skip metadataservice.resources + resources: + requests: + cpu: 250m + memory: 512Mi + limits: + cpu: 1000m + memory: 2048Mi ## @param metadataservice.endpoint The Metadata Service endpoint. endpoint: http://metadata-service admin: @@ -399,10 +488,8 @@ metadataservice: ## @section Data Service dataservice: - ## @param dataservice.enabled Enable the Metadata Service. + ## @param dataservice.enabled Enable the Broker Service. enabled: true - ## @param dataservice.endpoint The endpoint for the microservices. - endpoint: http://data-service image: ## @skip dataservice.image.name name: registry.datalab.tuwien.ac.at/dbrepo/data-service:1.4.4 @@ -410,6 +497,40 @@ dataservice: pullPolicy: Always ## @param dataservice.image.debug Set the logging level to `trace`. Otherwise, set to `info`. debug: false + ## ref: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/#set-the-security-context-for-a-pod + podSecurityContext: + ## @param dataservice.podSecurityContext.enabled Enable pods' Security Context + enabled: true + ## @param dataservice.podSecurityContext.fsGroupChangePolicy Set filesystem group change policy + fsGroupChangePolicy: Always + ## @param dataservice.podSecurityContext.sysctls Set kernel settings using the sysctl interface + sysctls: [ ] + ## @param dataservice.podSecurityContext.supplementalGroups Set filesystem extra groups + supplementalGroups: [ ] + ## @param dataservice.podSecurityContext.fsGroup Set RabbitMQ pod's Security Context fsGroup + fsGroup: 1001 + containerSecurityContext: + ## @param dataservice.containerSecurityContext.enabled Enabled containers' Security Context + enabled: true + ## @param dataservice.containerSecurityContext.seLinuxOptions Set SELinux options in container + seLinuxOptions: "" + ## @param dataservice.containerSecurityContext.runAsUser Set RabbitMQ containers' Security Context runAsUser + runAsUser: 1001 + ## @param dataservice.containerSecurityContext.runAsGroup Set RabbitMQ containers' Security Context runAsGroup + runAsGroup: 1001 + ## @param dataservice.containerSecurityContext.runAsNonRoot Set RabbitMQ container's Security Context runAsNonRoot + runAsNonRoot: true + ## @param dataservice.containerSecurityContext.allowPrivilegeEscalation Set container's privilege escalation + allowPrivilegeEscalation: false + ## @param dataservice.containerSecurityContext.readOnlyRootFilesystem Set container's Security Context readOnlyRootFilesystem + readOnlyRootFilesystem: false + capabilities: + ## @param dataservice.containerSecurityContext.capabilities.drop Set container's Security Context runAsNonRoot + drop: [ "ALL" ] + seccompProfile: + ## @param dataservice.containerSecurityContext.seccompProfile.type Set container's Security Context seccomp profile + type: "RuntimeDefault" + ## @skip dataservice.resources grant: ## @param dataservice.grant.read The default database permissions for users with read access. read: SELECT @@ -454,10 +575,8 @@ dataservice: ## @section Search Service searchservice: - ## @param searchservice.enabled Enable the Search Service. + ## @param searchservice.enabled Enable the Broker Service. enabled: true - ## @param searchservice.endpoint The endpoint for the microservices. - endpoint: http://search-service image: ## @skip searchservice.image.name name: registry.datalab.tuwien.ac.at/dbrepo/search-service:1.4.4 @@ -465,6 +584,47 @@ searchservice: pullPolicy: Always ## @param searchservice.image.debug Set the logging level to `trace`. Otherwise, set to `info`. debug: false + ## ref: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/#set-the-security-context-for-a-pod + podSecurityContext: + ## @param searchservice.podSecurityContext.enabled Enable pods' Security Context + enabled: true + ## @param searchservice.podSecurityContext.fsGroupChangePolicy Set filesystem group change policy + fsGroupChangePolicy: Always + ## @param searchservice.podSecurityContext.sysctls Set kernel settings using the sysctl interface + sysctls: [ ] + ## @param searchservice.podSecurityContext.supplementalGroups Set filesystem extra groups + supplementalGroups: [ ] + ## @param searchservice.podSecurityContext.fsGroup Set RabbitMQ pod's Security Context fsGroup + fsGroup: 1001 + containerSecurityContext: + ## @param searchservice.containerSecurityContext.enabled Enabled containers' Security Context + enabled: true + ## @param searchservice.containerSecurityContext.seLinuxOptions Set SELinux options in container + seLinuxOptions: "" + ## @param searchservice.containerSecurityContext.runAsUser Set RabbitMQ containers' Security Context runAsUser + runAsUser: 1001 + ## @param searchservice.containerSecurityContext.runAsGroup Set RabbitMQ containers' Security Context runAsGroup + runAsGroup: 1001 + ## @param searchservice.containerSecurityContext.runAsNonRoot Set RabbitMQ container's Security Context runAsNonRoot + runAsNonRoot: true + ## @param searchservice.containerSecurityContext.allowPrivilegeEscalation Set container's privilege escalation + allowPrivilegeEscalation: false + ## @param searchservice.containerSecurityContext.readOnlyRootFilesystem Set container's Security Context readOnlyRootFilesystem + readOnlyRootFilesystem: true + capabilities: + ## @param searchservice.containerSecurityContext.capabilities.drop Set container's Security Context runAsNonRoot + drop: [ "ALL" ] + seccompProfile: + ## @param searchservice.containerSecurityContext.seccompProfile.type Set container's Security Context seccomp profile + type: "RuntimeDefault" + ## @skip searchservice.resources + resources: + requests: + cpu: 250m + memory: 512Mi + limits: + cpu: 1000m + memory: 2048Mi ## @skip searchservice.init init: image: @@ -574,7 +734,7 @@ identityservice: ## @section User Interface ui: - ## @param ui.enabled Enable the User Interface. + ## @param ui.enabled Enable the Broker Service. enabled: true image: ## @skip ui.image.name @@ -583,6 +743,47 @@ ui: pullPolicy: Always ## @param ui.image.debug Set the logging level to `trace`. Otherwise, set to `info`. debug: false + ## ref: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/#set-the-security-context-for-a-pod + podSecurityContext: + ## @param ui.podSecurityContext.enabled Enable pods' Security Context + enabled: true + ## @param ui.podSecurityContext.fsGroupChangePolicy Set filesystem group change policy + fsGroupChangePolicy: Always + ## @param ui.podSecurityContext.sysctls Set kernel settings using the sysctl interface + sysctls: [ ] + ## @param ui.podSecurityContext.supplementalGroups Set filesystem extra groups + supplementalGroups: [ ] + ## @param ui.podSecurityContext.fsGroup Set RabbitMQ pod's Security Context fsGroup + fsGroup: 1001 + containerSecurityContext: + ## @param ui.containerSecurityContext.enabled Enabled containers' Security Context + enabled: true + ## @param ui.containerSecurityContext.seLinuxOptions Set SELinux options in container + seLinuxOptions: "" + ## @param ui.containerSecurityContext.runAsUser Set RabbitMQ containers' Security Context runAsUser + runAsUser: 1001 + ## @param ui.containerSecurityContext.runAsGroup Set RabbitMQ containers' Security Context runAsGroup + runAsGroup: 1001 + ## @param ui.containerSecurityContext.runAsNonRoot Set RabbitMQ container's Security Context runAsNonRoot + runAsNonRoot: true + ## @param ui.containerSecurityContext.allowPrivilegeEscalation Set container's privilege escalation + allowPrivilegeEscalation: false + ## @param ui.containerSecurityContext.readOnlyRootFilesystem Set container's Security Context readOnlyRootFilesystem + readOnlyRootFilesystem: false + capabilities: + ## @param ui.containerSecurityContext.capabilities.drop Set container's Security Context runAsNonRoot + drop: [ "ALL" ] + seccompProfile: + ## @param ui.containerSecurityContext.seccompProfile.type Set container's Security Context seccomp profile + type: "RuntimeDefault" + ## @skip ui.resources + resources: + requests: + cpu: 250m + memory: 512Mi + limits: + cpu: 1000m + memory: 2048Mi public: api: ## @param ui.public.api.client The endpoint for the client api. diff --git a/make/gen.mk b/make/gen.mk index 1f8e6fd45d..b81d504213 100644 --- a/make/gen.mk +++ b/make/gen.mk @@ -1,6 +1,6 @@ ##@ Generate -.PHONY: gen-swagger-doc-fe +.PHONY: gen-swagger-doc gen-swagger-doc: build-images ## Generate Swagger documentation and fetch. docker compose up -d bash .docs/.swagger/swagger-generate.sh diff --git a/make/test.mk b/make/test.mk index 5760075a29..c3d2cd8804 100644 --- a/make/test.mk +++ b/make/test.mk @@ -15,30 +15,3 @@ test-analyse-service: ## Test the Analyse Service. .PHONY: test-lib test-lib: ## Test the Python Library. bash ./lib/python/test.sh - -.PHONY: scan-images -scan-images: ## Scan the docker images for vulnerabilities. - trivy image --insecure --exit-code 0 --format template --template "@.trivy/gitlab.tpl" -o ./.trivy/trivy-analyse-service-report.json dbrepo-analyse-service:latest - trivy image --insecure --exit-code 1 --severity CRITICAL dbrepo-analyse-service:latest - trivy image --insecure --exit-code 0 --format template --template "@.trivy/gitlab.tpl" -o ./.trivy/trivy-authentication-service-report.json dbrepo-authentication-service:latest - trivy image --insecure --exit-code 1 --severity CRITICAL dbrepo-authentication-service:latest - trivy image --insecure --exit-code 0 --format template --template "@.trivy/gitlab.tpl" -o ./.trivy/trivy-broker-service-report.json bitnami/rabbitmq:3.10 - trivy image --insecure --exit-code 1 --severity CRITICAL bitnami/rabbitmq:3.10 - trivy image --insecure --exit-code 0 --format template --template "@.trivy/gitlab.tpl" -o ./.trivy/trivy-gateway-service-report.json "nginx:1.25.0-alpine-slim" - trivy image --insecure --exit-code 1 --severity CRITICAL "nginx:1.25.0-alpine-slim" - trivy image --insecure --exit-code 0 --format template --template "@.trivy/gitlab.tpl" -o ./.trivy/trivy-metadata-db-report.json dbrepo-metadata-db:latest - trivy image --insecure --exit-code 1 --severity CRITICAL dbrepo-metadata-db:latest - trivy image --insecure --exit-code 0 --format template --template "@.trivy/gitlab.tpl" -o ./.trivy/trivy-metadata-service-report.json dbrepo-metadata-service:latest - trivy image --insecure --exit-code 1 --severity CRITICAL dbrepo-metadata-service:latest - trivy image --insecure --exit-code 0 --format template --template "@.trivy/gitlab.tpl" -o ./.trivy/trivy-data-service-report.json dbrepo-data-service:latest - trivy image --insecure --exit-code 1 --severity CRITICAL dbrepo-data-service:latest - trivy image --insecure --exit-code 0 --format template --template "@.trivy/gitlab.tpl" -o ./.trivy/trivy-search-db-report.json "dbrepo-search-db" - trivy image --insecure --exit-code 1 --severity CRITICAL "dbrepo-search-db" - trivy image --insecure --exit-code 0 --format template --template "@.trivy/gitlab.tpl" -o ./.trivy/trivy-search-db-report.json "opensearchproject/opensearch-dashboards:2.10.0" - trivy image --insecure --exit-code 1 --severity CRITICAL "opensearchproject/opensearch-dashboards:2.10.0" - trivy image --insecure --exit-code 0 --format template --template "@.trivy/gitlab.tpl" -o ./.trivy/trivy-data-db-report.json "bitnami/mariadb:11.2.2-debian-11-r0" - trivy image --insecure --exit-code 1 --severity CRITICAL "bitnami/mariadb:11.2.2-debian-11-r0" - trivy image --insecure --exit-code 0 --format template --template "@.trivy/gitlab.tpl" -o ./.trivy/trivy-ui-report.json dbrepo-ui:latest - trivy image --insecure --exit-code 1 --severity CRITICAL dbrepo-ui:latest - trivy image --insecure --exit-code 0 --format template --template "@.trivy/gitlab.tpl" -o ./.trivy/trivy-search-service-report.json dbrepo-search-service:latest - trivy image --insecure --exit-code 1 --severity CRITICAL dbrepo-search-service:latest \ No newline at end of file -- GitLab