diff --git a/.docs/.swagger/api-data.yaml b/.docs/.swagger/api-data.yaml index 0b089a632761ddbebfcd7fe34c4a51b41d3573c5..3c8bc053922fe9e29173765b42007ac7f27f5877 100644 --- a/.docs/.swagger/api-data.yaml +++ b/.docs/.swagger/api-data.yaml @@ -14,9 +14,9 @@ externalDocs: url: https://gitlab.phaidra.org/fair-data-austria-db-repository/fda-services servers: - url: http://localhost:9093 - description: Generated server url + description: Development instance - url: https://test.dbrepo.tuwien.ac.at - description: Sandbox + description: Staging instance paths: {} components: securitySchemes: diff --git a/.docs/.swagger/api-metadata.yaml b/.docs/.swagger/api-metadata.yaml index 185142b56e223473f237db623f01f7be43eed5a3..df5304c003b4f4b88431c223fa2233f9dc90a8b6 100644 --- a/.docs/.swagger/api-metadata.yaml +++ b/.docs/.swagger/api-metadata.yaml @@ -14,9 +14,9 @@ externalDocs: url: https://gitlab.phaidra.org/fair-data-austria-db-repository/fda-services servers: - url: http://localhost:9099 - description: Generated server url + description: Development instance - url: https://test.dbrepo.tuwien.ac.at - description: Sandbox + description: Staging instance paths: /api/database/{databaseId}/table/{tableId}/history: get: @@ -38,6 +38,12 @@ paths: type: integer format: int64 responses: + "404": + description: "Table, database or user could not be found" + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorDto' "400": description: Table history query is malformed content: @@ -58,12 +64,6 @@ paths: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "404": - description: "Table, database or user could not be found" - content: - application/json: - schema: - $ref: '#/components/schemas/ApiErrorDto' "409": description: Query store failed to query table history content: @@ -72,6 +72,7 @@ paths: $ref: '#/components/schemas/ApiErrorDto' security: - bearerAuth: [] + - basicAuth: [] head: tags: - table-history-endpoint @@ -91,6 +92,12 @@ paths: type: integer format: int64 responses: + "404": + description: "Table, database or user could not be found" + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorDto' "400": description: Table history query is malformed content: @@ -111,12 +118,6 @@ paths: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "404": - description: "Table, database or user could not be found" - content: - application/json: - schema: - $ref: '#/components/schemas/ApiErrorDto' "409": description: Query store failed to query table history content: @@ -125,6 +126,7 @@ paths: $ref: '#/components/schemas/ApiErrorDto' security: - bearerAuth: [] + - basicAuth: [] /api/database/{databaseId}/table/{tableId}/data: get: tags: @@ -176,20 +178,14 @@ paths: schema: type: string responses: - "422": - description: Could not import csv via sidecar - content: - application/json: - schema: - $ref: '#/components/schemas/ApiErrorDto' - "403": - description: Access to the database is forbidden + "400": + description: Table data is malformed or image is not supported content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "400": - description: Table data is malformed or image is not supported + "404": + description: Table or database could not be found content: application/json: schema: @@ -200,18 +196,26 @@ paths: '*/*': schema: $ref: '#/components/schemas/QueryResultDto' - "404": - description: Table or database could not be found + "403": + description: Access to the database is forbidden + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorDto' + "422": + description: Could not import csv via sidecar content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' security: - bearerAuth: [] + - basicAuth: [] post: tags: - table-data-endpoint summary: Insert data + description: Insert data directly as key-value map tuple operationId: insert parameters: - name: databaseId @@ -239,12 +243,6 @@ paths: '*/*': schema: type: object - "403": - description: Access to the database is forbidden - content: - application/json: - schema: - $ref: '#/components/schemas/ApiErrorDto' "404": description: Table or database could not be found content: @@ -257,12 +255,20 @@ paths: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' + "403": + description: Access to the database is forbidden + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorDto' security: - bearerAuth: [] + - basicAuth: [] delete: tags: - table-data-endpoint summary: Delete data + description: Delete a tuples that match a key-value map operationId: delete_6 parameters: - name: databaseId @@ -286,26 +292,27 @@ paths: responses: "202": description: Deleted table data successfully - "400": - description: Table data or query is malformed + "404": + description: Table or database could not be found content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "403": - description: Access to the database is forbidden + "400": + description: Table data or query is malformed content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "404": - description: Table or database could not be found + "403": + description: Access to the database is forbidden content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' security: - bearerAuth: [] + - basicAuth: [] head: tags: - table-data-endpoint @@ -356,20 +363,14 @@ paths: schema: type: string responses: - "422": - description: Could not import csv via sidecar - content: - application/json: - schema: - $ref: '#/components/schemas/ApiErrorDto' - "403": - description: Access to the database is forbidden + "400": + description: Table data is malformed or image is not supported content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "400": - description: Table data is malformed or image is not supported + "404": + description: Table or database could not be found content: application/json: schema: @@ -380,14 +381,21 @@ paths: '*/*': schema: $ref: '#/components/schemas/QueryResultDto' - "404": - description: Table or database could not be found + "403": + description: Access to the database is forbidden + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorDto' + "422": + description: Could not import csv via sidecar content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' security: - bearerAuth: [] + - basicAuth: [] /api/user/{id}: get: tags: @@ -402,6 +410,12 @@ paths: type: string format: uuid responses: + "200": + description: Found user + content: + application/json: + schema: + $ref: '#/components/schemas/UserDto' "404": description: User was not found content: @@ -414,14 +428,9 @@ paths: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "200": - description: Found user - content: - application/json: - schema: - $ref: '#/components/schemas/UserDto' security: - bearerAuth: [] + - basicAuth: [] put: tags: - user-endpoint @@ -441,8 +450,8 @@ paths: $ref: '#/components/schemas/UserUpdateDto' required: true responses: - "404": - description: User attribute was not found + "403": + description: Modify user is not permitted content: application/json: schema: @@ -453,8 +462,8 @@ paths: application/json: schema: $ref: '#/components/schemas/UserDto' - "403": - description: Modify user is not permitted + "405": + description: Foreign user modification content: application/json: schema: @@ -465,14 +474,15 @@ paths: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "405": - description: Foreign user modification + "404": + description: User attribute was not found content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' security: - bearerAuth: [] + - basicAuth: [] /api/user/{id}/theme: put: tags: @@ -493,6 +503,12 @@ paths: $ref: '#/components/schemas/UserThemeSetDto' required: true responses: + "404": + description: User or user attribute was not found + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorDto' "403": description: Modify user is not permitted content: @@ -505,12 +521,6 @@ paths: application/json: schema: $ref: '#/components/schemas/UserDto' - "404": - description: User or user attribute was not found - content: - application/json: - schema: - $ref: '#/components/schemas/ApiErrorDto' "405": description: Foreign user modification content: @@ -519,6 +529,7 @@ paths: $ref: '#/components/schemas/ApiErrorDto' security: - bearerAuth: [] + - basicAuth: [] /api/user/{id}/password: put: tags: @@ -539,14 +550,14 @@ paths: $ref: '#/components/schemas/UserPasswordDto' required: true responses: - "503": - description: Authentication service does not respond + "404": + description: User was not found content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "404": - description: User was not found + "503": + description: Authentication service does not respond content: application/json: schema: @@ -557,20 +568,21 @@ paths: application/json: schema: $ref: '#/components/schemas/UserDto' - "403": - description: Modify is not allowed + "405": + description: Foreign user modification content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "405": - description: Foreign user modification + "403": + description: Modify is not allowed content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' security: - bearerAuth: [] + - basicAuth: [] /api/semantic/ontology/{id}: get: tags: @@ -616,20 +628,21 @@ paths: $ref: '#/components/schemas/OntologyModifyDto' required: true responses: - "404": - description: Could not find ontology - content: - application/json: - schema: - $ref: '#/components/schemas/ApiErrorDto' "202": description: Updated ontology successfully content: application/json: schema: $ref: '#/components/schemas/OntologyDto' + "404": + description: Could not find ontology + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorDto' security: - bearerAuth: [] + - basicAuth: [] delete: tags: - ontology-endpoint @@ -655,6 +668,7 @@ paths: application/json: {} security: - bearerAuth: [] + - basicAuth: [] /api/maintenance/message/{id}: get: tags: @@ -700,18 +714,18 @@ paths: $ref: '#/components/schemas/BannerMessageUpdateDto' required: true responses: - "202": - description: Updated message - content: - application/json: - schema: - $ref: '#/components/schemas/BannerMessageBriefDto' "404": description: Could not find message content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' + "202": + description: Updated message + content: + application/json: + schema: + $ref: '#/components/schemas/BannerMessageBriefDto' delete: tags: - maintenance-endpoint @@ -749,18 +763,18 @@ paths: type: integer format: int64 responses: - "200": - description: Found image - content: - application/json: - schema: - $ref: '#/components/schemas/ImageDto' "404": description: Image could not be found content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' + "200": + description: Found image + content: + application/json: + schema: + $ref: '#/components/schemas/ImageDto' put: tags: - image-endpoint @@ -780,20 +794,21 @@ paths: $ref: '#/components/schemas/ImageChangeDto' required: true responses: - "202": - description: Updated image successfully - content: - application/json: - schema: - $ref: '#/components/schemas/ImageDto' "404": description: Image could not be found content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' + "202": + description: Updated image successfully + content: + application/json: + schema: + $ref: '#/components/schemas/ImageDto' security: - bearerAuth: [] + - basicAuth: [] delete: tags: - image-endpoint @@ -807,16 +822,17 @@ paths: type: integer format: int64 responses: - "202": - description: Deleted image successfully "404": description: Image could not be found content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' + "202": + description: Deleted image successfully security: - bearerAuth: [] + - basicAuth: [] /api/database/{id}/visibility: put: tags: @@ -837,6 +853,12 @@ paths: $ref: '#/components/schemas/DatabaseModifyVisibilityDto' required: true responses: + "404": + description: Database could not be found + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorDto' "403": description: Visibility modification is not permitted content: @@ -849,14 +871,9 @@ paths: application/json: schema: $ref: '#/components/schemas/DatabaseDto' - "404": - description: Database could not be found - content: - application/json: - schema: - $ref: '#/components/schemas/ApiErrorDto' security: - bearerAuth: [] + - basicAuth: [] /api/database/{id}/transfer: put: tags: @@ -877,12 +894,6 @@ paths: $ref: '#/components/schemas/DatabaseTransferDto' required: true responses: - "404": - description: Database or user could not be found - content: - application/json: - schema: - $ref: '#/components/schemas/ApiErrorDto' "403": description: Transfer of ownership is not permitted content: @@ -895,8 +906,15 @@ paths: application/json: schema: $ref: '#/components/schemas/DatabaseDto' + "404": + description: Database or user could not be found + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorDto' security: - bearerAuth: [] + - basicAuth: [] /api/database/{id}/table/{tableId}/column/{columnId}: put: tags: @@ -934,15 +952,8 @@ paths: $ref: '#/components/schemas/ColumnSemanticsUpdateDto' required: true responses: - "400": - description: Update semantic concept query is malformed or update unit of - measurement query is malformed - content: - application/json: - schema: - $ref: '#/components/schemas/ApiErrorDto' - "403": - description: Access to the database is forbidden + "404": + description: Table or database could not be found content: application/json: schema: @@ -953,20 +964,28 @@ paths: application/json: schema: $ref: '#/components/schemas/ColumnDto' - "404": - description: Table or database could not be found + "400": + description: Update semantic concept query is malformed or update unit of + measurement query is malformed + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorDto' + "403": + description: Access to the database is forbidden content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' security: - bearerAuth: [] - /api/database/{id}/access/{userId}: + - basicAuth: [] + /api/database/{id}/image: put: tags: - - access-endpoint - summary: Modify access to some database - operationId: update_4 + - database-endpoint + summary: Modify database image + operationId: modifyImage parameters: - name: id in: path @@ -974,11 +993,58 @@ paths: schema: type: integer format: int64 - - name: userId - in: path - required: true - schema: - type: string + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/DatabaseModifyImageDto' + required: true + responses: + "410": + description: File was not found in the Storage Service + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorDto' + "202": + description: Modify of image was successful + content: + application/json: + schema: + $ref: '#/components/schemas/DatabaseDto' + "403": + description: Modify of image is not permitted + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorDto' + "404": + description: Database or user could not be found + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorDto' + security: + - bearerAuth: [] + - basicAuth: [] + /api/database/{id}/access/{userId}: + put: + tags: + - access-endpoint + summary: Modify access to some database + operationId: update_4 + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int64 + - name: userId + in: path + required: true + schema: + type: string format: uuid requestBody: content: @@ -987,8 +1053,10 @@ paths: $ref: '#/components/schemas/DatabaseModifyAccessDto' required: true responses: - "400": - description: Modify access query or database connection is malformed + "202": + description: Modify access succeeded + "404": + description: Database or user not found content: application/json: schema: @@ -1000,16 +1068,15 @@ paths: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "404": - description: Database or user not found + "400": + description: Modify access query or database connection is malformed content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "202": - description: Modify access succeeded security: - bearerAuth: [] + - basicAuth: [] post: tags: - access-endpoint @@ -1035,34 +1102,35 @@ paths: $ref: '#/components/schemas/DatabaseGiveAccessDto' required: true responses: - "403": - description: Failed giving access + "404": + description: Database or user not found content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "202": - description: Granting access succeeded - "404": - description: Database or user not found + "400": + description: Granting access query or database connection is malformed content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "405": - description: Granting access not permitted + "403": + description: Failed giving access content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "400": - description: Granting access query or database connection is malformed + "202": + description: Granting access succeeded + "405": + description: Granting access not permitted content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' security: - bearerAuth: [] + - basicAuth: [] delete: tags: - access-endpoint @@ -1082,28 +1150,29 @@ paths: type: string format: uuid responses: - "400": - description: Modify access query or database connection is malformed + "202": + description: Revoked access successfully + "403": + description: Revoke of access not permitted as no access was found content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "404": - description: "User, database with access was not found" + "400": + description: Modify access query or database connection is malformed content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "403": - description: Revoke of access not permitted as no access was found + "404": + description: "User, database with access was not found" content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "202": - description: Revoked access successfully security: - bearerAuth: [] + - basicAuth: [] /api/database/{databaseId}/query/{queryId}: get: tags: @@ -1124,20 +1193,20 @@ paths: type: integer format: int64 responses: - "404": - description: "Database, query or user could not be found" + "501": + description: Image is not supported content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "405": - description: Find query is not permitted + "504": + description: Query store failed to select query content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "503": - description: Connection to the database failed + "404": + description: "Database, query or user could not be found" content: application/json: schema: @@ -1148,20 +1217,21 @@ paths: application/json: schema: $ref: '#/components/schemas/QueryDto' - "501": - description: Image is not supported + "503": + description: Connection to the database failed content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "504": - description: Query store failed to select query + "405": + description: Find query is not permitted content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' security: - bearerAuth: [] + - basicAuth: [] put: tags: - store-endpoint @@ -1187,26 +1257,26 @@ paths: $ref: '#/components/schemas/QueryPersistDto' required: true responses: - "400": - description: Image not supported + "200": + description: Persist query successful content: application/json: schema: - $ref: '#/components/schemas/ApiErrorDto' - "404": - description: "Database, query or user could not be found" + $ref: '#/components/schemas/QueryDto' + "403": + description: Not allowed to persist query content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "403": - description: Not allowed to persist query + "400": + description: Image not supported content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "405": - description: Persist query is not permitted + "404": + description: "Database, query or user could not be found" content: application/json: schema: @@ -1217,14 +1287,15 @@ paths: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "200": - description: Persist query successful + "405": + description: Persist query is not permitted content: application/json: schema: - $ref: '#/components/schemas/QueryDto' + $ref: '#/components/schemas/ApiErrorDto' security: - bearerAuth: [] + - basicAuth: [] /api/user: get: tags: @@ -1258,16 +1329,6 @@ paths: application/json: schema: $ref: '#/components/schemas/UserBriefDto' - "400": - description: Parameters are not well-formed (likely email) - content: - application/json: {} - "417": - description: User with e-mail already exists - content: - application/json: - schema: - $ref: '#/components/schemas/ApiErrorDto' "409": description: User with username already exists content: @@ -1280,6 +1341,16 @@ paths: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' + "400": + description: Parameters are not well-formed (likely email) + content: + application/json: {} + "417": + description: User with e-mail already exists + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorDto' /api/semantic/ontology: get: tags: @@ -1315,6 +1386,7 @@ paths: $ref: '#/components/schemas/OntologyDto' security: - bearerAuth: [] + - basicAuth: [] /api/maintenance/message: get: tags: @@ -1375,26 +1447,27 @@ paths: $ref: '#/components/schemas/ImageCreateDto' required: true responses: - "201": - description: Created image + "400": + description: Image specification is invalid content: application/json: schema: - $ref: '#/components/schemas/ImageDto' + $ref: '#/components/schemas/ApiErrorDto' "409": description: Image already exists content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "400": - description: Image specification is invalid + "201": + description: Created image content: application/json: schema: - $ref: '#/components/schemas/ApiErrorDto' + $ref: '#/components/schemas/ImageDto' security: - bearerAuth: [] + - basicAuth: [] /api/identifier: get: tags: @@ -1457,26 +1530,26 @@ paths: $ref: '#/components/schemas/IdentifierSaveDto' required: true responses: - "502": - description: Query information could not be retrieved + "503": + description: DataCite system did not respond content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "405": - description: Creating identifier not permitted + "403": + description: Insufficient access rights or authorities content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "204": - description: Identifier could not be created + "404": + description: "Failed to find database, table or view" content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "406": - description: Creating identifier not allowed + "204": + description: Identifier could not be created content: application/json: schema: @@ -1487,38 +1560,39 @@ paths: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "404": - description: "Failed to find database, table or view" + "409": + description: Identifier for this resource already exists content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "503": - description: DataCite system did not respond + "201": + description: Created identifier content: application/json: schema: - $ref: '#/components/schemas/ApiErrorDto' - "409": - description: Identifier for this resource already exists + $ref: '#/components/schemas/IdentifierDto' + "405": + description: Creating identifier not permitted content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "201": - description: Created identifier + "502": + description: Query information could not be retrieved content: application/json: schema: - $ref: '#/components/schemas/IdentifierDto' - "403": - description: Insufficient access rights or authorities + $ref: '#/components/schemas/ApiErrorDto' + "406": + description: Creating identifier not allowed content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' security: - bearerAuth: [] + - basicAuth: [] /api/database: get: tags: @@ -1532,6 +1606,12 @@ paths: schema: type: string responses: + "404": + description: User not found + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorDto' "200": description: List of databases content: @@ -1540,12 +1620,6 @@ paths: type: array items: $ref: '#/components/schemas/DatabaseBriefDto' - "404": - description: User not found - content: - application/json: - schema: - $ref: '#/components/schemas/ApiErrorDto' post: tags: - database-endpoint @@ -1558,8 +1632,9 @@ paths: $ref: '#/components/schemas/DatabaseCreateDto' required: true responses: - "503": - description: Connection to the database failed + "403": + description: Database create permission is missing or grant permissions + at broker service failed content: application/json: schema: @@ -1570,15 +1645,8 @@ paths: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "400": - description: Database create query is malformed or image is not supported - content: - application/json: - schema: - $ref: '#/components/schemas/ApiErrorDto' - "403": - description: Database create permission is missing or grant permissions - at broker service failed + "404": + description: "Container, user or database could not be found" content: application/json: schema: @@ -1589,14 +1657,21 @@ paths: application/json: schema: $ref: '#/components/schemas/DatabaseBriefDto' - "404": - description: "Container, user or database could not be found" + "400": + description: Database create query is malformed or image is not supported + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorDto' + "503": + description: Connection to the database failed content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' security: - bearerAuth: [] + - basicAuth: [] head: tags: - database-endpoint @@ -1653,6 +1728,7 @@ paths: $ref: '#/components/schemas/ApiErrorDto' security: - bearerAuth: [] + - basicAuth: [] post: tags: - view-endpoint @@ -1672,14 +1748,20 @@ paths: $ref: '#/components/schemas/ViewCreateDto' required: true responses: - "403": - description: Credentials missing + "423": + description: Create view resulted in an invalid query statement content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "404": - description: Database or user could not be found + "201": + description: Create view successfully + content: + application/json: + schema: + $ref: '#/components/schemas/ViewBriefDto' + "401": + description: Credentials missing content: application/json: schema: @@ -1690,38 +1772,33 @@ paths: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "201": - description: Create view successfully - content: - application/json: - schema: - $ref: '#/components/schemas/ViewBriefDto' "503": description: Connection to the database failed content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "401": - description: Credentials missing + "400": + description: Create view query is malformed content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "400": - description: Create view query is malformed + "403": + description: Credentials missing content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "423": - description: Create view resulted in an invalid query statement + "404": + description: Database or user could not be found content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' security: - bearerAuth: [] + - basicAuth: [] /api/database/{databaseId}/table: get: tags: @@ -1736,6 +1813,12 @@ paths: type: integer format: int64 responses: + "403": + description: List tables not permitted + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorDto' "404": description: Database could not be found content: @@ -1750,14 +1833,9 @@ paths: type: array items: $ref: '#/components/schemas/TableBriefDto' - "403": - description: List tables not permitted - content: - application/json: - schema: - $ref: '#/components/schemas/ApiErrorDto' security: - bearerAuth: [] + - basicAuth: [] post: tags: - table-endpoint @@ -1777,38 +1855,39 @@ paths: $ref: '#/components/schemas/TableCreateDto' required: true responses: - "201": - description: Created a new table + "403": + description: Create table not permitted content: application/json: schema: - $ref: '#/components/schemas/TableBriefDto' + $ref: '#/components/schemas/ApiErrorDto' "409": description: Create table conflicts with existing table name content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "400": - description: Create table query is malformed + "404": + description: "Database, container or user could not be found" content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "404": - description: "Database, container or user could not be found" + "201": + description: Created a new table content: application/json: schema: - $ref: '#/components/schemas/ApiErrorDto' - "403": - description: Create table not permitted + $ref: '#/components/schemas/TableBriefDto' + "400": + description: Create table query is malformed content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' security: - bearerAuth: [] + - basicAuth: [] /api/database/{databaseId}/table/{tableId}/data/import: post: tags: @@ -1837,32 +1916,39 @@ paths: responses: "202": description: Import table data successfully - "422": - description: Could not import csv via sidecar + "400": + description: Table data is malformed content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "403": - description: Access to the database is forbidden + "404": + description: Table or database could not be found content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "404": - description: Table or database could not be found + "409": + description: Import failed in sidecar content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "400": - description: Table data is malformed + "403": + description: Access to the database is forbidden + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorDto' + "422": + description: Could not import csv via sidecar content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' security: - bearerAuth: [] + - basicAuth: [] /api/database/{databaseId}/query: get: tags: @@ -1882,23 +1968,20 @@ paths: schema: type: boolean responses: - "200": - description: List queries + "501": + description: Image is not supported content: application/json: schema: - type: array - items: - $ref: '#/components/schemas/QueryBriefDto' - "405": - description: Find all queries is not permitted + $ref: '#/components/schemas/ApiErrorDto' + "404": + description: "Database, container or user could not be found" content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "423": - description: Selection of time-versioned query resulted in an invalid query - statement + "504": + description: Query store failed to select query content: application/json: schema: @@ -1909,26 +1992,30 @@ paths: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "501": - description: Image is not supported + "200": + description: List queries content: application/json: schema: - $ref: '#/components/schemas/ApiErrorDto' - "504": - description: Query store failed to select query + type: array + items: + $ref: '#/components/schemas/QueryBriefDto' + "405": + description: Find all queries is not permitted content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "404": - description: "Database, container or user could not be found" + "423": + description: Selection of time-versioned query resulted in an invalid query + statement content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' security: - bearerAuth: [] + - basicAuth: [] post: tags: - query-endpoint @@ -1973,30 +2060,18 @@ paths: $ref: '#/components/schemas/ExecuteStatementDto' required: true responses: - "404": - description: "Database, query or user could not be found" + "403": + description: Execute query not permitted content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "202": - description: Executed query - content: - application/json: - schema: - $ref: '#/components/schemas/QueryResultDto' "400": description: Image is not supported content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "403": - description: Execute query not permitted - content: - application/json: - schema: - $ref: '#/components/schemas/ApiErrorDto' "417": description: Could not parse columns content: @@ -2009,8 +2084,21 @@ paths: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' + "404": + description: "Database, query or user could not be found" + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorDto' + "202": + description: Executed query + content: + application/json: + schema: + $ref: '#/components/schemas/QueryResultDto' security: - bearerAuth: [] + - basicAuth: [] /api/container: get: tags: @@ -2045,12 +2133,6 @@ paths: $ref: '#/components/schemas/ContainerCreateRequestDto' required: true responses: - "404": - description: Container image or user could not be found - content: - application/json: - schema: - $ref: '#/components/schemas/ApiErrorDto' "201": description: Created a new container content: @@ -2063,8 +2145,15 @@ paths: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' + "404": + description: Container image or user could not be found + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorDto' security: - bearerAuth: [] + - basicAuth: [] /api/semantic/unit: get: tags: @@ -2104,28 +2193,20 @@ paths: schema: type: string responses: - "404": - description: Could not find ontology - content: - application/json: - schema: - $ref: '#/components/schemas/ApiErrorDto' "400": description: Filter params are invalid content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "200": - description: Found entities + "417": + description: Generated query or uri is malformed content: application/json: schema: - type: array - items: - $ref: '#/components/schemas/EntityDto' - "417": - description: Generated query or uri is malformed + $ref: '#/components/schemas/ApiErrorDto' + "404": + description: Could not find ontology content: application/json: schema: @@ -2136,8 +2217,17 @@ paths: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' + "200": + description: Found entities + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/EntityDto' security: - bearerAuth: [] + - basicAuth: [] /api/semantic/database/{databaseId}/table/{tableId}: get: tags: @@ -2158,8 +2248,8 @@ paths: type: integer format: int64 responses: - "404": - description: Could not find the table + "417": + description: Generated query is malformed content: application/json: schema: @@ -2172,8 +2262,8 @@ paths: type: array items: $ref: '#/components/schemas/TableColumnEntityDto' - "417": - description: Generated query is malformed + "404": + description: Could not find the table content: application/json: schema: @@ -2186,6 +2276,7 @@ paths: $ref: '#/components/schemas/ApiErrorDto' security: - bearerAuth: [] + - basicAuth: [] /api/semantic/database/{databaseId}/table/{tableId}/column/{columnId}: get: tags: @@ -2212,8 +2303,8 @@ paths: type: integer format: int64 responses: - "404": - description: Could not find the table column + "417": + description: Generated query is malformed content: application/json: schema: @@ -2226,8 +2317,8 @@ paths: type: array items: $ref: '#/components/schemas/TableColumnEntityDto' - "417": - description: Generated query is malformed + "404": + description: Could not find the table column content: application/json: schema: @@ -2240,6 +2331,7 @@ paths: $ref: '#/components/schemas/ApiErrorDto' security: - bearerAuth: [] + - basicAuth: [] /api/semantic/concept: get: tags: @@ -2274,18 +2366,6 @@ paths: schema: type: string responses: - "200": - description: Found identifier successfully - content: - application/json: - schema: - $ref: '#/components/schemas/IdentifierDto' - text/csv: {} - text/xml: {} - text/bibliography: {} - text/bibliography; style=apa: {} - text/bibliography; style=ieee: {} - text/bibliography; style=bibtex: {} "400": description: "Identifier could not be exported, the requested style is not\ \ known" @@ -2323,6 +2403,18 @@ paths: text/csv: schema: $ref: '#/components/schemas/ApiErrorDto' + "200": + description: Found identifier successfully + content: + application/json: + schema: + $ref: '#/components/schemas/IdentifierDto' + text/csv: {} + text/xml: {} + text/bibliography: {} + text/bibliography; style=apa: {} + text/bibliography; style=ieee: {} + text/bibliography; style=bibtex: {} /api/oai: get: tags: @@ -2390,6 +2482,12 @@ paths: type: integer format: int64 responses: + "404": + description: Database or exchange could not be found + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorDto' "503": description: Connection to the broker service could not be established content: @@ -2402,14 +2500,9 @@ paths: application/json: schema: $ref: '#/components/schemas/DatabaseDto' - "404": - description: Database or exchange could not be found - content: - application/json: - schema: - $ref: '#/components/schemas/ApiErrorDto' security: - bearerAuth: [] + - basicAuth: [] /api/database/{id}/table/{tableId}/export: get: tags: @@ -2436,50 +2529,57 @@ paths: type: string format: date-time responses: - "404": - description: "Table, database or user was not found" + "422": + description: Sidecar operation could not be completed content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "503": - description: Database connection could not be established + "201": + description: Created identifier + content: + application/json: + schema: + $ref: '#/components/schemas/IdentifierDto' + "409": + description: Failed to export file from sidecar content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "422": - description: Sidecar operation could not be completed + "400": + description: Images is not supported or table/query is malformed content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "403": - description: Operation is not allowed + "410": + description: Blob storage operation could not be completed content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "400": - description: Images is not supported or table/query is malformed + "503": + description: Database connection could not be established content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "410": - description: Blob storage operation could not be completed + "404": + description: "Table, database or user was not found" content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "201": - description: Created identifier + "403": + description: Operation is not allowed content: application/json: schema: - $ref: '#/components/schemas/IdentifierDto' + $ref: '#/components/schemas/ApiErrorDto' security: - bearerAuth: [] + - basicAuth: [] /api/database/{id}/access: get: tags: @@ -2494,12 +2594,6 @@ paths: type: integer format: int64 responses: - "200": - description: Found database access - content: - application/json: - schema: - $ref: '#/components/schemas/DatabaseAccessDto' "403": description: No access to this database content: @@ -2512,8 +2606,15 @@ paths: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' + "200": + description: Found database access + content: + application/json: + schema: + $ref: '#/components/schemas/DatabaseAccessDto' security: - bearerAuth: [] + - basicAuth: [] /api/database/{databaseId}/view/{viewId}: get: tags: @@ -2534,12 +2635,6 @@ paths: type: integer format: int64 responses: - "200": - description: Find view successfully - content: - application/json: - schema: - $ref: '#/components/schemas/ViewDto' "403": description: Find view is not permitted content: @@ -2552,8 +2647,15 @@ paths: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' + "200": + description: Find view successfully + content: + application/json: + schema: + $ref: '#/components/schemas/ViewDto' security: - bearerAuth: [] + - basicAuth: [] delete: tags: - view-endpoint @@ -2573,46 +2675,47 @@ paths: type: integer format: int64 responses: - "403": - description: Deletion not allowed + "405": + description: Delete view is not permitted content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "503": - description: Connection to the database failed + "404": + description: "Database, view or user could not be found" content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "405": - description: Delete view is not permitted + "400": + description: Delete view query is malformed content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "404": - description: "Database, view or user could not be found" + "423": + description: Delete view resulted in an invalid query statement content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "423": - description: Delete view resulted in an invalid query statement + "200": + description: Delete view successfully + "503": + description: Connection to the database failed content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "400": - description: Delete view query is malformed + "403": + description: Deletion not allowed content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "200": - description: Delete view successfully security: - bearerAuth: [] + - basicAuth: [] /api/database/{databaseId}/view/{viewId}/data: get: tags: @@ -2645,32 +2748,33 @@ paths: type: integer format: int64 responses: - "403": - description: View data not allowed + "400": + description: Pagination not in valid range or find data query is malformed content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "404": - description: "Database, view, container or user could not be found" + "200": + description: Find data successfully content: application/json: schema: - $ref: '#/components/schemas/ApiErrorDto' - "400": - description: Pagination not in valid range or find data query is malformed + $ref: '#/components/schemas/QueryResultDto' + "404": + description: "Database, view, container or user could not be found" content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "200": - description: Find data successfully + "403": + description: View data not allowed content: application/json: schema: - $ref: '#/components/schemas/QueryResultDto' + $ref: '#/components/schemas/ApiErrorDto' security: - bearerAuth: [] + - basicAuth: [] /api/database/{databaseId}/view/{viewId}/data/count: get: tags: @@ -2691,20 +2795,14 @@ paths: type: integer format: int64 responses: - "404": - description: "Database, view, container or user could not be found" - content: - application/json: - schema: - $ref: '#/components/schemas/ApiErrorDto' "400": description: Pagination not in valid range or find data query is malformed content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "403": - description: Count data not allowed + "404": + description: "Database, view, container or user could not be found" content: application/json: schema: @@ -2722,8 +2820,15 @@ paths: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' + "403": + description: Count data not allowed + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorDto' security: - bearerAuth: [] + - basicAuth: [] /api/database/{databaseId}/table/{tableId}: get: tags: @@ -2744,32 +2849,33 @@ paths: type: integer format: int64 responses: - "403": - description: Access to the database is forbidden + "200": + description: Find table successfully content: application/json: schema: - $ref: '#/components/schemas/ApiErrorDto' - "404": - description: "Table, database or container could not be found" + $ref: '#/components/schemas/TableDto' + "503": + description: Could not communicate with the broker service content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "200": - description: Find table successfully + "403": + description: Access to the database is forbidden content: application/json: schema: - $ref: '#/components/schemas/TableDto' - "503": - description: Could not communicate with the broker service + $ref: '#/components/schemas/ApiErrorDto' + "404": + description: "Table, database or container could not be found" content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' security: - bearerAuth: [] + - basicAuth: [] delete: tags: - table-endpoint @@ -2791,26 +2897,27 @@ paths: responses: "202": description: Delete table successfully - "403": - description: Access to the database is forbidden + "400": + description: Delete table query resulted in an invalid query statement content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "404": - description: "Table, database or container could not be found" + "403": + description: Access to the database is forbidden content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "400": - description: Delete table query resulted in an invalid query statement + "404": + description: "Table, database or container could not be found" content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' security: - bearerAuth: [] + - basicAuth: [] /api/database/{databaseId}/table/{tableId}/data/count: get: tags: @@ -2837,18 +2944,6 @@ paths: type: string format: date-time responses: - "422": - description: Could not import csv via sidecar - content: - application/json: - schema: - $ref: '#/components/schemas/ApiErrorDto' - "403": - description: Access to the database is forbidden - content: - application/json: - schema: - $ref: '#/components/schemas/ApiErrorDto' "400": description: Table data is malformed or image is not supported content: @@ -2868,8 +2963,21 @@ paths: schema: type: integer format: int64 + "403": + description: Access to the database is forbidden + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorDto' + "422": + description: Could not import csv via sidecar + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorDto' security: - bearerAuth: [] + - basicAuth: [] /api/database/{databaseId}/query/{queryId}/export: get: tags: @@ -2895,38 +3003,38 @@ paths: schema: type: string responses: - "404": - description: Database or query could not be found + "403": + description: Execute query not permitted content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "200": - description: Executed query - content: - '*/*': - schema: - type: object "400": description: Image is not supported content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "403": - description: Execute query not permitted + "410": + description: Could not find in S3 storage content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "422": - description: Sidecar failed to export + "200": + description: Executed query + content: + '*/*': + schema: + type: object + "404": + description: Database or query could not be found content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "410": - description: Could not find in S3 storage + "422": + description: Sidecar failed to export content: application/json: schema: @@ -2939,6 +3047,7 @@ paths: $ref: '#/components/schemas/ApiErrorDto' security: - bearerAuth: [] + - basicAuth: [] /api/database/{databaseId}/query/{queryId}/data: get: tags: @@ -2984,32 +3093,26 @@ paths: schema: type: string responses: - "404": - description: Database or query could not be found + "403": + description: Execute query not permitted content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "202": - description: Executed query - content: - application/json: - schema: - $ref: '#/components/schemas/QueryResultDto' "400": description: Image is not supported content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "403": - description: Execute query not permitted + "417": + description: Could not parse columns content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "417": - description: Could not parse columns + "404": + description: Database or query could not be found content: application/json: schema: @@ -3020,8 +3123,15 @@ paths: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' + "202": + description: Executed query + content: + application/json: + schema: + $ref: '#/components/schemas/QueryResultDto' security: - bearerAuth: [] + - basicAuth: [] /api/database/{databaseId}/query/{queryId}/data/count: get: tags: @@ -3042,38 +3152,39 @@ paths: type: integer format: int64 responses: - "404": - description: Database or query could not be found + "403": + description: Execute query not permitted content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "202": - description: Executed query - content: - application/json: - schema: - $ref: '#/components/schemas/QueryResultDto' "400": description: Image is not supported content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "403": - description: Execute query not permitted + "417": + description: Could not parse columns content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "417": - description: Could not parse columns + "404": + description: Database or query could not be found content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' + "202": + description: Executed query + content: + application/json: + schema: + $ref: '#/components/schemas/QueryResultDto' security: - bearerAuth: [] + - basicAuth: [] /api/database/license: get: tags: @@ -3142,6 +3253,7 @@ paths: type: object security: - bearerAuth: [] + - basicAuth: [] /api/pid/{id}: delete: tags: @@ -3156,14 +3268,14 @@ paths: type: integer format: int64 responses: - "404": - description: Identifier or database could not be found + "403": + description: Deleting identifier not permitted content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "403": - description: Deleting identifier not permitted + "404": + description: Identifier or database could not be found content: application/json: schema: @@ -3176,6 +3288,7 @@ paths: type: object security: - bearerAuth: [] + - basicAuth: [] components: schemas: ApiErrorDto: @@ -3332,6 +3445,9 @@ components: affiliation: type: string example: Brown University + theme_dark: + type: boolean + example: false UserDto: required: - attributes @@ -4011,6 +4127,9 @@ components: $ref: '#/components/schemas/UserDto' owner: $ref: '#/components/schemas/UserDto' + image: + type: string + format: byte created: type: string format: date-time @@ -4998,6 +5117,25 @@ components: is_public: type: boolean example: true + num_rows: + type: integer + format: int64 + example: 5 + data_length: + type: integer + description: in bytes + format: int64 + example: 16384 + max_data_length: + type: integer + description: in bytes + format: int64 + example: 0 + avg_row_length: + type: integer + description: in bytes + format: int64 + example: 3276 UniqueDto: required: - columns @@ -5112,6 +5250,11 @@ components: type: string unit_uri: type: string + DatabaseModifyImageDto: + type: object + properties: + key: + type: string DatabaseModifyAccessDto: required: - type @@ -6839,12 +6982,12 @@ components: type: string identifier: $ref: '#/components/schemas/Identifier' - apaName: - type: string bibtexName: type: string ieeeName: type: string + apaName: + type: string Database: type: object properties: @@ -6901,6 +7044,9 @@ components: $ref: '#/components/schemas/DatabaseAccess' isPublic: type: boolean + image: + type: string + format: byte created: type: string format: date-time @@ -8109,6 +8255,9 @@ components: additionalProperties: type: object securitySchemes: + basicAuth: + type: http + scheme: basic bearerAuth: type: http scheme: bearer diff --git a/.docs/.swagger/custom.css b/.docs/.swagger/custom.css new file mode 100644 index 0000000000000000000000000000000000000000..b4eb2af41ecae38ea62d2a693853ee79905985b8 --- /dev/null +++ b/.docs/.swagger/custom.css @@ -0,0 +1,10 @@ +html, +body { + background: #eee; +} + +.scheme-container { + background: none !important; + box-shadow: none !important; + padding: 0 !important; +} \ No newline at end of file diff --git a/.docs/.swagger/dist.tar.gz b/.docs/.swagger/dist.tar.gz deleted file mode 100644 index c23425955d630805b73530bf20cad4c28369bcb1..0000000000000000000000000000000000000000 Binary files a/.docs/.swagger/dist.tar.gz and /dev/null differ diff --git a/.docs/.swagger/swagger-site.sh b/.docs/.swagger/swagger-site.sh index 98b8056a7773ac0288b3a6f7eaede10444ad0836..b9a0d163e9649e1766a2efed232ea634a45c5916 100644 --- a/.docs/.swagger/swagger-site.sh +++ b/.docs/.swagger/swagger-site.sh @@ -8,8 +8,8 @@ services[9099]=metadata services[3305]=sidecar # clean up -echo "clean up ./dist ./site" -rm -rf ./dist ./site +echo "clean up ./site" +rm -rf ./site # ensure target directories are present echo "ensure target directory ./site are present" @@ -17,11 +17,11 @@ mkdir -p ./site # extract static site echo "extract static site .docs/.swagger/dist.tar.gz" -tar xzf .docs/.swagger/dist.tar.gz for key in "${!services[@]}"; do mkdir -p ./site/${services[$key]} - echo "extract static site ./dist -> ./site/${services[$key]}" - cp -r ./dist/* ./site/${services[$key]} - echo "placing .docs/.swagger/api-${services[$key]}.yaml -> ./site/${services[$key]}/api.yaml" + echo "extract static site ./swagger-ui.html -> ./site/${services[$key]}" + cp .docs/.swagger/swagger-ui.html ./site/${services[$key]}/index.html + cp .docs/.swagger/custom.css ./site/${services[$key]}/custom.css + sed -i -e "s/__SERVICENAME__/${services[$key]^} Service/g" ./site/${services[$key]}/index.html cp ".docs/.swagger/api-${services[$key]}.yaml" "./site/${services[$key]}/api.yaml" done \ No newline at end of file diff --git a/.docs/.swagger/swagger-ui.html b/.docs/.swagger/swagger-ui.html new file mode 100644 index 0000000000000000000000000000000000000000..ef3ce5574300058bb87b6b7d5c2dbbb56c9255d3 --- /dev/null +++ b/.docs/.swagger/swagger-ui.html @@ -0,0 +1,25 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="utf-8"/> + <meta name="viewport" content="width=device-width, initial-scale=1"/> + <meta name="description" content="__SERVICENAME__ API description in OpenAPI 3.0"/> + <title>__SERVICENAME__ API</title> + <link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@5.11.1/swagger-ui.css"/> + <link rel="stylesheet" href="./custom.css"/> + <link rel="icon" href="https://gitlab.phaidra.org/fair-data-austria-db-repository/fda-services/-/raw/master/.docs/images/signet_white.png" /> +</head> +<body> +<div id="swagger-ui"></div> +<script src="https://unpkg.com/swagger-ui-dist@5.11.1/swagger-ui-bundle.js" crossorigin></script> +<script src="https://unpkg.com/swagger-ui-dist@5.11.1/swagger-ui-standalone-preset.js" crossorigin></script> +<script> + window.onload = () => { + window.ui = SwaggerUIBundle({ + url: 'api.yaml', + dom_id: '#swagger-ui', + }); + }; +</script> +</body> +</html> \ No newline at end of file diff --git a/.docs/DEVELOPMENT.md b/.docs/DEVELOPMENT.md index db427a33f97c4b7afdd0a6448e465577f10e4e77..c5ab158886b974f710455783d4fb12c38ecaa4cd 100644 --- a/.docs/DEVELOPMENT.md +++ b/.docs/DEVELOPMENT.md @@ -25,6 +25,18 @@ mvn -f ./dbrepo-metadata-service/pom.xml clean install -DskipTests We practice test-driven development and require contributors to test their code with at least 90% code coverage. +## Code Documentation + +Before creating a merge request, make sure you: + +- [x] Generate the [Swagger Docs](#swagger-docs) endpoint documentation + +### Swagger Docs + +```shell +bash .swagger/swagger-generate.sh +``` + ## Code Versioning ### Branching Strategy diff --git a/.docs/deployment-helm.md b/.docs/deployment-helm.md index 70372724aab70877e3ddebc416085fd07cde5d0f..17d54e79d3d9dc2d0740df4b9c0ccc3f1c81148e 100644 --- a/.docs/deployment-helm.md +++ b/.docs/deployment-helm.md @@ -29,7 +29,7 @@ about values, etc. ## Limitations 1. MariaDB Galera does not (yet) support XA-transactions required by the authentication service (=Keycloak). Therefore - only a single MariaDB pod can be deployed at once for the [auth database](../system-databases-auth). + only a single MariaDB pod can be deployed at once for the [auth database](../system-databases-authentication). !!! question "Do you miss functionality? Do these limitations affect you?" diff --git a/.docs/images/architecture.drawio b/.docs/images/architecture.drawio index f493f476b2e933ed3c1fc5d78ef864dd629ae9b2..430288776b59c34f9274af800c42a0c6fc3d3f44 100644 --- a/.docs/images/architecture.drawio +++ b/.docs/images/architecture.drawio @@ -1,4 +1,4 @@ -<mxfile host="Electron" modified="2024-01-18T09:36:17.787Z" agent="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/22.1.18 Chrome/120.0.6099.199 Electron/28.1.2 Safari/537.36" etag="VyIG_SLmjDzwPbzvCqrk" version="22.1.18" type="device" pages="7"> +<mxfile host="Electron" modified="2024-01-27T20:29:22.079Z" agent="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/23.0.2 Chrome/120.0.6099.109 Electron/28.1.0 Safari/537.36" etag="yip1JA4z9fltPW3TVpkZ" version="23.0.2" type="device" pages="7"> <diagram id="mvBsv1rP8O80Qe3yGnn_" name="docker-compose"> <mxGraphModel dx="1434" dy="822" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1169" pageHeight="827" math="0" shadow="0"> <root> @@ -581,8 +581,8 @@ </root> </mxGraphModel> </diagram> - <diagram id="e2fuorb2m_XjboQ7R7_y" name="Data Ingest"> - <mxGraphModel dx="1147" dy="658" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1169" pageHeight="827" math="0" shadow="0"> + <diagram id="e2fuorb2m_XjboQ7R7_y" name="data-ingest"> + <mxGraphModel dx="1195" dy="685" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1169" pageHeight="827" math="0" shadow="0"> <root> <mxCell id="0" /> <mxCell id="1" parent="0" /> @@ -600,21 +600,6 @@ <mxPoint as="offset" /> </mxGeometry> </mxCell> - <mxCell id="0VJCIkSjTVNNat6pGbHc-35" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.75;exitY=1;exitDx=0;exitDy=0;entryX=0;entryY=0;entryDx=0;entryDy=27.5;entryPerimeter=0;" parent="1" source="0VJCIkSjTVNNat6pGbHc-3" edge="1"> - <mxGeometry relative="1" as="geometry"> - <Array as="points"> - <mxPoint x="480" y="340" /> - <mxPoint x="530" y="340" /> - <mxPoint x="530" y="428" /> - </Array> - <mxPoint x="575" y="427.5" as="targetPoint" /> - </mxGeometry> - </mxCell> - <mxCell id="0VJCIkSjTVNNat6pGbHc-36" value="3306/tcp" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="0VJCIkSjTVNNat6pGbHc-35" vertex="1" connectable="0"> - <mxGeometry x="0.2112" y="-1" relative="1" as="geometry"> - <mxPoint as="offset" /> - </mxGeometry> - </mxCell> <mxCell id="0VJCIkSjTVNNat6pGbHc-41" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.25;exitY=1;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;" parent="1" source="0VJCIkSjTVNNat6pGbHc-3" target="0VJCIkSjTVNNat6pGbHc-33" edge="1"> <mxGeometry relative="1" as="geometry"> <mxPoint x="395" y="318" as="sourcePoint" /> @@ -648,8 +633,8 @@ <mxCell id="0VJCIkSjTVNNat6pGbHc-7" value="<b>JDBC</b>" style="shape=umlActor;verticalLabelPosition=bottom;verticalAlign=top;html=1;outlineConnect=0;" parent="1" vertex="1"> <mxGeometry x="584" y="170" width="30" height="60" as="geometry" /> </mxCell> - <mxCell id="0VJCIkSjTVNNat6pGbHc-8" value="3306/tcp" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#000000;startArrow=classic;startFill=1;dashed=1;" parent="1" edge="1"> - <mxGeometry x="-0.314" relative="1" as="geometry"> + <mxCell id="0VJCIkSjTVNNat6pGbHc-8" value="3307/tcp" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#000000;startArrow=classic;startFill=1;dashed=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;exitPerimeter=0;" parent="1" edge="1" source="yvNGLmbJRMNvVS071MuP-1"> + <mxGeometry x="-0.0158" relative="1" as="geometry"> <mxPoint x="513" y="300" as="sourcePoint" /> <mxPoint as="offset" /> <mxPoint x="600" y="250" as="targetPoint" /> @@ -675,7 +660,7 @@ </mxCell> <mxCell id="0VJCIkSjTVNNat6pGbHc-38" value="3306/tcp" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="0VJCIkSjTVNNat6pGbHc-37" vertex="1" connectable="0"> <mxGeometry x="-0.1716" y="1" relative="1" as="geometry"> - <mxPoint as="offset" /> + <mxPoint x="3" y="-8" as="offset" /> </mxGeometry> </mxCell> <mxCell id="0VJCIkSjTVNNat6pGbHc-26" value="Data Service<br>(Spring AMQP)" style="rounded=1;whiteSpace=wrap;html=1;" parent="1" vertex="1"> @@ -687,18 +672,13 @@ <mxCell id="0VJCIkSjTVNNat6pGbHc-29" value="Broker Service<br style="border-color: var(--border-color);">(RabbitMQ)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#E6E6E6;" parent="1" vertex="1"> <mxGeometry x="399.5" y="360" width="96" height="40" as="geometry" /> </mxCell> - <mxCell id="0VJCIkSjTVNNat6pGbHc-39" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0;entryDx=0;entryDy=52.5;entryPerimeter=0;" parent="1" source="0VJCIkSjTVNNat6pGbHc-33" edge="1"> + <mxCell id="0VJCIkSjTVNNat6pGbHc-39" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;entryPerimeter=0;" parent="1" source="0VJCIkSjTVNNat6pGbHc-33" edge="1" target="yvNGLmbJRMNvVS071MuP-1"> <mxGeometry relative="1" as="geometry"> <Array as="points"> <mxPoint x="530" y="500" /> - <mxPoint x="530" y="453" /> + <mxPoint x="530" y="440" /> </Array> - <mxPoint x="575" y="452.5" as="targetPoint" /> - </mxGeometry> - </mxCell> - <mxCell id="0VJCIkSjTVNNat6pGbHc-40" value="3306/tcp" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="0VJCIkSjTVNNat6pGbHc-39" vertex="1" connectable="0"> - <mxGeometry x="-0.0808" y="-1" relative="1" as="geometry"> - <mxPoint x="-29" y="-1" as="offset" /> + <mxPoint x="570" y="440" as="targetPoint" /> </mxGeometry> </mxCell> <mxCell id="0VJCIkSjTVNNat6pGbHc-33" value="Metadata Service<br>(Spring AMQP)" style="rounded=1;whiteSpace=wrap;html=1;" parent="1" vertex="1"> @@ -710,25 +690,25 @@ <mxCell id="JkB3rZmVi6YrdEwAjtGV-5" value="Storage Service<br>(SeaweedFS)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#E6E6E6;" parent="1" vertex="1"> <mxGeometry x="237" y="394" width="96" height="40" as="geometry" /> </mxCell> - <mxCell id="yvNGLmbJRMNvVS071MuP-1" value="" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;size=8.600000000000023;fillColor=#dae8fc;strokeColor=#000000;" vertex="1" parent="1"> + <mxCell id="yvNGLmbJRMNvVS071MuP-1" value="" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;size=8.600000000000023;fillColor=#dae8fc;strokeColor=#000000;" parent="1" vertex="1"> <mxGeometry x="575" y="408" width="50" height="64" as="geometry" /> </mxCell> - <mxCell id="yvNGLmbJRMNvVS071MuP-2" value="data-db" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;spacing=-1;" vertex="1" parent="1"> + <mxCell id="yvNGLmbJRMNvVS071MuP-2" value="data-db" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;spacing=-1;" parent="1" vertex="1"> <mxGeometry x="559" y="470" width="85" height="20" as="geometry" /> </mxCell> - <mxCell id="jJosZR_OhRTGx9p3jnjP-1" value="" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#E6E6E6;" vertex="1" parent="1"> + <mxCell id="jJosZR_OhRTGx9p3jnjP-1" value="" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#E6E6E6;" parent="1" vertex="1"> <mxGeometry x="447.5" y="540" width="30" height="16" as="geometry" /> </mxCell> - <mxCell id="jJosZR_OhRTGx9p3jnjP-2" value="External images" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1"> + <mxCell id="jJosZR_OhRTGx9p3jnjP-2" value="External images" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="1" vertex="1"> <mxGeometry x="485" y="540" width="140" height="16" as="geometry" /> </mxCell> - <mxCell id="jJosZR_OhRTGx9p3jnjP-3" value="" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1"> + <mxCell id="jJosZR_OhRTGx9p3jnjP-3" value="" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1"> <mxGeometry x="447.5" y="560" width="30" height="16" as="geometry" /> </mxCell> - <mxCell id="jJosZR_OhRTGx9p3jnjP-4" value="Maintained images" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1"> + <mxCell id="jJosZR_OhRTGx9p3jnjP-4" value="Maintained images" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="1" vertex="1"> <mxGeometry x="485" y="560" width="140" height="16" as="geometry" /> </mxCell> - <mxCell id="jJosZR_OhRTGx9p3jnjP-5" value="" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#000000;" vertex="1" parent="1"> + <mxCell id="jJosZR_OhRTGx9p3jnjP-5" value="" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#000000;" parent="1" vertex="1"> <mxGeometry x="462.5" y="540" width="15" height="16" as="geometry" /> </mxCell> </root> diff --git a/.docs/images/data-ingest.svg b/.docs/images/data-ingest.svg index 7d2d1c383092ca7be8be502a74d8f1df3c09f920..20dac76c850b583c00933cffc712aa1158fc2898 100644 --- a/.docs/images/data-ingest.svg +++ b/.docs/images/data-ingest.svg @@ -1,3 +1,3 @@ <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> -<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="408px" height="440px" viewBox="-0.5 -0.5 408 440"><defs/><g><path d="M 211 83 L 211.52 112.52 L 210.48 112.52 L 210.5 134.63" fill="none" stroke="#000000" stroke-miterlimit="10" stroke-dasharray="3 3" pointer-events="stroke"/><path d="M 210.5 139.88 L 206.99 132.88 L 210.5 134.63 L 213.99 132.88 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 112px; margin-left: 212px;"><div data-drawio-colors="color: rgb(0, 0, 0); background-color: rgb(255, 255, 255); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 11px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; background-color: rgb(255, 255, 255); white-space: nowrap;">5672/tcp</div></div></div></foreignObject><text x="212" y="115" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="11px" text-anchor="middle">5672/tcp</text></switch></g><ellipse cx="210.5" cy="7.5" rx="7.5" ry="7.5" fill="rgb(255, 255, 255)" stroke="rgb(0, 0, 0)" pointer-events="all"/><path d="M 210.5 15 L 210.5 40 M 210.5 20 L 195.5 20 M 210.5 20 L 225.5 20 M 210.5 40 L 195.5 60 M 210.5 40 L 225.5 60" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe flex-start; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 67px; margin-left: 211px;"><div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: nowrap;"><b>AMQP</b></div></div></div></foreignObject><text x="211" y="79" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">AMQP</text></switch></g><path d="M 210.5 181 L 210.5 216.63" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 210.5 221.88 L 207 214.88 L 210.5 216.63 L 214 214.88 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 198px; margin-left: 210px;"><div data-drawio-colors="color: rgb(0, 0, 0); background-color: rgb(255, 255, 255); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 11px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; background-color: rgb(255, 255, 255); white-space: nowrap;">5672/tcp</div></div></div></foreignObject><text x="210" y="201" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="11px" text-anchor="middle">5672/tcp</text></switch></g><path d="M 243 181 L 243.04 203 L 293.04 203 L 293.04 290.52 L 331.63 290.5" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 336.88 290.5 L 329.88 294 L 331.63 290.5 L 329.88 287 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 255px; margin-left: 292px;"><div data-drawio-colors="color: rgb(0, 0, 0); background-color: rgb(255, 255, 255); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 11px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; background-color: rgb(255, 255, 255); white-space: nowrap;">3306/tcp</div></div></div></foreignObject><text x="292" y="259" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="11px" text-anchor="middle">3306/tcp</text></switch></g><path d="M 178 181 L 178 203 L 127.04 203 L 127 336.63" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 127 341.88 L 123.5 334.88 L 127 336.63 L 130.5 334.88 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 250px; margin-left: 126px;"><div data-drawio-colors="color: rgb(0, 0, 0); background-color: rgb(255, 255, 255); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 11px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; background-color: rgb(255, 255, 255); white-space: nowrap;">80/tcp</div></div></div></foreignObject><text x="126" y="254" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="11px" text-anchor="middle">80/tcp</text></switch></g><path d="M 178 181 L 178 203 L 48 203 L 48 250.63" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 48 255.88 L 44.5 248.88 L 48 250.63 L 51.5 248.88 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 220px; margin-left: 46px;"><div data-drawio-colors="color: rgb(0, 0, 0); background-color: rgb(255, 255, 255); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 11px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; background-color: rgb(255, 255, 255); white-space: nowrap;">9000/tcp</div></div></div></foreignObject><text x="46" y="224" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="11px" text-anchor="middle">9000/tcp</text></switch></g><rect x="145.5" y="141" width="130" height="40" rx="6" ry="6" fill="#e6e6e6" stroke="rgb(0, 0, 0)" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 128px; height: 1px; padding-top: 161px; margin-left: 147px;"><div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">Gateway Service<br style="border-color: var(--border-color);" />(NGINX)</div></div></div></foreignObject><text x="211" y="165" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">Gateway Service...</text></switch></g><ellipse cx="362" cy="40.5" rx="7.5" ry="7.5" fill="rgb(255, 255, 255)" stroke="rgb(0, 0, 0)" pointer-events="all"/><path d="M 362 48 L 362 73 M 362 53 L 347 53 M 362 53 L 377 53 M 362 73 L 347 93 M 362 73 L 377 93" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe flex-start; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 100px; margin-left: 362px;"><div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: nowrap;"><b>JDBC</b></div></div></div></foreignObject><text x="362" y="112" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">JDBC</text></switch></g><path d="M 282.37 163.03 L 363.52 163.48 L 363.07 119.37" fill="none" stroke="#000000" stroke-miterlimit="10" stroke-dasharray="3 3" pointer-events="stroke"/><path d="M 277.12 163.01 L 284.14 159.54 L 282.37 163.03 L 284.1 166.54 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/><path d="M 363.01 114.12 L 366.58 121.08 L 363.07 119.37 L 359.58 121.15 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 163px; margin-left: 323px;"><div data-drawio-colors="color: rgb(0, 0, 0); background-color: rgb(255, 255, 255); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 11px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; background-color: rgb(255, 255, 255); white-space: nowrap;">3306/tcp</div></div></div></foreignObject><text x="323" y="167" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="11px" text-anchor="middle">3306/tcp</text></switch></g><ellipse cx="58" cy="40.5" rx="7.5" ry="7.5" fill="rgb(255, 255, 255)" stroke="rgb(0, 0, 0)" pointer-events="all"/><path d="M 58 48 L 58 73 M 58 53 L 43 53 M 58 53 L 73 53 M 58 73 L 43 93 M 58 73 L 73 93" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe flex-start; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 100px; margin-left: 58px;"><div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: nowrap;"><b>API</b></div></div></div></foreignObject><text x="58" y="112" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">API</text></switch></g><path d="M 58 119.37 L 58 163 L 139.63 163" fill="none" stroke="#000000" stroke-miterlimit="10" stroke-dasharray="3 3" pointer-events="stroke"/><path d="M 58 114.12 L 61.5 121.12 L 58 119.37 L 54.5 121.12 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/><path d="M 144.88 163 L 137.88 166.5 L 139.63 163 L 137.88 159.5 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 163px; margin-left: 93px;"><div data-drawio-colors="color: rgb(0, 0, 0); background-color: rgb(255, 255, 255); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 11px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; background-color: rgb(255, 255, 255); white-space: nowrap;">80/tcp</div></div></div></foreignObject><text x="93" y="166" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="11px" text-anchor="middle">80/tcp</text></switch></g><path d="M 258.5 303 L 298.24 303 L 331.63 303" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 336.88 303 L 329.88 306.5 L 331.63 303 L 329.88 299.5 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 302px; margin-left: 292px;"><div data-drawio-colors="color: rgb(0, 0, 0); background-color: rgb(255, 255, 255); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 11px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; background-color: rgb(255, 255, 255); white-space: nowrap;">3306/tcp</div></div></div></foreignObject><text x="292" y="306" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="11px" text-anchor="middle">3306/tcp</text></switch></g><rect x="162.5" y="283" width="96" height="40" rx="6" ry="6" fill="rgb(255, 255, 255)" stroke="rgb(0, 0, 0)" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 94px; height: 1px; padding-top: 303px; margin-left: 164px;"><div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">Data Service<br />(Spring AMQP)</div></div></div></foreignObject><text x="211" y="307" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">Data Service...</text></switch></g><path d="M 210.5 263 L 210.5 276.63" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 210.5 281.88 L 207 274.88 L 210.5 276.63 L 214 274.88 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/><rect x="162.5" y="223" width="96" height="40" rx="6" ry="6" fill="#e6e6e6" stroke="rgb(0, 0, 0)" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 94px; height: 1px; padding-top: 243px; margin-left: 164px;"><div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">Broker Service<br style="border-color: var(--border-color);" />(RabbitMQ)</div></div></div></foreignObject><text x="211" y="247" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">Broker Service...</text></switch></g><path d="M 175 363 L 293.04 363 L 293.04 315.48 L 331.63 315.5" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 336.88 315.5 L 329.88 319 L 331.63 315.5 L 329.88 312 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 363px; margin-left: 243px;"><div data-drawio-colors="color: rgb(0, 0, 0); background-color: rgb(255, 255, 255); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 11px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; background-color: rgb(255, 255, 255); white-space: nowrap;">3306/tcp</div></div></div></foreignObject><text x="243" y="367" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="11px" text-anchor="middle">3306/tcp</text></switch></g><rect x="79" y="343" width="96" height="40" rx="6" ry="6" fill="rgb(255, 255, 255)" stroke="rgb(0, 0, 0)" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 94px; height: 1px; padding-top: 363px; margin-left: 80px;"><div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">Metadata Service<br />(Spring AMQP)</div></div></div></foreignObject><text x="127" y="367" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">Metadata Service...</text></switch></g><path d="M 48 297 L 48 363 L 72.63 363" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 77.88 363 L 70.88 366.5 L 72.63 363 L 70.88 359.5 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/><rect x="0" y="257" width="96" height="40" rx="6" ry="6" fill="#e6e6e6" stroke="rgb(0, 0, 0)" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 94px; height: 1px; padding-top: 277px; margin-left: 1px;"><div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">Storage Service<br />(SeaweedFS)</div></div></div></foreignObject><text x="48" y="281" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">Storage Service...</text></switch></g><path d="M 338 279.6 C 338 274.85 349.19 271 363 271 C 369.63 271 375.99 271.91 380.68 273.52 C 385.37 275.13 388 277.32 388 279.6 L 388 326.4 C 388 331.15 376.81 335 363 335 C 349.19 335 338 331.15 338 326.4 Z" fill="#dae8fc" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/><path d="M 388 279.6 C 388 284.35 376.81 288.2 363 288.2 C 349.19 288.2 338 284.35 338 279.6" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/><rect x="322" y="333" width="85" height="20" fill="none" stroke="none" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 89px; height: 1px; padding-top: 343px; margin-left: 320px;"><div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">data-db</div></div></div></foreignObject><text x="365" y="347" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">data-db</text></switch></g><rect x="210.5" y="403" width="30" height="16" fill="#e6e6e6" stroke="rgb(0, 0, 0)" pointer-events="all"/><rect x="248" y="403" width="140" height="16" fill="none" stroke="none" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe flex-start; width: 138px; height: 1px; padding-top: 411px; margin-left: 250px;"><div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: left;"><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">External images</div></div></div></foreignObject><text x="250" y="415" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px">External images</text></switch></g><rect x="210.5" y="423" width="30" height="16" fill="rgb(255, 255, 255)" stroke="rgb(0, 0, 0)" pointer-events="all"/><rect x="248" y="423" width="140" height="16" fill="none" stroke="none" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe flex-start; width: 138px; height: 1px; padding-top: 431px; margin-left: 250px;"><div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: left;"><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">Maintained images</div></div></div></foreignObject><text x="250" y="435" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px">Maintained images</text></switch></g><rect x="225.5" y="403" width="15" height="16" fill="#dae8fc" stroke="#000000" pointer-events="all"/></g><switch><g requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"/><a transform="translate(0,-5)" xlink:href="https://www.drawio.com/doc/faq/svg-export-text-problems" target="_blank"><text text-anchor="middle" font-size="10px" x="50%" y="100%">Text is not SVG - cannot display</text></a></switch></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="408px" height="441px" viewBox="-0.5 -0.5 408 441" style="background-color: rgb(255, 255, 255);"><defs/><rect fill="#ffffff" width="100%" height="100%" x="0" y="0"/><g><path d="M 211 83 L 211.5 112.5 L 210.5 112.5 L 210.5 134.63" fill="none" stroke="#000000" stroke-miterlimit="10" stroke-dasharray="3 3" pointer-events="stroke"/><path d="M 210.5 139.88 L 207 132.88 L 210.5 134.63 L 214 132.88 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 111px; margin-left: 211px;"><div data-drawio-colors="color: rgb(0, 0, 0); background-color: rgb(255, 255, 255); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 11px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; background-color: rgb(255, 255, 255); white-space: nowrap;">5672/tcp</div></div></div></foreignObject><image x="190" y="105" width="42" height="18.5" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAKgAAABKCAYAAAAvxx89AAAAAXNSR0IArs4c6QAAEXVJREFUeF7tnGO0JEkThmPWtm3bnp21bdu2OWvbtm3btm3bO3ue/L6ojc6pqszq27P3VnfGOfNjbmUl3wy8EdW9+vXr10+SpB3ooTvQKwG0h55MmpbbgQTQBIQevQMJoD36eNLkEkATBnr0DiSA9ujjSZNLAE0Y6NE7kADao48nTS4BtCYYuPbaa2X55Zf/H/XSq5d8+umnMtpoo9Vk9s1PswGg5557rqy//vrN9yYiE088sbz55puV+nj22WflggsukPvuu8+9++OPP8rQQw8tY401lsw888yy0korydJLLy0DDzxwab/zzz+/66NVcscdd8hCCy0U7O7++++Xq666Sh555BF5//335ZtvvpFBBx1URhxxRJlssslkjjnmkDXWWEOmnnrqYF9FDbbYYgs55ZRT3OMZZphBnnnmmab7qtOLDQA99thjZfvtt+/S/KsA9MsvvxQ2/oorrgiOOc0008g555wjs8wyS2Hb/xqgTz31lGyyySby9NNPB+dPgyWXXFJOP/10d/GqCvv69ttvu9d22WUXOeyww6p2Ucv2DQDdb7/9ZP/99+/SQmIBymYvuOCC8u6770aPN9RQQ8nNN98svXv3zn3nvwToeeedJxtttJH8+eef0fOn4SijjCJo3CmnnDL6vbfeeksmmWSSrP2dd97p9s7KwQcfLHvuuaf7E5ocrd0O0gDQ7bbbTo477rhsXccff7yMOeaYldY5zDDDyGKLLVb6znfffedMNxuvMvzww8u6664rffr0kTHGGEM++eQTefTRR+XUU0+V77//Pms36qijyssvv+wO2hfM+xdffFFpvrbxoYceKmhFZNhhh5VXX301V9tdd911ssIKK8jff/+dvc7lWX311d382bPffvtNXnvtNbnyyivloYceapjT+OOPLy+99JJzY2Lk5JNPli233NI1ZZyvv/5aBh988IZXl1pqKbnpppvaG6DrrbeeoBlUPv7448oAjdnwjTfeWM4888ys6WyzzSZXX321jD322P29/vnnnwua8ZVXXsmecZGOOeaYmKGi26DVGEdrZ7io22yzTX/vf/vttzLFFFPIZ599lj1DW+GmjDPOOLnjnX/++bLBBhvIX3/9lT0/8MADZa+99oqa37LL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"/></switch></g><ellipse cx="210.5" cy="7.5" rx="7.5" ry="7.5" fill="rgb(255, 255, 255)" stroke="rgb(0, 0, 0)" pointer-events="all"/><path d="M 210.5 15 L 210.5 40 M 210.5 20 L 195.5 20 M 210.5 20 L 225.5 20 M 210.5 40 L 195.5 60 M 210.5 40 L 225.5 60" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe flex-start; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 67px; margin-left: 211px;"><div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: nowrap;"><b>AMQP</b></div></div></div></foreignObject><image x="193" y="67.5" width="36" height="17" xlink:href="data:image/png;base64,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"/></switch></g><path d="M 210.5 181 L 210.5 216.63" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 210.5 221.88 L 207 214.88 L 210.5 216.63 L 214 214.88 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 198px; margin-left: 210px;"><div data-drawio-colors="color: rgb(0, 0, 0); background-color: rgb(255, 255, 255); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 11px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; background-color: rgb(255, 255, 255); white-space: nowrap;">5672/tcp</div></div></div></foreignObject><image x="189" y="190.625" width="42" height="18.5" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAKgAAABKCAYAAAAvxx89AAAAAXNSR0IArs4c6QAAEXVJREFUeF7tnGO0JEkThmPWtm3bnp21bdu2OWvbtm3btm3bO3ue/L6ojc6pqszq27P3VnfGOfNjbmUl3wy8EdW9+vXr10+SpB3ooTvQKwG0h55MmpbbgQTQBIQevQMJoD36eNLkEkATBnr0DiSA9ujjSZNLAE0Y6NE7kADao48nTS4BtCYYuPbaa2X55Zf/H/XSq5d8+umnMtpoo9Vk9s1PswGg5557rqy//vrN9yYiE088sbz55puV+nj22WflggsukPvuu8+9++OPP8rQQw8tY401lsw888yy0korydJLLy0DDzxwab/zzz+/66NVcscdd8hCCy0U7O7++++Xq666Sh555BF5//335ZtvvpFBBx1URhxxRJlssslkjjnmkDXWWEOmnnrqYF9FDbbYYgs55ZRT3OMZZphBnnnmmab7qtOLDQA99thjZfvtt+/S/KsA9MsvvxQ2/oorrgiOOc0008g555wjs8wyS2Hb/xqgTz31lGyyySby9NNPB+dPgyWXXFJOP/10d/GqCvv69ttvu9d22WUXOeyww6p2Ucv2DQDdb7/9ZP/99+/SQmIBymYvuOCC8u6770aPN9RQQ8nNN98svXv3zn3nvwToeeedJxtttJH8+eef0fOn4SijjCJo3CmnnDL6vbfeeksmmWSSrP2dd97p9s7KwQcfLHvuuaf7E5ocrd0O0gDQ7bbbTo477rhsXccff7yMOeaYldY5zDDDyGKLLVb6znfffedMNxuvMvzww8u6664rffr0kTHGGEM++eQTefTRR+XUU0+V77//Pms36qijyssvv+wO2hfM+xdffFFpvrbxoYceKmhFZNhhh5VXX301V9tdd911ssIKK8jff/+dvc7lWX311d382bPffvtNXnvtNbnyyivloYceapjT+OOPLy+99JJzY2Lk5JNPli233NI1ZZyvv/5aBh988IZXl1pqKbnpppvaG6DrrbeeoBlUPv7448oAjdnwjTfeWM4888ys6WyzzSZXX321jD322P29/vnnnwua8ZVXXsmecZGOOeaYmKGi26DVGEdrZ7io22yzTX/vf/vttzLFFFPIZ599lj1DW+GmjDPOOLnjnX/++bLBBhvIX3/9lT0/8MADZa+99oqa37LL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"/></switch></g><path d="M 178 181 L 178 203 L 127 203 L 127 336.63" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 127 341.88 L 123.5 334.88 L 127 336.63 L 130.5 334.88 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 250px; margin-left: 126px;"><div data-drawio-colors="color: rgb(0, 0, 0); background-color: rgb(255, 255, 255); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 11px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; background-color: rgb(255, 255, 255); white-space: nowrap;">80/tcp</div></div></div></foreignObject><image x="111" y="244" width="30" height="18.5" xlink:href="data:image/png;base64,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"/></switch></g><path d="M 178 181 L 178 203 L 48 203 L 48 250.63" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 48 255.88 L 44.5 248.88 L 48 250.63 L 51.5 248.88 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 221px; margin-left: 46px;"><div data-drawio-colors="color: rgb(0, 0, 0); background-color: rgb(255, 255, 255); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 11px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; background-color: rgb(255, 255, 255); white-space: nowrap;">9000/tcp</div></div></div></foreignObject><image x="25" y="215" width="42" height="15.75" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAKgAAAA/CAYAAAB0KINPAAAAAXNSR0IArs4c6QAAESxJREFUeF7tnQXQFMcSx5u4u7s7cXd3d3c34u6eQNwV4oG4C4G4u7sQd3fj1W/e663e/lYvd/cOarqKqnx3MzvTPf9p30u3wYMHD5ZIUQIdKoFuEaAdejJxW0ECEaARCB0tgQjQjj6euLkI0IiBjpZABGhHH0/cXARoxEBHSyACtKOPJ24uAnQIwcBNN90ka6211n9TL926yWeffSYTTDDBELL7xrdZCNC3335brr32WrnvvvvkzTfflK+//lr+/vtvmXjiiWXqqaeWNdZYQ9Zbbz2ZaKKJGt7BSy+9FNa499575ZNPPpHPP/9cRh555LDGzDPPLOuuu66sttpqMuqooza0xk8//SS33Xab3HDDDfLqq6+Gg+WzCSecUCaZZBJZZpllZMMNN5TZZputoeczqdU8sMbOO+8s5557btjjnHPOKc8991zD+x2SJmYC9JtvvpE99thDrrrqqgDIIgJMxxxzTBg/zDDDVOb9yy+/lF122SWAs4wA0wUXXCCrr7562dDU9zybNVirjDbeeGM566yzZOyxxy4bmnzfDh50sWmnnVbefffd8Od+++0nJ554YuV9DskDuwD09ddfl1VWWSURRlXm0HR9+/atBFIEvfDCCwdtWYeOPvpoOeSQQypN2XfffaVXr16VxuqgKaaYQh599FGZdNJJS+e1gwfdxDvvvCPTTTddsiesDZrf0nHHHScHH3xw+Oixxx6TBRdcsJSHIWFACqDffvutzDfffIJALKG51l57beEWQ4D4yiuvlPvvvz81Di166qmnFvL93XffBeG98cYbybhRRhlF1l9/fVl55ZVlsskmk59//llee+016dOnjzz77LOp51199dXBJBfReeedJzvttFNqyOyzzy5bbrmlzDLLLDLmmGMGd+Luu+8OVgKTrzT33HPLQw89JOwpj9rBg137nHPOCZYAYl9YuBFHHDG1vVVXXVVuv/32oRug2267rVx88cUJ46OPPrpcf/31stxyy2We1SWXXCLbb7994gbgvD/11FMyzzzz5B7uXnvtlQLxTDPNJAQAM844Y+Yc3IdDDz00+W6sscaS999/P4Asi/AxuUi//PJL8vWRRx4ZnsH+PL311lvBdeDSKR1xxBFy+OGH/994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"/></switch></g><rect x="145.5" y="141" width="130" height="40" rx="6" ry="6" fill="#e6e6e6" stroke="rgb(0, 0, 0)" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 128px; height: 1px; padding-top: 161px; margin-left: 147px;"><div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">Gateway Service<br style="border-color: var(--border-color);" />(NGINX)</div></div></div></foreignObject><image x="147" y="147" width="128" height="32" xlink:href="data:image/png;base64,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"/></switch></g><ellipse cx="362" cy="40.5" rx="7.5" ry="7.5" fill="rgb(255, 255, 255)" stroke="rgb(0, 0, 0)" pointer-events="all"/><path d="M 362 48 L 362 73 M 362 53 L 347 53 M 362 53 L 377 53 M 362 73 L 347 93 M 362 73 L 377 93" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe flex-start; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 100px; margin-left: 362px;"><div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: nowrap;"><b>JDBC</b></div></div></div></foreignObject><image x="345.5" y="100.5" width="33" height="17" xlink:href="data:image/png;base64,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"/></switch></g><path d="M 363 264.63 L 363 192.5 L 363 119.37" fill="none" stroke="#000000" stroke-miterlimit="10" stroke-dasharray="3 3" pointer-events="stroke"/><path d="M 363 269.88 L 359.5 262.88 L 363 264.63 L 366.5 262.88 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/><path d="M 363 114.12 L 366.5 121.12 L 363 119.37 L 359.5 121.12 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 193px; margin-left: 363px;"><div data-drawio-colors="color: rgb(0, 0, 0); background-color: rgb(255, 255, 255); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 11px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; background-color: rgb(255, 255, 255); white-space: nowrap;">3307/tcp</div></div></div></foreignObject><image x="342" y="187" width="42" height="15.75" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAKgAAAA/CAYAAAB0KINPAAAAAXNSR0IArs4c6QAAEZlJREFUeF7tnQW0VVUTxwcTW8EO7MTuAuwEC6UUsbC7G7sLDBABW0JFDLBQUbCxuzuwu1D51m+75qy5m3Pu2fu++6337n1n1mIp9+6zz47ZM//5z+xLiylTpkyRQooVaKIr0KJQ0Ca6M8Ww3AoUClooQpNegUJBm/T2FIMrFLTQgSa9AoWCNuntKQZXKGihA016BQoFbdLbUwyuUNAa0YFRo0bJjjvu+B/10qKFfPnllzLvvPPWyOgrH2ZZBX355Zdl6NCh8vTTT8vbb78t33//vfz9998yxxxzyIILLiirr766bLbZZrLTTjtJy5Yto0fx22+/ya233ipjx46V5557TiZNmiQ//fSTzDzzzDLnnHPK8ssvL+uss450795dlltuuej+eeCXX36Re+65R0aOHCmvv/6621g+m2+++dwcNt10U+nWrZusuOKKuf2PGzdONt5449x2oQ022GADmTBhQlDzAw88UPr37+/arrrqqvLCCy8EPVfrjVIV9K233pKDDjpIHnrooaD5tWrVSs477zzZZ5993OnOk3/++Ucuv/xyOe200+THH3/Ma+6+33bbbeXKK6+URRddNKg9jVB+5vH111/nPtOjRw+54oorZK655sps25gKuuSSS8r777/vxnbsscfK+eefnzunemgwlYLee++90rlzZ/n999+j59erVy8ZMmSITDPNNJnP0i/9855YwapibddYY43cR4855hi56KKLctvZBm3atJEnnnhCFlpoodTnGktB33vvPVlqqaWSMbEGWH4r55xzjpx00knuoyeffFLWXXfdqLk31cYlCoqbXX/99eWvv/5KxovrRqE22WQTWWSRRZzy4SbZyFtuuUV++OGHkrmdddZZyUKlTXrnnXeW22+/veSrFVZYwbnZlVde2cGHX3/9Vd544w258847p3KB88wzj2Dhy1m6AQMGyAEHHFDyDvreY489hHfxjs8//1zuv/9+NwdcvgqwZfz48Q5m+IIlfvTRRyveSyzgcccdlzx/1FFHBR2iq666ynkChHF99913MuOMM5aMo2PHjjJ69Oj6VVBqRtjEV199NZk4lgo3ufjii6duyjfffCNdunSRRx55JPl+pplmkg8//DAVwN92222yyy67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"/></switch></g><ellipse cx="58" cy="40.5" rx="7.5" ry="7.5" fill="rgb(255, 255, 255)" stroke="rgb(0, 0, 0)" pointer-events="all"/><path d="M 58 48 L 58 73 M 58 53 L 43 53 M 58 53 L 73 53 M 58 73 L 43 93 M 58 73 L 73 93" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe flex-start; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 100px; margin-left: 58px;"><div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: nowrap;"><b>API</b></div></div></div></foreignObject><image x="48" y="100.5" width="20" height="17" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFAAAABECAYAAAAWVrIgAAAAAXNSR0IArs4c6QAABOFJREFUeF7tmVkIblMUx3/XXJQ5Q+b5liSRIcqLMYoMb1fcS917JZFZxlCGIooQ8aQQohBKZvEgeTDPQ8gcyez8tb/6bGufs89ZZ3/5uns9nrPX3mv/ztrDf50FVHMRWODyrs5UgM4kqAArQCcBp3vNwArQScDpXjOwAnQScLrXDKwAnQSc7kMycBXgM2DDlrFvApY7YlsD+Nnh/xPwPfA18CrwEvAw8G5mn23jfwhsNelnCMDDgIc6AvkG2AT4NTPguJkXoDXsX8ATwCXAcx1xFQV4N3BMBpgjgQcy2llNSgCcjPMncG2T4WcDfyTiKwZwbeBzQAN02X3AUV2NBkxgYJf/cbsTOAFQZvZZAa4lfBJwizGg9pw1o+davhsD3w6YcckMnA7nTOCaWQJ8GtgvGlBf8OKwt8SxLG32wptHBqj99fyWPlcH1m+2j12bve6AjtXyA7B9c+B9GfVXZAlvHU6x+OB5uTnptN993ECM32mz3ndkgP9aQh19rxf2u+Na2mkvvGoWAC8ALjUC0fPLwlVhD+P9tsB7PSFmZ0Bmv9qP9ZEtewrYfxYA3wR2MCJYCLwBnAtcYby/KAG+be5jA9y5OdBeSwyo/Xut0gD3Al4wAlBQu4TnOwaQcbN3wj6TmSz/NBsboPr8KuyNVhwCKJATyx4/9yJ9I7CsZflOXunWPwE63Xxv4MUeBLMn0KPP1ApSF5sBn5YCuFqQbjrZYtOSfnvq4XnA5Ua7vtKuBEDdXzdKAF8nSL8iGXgEcL8x8CvAbtFzHRhasrH1lXZjA9wc+CgB7ztg3ehd9vg5Szh1gp0DXGkEpWvN7sbzPtIuewKZS1hxnpVo+zhwYCmA+jJKfS3j2LYB3jeen9GokquN532k3ZgApdvvAlZOAFTVSFvMtGWP35WBOjh0gMSm8tCeiYC2AD4wLtV9pF32BIwYVG7bIKyC4zv0uLaWLYEfSwF8HtAJGtvp4YafWkEpv1xpNystfHIiQbI/YFsGbhedsBNY0r7Ksk9a9p9TgeuM97nSbhYA7wWOLVmNUeHxQgPCs0ZBIW62adDGKxn+OdKuNMB7Gl2/qFFPvySSYJQMVPlbB8XYliPtSgFUaU2VnPjQiOfoBqgKyjNjkwv95Ui7MQH+HlSQKukqoqqE1WVugCqaqnhayrqkXdsEpGlP6wjst3Cyap9+a8APKhdAFSR195O8KWVd0i57AoUCzB7fOoWPBrTJlrQuaZc9gUJBZo9vAXwQONwITJrRet42B5XVb0g0aJN22RP4vwHUDV4/zVc1Arsj/MXqE7P6+cIQ6+qjTdrNLcBTgOsThA5t5NsjfeiFtrcnwLdJu7kFKI1r/dfQ/Um1NJ1ufe3gFvApaTeXAHcCXk/QURYt6UsutJe41zLWH7LYUtJuLgGqkqyKsmWHAI8OBCi325qC5uKEvyXt5g6gTmPV9lTaiU1XDi1f3eiH2kEtH8CSdnMHUP9Fn0zQUfacOJTc1DLW5dz6r2JJu7kD2LbElD2POQHK/daWD7FP9Nt07gCOwGfF7KKrpL9iUukx6wqwByyraQVYAToJON1rBlaATgJO95qBFaCTgNO9ZmAF6CTgdK8ZWAE6CTjdawZWgE4CTveagRWgk4DTvWZgBegk4HSvGVgBOgk43WsGVoBOAk73moFOgH8DAtsrVLJLg14AAAAASUVORK5CYII="/></switch></g><path d="M 58 119.37 L 58 163 L 139.63 163" fill="none" stroke="#000000" stroke-miterlimit="10" stroke-dasharray="3 3" pointer-events="stroke"/><path d="M 58 114.12 L 61.5 121.12 L 58 119.37 L 54.5 121.12 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/><path d="M 144.88 163 L 137.88 166.5 L 139.63 163 L 137.88 159.5 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 163px; margin-left: 93px;"><div data-drawio-colors="color: rgb(0, 0, 0); background-color: rgb(255, 255, 255); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 11px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; background-color: rgb(255, 255, 255); white-space: nowrap;">80/tcp</div></div></div></foreignObject><image x="78" y="155.625" width="30" height="18.5" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAHgAAABKCAYAAABw1pB0AAAAAXNSR0IArs4c6QAADmNJREFUeF7tnAnQT9Ubxx9J9hKyZJcllGilhbKHFmUPLZjQYpeksiZ70YYWytZQ1qISipoWa5ElS4rsFEpZ8p/Pac6d8zvvvfd37zve+b9u95lp5nXvOeeec77n2b7P+ZXhzJkzZySWyO5AhhjgyGKrFhYDHG18Y4Ajjm8McAxw1Hcg4uuLfXAMcMR3IOLL+09o8OzZs6VRo0b/pg0ZMsiePXskX758EYf23+X5Arxu3TqZNm2afPXVV7Jp0yY5fPiwnDp1Si666CIpWLCgXHPNNVKnTh21eZkzZ07Vhn3//fcyY8YMWbRokfz666+yd+9eyZo1qxq/XLly0rhxY7njjjske/bsqRqfTp06dZJXX31V9a9UqZKsXr061WOdax1dAd6+fbt07NhRPvroo0DryZMnjwwZMkTatWunNCSI7N+/Xx555BEFbjLJnz+/jB8/Xu68885kTV3fX3bZZbJt2zb1rlevXjJ06NBUjXMudkoB8PLly6V+/fpy9OjRFOvJkiWLXHDBBeqdG8N53333yaRJkyRjxoy+e8Fm33jjjUpbw8jAgQOlb9++YbrI1q1bpVSpUk4fLEXNmjUTxvjyyy/lpptuUs84qL179w71jfTcOAHgn376Sa688ko5duyYM+errrpKLbhWrVqSN29e9fzvv/9WZu6NN95Q/5lgJ9OQ3377TapUqaJMvpZs2bJJ06ZN1cEqXLiw/PHHH7JhwwaZOHGirFq1KmH/cBnNmzcPvKevvPKKshQI3zl06FAKdzJixAjp2bNn9AG+5557ZNasWc7m4fvee+89yZQpk+eGzpw5U4GjQT7//PPlhx9+kNKlS7v26datm4wePdp5d/nllwtBUNmyZV3bDxo0SJ5++mnnXa5cuYSDSBwQRO666y6ZO3eualqvXj1ZsGBBim7muiOrwZxsfB1BFII5ZiN5lkwIhDgIWgYMGJAAin5OEIU//Ouvv9SjHDlyKEtgmlC3b+HbsRRannnmGenfv3+yacnJkyeF+EC7m1GjRknXrl1T9CtQoIDjLiILML6pdu3azuL5++OPP066iTSYMmWKtGrVKkHztdaYA9jaGBQoovdLL73UORi4Cvz3eeed5zu/zz//XKpXr+60ISuoUKFCQh/bR0cW4KlTpwpBkha0ZsKECYEA/uKLL+Tmm2922l5//fXy9ddfp+h7ww03yDfffKOeA86OHTuUzw0izI05aiEY1IGRV/+nnnpKnnvuOfWaA7Jr1y7194EDB+SSSy4J8ln1Db7lJcQjuLUPPvhAVqxYIfv27VMWAxeCZbrlllvkwQcfVCmfn9CGmEML1rNYsWLqn9999528/vrrwoHlOTHKxRdfLIUKFVJpauvWreWKK65wHd4JsubMmSN333230+j+++9P+KDf5JYuXSq33Xab04S/Fy9enNCFTYVc0L6aCZEDB5W3335bmJOWJ5980gHPa4zrrrtObTpirudsAcyeYfJJK5MJ33/hhReEGMJNHnvsMXnppZecV2vWrJGKFSvKE088IQSBfvcyyFoef/xxGT58eIoMxgGYSZYsWdL5AJujtS3Z5PGPaLwWPvbiiy8mdLNdwMMPPyyvvfZasqGd9z/++KOUKVPG+XfdunVl4cKFnv0PHjyoDtQ///yj2uBGWrZsqf7WkTx///7774rZ0oJm586d2/k3ZA59bWF9gBvmQgzugX3A59vSvXt3IUbQgtV4//33E54l26w2bdoopTS5iIQ06dprr5WVK1eqcTCha9eu9VR982O33357wmZjSjBNprz88svy6KOPOo+IpLt06ZJszs57gCLwI3BCihcv7qs506dPlxYtWqi2LBif7WaWMX3t27d3vhPEBxNQEliawreIQzDFsG67d+9WETtaRQCrhX1ZsmRJCk0jvaStlj59+jgW6tZbbxU0nLFxNRzQZcuWKcKGuMIU8yCrtZtXdtAIwNJCTvzpp5/6+it7g7w0q3PnzjJmzBhnbNKre++9NzDANMQn/fzzz84BJF+H1nQT06dVrlw5RT6t+4QFGLDYaHytVgRyc1JFNyGIQ3EARcubb76p/LIpcA0mwwahdOLECaXBbpE/ffH/DRs2VFZBC3sEkaQD0BRMFjkn0a4W0iQ+AAmBCYdzxvyh6UzUTI94T8DlZoI44WiVFtrBZoWRqlWrKl5cC2AXKVLEdQgCENIyBD/2/PPPu7YLCzBahrZpwSqNHTvWdxmTJ09WgZAWNz7cBpi2cPyYaT/BMmHNdOpJ288++0yqVaumurly0W+99ZbaFPjiIIIJxGRhhr2iU3jkefPmOcOR/7LQMALFaAZvsF0QJbZgtrA+WrBCNWrUOCsAQ8hs3rzZGYvYhQ32k+PHjysW8M8//1TNIIN27tyZwDG4AQyLh/VJJgRwBKFaUEjtzz2rSaQUOP53333Xd3y0Fr/lZaJ056Dg+H2sQYMG8uGHHzpNvv32W2X+bBk5cqT06NFDPfaiJ3WfMBqMxdCpC/0x1bB2QYR2mF2IFyJpuyhjA0yKRWAZRGwLYWYxKQDmtGGm4XD52xROHrSl/Zw25cuXV+G86cPNvuSTkPpa8E1m1B5kITaVSqBh5t96DHLDTz75RP2T+ZiHwv5OGIAxl2bcgNsxc/Mga/BqYwNMREzhJoiQJ1Mz0IKLJMhDEgAmEKCooCNpGtAR/pjnRHAIAGNiCS4gQ3D2WgAZzbclLTSYHJc0xhR8ESSA9kl+QQr9wgAMaQJ5ooVId/DgwUEwSNrGBpg4yPyW3wAQKxdeeGFCEyhn8uMEgG0TSPqAJqO5XgJZgcaYuSR1ZJ6ZYpL+PE+ND8aPkmJo2bhxY4oiBd+mqKDFjZ405xUGYDuVGTZsmFOFSopgkgY2wMyrbdu2gYcFTJ3z0wllhU1zAIY7BgQtmD6isWR8L+1pR66mhYTezs9SQzXaq4MCxe9qIVAhWjbFJAx4Rxs/CQMw5AwXD7TAPOlSZGAkPBraAIcti1K4gcLUQgyFxXUAJp+CT9WC3/Lyp25ztDffDoAIegh+tJAyNWvWLNS+cI1HWwqsCgsicDEFCnT9+vXq0QMPPCBkBGcL4A4dOsi4ceOc4WCzYO3OhtgAE9wmC1zN70Ku6Cid5/hgfLEDMOp85MgR1YcIj8YwR0HFrvNCasC+aLE1Jax5I+lnPpoaJF3BRJvCqTWLFwRAms3yWkcYDSZ1ZN5agrBeQffPBpiDyQENKlhakzbFL6PVCmDbSROkmPRakI+wWIIOLXYxAG7VpC/DFDMYk4AKflwLhRHzcgLP2ZSHHnrIOaRe9KS5njAA22uEnaOAcDbEBjhZcGh+E8U0L0AANkEWiqoAhnA3qxyou3ltJ8gCqO1yZ0qLHWFyiEj20USEwv+WLVuCDK3asJEmZeemPVzl0Xn71VdfnZANnA0NNq/fMl6yFCzw4kTUtSiTquQmKMRREKFmYJJG5t46JhoKUm8+g0JHmlWVZB8i4oO61OJWTCC6NW9qejFRbt8ieacsqcXuSwRJ9Yh5I2wYhyCZhNHgX375RYoWLeoMiaWD7Ut2yZAOFB4AQgv+1eQBbICpnZu0rN867Fo+8ZRmDR2AOfHmfeGwxQCbwnO7vUh5kOu4WoKaOMCESNECPckzUwjqCPS0+NGTZr8wANPPXie3XsybMF5AmLVp2lAQKFGihNPcBhgzS0zhxuvb38AtmcGkefvUAZjrqGbSDrGP3wySJtm5Z86cOdWtCTvCxexzajXHzXtqziYLY08ezUR7KUFqof6sfa1+Zl4Hgp7kmo/9fbfNt2vZya4RYUbNa7Wkk8zN7z44xJFJqboFiG5cNJmHWUJ0mz+xElbFTJHM3N8BmMoLNyHNUJsoGN/nBzJ+lBxYX4dhEhwW0x+bE8N0E3FrgdslB+f2gi0wZJAt77zzjvOKzSENss0i1ROoSySMb8Rnm9dw4QLwtV6CC8CCcIBNS0RQ5LZPpCsQNGbE75Y/u2kw4xFIYnLdhKi5SZMmCRU9+7pUApNlBzIMysmjMI8WaaqSojsMFtwspTKdXtGexaOVaLGb0BeTBjmiBf9v3osm6OPKCuaTO0haqP3ih01TzDsCOOIFfSM0zGUCW7vYVA4n1S9ybQh/on8zCIWE0LdD9NzwmQRGUKfQhigMlo1c2cxImDucvH1AbYCpF2N2aQfBwo0ZLB3zO336tCrL9uvXL4HZYy7sj3nRMGk92ASJj5GLmubAfE/0RjnPDES8zAqbFrQSwxgUOdA2/SMyc1w7ukXDTZ/tqY4iarMo9/kxXm7BoG2q/b6h33HliIPt5ldtgNFc4iDzuhD7TzqEAjBvWygScWXZFNdyIZUYAiA7kPFaBL4On4i/ILkOIhwSfEyQe1nUdql3etWPzR+XBaEn7flxaQELYnK5ZhuvaB8AoEb1LRO/daPxmGYibzexAWZOWBGsJ/UAv7tfWMBnn31W4B5s8f11Ieo+f/58ZXKJ+iCw8YuYIHJaNpwyIGxR0Guo9gQwgYT5mDO0iKswmHdoSSJ7xqaS5ZeKmD8uw7SZ6VqQw0Ybon5ufRCNE4dgkjkszAH2Sv9sxx6PqhWuCmoXc8/8NfHAvPS1Wfs+tj2ODbCZxeAOCQbRfnBgfhwULCXxBqSR148H/hO/Dw4K8v+znQ0wv7q0L/alZn4xwKnZtTToEwOcBpuanoaMAU5PaKTBXGKA02BT09OQMcDpCY00mEsMcBpsanoaMgY4PaGRBnOJAU6DTU1PQ8YApyc0zqG5xETHOQRWaqYaA5yaXTuH+gT739KdQwuKp5q4AzHAET8RMcAxwBHfgYgvL9bgGOCI70DElxdrcAxwxHcg4suLNTgGOOI7EPHlxRocAxzxHYj48mINjgGO+A5EfHmxBscAR3wHIr68WINjgCO+AxFfXqzBMcAR34GILy/W4BjgiO9AxJf3P7qGQ/OM5AeAAAAAAElFTkSuQmCC"/></switch></g><path d="M 258.5 303 L 298.25 303 L 331.63 303" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 336.88 303 L 329.88 306.5 L 331.63 303 L 329.88 299.5 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 294px; margin-left: 295px;"><div data-drawio-colors="color: rgb(0, 0, 0); background-color: rgb(255, 255, 255); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 11px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; background-color: rgb(255, 255, 255); white-space: nowrap;">3306/tcp</div></div></div></foreignObject><image x="274" y="288" width="42" height="15.75" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAKgAAAA/CAYAAAB0KINPAAAAAXNSR0IArs4c6QAAEqtJREFUeF7tnQWQHFUQhjtYgkNwJwnu7i4hwUJwd3d3DwkQXIJbILgmuLu7u7u7S6jvUT3V2zfyZm+5ut2arqLgdt88/V/33zJLl5EjR46USqod6KQ70KUCaCc9mWpaYQcqgFZA6NQ7UAG0Ux9PNbkKoBUGOvUOVADt1MdTTa4CaIWBTr0DFUA79fFUk6sA2iQYuOGGG6R///7/hV66dJHPPvtMJp100iaZff3TzAXoCy+8IJdffrk8/vjj8sYbb8i3334rf/31l4w//vgy5ZRTynzzzScrrLCCrLnmmtKtW7fSs/jll1/k6quvlrvuukuefvpp+fzzz+WHH36QscYaSyaYYAKZddZZZeGFF5YNNthAZpllltL988BPP/0kN910k1x33XXyyiuvhIPls8kmmyysYfnll5f1119f5phjjrr614eee+45ueSSS+T++++Xt956K4wx9thjhzHmn39+WXvttWW11VaTUUcdta5xdtxxRznzzDPDs/PMM488++yzdfXTbA+lAvT111+XnXbaSe6+++6o9XTv3l2OOeYY2XrrrcPtLpK///5bTjvtNDn88MPl+++/L2oevl9llVVkyJAhMt1000W1pxHgZx1ffvll4TMbbrihnH766TLhhBMWtrUNvvrqKwE8jFUkXIILL7xQFlhggaKmbb7v1auXvPPOO+HzfffdV4499tjSfTTjA20Aeuutt8paa60lv/76a+n1bLbZZnLBBRfIKKOMkvks/dI/45QVtCraFo1UJPvss48cf/zxRc1qvp922mnlkUcekammmirqOQCDBn7vvfei2tMI63DLLbfI0ksvHf3M22+/LTPMMEPSnj1gXCuDBg2Sgw46KHz06KOPyiKLLBLdf2duWANQzOxiiy0mf/zxRzJnTDeAWm655WSaaaYJ4MNMcpCXXXaZfPfddzXrO+qoo5KNSls4pu7aa6+t+Wq22WYLZnauueYK9OHnn3+WV199VYYPHy4PPfRQTdtJJplE0PB5mu6ss86SHXbYoeY5+t58882FsRjjk08+kdtvvz2sAXOsAm158MEHA5DyBM3PRQE8KvTLJV122WVl8sknl08//VQee+wxYT5QFxXWAN2YeOKJo7BxxhlnBEugAP/m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"/></switch></g><rect x="162.5" y="283" width="96" height="40" rx="6" ry="6" fill="rgb(255, 255, 255)" stroke="rgb(0, 0, 0)" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 94px; height: 1px; padding-top: 303px; margin-left: 163px;"><div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">Data Service<br />(Spring AMQP)</div></div></div></foreignObject><image x="163" y="289" width="94" height="32" xlink:href="data:image/png;base64,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"/></switch></g><path d="M 210.5 263 L 210.5 276.63" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 210.5 281.88 L 207 274.88 L 210.5 276.63 L 214 274.88 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/><rect x="162.5" y="223" width="96" height="40" rx="6" ry="6" fill="#e6e6e6" stroke="rgb(0, 0, 0)" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 94px; height: 1px; padding-top: 243px; margin-left: 163px;"><div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">Broker Service<br style="border-color: var(--border-color);" />(RabbitMQ)</div></div></div></foreignObject><image x="163" y="229" width="94" height="32" xlink:href="data:image/png;base64,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"/></switch></g><path d="M 175 363 L 293 363 L 293 303 L 331.63 303" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 336.88 303 L 329.88 306.5 L 331.63 303 L 329.88 299.5 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/><rect x="79" y="343" width="96" height="40" rx="6" ry="6" fill="rgb(255, 255, 255)" stroke="rgb(0, 0, 0)" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 94px; height: 1px; padding-top: 363px; margin-left: 80px;"><div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">Metadata Service<br />(Spring AMQP)</div></div></div></foreignObject><image x="80" y="349" width="94" height="32" xlink:href="data:image/png;base64,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"/></switch></g><path d="M 48 297 L 48 363 L 72.63 363" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 77.88 363 L 70.88 366.5 L 72.63 363 L 70.88 359.5 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/><rect x="0" y="257" width="96" height="40" rx="6" ry="6" fill="#e6e6e6" stroke="rgb(0, 0, 0)" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 94px; height: 1px; padding-top: 277px; margin-left: 1px;"><div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">Storage Service<br />(SeaweedFS)</div></div></div></foreignObject><image x="1" y="263" width="94" height="32" xlink:href="data:image/png;base64,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"/></switch></g><path d="M 338 279.6 C 338 274.85 349.19 271 363 271 C 369.63 271 375.99 271.91 380.68 273.52 C 385.37 275.13 388 277.32 388 279.6 L 388 326.4 C 388 331.15 376.81 335 363 335 C 349.19 335 338 331.15 338 326.4 Z" fill="#dae8fc" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/><path d="M 388 279.6 C 388 284.35 376.81 288.2 363 288.2 C 349.19 288.2 338 284.35 338 279.6" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/><rect x="322" y="333" width="85" height="20" fill="none" stroke="none" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 89px; height: 1px; padding-top: 343px; margin-left: 320px;"><div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">data-db</div></div></div></foreignObject><image x="320" y="336.5" width="89" height="17" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAWQAAABECAYAAAC2wE+iAAAAAXNSR0IArs4c6QAADoFJREFUeF7tnXXQLTkRxc/i7g4Fi7u7Lla4O4W7e+Gui3vh7u5W2OK6QCG7sLgWsLi7zI9KtrJhJLl3Mnfeq9P/vHrfzXR6TmbOJJ3uzj6yGAEjYASMwCoQ2GcVVtgII2AEjIARkAnZD4ERMAJGYCUImJBXMhA2wwgYASNgQvYzYASMgBFYCQIm5JUMhM0wAkbACJiQ/QwYASNgBFaCgAl5JQNhM4yAETACJmQ/A0bACBiBlSBgQl7JQMxkxqUlHZDo+puko82k22raIfCRbtz2S9Q/sBu3x/d0dyRJ/8j+fkFJB7YzzZqXRMCEvCTa7fsyIbfHuEUPJuQWqO6BOk3Ie+CgjZhsQt4zx9OEvGeO2+xWm5Bnh3SnCtdMyDxrD5F0REm/lfT0nSK1rs5NyOsaj51ZY0LeGfRNOl4zIZ9N0kHhrn8gad8mCOyZSk3Ie+a4zW61CXl2SHeqcM2EfBtJLzIh9z4fJuSdvjbr6dyEvJ6xmMOSNRPyiyXd2oRsQp7jQd9bdZiQ966RXTMhHyzprCZkE/Le9crNezcm5Hnx3LW2tRLy8ST9Wjqs3Kt9yId/Uuyy2PWbs5L+TcgrGYgBM04o6YaSriXpjJJOFtr9ImyQvUfSKyT9Lvx9G0Im+uGSkq4u6Tyhv+NKOqakP4XIiG9I+pyk10pixjsml5f0gQp4n9H1cc+R9nPbV2FaddNLdfjcQBL/nryLKOGDBIbfk/QpSa+R9MlE6zaEfIFuzL4QdB2/6+MWkq4k6RySTiTp35J+Luk7kt4l6XWSflZ9R75gEQRMyIvAvFEnNwuhYSeYuJoQsrtJelXI9uLljlKaqXe1jnif1L2sZ6mwlI/B7SX9ZOCaOQm5hX0Vt1rcFPJ9nqRrFFwBfreSdGjIruRjGqUmU+98kr4k6ZaS+KgdZ6LvP3TZfg+W9KwCG91kYQRMyAsDXtgdM8WnFbaNze4eXsyPVxIyscGPruwrNv+lpEtIOqTn+rkIuZV9G97y4GUnDrNeVjKl8k1JF+t866+XdLkNCflcki67QVz34wIxl9rqdgsgYEJeAOTKLngxWeqnY8Oy89WS3tLNhL4f9J1CEqTHEpVZ9L8k3Sd7MadmyCyrIYNU/iLpDZ3uT0j6kaS/hlkXG3K0P3/WnqXw2SXRVypHCXbxt2uGmWP8/ceSqMGQCkt6Zm+ptLSvclgmm380uCjSht/qXBbPl/TZ4EPHpYArATfUZUJD3AhHlnTFDQn5JsFthUvnnx3Bvz88P6xc/iPptJKuLemiPXdwPUlvnrwzN1gMARPyYlAXdXSEMMtl1hMForpyN4NKZ76pMmZm+HQh8t90y1Ze+ihjhAwJQBinSdp/N+iJpN9n9P0kPSH7gRk9y+UhuVGwMf5esqm3pH1FgzPSCGJ7Y/Y7HzpcEnzg+gRSxv/PhysftxqXBX5pSPfr3Urlxh35fnmgP2x5YciUjE2+3X18SdjJCxZti4ev3xABE/KGwDW67Kph4yVVj2/w5RP9HUvS53t8wGOEDMnjx0yFWRozrCl5d7e5d5WkETPAi8xMyEvaN3W/U7+DPZtrUdj8PG9YXYxdmybLpO1qCJnr2KRjxcHKY0zwHT8ma3AdSW+dukH/vgwCJuRlcC7t5SVhVhXbM/vBJ4k7YkpYluLSSGWMkJmh3UnSSSQxy+ZZ4F+WuVOSuxKwj82kPw9cuMkMeUn7pu537HdWGPmKggSYlxYoBfMvhqiWbQj5jsE1MtXlMbrknB92G3pE70RhM5gNZMsKEDAhr2AQEhMIT4Igo1CA516FJuJD5Pr0ZZvyIaeqeRZKyJhrztSzkXe6ENbVZ+4mhJzraWlfIcS9zfioPSf5BX8/4Wa4IUqkbwO3ZoaMS4sxz334Q32/LOw7xN95ZmI4ZYm9btMQARNyQ3ArVUPEvByp1C4n8SVDflFqCLnGXAiHWOhU2OxjtteKkFvaV6M7bwsZQ8pRvtK5AM5dofDM3Zjh4kilhpDfETZNS7vEBZbP3iF0EncsO0bAhLzjAUi6z5M6+Omckr5WYeJDJT0qad+KkEl0yGeAYydXzDFDroDhf4kYNfbV6M7b5kkdRC2wyVcqrGxw9bC5F6WGkB/ZXfSI0s66CIyLhwia9BJCF9NElQp1bjonAibkOdHcTtdNJb0yU4FfNg8FG+uFECh8glFqCBlfKAkYRHiw8060xrEl4XfMnxP+nyestCbklvZtM3JEKpw+UfDUEH5Yo5NolzNsSMg8N4RElsqpQjhj2v66PfsPpfrcbkYETMgzgrmlqtwXiTrC4Er9urQnQ+ztlYRMmjQkwgx9m+ehFSG3to9UYkiqRIhiSF1CXJP7/WtnrOjA1UNURpSaGXJpZEzUTTo82Z2pEMtOCJ5lxwhs8wLu2PS9rvv7drPRJyZ3RULG0SvvkljkD1YQ8u3ChhSHZ24rLQh5CfvyGe4YDiTBpDNZ2rKpxioiygN64rSnsCXGHLfBJoRM/RGSeEqF+O6/Z43vkm1Mlupyu5kRMCHPDOgW6u6fnTRc426I3eaxu2M6iCN+Z5iFp2Z/OLg9mLUxI8Rlkr/AtT7aTXzIS9m3LSGT+JGe7D00ux17NIjjvtCGhHzhUPCp9NHrO7ma1Rk1OCw7RsCEvOMBSLq/a0/BFzZ8CKMqlTxjbIiQeSnJ7Epne7Ql06skSaA1IS9p37aETD2PNNSQTdWHlw5YaMfGLennm8yQScFOC0pNdc2+RKwOGNvW+qGn+vDvGyJgQt4QuAaX9YUj4e/7fUVfuR96iJBz1wZd3EHSCwr7OmlPCcc5XRZL21d4273NSApJ088pCnXvSoW5H7rGh0ydEELfSuWUPRl9lHdN9x5KdbndzAiYkGcGdAt1fanCRDx8tUIniST3SNoPETKVvnjpoxBTzMZW7poY6ppl8meyH+ck5KXtq4D4/5rm7gayJYlaKJW+TbYaQiZxqOYEb6rL5SFuaU3lUrvdrgECJuQGoG6oklOYSZVOpbYa18dCkfmoY4iQ2VFP02XxG6flH6duoa8k5pyEvLR9U/c79nue+Yb7gfjxUqF05oeyxjWEzMGxbH6Wys2z2ihE8VALZSjtvVSv282AgAl5BhBnUsFYsIHGCR1RKCJOneMSIW6YmS5+5ylCxk/MMjUKJ1gQw1wi7NLjdz111nhOQl7avpL7HmqTR8dAcNQE+VWh0qf0uDhqCJkSqflYjHWdHjZLu77IkULT3WxuBEzIcyO6nb63ZWmwvGwkHZSUR3xQV1f3sVn3QzPkfFb33qx629hdEJoHCeVCBhjHE/VJHmUxRSJL27fNqBEnzYkdqZQW+yHxhlVRuimInhpCpj2rG1Y5U3LUrngVpU/ZA4jyzMzNNaXDvzdEwITcENwNVOfLSVRwPNOzJ3RREY6z7oh+SGWIkImV3T9pyK47Be+nlq1UeSORgvaEeqXhXmObS/hU35T0Rz8sk4eSXpa2b4OhOtwl1JGmJnEUSJZ6FlNZlowrMcC51BLygSElemoPgP2F3N+8X3fiCK4uywoQMCGvYBASE0gw4FgfdsKj8JLhTkgJLbWa+FVIEkJg+Zmm8Q4RMps41PBNZSw6ADcF9RIgCp4ZIjLYTErP4ONMPorX9wmhWfkM7gojh6Aubd+2T0Ef0XHqC2GEfa4LZqqsZjjhBYHQqZYXZYiQqXeRV3WLxe1x81CEPg9pizrJ6COSgr6jsDHbd5LItnj4+g0RMCFvCFzDy/rC3+iOI4J4oVhy4icmKoLThTnGiRRriJsZLG6PKLg60qI1qdm4F/KXEf3PDdXHeDbwTTKDor4vm44IIVb4nzkhI40mIEGCsDuyxogcSCu/sSTHv50+b2S4kYxAHQdiYynpmW5OLWnftsPJB+ugULs61UWMMh9LPn4QJzjg4gC3mK5NHQpWCsQCR6GQPJEmuUCmZHCmcttwEgjYUqiewwwO6D6aPw3PBUR//XBsVIo/NazJ8vv0tjfv6+dDwIQ8H5ZzasKvh6uiRjgBmpM80lOgedEh6z6hgBBujnQTcao/Unw51YSl+NCHAx2QLS6JVDidhNC+IYEg0hTupe2buvep3ymjyceI0qSlcnA45JTVRfoxIrEkrdoX9TFWf8yUc9I1H8x8/2DKBp4XjnSyrAgBE/KKBiMxhXFh+f+wrE5Cn7UsUe/czTCJlGCTKE8kYYY8tCnIsUuUi8R/PCYcnkndX3y78Yw49DLzS8//izr6CBk/N7PeIcLKCRldS9o3x5PAQbCEoRHrOyV8oCjqwyw6j7QYOhGaCnu5C4QPH3izOnlywfNyaIjcyQ+3nbLXvy+AgAl5AZC36IJlLX5IZqUsPQmnYsbL8p+EkfeFJWpavQsfY+qmmCo+TgEjfI+U3mQjipeePihYTuF0fL+UBe07+BTdVDejyhynTjBzxh/KTL2vRi8ZbSzH8Wcys8NWlvL4zSH3NFklwrakfVsM1WGX8k5Rh4PDBXAJEdGACwfSZLMPFwEfz/TQ2jxCZijcERcSLqsouUuKQw5YueCfZ4XB+PChIxPwkODOgohLTzOZAw/rqEDAhFwBlpsaASNgBFoiYEJuia51GwEjYAQqEDAhV4DlpkbACBiBlgiYkFuia91GwAgYgQoETMgVYLmpETACRqAlAibkluhatxEwAkagAgETcgVYbmoEjIARaImACbklutZtBIyAEahAwIRcAZabGgEjYARaImBCbomudRsBI2AEKhAwIVeA5aZGwAgYgZYImJBbomvdRsAIGIEKBEzIFWC5qREwAkagJQIm5JboWrcRMAJGoAIBE3IFWG5qBIyAEWiJgAm5JbrWbQSMgBGoQMCEXAGWmxoBI2AEWiJgQm6JrnUbASNgBCoQMCFXgOWmRsAIGIGWCJiQW6Jr3UbACBiBCgRMyBVguakRMAJGoCUCJuSW6Fq3ETACRqACARNyBVhuagSMgBFoiYAJuSW61m0EjIARqEDgvxIi9WMJqqzcAAAAAElFTkSuQmCC"/></switch></g><rect x="210.5" y="403" width="30" height="16" fill="#e6e6e6" stroke="rgb(0, 0, 0)" pointer-events="all"/><rect x="248" y="403" width="140" height="16" fill="none" stroke="none" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe flex-start; width: 138px; height: 1px; padding-top: 411px; margin-left: 250px;"><div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: left;"><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">External images</div></div></div></foreignObject><image x="250" y="404.5" width="138" height="17" xlink:href="data:image/png;base64,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"/></switch></g><rect x="210.5" y="423" width="30" height="16" fill="rgb(255, 255, 255)" stroke="rgb(0, 0, 0)" pointer-events="all"/><rect x="248" y="423" width="140" height="16" fill="none" stroke="none" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe flex-start; width: 138px; height: 1px; padding-top: 431px; margin-left: 250px;"><div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: left;"><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">Maintained images</div></div></div></foreignObject><image x="250" y="424.5" width="138" height="17" xlink:href="data:image/png;base64,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"/></switch></g><rect x="225.5" y="403" width="15" height="16" fill="#dae8fc" stroke="#000000" pointer-events="all"/></g></svg> \ No newline at end of file diff --git a/.docs/stylesheets/extra.css b/.docs/stylesheets/extra.css index 3275909e35faa14a0971ae45755452b10e216ab4..6d045c82da403138bbce2764c87965ecd912c76a 100644 --- a/.docs/stylesheets/extra.css +++ b/.docs/stylesheets/extra.css @@ -9,14 +9,14 @@ figure img.img-border { border: 1px solid #b3b3b3; } -.md-main .md-content a:not(.action-button), -.md-main .md-content a:not(.action-button) { +.md-main .md-content a:not(.action-button):not([tabindex]), +.md-main .md-content a:not(.action-button):not([tabindex]) { color: var(--md-typeset-color); border-bottom: 2px solid var(--md-primary-fg-color); } -.md-main .md-content a:not(.action-button):focus, -.md-main .md-content a:not(.action-button):hover { +.md-main .md-content a:not(.action-button):not([tabindex]):focus, +.md-main .md-content a:not(.action-button):not([tabindex]):hover { color: var(--md-typeset-color); border-bottom: 2px solid var(--md-primary-fg-color--dark); } diff --git a/.docs/system-databases-auth.md b/.docs/system-databases-authentication.md similarity index 96% rename from .docs/system-databases-auth.md rename to .docs/system-databases-authentication.md index 90ba31bcf55c6e53ef2471f45911c02a69d425fa..42b729403f03dab8cae8a0d31ea9fbcf06e819f2 100644 --- a/.docs/system-databases-auth.md +++ b/.docs/system-databases-authentication.md @@ -2,7 +2,7 @@ author: Martin Weise --- -# Auth Database +# Authentication Database ## tl;dr diff --git a/.docs/system-databases-data.md b/.docs/system-databases-data.md index 7c31d5e967951b287104a29d0978f6f3b8577202..e290bc1342373ea784d25c6ef2da6eb840abd8bd 100644 --- a/.docs/system-databases-data.md +++ b/.docs/system-databases-data.md @@ -25,13 +25,15 @@ author: Martin Weise By default, only one Data Database is deployed. You can deploy multiple (different) Data Database instances and make them available in the repository as follows: -```console -curl \ - -sSL \ - http://<hostname>/api/container \ - -X POST \ - -d '{"name": "Data Database 2", "imageId": 1, "host": "example.com", "port": 3306, "privilegedUsername": "root", "privilegedPassword": "s3cr3t" }' -``` +=== "Terminal" + + ```shell + curl \ + -sSL \ + http://<hostname>/api/container \ + -X POST \ + -d '{"name": "Data Database 2", "imageId": 1, "host": "example.com", "port": 3306, "privilegedUsername": "root", "privilegedPassword": "s3cr3t" }' + ``` ### Settings @@ -71,23 +73,27 @@ natively. Export all databases with `--skip-lock-tables` option for MariaDB Galera clusters as it is not supported currently by MariaDB Galera. -```console -mariadb \ - -u <privilegedUsername> \ - -p<privilegedPassword> \ - --complete-insert \ - --skip-lock-tables \ - --skip-add-locks \ - --all-databases > dump.sql -``` +=== "Terminal" + + ```shell + mariadb \ + -u <privilegedUsername> \ + -p<privilegedPassword> \ + --complete-insert \ + --skip-lock-tables \ + --skip-add-locks \ + --all-databases > dump.sql + ``` ### Restore -```console -mariadb \ - -u <privilegedUsername> \ - -p<privilegedPassword> < dump.sql -``` +=== "Terminal" + + ```shell + mariadb \ + -u <privilegedUsername> \ + -p<privilegedPassword> < dump.sql + ``` ## Limitations diff --git a/.docs/system-other-search-dashboard.md b/.docs/system-other-search-dashboard.md index a3d0e9ae12c590a226eb94afcc3c8034e4376eca..dd23d56c6784a6765f2b9b403632a209ac045932 100644 --- a/.docs/system-other-search-dashboard.md +++ b/.docs/system-other-search-dashboard.md @@ -27,6 +27,12 @@ the [Search Database](../system-databases-search). (none) +!!! question "Do you miss functionality? Do these limitations affect you?" + + We strongly encourage you to help us implement it as we are welcoming contributors to open-source software and get + in [contact](../contact) with us, we happily answer requests for collaboration with attached CV and your programming + experience! + ## Security (none) diff --git a/.docs/system-other-ui.md b/.docs/system-other-ui.md index 955120bb3e1ed87978ae58bea95f6f51362c78c6..54e10689388063e1f8f2b9926acc5a4705a369d0 100644 --- a/.docs/system-other-ui.md +++ b/.docs/system-other-ui.md @@ -45,17 +45,16 @@ configured as well via the `dbrepo.config.json` values file. The important links ```json title="dbrepo.config.json" { - "title": "Database Repository", - "version": "__APPVERSION___DOCKER-COMPOSE", - "logo": { - "path": "/my_logo.png" - }, - "page": { + "title": "Database Repository", + "version": "__APPVERSION___DOCKER-COMPOSE", + "logo": { + "path": "/my_logo.png" + }, + "page": { "information": { "links": [] - } - }, - ... + }, + ... } ``` @@ -95,20 +94,20 @@ The response looks like this: ```json { - "fieldname": "file", - "originalname": "gps.csv", - "encoding": "7bit", - "mimetype": "text/csv", - "buffer": { - "type": "Buffer", - "data": [ - 34, - 73, - ... - ] - }, - "size": 130279, - "etag": "9d23e73f4ed9f7e5afc80e696db69ebb" + "fieldname": "file", + "originalname": "gps.csv", + "encoding": "7bit", + "mimetype": "text/csv", + "buffer": { + "type": "Buffer", + "data": [ + 34, + 73, + ... + ] + }, + "size": 130279, + "etag": "9d23e73f4ed9f7e5afc80e696db69ebb" } ``` @@ -116,6 +115,12 @@ The response looks like this: (none) +!!! question "Do you miss functionality? Do these limitations affect you?" + + We strongly encourage you to help us implement it as we are welcoming contributors to open-source software and get + in [contact](../contact) with us, we happily answer requests for collaboration with attached CV and your programming + experience! + ## Security (none) diff --git a/.docs/system-services-authentication.md b/.docs/system-services-authentication.md index 0f1d32f3291292f489f10042ac280f47debe2499..5f6d982a0e5c0194b0da9d2e33e4e2b3194f122c 100644 --- a/.docs/system-services-authentication.md +++ b/.docs/system-services-authentication.md @@ -11,19 +11,13 @@ author: Martin Weise Image: [`dbrepo/authentication-service:__APPVERSION__`](https://hub.docker.com/r/dbrepo/authentication-service) * Ports: 8080/tcp - * Health: `http://<hostname>:8080/api/auth/health` - * Prometheus: `http://<hostname>:8080/api/auth/metrics` * UI: `http://<hostname>/api/auth/admin/` ## Overview -From version 1.2 onwards we use Keycloak for authentication for managing a part of the user identity and deprecated the -Spring Boot application instead. Going forward, the authentication will be -through [Keycloak by RedHat](https://quay.io/repository/keycloak/keycloak?tab=info). - -By default, users are created using the [UI](../system-other-ui) and the sign-up page in the UI. A new user is also -created in the UI creates a new user in the [Auth Database](../system-databases-auth), consequently a part of the -user identity is managed by Keycloak. +By default, users are created using the [User Interface](../system-other-ui) and the sign-up page in the User Interface. +This creates a new user in the [Authentication Database](../system-databases-authentication), the user identity is then managed by the +Authentication Service. ## Groups @@ -203,6 +197,7 @@ public ResponseEntity<DatabaseBriefDto> create(@NotNull Long containerId, * No support for sending e-mails through Keycloak by default. * No support for temporary passwords. +* No support for adding identifies in Keycloak directly. * No support for multi-factor authentication. !!! question "Do you miss functionality? Do these limitations affect you?" diff --git a/.docs/system-services-gateway.md b/.docs/system-services-gateway.md index aa3c24db64c67c4c553b3857947b8a38cc72b603..9950110c71c6a86088e3e530f59a5d5bca6935f7 100644 --- a/.docs/system-services-gateway.md +++ b/.docs/system-services-gateway.md @@ -18,13 +18,16 @@ Provides a single point of access to the *application programming interface* (AP standard [NGINX](https://www.nginx.com/) reverse proxy for load balancing. This component is optional if you already have a load balancer or reverse proxy running. -### Settings +## Settings + +### SSL/TLS Security To setup SSL/TLS encryption, mount your TLS certificate and TLS private key into the container directly into the `/etc/nginx/` directory. ```yaml title="docker-compose.yml" services: + ... dbrepo-gateway-service: image: docker.io/nginx:1.25-alpine-slim ports: @@ -38,10 +41,32 @@ services: If your TLS private key as a password, you need to specify it in the `dbrepo.conf` file. +### User Interface + +To serve the [User Interface](../system-other-ui/) under different port than `80`, change the port mapping in +the `docker-compose.yml` to e.g. port `8000`: + +```yaml title="docker-compose.yml" +services: + ... + dbrepo-gateway-service: + image: docker.io/nginx:1.25-alpine-slim + ports: + - "8000:80" + ... +``` + ## Limitations (none relevant to DBRepo) +!!! question "Do you miss functionality? Do these limitations affect you?" + + We strongly encourage you to help us implement it as we are welcoming contributors to open-source software and get + in [contact](../contact) with us, we happily answer requests for collaboration with attached CV and your programming + experience! + + ## Security 1. Enable TLS encryption by downloading diff --git a/.docs/system-services-search.md b/.docs/system-services-search.md index 85bba96e9f38cbb5265fc7c1d2207d809d08ce44..59dcf2081323e574ec54aff43552c949eab8008a 100644 --- a/.docs/system-services-search.md +++ b/.docs/system-services-search.md @@ -29,7 +29,7 @@ that holds all the metadata information which is mirrored from the [Metadata Dat <figure markdown>  -<figcaption>Statistical properties in Metadata Database and Search Database</figcaption> +<figcaption>Figure 1: Statistical properties in Metadata Database and Search Database</figcaption> </figure> ## Faceted Browsing @@ -54,7 +54,7 @@ the units of measurements can be transformed. <figure markdown>  -<figcaption>Two tables with compatible semantic concepts and units of measurement</figcaption> +<figcaption>Figure 2: Two tables with compatible semantic concepts and units of measurement</figcaption> </figure> In short, the search service transforms the statistical properties not in the target unit of measurements is transformed @@ -66,12 +66,12 @@ between 32 - 50 °F"* instead. <figure markdown>  -<figcaption>Unit independent search query transformation</figcaption> +<figcaption>Figure 3: Unit independent search query transformation</figcaption> </figure> ## Examples -TBD +View [usage examples](../usage-search/). ## Limitations diff --git a/.docs/system.md b/.docs/system.md index c1cc7725842803ad95154238dab2526a455c3d1e..12b6553820e14feae108f51e4ce0649d81227e66 100644 --- a/.docs/system.md +++ b/.docs/system.md @@ -6,14 +6,22 @@ author: Martin Weise !!! abstract "Abstract" - This is the full system description from a technical/developer view. + This is the full system description from a technical/developer view and continously being updated as the development + progresses. -We invite all open-source developers to help us fixing bugs and introducing features to the source code. Get involved by -sending a mail to Prof. Andreas Rauber and Projektass. Martin Weise. - -## Data Ingest +## Usage <figure markdown>  -<figcaption>Modes of data ingest</figcaption> +<figcaption>Figure 1: Modes of data ingest</figcaption> </figure> + +More [usage examples](../usage-overview/) include how to ingest datasets, data dumps, live data, etc. + +## Limitations + +!!! question "Do you miss functionality? Do these limitations affect you?" + + We strongly encourage you to help us implement it as we are welcoming contributors to open-source software and get + in [contact](../contact) with us, we happily answer requests for collaboration with attached CV and your programming + experience! \ No newline at end of file diff --git a/.docs/usage-analyse.md b/.docs/usage-analyse.md index 92e4b5c15417357e21847c7d5ae61e6ac70fe781..8e3234cc0c1229bc66434ea69838f293228fa0a0 100644 --- a/.docs/usage-analyse.md +++ b/.docs/usage-analyse.md @@ -10,7 +10,7 @@ containing GPS-data `gps.csv` already uploaded in the `dbrepo-upload` bucket of ```shell curl -X POST \ -d '{"filename":"gps.csv","separator":","}' - http://<hostname>:5000/api/analyse/determinedt + http://<hostname>/api/analyse/determinedt ``` This results in the response: diff --git a/.docs/usage-auth.md b/.docs/usage-authentication.md similarity index 51% rename from .docs/usage-auth.md rename to .docs/usage-authentication.md index 6b1967ca0544699a671db371855091046ba7e922..21a27cb9379e0f413c3a26e15d703f24fa53d584 100644 --- a/.docs/usage-auth.md +++ b/.docs/usage-authentication.md @@ -10,7 +10,7 @@ Access tokens are needed for almost all operations. === "Terminal" - ``` console + ```shell curl -X POST \ -d "username=foo&password=bar&grant_type=password&client_id=dbrepo-client&scope=openid&client_secret=MUwRc7yfXSJwX8AdRMWaQC3Nep1VjwgG" \ http://localhost/api/auth/realms/dbrepo/protocol/openid-connect/token @@ -18,18 +18,18 @@ Access tokens are needed for almost all operations. === "Python" - ``` py - import requests - - auth = requests.post("http://localhost/api/auth/realms/dbrepo/protocol/openid-connect/token", data={ - "username": "foo", - "password": "bar", - "grant_type": "password", - "client_id": "dbrepo-client", - "scope": "openid", - "client_secret": "MUwRc7yfXSJwX8AdRMWaQC3Nep1VjwgG" - }) - print(auth.json()["access_token"]) + ```python + from keycloak import KeycloakOpenID + + # Configure client + openid = KeycloakOpenID(server_url="http://<hostname>/api/auth", + realm_name="dbrepo", client_id="dbrepo-client", + client_secret_key="MUwRc7yfXSJwX8AdRMWaQC3Nep1VjwgG") + + # Get Token + token = openid.token("username", "password") + access_token = token['access_token'] + refresh_token = token['refresh_token'] ``` ## Refresh Access Token @@ -38,7 +38,7 @@ Using the response from above, a new access token can be created via the refresh === "Terminal" - ``` console + ```shell curl -X POST \ -d "grant_type=refresh_token&client_id=dbrepo-client&refresh_token=THE_REFRESH_TOKEN&client_secret=MUwRc7yfXSJwX8AdRMWaQC3Nep1VjwgG" \ http://localhost/api/auth/realms/dbrepo/protocol/openid-connect/token @@ -46,15 +46,15 @@ Using the response from above, a new access token can be created via the refresh === "Python" - ``` py - import requests + ```python + from keycloak import KeycloakOpenID + + # Configure client + openid = KeycloakOpenID(server_url="http://<hostname>/api/auth", + realm_name="dbrepo", client_id="dbrepo-client", + client_secret_key="MUwRc7yfXSJwX8AdRMWaQC3Nep1VjwgG") - auth = requests.post("http://localhost/api/auth/realms/dbrepo/protocol/openid-connect/token", data={ - "grant_type": "refresh_token", - "client_id": "dbrepo-client", - "client_secret": "MUwRc7yfXSJwX8AdRMWaQC3Nep1VjwgG", - "refresh_token": "THE_REFRESH_TOKEN" - }) - print(auth.json()["access_token"]) + # Get Token + token = keycloak_openid.refresh_token(refresh_token) ``` diff --git a/.docs/usage-broker.md b/.docs/usage-broker.md index 501b9dcb73d29e39638c492aa127b462c9c64ce4..67f063be623d808bf72be50201a9554cf588f5f3 100644 --- a/.docs/usage-broker.md +++ b/.docs/usage-broker.md @@ -4,42 +4,49 @@ author: Martin Weise # Broker Service -## Authentication +## Preliminary -The RabbitMQ client can be authenticated through plain (username, password) and OAuth2 mechanisms. Note that the access -token already contains a field `client_id=foo`, so the username is optional in `PlainCredentials()`. +The RabbitMQ client can be authenticated through Basic Authentication (username, password) and Bearer Authentication. -=== "Plain" +!!! example "Bearer Authentication" - ``` py - import pika + Note that the encoded/signed `ACCESS_TOKEN` already contains a field `client_id=username`, so the username is + optional in `PlainCredentials` when using Bearer Authentication, but provided must match the username. + +=== "Bearer Authentication" - credentials = pika.credentials.PlainCredentials("foo", "bar") + ```python + import pika + + # Configure client + credentials = pika.credentials.PlainCredentials("", "ACCESS_TOKEN") parameters = pika.ConnectionParameters('localhost', 5672, '/', credentials) connection = pika.BlockingConnection(parameters) + + # Channel channel = connection.channel() - channel.queue_declare(queue='test', durable=True) - channel.basic_publish(exchange='', - routing_key='test', - body=b'Hello World!') + channel.basic_publish(exchange='dbrepo', + routing_key='dbrepo.database_name.table_name', + body=b'Hello World!') print(" [x] Sent 'Hello World!'") connection.close() ``` -=== "OAuth2" +=== "Basic Authentication" - ``` py + ```python import pika - credentials = pika.credentials.PlainCredentials("", "THE_ACCESS_TOKEN") + # Configure client + credentials = pika.credentials.PlainCredentials("username", "password") parameters = pika.ConnectionParameters('localhost', 5672, '/', credentials) connection = pika.BlockingConnection(parameters) + + # Channel channel = connection.channel() - channel.queue_declare(queue='test', durable=True) - channel.basic_publish(exchange='', - routing_key='test', - body=b'Hello World!') + channel.basic_publish(exchange='dbrepo', + routing_key='dbrepo.database_name.table_name', + body=b'Hello World!') print(" [x] Sent 'Hello World!'") connection.close() ``` - diff --git a/.docs/usage-metadata.md b/.docs/usage-metadata.md new file mode 100644 index 0000000000000000000000000000000000000000..690e07f4b07f4d4726693a9fd86ff5d3f37b39af --- /dev/null +++ b/.docs/usage-metadata.md @@ -0,0 +1,23 @@ +--- +author: Martin Weise +--- + +# Metadata Service + +## Preliminary + +<!-- md:version 1.4.1 --> + +!!! example "Basic Authentication" + + The use of **Basic Authentication** (username, password) instead of *Bearer Authentication* may be useful for + applications that do not have the technical capability of refreshing tokens in intervals (e.g. single-threaded + applications). It is however not recommended for any other applications as **Basic Authentication** transmits the + user password with every request. + + Additionally, performance is decreased as with every **Basic Authentication** request, an additional request is + sent to the [Authentication Service](../system-services-authentication/) where the authorization is requested before + authentication to the Metadata Service. This performance degradation should be avoided whenever possible. Use + **Bearer Authentication** instead, see how to + [obtain an access token](../usage-authentication/#obtain-access-token). + diff --git a/.docs/usage-overview.md b/.docs/usage-overview.md index 075de96375e6a1a65e70659bd4d5773f53ae1e31..f62ca1967ca23e0e4f6877e89e13e1117783d3d0 100644 --- a/.docs/usage-overview.md +++ b/.docs/usage-overview.md @@ -637,7 +637,7 @@ A user wants to import live data from e.g. sensor measurements fast and without Beware that access tokens are short lived (default is 15 minutes) and need to be refreshed regularly with refresh tokens (default is 10 days). See the usage page - on [how to refresh access tokens](../usage-auth/#refresh-access-token). + on [how to refresh access tokens](../usage-authentication/#refresh-access-token). Add a data tuple to an already existing table where the user has at least `write-own` access. @@ -699,35 +699,35 @@ A user wants to create a subset and export it as csv file. Login and select a database where you have at least `read` access (this is the case for e.g. self-created databases). Click the ":material-wrench: CREATE SUBSET" button :material-numeric-1-circle-outline: as seen in - Figure 16. + Figure 17. <figure markdown> { .img-border } - <figcaption>Figure 16: Open the create subset form.</figcaption> + <figcaption>Figure 17: Open the create subset form.</figcaption> </figure> A subset can be created by using our query builder that is visible by default in the "SIMPLE" tab. First, a source table :material-numeric-1-circle-outline: needs to be selected, then the columns that are part of the subset in :material-numeric-2-circle-outline:. Optionally the subset can be filtered. The subset query (=SQL) is displayed - in :material-numeric-3-circle-outline: in Figure 17. + in :material-numeric-3-circle-outline: in Figure 18. Once you are confident the query covers the desired result, click ":material-run: Create". <figure markdown> { .img-border } - <figcaption>Figure 17: Subset query building.</figcaption> + <figcaption>Figure 18: Subset query building.</figcaption> </figure> Once the subset is created (may take some seconds), the user is presented with the result set in :material-numeric-1-circle-outline:, more information on the subset can be obtained by clicking ":material-run: - View" on the top (c.f. Figure 18). + View" on the top (c.f. Figure 19). <figure markdown> { .img-border } - <figcaption>Figure 18: Subset result set.</figcaption> + <figcaption>Figure 19: Subset result set.</figcaption> </figure> - The subset information page in Figure 19 shows the most important metadata like subset query hash and result hash + The subset information page in Figure 20 shows the most important metadata like subset query hash and result hash (e.g. for reproducability) and subset result count. Note that although this subset is stored in the query store already, it is only temporarly stored there for 24 hours (default configuration). @@ -737,7 +737,7 @@ A user wants to create a subset and export it as csv file. <figure markdown> { .img-border } - <figcaption>Figure 19: Subset information.</figcaption> + <figcaption>Figure 20: Subset information.</figcaption> </figure> === "Terminal" diff --git a/.docs/usage-search.md b/.docs/usage-search.md new file mode 100644 index 0000000000000000000000000000000000000000..c21ae72c32bfc569fc29441cb11661f6461f5cf1 --- /dev/null +++ b/.docs/usage-search.md @@ -0,0 +1,9 @@ +--- +author: Martin Weise +--- + +# Search Service + +The Search Service connects to the [Search Database](../system-databases-search/). + +!!! note "This section will be expanded" \ No newline at end of file diff --git a/.docs/usage-storage.md b/.docs/usage-storage.md index d9fc967f28e8814619d5fa3396c4203b0e3997ab..253fe8e960814e2a91d723dd44c620726df6b7e9 100644 --- a/.docs/usage-storage.md +++ b/.docs/usage-storage.md @@ -4,9 +4,11 @@ author: Martin Weise # Storage Service +## Preliminary + Configure the credentials to access the S3 endpoint: -```console +```shell $ aws configure \ --endpoint-url http://localhost:9000 AWS Access Key ID [None]: seaweedfsadmin @@ -15,9 +17,11 @@ Default region name [None]: Default output format [None]: ``` +## Upload + Upload a CSV-file into the `dbrepo-upload` bucket with the AWS CLI: -```console +```shell $ aws --endpoint-url http://localhost:9000 \ s3 \ cp /path/to/file.csv \ @@ -25,9 +29,11 @@ $ aws --endpoint-url http://localhost:9000 \ upload: /path/to/file.csv to s3://dbrepo-upload/file.csv ``` +## List + You can list the buckets: -```console +```shell $ aws --endpoint-url http://localhost:9000 \ s3 \ ls @@ -37,7 +43,7 @@ $ aws --endpoint-url http://localhost:9000 \ And list the files in the bucket `dbrepo-upload` with: -```console +```shell $ aws --endpoint-url http://localhost:9000 \ s3 \ ls \ @@ -45,6 +51,8 @@ $ aws --endpoint-url http://localhost:9000 \ 2023-12-03 16:28:05 535219 file.csv ``` +## Other + Alternatively, you can use the middleware of the [User Interface](../system-other-ui/) to upload files. Alternatively, you can use a S3-compatible client: diff --git a/.docs/usage-upload.md b/.docs/usage-upload.md index e48bc364c1c41fc1141afd0cc2dd4ff09696c4cd..3e78928ec90f53428f6ca1293da2894c7dae2e90 100644 --- a/.docs/usage-upload.md +++ b/.docs/usage-upload.md @@ -19,8 +19,8 @@ You can also upload a file `file.csv` in 200 byte chunks with Python: === "Python" ```python - #!/bin/env python3 from tusclient import client + my_client = client.TusClient('http://localhost/api/upload/files') uploader = my_client.uploader('/path/to/file.csv', chunk_size=200) uploader.upload() diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index f76b8f298cc1f20d9694249a19fe6b23aef0a734..84dafc88bfad06d716e956f47289f287d6af5865 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -4,8 +4,8 @@ variables: TRIVY_CACHE_DIR: ".trivycache/" DOCKER_HOST: "unix:///var/run/dind/docker.sock" TESTCONTAINERS_RYUK_DISABLED: "false" - APP_VERSION: "1.4.0" - CHART_VERSION: "1.4.0" + APP_VERSION: "1.4.1" + CHART_VERSION: "1.4.1" image: debian:12-slim @@ -591,14 +591,14 @@ build-api-1.4: docs-registry: stage: docs - image: docker.io/python:3.9-slim + image: docker.io/python:3.11-slim only: refs: - master - release-v1.3 - release-v1.4 script: - - pip install -r ./requirements.txt + - pip install pipenv && pipenv install --dev --system --deploy - python3 .docs/docker/release.py cache: paths: @@ -624,7 +624,7 @@ docs-latest: script: - apt-get update && apt-get install -y git make sed - git fetch && git checkout master - - pip install -r ./requirements.txt + - pip install pipenv && pipenv install --dev --system --deploy - mkdir -p ./final - sed -i -e "s/__APPVERSION__/${APP_VERSION}/g" .docs/redirect.html - cp ./.docs/redirect.html ./final/index.html @@ -671,7 +671,7 @@ docs-1.3: docs-1.4: stage: docs - image: docker.io/python:3.9-slim + image: docker.io/python:3.11-slim needs: - build-api-latest - build-api-1.3 @@ -684,7 +684,7 @@ docs-1.4: script: - apt-get update && apt-get install -y git make sed wget - git fetch && git checkout release-v1.4 - - pip install -r ./.docs/requirements.txt + - pip install pipenv && pipenv install --dev --system --deploy - wget https://gitlab.phaidra.org/fair-data-austria-db-repository/fda-services/-/raw/dev/.docs/overrides/main.html -O .docs/overrides/main.html -q - mkdir -p ./final - find .docs/ -type f -exec sed -i -e "s/__APPVERSION__/${APP_VERSION}/g" {} \; diff --git a/Pipfile b/Pipfile index 0757494bb360a3a87f122f43d490716830b47915..9307a131066c87c88b03783fc0181d3570e762fa 100644 --- a/Pipfile +++ b/Pipfile @@ -4,6 +4,13 @@ verify_ssl = true name = "pypi" [packages] +mkdocs = "1.5.3" +mkdocs-material = "9.5.5" +mkdocs-with-pdf = "0.9.3" +mkdocs-material-extensions = "*" +requests = "*" +py-dotenv = "*" +python-dotenv = "*" [dev-packages] diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000000000000000000000000000000000000..ae6ef935a172421420b4f84f7309c70236cc2304 --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,1003 @@ +{ + "_meta": { + "hash": { + "sha256": "f1691729be450945956d143e93645380de19a546125e08a29e5eccbf54d97e1a" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.11" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "babel": { + "hashes": [ + "sha256:6919867db036398ba21eb5c7a0f6b28ab8cbc3ae7a73a44ebe34ae74a4e7d363", + "sha256:efb1a25b7118e67ce3a259bed20545c29cb68be8ad2c784c83689981b7a57287" + ], + "markers": "python_version >= '3.7'", + "version": "==2.14.0" + }, + "beautifulsoup4": { + "hashes": [ + "sha256:74e3d1928edc070d21748185c46e3fb33490f22f52a3addee9aee0f4f7781051", + "sha256:b80878c9f40111313e55da8ba20bdba06d8fa3969fc68304167741bbf9e082ed" + ], + "markers": "python_full_version >= '3.6.0'", + "version": "==4.12.3" + }, + "brotli": { + "hashes": [ + "sha256:03d20af184290887bdea3f0f78c4f737d126c74dc2f3ccadf07e54ceca3bf208", + "sha256:0541e747cce78e24ea12d69176f6a7ddb690e62c425e01d31cc065e69ce55b48", + "sha256:069a121ac97412d1fe506da790b3e69f52254b9df4eb665cd42460c837193354", + "sha256:0b63b949ff929fbc2d6d3ce0e924c9b93c9785d877a21a1b678877ffbbc4423a", + "sha256:0c6244521dda65ea562d5a69b9a26120769b7a9fb3db2fe9545935ed6735b128", + "sha256:11d00ed0a83fa22d29bc6b64ef636c4552ebafcef57154b4ddd132f5638fbd1c", + "sha256:141bd4d93984070e097521ed07e2575b46f817d08f9fa42b16b9b5f27b5ac088", + "sha256:19c116e796420b0cee3da1ccec3b764ed2952ccfcc298b55a10e5610ad7885f9", + "sha256:1ab4fbee0b2d9098c74f3057b2bc055a8bd92ccf02f65944a241b4349229185a", + "sha256:1ae56aca0402a0f9a3431cddda62ad71666ca9d4dc3a10a142b9dce2e3c0cda3", + "sha256:224e57f6eac61cc449f498cc5f0e1725ba2071a3d4f48d5d9dffba42db196438", + "sha256:22fc2a8549ffe699bfba2256ab2ed0421a7b8fadff114a3d201794e45a9ff578", + "sha256:23032ae55523cc7bccb4f6a0bf368cd25ad9bcdcc1990b64a647e7bbcce9cb5b", + "sha256:2333e30a5e00fe0fe55903c8832e08ee9c3b1382aacf4db26664a16528d51b4b", + "sha256:2954c1c23f81c2eaf0b0717d9380bd348578a94161a65b3a2afc62c86467dd68", + "sha256:2de9d02f5bda03d27ede52e8cfe7b865b066fa49258cbab568720aa5be80a47d", + "sha256:30924eb4c57903d5a7526b08ef4a584acc22ab1ffa085faceb521521d2de32dd", + "sha256:316cc9b17edf613ac76b1f1f305d2a748f1b976b033b049a6ecdfd5612c70409", + "sha256:38025d9f30cf4634f8309c6874ef871b841eb3c347e90b0851f63d1ded5212da", + "sha256:39da8adedf6942d76dc3e46653e52df937a3c4d6d18fdc94a7c29d263b1f5b50", + "sha256:3d7954194c36e304e1523f55d7042c59dc53ec20dd4e9ea9d151f1b62b4415c0", + "sha256:4093c631e96fdd49e0377a9c167bfd75b6d0bad2ace734c6eb20b348bc3ea180", + "sha256:43ce1b9935bfa1ede40028054d7f48b5469cd02733a365eec8a329ffd342915d", + "sha256:4d4a848d1837973bf0f4b5e54e3bec977d99be36a7895c61abb659301b02c112", + "sha256:4ed11165dd45ce798d99a136808a794a748d5dc38511303239d4e2363c0695dc", + "sha256:510b5b1bfbe20e1a7b3baf5fed9e9451873559a976c1a78eebaa3b86c57b4265", + "sha256:524f35912131cc2cabb00edfd8d573b07f2d9f21fa824bd3fb19725a9cf06327", + "sha256:587ca6d3cef6e4e868102672d3bd9dc9698c309ba56d41c2b9c85bbb903cdb95", + "sha256:5b3cc074004d968722f51e550b41a27be656ec48f8afaeeb45ebf65b561481dd", + "sha256:5eeb539606f18a0b232d4ba45adccde4125592f3f636a6182b4a8a436548b914", + "sha256:5f4d5ea15c9382135076d2fb28dde923352fe02951e66935a9efaac8f10e81b0", + "sha256:5fb2ce4b8045c78ebbc7b8f3c15062e435d47e7393cc57c25115cfd49883747a", + "sha256:6172447e1b368dcbc458925e5ddaf9113477b0ed542df258d84fa28fc45ceea7", + "sha256:6c3020404e0b5eefd7c9485ccf8393cfb75ec38ce75586e046573c9dc29967a0", + "sha256:70051525001750221daa10907c77830bc889cb6d865cc0b813d9db7fefc21451", + "sha256:7905193081db9bfa73b1219140b3d315831cbff0d8941f22da695832f0dd188f", + "sha256:7c4855522edb2e6ae7fdb58e07c3ba9111e7621a8956f481c68d5d979c93032e", + "sha256:7e4c4629ddad63006efa0ef968c8e4751c5868ff0b1c5c40f76524e894c50248", + "sha256:7f4bf76817c14aa98cc6697ac02f3972cb8c3da93e9ef16b9c66573a68014f91", + "sha256:81de08ac11bcb85841e440c13611c00b67d3bf82698314928d0b676362546724", + "sha256:861bf317735688269936f755fa136a99d1ed526883859f86e41a5d43c61d8966", + "sha256:890b5a14ce214389b2cc36ce82f3093f96f4cc730c1cffdbefff77a7c71f2a97", + "sha256:89f4988c7203739d48c6f806f1e87a1d96e0806d44f0fba61dba81392c9e474d", + "sha256:8dadd1314583ec0bf2d1379f7008ad627cd6336625d6679cf2f8e67081b83acf", + "sha256:901032ff242d479a0efa956d853d16875d42157f98951c0230f69e69f9c09bac", + "sha256:906bc3a79de8c4ae5b86d3d75a8b77e44404b0f4261714306e3ad248d8ab0951", + "sha256:919e32f147ae93a09fe064d77d5ebf4e35502a8df75c29fb05788528e330fe74", + "sha256:929811df5462e182b13920da56c6e0284af407d1de637d8e536c5cd00a7daf60", + "sha256:949f3b7c29912693cee0afcf09acd6ebc04c57af949d9bf77d6101ebb61e388c", + "sha256:a090ca607cbb6a34b0391776f0cb48062081f5f60ddcce5d11838e67a01928d1", + "sha256:a1fd8a29719ccce974d523580987b7f8229aeace506952fa9ce1d53a033873c8", + "sha256:a37b8f0391212d29b3a91a799c8e4a2855e0576911cdfb2515487e30e322253d", + "sha256:a3daabb76a78f829cafc365531c972016e4aa8d5b4bf60660ad8ecee19df7ccc", + "sha256:a469274ad18dc0e4d316eefa616d1d0c2ff9da369af19fa6f3daa4f09671fd61", + "sha256:a599669fd7c47233438a56936988a2478685e74854088ef5293802123b5b2460", + "sha256:a743e5a28af5f70f9c080380a5f908d4d21d40e8f0e0c8901604d15cfa9ba751", + "sha256:a77def80806c421b4b0af06f45d65a136e7ac0bdca3c09d9e2ea4e515367c7e9", + "sha256:aac0411d20e345dc0920bdec5548e438e999ff68d77564d5e9463a7ca9d3e7b1", + "sha256:ae15b066e5ad21366600ebec29a7ccbc86812ed267e4b28e860b8ca16a2bc474", + "sha256:be36e3d172dc816333f33520154d708a2657ea63762ec16b62ece02ab5e4daf2", + "sha256:c8146669223164fc87a7e3de9f81e9423c67a79d6b3447994dfb9c95da16e2d6", + "sha256:c8fd5270e906eef71d4a8d19b7c6a43760c6abcfcc10c9101d14eb2357418de9", + "sha256:caf9ee9a5775f3111642d33b86237b05808dafcd6268faa492250e9b78046eb2", + "sha256:cdad5b9014d83ca68c25d2e9444e28e967ef16e80f6b436918c700c117a85467", + "sha256:cdbc1fc1bc0bff1cef838eafe581b55bfbffaed4ed0318b724d0b71d4d377619", + "sha256:ceb64bbc6eac5a140ca649003756940f8d6a7c444a68af170b3187623b43bebf", + "sha256:d0c5516f0aed654134a2fc936325cc2e642f8a0e096d075209672eb321cff408", + "sha256:d143fd47fad1db3d7c27a1b1d66162e855b5d50a89666af46e1679c496e8e579", + "sha256:d192f0f30804e55db0d0e0a35d83a9fead0e9a359a9ed0285dbacea60cc10a84", + "sha256:db85ecf4e609a48f4b29055f1e144231b90edc90af7481aa731ba2d059226b1b", + "sha256:de6551e370ef19f8de1807d0a9aa2cdfdce2e85ce88b122fe9f6b2b076837e59", + "sha256:e1140c64812cb9b06c922e77f1c26a75ec5e3f0fb2bf92cc8c58720dec276752", + "sha256:e6a904cb26bfefc2f0a6f240bdf5233be78cd2488900a2f846f3c3ac8489ab80", + "sha256:e84799f09591700a4154154cab9787452925578841a94321d5ee8fb9a9a328f0", + "sha256:e93dfc1a1165e385cc8239fab7c036fb2cd8093728cbd85097b284d7b99249a2", + "sha256:efa8b278894b14d6da122a72fefcebc28445f2d3f880ac59d46c90f4c13be9a3", + "sha256:f0d8a7a6b5983c2496e364b969f0e526647a06b075d034f3297dc66f3b360c64", + "sha256:f296c40e23065d0d6650c4aefe7470d2a25fffda489bcc3eb66083f3ac9f6643", + "sha256:f66b5337fa213f1da0d9000bc8dc0cb5b896b726eefd9c6046f699b169c41b9e", + "sha256:f733d788519c7e3e71f0855c96618720f5d3d60c3cb829d8bbb722dddce37985", + "sha256:fce1473f3ccc4187f75b4690cfc922628aed4d3dd013d047f95a9b3919a86596", + "sha256:fd5f17ff8f14003595ab414e45fce13d073e0762394f957182e69035c9f3d7c2", + "sha256:fdc3ff3bfccdc6b9cc7c342c03aa2400683f0cb891d46e94b64a197910dc4064" + ], + "version": "==1.1.0" + }, + "certifi": { + "hashes": [ + "sha256:9b469f3a900bf28dc19b8cfbf8019bf47f7fdd1a65a1d4ffb98fc14166beb4d1", + "sha256:e036ab49d5b79556f99cfc2d9320b34cfbe5be05c5871b51de9329f0603b0474" + ], + "markers": "python_version >= '3.6'", + "version": "==2023.11.17" + }, + "cffi": { + "hashes": [ + "sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc", + "sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a", + "sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417", + "sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab", + "sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520", + "sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36", + "sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743", + "sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8", + "sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed", + "sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684", + "sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56", + "sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324", + "sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d", + "sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235", + "sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e", + "sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088", + "sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000", + "sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7", + "sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e", + "sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673", + "sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c", + "sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe", + "sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2", + "sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098", + "sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8", + "sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a", + "sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0", + "sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b", + "sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896", + "sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e", + "sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9", + "sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2", + "sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b", + "sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6", + "sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404", + "sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f", + "sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0", + "sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4", + "sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc", + "sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936", + "sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba", + "sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872", + "sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb", + "sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614", + "sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1", + "sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d", + "sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969", + "sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b", + "sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4", + "sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627", + "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956", + "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357" + ], + "markers": "python_version >= '3.8'", + "version": "==1.16.0" + }, + "charset-normalizer": { + "hashes": [ + "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027", + "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087", + "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786", + "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8", + "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09", + "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185", + "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574", + "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e", + "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519", + "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898", + "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269", + "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3", + "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f", + "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6", + "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8", + "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a", + "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73", + "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc", + "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714", + "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2", + "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc", + "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce", + "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d", + "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e", + "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6", + "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269", + "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96", + "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d", + "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a", + "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4", + "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77", + "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d", + "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0", + "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed", + "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068", + "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac", + "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25", + "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8", + "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab", + "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26", + "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2", + "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db", + "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f", + "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5", + "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99", + "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c", + "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d", + "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811", + "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa", + "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a", + "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03", + "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b", + "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04", + "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c", + "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001", + "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458", + "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389", + "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99", + "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985", + "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537", + "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238", + "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f", + "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d", + "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796", + "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a", + "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143", + "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8", + "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c", + "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5", + "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5", + "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711", + "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4", + "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6", + "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c", + "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7", + "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4", + "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b", + "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae", + "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12", + "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c", + "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae", + "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8", + "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887", + "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b", + "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4", + "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f", + "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5", + "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33", + "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519", + "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561" + ], + "markers": "python_full_version >= '3.7.0'", + "version": "==3.3.2" + }, + "click": { + "hashes": [ + "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", + "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de" + ], + "markers": "python_version >= '3.7'", + "version": "==8.1.7" + }, + "colorama": { + "hashes": [ + "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", + "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6'", + "version": "==0.4.6" + }, + "cssselect2": { + "hashes": [ + "sha256:1ccd984dab89fc68955043aca4e1b03e0cf29cad9880f6e28e3ba7a74b14aa5a", + "sha256:fd23a65bfd444595913f02fc71f6b286c29261e354c41d722ca7a261a49b5969" + ], + "markers": "python_version >= '3.7'", + "version": "==0.7.0" + }, + "fonttools": { + "extras": [ + "woff" + ], + "hashes": [ + "sha256:0255dbc128fee75fb9be364806b940ed450dd6838672a150d501ee86523ac61e", + "sha256:0a00bd0e68e88987dcc047ea31c26d40a3c61185153b03457956a87e39d43c37", + "sha256:0a1d313a415eaaba2b35d6cd33536560deeebd2ed758b9bfb89ab5d97dc5deac", + "sha256:0f750037e02beb8b3569fbff701a572e62a685d2a0e840d75816592280e5feae", + "sha256:13819db8445a0cec8c3ff5f243af6418ab19175072a9a92f6cc8ca7d1452754b", + "sha256:254d9a6f7be00212bf0c3159e0a420eb19c63793b2c05e049eb337f3023c5ecc", + "sha256:29495d6d109cdbabe73cfb6f419ce67080c3ef9ea1e08d5750240fd4b0c4763b", + "sha256:32ab2e9702dff0dd4510c7bb958f265a8d3dd5c0e2547e7b5f7a3df4979abb07", + "sha256:3480eeb52770ff75140fe7d9a2ec33fb67b07efea0ab5129c7e0c6a639c40c70", + "sha256:3a808f3c1d1df1f5bf39be869b6e0c263570cdafb5bdb2df66087733f566ea71", + "sha256:3b629108351d25512d4ea1a8393a2dba325b7b7d7308116b605ea3f8e1be88df", + "sha256:3d71606c9321f6701642bd4746f99b6089e53d7e9817fc6b964e90d9c5f0ecc6", + "sha256:3e2b95dce2ead58fb12524d0ca7d63a63459dd489e7e5838c3cd53557f8933e1", + "sha256:4a5a5318ba5365d992666ac4fe35365f93004109d18858a3e18ae46f67907670", + "sha256:4c811d3c73b6abac275babb8aa439206288f56fdb2c6f8835e3d7b70de8937a7", + "sha256:4e743935139aa485fe3253fc33fe467eab6ea42583fa681223ea3f1a93dd01e6", + "sha256:4ec558c543609e71b2275c4894e93493f65d2f41c15fe1d089080c1d0bb4d635", + "sha256:5465df494f20a7d01712b072ae3ee9ad2887004701b95cb2cc6dcb9c2c97a899", + "sha256:5b60e3afa9635e3dfd3ace2757039593e3bd3cf128be0ddb7a1ff4ac45fa5a50", + "sha256:63fbed184979f09a65aa9c88b395ca539c94287ba3a364517698462e13e457c9", + "sha256:69731e8bea0578b3c28fdb43dbf95b9386e2d49a399e9a4ad736b8e479b08085", + "sha256:6dd58cc03016b281bd2c74c84cdaa6bd3ce54c5a7f47478b7657b930ac3ed8eb", + "sha256:740947906590a878a4bde7dd748e85fefa4d470a268b964748403b3ab2aeed6c", + "sha256:7df26dd3650e98ca45f1e29883c96a0b9f5bb6af8d632a6a108bc744fa0bd9b3", + "sha256:7eb7ad665258fba68fd22228a09f347469d95a97fb88198e133595947a20a184", + "sha256:7ee48bd9d6b7e8f66866c9090807e3a4a56cf43ffad48962725a190e0dd774c8", + "sha256:86e0427864c6c91cf77f16d1fb9bf1bbf7453e824589e8fb8461b6ee1144f506", + "sha256:8f57ecd742545362a0f7186774b2d1c53423ed9ece67689c93a1055b236f638c", + "sha256:90f898cdd67f52f18049250a6474185ef6544c91f27a7bee70d87d77a8daf89c", + "sha256:94208ea750e3f96e267f394d5588579bb64cc628e321dbb1d4243ffbc291b18b", + "sha256:a1c154bb85dc9a4cf145250c88d112d88eb414bad81d4cb524d06258dea1bdc0", + "sha256:a5d77479fb885ef38a16a253a2f4096bc3d14e63a56d6246bfdb56365a12b20c", + "sha256:a86a5ab2873ed2575d0fcdf1828143cfc6b977ac448e3dc616bb1e3d20efbafa", + "sha256:ac71e2e201df041a2891067dc36256755b1229ae167edbdc419b16da78732c2f", + "sha256:b3e1304e5f19ca861d86a72218ecce68f391646d85c851742d265787f55457a4", + "sha256:b8be28c036b9f186e8c7eaf8a11b42373e7e4949f9e9f370202b9da4c4c3f56c", + "sha256:c19044256c44fe299d9a73456aabee4b4d06c6b930287be93b533b4737d70aa1", + "sha256:d49ce3ea7b7173faebc5664872243b40cf88814ca3eb135c4a3cdff66af71946", + "sha256:e040f905d542362e07e72e03612a6270c33d38281fd573160e1003e43718d68d", + "sha256:eabae77a07c41ae0b35184894202305c3ad211a93b2eb53837c2a1143c8bc952", + "sha256:f791446ff297fd5f1e2247c188de53c1bfb9dd7f0549eba55b73a3c2087a2703", + "sha256:f83a4daef6d2a202acb9bf572958f91cfde5b10c8ee7fb1d09a4c81e5d851fd8" + ], + "markers": "python_version >= '3.8'", + "version": "==4.47.2" + }, + "ghp-import": { + "hashes": [ + "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619", + "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343" + ], + "version": "==2.1.0" + }, + "html5lib": { + "hashes": [ + "sha256:0d78f8fde1c230e99fe37986a60526d7049ed4bf8a9fadbad5f00e22e58e041d", + "sha256:b2e5b40261e20f354d198eae92afc10d750afb487ed5e50f9c4eaf07c184146f" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==1.1" + }, + "idna": { + "hashes": [ + "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca", + "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f" + ], + "markers": "python_version >= '3.5'", + "version": "==3.6" + }, + "jinja2": { + "hashes": [ + "sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa", + "sha256:ac8bd6544d4bb2c9792bf3a159e80bba8fda7f07e81bc3aed565432d5925ba90" + ], + "markers": "python_version >= '3.7'", + "version": "==3.1.3" + }, + "libsass": { + "hashes": [ + "sha256:31e86d92a5c7a551df844b72d83fc2b5e50abc6fbbb31e296f7bebd6489ed1b4", + "sha256:34cae047cbbfc4ffa832a61cbb110f3c95f5471c6170c842d3fed161e40814dc", + "sha256:4a218406d605f325d234e4678bd57126a66a88841cb95bee2caeafdc6f138306", + "sha256:6f209955ede26684e76912caf329f4ccb57e4a043fd77fe0e7348dd9574f1880", + "sha256:a2ec85d819f353cbe807432d7275d653710d12b08ec7ef61c124a580a8352f3c", + "sha256:ea97d1b45cdc2fc3590cb9d7b60f1d8915d3ce17a98c1f2d4dd47ee0d9c68ce6" + ], + "markers": "python_version >= '3.8'", + "version": "==0.23.0" + }, + "markdown": { + "hashes": [ + "sha256:d43323865d89fc0cb9b20c75fc8ad313af307cc087e84b657d9eec768eddeadd", + "sha256:e1ac7b3dc550ee80e602e71c1d168002f062e49f1b11e26a36264dafd4df2ef8" + ], + "markers": "python_version >= '3.8'", + "version": "==3.5.2" + }, + "markupsafe": { + "hashes": [ + "sha256:0042d6a9880b38e1dd9ff83146cc3c9c18a059b9360ceae207805567aacccc69", + "sha256:0c26f67b3fe27302d3a412b85ef696792c4a2386293c53ba683a89562f9399b0", + "sha256:0fbad3d346df8f9d72622ac71b69565e621ada2ce6572f37c2eae8dacd60385d", + "sha256:15866d7f2dc60cfdde12ebb4e75e41be862348b4728300c36cdf405e258415ec", + "sha256:1c98c33ffe20e9a489145d97070a435ea0679fddaabcafe19982fe9c971987d5", + "sha256:21e7af8091007bf4bebf4521184f4880a6acab8df0df52ef9e513d8e5db23411", + "sha256:23984d1bdae01bee794267424af55eef4dfc038dc5d1272860669b2aa025c9e3", + "sha256:31f57d64c336b8ccb1966d156932f3daa4fee74176b0fdc48ef580be774aae74", + "sha256:3583a3a3ab7958e354dc1d25be74aee6228938312ee875a22330c4dc2e41beb0", + "sha256:36d7626a8cca4d34216875aee5a1d3d654bb3dac201c1c003d182283e3205949", + "sha256:396549cea79e8ca4ba65525470d534e8a41070e6b3500ce2414921099cb73e8d", + "sha256:3a66c36a3864df95e4f62f9167c734b3b1192cb0851b43d7cc08040c074c6279", + "sha256:3aae9af4cac263007fd6309c64c6ab4506dd2b79382d9d19a1994f9240b8db4f", + "sha256:3ab3a886a237f6e9c9f4f7d272067e712cdb4efa774bef494dccad08f39d8ae6", + "sha256:47bb5f0142b8b64ed1399b6b60f700a580335c8e1c57f2f15587bd072012decc", + "sha256:49a3b78a5af63ec10d8604180380c13dcd870aba7928c1fe04e881d5c792dc4e", + "sha256:4df98d4a9cd6a88d6a585852f56f2155c9cdb6aec78361a19f938810aa020954", + "sha256:5045e892cfdaecc5b4c01822f353cf2c8feb88a6ec1c0adef2a2e705eef0f656", + "sha256:5244324676254697fe5c181fc762284e2c5fceeb1c4e3e7f6aca2b6f107e60dc", + "sha256:54635102ba3cf5da26eb6f96c4b8c53af8a9c0d97b64bdcb592596a6255d8518", + "sha256:54a7e1380dfece8847c71bf7e33da5d084e9b889c75eca19100ef98027bd9f56", + "sha256:55d03fea4c4e9fd0ad75dc2e7e2b6757b80c152c032ea1d1de487461d8140efc", + "sha256:698e84142f3f884114ea8cf83e7a67ca8f4ace8454e78fe960646c6c91c63bfa", + "sha256:6aa5e2e7fc9bc042ae82d8b79d795b9a62bd8f15ba1e7594e3db243f158b5565", + "sha256:7653fa39578957bc42e5ebc15cf4361d9e0ee4b702d7d5ec96cdac860953c5b4", + "sha256:765f036a3d00395a326df2835d8f86b637dbaf9832f90f5d196c3b8a7a5080cb", + "sha256:78bc995e004681246e85e28e068111a4c3f35f34e6c62da1471e844ee1446250", + "sha256:7a07f40ef8f0fbc5ef1000d0c78771f4d5ca03b4953fc162749772916b298fc4", + "sha256:8b570a1537367b52396e53325769608f2a687ec9a4363647af1cded8928af959", + "sha256:987d13fe1d23e12a66ca2073b8d2e2a75cec2ecb8eab43ff5624ba0ad42764bc", + "sha256:9896fca4a8eb246defc8b2a7ac77ef7553b638e04fbf170bff78a40fa8a91474", + "sha256:9e9e3c4020aa2dc62d5dd6743a69e399ce3de58320522948af6140ac959ab863", + "sha256:a0b838c37ba596fcbfca71651a104a611543077156cb0a26fe0c475e1f152ee8", + "sha256:a4d176cfdfde84f732c4a53109b293d05883e952bbba68b857ae446fa3119b4f", + "sha256:a76055d5cb1c23485d7ddae533229039b850db711c554a12ea64a0fd8a0129e2", + "sha256:a76cd37d229fc385738bd1ce4cba2a121cf26b53864c1772694ad0ad348e509e", + "sha256:a7cc49ef48a3c7a0005a949f3c04f8baa5409d3f663a1b36f0eba9bfe2a0396e", + "sha256:abf5ebbec056817057bfafc0445916bb688a255a5146f900445d081db08cbabb", + "sha256:b0fe73bac2fed83839dbdbe6da84ae2a31c11cfc1c777a40dbd8ac8a6ed1560f", + "sha256:b6f14a9cd50c3cb100eb94b3273131c80d102e19bb20253ac7bd7336118a673a", + "sha256:b83041cda633871572f0d3c41dddd5582ad7d22f65a72eacd8d3d6d00291df26", + "sha256:b835aba863195269ea358cecc21b400276747cc977492319fd7682b8cd2c253d", + "sha256:bf1196dcc239e608605b716e7b166eb5faf4bc192f8a44b81e85251e62584bd2", + "sha256:c669391319973e49a7c6230c218a1e3044710bc1ce4c8e6eb71f7e6d43a2c131", + "sha256:c7556bafeaa0a50e2fe7dc86e0382dea349ebcad8f010d5a7dc6ba568eaaa789", + "sha256:c8f253a84dbd2c63c19590fa86a032ef3d8cc18923b8049d91bcdeeb2581fbf6", + "sha256:d18b66fe626ac412d96c2ab536306c736c66cf2a31c243a45025156cc190dc8a", + "sha256:d5291d98cd3ad9a562883468c690a2a238c4a6388ab3bd155b0c75dd55ece858", + "sha256:d5c31fe855c77cad679b302aabc42d724ed87c043b1432d457f4976add1c2c3e", + "sha256:d6e427c7378c7f1b2bef6a344c925b8b63623d3321c09a237b7cc0e77dd98ceb", + "sha256:dac1ebf6983148b45b5fa48593950f90ed6d1d26300604f321c74a9ca1609f8e", + "sha256:de8153a7aae3835484ac168a9a9bdaa0c5eee4e0bc595503c95d53b942879c84", + "sha256:e1a0d1924a5013d4f294087e00024ad25668234569289650929ab871231668e7", + "sha256:e7902211afd0af05fbadcc9a312e4cf10f27b779cf1323e78d52377ae4b72bea", + "sha256:e888ff76ceb39601c59e219f281466c6d7e66bd375b4ec1ce83bcdc68306796b", + "sha256:f06e5a9e99b7df44640767842f414ed5d7bedaaa78cd817ce04bbd6fd86e2dd6", + "sha256:f6be2d708a9d0e9b0054856f07ac7070fbe1754be40ca8525d5adccdbda8f475", + "sha256:f9917691f410a2e0897d1ef99619fd3f7dd503647c8ff2475bf90c3cf222ad74", + "sha256:fc1a75aa8f11b87910ffd98de62b29d6520b6d6e8a3de69a70ca34dea85d2a8a", + "sha256:fe8512ed897d5daf089e5bd010c3dc03bb1bdae00b35588c49b98268d4a01e00" + ], + "markers": "python_version >= '3.7'", + "version": "==2.1.4" + }, + "mergedeep": { + "hashes": [ + "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8", + "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307" + ], + "markers": "python_version >= '3.6'", + "version": "==1.3.4" + }, + "mkdocs": { + "hashes": [ + "sha256:3b3a78e736b31158d64dbb2f8ba29bd46a379d0c6e324c2246c3bc3d2189cfc1", + "sha256:eb7c99214dcb945313ba30426c2451b735992c73c2e10838f76d09e39ff4d0e2" + ], + "index": "pypi", + "version": "==1.5.3" + }, + "mkdocs-material": { + "hashes": [ + "sha256:4480d9580faf42fed0123d0465502bfc1c0c239ecc9c4d66159cf0459ea1b4ae", + "sha256:ac50b2431a79a3b160fdefbba37c9132485f1a69166aba115ad49fafdbbbc5df" + ], + "index": "pypi", + "version": "==9.5.5" + }, + "mkdocs-material-extensions": { + "hashes": [ + "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443", + "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31" + ], + "index": "pypi", + "version": "==1.3.1" + }, + "mkdocs-with-pdf": { + "hashes": [ + "sha256:002d76417b5cc584effdfdb6ec8d073266a308a85680c430562e97f00b886e49", + "sha256:bda3375d7040d1b8871da17c6d71ea736bdca6c669608f28ed62771031d2e0c6" + ], + "index": "pypi", + "version": "==0.9.3" + }, + "packaging": { + "hashes": [ + "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5", + "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7" + ], + "markers": "python_version >= '3.7'", + "version": "==23.2" + }, + "paginate": { + "hashes": [ + "sha256:5e6007b6a9398177a7e1648d04fdd9f8c9766a1a945bceac82f1929e8c78af2d" + ], + "version": "==0.5.6" + }, + "pathspec": { + "hashes": [ + "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", + "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712" + ], + "markers": "python_version >= '3.8'", + "version": "==0.12.1" + }, + "pillow": { + "hashes": [ + "sha256:0304004f8067386b477d20a518b50f3fa658a28d44e4116970abfcd94fac34a8", + "sha256:0689b5a8c5288bc0504d9fcee48f61a6a586b9b98514d7d29b840143d6734f39", + "sha256:0eae2073305f451d8ecacb5474997c08569fb4eb4ac231ffa4ad7d342fdc25ac", + "sha256:0fb3e7fc88a14eacd303e90481ad983fd5b69c761e9e6ef94c983f91025da869", + "sha256:11fa2e5984b949b0dd6d7a94d967743d87c577ff0b83392f17cb3990d0d2fd6e", + "sha256:127cee571038f252a552760076407f9cff79761c3d436a12af6000cd182a9d04", + "sha256:154e939c5f0053a383de4fd3d3da48d9427a7e985f58af8e94d0b3c9fcfcf4f9", + "sha256:15587643b9e5eb26c48e49a7b33659790d28f190fc514a322d55da2fb5c2950e", + "sha256:170aeb00224ab3dc54230c797f8404507240dd868cf52066f66a41b33169bdbe", + "sha256:1b5e1b74d1bd1b78bc3477528919414874748dd363e6272efd5abf7654e68bef", + "sha256:1da3b2703afd040cf65ec97efea81cfba59cdbed9c11d8efc5ab09df9509fc56", + "sha256:1e23412b5c41e58cec602f1135c57dfcf15482013ce6e5f093a86db69646a5aa", + "sha256:2247178effb34a77c11c0e8ac355c7a741ceca0a732b27bf11e747bbc950722f", + "sha256:257d8788df5ca62c980314053197f4d46eefedf4e6175bc9412f14412ec4ea2f", + "sha256:3031709084b6e7852d00479fd1d310b07d0ba82765f973b543c8af5061cf990e", + "sha256:322209c642aabdd6207517e9739c704dc9f9db943015535783239022002f054a", + "sha256:322bdf3c9b556e9ffb18f93462e5f749d3444ce081290352c6070d014c93feb2", + "sha256:33870dc4653c5017bf4c8873e5488d8f8d5f8935e2f1fb9a2208c47cdd66efd2", + "sha256:35bb52c37f256f662abdfa49d2dfa6ce5d93281d323a9af377a120e89a9eafb5", + "sha256:3c31822339516fb3c82d03f30e22b1d038da87ef27b6a78c9549888f8ceda39a", + "sha256:3eedd52442c0a5ff4f887fab0c1c0bb164d8635b32c894bc1faf4c618dd89df2", + "sha256:3ff074fc97dd4e80543a3e91f69d58889baf2002b6be64347ea8cf5533188213", + "sha256:47c0995fc4e7f79b5cfcab1fc437ff2890b770440f7696a3ba065ee0fd496563", + "sha256:49d9ba1ed0ef3e061088cd1e7538a0759aab559e2e0a80a36f9fd9d8c0c21591", + "sha256:51f1a1bffc50e2e9492e87d8e09a17c5eea8409cda8d3f277eb6edc82813c17c", + "sha256:52a50aa3fb3acb9cf7213573ef55d31d6eca37f5709c69e6858fe3bc04a5c2a2", + "sha256:54f1852cd531aa981bc0965b7d609f5f6cc8ce8c41b1139f6ed6b3c54ab82bfb", + "sha256:609448742444d9290fd687940ac0b57fb35e6fd92bdb65386e08e99af60bf757", + "sha256:69ffdd6120a4737710a9eee73e1d2e37db89b620f702754b8f6e62594471dee0", + "sha256:6fad5ff2f13d69b7e74ce5b4ecd12cc0ec530fcee76356cac6742785ff71c452", + "sha256:7049e301399273a0136ff39b84c3678e314f2158f50f517bc50285fb5ec847ad", + "sha256:70c61d4c475835a19b3a5aa42492409878bbca7438554a1f89d20d58a7c75c01", + "sha256:716d30ed977be8b37d3ef185fecb9e5a1d62d110dfbdcd1e2a122ab46fddb03f", + "sha256:753cd8f2086b2b80180d9b3010dd4ed147efc167c90d3bf593fe2af21265e5a5", + "sha256:773efe0603db30c281521a7c0214cad7836c03b8ccff897beae9b47c0b657d61", + "sha256:7823bdd049099efa16e4246bdf15e5a13dbb18a51b68fa06d6c1d4d8b99a796e", + "sha256:7c8f97e8e7a9009bcacbe3766a36175056c12f9a44e6e6f2d5caad06dcfbf03b", + "sha256:823ef7a27cf86df6597fa0671066c1b596f69eba53efa3d1e1cb8b30f3533068", + "sha256:8373c6c251f7ef8bda6675dd6d2b3a0fcc31edf1201266b5cf608b62a37407f9", + "sha256:83b2021f2ade7d1ed556bc50a399127d7fb245e725aa0113ebd05cfe88aaf588", + "sha256:870ea1ada0899fd0b79643990809323b389d4d1d46c192f97342eeb6ee0b8483", + "sha256:8d12251f02d69d8310b046e82572ed486685c38f02176bd08baf216746eb947f", + "sha256:9c23f307202661071d94b5e384e1e1dc7dfb972a28a2310e4ee16103e66ddb67", + "sha256:9d189550615b4948f45252d7f005e53c2040cea1af5b60d6f79491a6e147eef7", + "sha256:a086c2af425c5f62a65e12fbf385f7c9fcb8f107d0849dba5839461a129cf311", + "sha256:a2b56ba36e05f973d450582fb015594aaa78834fefe8dfb8fcd79b93e64ba4c6", + "sha256:aebb6044806f2e16ecc07b2a2637ee1ef67a11840a66752751714a0d924adf72", + "sha256:b1b3020d90c2d8e1dae29cf3ce54f8094f7938460fb5ce8bc5c01450b01fbaf6", + "sha256:b4b6b1e20608493548b1f32bce8cca185bf0480983890403d3b8753e44077129", + "sha256:b6f491cdf80ae540738859d9766783e3b3c8e5bd37f5dfa0b76abdecc5081f13", + "sha256:b792a349405fbc0163190fde0dc7b3fef3c9268292586cf5645598b48e63dc67", + "sha256:b7c2286c23cd350b80d2fc9d424fc797575fb16f854b831d16fd47ceec078f2c", + "sha256:babf5acfede515f176833ed6028754cbcd0d206f7f614ea3447d67c33be12516", + "sha256:c365fd1703040de1ec284b176d6af5abe21b427cb3a5ff68e0759e1e313a5e7e", + "sha256:c4225f5220f46b2fde568c74fca27ae9771536c2e29d7c04f4fb62c83275ac4e", + "sha256:c570f24be1e468e3f0ce7ef56a89a60f0e05b30a3669a459e419c6eac2c35364", + "sha256:c6dafac9e0f2b3c78df97e79af707cdc5ef8e88208d686a4847bab8266870023", + "sha256:c8de2789052ed501dd829e9cae8d3dcce7acb4777ea4a479c14521c942d395b1", + "sha256:cb28c753fd5eb3dd859b4ee95de66cc62af91bcff5db5f2571d32a520baf1f04", + "sha256:cb4c38abeef13c61d6916f264d4845fab99d7b711be96c326b84df9e3e0ff62d", + "sha256:d1b35bcd6c5543b9cb547dee3150c93008f8dd0f1fef78fc0cd2b141c5baf58a", + "sha256:d8e6aeb9201e655354b3ad049cb77d19813ad4ece0df1249d3c793de3774f8c7", + "sha256:d8ecd059fdaf60c1963c58ceb8997b32e9dc1b911f5da5307aab614f1ce5c2fb", + "sha256:da2b52b37dad6d9ec64e653637a096905b258d2fc2b984c41ae7d08b938a67e4", + "sha256:e87f0b2c78157e12d7686b27d63c070fd65d994e8ddae6f328e0dcf4a0cd007e", + "sha256:edca80cbfb2b68d7b56930b84a0e45ae1694aeba0541f798e908a49d66b837f1", + "sha256:f379abd2f1e3dddb2b61bc67977a6b5a0a3f7485538bcc6f39ec76163891ee48", + "sha256:fe4c15f6c9285dc54ce6553a3ce908ed37c8f3825b5a51a15c91442bb955b868" + ], + "markers": "python_version >= '3.8'", + "version": "==10.2.0" + }, + "platformdirs": { + "hashes": [ + "sha256:11c8f37bcca40db96d8144522d925583bdb7a31f7b0e37e3ed4318400a8e2380", + "sha256:906d548203468492d432bcb294d4bc2fff751bf84971fbb2c10918cc206ee420" + ], + "markers": "python_version >= '3.8'", + "version": "==4.1.0" + }, + "py-dotenv": { + "hashes": [ + "sha256:548c588c3b7e2ee2142b0ac97d2912d223ff38e874302426bbb6c21353817cc2" + ], + "index": "pypi", + "version": "==0.1" + }, + "pycparser": { + "hashes": [ + "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9", + "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206" + ], + "version": "==2.21" + }, + "pydyf": { + "hashes": [ + "sha256:901186a2e9f897108139426a6486f5225bdcc9b70be2ec965f25111e42f8ac5d", + "sha256:b22b1ef016141b54941ad66ed4e036a7bdff39c0b360993b283875c3f854dd9a" + ], + "markers": "python_version >= '3.7'", + "version": "==0.8.0" + }, + "pygments": { + "hashes": [ + "sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c", + "sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367" + ], + "markers": "python_version >= '3.7'", + "version": "==2.17.2" + }, + "pymdown-extensions": { + "hashes": [ + "sha256:6ca215bc57bc12bf32b414887a68b810637d039124ed9b2e5bd3325cbb2c050c", + "sha256:c0d64d5cf62566f59e6b2b690a4095c931107c250a8c8e1351c1de5f6b036deb" + ], + "markers": "python_version >= '3.8'", + "version": "==10.7" + }, + "pyphen": { + "hashes": [ + "sha256:414c9355958ca3c6a3ff233f65678c245b8ecb56418fb291e2b93499d61cd510", + "sha256:596c8b3be1c1a70411ba5f6517d9ccfe3083c758ae2b94a45f2707346d8e66fa" + ], + "markers": "python_version >= '3.7'", + "version": "==0.14.0" + }, + "python-dateutil": { + "hashes": [ + "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86", + "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", + "version": "==2.8.2" + }, + "python-dotenv": { + "hashes": [ + "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca", + "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a" + ], + "index": "pypi", + "version": "==1.0.1" + }, + "pyyaml": { + "hashes": [ + "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5", + "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc", + "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df", + "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741", + "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206", + "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27", + "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595", + "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62", + "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98", + "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696", + "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290", + "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9", + "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d", + "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6", + "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867", + "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47", + "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486", + "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6", + "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3", + "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007", + "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938", + "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0", + "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c", + "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735", + "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d", + "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28", + "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4", + "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba", + "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8", + "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef", + "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5", + "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd", + "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3", + "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0", + "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515", + "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c", + "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c", + "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924", + "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34", + "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43", + "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859", + "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673", + "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54", + "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a", + "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b", + "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab", + "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa", + "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c", + "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585", + "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d", + "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f" + ], + "markers": "python_version >= '3.6'", + "version": "==6.0.1" + }, + "pyyaml-env-tag": { + "hashes": [ + "sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb", + "sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069" + ], + "markers": "python_version >= '3.6'", + "version": "==0.1" + }, + "regex": { + "hashes": [ + "sha256:0694219a1d54336fd0445ea382d49d36882415c0134ee1e8332afd1529f0baa5", + "sha256:086dd15e9435b393ae06f96ab69ab2d333f5d65cbe65ca5a3ef0ec9564dfe770", + "sha256:094ba386bb5c01e54e14434d4caabf6583334090865b23ef58e0424a6286d3dc", + "sha256:09da66917262d9481c719599116c7dc0c321ffcec4b1f510c4f8a066f8768105", + "sha256:0ecf44ddf9171cd7566ef1768047f6e66975788258b1c6c6ca78098b95cf9a3d", + "sha256:0fda75704357805eb953a3ee15a2b240694a9a514548cd49b3c5124b4e2ad01b", + "sha256:11a963f8e25ab5c61348d090bf1b07f1953929c13bd2309a0662e9ff680763c9", + "sha256:150c39f5b964e4d7dba46a7962a088fbc91f06e606f023ce57bb347a3b2d4630", + "sha256:1b9d811f72210fa9306aeb88385b8f8bcef0dfbf3873410413c00aa94c56c2b6", + "sha256:1e0eabac536b4cc7f57a5f3d095bfa557860ab912f25965e08fe1545e2ed8b4c", + "sha256:22a86d9fff2009302c440b9d799ef2fe322416d2d58fc124b926aa89365ec482", + "sha256:22f3470f7524b6da61e2020672df2f3063676aff444db1daa283c2ea4ed259d6", + "sha256:263ef5cc10979837f243950637fffb06e8daed7f1ac1e39d5910fd29929e489a", + "sha256:283fc8eed679758de38fe493b7d7d84a198b558942b03f017b1f94dda8efae80", + "sha256:29171aa128da69afdf4bde412d5bedc335f2ca8fcfe4489038577d05f16181e5", + "sha256:298dc6354d414bc921581be85695d18912bea163a8b23cac9a2562bbcd5088b1", + "sha256:2aae8101919e8aa05ecfe6322b278f41ce2994c4a430303c4cd163fef746e04f", + "sha256:2f4e475a80ecbd15896a976aa0b386c5525d0ed34d5c600b6d3ebac0a67c7ddf", + "sha256:34e4af5b27232f68042aa40a91c3b9bb4da0eeb31b7632e0091afc4310afe6cb", + "sha256:37f8e93a81fc5e5bd8db7e10e62dc64261bcd88f8d7e6640aaebe9bc180d9ce2", + "sha256:3a17d3ede18f9cedcbe23d2daa8a2cd6f59fe2bf082c567e43083bba3fb00347", + "sha256:3b1de218d5375cd6ac4b5493e0b9f3df2be331e86520f23382f216c137913d20", + "sha256:43f7cd5754d02a56ae4ebb91b33461dc67be8e3e0153f593c509e21d219c5060", + "sha256:4558410b7a5607a645e9804a3e9dd509af12fb72b9825b13791a37cd417d73a5", + "sha256:4719bb05094d7d8563a450cf8738d2e1061420f79cfcc1fa7f0a44744c4d8f73", + "sha256:4bfc2b16e3ba8850e0e262467275dd4d62f0d045e0e9eda2bc65078c0110a11f", + "sha256:518440c991f514331f4850a63560321f833979d145d7d81186dbe2f19e27ae3d", + "sha256:51f4b32f793812714fd5307222a7f77e739b9bc566dc94a18126aba3b92b98a3", + "sha256:531ac6cf22b53e0696f8e1d56ce2396311254eb806111ddd3922c9d937151dae", + "sha256:5cd05d0f57846d8ba4b71d9c00f6f37d6b97d5e5ef8b3c3840426a475c8f70f4", + "sha256:5dd58946bce44b53b06d94aa95560d0b243eb2fe64227cba50017a8d8b3cd3e2", + "sha256:60080bb3d8617d96f0fb7e19796384cc2467447ef1c491694850ebd3670bc457", + "sha256:636ba0a77de609d6510235b7f0e77ec494d2657108f777e8765efc060094c98c", + "sha256:67d3ccfc590e5e7197750fcb3a2915b416a53e2de847a728cfa60141054123d4", + "sha256:68191f80a9bad283432385961d9efe09d783bcd36ed35a60fb1ff3f1ec2efe87", + "sha256:7502534e55c7c36c0978c91ba6f61703faf7ce733715ca48f499d3dbbd7657e0", + "sha256:7aa47c2e9ea33a4a2a05f40fcd3ea36d73853a2aae7b4feab6fc85f8bf2c9704", + "sha256:7d2af3f6b8419661a0c421584cfe8aaec1c0e435ce7e47ee2a97e344b98f794f", + "sha256:7e316026cc1095f2a3e8cc012822c99f413b702eaa2ca5408a513609488cb62f", + "sha256:88ad44e220e22b63b0f8f81f007e8abbb92874d8ced66f32571ef8beb0643b2b", + "sha256:88d1f7bef20c721359d8675f7d9f8e414ec5003d8f642fdfd8087777ff7f94b5", + "sha256:89723d2112697feaa320c9d351e5f5e7b841e83f8b143dba8e2d2b5f04e10923", + "sha256:8a0ccf52bb37d1a700375a6b395bff5dd15c50acb745f7db30415bae3c2b0715", + "sha256:8c2c19dae8a3eb0ea45a8448356ed561be843b13cbc34b840922ddf565498c1c", + "sha256:905466ad1702ed4acfd67a902af50b8db1feeb9781436372261808df7a2a7bca", + "sha256:9852b76ab558e45b20bf1893b59af64a28bd3820b0c2efc80e0a70a4a3ea51c1", + "sha256:98a2636994f943b871786c9e82bfe7883ecdaba2ef5df54e1450fa9869d1f756", + "sha256:9aa1a67bbf0f957bbe096375887b2505f5d8ae16bf04488e8b0f334c36e31360", + "sha256:9eda5f7a50141291beda3edd00abc2d4a5b16c29c92daf8d5bd76934150f3edc", + "sha256:a6d1047952c0b8104a1d371f88f4ab62e6275567d4458c1e26e9627ad489b445", + "sha256:a9b6d73353f777630626f403b0652055ebfe8ff142a44ec2cf18ae470395766e", + "sha256:a9cc99d6946d750eb75827cb53c4371b8b0fe89c733a94b1573c9dd16ea6c9e4", + "sha256:ad83e7545b4ab69216cef4cc47e344d19622e28aabec61574b20257c65466d6a", + "sha256:b014333bd0217ad3d54c143de9d4b9a3ca1c5a29a6d0d554952ea071cff0f1f8", + "sha256:b43523d7bc2abd757119dbfb38af91b5735eea45537ec6ec3a5ec3f9562a1c53", + "sha256:b521dcecebc5b978b447f0f69b5b7f3840eac454862270406a39837ffae4e697", + "sha256:b77e27b79448e34c2c51c09836033056a0547aa360c45eeeb67803da7b0eedaf", + "sha256:b7a635871143661feccce3979e1727c4e094f2bdfd3ec4b90dfd4f16f571a87a", + "sha256:b7fca9205b59c1a3d5031f7e64ed627a1074730a51c2a80e97653e3e9fa0d415", + "sha256:ba1b30765a55acf15dce3f364e4928b80858fa8f979ad41f862358939bdd1f2f", + "sha256:ba99d8077424501b9616b43a2d208095746fb1284fc5ba490139651f971d39d9", + "sha256:c25a8ad70e716f96e13a637802813f65d8a6760ef48672aa3502f4c24ea8b400", + "sha256:c3c4a78615b7762740531c27cf46e2f388d8d727d0c0c739e72048beb26c8a9d", + "sha256:c40281f7d70baf6e0db0c2f7472b31609f5bc2748fe7275ea65a0b4601d9b392", + "sha256:c7ad32824b7f02bb3c9f80306d405a1d9b7bb89362d68b3c5a9be53836caebdb", + "sha256:cb3fe77aec8f1995611f966d0c656fdce398317f850d0e6e7aebdfe61f40e1cd", + "sha256:cc038b2d8b1470364b1888a98fd22d616fba2b6309c5b5f181ad4483e0017861", + "sha256:cc37b9aeebab425f11f27e5e9e6cf580be7206c6582a64467a14dda211abc232", + "sha256:cc6bb9aa69aacf0f6032c307da718f61a40cf970849e471254e0e91c56ffca95", + "sha256:d126361607b33c4eb7b36debc173bf25d7805847346dd4d99b5499e1fef52bc7", + "sha256:d15b274f9e15b1a0b7a45d2ac86d1f634d983ca40d6b886721626c47a400bf39", + "sha256:d166eafc19f4718df38887b2bbe1467a4f74a9830e8605089ea7a30dd4da8887", + "sha256:d498eea3f581fbe1b34b59c697512a8baef88212f92e4c7830fcc1499f5b45a5", + "sha256:d6f7e255e5fa94642a0724e35406e6cb7001c09d476ab5fce002f652b36d0c39", + "sha256:d78bd484930c1da2b9679290a41cdb25cc127d783768a0369d6b449e72f88beb", + "sha256:d865984b3f71f6d0af64d0d88f5733521698f6c16f445bb09ce746c92c97c586", + "sha256:d902a43085a308cef32c0d3aea962524b725403fd9373dea18110904003bac97", + "sha256:d94a1db462d5690ebf6ae86d11c5e420042b9898af5dcf278bd97d6bda065423", + "sha256:da695d75ac97cb1cd725adac136d25ca687da4536154cdc2815f576e4da11c69", + "sha256:db2a0b1857f18b11e3b0e54ddfefc96af46b0896fb678c85f63fb8c37518b3e7", + "sha256:df26481f0c7a3f8739fecb3e81bc9da3fcfae34d6c094563b9d4670b047312e1", + "sha256:e14b73607d6231f3cc4622809c196b540a6a44e903bcfad940779c80dffa7be7", + "sha256:e2610e9406d3b0073636a3a2e80db05a02f0c3169b5632022b4e81c0364bcda5", + "sha256:e692296c4cc2873967771345a876bcfc1c547e8dd695c6b89342488b0ea55cd8", + "sha256:e693e233ac92ba83a87024e1d32b5f9ab15ca55ddd916d878146f4e3406b5c91", + "sha256:e81469f7d01efed9b53740aedd26085f20d49da65f9c1f41e822a33992cb1590", + "sha256:e8c7e08bb566de4faaf11984af13f6bcf6a08f327b13631d41d62592681d24fe", + "sha256:ed19b3a05ae0c97dd8f75a5d8f21f7723a8c33bbc555da6bbe1f96c470139d3c", + "sha256:efb2d82f33b2212898f1659fb1c2e9ac30493ac41e4d53123da374c3b5541e64", + "sha256:f44dd4d68697559d007462b0a3a1d9acd61d97072b71f6d1968daef26bc744bd", + "sha256:f72cbae7f6b01591f90814250e636065850c5926751af02bb48da94dfced7baa", + "sha256:f7bc09bc9c29ebead055bcba136a67378f03d66bf359e87d0f7c759d6d4ffa31", + "sha256:ff100b203092af77d1a5a7abe085b3506b7eaaf9abf65b73b7d6905b6cb76988" + ], + "markers": "python_version >= '3.7'", + "version": "==2023.12.25" + }, + "requests": { + "hashes": [ + "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f", + "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1" + ], + "index": "pypi", + "version": "==2.31.0" + }, + "six": { + "hashes": [ + "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", + "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", + "version": "==1.16.0" + }, + "soupsieve": { + "hashes": [ + "sha256:5663d5a7b3bfaeee0bc4372e7fc48f9cff4940b3eec54a6451cc5299f1097690", + "sha256:eaa337ff55a1579b6549dc679565eac1e3d000563bcb1c8ab0d0fefbc0c2cdc7" + ], + "markers": "python_version >= '3.8'", + "version": "==2.5" + }, + "tinycss2": { + "hashes": [ + "sha256:2b80a96d41e7c3914b8cda8bc7f705a4d9c49275616e886103dd839dfc847847", + "sha256:8cff3a8f066c2ec677c06dbc7b45619804a6938478d9d73c284b29d14ecb0627" + ], + "markers": "python_version >= '3.7'", + "version": "==1.2.1" + }, + "urllib3": { + "hashes": [ + "sha256:55901e917a5896a349ff771be919f8bd99aff50b79fe58fec595eb37bbc56bb3", + "sha256:df7aa8afb0148fa78488e7899b2c59b5f4ffcfa82e6c54ccb9dd37c1d7b52d54" + ], + "markers": "python_version >= '3.8'", + "version": "==2.1.0" + }, + "watchdog": { + "hashes": [ + "sha256:0e06ab8858a76e1219e68c7573dfeba9dd1c0219476c5a44d5333b01d7e1743a", + "sha256:13bbbb462ee42ec3c5723e1205be8ced776f05b100e4737518c67c8325cf6100", + "sha256:233b5817932685d39a7896b1090353fc8efc1ef99c9c054e46c8002561252fb8", + "sha256:25f70b4aa53bd743729c7475d7ec41093a580528b100e9a8c5b5efe8899592fc", + "sha256:2b57a1e730af3156d13b7fdddfc23dea6487fceca29fc75c5a868beed29177ae", + "sha256:336adfc6f5cc4e037d52db31194f7581ff744b67382eb6021c868322e32eef41", + "sha256:3aa7f6a12e831ddfe78cdd4f8996af9cf334fd6346531b16cec61c3b3c0d8da0", + "sha256:3ed7c71a9dccfe838c2f0b6314ed0d9b22e77d268c67e015450a29036a81f60f", + "sha256:4c9956d27be0bb08fc5f30d9d0179a855436e655f046d288e2bcc11adfae893c", + "sha256:4d98a320595da7a7c5a18fc48cb633c2e73cda78f93cac2ef42d42bf609a33f9", + "sha256:4f94069eb16657d2c6faada4624c39464f65c05606af50bb7902e036e3219be3", + "sha256:5113334cf8cf0ac8cd45e1f8309a603291b614191c9add34d33075727a967709", + "sha256:51f90f73b4697bac9c9a78394c3acbbd331ccd3655c11be1a15ae6fe289a8c83", + "sha256:5d9f3a10e02d7371cd929b5d8f11e87d4bad890212ed3901f9b4d68767bee759", + "sha256:7ade88d0d778b1b222adebcc0927428f883db07017618a5e684fd03b83342bd9", + "sha256:7c5f84b5194c24dd573fa6472685b2a27cc5a17fe5f7b6fd40345378ca6812e3", + "sha256:7e447d172af52ad204d19982739aa2346245cc5ba6f579d16dac4bfec226d2e7", + "sha256:8ae9cda41fa114e28faf86cb137d751a17ffd0316d1c34ccf2235e8a84365c7f", + "sha256:8f3ceecd20d71067c7fd4c9e832d4e22584318983cabc013dbf3f70ea95de346", + "sha256:9fac43a7466eb73e64a9940ac9ed6369baa39b3bf221ae23493a9ec4d0022674", + "sha256:a70a8dcde91be523c35b2bf96196edc5730edb347e374c7de7cd20c43ed95397", + "sha256:adfdeab2da79ea2f76f87eb42a3ab1966a5313e5a69a0213a3cc06ef692b0e96", + "sha256:ba07e92756c97e3aca0912b5cbc4e5ad802f4557212788e72a72a47ff376950d", + "sha256:c07253088265c363d1ddf4b3cdb808d59a0468ecd017770ed716991620b8f77a", + "sha256:c9d8c8ec7efb887333cf71e328e39cffbf771d8f8f95d308ea4125bf5f90ba64", + "sha256:d00e6be486affb5781468457b21a6cbe848c33ef43f9ea4a73b4882e5f188a44", + "sha256:d429c2430c93b7903914e4db9a966c7f2b068dd2ebdd2fa9b9ce094c7d459f33" + ], + "markers": "python_version >= '3.7'", + "version": "==3.0.0" + }, + "weasyprint": { + "hashes": [ + "sha256:0c0cdd617a78699262b80026e67fa1692e3802cfa966395436eeaf6f787dd126", + "sha256:3e98eedcc1c5a14cb310c293c6d59a479f59a13f0d705ff07106482827fa5705" + ], + "markers": "python_version >= '3.7'", + "version": "==60.2" + }, + "webencodings": { + "hashes": [ + "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", + "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923" + ], + "version": "==0.5.1" + }, + "zopfli": { + "hashes": [ + "sha256:0574372283befa5af98fb31407e1fe6822f2f9c437ef69e7fa260e49022d8a65", + "sha256:082f030b2b7d6d4597ac517816e499c63b92130aa8f4f74a3788ebaa5770f974", + "sha256:08d105a49576a9e629f53a710f0009c4bf0a1d8a3239a74e41d0944f26e28a61", + "sha256:09ad5f8d7e0fe1975ca6d9fd5ad61c74233ae277982d3bc8814b599bbeb92f44", + "sha256:0fbb6e7fc0da56835167e3c83a45b28e99ba14b671ecb8e51100ad03dfffc3d0", + "sha256:13d151d5c83980f384439c87a5511853890182c05d93444f3cb05e5ceed37d82", + "sha256:1c5fd29730024f5fb0e2623e3853ca422bd3cf57042389c8e0e771dc47f88084", + "sha256:1f25f1bb6440ed90a1d458772fa6ce53632f5fb61e435b12ae6b9b39af98d758", + "sha256:2073b07c3ec4fcbc895bb02565a90f9f31373233979f6be398e82eacbd1105f3", + "sha256:22b1cfc398a87754730f7e268693c8eb480cb688fd645648fda85614a8b1c08c", + "sha256:2770cf6b88e9985c79b90fd6d4c15d8dab0caa37c1c3b17773e61ce857eab586", + "sha256:27f2b58050f84fa059db7a6ec17d98b388c18f9783551e5f97605f790f25e155", + "sha256:2da6f30632cefda8ebe032fdcb69cf062f5a6435af9d32de82ccef320e0261f5", + "sha256:31c467a300ba46f55aa0ea958ea388e350eefd039cf15764bf4cd737d5eeb8a6", + "sha256:39d8a73bee07cf7f2c73e08508bf788bfdf28a527da353b5d3e2a0ee4aaf770c", + "sha256:3e4675ca4c7b1215b8a53cec1831cbdb6914f91ea2f183817a06fc7b38e27642", + "sha256:40665bf0bacc8b82652a1af4016648dd69f896afa59fc481c1d19a222aa746ea", + "sha256:40b830244e6458ef982b4a5ebb0f228986d481408bae557a95eeece2c5ede4e6", + "sha256:52438999888715a378fc6fe1477ab7813e9e9b58a27a38d2ad7be0e396b1ab2e", + "sha256:57f93802e5ddb20647747ee4039a2e18a26e91bac4c41d3d75a2b2c97f270549", + "sha256:5e52aaab3a93470cf0ff2bb2135a8628dda7b70f675c46f35b6a1b30e8e482f4", + "sha256:6020a3533c6c7be09db9e59c2a8f3f894bf5d8e95cc01890d82114c923317c57", + "sha256:61a2fcc624e8b038d4fca84ba927dc3f31df53a7284692d46aa44d16fb3f47b2", + "sha256:61abe5f11400f9c6b22be578091e28dfb9f1a61efaaeaa2da66138b03ee93072", + "sha256:6225bbc33c4f803cdc1e71e3028af96dd0e1ed3cf061780d1bf05648df616a05", + "sha256:711d4fde9cb99e1a9158978e9d1624a37cdd170ff057f6340059514fcf38e808", + "sha256:72349c78da402e6784bd9c5f4aff5cc7017bd969016ec07b656722f7f29fc975", + "sha256:7463b42a2cee33f0a018bf8f1304da2379d6cb8111aa4e04d8f8590d0f1099e1", + "sha256:7599ce108386d91a402969cba4f17247e33a594b21cbd662e008414ccb0b4cf7", + "sha256:7769f6ca73f37dff92159127bd25b0cc7d81d3feb819d355dc7ac01ad05c673d", + "sha256:78022777139ac973286219e9e085d9496fb6c935502d93a52bd1bed01dfc2002", + "sha256:7bc89b71d1c4677f708cc162f40a4560f78f5f4c6aa6d884b423df7d38e8ba0b", + "sha256:7ddcbc258bb5c07ebb7f6ee64c46d4e35c39c6abba2b3dfa72c0ea4daf9e65fc", + "sha256:7ebb4e1b0f102d431830151041777c55700d12afd1e5adb5bcbce72037c1a10e", + "sha256:81d61eba5a8e221b297a1dd27f1dae2785a14a5524cc1e144da53705cf90d5c4", + "sha256:8293062567917201609b28b865289d5ddee55030c779fa9264caae4cc2e00fb3", + "sha256:84321886cf3e80e086e0f6f7b765975343aafa61165315bb8db514d0bec2d887", + "sha256:92ca61eaa1df774908c173683e23c512189bf791a7ebb49fac61324658cff490", + "sha256:975d45745cf6c3e3b61127e0140dcf145fa515f2021f669bd82768937b7bb1fb", + "sha256:978395a4ce5cc46db29a36cdb80549b564dc7706237abaca5aac328dd5842f65", + "sha256:97d2f993142fed4f9c11c1766eb53409efe7298c755cf4599c171bfedcbaddae", + "sha256:9dcf7af42c11b3cf5d3fbf665799e10f54f66caea2020fe304602df83b9a1a69", + "sha256:ad2a98890045d13b0cdc93c1637990c211dc877493469afc61a097a00a70cf22", + "sha256:ae890df6e5f1e8fa0697cafd848826decce0ac53e54e5a018fd97775e3a354c0", + "sha256:b30a922b9d73f22da2b589b35e594dcc6d144eb38ad890c542f2b92902ba9892", + "sha256:c1afe5ba0d957e462afbd3da116ac1a2a6d23e8a94436a95b692c5c324694a16", + "sha256:c3c61787a90439cf68f751b2a1ab789b0805876c0cd9b58398adc212d1eeace5", + "sha256:c6555293e42e7a9154940bb18613de2abce21a855780baff8a6c372e395c59b3", + "sha256:ca9a6df3d11c2f8f0356c141523c4914a2850dd39fc213d968c0272db635eea9", + "sha256:d0a8e556916088fadb098ddb6eed034d5c2df3b8fba7f2488e87e8c224002eca", + "sha256:d40373db61883f6fc8b7040c9196a16f737e3063632afd15e8b3f25e871a30e8", + "sha256:dbc9841bedd736041eb5e6982cd92da93bee145745f5422f3795f6f258cdc6ef", + "sha256:dc59299eda2aaf57f0ee5c4b42ada0b80e3dc4c09c5bdda8ee9ae5cf93fafa9e", + "sha256:deffa15253a43a597e8ebf82ca1908bd70b3bf899da163b017d49ddd5e12732a", + "sha256:e4068d4d35b2e63898d22e1b7777d986b8f5d61fe83a77973730ce9cff1b4ba1", + "sha256:e5f62ca9a947f09f531c721e2a3f2e0094523436b8eb5df18d71245c1924f89a", + "sha256:eef08c02295bb99c7fdca380c52e5454fa1c08025fb0bea2c7ae6c0e1e9c034b", + "sha256:f07997453e7777e19ef0a2445cc1b90e1bb90c623dd77554325932dea6350fee", + "sha256:f48de4818c10c539fdd01276512043ae4ae738e0301e9cace1dd38f4bcffad6a", + "sha256:f69b161b4d49e256ab285c6c6ee1cf217fda864a9b175d24fa0a0b8c2de9ff13", + "sha256:ff86a2cd6b9864027861a129d6d73231b6d463f0d364ca0fdca4492390357cba" + ], + "version": "==0.2.3" + } + }, + "develop": {} +} diff --git a/dbrepo-authentication-service/dbrepo-realm.json b/dbrepo-authentication-service/dbrepo-realm.json index c861fbd662986b4363f6a08dbe48bdc5333592d0..b2730612a700e95f216b96c36e744ceab957ebfd 100644 --- a/dbrepo-authentication-service/dbrepo-realm.json +++ b/dbrepo-authentication-service/dbrepo-realm.json @@ -99,7 +99,7 @@ "description" : "${default-database-handling}", "composite" : true, "composites" : { - "realm" : [ "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" ] + "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", @@ -220,6 +220,14 @@ "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", @@ -2049,7 +2057,7 @@ "subType" : "authenticated", "subComponents" : { }, "config" : { - "allowed-protocol-mapper-types" : [ "saml-role-list-mapper", "oidc-address-mapper", "oidc-sha256-pairwise-sub-mapper", "saml-user-attribute-mapper", "saml-user-property-mapper", "oidc-full-name-mapper", "oidc-usermodel-attribute-mapper", "oidc-usermodel-property-mapper" ] + "allowed-protocol-mapper-types" : [ "oidc-sha256-pairwise-sub-mapper", "oidc-full-name-mapper", "saml-role-list-mapper", "oidc-address-mapper", "saml-user-attribute-mapper", "saml-user-property-mapper", "oidc-usermodel-attribute-mapper", "oidc-usermodel-property-mapper" ] } }, { "id" : "3ab11d74-5e76-408a-b85a-26bf8950f979", @@ -2058,7 +2066,7 @@ "subType" : "anonymous", "subComponents" : { }, "config" : { - "allowed-protocol-mapper-types" : [ "oidc-usermodel-property-mapper", "saml-user-property-mapper", "oidc-full-name-mapper", "oidc-address-mapper", "saml-user-attribute-mapper", "oidc-sha256-pairwise-sub-mapper", "saml-role-list-mapper", "oidc-usermodel-attribute-mapper" ] + "allowed-protocol-mapper-types" : [ "saml-user-attribute-mapper", "saml-role-list-mapper", "oidc-address-mapper", "oidc-sha256-pairwise-sub-mapper", "oidc-usermodel-property-mapper", "oidc-full-name-mapper", "oidc-usermodel-attribute-mapper", "saml-user-property-mapper" ] } } ], "org.keycloak.keys.KeyProvider" : [ { @@ -2110,7 +2118,7 @@ "internationalizationEnabled" : false, "supportedLocales" : [ ], "authenticationFlows" : [ { - "id" : "b8378805-a082-46a0-9e28-a1e5d4db7e41", + "id" : "05f92ecb-5a34-416a-a9a4-b4aeab2704c4", "alias" : "Account verification options", "description" : "Method with which to verity the existing account", "providerId" : "basic-flow", @@ -2132,7 +2140,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "2652bbd9-bd49-465c-8595-690099333bf7", + "id" : "e85f1d42-30c8-4878-ab0c-3cb9baaa308f", "alias" : "Authentication Options", "description" : "Authentication options.", "providerId" : "basic-flow", @@ -2161,7 +2169,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "967c3248-c2e9-45a9-b770-b02e965b958a", + "id" : "754e6269-c096-41d6-88df-44bd2652ec82", "alias" : "Browser - Conditional OTP", "description" : "Flow to determine if the OTP is required for the authentication", "providerId" : "basic-flow", @@ -2183,7 +2191,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "f78ad348-c3e1-476e-a916-fce0c383376a", + "id" : "5b2a16dd-7192-4558-931a-a67dfa7b14e1", "alias" : "Direct Grant - Conditional OTP", "description" : "Flow to determine if the OTP is required for the authentication", "providerId" : "basic-flow", @@ -2205,7 +2213,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "788cf02b-5744-4ea6-940a-96bc762da4bd", + "id" : "c12d7c33-256e-486f-8fb8-c8594eafd64e", "alias" : "First broker login - Conditional OTP", "description" : "Flow to determine if the OTP is required for the authentication", "providerId" : "basic-flow", @@ -2227,7 +2235,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "273e61b7-9cc3-464e-a7b8-27c71aca4014", + "id" : "711adf58-692f-4f22-ae20-0ba01d8d667c", "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", @@ -2249,7 +2257,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "00f41bfc-8513-466d-8c6a-366b7f2f36ca", + "id" : "dd53182d-ca4a-4096-b1fc-60237af977c4", "alias" : "Reset - Conditional OTP", "description" : "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.", "providerId" : "basic-flow", @@ -2271,7 +2279,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "980ebf01-fe0a-4cfa-880e-dd86ce8e190e", + "id" : "23c368c2-dce4-4ca8-8096-b6c726fa0e32", "alias" : "User creation or linking", "description" : "Flow for the existing/non-existing user alternatives", "providerId" : "basic-flow", @@ -2294,7 +2302,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "5e6a7a10-4be8-4038-8fc5-0588b452328d", + "id" : "37ff6b93-bdfe-4245-9247-009061fdfc7b", "alias" : "Verify Existing Account by Re-authentication", "description" : "Reauthentication of existing account", "providerId" : "basic-flow", @@ -2316,7 +2324,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "024e07f8-f975-41ef-b755-d2b089b5567c", + "id" : "c1f58e18-5d41-40b1-aa73-4a4e4a970430", "alias" : "browser", "description" : "browser based authentication", "providerId" : "basic-flow", @@ -2352,7 +2360,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "324da9be-755e-4556-a1d3-58569b9df47c", + "id" : "9229472e-78c8-4e83-aa20-7a2e22c28f59", "alias" : "clients", "description" : "Base authentication for clients", "providerId" : "client-flow", @@ -2388,7 +2396,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "bced47d4-5d04-4bb9-8605-94041185c0f3", + "id" : "d841dca1-b9ca-47bc-8f9a-dcd5896678dd", "alias" : "direct grant", "description" : "OpenID Connect Resource Owner Grant", "providerId" : "basic-flow", @@ -2417,7 +2425,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "6b301d9d-68c0-44c3-9a57-92669d08b2f3", + "id" : "42e0301c-d81c-4127-9e17-064811566f9a", "alias" : "docker auth", "description" : "Used by Docker clients to authenticate against the IDP", "providerId" : "basic-flow", @@ -2432,7 +2440,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "9c9ddfeb-37a2-4186-a58f-cf90dca8e191", + "id" : "4809629a-0e3c-4894-8cd7-60d99abeb2e8", "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", @@ -2455,7 +2463,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "a9ef5094-93bf-49fc-9d0f-dcfc551cac5a", + "id" : "7ce37ac0-9aba-412d-98fb-78745e6df1ff", "alias" : "forms", "description" : "Username, password, otp and other auth forms.", "providerId" : "basic-flow", @@ -2477,7 +2485,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "fae6e2e4-a071-458b-ac03-41dda3456f5a", + "id" : "9fa4ee30-9ab4-40c3-bb9f-b56b8738d1c0", "alias" : "http challenge", "description" : "An authentication flow based on challenge-response HTTP Authentication Schemes", "providerId" : "basic-flow", @@ -2499,7 +2507,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "ae5bcac5-8867-42e1-887f-fc67418b0c4b", + "id" : "bba37884-4bd0-4597-9f26-e8b8c7d60dc6", "alias" : "registration", "description" : "registration flow", "providerId" : "basic-flow", @@ -2515,7 +2523,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "72524b5d-1cfc-41b0-b29b-6f6890d2dc7f", + "id" : "9e3b3ba5-e37e-4f6d-a7a7-fd37558f6e2d", "alias" : "registration form", "description" : "registration form", "providerId" : "form-flow", @@ -2551,7 +2559,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "834c96b8-790d-4869-8c66-d42cd35e4873", + "id" : "e38d574a-2171-408b-9f9d-1ebe60791110", "alias" : "reset credentials", "description" : "Reset credentials for a user if they forgot their password or something", "providerId" : "basic-flow", @@ -2587,7 +2595,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "7f131501-e3ff-48f2-98e6-e34e4c5d6f9e", + "id" : "5560dfff-822c-43fb-a910-db38b4470268", "alias" : "saml ecp", "description" : "SAML ECP Profile Authentication Flow", "providerId" : "basic-flow", @@ -2603,13 +2611,13 @@ } ] } ], "authenticatorConfig" : [ { - "id" : "638341f1-94ba-4042-a3ee-41a0f41718f6", + "id" : "201f18f6-b170-4fcc-bcc2-2ca05b1558aa", "alias" : "create unique user config", "config" : { "require.password.update.after.registration" : "false" } }, { - "id" : "3c355b8c-8a51-4346-88f2-1ff81856b55c", + "id" : "f6e84d09-4994-452a-be1a-fe896289ae9d", "alias" : "review profile config", "config" : { "update.profile.on.first.login" : "missing" diff --git a/dbrepo-data-service/README.md b/dbrepo-data-service/README.md index 0441a17305f7f085082c445d8f77825c721a41be..dfea03bc6bc415d9b4792853cff16ff1372fe377 100644 --- a/dbrepo-data-service/README.md +++ b/dbrepo-data-service/README.md @@ -1,6 +1,31 @@ # Data Service -## Actuator +## Test + +Run all unit and integration tests and create an HTML+TXT coverage report located in the `report` module: + +```bash +mvn -pl rest-service clean test verify +``` + +Or run only tests +in [`DatabaseServiceIntegrationTest.java`](https://gitlab.phaidra.org/fair-data-austria-db-repository/fda-services/-/blob/master/dbrepo-data-service/rest-service/src/test/java/at/tuwien/service/DatabaseServiceIntegrationTest.java): + +```bash +mvn -pl rest-service -Dtest="DatabaseServiceIntegrationTest" clean test +``` + +## Run + +Start the Metadata Database, Data Database, Broker Service before and then run the Data Service: + +```bash +mvn -pl rest-service clean spring-boot:run -Dspring-boot.run.profiles=local +``` + +### Endpoints + +#### Actuator - Info: http://localhost:9093/actuator/info - Health: http://localhost:9093/actuator/health @@ -8,17 +33,10 @@ - Liveness: http://localhost:9093/actuator/health/liveness - Prometheus: http://localhost:9093/actuator/prometheus -## Swagger UI Endpoints +#### Swagger UI - Swagger UI: http://localhost:9093/swagger-ui/index.html -## OpenAPI Endpoints - -- OpenAPI v3 as .yaml: http://localhost:9093/v3/api-docs.yaml - -## Build +#### OpenAPI -```shell -mvn -f ../dbrepo-metadata-service/pom.xml clean install -DskipTests -mvn clean package -DskipTests -``` \ No newline at end of file +- OpenAPI v3 as .yaml: http://localhost:9093/v3/api-docs.yaml \ No newline at end of file diff --git a/dbrepo-data-service/pom.xml b/dbrepo-data-service/pom.xml index 22dcf3329572f60d0efbf921276414945902e8ba..6b9556bf5f2de2d20b6680f13913333bd1350aea 100644 --- a/dbrepo-data-service/pom.xml +++ b/dbrepo-data-service/pom.xml @@ -13,7 +13,7 @@ <name>dbrepo-data-service</name> <version>1.4.1</version> - <description>This service is responsible for the database-specific CRUD operations.</description> + <description>Service that manages the data</description> <url>https://www.ifs.tuwien.ac.at/infrastructures/dbrepo/</url> <developers> @@ -60,7 +60,7 @@ <opencsv.version>5.7.1</opencsv.version> <super-csv.version>2.4.0</super-csv.version> <jsql.version>4.6</jsql.version> - <springdoc-openapi.version>2.1.0</springdoc-openapi.version> + <springdoc-openapi.version>2.3.0</springdoc-openapi.version> <hsqldb.version>2.7.2</hsqldb.version> <testcontainers.version>1.19.1</testcontainers.version> <opensearch-testcontainer.version>2.0.0</opensearch-testcontainer.version> @@ -195,12 +195,7 @@ <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> - <exclusions> - <exclusion> - <groupId>org.junit.jupiter</groupId> - <artifactId>junit-vintage-engine</artifactId> - </exclusion> - </exclusions> + <scope>test</scope> </dependency> <dependency> <groupId>com.h2database</groupId> diff --git a/dbrepo-data-service/rest-service/src/main/java/at/tuwien/config/SwaggerConfig.java b/dbrepo-data-service/rest-service/src/main/java/at/tuwien/config/SwaggerConfig.java index f7b33979a78946d23cfea809a49a20f2d75bdb76..56ea660541925aa401a967700764b431c2b41fab 100644 --- a/dbrepo-data-service/rest-service/src/main/java/at/tuwien/config/SwaggerConfig.java +++ b/dbrepo-data-service/rest-service/src/main/java/at/tuwien/config/SwaggerConfig.java @@ -7,6 +7,7 @@ import io.swagger.v3.oas.models.info.Info; import io.swagger.v3.oas.models.info.License; import io.swagger.v3.oas.models.servers.Server; import org.springdoc.core.models.GroupedOpenApi; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -15,6 +16,9 @@ import java.util.List; @Configuration public class SwaggerConfig { + @Value("${server.port}") + private Integer port; + @Bean public OpenAPI springShopOpenAPI() { return new OpenAPI() @@ -32,10 +36,10 @@ public class SwaggerConfig { .description("Sourcecode Documentation") .url("https://gitlab.phaidra.org/fair-data-austria-db-repository/fda-services")) .servers(List.of(new Server() - .description("Generated server url") - .url("http://localhost:9093"), + .description("Development instance") + .url("http://localhost:" + port), new Server() - .description("Sandbox") + .description("Staging instance") .url("https://test.dbrepo.tuwien.ac.at"))); } diff --git a/dbrepo-gateway-service/dbrepo.conf b/dbrepo-gateway-service/dbrepo.conf index d05efc2ef902923b1b6ea5de57c7d2bbfc1a6014..3ae1a2b110aca23d29b079c2add2c10046c21dda 100644 --- a/dbrepo-gateway-service/dbrepo.conf +++ b/dbrepo-gateway-service/dbrepo.conf @@ -73,8 +73,13 @@ server { proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host $host; proxy_pass http://upload; proxy_read_timeout 90; + # Disable request and response buffering + proxy_request_buffering off; + proxy_buffering off; + proxy_http_version 1.1; } location /api/analyse { diff --git a/dbrepo-metadata-db/setup-schema.sql b/dbrepo-metadata-db/setup-schema.sql index 2981117119cc6fbba19a9ca80752695925ff1f63..f3de67233f485a0c9689da14ef6a55559331f78d 100644 --- a/dbrepo-metadata-db/setup-schema.sql +++ b/dbrepo-metadata-db/setup-schema.sql @@ -95,6 +95,7 @@ CREATE TABLE IF NOT EXISTS `mdb_databases` description text, engine character varying(20), is_public boolean NOT NULL DEFAULT TRUE, + image longblob, created_by character varying(36), owned_by character varying(36), contact_person character varying(36), diff --git a/dbrepo-metadata-service/Dockerfile b/dbrepo-metadata-service/Dockerfile index 14f3dbe95ff1862c2303feee409d6d6b4081409f..4c9ae73e5462ac08c1f471552fbfc4f27ac01c7c 100644 --- a/dbrepo-metadata-service/Dockerfile +++ b/dbrepo-metadata-service/Dockerfile @@ -75,6 +75,7 @@ ENV DATACITE_PASSWORD="" ENV S3_STORAGE_ENDPOINT="http://storage-service:9000" ENV S3_ACCESS_KEY_ID="seaweedfsadmin" ENV S3_SECRET_ACCESS_KEY="seaweedfsadmin" +ENV DELETE_STALE_FILES_RATE=60 ENV MIRROR_RATE=60 ENV OBTAIN_METADATA_RATE=60 ENV DELETE_STALE_QUERIES_RATE=60 diff --git a/dbrepo-metadata-service/README.md b/dbrepo-metadata-service/README.md index 54443a4e20a8a7d2e34af4bb7bdada7833b11149..f7abaeaab0a521fd611fa4eedad1b0d50c975f9f 100644 --- a/dbrepo-metadata-service/README.md +++ b/dbrepo-metadata-service/README.md @@ -1,15 +1,42 @@ -# Semantics Service +# Metadata Service -## Actuator +## Test -- Actuator Info: http://localhost:9099/actuator/info -- Actuator Health: http://localhost:9099/actuator/health -- Actuator Prometheus: http://localhost:9099/actuator/prometheus +Run all unit and integration tests and create an HTML+TXT coverage report located in the `report` module: -## Swagger UI Endpoints +```bash +mvn -pl rest-service clean test verify +``` + +Or run only unit tests +in [`KeycloakGatewayUnitTest.java`](https://gitlab.phaidra.org/fair-data-austria-db-repository/fda-services/-/blob/master/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/gateway/BrokerServiceGatewayTest.java): + +```bash +mvn -pl rest-service -Dtest="KeycloakGatewayUnitTest" clean test +``` + +## Run + +Start the Metadata Database before and then run the Metadata Service: + +```bash +mvn -pl rest-service clean spring-boot:run -Dspring-boot.run.profiles=local +``` + +### Endpoints + +#### Actuator + +- Info: http://localhost:9099/actuator/info +- Health: http://localhost:9099/actuator/health + - Readiness: http://localhost:9099/actuator/health/readiness + - Liveness: http://localhost:9099/actuator/health/liveness +- Prometheus: http://localhost:9099/actuator/prometheus + +#### Swagger UI - Swagger UI: http://localhost:9099/swagger-ui/index.html -## OpenAPI Endpoints +#### OpenAPI - OpenAPI v3 as .yaml: http://localhost:9099/v3/api-docs.yaml \ No newline at end of file diff --git a/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/database/DatabaseDto.java b/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/database/DatabaseDto.java index e6db0c9736efc00906c9ea3a8cedf36bd669f164..9ecf26e3865592ab3c64a37dc3ab56483cf298ea 100644 --- a/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/database/DatabaseDto.java +++ b/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/database/DatabaseDto.java @@ -97,6 +97,9 @@ public class DatabaseDto { @Field(name = "owner", type = FieldType.Object) private UserDto owner; + @ToString.Exclude + private byte[] image; + @NotNull @Schema(example = "2021-03-12T15:26:21Z") @Field(name = "created", type = FieldType.Date) diff --git a/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/database/DatabaseModifyImageDto.java b/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/database/DatabaseModifyImageDto.java new file mode 100644 index 0000000000000000000000000000000000000000..627714f6cb358d1d7d53285f1646d565378553fd --- /dev/null +++ b/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/database/DatabaseModifyImageDto.java @@ -0,0 +1,17 @@ +package at.tuwien.api.database; + +import lombok.*; +import lombok.extern.jackson.Jacksonized; + +@Getter +@Setter +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Jacksonized +@ToString +public class DatabaseModifyImageDto { + + private String key; + +} diff --git a/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/database/table/TableDto.java b/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/database/table/TableDto.java index 72f666fdd05b3cbb1f877628db19f5a07ab59a2c..f4560c5a21f823f47dd1f68e761d1615ba12ae44 100644 --- a/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/database/table/TableDto.java +++ b/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/database/table/TableDto.java @@ -16,6 +16,7 @@ import org.springframework.data.annotation.Id; import org.springframework.data.elasticsearch.annotations.Field; import org.springframework.data.elasticsearch.annotations.FieldType; +import java.math.BigInteger; import java.time.Instant; import java.util.List; import java.util.UUID; @@ -98,6 +99,26 @@ public class TableDto { @Field(name = "is_public", type = FieldType.Boolean) private Boolean isPublic; + @JsonProperty("num_rows") + @Schema(example = "5") + @Field(name = "num_rows", type = FieldType.Long) + private Long numRows; + + @JsonProperty("data_length") + @Schema(example = "16384", description = "in bytes") + @Field(name = "data_length", type = FieldType.Long) + private Long dataLength; + + @JsonProperty("max_data_length") + @Schema(example = "0", description = "in bytes") + @Field(name = "max_data_length", type = FieldType.Long) + private Long maxDataLength; + + @JsonProperty("avg_row_length") + @Schema(example = "3276", description = "in bytes") + @Field(name = "avg_row_length", type = FieldType.Long) + private Long avgRowLength; + @NotNull @Schema(example = "2021-03-12T15:26:21Z") @Field(name = "created", type = FieldType.Date) diff --git a/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/user/UserAttributesDto.java b/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/user/UserAttributesDto.java index 69a40539e3d90ee33665b133ceee204f6e984fac..4036a35ede621117518136e8e8a5c9c0703d02ea 100644 --- a/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/user/UserAttributesDto.java +++ b/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/user/UserAttributesDto.java @@ -17,7 +17,6 @@ import org.springframework.data.elasticsearch.annotations.FieldType; @ToString public class UserAttributesDto { - @JsonIgnore @org.springframework.data.annotation.Transient @JsonProperty("theme_dark") @Schema(example = "false") diff --git a/dbrepo-metadata-service/entities/src/main/java/at/tuwien/entities/database/Database.java b/dbrepo-metadata-service/entities/src/main/java/at/tuwien/entities/database/Database.java index 8a1f869a0e47db18c48cffd9c826542611960577..19b7eae6c93f2c47fe987cbe3ca455da11a010d1 100644 --- a/dbrepo-metadata-service/entities/src/main/java/at/tuwien/entities/database/Database.java +++ b/dbrepo-metadata-service/entities/src/main/java/at/tuwien/entities/database/Database.java @@ -17,6 +17,7 @@ import org.springframework.data.annotation.LastModifiedDate; import org.springframework.data.jpa.domain.support.AuditingEntityListener; import java.io.Serializable; +import java.sql.Blob; import java.time.Instant; import java.util.List; import java.util.UUID; @@ -131,6 +132,11 @@ public class Database implements Serializable { @Column(nullable = false) private Boolean isPublic; + @Lob + @Basic(fetch = FetchType.LAZY) + @Column(columnDefinition = "LONGBLOB") + private byte[] image; + @CreatedDate @Column(nullable = false, updatable = false, columnDefinition = "TIMESTAMP default NOW()") @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX", timezone = "UTC") diff --git a/dbrepo-metadata-service/pom.xml b/dbrepo-metadata-service/pom.xml index 8d29d42c862d34bb6a129bde5348ef82057c8190..22f9a858fb6e21977094852d65ff1557c77495af 100644 --- a/dbrepo-metadata-service/pom.xml +++ b/dbrepo-metadata-service/pom.xml @@ -63,7 +63,8 @@ <mapstruct.version>1.5.5.Final</mapstruct.version> <rabbitmq.version>5.20.0</rabbitmq.version> <jackson-datatype.version>2.15.0</jackson-datatype.version> - <commons.version>2.11.0</commons.version> + <commons-io.version>2.15.0</commons-io.version> + <commons-validator.version>1.8.0</commons-validator.version> <guava.version>33.0.0-jre</guava.version> <jacoco.version>0.8.11</jacoco.version> <jwt.version>4.3.0</jwt.version> @@ -74,7 +75,7 @@ <super-csv.version>2.4.0</super-csv.version> <jsql-parser.version>4.6</jsql-parser.version> <keycloak.version>21.0.2</keycloak.version> - <springdoc-openapi.version>2.1.0</springdoc-openapi.version> + <springdoc-openapi.version>2.3.0</springdoc-openapi.version> <testcontainers.version>1.19.1</testcontainers.version> <opensearch-testcontainer.version>2.0.0</opensearch-testcontainer.version> <keycloak-testcontainer.version>3.2.0</keycloak-testcontainer.version> @@ -238,7 +239,12 @@ <dependency> <groupId>commons-io</groupId> <artifactId>commons-io</artifactId> - <version>${commons.version}</version> + <version>${commons-io.version}</version> + </dependency> + <dependency> + <groupId>commons-validator</groupId> + <artifactId>commons-validator</artifactId> + <version>${commons-validator.version}</version> </dependency> <!-- AMPQ --> <dependency> @@ -370,6 +376,7 @@ <exclude>at/tuwien/utils/**/*</exclude> <exclude>at/tuwien/config/**/*</exclude> <exclude>at/tuwien/auth/**/*</exclude> + <exclude>at/tuwien/gateway/impl/ApiTemplateInterceptorImpl.class</exclude> <exclude>**/testcontainers.properties</exclude> <exclude>**/HibernateConnector.class</exclude> <exclude>**/DbrepoMetadataServiceApplication.class</exclude> diff --git a/dbrepo-metadata-service/report/pom.xml b/dbrepo-metadata-service/report/pom.xml index 16300d9c59deda00d3994fd02d0eb745b7d4340b..5720cb7752707a5051f5d9ed76a104dd2b117148 100644 --- a/dbrepo-metadata-service/report/pom.xml +++ b/dbrepo-metadata-service/report/pom.xml @@ -19,6 +19,11 @@ <artifactId>dbrepo-metadata-service-rest-service</artifactId> <version>${project.version}</version> </dependency> + <dependency> + <groupId>at.tuwien</groupId> + <artifactId>dbrepo-metadata-service-services</artifactId> + <version>${project.version}</version> + </dependency> </dependencies> <build> diff --git a/dbrepo-metadata-service/repositories/src/main/java/at/tuwien/mapper/DatabaseMapper.java b/dbrepo-metadata-service/repositories/src/main/java/at/tuwien/mapper/DatabaseMapper.java index f814b535176456bb0bd8e154f0a4f7ffe92ebdc9..7bd1c5d0eb55eb4aa815d9662732d9547f38aef4 100644 --- a/dbrepo-metadata-service/repositories/src/main/java/at/tuwien/mapper/DatabaseMapper.java +++ b/dbrepo-metadata-service/repositories/src/main/java/at/tuwien/mapper/DatabaseMapper.java @@ -117,7 +117,7 @@ public interface DatabaseMapper { default PreparedStatement databaseToDatabaseMetadata(Connection connection, Database database) throws QueryMalformedException { final StringBuilder statement = new StringBuilder("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` = '") .append(database.getInternalName()) - .append("' AND t.`TABLE_TYPE` IN ('BASE TABLE', 'SYSTEM VERSIONED', 'VIEW') AND t.`TABLE_NAME` NOT IN ('qs_queries', '_tmp') AND t.`TABLE_NAME` NOT LIKE 'hs_%'"); + .append("' AND t.`TABLE_TYPE` IN ('BASE TABLE', 'SYSTEM VERSIONED', 'VIEW') AND t.`TABLE_NAME` NOT IN ('qs_queries', '_tmp') AND t.`TABLE_NAME` NOT LIKE 'hs_%' AND t.`TABLE_NAME` NOT LIKE '%_temporary'"); log.trace("statement={}", statement); try { return connection.prepareStatement(statement.toString()); diff --git a/dbrepo-metadata-service/repositories/src/main/java/at/tuwien/mapper/QueryMapper.java b/dbrepo-metadata-service/repositories/src/main/java/at/tuwien/mapper/QueryMapper.java index 4c232eafcee6be72b107c9b315ccae9203e69a84..e45ce93aaae7cd8bb8b69a53af76edc8247a3e23 100644 --- a/dbrepo-metadata-service/repositories/src/main/java/at/tuwien/mapper/QueryMapper.java +++ b/dbrepo-metadata-service/repositories/src/main/java/at/tuwien/mapper/QueryMapper.java @@ -935,15 +935,6 @@ public interface QueryMapper { } } - default String selectItemToEscapedString(SelectItem data) { - final String item = data.toString(); - final int idx = item.indexOf('.'); - if (idx == -1) { - return "`" + item + "`"; - } - return "`" + item.substring(idx + 1) + "`"; - } - default String generateInsertFromTemporaryTableSQL(Table table) { final StringBuilder statement = new StringBuilder("INSERT INTO `") .append(table.getDatabase().getInternalName()) diff --git a/dbrepo-metadata-service/repositories/src/main/java/at/tuwien/mapper/S3Mapper.java b/dbrepo-metadata-service/repositories/src/main/java/at/tuwien/mapper/S3Mapper.java index 87e68ef389cfa15b7ec9f8170d2f36efda645a5d..6e89e98494fa3cb47789c4f5bcc50db95e224536 100644 --- a/dbrepo-metadata-service/repositories/src/main/java/at/tuwien/mapper/S3Mapper.java +++ b/dbrepo-metadata-service/repositories/src/main/java/at/tuwien/mapper/S3Mapper.java @@ -1,28 +1,10 @@ package at.tuwien.mapper; -import io.minio.GetObjectArgs; import org.mapstruct.Mapper; -import org.springframework.http.MediaType; -import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; - -import java.util.Collections; @Mapper(componentModel = "spring") public interface S3Mapper { org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(S3Mapper.class); - default GetObjectArgs s3ArgsToObjectArgs(String bucketName, String key) { - return s3ArgsToObjectArgs(bucketName, key, null); - } - - default GetObjectArgs s3ArgsToObjectArgs(String bucketName, String key, Long length) { - final GetObjectArgs.Builder builder = GetObjectArgs.builder() - .bucket(bucketName) - .object(key); - if (length != null) { - builder.length(length); - } - return builder.build(); - } } diff --git a/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/config/SwaggerConfig.java b/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/config/SwaggerConfig.java index 8aa287f9fd5c51548aec0ca20562cd07a84d335d..c3e047da3afba5c60eaf8d5e7fe93311d2db782a 100644 --- a/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/config/SwaggerConfig.java +++ b/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/config/SwaggerConfig.java @@ -7,6 +7,7 @@ import io.swagger.v3.oas.models.info.Info; import io.swagger.v3.oas.models.info.License; import io.swagger.v3.oas.models.servers.Server; import org.springdoc.core.models.GroupedOpenApi; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -15,6 +16,9 @@ import java.util.List; @Configuration public class SwaggerConfig { + @Value("${server.port}") + private Integer port; + @Bean public OpenAPI springShopOpenAPI() { return new OpenAPI() @@ -32,10 +36,10 @@ public class SwaggerConfig { .description("Sourcecode Documentation") .url("https://gitlab.phaidra.org/fair-data-austria-db-repository/fda-services")) .servers(List.of(new Server() - .description("Generated server url") - .url("http://localhost:9099"), + .description("Development instance") + .url("http://localhost:" + port), new Server() - .description("Sandbox") + .description("Staging instance") .url("https://test.dbrepo.tuwien.ac.at"))); } 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 054bc7cfa9e29eb819ec0329d6c4ba23aa559470..0d79f8288aa1e0eb1a330bb52f4070dd0b668b48 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 @@ -49,7 +49,7 @@ public class AccessEndpoint { @Transactional @Observed(name = "dbr_access_give") @PreAuthorize("hasAuthority('create-database-access')") - @Operation(summary = "Give access to some database", security = @SecurityRequirement(name = "bearerAuth")) + @Operation(summary = "Give access to some database", security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) @ApiResponses(value = { @ApiResponse(responseCode = "202", description = "Granting access succeeded", @@ -98,7 +98,7 @@ public class AccessEndpoint { @Transactional @Observed(name = "dbr_access_modify") @PreAuthorize("hasAuthority('update-database-access')") - @Operation(summary = "Modify access to some database", security = @SecurityRequirement(name = "bearerAuth")) + @Operation(summary = "Modify access to some database", security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) @ApiResponses(value = { @ApiResponse(responseCode = "202", description = "Modify access succeeded", @@ -136,7 +136,7 @@ public class AccessEndpoint { @Transactional @Observed(name = "dbr_access_check") @PreAuthorize("hasAuthority('check-database-access')") - @Operation(summary = "Check access to some database", security = @SecurityRequirement(name = "bearerAuth")) + @Operation(summary = "Check access to some database", security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Found database access", @@ -168,7 +168,7 @@ public class AccessEndpoint { @Transactional @Observed(name = "dbr_access_delete") @PreAuthorize("hasAuthority('delete-database-access')") - @Operation(summary = "Revoke access to some database", security = @SecurityRequirement(name = "bearerAuth")) + @Operation(summary = "Revoke access to some database", security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) @ApiResponses(value = { @ApiResponse(responseCode = "202", description = "Revoked access successfully", 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 c0e01632f35a48bda6b0be6534d84b0a054b6416..4923e6a859cd0397ff72e81fe837bc59625fea4d 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 @@ -77,7 +77,7 @@ public class ContainerEndpoint { @Transactional @Observed(name = "dbr_container_create") @PreAuthorize("hasAuthority('create-container')") - @Operation(summary = "Create container", security = @SecurityRequirement(name = "bearerAuth")) + @Operation(summary = "Create container", security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) @ApiResponses(value = { @ApiResponse(responseCode = "201", description = "Created a new container", @@ -136,7 +136,7 @@ public class ContainerEndpoint { @Transactional @Observed(name = "dbr_container_delete") @PreAuthorize("hasAuthority('delete-container')") - @Operation(summary = "Delete some container", security = @SecurityRequirement(name = "bearerAuth")) + @Operation(summary = "Delete some container", security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) @ApiResponses(value = { @ApiResponse(responseCode = "202", description = "Deleted container successfully"), 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 beec5ad35112142480210e50c394df248cff2f29..435e31feeb415285266f913ebec75f12b82ee99d 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 @@ -4,6 +4,7 @@ import at.tuwien.api.amqp.ExchangeDto; import at.tuwien.api.database.*; import at.tuwien.api.error.ApiErrorDto; import at.tuwien.config.RabbitConfig; +import at.tuwien.config.S3Config; import at.tuwien.entities.database.Database; import at.tuwien.entities.database.DatabaseAccess; import at.tuwien.entities.user.User; @@ -13,6 +14,10 @@ import at.tuwien.service.*; import at.tuwien.utils.PrincipalUtil; import at.tuwien.utils.UserUtil; import io.micrometer.observation.annotation.Observed; +import io.minio.GetObjectArgs; +import io.minio.GetObjectResponse; +import io.minio.MinioClient; +import io.minio.errors.*; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Content; @@ -31,6 +36,9 @@ import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.*; +import java.io.IOException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; import java.security.Principal; import java.util.List; import java.util.stream.Collectors; @@ -45,16 +53,19 @@ public class DatabaseEndpoint { private final RabbitConfig rabbitConfig; private final AccessService accessService; private final DatabaseMapper databaseMapper; + private final StorageService storageService; private final DatabaseService databaseService; private final QueryStoreService queryStoreService; private final MessageQueueService messageQueueService; @Autowired public DatabaseEndpoint(DatabaseMapper databaseMapper, UserService userService, RabbitConfig rabbitConfig, - DatabaseService databaseService, QueryStoreService queryStoreService, - AccessService accessService, MessageQueueService messageQueueService) { + StorageService storageService, DatabaseService databaseService, + QueryStoreService queryStoreService, AccessService accessService, + MessageQueueService messageQueueService) { this.userService = userService; this.rabbitConfig = rabbitConfig; + this.storageService = storageService; this.accessService = accessService; this.databaseMapper = databaseMapper; this.databaseService = databaseService; @@ -141,7 +152,7 @@ public class DatabaseEndpoint { @Transactional(rollbackFor = Exception.class) @PreAuthorize("hasAuthority('create-database')") @Observed(name = "dbr_database_create") - @Operation(summary = "Create database", security = @SecurityRequirement(name = "bearerAuth")) + @Operation(summary = "Create database", security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) @ApiResponses(value = { @ApiResponse(responseCode = "201", description = "Created a new database", @@ -196,7 +207,7 @@ public class DatabaseEndpoint { @Transactional @PreAuthorize("hasAuthority('modify-database-visibility')") @Observed(name = "dbr_database_visibility") - @Operation(summary = "Update database", security = @SecurityRequirement(name = "bearerAuth")) + @Operation(summary = "Update database", security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) @ApiResponses(value = { @ApiResponse(responseCode = "202", description = "Visibility modified successfully", @@ -218,11 +229,11 @@ public class DatabaseEndpoint { @Valid @RequestBody DatabaseModifyVisibilityDto data, @NotNull Principal principal) throws DatabaseNotFoundException, NotAllowedException { - log.debug("endpoint update database, id={}, data={}, {}", id, data, PrincipalUtil.formatForDebug(principal)); + log.debug("endpoint modify database visibility, id={}, data={}, {}", id, data, PrincipalUtil.formatForDebug(principal)); final Database database = databaseService.findById(id); if (!database.getOwnedBy().equals(UserUtil.getId(principal))) { - log.error("Failed to create database: not owner"); - throw new NotAllowedException(("Failed to create database: not owner")); + log.error("Failed to modify database visibility: not owner"); + throw new NotAllowedException("Failed to modify database visibility: not owner"); } final DatabaseDto dto = databaseMapper.databaseToDatabaseDto(databaseService.visibility(id, data)); log.trace("update database resulted in database {}", dto); @@ -234,7 +245,7 @@ public class DatabaseEndpoint { @Transactional @PreAuthorize("hasAuthority('modify-database-owner')") @Observed(name = "dbr_database_transfer") - @Operation(summary = "Transfer database", security = @SecurityRequirement(name = "bearerAuth")) + @Operation(summary = "Transfer database", security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) @ApiResponses(value = { @ApiResponse(responseCode = "202", description = "Transfer of ownership was successful", @@ -256,12 +267,12 @@ public class DatabaseEndpoint { @Valid @RequestBody DatabaseTransferDto transferDto, @NotNull Principal principal) throws DatabaseNotFoundException, UserNotFoundException, NotAllowedException { - log.debug("endpoint update database, id={}, transferDto={}, {}", id, transferDto, PrincipalUtil.formatForDebug(principal)); + log.debug("endpoint transfer database, id={}, transferDto={}, {}", id, transferDto, PrincipalUtil.formatForDebug(principal)); final Database database = databaseService.findById(id); final User user = userService.findByUsername(principal.getName()); if (!database.getOwnedBy().equals(user.getId())) { - log.error("Failed to create database: not owner"); - throw new NotAllowedException(("Failed to create database: not owner")); + log.error("Failed to transfer database: not owner"); + throw new NotAllowedException("Failed to transfer database: not owner"); } final DatabaseDto dto = databaseMapper.databaseToDatabaseDto(databaseService.transfer(id, transferDto)); log.trace("update database resulted in database {}", dto); @@ -269,10 +280,59 @@ public class DatabaseEndpoint { .body(dto); } + @PutMapping("/{id}/image") + @Transactional + @PreAuthorize("hasAuthority('modify-database-image')") + @Observed(name = "dbr_database_image") + @Operation(summary = "Modify database image", security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) + @ApiResponses(value = { + @ApiResponse(responseCode = "202", + description = "Modify of image was successful", + content = {@Content( + mediaType = "application/json", + schema = @Schema(implementation = DatabaseDto.class))}), + @ApiResponse(responseCode = "404", + description = "Database or user could not be found", + content = {@Content( + mediaType = "application/json", + schema = @Schema(implementation = ApiErrorDto.class))}), + @ApiResponse(responseCode = "403", + description = "Modify of image is not permitted", + content = {@Content( + mediaType = "application/json", + schema = @Schema(implementation = ApiErrorDto.class))}), + @ApiResponse(responseCode = "410", + description = "File was not found in the Storage Service", + content = {@Content( + mediaType = "application/json", + schema = @Schema(implementation = ApiErrorDto.class))}), + }) + public ResponseEntity<DatabaseDto> modifyImage(@NotNull @PathVariable Long id, + @Valid @RequestBody DatabaseModifyImageDto imageDto, + @NotNull Principal principal) throws DatabaseNotFoundException, + UserNotFoundException, NotAllowedException, FileStorageException { + log.debug("endpoint modify database image, id={}, imageDto={}, {}", id, imageDto, PrincipalUtil.formatForDebug(principal)); + final Database database = databaseService.findById(id); + final User user = userService.findByUsername(principal.getName()); + if (!database.getOwnedBy().equals(user.getId())) { + log.error("Failed to update database image: not owner"); + throw new NotAllowedException("Failed to update database image: not owner"); + } + final DatabaseDto dto; + byte[] image = null; + if (imageDto.getKey() != null) { + image = storageService.getBytes(imageDto.getKey()); + } + dto = databaseMapper.databaseToDatabaseDto(databaseService.modifyImage(id, image)); + log.trace("update database resulted in database {}", dto); + return ResponseEntity.accepted() + .body(dto); + } + @GetMapping("/{id}") @Transactional(readOnly = true) @Observed(name = "dbr_database_find") - @Operation(summary = "Find some database", security = @SecurityRequirement(name = "bearerAuth")) + @Operation(summary = "Find some database", security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Database found successfully", diff --git a/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/ExportEndpoint.java b/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/ExportEndpoint.java index 666b81b3935952e297412817ae7d6f8f9d22777f..184869eddc6edf41c59bc201e29d62daed81dbad 100644 --- a/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/ExportEndpoint.java +++ b/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/ExportEndpoint.java @@ -46,7 +46,7 @@ public class ExportEndpoint { @GetMapping @Transactional(readOnly = true) @Observed(name = "dbr_table_export") - @Operation(summary = "Export table", security = @SecurityRequirement(name = "bearerAuth")) + @Operation(summary = "Export table", security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) @ApiResponses(value = { @ApiResponse(responseCode = "201", description = "Created identifier", @@ -68,6 +68,11 @@ public class ExportEndpoint { content = {@Content( mediaType = "application/json", schema = @Schema(implementation = ApiErrorDto.class))}), + @ApiResponse(responseCode = "409", + description = "Failed to export file from sidecar", + content = {@Content( + mediaType = "application/json", + schema = @Schema(implementation = ApiErrorDto.class))}), @ApiResponse(responseCode = "410", description = "Blob storage operation could not be completed", content = {@Content( @@ -88,9 +93,8 @@ public class ExportEndpoint { @NotNull @PathVariable("tableId") Long tableId, @RequestParam(required = false) Instant timestamp, Principal principal) - throws TableNotFoundException, DatabaseConnectionException, TableMalformedException, - DatabaseNotFoundException, ImageNotSupportedException, PaginationException, FileStorageException, - QueryMalformedException, UserNotFoundException, NotAllowedException, DataDbSidecarException { + throws TableNotFoundException, DatabaseNotFoundException, FileStorageException, QueryMalformedException, + NotAllowedException, DataDbSidecarException, DataProcessingException { log.debug("endpoint export table, id={}, tableId={}, timestamp={}, {}", databaseId, tableId, timestamp, PrincipalUtil.formatForDebug(principal)); final Database database = databaseService.find(databaseId); if (!database.getIsPublic()) { 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 aac1e1c4ef266fed4be72521518fb5821acb084e..0af00f4c0e9b9a34779e7ed22a7e66b21ade475a 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 @@ -38,7 +38,6 @@ import org.springframework.web.bind.annotation.*; import java.security.Principal; import java.util.List; -import java.util.UUID; import java.util.stream.Collectors; @Log4j2 @@ -104,7 +103,7 @@ public class IdentifierEndpoint { @Transactional @Observed(name = "dbr_identifier_create") @PreAuthorize("hasAuthority('create-identifier') or hasAuthority('create-foreign-identifier')") - @Operation(summary = "Create identifier", security = @SecurityRequirement(name = "bearerAuth")) + @Operation(summary = "Create identifier", security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) @ApiResponses(value = { @ApiResponse(responseCode = "201", description = "Created identifier", @@ -163,6 +162,12 @@ public class IdentifierEndpoint { QueryStoreException, QueryNotFoundException, ImageNotSupportedException, UserNotFoundException, DatabaseConnectionException, RemoteUnavailableException { log.debug("endpoint create identifier, data={}, {}", data, PrincipalUtil.formatForDebug(principal)); + /* check data */ + if (!endpointValidator.validatePublicationDate(data)) { + log.error("Failed to create identifier: publication date is invalid"); + throw new IdentifierRequestException("Failed to create identifier: publication date is invalid"); + } + /* check access */ DatabaseAccess access = null; try { access = accessService.find(data.getDatabaseId(), UserUtil.getId(principal)); @@ -172,21 +177,22 @@ public class IdentifierEndpoint { throw new NotAllowedException("Failed to create identifier: insufficient role"); } } + /* create identifier */ final Database database = databaseService.find(data.getDatabaseId()); switch (data.getType()) { case VIEW -> { - if (data.getDatabaseId() == null || data.getQueryId() != null || data.getViewId() == null || data.getTableId() != null) { + if (data.getQueryId() != null || data.getViewId() == null || data.getTableId() != null) { log.error("Failed to create view identifier: only parameters database_id & view_id must be present"); throw new IdentifierRequestException("Failed to create view identifier: only parameters database_id & view_id must be present"); } - final View view = viewService.findById(data.getViewId()); + final View view = viewService.findById(data.getDatabaseId(), data.getViewId()); if (!endpointValidator.validateOnlyMineOrReadAccessOrHasRole(view.getCreatedBy(), principal, access, "create-foreign-identifier")) { log.error("Failed to create view identifier: insufficient access or role"); throw new IdentifierRequestException("Failed to create view identifier: insufficient access or role"); } } case TABLE -> { - if (data.getDatabaseId() == null || data.getQueryId() != null || data.getViewId() != null || data.getTableId() == null) { + if (data.getQueryId() != null || data.getViewId() != null || data.getTableId() == null) { log.error("Failed to create table identifier: only parameters database_id & table_id must be present"); throw new IdentifierRequestException("Failed to create table identifier: only parameters database_id & table_id must be present"); } @@ -197,7 +203,7 @@ public class IdentifierEndpoint { } } case SUBSET -> { - if (data.getDatabaseId() == null || data.getQueryId() == null || data.getViewId() != null || data.getTableId() != null) { + if (data.getQueryId() == null || data.getViewId() != null || data.getTableId() != null) { log.error("Failed to create subset identifier: only parameters database_id & query_id must be present"); throw new IdentifierRequestException("Failed to create subset identifier: only parameters database_id & query_id must be present"); } @@ -209,7 +215,7 @@ public class IdentifierEndpoint { } } case DATABASE -> { - if (data.getDatabaseId() == null || data.getQueryId() != null || data.getViewId() != null || data.getTableId() != null) { + if (data.getQueryId() != null || data.getViewId() != null || data.getTableId() != null) { log.error("Failed to create database identifier: only parameters database_id must be present"); throw new IdentifierRequestException("Failed to create database identifier: only parameters database_id must be present"); } diff --git a/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/ImageEndpoint.java b/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/ImageEndpoint.java index 1ac1988a2aca03557e63d19c884001bd8da06476..2544d36f15b975e1c26356745e59760ada1a3cea 100644 --- a/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/ImageEndpoint.java +++ b/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/ImageEndpoint.java @@ -75,7 +75,7 @@ public class ImageEndpoint { @Transactional @Observed(name = "dbr_image_create") @PreAuthorize("hasAuthority('create-image')") - @Operation(summary = "Create image", security = @SecurityRequirement(name = "bearerAuth")) + @Operation(summary = "Create image", security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) @ApiResponses(value = { @ApiResponse(responseCode = "201", description = "Created image", @@ -137,7 +137,7 @@ public class ImageEndpoint { @Transactional @Observed(name = "dbr_image_update") @PreAuthorize("hasAuthority('modify-image')") - @Operation(summary = "Update some image", security = @SecurityRequirement(name = "bearerAuth")) + @Operation(summary = "Update some image", security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) @ApiResponses(value = { @ApiResponse(responseCode = "202", description = "Updated image successfully", @@ -166,7 +166,7 @@ public class ImageEndpoint { @Transactional @Observed(name = "dbr_image_delete") @PreAuthorize("hasAuthority('delete-image')") - @Operation(summary = "Delete some image", security = @SecurityRequirement(name = "bearerAuth")) + @Operation(summary = "Delete some image", security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) @ApiResponses(value = { @ApiResponse(responseCode = "202", description = "Deleted image successfully", diff --git a/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/MaintenanceEndpoint.java b/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/MaintenanceEndpoint.java index e0702cd8a5eeed8bb94102db3f46aed1f7ff5b7f..dd003742bde608ec80812659a57d5b8dbb643781 100644 --- a/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/MaintenanceEndpoint.java +++ b/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/MaintenanceEndpoint.java @@ -15,6 +15,7 @@ import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; import lombok.extern.log4j.Log4j2; @@ -106,7 +107,7 @@ public class MaintenanceEndpoint { @PostMapping("/message") @Observed(name = "dbr_maintenance_create") - @Operation(summary = "Create maintenance message") + @Operation(summary = "Create maintenance message", security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) @PreAuthorize("hasAuthority('create-maintenance-message')") @ApiResponses(value = { @ApiResponse(responseCode = "201", @@ -125,7 +126,7 @@ public class MaintenanceEndpoint { @PutMapping("/message/{id}") @Observed(name = "dbr_maintenance_update") - @Operation(summary = "Update maintenance message") + @Operation(summary = "Update maintenance message", security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) @PreAuthorize("hasAuthority('update-maintenance-message')") @ApiResponses(value = { @ApiResponse(responseCode = "202", @@ -151,7 +152,7 @@ public class MaintenanceEndpoint { @DeleteMapping("/message/{id}") @Observed(name = "dbr_maintenance_delete") - @Operation(summary = "Delete maintenance message") + @Operation(summary = "Delete maintenance message", security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) @PreAuthorize("hasAuthority('delete-maintenance-message')") @ApiResponses(value = { @ApiResponse(responseCode = "202", diff --git a/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/OntologyEndpoint.java b/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/OntologyEndpoint.java index b03d183707f2767c8e9b2c7929424e4198aa3f4f..9060aacb15e8f55582d9604dfb0858ba29b0880e 100644 --- a/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/OntologyEndpoint.java +++ b/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/OntologyEndpoint.java @@ -90,7 +90,7 @@ public class OntologyEndpoint { @PostMapping @PreAuthorize("hasAuthority('create-ontology')") @Observed(name = "dbr_ontologies_create") - @Operation(summary = "Register a new ontology", security = @SecurityRequirement(name = "bearerAuth")) + @Operation(summary = "Register a new ontology", security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) @ApiResponses(value = { @ApiResponse(responseCode = "201", description = "Registered ontology successfully", @@ -110,7 +110,7 @@ public class OntologyEndpoint { @PutMapping("/{id}") @PreAuthorize("hasAuthority('update-ontology')") @Observed(name = "dbr_ontologies_update") - @Operation(summary = "Update an ontology", security = @SecurityRequirement(name = "bearerAuth")) + @Operation(summary = "Update an ontology", security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) @ApiResponses(value = { @ApiResponse(responseCode = "202", description = "Updated ontology successfully", @@ -136,7 +136,7 @@ public class OntologyEndpoint { @DeleteMapping("/{id}") @PreAuthorize("hasAuthority('delete-ontology')") @Observed(name = "dbr_ontologies_delete") - @Operation(summary = "Delete an ontology", security = @SecurityRequirement(name = "bearerAuth")) + @Operation(summary = "Delete an ontology", security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) @ApiResponses(value = { @ApiResponse(responseCode = "202", description = "Deleted ontology successfully", @@ -158,7 +158,7 @@ public class OntologyEndpoint { @GetMapping("/{id}/entity") @PreAuthorize("hasAuthority('execute-semantic-query')") @Observed(name = "dbr_ontologies_entities_find") - @Operation(summary = "Find entities", security = @SecurityRequirement(name = "bearerAuth")) + @Operation(summary = "Find entities", security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Found entities", diff --git a/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/PersistenceEndpoint.java b/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/PersistenceEndpoint.java index a8d46160496c2144e3a92544ffcfed95af0eb7dc..8b588ab3e22dac539b31e175a3049f44a93e9a79 100644 --- a/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/PersistenceEndpoint.java +++ b/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/PersistenceEndpoint.java @@ -102,7 +102,7 @@ public class PersistenceEndpoint { @NotNull Principal principal) throws IdentifierNotFoundException, QueryNotFoundException, IdentifierRequestException, UserNotFoundException, QueryStoreException, TableMalformedException, DatabaseConnectionException, QueryMalformedException, DatabaseNotFoundException, - ImageNotSupportedException, FileStorageException, DataDbSidecarException { + ImageNotSupportedException, FileStorageException, DataDbSidecarException, DataProcessingException { log.debug("endpoint find identifier, pid={}, accept={}", pid, accept); final Identifier identifier = identifierService.find(pid); log.info("Found persistent identifier with id {}", identifier.getId()); @@ -163,7 +163,7 @@ public class PersistenceEndpoint { @Transactional @Observed(name = "dbr_pid_delete") @PreAuthorize("hasAuthority('delete-identifier')") - @Operation(summary = "Delete some identifier", security = @SecurityRequirement(name = "bearerAuth")) + @Operation(summary = "Delete some identifier", security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) @ApiResponses(value = { @ApiResponse(responseCode = "202", description = "Deleted identifier"), diff --git a/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/QueryEndpoint.java b/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/QueryEndpoint.java index 0b1c053865683fc0a2ed78cf896c792ea6d2530f..f46afa62cc9ec15f135f939f5e2436acb2fb313e 100644 --- a/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/QueryEndpoint.java +++ b/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/QueryEndpoint.java @@ -57,7 +57,7 @@ public class QueryEndpoint { @Transactional(readOnly = true) @Observed(name = "dbr_query_execute") @PreAuthorize("hasAuthority('execute-query')") - @Operation(summary = "Execute query", security = @SecurityRequirement(name = "bearerAuth")) + @Operation(summary = "Execute query", security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) @ApiResponses(value = { @ApiResponse(responseCode = "202", description = "Executed query", @@ -121,7 +121,7 @@ public class QueryEndpoint { @GetMapping("/{queryId}/data") @Transactional(readOnly = true) @Observed(name = "dbr_query_reexecute") - @Operation(summary = "Re-execute some query", security = @SecurityRequirement(name = "bearerAuth")) + @Operation(summary = "Re-execute some query", security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) @ApiResponses(value = { @ApiResponse(responseCode = "202", description = "Executed query", @@ -181,7 +181,7 @@ public class QueryEndpoint { @GetMapping("/{queryId}/data/count") @Transactional(readOnly = true) @Observed(name = "dbr_query_reexecute_count") - @Operation(summary = "Re-execute some query", security = @SecurityRequirement(name = "bearerAuth")) + @Operation(summary = "Re-execute some query", security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) @ApiResponses(value = { @ApiResponse(responseCode = "202", description = "Executed query", @@ -228,7 +228,7 @@ public class QueryEndpoint { @GetMapping("/{queryId}/export") @Transactional(readOnly = true) @Observed(name = "dbr_query_export") - @Operation(summary = "Exports some query", security = @SecurityRequirement(name = "bearerAuth")) + @Operation(summary = "Exports some query", security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Executed query"), @@ -268,7 +268,7 @@ public class QueryEndpoint { @RequestHeader(HttpHeaders.ACCEPT) String accept, Principal principal) throws QueryStoreException, QueryNotFoundException, DatabaseNotFoundException, ImageNotSupportedException, - FileStorageException, QueryMalformedException, NotAllowedException, DataDbSidecarException { + FileStorageException, QueryMalformedException, NotAllowedException, DataDbSidecarException, DataProcessingException { log.debug("endpoint export query, databaseId={}, queryId={}, accept={}, {}", databaseId, queryId, accept, PrincipalUtil.formatForDebug(principal)); final Database database = databaseService.find(databaseId); if (!database.getIsPublic()) { diff --git a/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/SemanticsEndpoint.java b/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/SemanticsEndpoint.java index 1fb5b883e6481cfbf5eacc56eb1281c82f271cfc..46c418be905b9d786cc6a08143ca2c45c8e5604e 100644 --- a/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/SemanticsEndpoint.java +++ b/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/SemanticsEndpoint.java @@ -93,7 +93,7 @@ public class SemanticsEndpoint { @Transactional(readOnly = true) @PreAuthorize("hasAuthority('table-semantic-analyse')") @Observed(name = "dbr_semantic_table_analyse") - @Operation(summary = "Suggest table semantics", security = @SecurityRequirement(name = "bearerAuth")) + @Operation(summary = "Suggest table semantics", security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Suggested table semantics successfully", @@ -130,7 +130,7 @@ public class SemanticsEndpoint { @Transactional(readOnly = true) @PreAuthorize("hasAuthority('table-semantic-analyse')") @Observed(name = "dbr_semantic_column_analyse") - @Operation(summary = "Suggest table column semantics", security = @SecurityRequirement(name = "bearerAuth")) + @Operation(summary = "Suggest table column semantics", security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Suggested table column semantics successfully", diff --git a/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/StoreEndpoint.java b/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/StoreEndpoint.java index 1ff758a309f0b54c2f5f48005dc525d6571b698e..d96fea7342c1be5083194dcf6f9e93b4ea5c50a2 100644 --- a/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/StoreEndpoint.java +++ b/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/StoreEndpoint.java @@ -74,7 +74,7 @@ public class StoreEndpoint { @GetMapping @Transactional(readOnly = true) @Observed(name = "dbr_queries_findall") - @Operation(summary = "Find queries", security = @SecurityRequirement(name = "bearerAuth")) + @Operation(summary = "Find queries", security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "List queries", @@ -151,7 +151,7 @@ public class StoreEndpoint { @GetMapping("/{queryId}") @Transactional(readOnly = true) @Observed(name = "dbr_queries_find") - @Operation(summary = "Find some query", security = @SecurityRequirement(name = "bearerAuth")) + @Operation(summary = "Find some query", security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "List queries", @@ -212,7 +212,7 @@ public class StoreEndpoint { @Transactional(readOnly = true) @PreAuthorize("hasAuthority('persist-query')") @Observed(name = "dbr_query_persist") - @Operation(summary = "Persist some query", security = @SecurityRequirement(name = "bearerAuth")) + @Operation(summary = "Persist some query", security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Persist query successful", diff --git a/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/TableColumnEndpoint.java b/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/TableColumnEndpoint.java index 999a66bb70f150e7fa1459724f782eca4744636c..47193508ba32c0cd0ee33091b5860f7c3380b5c7 100644 --- a/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/TableColumnEndpoint.java +++ b/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/TableColumnEndpoint.java @@ -49,7 +49,7 @@ public class TableColumnEndpoint { @Transactional @PreAuthorize("hasAuthority('modify-table-column-semantics') or hasAuthority('modify-foreign-table-column-semantics')") @Observed(name = "dbr_semantics_column_save") - @Operation(summary = "Update a table column semantic mapping", security = @SecurityRequirement(name = "bearerAuth")) + @Operation(summary = "Update a table column semantic mapping", security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) @ApiResponses(value = { @ApiResponse(responseCode = "202", description = "Updated column semantics successfully", diff --git a/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/TableDataEndpoint.java b/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/TableDataEndpoint.java index 0b1f25f0fe4dc5e105a4e3626dd186471a507cb6..fe12e3aeff6d8e7224e1b57b5f54436fc2ac1f7b 100644 --- a/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/TableDataEndpoint.java +++ b/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/TableDataEndpoint.java @@ -5,7 +5,6 @@ import at.tuwien.api.database.query.ImportDto; import at.tuwien.api.database.query.QueryResultDto; import at.tuwien.api.database.table.TableCsvDeleteDto; import at.tuwien.api.database.table.TableCsvDto; -import at.tuwien.api.database.table.columns.ColumnDto; import at.tuwien.api.error.ApiErrorDto; import at.tuwien.entities.database.Database; import at.tuwien.exception.*; @@ -15,6 +14,7 @@ import at.tuwien.utils.PrincipalUtil; import at.tuwien.utils.UserUtil; import at.tuwien.validation.EndpointValidator; import io.micrometer.observation.annotation.Observed; +import io.swagger.v3.oas.annotations.ExternalDocumentation; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; @@ -55,7 +55,8 @@ public class TableDataEndpoint { @Transactional @Observed(name = "dbr_table_data_insert") @PreAuthorize("hasAuthority('insert-table-data')") - @Operation(summary = "Insert data", security = @SecurityRequirement(name = "bearerAuth")) + @Operation(summary = "Insert data", description = "Insert data directly as key-value map tuple", + security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) @ApiResponses(value = { @ApiResponse(responseCode = "202", description = "Inserted data successfully"), @@ -94,7 +95,8 @@ public class TableDataEndpoint { @Transactional @PreAuthorize("hasAuthority('delete-table-data')") @Observed(name = "dbr_table_data_delete") - @Operation(summary = "Delete data", security = @SecurityRequirement(name = "bearerAuth")) + @Operation(summary = "Delete data", description = "Delete a tuples that match a key-value map", + security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) @ApiResponses(value = { @ApiResponse(responseCode = "202", description = "Deleted table data successfully"), @@ -133,7 +135,7 @@ public class TableDataEndpoint { @Transactional @PreAuthorize("hasAuthority('insert-table-data')") @Observed(name = "dbr_table_data_import") - @Operation(summary = "Insert data from csv", security = @SecurityRequirement(name = "bearerAuth")) + @Operation(summary = "Insert data from csv", security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) @ApiResponses(value = { @ApiResponse(responseCode = "202", description = "Import table data successfully"), @@ -152,6 +154,11 @@ public class TableDataEndpoint { content = {@Content( mediaType = "application/json", schema = @Schema(implementation = ApiErrorDto.class))}), + @ApiResponse(responseCode = "409", + description = "Import failed in sidecar", + content = {@Content( + mediaType = "application/json", + schema = @Schema(implementation = ApiErrorDto.class))}), @ApiResponse(responseCode = "422", description = "Could not import csv via sidecar", content = {@Content( @@ -163,7 +170,7 @@ public class TableDataEndpoint { @NotNull @Valid @RequestBody ImportDto data, @NotNull Principal principal) throws TableNotFoundException, DatabaseNotFoundException, TableMalformedException, - NotAllowedException, AccessDeniedException, DataDbSidecarException { + NotAllowedException, AccessDeniedException, DataDbSidecarException, DataProcessingException { log.debug("endpoint insert data from csv, databaseId={}, tableId={}, data={}, {}", databaseId, tableId, data, PrincipalUtil.formatForDebug(principal)); /* check */ endpointValidator.validateOnlyWriteOwnOrWriteAllAccess(databaseId, tableId, principal); @@ -176,7 +183,7 @@ public class TableDataEndpoint { @RequestMapping(method = {RequestMethod.GET, RequestMethod.HEAD}) @Transactional(readOnly = true) @Observed(name = "dbr_table_data_findall") - @Operation(summary = "Find data", security = @SecurityRequirement(name = "bearerAuth")) + @Operation(summary = "Find data", security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) @ApiResponses(value = { @ApiResponse(responseCode = "202", description = "Get table data successfully"), @@ -232,7 +239,7 @@ public class TableDataEndpoint { @GetMapping("/count") @Transactional(readOnly = true) @Observed(name = "dbr_table_data_countall") - @Operation(summary = "Find data", security = @SecurityRequirement(name = "bearerAuth")) + @Operation(summary = "Find data", security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) @ApiResponses(value = { @ApiResponse(responseCode = "202", description = "Get table data count successfully"), 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 e3be34e34746452667def2da8ef63d937d6fdf62..a7f88258b70956bd5417eda11a0751f3bfcfd188 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 @@ -61,7 +61,7 @@ public class TableEndpoint { @GetMapping @Transactional(readOnly = true) @Observed(name = "dbr_tables_findall") - @Operation(summary = "List all tables", security = @SecurityRequirement(name = "bearerAuth")) + @Operation(summary = "List all tables", security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "List tables", @@ -97,7 +97,7 @@ public class TableEndpoint { @Transactional @PreAuthorize("hasAuthority('create-table')") @Observed(name = "dbr_table_create") - @Operation(summary = "Create a table", security = @SecurityRequirement(name = "bearerAuth")) + @Operation(summary = "Create a table", security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) @ApiResponses(value = { @ApiResponse(responseCode = "201", description = "Created a new table", @@ -150,7 +150,7 @@ public class TableEndpoint { @GetMapping("/{tableId}") @Transactional(readOnly = true) @Observed(name = "dbr_tables_find") - @Operation(summary = "Get information about table", security = @SecurityRequirement(name = "bearerAuth")) + @Operation(summary = "Get information about table", security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Find table successfully", @@ -193,7 +193,7 @@ public class TableEndpoint { @Transactional @PreAuthorize("hasAuthority('delete-table') or hasAuthority('delete-foreign-table')") @Observed(name = "dbr_table_delete") - @Operation(summary = "Delete a table", security = @SecurityRequirement(name = "bearerAuth")) + @Operation(summary = "Delete a table", security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) @ApiResponses(value = { @ApiResponse(responseCode = "202", description = "Delete table successfully", diff --git a/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/TableHistoryEndpoint.java b/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/TableHistoryEndpoint.java index 727b3f69c3c27d8e1a652445dcbd753bc9f37ddd..dcf9e9199a3d47c3f30510acfb98e043ce1572dc 100644 --- a/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/TableHistoryEndpoint.java +++ b/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/TableHistoryEndpoint.java @@ -39,7 +39,7 @@ public class TableHistoryEndpoint { @RequestMapping(method = {RequestMethod.GET, RequestMethod.HEAD}) @Transactional(readOnly = true) @Observed(name = "dbr_table_history_findall") - @Operation(summary = "Find all history", security = @SecurityRequirement(name = "bearerAuth")) + @Operation(summary = "Find all history", security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Find table history successfully", 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 4b9418005bfd9df475992e37fbff6c1978c371d7..aa63d1110c9ce03a88d4f9143f909342a910eaaa 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 @@ -147,7 +147,7 @@ public class UserEndpoint { @Transactional @PreAuthorize("isAuthenticated() or hasAuthority('find-user')") @Observed(name = "dbr_user_find") - @Operation(summary = "Get a user info", security = @SecurityRequirement(name = "bearerAuth")) + @Operation(summary = "Get a user info", security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Found user", @@ -189,7 +189,7 @@ public class UserEndpoint { @Transactional @PreAuthorize("hasAuthority('modify-user-information')") @Observed(name = "dbr_user_modify") - @Operation(summary = "Modify user information", security = @SecurityRequirement(name = "bearerAuth")) + @Operation(summary = "Modify user information", security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) @ApiResponses(value = { @ApiResponse(responseCode = "202", description = "Modified user information", @@ -240,7 +240,7 @@ public class UserEndpoint { @Transactional @PreAuthorize("hasAuthority('modify-user-theme')") @Observed(name = "dbr_user_theme_modify") - @Operation(summary = "Modify user theme", security = @SecurityRequirement(name = "bearerAuth")) + @Operation(summary = "Modify user theme", security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) @ApiResponses(value = { @ApiResponse(responseCode = "202", description = "Modified user theme", @@ -285,7 +285,7 @@ public class UserEndpoint { @Transactional @PreAuthorize("isAuthenticated()") @Observed(name = "dbr_user_password_modify") - @Operation(summary = "Modify user password", security = @SecurityRequirement(name = "bearerAuth")) + @Operation(summary = "Modify user password", security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) @ApiResponses(value = { @ApiResponse(responseCode = "202", description = "Modified user password", 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 fd379d6afd4468a6cd0fda52b8f349425335c3a6..f3a1b76c06de61b3867fac1c1415f5034d179b21 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 @@ -62,7 +62,7 @@ public class ViewEndpoint { @GetMapping @Transactional(readOnly = true) @Observed(name = "dbr_views_findall") - @Operation(summary = "Find all views", security = @SecurityRequirement(name = "bearerAuth")) + @Operation(summary = "Find all views", security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Find views successfully", @@ -93,7 +93,7 @@ public class ViewEndpoint { @Transactional @PreAuthorize("hasAuthority('create-database-view')") @Observed(name = "dbr_view_create") - @Operation(summary = "Create a view", security = @SecurityRequirement(name = "bearerAuth")) + @Operation(summary = "Create a view", security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) @ApiResponses(value = { @ApiResponse(responseCode = "201", description = "Create view successfully", @@ -160,7 +160,7 @@ public class ViewEndpoint { @GetMapping("/{viewId}") @Transactional(readOnly = true) @Observed(name = "dbr_view_find") - @Operation(summary = "Find one view", security = @SecurityRequirement(name = "bearerAuth")) + @Operation(summary = "Find one view", security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Find view successfully", @@ -194,7 +194,7 @@ public class ViewEndpoint { @Transactional @PreAuthorize("hasAuthority('delete-database-view')") @Observed(name = "dbr_view_delete") - @Operation(summary = "Delete one view", security = @SecurityRequirement(name = "bearerAuth")) + @Operation(summary = "Delete one view", security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Delete view successfully", @@ -250,7 +250,7 @@ public class ViewEndpoint { @GetMapping("/{viewId}/data") @Transactional(readOnly = true) @Observed(name = "dbr_view_data_findall") - @Operation(summary = "Find view data", security = @SecurityRequirement(name = "bearerAuth")) + @Operation(summary = "Find view data", security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Find data successfully", @@ -307,7 +307,7 @@ public class ViewEndpoint { @GetMapping("/{viewId}/data/count") @Transactional(readOnly = true) @Observed(name = "dbr_view_data_count") - @Operation(summary = "Find view data count", security = @SecurityRequirement(name = "bearerAuth")) + @Operation(summary = "Find view data count", security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Count data successfully", diff --git a/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/validation/EndpointValidator.java b/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/validation/EndpointValidator.java index 5fa79263ba6230f2a1dd54483325a28bf66b9faa..6d874dbc9c9849f05ce7b90b2450550078a34c45 100644 --- a/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/validation/EndpointValidator.java +++ b/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/validation/EndpointValidator.java @@ -5,19 +5,19 @@ import at.tuwien.api.database.query.ExecuteStatementDto; import at.tuwien.api.database.table.TableCreateDto; import at.tuwien.api.database.table.columns.ColumnCreateDto; import at.tuwien.api.database.table.columns.ColumnTypeDto; +import at.tuwien.api.identifier.IdentifierSaveDto; import at.tuwien.config.QueryConfig; import at.tuwien.entities.database.AccessType; import at.tuwien.entities.database.Database; import at.tuwien.entities.database.DatabaseAccess; import at.tuwien.entities.database.table.Table; -import at.tuwien.entities.user.User; import at.tuwien.exception.*; -import at.tuwien.repository.mdb.IdentifierRepository; import at.tuwien.service.AccessService; import at.tuwien.service.DatabaseService; import at.tuwien.service.TableService; import at.tuwien.utils.UserUtil; import lombok.extern.log4j.Log4j2; +import org.apache.commons.validator.GenericValidator; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @@ -321,4 +321,34 @@ public class EndpointValidator { throw new NotAllowedException("Access not allowed: no write access for table with id " + tableId); } + /** + * Precondition: identifier.getPublicationYear() is not null + * + * @param identifier The identifier that will be created. + * @return True if the publication date is valid, false otherwise. + */ + public boolean validatePublicationDate(IdentifierSaveDto identifier) { + if (identifier.getPublicationMonth() != null && (identifier.getPublicationMonth() < 1 || identifier.getPublicationMonth() > 12)) { + log.trace("publication month {} needs to fulfill: 1 >= publicationMonth <= 12", identifier.getPublicationMonth()); + return false; + } + if (identifier.getPublicationDay() != null && (identifier.getPublicationDay() < 1 || identifier.getPublicationDay() > 31)) { + log.trace("publication day {} needs to fulfill: 1 >= publicationDay <= 31", identifier.getPublicationDay()); + return false; + } + if (identifier.getPublicationMonth() != null && identifier.getPublicationDay() != null) { + final String paddedMonth = identifier.getPublicationMonth() < 9 ? "0" + identifier.getPublicationMonth() : "" + identifier.getPublicationMonth(); + final boolean result = GenericValidator.isDate(identifier.getPublicationYear() + "-" + paddedMonth + "-" + + identifier.getPublicationDay(), "yyyy-MM-dd", true); + if (!result) { + log.trace("publication date {}-{}-{} needs to be valid", identifier.getPublicationYear(), paddedMonth, + identifier.getPublicationDay()); + return false; + } + return true; + } + log.trace("publication date is valid"); + return true; + } + } 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 5db5076f6a90d66395f3803e235b11c2aa10bb8c..1ae0d498b22b598e1d1306fdb48cf681cc778b64 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 @@ -40,7 +40,7 @@ spring: loadbalancer.ribbon.enabled: false management.endpoints.web.exposure.include: health,info,prometheus server: - port: 9099 + port: 19099 logging: pattern.console: "%d %highlight(%-5level) %msg%n" level: @@ -59,6 +59,8 @@ fda: secretAccessKey: seaweedfsadmin importBucket: dbrepo-upload exportBucket: dbrepo-download + deleteStaleFilesRate: 60 + staleSeconds: 60 jwt: issuer: http://localhost/api/auth/realms/dbrepo public_key: MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqqnHQ2BWWW9vDNLRCcxD++xZg/16oqMo/c1l+lcFEjjAIJjJp/HqrPYU/U9GvquGE6PbVFtTzW1KcKawOW+FJNOA3CGo8Q1TFEfz43B8rZpKsFbJKvQGVv1Z4HaKPvLUm7iMm8Hv91cLduuoWx6Q3DPe2vg13GKKEZe7UFghF+0T9u8EKzA/XqQ0OiICmsmYPbwvf9N3bCKsB/Y10EYmZRb8IhCoV9mmO5TxgWgiuNeCTtNCv2ePYqL/U0WvyGFW0reasIK8eg3KrAUj8DpyOgPOVBn3lBGf+3KFSYi+0bwZbJZWqbC/Xlk20Go1YfeJPRIt7ImxD27R/lNjgDO/MwIDAQAB @@ -66,6 +68,7 @@ fda: endpoint: "http://authentication-service:8080" username: fda password: fda + clientSecret: MUwRc7yfXSJwX8AdRMWaQC3Nep1VjwgG unsupported: \*,AVG,BIT_AND,BIT_OR,BIT_XOR,COUNT,COUNTDISTINCT,GROUP_CONCAT,JSON_ARRAYAGG,JSON_OBJECTAGG,MAX,MIN,STD,STDDEV,STDDEV_POP,STDDEV_SAMP,SUM,VARIANCE,VAR_POP,VAR_SAMP,-- website: http://localhost minConcurrent: 1 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 341ba025441566c796dd29d1bd945ffc1b344774..994ce611d0ad06e414b08121276ef2b967ce3768 100644 --- a/dbrepo-metadata-service/rest-service/src/main/resources/application.yml +++ b/dbrepo-metadata-service/rest-service/src/main/resources/application.yml @@ -72,6 +72,8 @@ fda: secretAccessKey: "${S3_SECRET_ACCESS_KEY}" importBucket: "${S3_IMPORT_BUCKET}" exportBucket: "${S3_EXPORT_BUCKET}" + deleteStaleFilesRate: "${DELETE_STALE_FILES_RATE}" + staleSeconds: 3600 jwt: issuer: "${JWT_ISSUER}" public_key: "${JWT_PUBKEY}" @@ -79,6 +81,7 @@ fda: endpoint: "${KEYCLOAK_HOST}" username: "${KEYCLOAK_ADMIN}" password: "${KEYCLOAK_ADMIN_PASSWORD}" + clientSecret: "${KEYCLOAK_CLIENT_SECRET}" unsupported: "${NOT_SUPPORTED_KEYWORDS}" website: "${WEBSITE}" minConcurrent: "${MIN_CONCURRENT_CONSUMERS}" diff --git a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/annotations/MockListeners.java b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/annotations/MockListeners.java index e5b7427ad085326da95a4c0e5335703823756cf7..14fb3972ef071a53f3fc2e00e598016e3673b03b 100644 --- a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/annotations/MockListeners.java +++ b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/annotations/MockListeners.java @@ -2,6 +2,7 @@ package at.tuwien.annotations; import at.tuwien.listener.DatabaseListener; import at.tuwien.listener.MirrorListener; +import at.tuwien.listener.StorageListener; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.boot.test.mock.mockito.MockBeans; @@ -12,6 +13,6 @@ import java.lang.annotation.Target; @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) -@MockBeans({@MockBean(DatabaseListener.class), @MockBean(MirrorListener.class)}) +@MockBeans({@MockBean(DatabaseListener.class), @MockBean(MirrorListener.class), @MockBean(StorageListener.class)}) public @interface MockListeners { } diff --git a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/config/S3Config.java b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/config/S3Config.java index 98ed8d5a219b848a54bcfcf197dc164736806076..7ecd9496e43fe13bb2adb77f0fa2cd432b13a807 100644 --- a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/config/S3Config.java +++ b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/config/S3Config.java @@ -1,20 +1,17 @@ package at.tuwien.config; -import io.minio.BucketExistsArgs; -import io.minio.MakeBucketArgs; -import io.minio.MinioClient; -import io.minio.UploadObjectArgs; +import io.minio.*; +import io.minio.errors.*; import lombok.Getter; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.io.FileUtils; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.io.File; import java.io.IOException; -import java.nio.charset.Charset; -import java.nio.file.Files; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; @Slf4j @Getter @@ -36,6 +33,9 @@ public class S3Config { @Value("${fda.s3.exportBucket}") private String s3ExportBucket; + @Value("${fda.s3.staleSeconds}") + private Integer staleSeconds; + @Bean public MinioClient minioClient() { return MinioClient.builder() @@ -74,6 +74,18 @@ public class S3Config { } } + public boolean objectExists(String bucket, String key) { + try { + final StatObjectResponse response = minioClient().statObject(StatObjectArgs.builder() + .object(key) + .bucket(bucket) + .build()); + return true; + } catch (Exception e) { + return false; + } + } + public void uploadFile(String bucket, String filepath, String filename) throws IOException { final File file = new File(filepath); if (!file.exists()) { diff --git a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/endpoints/DatabaseEndpointIntegrationTest.java b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/endpoints/DatabaseEndpointIntegrationTest.java deleted file mode 100644 index 221cc92b70bd35d53d41f434511d60bf9a8d481d..0000000000000000000000000000000000000000 --- a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/endpoints/DatabaseEndpointIntegrationTest.java +++ /dev/null @@ -1,107 +0,0 @@ -package at.tuwien.endpoints; - -import at.tuwien.BaseUnitTest; -import at.tuwien.annotations.MockAmqp; -import at.tuwien.annotations.MockOpensearch; -import at.tuwien.api.database.*; -import at.tuwien.config.MariaDbContainerConfig; -import at.tuwien.exception.*; -import at.tuwien.repository.mdb.ContainerRepository; -import at.tuwien.repository.mdb.ImageRepository; -import at.tuwien.repository.mdb.UserRepository; -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.context.SpringBootTest; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -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.testcontainers.containers.MariaDBContainer; -import org.testcontainers.containers.RabbitMQContainer; -import org.testcontainers.images.PullPolicy; -import org.testcontainers.junit.jupiter.Container; -import org.testcontainers.junit.jupiter.Testcontainers; - -import java.util.Set; - -import static org.junit.jupiter.api.Assertions.*; - -@Log4j2 -@SpringBootTest -@ExtendWith(SpringExtension.class) -@Testcontainers -@MockAmqp -@MockOpensearch -public class DatabaseEndpointIntegrationTest extends BaseUnitTest { - - @Autowired - private ImageRepository imageRepository; - - @Autowired - private ContainerRepository containerRepository; - - @Autowired - private UserRepository userRepository; - - @Autowired - private DatabaseEndpoint databaseEndpoint; - - @Container - private static final RabbitMQContainer rabbitContainer = new RabbitMQContainer("rabbitmq:3-management") - .withUser(USER_1_USERNAME, USER_1_PASSWORD, Set.of("administrator")) - .withVhost("dbrepo"); - - @Container - private static MariaDBContainer<?> mariaDBContainer = MariaDbContainerConfig.getContainer(); - - @Container - private static KeycloakContainer keycloakContainer = new KeycloakContainer("quay.io/keycloak/keycloak:21.0") - .withImagePullPolicy(PullPolicy.alwaysPull()) - .withAdminUsername("fda") - .withAdminPassword("fda") - .withRealmImportFile("./dbrepo-realm.json") - .withEnv("KC_HOSTNAME_STRICT_HTTPS", "false"); - - @DynamicPropertySource - static void keycloakProperties(DynamicPropertyRegistry registry) { - registry.add("fda.keycloak.endpoint", () -> "http://localhost:" + keycloakContainer.getMappedPort(8080)); - registry.add("fda.broker.endpoint", 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 - public void beforeEach() { - imageRepository.save(IMAGE_1); - containerRepository.save(CONTAINER_1); - userRepository.save(USER_1); - } - - @Test - @WithMockUser(username = USER_1_USERNAME, authorities = {"create-database"}) - public void create_succeeds() throws UserNotFoundException, BrokerVirtualHostGrantException, - DatabaseNameExistsException, NotAllowedException, ContainerConnectionException, DatabaseMalformedException, - QueryStoreException, DatabaseConnectionException, QueryMalformedException, DatabaseNotFoundException, - ImageNotSupportedException, AmqpException, BrokerVirtualHostModificationException, ContainerNotFoundException, - KeycloakRemoteException, AccessDeniedException, BrokerRemoteException { - final DatabaseCreateDto request = DatabaseCreateDto.builder() - .cid(CONTAINER_1_ID) - .name(DATABASE_1_NAME) - .isPublic(DATABASE_1_PUBLIC) - .build(); - - /* test */ - final ResponseEntity<DatabaseBriefDto> response = databaseEndpoint.create(request, USER_1_PRINCIPAL); - assertEquals(HttpStatus.CREATED, response.getStatusCode()); - assertNotNull(response.getBody()); - } - -} 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 2b6b480b598ef1116d6fa962470a9a675d6dd5c4..1fc625b8dc7a911c458c7a5fdca5c1394966a776 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 @@ -16,7 +16,12 @@ import at.tuwien.service.ContainerService; import at.tuwien.service.MessageQueueService; import at.tuwien.service.QueryStoreService; import at.tuwien.service.impl.MariaDbServiceImpl; +import io.minio.GetObjectArgs; +import io.minio.GetObjectResponse; +import io.minio.MinioClient; +import io.minio.errors.*; import lombok.extern.log4j.Log4j2; +import okhttp3.Headers; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; @@ -28,6 +33,10 @@ import org.springframework.security.test.context.support.WithAnonymousUser; import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.context.junit.jupiter.SpringExtension; +import java.io.IOException; +import java.io.InputStream; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; import java.security.Principal; import java.util.List; import java.util.Optional; @@ -71,6 +80,9 @@ public class DatabaseEndpointUnitTest extends BaseUnitTest { @MockBean private UserRepository userRepository; + @MockBean + private MinioClient minioClient; + @Autowired private DatabaseEndpoint databaseEndpoint; @@ -275,6 +287,41 @@ public class DatabaseEndpointUnitTest extends BaseUnitTest { }); } + @Test + @WithMockUser(username = USER_4_USERNAME) + public void modifyImage_noRole_fails() { + final DatabaseModifyImageDto request = DatabaseModifyImageDto.builder() + .key("s3key_here") + .build(); + + /* test */ + assertThrows(org.springframework.security.access.AccessDeniedException.class, () -> { + databaseEndpoint.modifyImage(DATABASE_3_ID, request, USER_4_PRINCIPAL); + }); + } + + @Test + @WithMockUser(username = USER_1_USERNAME, authorities = {"modify-database-image"}) + public void modifyImage_hasRole_succeeds() throws UserNotFoundException, DatabaseNotFoundException, + NotAllowedException, IOException, FileStorageException, ServerException, InsufficientDataException, + ErrorResponseException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, + XmlParserException, InternalException { + final DatabaseModifyImageDto request = DatabaseModifyImageDto.builder() + .key("s3key_here") + .build(); + + /* mock */ + when(databaseService.findById(DATABASE_1_ID)) + .thenReturn(DATABASE_1); + when(minioClient.getObject(any(GetObjectArgs.class))) + .thenReturn(new GetObjectResponse(Headers.of(), "dbrepo-upload", "default", "object", InputStream.nullInputStream())); + when(userRepository.findByUsername(USER_1_USERNAME)) + .thenReturn(Optional.of(USER_1)); + + /* test */ + databaseEndpoint.modifyImage(DATABASE_1_ID, request, USER_1_PRINCIPAL); + } + @Test @WithMockUser(username = USER_4_USERNAME) public void transfer_noRole_fails() { diff --git a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/endpoints/ExportEndpointUnitTest.java b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/endpoints/ExportEndpointUnitTest.java index 53a91d996ed9be62ffd8a7055b8375388e42a726..c31415cb42b037cd1aaf51f9544232a6e2d403ea 100644 --- a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/endpoints/ExportEndpointUnitTest.java +++ b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/endpoints/ExportEndpointUnitTest.java @@ -5,7 +5,6 @@ import at.tuwien.ExportResource; import at.tuwien.annotations.MockAmqp; import at.tuwien.annotations.MockOpensearch; import at.tuwien.entities.database.Database; -import at.tuwien.entities.database.DatabaseAccess; import at.tuwien.exception.*; import at.tuwien.service.DatabaseService; import at.tuwien.service.QueryService; @@ -28,7 +27,6 @@ import java.io.IOException; import java.security.Principal; import java.time.Instant; import java.time.temporal.ChronoUnit; -import java.util.UUID; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.when; @@ -55,28 +53,28 @@ public class ExportEndpointUnitTest extends BaseUnitTest { /* test */ assertThrows(NotAllowedException.class, () -> { - export_generic(DATABASE_1_ID, TABLE_1_ID, DATABASE_1, null, null, null, null); + export_generic(DATABASE_1_ID, TABLE_1_ID, DATABASE_1, null, null); }); } @Test @WithMockUser(username = USER_1_USERNAME, authorities = {"export-table-data"}) - public void export_publicHasRoleNoAccess_succeeds() throws TableNotFoundException, DatabaseConnectionException, - TableMalformedException, DatabaseNotFoundException, ImageNotSupportedException, FileStorageException, - PaginationException, NotAllowedException, QueryMalformedException, UserNotFoundException, IOException { + public void export_publicHasRoleNoAccess_succeeds() throws TableNotFoundException, NotAllowedException, + QueryMalformedException, DatabaseNotFoundException, IOException, FileStorageException, + DataProcessingException { /* test */ - export_generic(DATABASE_1_ID, TABLE_1_ID, DATABASE_1, null, USER_1_PRINCIPAL, USER_1_ID, null); + export_generic(DATABASE_1_ID, TABLE_1_ID, DATABASE_1, null, USER_1_PRINCIPAL); } @Test @WithMockUser(username = USER_1_USERNAME, authorities = {"export-table-data"}) - public void export_publicHasRoleReadAccess_succeeds() throws TableNotFoundException, DatabaseConnectionException, - TableMalformedException, DatabaseNotFoundException, ImageNotSupportedException, FileStorageException, - PaginationException, NotAllowedException, QueryMalformedException, UserNotFoundException, IOException { + public void export_publicHasRoleReadAccess_succeeds() throws TableNotFoundException, NotAllowedException, + QueryMalformedException, DatabaseNotFoundException, IOException, FileStorageException, + DataProcessingException { /* test */ - export_generic(DATABASE_1_ID, TABLE_1_ID, DATABASE_1, null, USER_1_PRINCIPAL, USER_1_ID, DATABASE_1_USER_1_READ_ACCESS); + export_generic(DATABASE_1_ID, TABLE_1_ID, DATABASE_1, null, USER_1_PRINCIPAL); } @Test @@ -86,7 +84,7 @@ public class ExportEndpointUnitTest extends BaseUnitTest { /* test */ assertThrows(NotAllowedException.class, () -> { - export_generic(DATABASE_1_ID, TABLE_1_ID, DATABASE_1, timestamp, null, null, null); + export_generic(DATABASE_1_ID, TABLE_1_ID, DATABASE_1, timestamp, null); }); } @@ -96,7 +94,7 @@ public class ExportEndpointUnitTest extends BaseUnitTest { /* test */ assertThrows(NotAllowedException.class, () -> { - export_generic(DATABASE_1_ID, TABLE_1_ID, DATABASE_1, timestamp, null, null, null); + export_generic(DATABASE_1_ID, TABLE_1_ID, DATABASE_1, timestamp, null); }); } @@ -110,50 +108,50 @@ public class ExportEndpointUnitTest extends BaseUnitTest { /* test */ assertThrows(NotAllowedException.class, () -> { - export_generic(DATABASE_2_ID, TABLE_1_ID, DATABASE_2, null, null, null, null); + export_generic(DATABASE_2_ID, TABLE_1_ID, DATABASE_2, null, null); }); } @Test @WithMockUser(username = USER_2_USERNAME, authorities = {"export-table-data"}) - public void export_privateHasRoleNoAccess_fails() throws TableNotFoundException, DatabaseConnectionException, - TableMalformedException, DatabaseNotFoundException, ImageNotSupportedException, FileStorageException, - PaginationException, NotAllowedException, QueryMalformedException, UserNotFoundException, IOException { + public void export_privateHasRoleNoAccess_fails() throws TableNotFoundException, NotAllowedException, + QueryMalformedException, DatabaseNotFoundException, IOException, FileStorageException, + DataProcessingException { /* test */ - export_generic(DATABASE_2_ID, TABLE_1_ID, DATABASE_2, null, USER_2_PRINCIPAL, USER_2_ID, null); + export_generic(DATABASE_2_ID, TABLE_1_ID, DATABASE_2, null, USER_2_PRINCIPAL); } @Test @WithMockUser(username = USER_2_USERNAME, authorities = {"export-table-data"}) - public void export_HasRoleReadAccess_succeeds() throws TableNotFoundException, DatabaseConnectionException, - TableMalformedException, DatabaseNotFoundException, ImageNotSupportedException, FileStorageException, - PaginationException, NotAllowedException, QueryMalformedException, UserNotFoundException, IOException { + public void export_HasRoleReadAccess_succeeds() throws TableNotFoundException, NotAllowedException, + QueryMalformedException, DatabaseNotFoundException, IOException, FileStorageException, + DataProcessingException { /* test */ - export_generic(DATABASE_2_ID, TABLE_1_ID, DATABASE_2, null, USER_2_PRINCIPAL, USER_2_ID, DATABASE_2_USER_1_WRITE_OWN_ACCESS); + export_generic(DATABASE_2_ID, TABLE_1_ID, DATABASE_2, null, USER_2_PRINCIPAL); } @Test @WithMockUser(username = USER_2_USERNAME, authorities = {"export-table-data"}) - public void export_privateReadWithTimestamp_succeeds() throws TableNotFoundException, DatabaseConnectionException, - TableMalformedException, DatabaseNotFoundException, ImageNotSupportedException, FileStorageException, - PaginationException, NotAllowedException, QueryMalformedException, UserNotFoundException, IOException { + public void export_privateReadWithTimestamp_succeeds() throws TableNotFoundException, NotAllowedException, + QueryMalformedException, DatabaseNotFoundException, IOException, FileStorageException, + DataProcessingException { final Instant timestamp = Instant.now(); /* test */ - export_generic(DATABASE_2_ID, TABLE_1_ID, DATABASE_2, timestamp, USER_2_PRINCIPAL, USER_2_ID, DATABASE_2_USER_1_READ_ACCESS); + export_generic(DATABASE_2_ID, TABLE_1_ID, DATABASE_2, timestamp, USER_2_PRINCIPAL); } @Test @WithMockUser(username = USER_2_USERNAME, authorities = {"export-table-data"}) - public void export_privateReadWithTimestampInFuture_succeeds() throws TableNotFoundException, DatabaseConnectionException, - TableMalformedException, DatabaseNotFoundException, ImageNotSupportedException, FileStorageException, - PaginationException, NotAllowedException, QueryMalformedException, UserNotFoundException, IOException { + public void export_privateReadWithTimestampInFuture_succeeds() throws TableNotFoundException, NotAllowedException, + QueryMalformedException, DatabaseNotFoundException, IOException, FileStorageException, + DataProcessingException { final Instant timestamp = Instant.now().plus(10, ChronoUnit.DAYS); /* test */ - export_generic(DATABASE_2_ID, TABLE_1_ID, DATABASE_2, timestamp, USER_2_PRINCIPAL, USER_2_ID, DATABASE_2_USER_1_READ_ACCESS); + export_generic(DATABASE_2_ID, TABLE_1_ID, DATABASE_2, timestamp, USER_2_PRINCIPAL); } /* ################################################################################################### */ @@ -161,10 +159,9 @@ public class ExportEndpointUnitTest extends BaseUnitTest { /* ################################################################################################### */ protected void export_generic(Long databaseId, Long tableId, Database database, Instant timestamp, - Principal principal, UUID userId, DatabaseAccess access) throws IOException, - DatabaseNotFoundException, UserNotFoundException, TableNotFoundException, DatabaseConnectionException, - TableMalformedException, QueryMalformedException, ImageNotSupportedException, FileStorageException, - PaginationException, NotAllowedException { + Principal principal) throws IOException, + DatabaseNotFoundException, TableNotFoundException, QueryMalformedException, FileStorageException, + NotAllowedException, DataProcessingException { final ExportResource resource = ExportResource.builder() .filename("location.csv") .resource(new InputStreamResource(FileUtils.openInputStream(new File("src/test/resources/weather/location.csv")))) 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 26c180c5e21457d49e6108326e5d077a55c4d565..2b2627cc18e29d8dd69ccce06383dca4ab61a6ee 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 @@ -8,10 +8,8 @@ import at.tuwien.config.EndpointConfig; import at.tuwien.entities.database.Database; import at.tuwien.entities.database.DatabaseAccess; import at.tuwien.entities.identifier.Identifier; -import at.tuwien.entities.user.User; import at.tuwien.exception.*; import at.tuwien.repository.mdb.DatabaseRepository; -import at.tuwien.repository.mdb.IdentifierRepository; import at.tuwien.service.AccessService; import at.tuwien.service.IdentifierService; import at.tuwien.service.StoreService; @@ -38,7 +36,6 @@ import java.util.Optional; import java.util.UUID; import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.when; @@ -57,9 +54,6 @@ public class IdentifierEndpointUnitTest extends BaseUnitTest { @MockBean private UserService userService; - @MockBean - private IdentifierRepository identifierRepository; - @MockBean private AccessService accessService; @@ -82,10 +76,10 @@ public class IdentifierEndpointUnitTest extends BaseUnitTest { @Test @WithAnonymousUser - public void find_json_succeeds() throws IdentifierNotFoundException, QueryNotFoundException, - RemoteUnavailableException, IdentifierRequestException, UserNotFoundException, QueryStoreException, - TableMalformedException, DatabaseConnectionException, QueryMalformedException, DatabaseNotFoundException, - ImageNotSupportedException, FileStorageException, DataDbSidecarException { + public void find_json_succeeds() throws IdentifierNotFoundException, QueryNotFoundException, IOException, + IdentifierRequestException, UserNotFoundException, QueryStoreException, TableMalformedException, + DatabaseConnectionException, QueryMalformedException, DatabaseNotFoundException, ImageNotSupportedException, + FileStorageException, DataProcessingException { final String accept = "application/json"; /* mock */ @@ -111,10 +105,10 @@ public class IdentifierEndpointUnitTest extends BaseUnitTest { @Test @WithAnonymousUser - public void find_xml_succeeds() throws IdentifierNotFoundException, QueryNotFoundException, - RemoteUnavailableException, IdentifierRequestException, IOException, UserNotFoundException, - QueryStoreException, TableMalformedException, DatabaseConnectionException, QueryMalformedException, - DatabaseNotFoundException, ImageNotSupportedException, FileStorageException { + public void find_xml_succeeds() throws IdentifierNotFoundException, QueryNotFoundException, IOException, + IdentifierRequestException, UserNotFoundException, QueryStoreException, TableMalformedException, + DatabaseConnectionException, QueryMalformedException, DatabaseNotFoundException, ImageNotSupportedException, + FileStorageException, DataProcessingException { final InputStreamResource resource = new InputStreamResource(FileUtils.openInputStream( new File("src/test/resources/xml/datacite-example-dataset-v4.xml"))); @@ -129,10 +123,10 @@ public class IdentifierEndpointUnitTest extends BaseUnitTest { @Test @WithAnonymousUser - public void find_csv_succeeds() throws IdentifierNotFoundException, QueryNotFoundException, - RemoteUnavailableException, IOException, IdentifierRequestException, UserNotFoundException, - QueryStoreException, TableMalformedException, DatabaseConnectionException, QueryMalformedException, - DatabaseNotFoundException, ImageNotSupportedException, FileStorageException { + public void find_csv_succeeds() throws IdentifierNotFoundException, QueryNotFoundException, IOException, + IdentifierRequestException, UserNotFoundException, QueryStoreException, TableMalformedException, + DatabaseConnectionException, QueryMalformedException, DatabaseNotFoundException, ImageNotSupportedException, + FileStorageException, DataProcessingException { final InputStreamResource resource = new InputStreamResource(FileUtils.openInputStream( new File("src/test/resources/csv/testdata.csv"))); @@ -147,10 +141,10 @@ public class IdentifierEndpointUnitTest extends BaseUnitTest { @Test @WithAnonymousUser - public void find_httpRedirect_succeeds() throws IdentifierNotFoundException, QueryNotFoundException, - RemoteUnavailableException, IdentifierRequestException, UserNotFoundException, QueryStoreException, - TableMalformedException, DatabaseConnectionException, QueryMalformedException, DatabaseNotFoundException, - ImageNotSupportedException, FileStorageException, DataDbSidecarException { + public void find_httpRedirect_succeeds() throws IdentifierNotFoundException, QueryNotFoundException, IOException, + IdentifierRequestException, UserNotFoundException, QueryStoreException, TableMalformedException, + DatabaseConnectionException, QueryMalformedException, DatabaseNotFoundException, ImageNotSupportedException, + FileStorageException, DataProcessingException { /* test */ final ResponseEntity<?> response = generic_find(null, null, null); @@ -172,13 +166,10 @@ public class IdentifierEndpointUnitTest extends BaseUnitTest { @Test @WithMockUser(username = USER_1_USERNAME, authorities = {"create-identifier"}) - public void create_hasRoleDatabase_succeeds() throws IdentifierAlreadyExistsException, - UserNotFoundException, QueryNotFoundException, DatabaseNotFoundException, RemoteUnavailableException, - IdentifierPublishingNotAllowedException, IdentifierRequestException, NotAllowedException, + public void create_hasRoleDatabase_succeeds() throws UserNotFoundException, QueryNotFoundException, + DatabaseNotFoundException, RemoteUnavailableException, IdentifierRequestException, NotAllowedException, ViewNotFoundException, at.tuwien.exception.AccessDeniedException, QueryStoreException, - DatabaseConnectionException, ImageNotSupportedException, IdentifierNotFoundException, - TableNotFoundException, TableMalformedException, QueryMalformedException, FileStorageException, - DataDbSidecarException { + DatabaseConnectionException, ImageNotSupportedException, TableNotFoundException { /* test */ generic_create(DATABASE_1_ID, DATABASE_1, DATABASE_1_USER_1_READ_ACCESS, IDENTIFIER_1_DTO_REQUEST, IDENTIFIER_1, USER_1_PRINCIPAL, USER_1_ID); @@ -206,13 +197,10 @@ public class IdentifierEndpointUnitTest extends BaseUnitTest { @Test @WithMockUser(username = USER_2_USERNAME, authorities = {"create-identifier"}) - public void create_hasRoleReadAccessQuery_succeeds() throws IdentifierAlreadyExistsException, - UserNotFoundException, QueryNotFoundException, DatabaseNotFoundException, RemoteUnavailableException, - IdentifierPublishingNotAllowedException, IdentifierRequestException, NotAllowedException, - at.tuwien.exception.AccessDeniedException, ViewNotFoundException, QueryStoreException, - DatabaseConnectionException, ImageNotSupportedException, IdentifierNotFoundException, - TableNotFoundException, TableMalformedException, QueryMalformedException, FileStorageException, - DataDbSidecarException { + public void create_hasRoleReadAccessQuery_succeeds() throws UserNotFoundException, TableNotFoundException, + AccessDeniedException, QueryStoreException, NotAllowedException, DatabaseConnectionException, + QueryNotFoundException, DatabaseNotFoundException, ImageNotSupportedException, RemoteUnavailableException, + IdentifierRequestException, ViewNotFoundException { /* test */ generic_create(DATABASE_2_ID, DATABASE_2, DATABASE_2_USER_1_READ_ACCESS, IDENTIFIER_5_DTO_REQUEST, IDENTIFIER_5, USER_2_PRINCIPAL, USER_2_ID); @@ -263,6 +251,98 @@ public class IdentifierEndpointUnitTest extends BaseUnitTest { }); } + @Test + @WithMockUser(username = USER_1_USERNAME, authorities = {"create-identifier"}) + public void create_invalidView_fails() { + final IdentifierSaveDto request = IdentifierSaveDto.builder() + .tableId(1L) // <-- + .databaseId(DATABASE_1_ID) + .descriptions(List.of(IDENTIFIER_1_DESCRIPTION_1_CREATE_DTO)) + .titles(List.of(IDENTIFIER_1_TITLE_1_CREATE_DTO)) + .relatedIdentifiers(List.of(IDENTIFIER_1_RELATED_IDENTIFIER_5_CREATE_DTO)) + .publicationDay(IDENTIFIER_5_PUBLICATION_DAY) + .publicationMonth(IDENTIFIER_5_PUBLICATION_MONTH) + .publicationYear(IDENTIFIER_5_PUBLICATION_YEAR) + .creators(List.of(IDENTIFIER_5_CREATOR_1_CREATE_DTO, IDENTIFIER_5_CREATOR_2_CREATE_DTO)) + .publisher(IDENTIFIER_5_PUBLISHER) + .type(IdentifierTypeDto.VIEW) + .build(); + + /* test */ + assertThrows(IdentifierRequestException.class, () -> { + generic_create(DATABASE_1_ID, DATABASE_1, DATABASE_1_USER_1_READ_ACCESS, request, null, USER_1_PRINCIPAL, USER_1_ID); + }); + } + + @Test + @WithMockUser(username = USER_1_USERNAME, authorities = {"create-identifier"}) + public void create_viewNotFound_fails() { + final IdentifierSaveDto request = IdentifierSaveDto.builder() + .viewId(9999L) // <-- + .databaseId(DATABASE_1_ID) + .descriptions(List.of(IDENTIFIER_1_DESCRIPTION_1_CREATE_DTO)) + .titles(List.of(IDENTIFIER_1_TITLE_1_CREATE_DTO)) + .relatedIdentifiers(List.of(IDENTIFIER_1_RELATED_IDENTIFIER_5_CREATE_DTO)) + .publicationDay(IDENTIFIER_5_PUBLICATION_DAY) + .publicationMonth(IDENTIFIER_5_PUBLICATION_MONTH) + .publicationYear(IDENTIFIER_5_PUBLICATION_YEAR) + .creators(List.of(IDENTIFIER_5_CREATOR_1_CREATE_DTO, IDENTIFIER_5_CREATOR_2_CREATE_DTO)) + .publisher(IDENTIFIER_5_PUBLISHER) + .type(IdentifierTypeDto.VIEW) + .build(); + + /* test */ + assertThrows(ViewNotFoundException.class, () -> { + generic_create(DATABASE_1_ID, DATABASE_1, DATABASE_1_USER_1_READ_ACCESS, request, null, USER_1_PRINCIPAL, USER_1_ID); + }); + } + + @Test + @WithMockUser(username = USER_1_USERNAME, authorities = {"create-identifier"}) + public void create_invalidTable_fails() { + final IdentifierSaveDto request = IdentifierSaveDto.builder() + .viewId(1L) // <-- + .databaseId(DATABASE_1_ID) + .descriptions(List.of(IDENTIFIER_1_DESCRIPTION_1_CREATE_DTO)) + .titles(List.of(IDENTIFIER_1_TITLE_1_CREATE_DTO)) + .relatedIdentifiers(List.of(IDENTIFIER_1_RELATED_IDENTIFIER_5_CREATE_DTO)) + .publicationDay(IDENTIFIER_5_PUBLICATION_DAY) + .publicationMonth(IDENTIFIER_5_PUBLICATION_MONTH) + .publicationYear(IDENTIFIER_5_PUBLICATION_YEAR) + .creators(List.of(IDENTIFIER_5_CREATOR_1_CREATE_DTO, IDENTIFIER_5_CREATOR_2_CREATE_DTO)) + .publisher(IDENTIFIER_5_PUBLISHER) + .type(IdentifierTypeDto.TABLE) + .build(); + + /* test */ + assertThrows(IdentifierRequestException.class, () -> { + generic_create(DATABASE_1_ID, DATABASE_1, DATABASE_1_USER_1_READ_ACCESS, request, null, USER_1_PRINCIPAL, USER_1_ID); + }); + } + + @Test + @WithMockUser(username = USER_1_USERNAME, authorities = {"create-identifier"}) + public void create_tableNotFound_fails() { + final IdentifierSaveDto request = IdentifierSaveDto.builder() + .tableId(9999L) // <-- + .databaseId(DATABASE_1_ID) + .descriptions(List.of(IDENTIFIER_1_DESCRIPTION_1_CREATE_DTO)) + .titles(List.of(IDENTIFIER_1_TITLE_1_CREATE_DTO)) + .relatedIdentifiers(List.of(IDENTIFIER_1_RELATED_IDENTIFIER_5_CREATE_DTO)) + .publicationDay(IDENTIFIER_5_PUBLICATION_DAY) + .publicationMonth(IDENTIFIER_5_PUBLICATION_MONTH) + .publicationYear(IDENTIFIER_5_PUBLICATION_YEAR) + .creators(List.of(IDENTIFIER_5_CREATOR_1_CREATE_DTO, IDENTIFIER_5_CREATOR_2_CREATE_DTO)) + .publisher(IDENTIFIER_5_PUBLISHER) + .type(IdentifierTypeDto.TABLE) + .build(); + + /* test */ + assertThrows(TableNotFoundException.class, () -> { + generic_create(DATABASE_1_ID, DATABASE_1, DATABASE_1_USER_1_READ_ACCESS, request, null, USER_1_PRINCIPAL, USER_1_ID); + }); + } + @Test @WithMockUser(username = USER_1_USERNAME, authorities = {"create-identifier"}) public void create_queryForeign_fails() { @@ -279,11 +359,10 @@ public class IdentifierEndpointUnitTest extends BaseUnitTest { protected void generic_create(Long databaseId, Database database, DatabaseAccess access, IdentifierSaveDto data, Identifier identifier, Principal principal, UUID userId) - throws QueryNotFoundException, RemoteUnavailableException, IdentifierAlreadyExistsException, - UserNotFoundException, DatabaseNotFoundException, IdentifierPublishingNotAllowedException, + throws QueryNotFoundException, RemoteUnavailableException, UserNotFoundException, DatabaseNotFoundException, IdentifierRequestException, NotAllowedException, at.tuwien.exception.AccessDeniedException, ViewNotFoundException, QueryStoreException, DatabaseConnectionException, ImageNotSupportedException, - IdentifierNotFoundException, TableNotFoundException, TableMalformedException, QueryMalformedException, FileStorageException, DataDbSidecarException { + TableNotFoundException { /* mock */ when(databaseRepository.findById(databaseId)) @@ -316,10 +395,10 @@ public class IdentifierEndpointUnitTest extends BaseUnitTest { } protected ResponseEntity<?> generic_find(String accept, InputStreamResource resource, Principal principal) - throws IdentifierNotFoundException, QueryNotFoundException, RemoteUnavailableException, - IdentifierRequestException, UserNotFoundException, QueryStoreException, TableMalformedException, - DatabaseConnectionException, QueryMalformedException, DatabaseNotFoundException, ImageNotSupportedException, - FileStorageException, DataDbSidecarException { + throws IdentifierNotFoundException, QueryNotFoundException, IdentifierRequestException, + UserNotFoundException, QueryStoreException, TableMalformedException, DatabaseConnectionException, + QueryMalformedException, DatabaseNotFoundException, ImageNotSupportedException, FileStorageException, + DataDbSidecarException, DataProcessingException { /* mock */ when(identifierService.find(IDENTIFIER_1_ID)) diff --git a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/endpoints/ImageEndpointIntegrationTest.java b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/endpoints/ImageEndpointIntegrationTest.java deleted file mode 100644 index 7c15a099e6f00884a315c22a9360482ae3e56b41..0000000000000000000000000000000000000000 --- a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/endpoints/ImageEndpointIntegrationTest.java +++ /dev/null @@ -1,38 +0,0 @@ -package at.tuwien.endpoints; - -import at.tuwien.BaseUnitTest; -import at.tuwien.annotations.MockAmqp; -import at.tuwien.annotations.MockOpensearch; -import at.tuwien.exception.ImageAlreadyExistsException; -import at.tuwien.exception.ImageInvalidException; -import at.tuwien.exception.ImageNotFoundException; -import at.tuwien.exception.UserNotFoundException; -import lombok.extern.log4j.Log4j2; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.security.test.context.support.WithMockUser; -import org.springframework.test.context.junit.jupiter.SpringExtension; - -@Log4j2 -@ExtendWith(SpringExtension.class) -@SpringBootTest -@MockAmqp -@MockOpensearch -public class ImageEndpointIntegrationTest extends BaseUnitTest { - - @Autowired - private ImageEndpoint imageEndpoint; - - @Test - @WithMockUser(username = USER_2_USERNAME, authorities = {"create-image"}) - public void create_succeeds() throws UserNotFoundException, ImageAlreadyExistsException, - ImageNotFoundException, ImageInvalidException { - - - /* test */ - imageEndpoint.create(IMAGE_1_CREATE_DTO, USER_2_PRINCIPAL); - } - -} diff --git a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/endpoints/PersistenceEndpointUnitTest.java b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/endpoints/PersistenceEndpointUnitTest.java index ad4347d2db4d8d3de0910b2d4235a2b4dc480640..bc0bc1b066aabb257a6b2e5b861a572f2300869a 100644 --- a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/endpoints/PersistenceEndpointUnitTest.java +++ b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/endpoints/PersistenceEndpointUnitTest.java @@ -6,13 +6,9 @@ import at.tuwien.annotations.MockOpensearch; import at.tuwien.api.identifier.BibliographyTypeDto; import at.tuwien.api.identifier.CreatorDto; import at.tuwien.api.identifier.IdentifierDto; -import at.tuwien.api.identifier.IdentifierSaveDto; import at.tuwien.entities.identifier.Identifier; -import at.tuwien.entities.user.User; import at.tuwien.exception.*; -import at.tuwien.service.AccessService; import at.tuwien.service.IdentifierService; -import at.tuwien.service.UserService; import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; @@ -34,7 +30,6 @@ import java.io.File; import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; -import java.security.Principal; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.*; @@ -45,15 +40,9 @@ import static org.mockito.Mockito.*; @MockOpensearch public class PersistenceEndpointUnitTest extends BaseUnitTest { - @MockBean - private AccessService accessService; - @MockBean private IdentifierService identifierService; - @MockBean - private UserService userService; - @Autowired private ObjectMapper objectMapper; @@ -69,10 +58,10 @@ public class PersistenceEndpointUnitTest extends BaseUnitTest { @Test @WithAnonymousUser - public void find_json0_succeeds() throws IdentifierNotFoundException, QueryNotFoundException, - RemoteUnavailableException, IdentifierRequestException, IOException, UserNotFoundException, - QueryStoreException, TableMalformedException, DatabaseConnectionException, QueryMalformedException, - DatabaseNotFoundException, ImageNotSupportedException, FileStorageException { + public void find_json0_succeeds() throws IdentifierNotFoundException, QueryNotFoundException, IOException, + IdentifierRequestException, UserNotFoundException, QueryStoreException, TableMalformedException, + DatabaseConnectionException, QueryMalformedException, DatabaseNotFoundException, ImageNotSupportedException, + FileStorageException, DataProcessingException { 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); @@ -102,10 +91,10 @@ public class PersistenceEndpointUnitTest extends BaseUnitTest { @Test @WithAnonymousUser - public void find_json1_succeeds() throws IdentifierNotFoundException, QueryNotFoundException, - RemoteUnavailableException, IdentifierRequestException, IOException, UserNotFoundException, - QueryStoreException, TableMalformedException, DatabaseConnectionException, QueryMalformedException, - DatabaseNotFoundException, ImageNotSupportedException, FileStorageException { + public void find_json1_succeeds() throws IdentifierNotFoundException, QueryNotFoundException, IOException, + IdentifierRequestException, UserNotFoundException, QueryStoreException, TableMalformedException, + DatabaseConnectionException, QueryMalformedException, DatabaseNotFoundException, ImageNotSupportedException, + FileStorageException, DataProcessingException { 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); @@ -155,10 +144,10 @@ public class PersistenceEndpointUnitTest extends BaseUnitTest { @Test @WithAnonymousUser - public void find_csv_succeeds() throws IdentifierNotFoundException, QueryNotFoundException, - RemoteUnavailableException, IdentifierRequestException, IOException, UserNotFoundException, - QueryStoreException, TableMalformedException, DatabaseConnectionException, QueryMalformedException, - DatabaseNotFoundException, ImageNotSupportedException, FileStorageException { + public void find_csv_succeeds() throws IdentifierNotFoundException, QueryNotFoundException, IOException, + IdentifierRequestException, UserNotFoundException, QueryStoreException, TableMalformedException, + DatabaseConnectionException, QueryMalformedException, DatabaseNotFoundException, ImageNotSupportedException, + FileStorageException, DataProcessingException { 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"))); @@ -179,10 +168,10 @@ public class PersistenceEndpointUnitTest extends BaseUnitTest { @Test @Disabled("not testable with xml") - public void find_xml0_succeeds() throws IdentifierNotFoundException, QueryNotFoundException, - RemoteUnavailableException, IdentifierRequestException, IOException, UserNotFoundException, - QueryStoreException, TableMalformedException, DatabaseConnectionException, QueryMalformedException, - DatabaseNotFoundException, ImageNotSupportedException, FileStorageException { + public void find_xml0_succeeds() throws IdentifierNotFoundException, QueryNotFoundException, IOException, + IdentifierRequestException, UserNotFoundException, QueryStoreException, TableMalformedException, + DatabaseConnectionException, QueryMalformedException, DatabaseNotFoundException, ImageNotSupportedException, + FileStorageException, DataProcessingException { final String accept = "text/xml"; final InputStreamResource compare = new InputStreamResource(FileUtils.openInputStream(new File("src/test/resources/xml/metadata0.xml"))); @@ -200,10 +189,10 @@ public class PersistenceEndpointUnitTest extends BaseUnitTest { @Test @Disabled("not testable with xml") - public void find_xml1_succeeds() throws IdentifierNotFoundException, QueryNotFoundException, - RemoteUnavailableException, IdentifierRequestException, IOException, UserNotFoundException, - QueryStoreException, TableMalformedException, DatabaseConnectionException, QueryMalformedException, - DatabaseNotFoundException, ImageNotSupportedException, FileStorageException { + public void find_xml1_succeeds() throws IdentifierNotFoundException, QueryNotFoundException, IOException, + IdentifierRequestException, UserNotFoundException, QueryStoreException, TableMalformedException, + DatabaseConnectionException, QueryMalformedException, DatabaseNotFoundException, ImageNotSupportedException, + FileStorageException, DataProcessingException { final String accept = "text/xml"; final InputStreamResource compare = new InputStreamResource(FileUtils.openInputStream(new File("src/test/resources/xml/metadata1.xml"))); @@ -222,10 +211,10 @@ public class PersistenceEndpointUnitTest extends BaseUnitTest { @Test @WithAnonymousUser - public void find_bibliography_succeeds() throws IdentifierNotFoundException, QueryNotFoundException, - RemoteUnavailableException, IdentifierRequestException, IOException, UserNotFoundException, - QueryStoreException, TableMalformedException, DatabaseConnectionException, QueryMalformedException, - DatabaseNotFoundException, ImageNotSupportedException, FileStorageException { + public void find_bibliography_succeeds() throws IdentifierNotFoundException, QueryNotFoundException, IOException, + IdentifierRequestException, UserNotFoundException, QueryStoreException, TableMalformedException, + DatabaseConnectionException, QueryMalformedException, DatabaseNotFoundException, ImageNotSupportedException, + FileStorageException, DataProcessingException { final String accept = "text/bibliography"; final String compare = FileUtils.readFileToString(new File("src/test/resources/bibliography/style_apa1.txt"), StandardCharsets.UTF_8); @@ -247,9 +236,9 @@ public class PersistenceEndpointUnitTest extends BaseUnitTest { @Test @WithAnonymousUser public void find_bibliographyApa0_succeeds() throws IdentifierNotFoundException, QueryNotFoundException, - RemoteUnavailableException, IdentifierRequestException, IOException, UserNotFoundException, - QueryStoreException, TableMalformedException, DatabaseConnectionException, QueryMalformedException, - DatabaseNotFoundException, ImageNotSupportedException, FileStorageException { + IOException, IdentifierRequestException, UserNotFoundException, QueryStoreException, + TableMalformedException, DatabaseConnectionException, QueryMalformedException, DatabaseNotFoundException, + ImageNotSupportedException, FileStorageException, DataProcessingException { final String accept = "text/bibliography; style=apa"; final String compare = FileUtils.readFileToString(new File("src/test/resources/bibliography/style_apa0.txt"), StandardCharsets.UTF_8); @@ -271,9 +260,9 @@ public class PersistenceEndpointUnitTest extends BaseUnitTest { @Test @WithAnonymousUser public void find_bibliographyApa1_succeeds() throws IdentifierNotFoundException, QueryNotFoundException, - RemoteUnavailableException, IdentifierRequestException, IOException, UserNotFoundException, - QueryStoreException, TableMalformedException, DatabaseConnectionException, QueryMalformedException, - DatabaseNotFoundException, ImageNotSupportedException, FileStorageException { + IOException, IdentifierRequestException, UserNotFoundException, QueryStoreException, + TableMalformedException, DatabaseConnectionException, QueryMalformedException, DatabaseNotFoundException, + ImageNotSupportedException, FileStorageException, DataProcessingException { final String accept = "text/bibliography; style=apa"; final String compare = FileUtils.readFileToString(new File("src/test/resources/bibliography/style_apa1.txt"), StandardCharsets.UTF_8); @@ -295,9 +284,9 @@ public class PersistenceEndpointUnitTest extends BaseUnitTest { @Test @WithAnonymousUser public void find_bibliographyApa2_succeeds() throws IdentifierNotFoundException, QueryNotFoundException, - RemoteUnavailableException, IdentifierRequestException, IOException, UserNotFoundException, - QueryStoreException, TableMalformedException, DatabaseConnectionException, QueryMalformedException, - DatabaseNotFoundException, ImageNotSupportedException, FileStorageException { + IOException, IdentifierRequestException, UserNotFoundException, QueryStoreException, + TableMalformedException, DatabaseConnectionException, QueryMalformedException, DatabaseNotFoundException, + ImageNotSupportedException, FileStorageException, DataProcessingException { final String accept = "text/bibliography; style=apa"; final String compare = FileUtils.readFileToString(new File("src/test/resources/bibliography/style_apa2.txt"), StandardCharsets.UTF_8); @@ -319,9 +308,9 @@ public class PersistenceEndpointUnitTest extends BaseUnitTest { @Test @WithAnonymousUser public void find_bibliographyApa3_succeeds() throws IdentifierNotFoundException, QueryNotFoundException, - RemoteUnavailableException, IdentifierRequestException, IOException, UserNotFoundException, - QueryStoreException, TableMalformedException, DatabaseConnectionException, QueryMalformedException, - DatabaseNotFoundException, ImageNotSupportedException, FileStorageException { + IOException, IdentifierRequestException, UserNotFoundException, QueryStoreException, + TableMalformedException, DatabaseConnectionException, QueryMalformedException, DatabaseNotFoundException, + ImageNotSupportedException, FileStorageException, DataProcessingException { final String accept = "text/bibliography; style=apa"; final String compare = FileUtils.readFileToString(new File("src/test/resources/bibliography/style_apa3.txt"), StandardCharsets.UTF_8); @@ -343,9 +332,9 @@ public class PersistenceEndpointUnitTest extends BaseUnitTest { @Test @WithAnonymousUser public void find_bibliographyApa4_succeeds() throws IdentifierNotFoundException, QueryNotFoundException, - RemoteUnavailableException, IdentifierRequestException, IOException, UserNotFoundException, - QueryStoreException, TableMalformedException, DatabaseConnectionException, QueryMalformedException, - DatabaseNotFoundException, ImageNotSupportedException, FileStorageException { + IOException, IdentifierRequestException, UserNotFoundException, QueryStoreException, + TableMalformedException, DatabaseConnectionException, QueryMalformedException, DatabaseNotFoundException, + ImageNotSupportedException, FileStorageException, DataProcessingException { final String accept = "text/bibliography; style=apa"; final String compare = FileUtils.readFileToString(new File("src/test/resources/bibliography/style_apa4.txt"), StandardCharsets.UTF_8); @@ -367,9 +356,9 @@ public class PersistenceEndpointUnitTest extends BaseUnitTest { @Test @WithAnonymousUser public void find_bibliographyIeee0_succeeds() throws IdentifierNotFoundException, QueryNotFoundException, - RemoteUnavailableException, IdentifierRequestException, IOException, UserNotFoundException, - QueryStoreException, TableMalformedException, DatabaseConnectionException, QueryMalformedException, - DatabaseNotFoundException, ImageNotSupportedException, FileStorageException { + IOException, IdentifierRequestException, UserNotFoundException, QueryStoreException, + TableMalformedException, DatabaseConnectionException, QueryMalformedException, DatabaseNotFoundException, + ImageNotSupportedException, FileStorageException, DataProcessingException { final String accept = "text/bibliography; style=ieee"; final String compare = FileUtils.readFileToString(new File("src/test/resources/bibliography/style_ieee0.txt"), StandardCharsets.UTF_8); @@ -391,9 +380,9 @@ public class PersistenceEndpointUnitTest extends BaseUnitTest { @Test @WithAnonymousUser public void find_bibliographyIeee1_succeeds() throws IdentifierNotFoundException, QueryNotFoundException, - RemoteUnavailableException, IdentifierRequestException, IOException, UserNotFoundException, - QueryStoreException, TableMalformedException, DatabaseConnectionException, QueryMalformedException, - DatabaseNotFoundException, ImageNotSupportedException, FileStorageException { + IOException, IdentifierRequestException, UserNotFoundException, QueryStoreException, + TableMalformedException, DatabaseConnectionException, QueryMalformedException, DatabaseNotFoundException, + ImageNotSupportedException, FileStorageException, DataProcessingException { final String accept = "text/bibliography; style=ieee"; final String compare = FileUtils.readFileToString(new File("src/test/resources/bibliography/style_ieee1.txt"), StandardCharsets.UTF_8); @@ -415,9 +404,9 @@ public class PersistenceEndpointUnitTest extends BaseUnitTest { @Test @WithAnonymousUser public void find_bibliographyIeee2_succeeds() throws IdentifierNotFoundException, QueryNotFoundException, - RemoteUnavailableException, IdentifierRequestException, IOException, UserNotFoundException, - QueryStoreException, TableMalformedException, DatabaseConnectionException, QueryMalformedException, - DatabaseNotFoundException, ImageNotSupportedException, FileStorageException { + IOException, IdentifierRequestException, UserNotFoundException, QueryStoreException, + TableMalformedException, DatabaseConnectionException, QueryMalformedException, DatabaseNotFoundException, + ImageNotSupportedException, FileStorageException, DataProcessingException { final String accept = "text/bibliography; style=ieee"; final String compare = FileUtils.readFileToString(new File("src/test/resources/bibliography/style_ieee2.txt"), StandardCharsets.UTF_8); @@ -439,9 +428,9 @@ public class PersistenceEndpointUnitTest extends BaseUnitTest { @Test @WithAnonymousUser public void find_bibliographyIeee3_succeeds() throws IdentifierNotFoundException, QueryNotFoundException, - RemoteUnavailableException, IdentifierRequestException, IOException, UserNotFoundException, - QueryStoreException, TableMalformedException, DatabaseConnectionException, QueryMalformedException, - DatabaseNotFoundException, ImageNotSupportedException, FileStorageException { + IOException, IdentifierRequestException, UserNotFoundException, QueryStoreException, + TableMalformedException, DatabaseConnectionException, QueryMalformedException, DatabaseNotFoundException, + ImageNotSupportedException, FileStorageException, DataProcessingException { final String accept = "text/bibliography; style=ieee"; final String compare = FileUtils.readFileToString(new File("src/test/resources/bibliography/style_ieee3.txt"), StandardCharsets.UTF_8); @@ -463,9 +452,9 @@ public class PersistenceEndpointUnitTest extends BaseUnitTest { @Test @WithAnonymousUser public void find_bibliographyBibtex0_succeeds() throws IdentifierNotFoundException, QueryNotFoundException, - RemoteUnavailableException, IdentifierRequestException, IOException, UserNotFoundException, - QueryStoreException, TableMalformedException, DatabaseConnectionException, QueryMalformedException, - DatabaseNotFoundException, ImageNotSupportedException, FileStorageException { + IOException, IdentifierRequestException, UserNotFoundException, QueryStoreException, + TableMalformedException, DatabaseConnectionException, QueryMalformedException, DatabaseNotFoundException, + ImageNotSupportedException, FileStorageException, DataProcessingException { final String accept = "text/bibliography; style=bibtex"; final String compare = FileUtils.readFileToString(new File("src/test/resources/bibliography/style_bibtex0.txt"), StandardCharsets.UTF_8); @@ -487,9 +476,9 @@ public class PersistenceEndpointUnitTest extends BaseUnitTest { @Test @WithAnonymousUser public void find_bibliographyBibtex1_succeeds() throws IdentifierNotFoundException, QueryNotFoundException, - RemoteUnavailableException, IdentifierRequestException, IOException, UserNotFoundException, - QueryStoreException, TableMalformedException, DatabaseConnectionException, QueryMalformedException, - DatabaseNotFoundException, ImageNotSupportedException, FileStorageException { + IOException, IdentifierRequestException, UserNotFoundException, QueryStoreException, + TableMalformedException, DatabaseConnectionException, QueryMalformedException, DatabaseNotFoundException, + ImageNotSupportedException, FileStorageException, DataProcessingException { final String accept = "text/bibliography; style=bibtex"; final String compare = FileUtils.readFileToString(new File("src/test/resources/bibliography/style_bibtex1.txt"), StandardCharsets.UTF_8); @@ -511,9 +500,9 @@ public class PersistenceEndpointUnitTest extends BaseUnitTest { @Test @WithAnonymousUser public void find_bibliographyBibtex2_succeeds() throws IdentifierNotFoundException, QueryNotFoundException, - RemoteUnavailableException, IdentifierRequestException, IOException, UserNotFoundException, - QueryStoreException, TableMalformedException, DatabaseConnectionException, QueryMalformedException, - DatabaseNotFoundException, ImageNotSupportedException, FileStorageException { + IOException, IdentifierRequestException, UserNotFoundException, QueryStoreException, + TableMalformedException, DatabaseConnectionException, QueryMalformedException, DatabaseNotFoundException, + ImageNotSupportedException, FileStorageException, DataProcessingException { final String accept = "text/bibliography; style=bibtex"; final String compare = FileUtils.readFileToString(new File("src/test/resources/bibliography/style_bibtex2.txt"), StandardCharsets.UTF_8); @@ -535,9 +524,9 @@ public class PersistenceEndpointUnitTest extends BaseUnitTest { @Test @WithAnonymousUser public void find_bibliographyBibtex3_succeeds() throws IdentifierNotFoundException, QueryNotFoundException, - RemoteUnavailableException, IdentifierRequestException, IOException, UserNotFoundException, - QueryStoreException, TableMalformedException, DatabaseConnectionException, QueryMalformedException, - DatabaseNotFoundException, ImageNotSupportedException, FileStorageException { + IOException, IdentifierRequestException, UserNotFoundException, QueryStoreException, + TableMalformedException, DatabaseConnectionException, QueryMalformedException, DatabaseNotFoundException, + ImageNotSupportedException, FileStorageException, DataProcessingException { final String accept = "text/bibliography; style=bibtex"; final String compare = FileUtils.readFileToString(new File("src/test/resources/bibliography/style_bibtex3.txt"), StandardCharsets.UTF_8); diff --git a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/endpoints/QueryEndpointUnitTest.java b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/endpoints/QueryEndpointUnitTest.java index 662c12a02aab1010ad9ae82daa46126ab91631b6..f454704efec8a86aac7b733987735919a4af883e 100644 --- a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/endpoints/QueryEndpointUnitTest.java +++ b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/endpoints/QueryEndpointUnitTest.java @@ -8,12 +8,9 @@ import at.tuwien.annotations.MockOpensearch; import at.tuwien.api.database.query.ExecuteStatementDto; import at.tuwien.api.database.query.QueryResultDto; import at.tuwien.entities.database.Database; -import at.tuwien.entities.database.DatabaseAccess; import at.tuwien.exception.*; import at.tuwien.querystore.Query; -import at.tuwien.repository.mdb.ContainerRepository; import at.tuwien.repository.mdb.DatabaseRepository; -import at.tuwien.repository.mdb.ImageRepository; import at.tuwien.service.QueryService; import at.tuwien.service.StoreService; import lombok.extern.log4j.Log4j2; @@ -35,7 +32,6 @@ import java.io.IOException; import java.security.Principal; import java.util.List; import java.util.Optional; -import java.util.UUID; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.when; @@ -47,12 +43,6 @@ import static org.mockito.Mockito.when; @MockOpensearch public class QueryEndpointUnitTest extends BaseUnitTest { - @MockBean - private ImageRepository imageRepository; - - @MockBean - private ContainerRepository containerRepository; - @MockBean private DatabaseRepository databaseRepository; @@ -72,29 +62,27 @@ public class QueryEndpointUnitTest extends BaseUnitTest { /* test */ assertThrows(QueryMalformedException.class, () -> { - generic_execute(DATABASE_3_ID, statement, null, USER_2_PRINCIPAL, DATABASE_3, null); + generic_execute(DATABASE_3_ID, statement, USER_2_PRINCIPAL, DATABASE_3); }); } @Test @WithMockUser(username = USER_2_USERNAME, authorities = {"execute-query"}) public void execute_publicEmptyStatement_fails() { - final String statement = null; /* test */ assertThrows(QueryMalformedException.class, () -> { - generic_execute(DATABASE_3_ID, statement, USER_2_ID, USER_2_PRINCIPAL, DATABASE_3, DATABASE_3_USER_2_READ_ACCESS); + generic_execute(DATABASE_3_ID, null, USER_2_PRINCIPAL, DATABASE_3); }); } @Test @WithMockUser(username = USER_2_USERNAME, authorities = {"execute-query"}) public void execute_publicBlankStatement_fails() { - final String statement = ""; /* test */ assertThrows(QueryMalformedException.class, () -> { - generic_execute(DATABASE_3_ID, statement, USER_2_ID, USER_2_PRINCIPAL, DATABASE_3, DATABASE_3_USER_2_READ_ACCESS); + generic_execute(DATABASE_3_ID, "", USER_2_PRINCIPAL, DATABASE_3); }); } @@ -105,7 +93,7 @@ public class QueryEndpointUnitTest extends BaseUnitTest { /* test */ assertThrows(QueryMalformedException.class, () -> { - generic_execute(DATABASE_3_ID, statement, null, USER_2_PRINCIPAL, DATABASE_3, null); + generic_execute(DATABASE_3_ID, statement, USER_2_PRINCIPAL, DATABASE_3); }); } @@ -115,151 +103,143 @@ public class QueryEndpointUnitTest extends BaseUnitTest { /* test */ assertThrows(org.springframework.security.access.AccessDeniedException.class, () -> { - generic_execute(DATABASE_3_ID, QUERY_4_STATEMENT, null, null, DATABASE_3, null); + generic_execute(DATABASE_3_ID, QUERY_4_STATEMENT, null, DATABASE_3); }); } @Test @WithMockUser(username = USER_4_USERNAME, authorities = {"execute-query"}) - public void execute_publicNoAccess_succeeds() throws UserNotFoundException, QueryStoreException, SortException, - TableMalformedException, DatabaseConnectionException, NotAllowedException, QueryMalformedException, - ColumnParseException, DatabaseNotFoundException, ImageNotSupportedException, PaginationException, - KeycloakRemoteException, at.tuwien.exception.AccessDeniedException, QueryNotFoundException { + public void execute_publicNoAccess_succeeds() throws UserNotFoundException, AccessDeniedException, + QueryStoreException, SortException, TableMalformedException, NotAllowedException, QueryMalformedException, + ColumnParseException, QueryNotFoundException, DatabaseNotFoundException, ImageNotSupportedException, + PaginationException { /* test */ - generic_execute(DATABASE_3_ID, QUERY_4_STATEMENT, null, null, DATABASE_3, null); + generic_execute(DATABASE_3_ID, QUERY_4_STATEMENT, null, DATABASE_3); } @Test @WithMockUser(username = USER_2_USERNAME, authorities = {"execute-query"}) - public void execute_publicRead_succeeds() throws UserNotFoundException, QueryStoreException, TableMalformedException, - DatabaseConnectionException, QueryMalformedException, ColumnParseException, DatabaseNotFoundException, - ImageNotSupportedException, SortException, NotAllowedException, PaginationException, - KeycloakRemoteException, at.tuwien.exception.AccessDeniedException, QueryNotFoundException { + public void execute_publicRead_succeeds() throws UserNotFoundException, AccessDeniedException, QueryStoreException, + SortException, TableMalformedException, NotAllowedException, QueryMalformedException, ColumnParseException, + QueryNotFoundException, DatabaseNotFoundException, ImageNotSupportedException, PaginationException { /* test */ - generic_execute(DATABASE_3_ID, QUERY_4_STATEMENT, USER_2_ID, USER_2_PRINCIPAL, DATABASE_3, DATABASE_3_USER_2_WRITE_ALL_ACCESS); + generic_execute(DATABASE_3_ID, QUERY_4_STATEMENT, USER_2_PRINCIPAL, DATABASE_3); } @Test @WithMockUser(username = USER_2_USERNAME, authorities = {"execute-query"}) - public void execute_publicWriteOwn_succeeds() throws UserNotFoundException, QueryStoreException, TableMalformedException, - DatabaseConnectionException, QueryMalformedException, ColumnParseException, DatabaseNotFoundException, - ImageNotSupportedException, SortException, NotAllowedException, PaginationException, - KeycloakRemoteException, at.tuwien.exception.AccessDeniedException, QueryNotFoundException { + public void execute_publicWriteOwn_succeeds() throws UserNotFoundException, AccessDeniedException, + QueryStoreException, SortException, TableMalformedException, NotAllowedException, QueryMalformedException, + ColumnParseException, QueryNotFoundException, DatabaseNotFoundException, ImageNotSupportedException, + PaginationException { /* test */ - generic_execute(DATABASE_3_ID, QUERY_4_STATEMENT, USER_2_ID, USER_2_PRINCIPAL, DATABASE_3, DATABASE_3_USER_2_WRITE_ALL_ACCESS); + generic_execute(DATABASE_3_ID, QUERY_4_STATEMENT, USER_2_PRINCIPAL, DATABASE_3); } @Test @WithMockUser(username = USER_2_USERNAME, authorities = {"execute-query"}) - public void execute_publicWriteAll_succeeds() throws UserNotFoundException, QueryStoreException, TableMalformedException, - DatabaseConnectionException, QueryMalformedException, ColumnParseException, DatabaseNotFoundException, - ImageNotSupportedException, SortException, NotAllowedException, PaginationException, - KeycloakRemoteException, at.tuwien.exception.AccessDeniedException, QueryNotFoundException { + public void execute_publicWriteAll_succeeds() throws UserNotFoundException, AccessDeniedException, + QueryStoreException, SortException, TableMalformedException, NotAllowedException, QueryMalformedException, + ColumnParseException, QueryNotFoundException, DatabaseNotFoundException, ImageNotSupportedException, PaginationException { /* test */ - generic_execute(DATABASE_3_ID, QUERY_4_STATEMENT, USER_2_ID, USER_2_PRINCIPAL, DATABASE_3, DATABASE_3_USER_2_WRITE_ALL_ACCESS); + generic_execute(DATABASE_3_ID, QUERY_4_STATEMENT, USER_2_PRINCIPAL, DATABASE_3); } @Test @WithMockUser(username = USER_2_USERNAME, authorities = {"execute-query"}) - public void execute_publicOwner_succeeds() throws UserNotFoundException, QueryStoreException, TableMalformedException, - DatabaseConnectionException, QueryMalformedException, ColumnParseException, DatabaseNotFoundException, - ImageNotSupportedException, SortException, NotAllowedException, PaginationException, - KeycloakRemoteException, at.tuwien.exception.AccessDeniedException, QueryNotFoundException { + public void execute_publicOwner_succeeds() throws UserNotFoundException, AccessDeniedException, QueryStoreException, + SortException, TableMalformedException, NotAllowedException, QueryMalformedException, ColumnParseException, + QueryNotFoundException, DatabaseNotFoundException, ImageNotSupportedException, PaginationException { /* test */ - generic_execute(DATABASE_3_ID, QUERY_4_STATEMENT, USER_2_ID, USER_2_PRINCIPAL, DATABASE_3, DATABASE_3_USER_2_WRITE_ALL_ACCESS); + generic_execute(DATABASE_3_ID, QUERY_4_STATEMENT, USER_2_PRINCIPAL, DATABASE_3); } @Test @WithAnonymousUser - public void reExecute_publicAnonymized_succeeds() throws UserNotFoundException, QueryStoreException, SortException, - DatabaseConnectionException, TableMalformedException, NotAllowedException, QueryMalformedException, - QueryNotFoundException, ColumnParseException, DatabaseNotFoundException, ImageNotSupportedException, - PaginationException, at.tuwien.exception.AccessDeniedException { + public void reExecute_publicAnonymized_succeeds() throws AccessDeniedException, QueryStoreException, SortException, + TableMalformedException, NotAllowedException, QueryMalformedException, QueryNotFoundException, + ColumnParseException, DatabaseNotFoundException, ImageNotSupportedException, PaginationException { /* test */ generic_reExecute(DATABASE_3_ID, QUERY_4_ID, QUERY_4, QUERY_4_RESULT_ID, QUERY_4_RESULT_DTO, - null, null, DATABASE_3, null); + null, DATABASE_3); } @Test @WithMockUser(username = USER_2_USERNAME, authorities = {"execute-query"}) - public void reExecute_publicRead_succeeds() throws UserNotFoundException, QueryStoreException, TableMalformedException, - DatabaseConnectionException, QueryMalformedException, ColumnParseException, DatabaseNotFoundException, - ImageNotSupportedException, SortException, NotAllowedException, - PaginationException, QueryNotFoundException, at.tuwien.exception.AccessDeniedException { + public void reExecute_publicRead_succeeds() throws AccessDeniedException, QueryStoreException, SortException, + TableMalformedException, NotAllowedException, QueryMalformedException, QueryNotFoundException, + ColumnParseException, DatabaseNotFoundException, ImageNotSupportedException, PaginationException { /* test */ generic_reExecute(DATABASE_3_ID, QUERY_4_ID, QUERY_4, QUERY_4_RESULT_ID, QUERY_4_RESULT_DTO, - USER_2_ID, USER_2_PRINCIPAL, DATABASE_3, DATABASE_3_USER_1_READ_ACCESS); + USER_2_PRINCIPAL, DATABASE_3); } @Test @WithMockUser(username = USER_2_USERNAME, authorities = {"execute-query"}) - public void reExecute_public_succeeds() throws UserNotFoundException, QueryStoreException, TableMalformedException, - DatabaseConnectionException, QueryMalformedException, ColumnParseException, DatabaseNotFoundException, - ImageNotSupportedException, SortException, NotAllowedException, - PaginationException, QueryNotFoundException, at.tuwien.exception.AccessDeniedException { + public void reExecute_public_succeeds() throws AccessDeniedException, QueryStoreException, SortException, + TableMalformedException, NotAllowedException, QueryMalformedException, QueryNotFoundException, + ColumnParseException, DatabaseNotFoundException, ImageNotSupportedException, PaginationException { /* test */ generic_reExecute(DATABASE_3_ID, QUERY_4_ID, QUERY_4, QUERY_4_RESULT_ID, QUERY_4_RESULT_DTO, - USER_2_ID, USER_2_PRINCIPAL, DATABASE_3, DATABASE_3_USER_1_WRITE_OWN_ACCESS); + USER_2_PRINCIPAL, DATABASE_3); } @Test @WithAnonymousUser - public void export_publicAnonymized_succeeds() throws UserNotFoundException, QueryStoreException, - TableMalformedException, DatabaseConnectionException, QueryMalformedException, DatabaseNotFoundException, - ImageNotSupportedException, QueryNotFoundException, FileStorageException, - IOException, NotAllowedException { + public void export_publicAnonymized_succeeds() throws QueryStoreException, NotAllowedException, + QueryMalformedException, QueryNotFoundException, DatabaseNotFoundException, ImageNotSupportedException, + IOException, FileStorageException, DataProcessingException { /* test */ - export_generic(DATABASE_3_ID, QUERY_3_ID, null, null, DATABASE_3, null, null, HttpStatus.OK); + export_generic(DATABASE_3_ID, QUERY_3_ID, null, DATABASE_3, null, HttpStatus.OK); } @Test @WithAnonymousUser - public void export_publicAnonymizedInvalidFormat_fails() throws UserNotFoundException, QueryStoreException, DatabaseConnectionException, TableMalformedException, NotAllowedException, QueryMalformedException, QueryNotFoundException, DatabaseNotFoundException, ImageNotSupportedException, IOException, FileStorageException, ContainerNotFoundException { + public void export_publicAnonymizedInvalidFormat_fails() throws QueryStoreException, NotAllowedException, + QueryMalformedException, QueryNotFoundException, DatabaseNotFoundException, ImageNotSupportedException, + IOException, FileStorageException, DataProcessingException { /* test */ - export_generic(DATABASE_3_ID, QUERY_3_ID, null, null, DATABASE_3, null, "application/json", HttpStatus.NOT_IMPLEMENTED); + export_generic(DATABASE_3_ID, QUERY_3_ID, null, DATABASE_3, "application/json", HttpStatus.NOT_IMPLEMENTED); } @Test @WithMockUser(username = USER_2_USERNAME, authorities = {"export-query-data"}) - public void export_publicRead_succeeds() throws UserNotFoundException, QueryStoreException, - TableMalformedException, DatabaseConnectionException, QueryMalformedException, DatabaseNotFoundException, - ImageNotSupportedException, QueryNotFoundException, FileStorageException, - IOException, NotAllowedException { + public void export_publicRead_succeeds() throws QueryStoreException, NotAllowedException, QueryMalformedException, + QueryNotFoundException, DatabaseNotFoundException, ImageNotSupportedException, IOException, + FileStorageException, DataProcessingException { /* test */ - export_generic(DATABASE_3_ID, QUERY_3_ID, USER_2_ID, USER_2_PRINCIPAL, DATABASE_3, DATABASE_3_USER_1_READ_ACCESS, null, HttpStatus.OK); + export_generic(DATABASE_3_ID, QUERY_3_ID, USER_2_PRINCIPAL, DATABASE_3, null, HttpStatus.OK); } @Test @WithMockUser(username = USER_2_USERNAME, authorities = {"export-query-data"}) - public void export_publicWriteOwn_succeeds() throws UserNotFoundException, QueryStoreException, - TableMalformedException, DatabaseConnectionException, QueryMalformedException, DatabaseNotFoundException, - ImageNotSupportedException, QueryNotFoundException, FileStorageException, - IOException, NotAllowedException { + public void export_publicWriteOwn_succeeds() throws QueryStoreException, NotAllowedException, + QueryMalformedException, QueryNotFoundException, DatabaseNotFoundException, ImageNotSupportedException, + IOException, FileStorageException, DataProcessingException { /* test */ - export_generic(DATABASE_3_ID, QUERY_4_ID, USER_2_ID, USER_2_PRINCIPAL, DATABASE_3, DATABASE_3_USER_1_WRITE_OWN_ACCESS, null, HttpStatus.OK); + export_generic(DATABASE_3_ID, QUERY_4_ID, USER_2_PRINCIPAL, DATABASE_3, null, HttpStatus.OK); } @Test @WithMockUser(username = USER_2_USERNAME, authorities = {"export-query-data"}) - public void export_publicWriteAll_succeeds() throws UserNotFoundException, QueryStoreException, - TableMalformedException, DatabaseConnectionException, QueryMalformedException, DatabaseNotFoundException, - ImageNotSupportedException, QueryNotFoundException, FileStorageException, - IOException, NotAllowedException { + public void export_publicWriteAll_succeeds() throws QueryStoreException, NotAllowedException, + QueryMalformedException, QueryNotFoundException, DatabaseNotFoundException, ImageNotSupportedException, + IOException, FileStorageException, DataProcessingException { /* test */ - export_generic(DATABASE_3_ID, QUERY_4_ID, USER_2_ID, USER_2_PRINCIPAL, DATABASE_3, DATABASE_3_USER_1_WRITE_ALL_ACCESS, null, HttpStatus.OK); + export_generic(DATABASE_3_ID, QUERY_4_ID, USER_2_PRINCIPAL, DATABASE_3, null, HttpStatus.OK); } /* ################################################################################################### */ @@ -269,68 +249,66 @@ public class QueryEndpointUnitTest extends BaseUnitTest { @Test @WithAnonymousUser public void execute_privateAnonymized_fails() { - final Principal principal = null; /* test */ assertThrows(org.springframework.security.access.AccessDeniedException.class, () -> { - generic_execute(DATABASE_2_ID, QUERY_1_STATEMENT, null, principal, DATABASE_2, null); + generic_execute(DATABASE_2_ID, QUERY_1_STATEMENT, null, DATABASE_2); }); } @Test @WithMockUser(username = USER_2_USERNAME, authorities = {"execute-query"}) - public void execute_privateRead_succeeds() throws UserNotFoundException, QueryStoreException, TableMalformedException, - DatabaseConnectionException, QueryMalformedException, ColumnParseException, DatabaseNotFoundException, - ImageNotSupportedException, SortException, NotAllowedException, PaginationException, - KeycloakRemoteException, at.tuwien.exception.AccessDeniedException, QueryNotFoundException { + public void execute_privateRead_succeeds() throws UserNotFoundException, AccessDeniedException, QueryStoreException, + SortException, TableMalformedException, NotAllowedException, QueryMalformedException, ColumnParseException, + QueryNotFoundException, DatabaseNotFoundException, ImageNotSupportedException, PaginationException { /* mock */ DATABASE_2.setAccesses(List.of(DATABASE_2_USER_2_READ_ACCESS)); /* test */ - generic_execute(DATABASE_2_ID, QUERY_1_STATEMENT, USER_2_ID, USER_2_PRINCIPAL, DATABASE_2, DATABASE_2_USER_1_READ_ACCESS); + generic_execute(DATABASE_2_ID, QUERY_1_STATEMENT, USER_2_PRINCIPAL, DATABASE_2); } @Test @WithMockUser(username = USER_2_USERNAME, authorities = {"execute-query"}) - public void execute_privateWriteOwn_succeeds() throws UserNotFoundException, QueryStoreException, TableMalformedException, - DatabaseConnectionException, QueryMalformedException, ColumnParseException, DatabaseNotFoundException, - ImageNotSupportedException, SortException, NotAllowedException, PaginationException, - KeycloakRemoteException, at.tuwien.exception.AccessDeniedException, QueryNotFoundException { + public void execute_privateWriteOwn_succeeds() throws UserNotFoundException, AccessDeniedException, + QueryStoreException, SortException, TableMalformedException, NotAllowedException, QueryMalformedException, + ColumnParseException, QueryNotFoundException, DatabaseNotFoundException, ImageNotSupportedException, + PaginationException { /* mock */ DATABASE_2.setAccesses(List.of(DATABASE_2_USER_2_WRITE_OWN_ACCESS)); /* test */ - generic_execute(DATABASE_2_ID, QUERY_1_STATEMENT, USER_2_ID, USER_2_PRINCIPAL, DATABASE_2, DATABASE_2_USER_1_WRITE_OWN_ACCESS); + generic_execute(DATABASE_2_ID, QUERY_1_STATEMENT, USER_2_PRINCIPAL, DATABASE_2); } @Test @WithMockUser(username = USER_2_USERNAME, authorities = {"execute-query"}) - public void execute_privateWriteAll_succeeds() throws UserNotFoundException, QueryStoreException, TableMalformedException, - DatabaseConnectionException, QueryMalformedException, ColumnParseException, DatabaseNotFoundException, - ImageNotSupportedException, SortException, NotAllowedException, PaginationException, - KeycloakRemoteException, at.tuwien.exception.AccessDeniedException, QueryNotFoundException { + public void execute_privateWriteAll_succeeds() throws UserNotFoundException, AccessDeniedException, + QueryStoreException, SortException, TableMalformedException, NotAllowedException, QueryMalformedException, + ColumnParseException, QueryNotFoundException, DatabaseNotFoundException, ImageNotSupportedException, + PaginationException { /* mock */ DATABASE_2.setAccesses(List.of(DATABASE_2_USER_2_WRITE_ALL_ACCESS)); /* test */ - generic_execute(DATABASE_2_ID, QUERY_1_STATEMENT, USER_2_ID, USER_2_PRINCIPAL, DATABASE_2, DATABASE_2_USER_1_WRITE_ALL_ACCESS); + generic_execute(DATABASE_2_ID, QUERY_1_STATEMENT, USER_2_PRINCIPAL, DATABASE_2); } @Test @WithMockUser(username = USER_2_USERNAME, authorities = {"execute-query"}) - public void execute_privateOwner_succeeds() throws UserNotFoundException, QueryStoreException, TableMalformedException, - DatabaseConnectionException, QueryMalformedException, ColumnParseException, DatabaseNotFoundException, - ImageNotSupportedException, SortException, NotAllowedException, PaginationException, - KeycloakRemoteException, at.tuwien.exception.AccessDeniedException, QueryNotFoundException { + public void execute_privateOwner_succeeds() throws UserNotFoundException, AccessDeniedException, + QueryStoreException, SortException, TableMalformedException, NotAllowedException, QueryMalformedException, + ColumnParseException, QueryNotFoundException, DatabaseNotFoundException, ImageNotSupportedException, + PaginationException { /* mock */ DATABASE_2.setAccesses(List.of(DATABASE_2_USER_1_WRITE_ALL_ACCESS)); /* test */ - generic_execute(DATABASE_2_ID, QUERY_1_STATEMENT, USER_1_ID, USER_1_PRINCIPAL, DATABASE_2, DATABASE_2_USER_1_WRITE_ALL_ACCESS); + generic_execute(DATABASE_2_ID, QUERY_1_STATEMENT, USER_1_PRINCIPAL, DATABASE_2); } @Test @@ -340,53 +318,51 @@ public class QueryEndpointUnitTest extends BaseUnitTest { /* test */ assertThrows(NotAllowedException.class, () -> { generic_reExecute(DATABASE_2_ID, QUERY_1_ID, QUERY_1, QUERY_1_RESULT_ID, QUERY_1_RESULT_DTO, - null, null, DATABASE_2, null); + null, DATABASE_2); }); } @Test @WithMockUser(username = USER_2_USERNAME, authorities = {"execute-query"}) - public void reExecute_privateRead_succeeds() throws UserNotFoundException, QueryStoreException, TableMalformedException, - DatabaseConnectionException, QueryMalformedException, ColumnParseException, DatabaseNotFoundException, - ImageNotSupportedException, SortException, NotAllowedException, PaginationException, QueryNotFoundException, - at.tuwien.exception.AccessDeniedException { + public void reExecute_privateRead_succeeds() throws AccessDeniedException, QueryStoreException, SortException, + TableMalformedException, NotAllowedException, QueryMalformedException, QueryNotFoundException, + ColumnParseException, DatabaseNotFoundException, ImageNotSupportedException, PaginationException { /* mock */ DATABASE_2.setAccesses(List.of(DATABASE_2_USER_2_READ_ACCESS)); /* test */ generic_reExecute(DATABASE_2_ID, QUERY_1_ID, QUERY_1, QUERY_1_RESULT_ID, QUERY_1_RESULT_DTO, - USER_2_ID, USER_2_PRINCIPAL, DATABASE_2, DATABASE_2_USER_1_READ_ACCESS); + USER_2_PRINCIPAL, DATABASE_2); } @Test @WithMockUser(username = USER_2_USERNAME, authorities = {"execute-query"}) - public void reExecute_privateWriteOwn_succeeds() throws UserNotFoundException, QueryStoreException, - TableMalformedException, DatabaseConnectionException, QueryMalformedException, ColumnParseException, - DatabaseNotFoundException, ImageNotSupportedException, SortException, NotAllowedException, - PaginationException, QueryNotFoundException, at.tuwien.exception.AccessDeniedException { + public void reExecute_privateWriteOwn_succeeds() throws AccessDeniedException, QueryStoreException, SortException, + TableMalformedException, NotAllowedException, QueryMalformedException, QueryNotFoundException, + ColumnParseException, DatabaseNotFoundException, ImageNotSupportedException, PaginationException { /* mock */ DATABASE_2.setAccesses(List.of(DATABASE_2_USER_2_WRITE_OWN_ACCESS)); /* test */ generic_reExecute(DATABASE_2_ID, QUERY_1_ID, QUERY_1, QUERY_1_RESULT_ID, QUERY_1_RESULT_DTO, - USER_2_ID, USER_2_PRINCIPAL, DATABASE_2, DATABASE_2_USER_1_WRITE_OWN_ACCESS); + USER_2_PRINCIPAL, DATABASE_2); } @Test @WithMockUser(username = USER_2_USERNAME, authorities = {"execute-query"}) - public void reExecute_privateWriteAll_succeeds() throws UserNotFoundException, QueryStoreException, - TableMalformedException, DatabaseConnectionException, QueryMalformedException, ColumnParseException, - DatabaseNotFoundException, ImageNotSupportedException, SortException, NotAllowedException, - PaginationException, QueryNotFoundException, at.tuwien.exception.AccessDeniedException { + public void reExecute_privateWriteAll_succeeds() throws QueryStoreException, TableMalformedException, + QueryMalformedException, ColumnParseException, DatabaseNotFoundException, ImageNotSupportedException, + SortException, NotAllowedException, PaginationException, QueryNotFoundException, + at.tuwien.exception.AccessDeniedException { /* mock */ DATABASE_2.setAccesses(List.of(DATABASE_2_USER_2_WRITE_ALL_ACCESS)); /* test */ generic_reExecute(DATABASE_2_ID, QUERY_1_ID, QUERY_1, QUERY_1_RESULT_ID, QUERY_1_RESULT_DTO, - USER_2_ID, USER_2_PRINCIPAL, DATABASE_2, DATABASE_2_USER_1_WRITE_ALL_ACCESS); + USER_2_PRINCIPAL, DATABASE_2); } @Test @@ -395,64 +371,59 @@ public class QueryEndpointUnitTest extends BaseUnitTest { /* test */ assertThrows(NotAllowedException.class, () -> { - export_generic(DATABASE_2_ID, QUERY_1_ID, null, null, DATABASE_2, null, null, HttpStatus.OK); + export_generic(DATABASE_2_ID, QUERY_1_ID, null, DATABASE_2, null, HttpStatus.OK); }); } @Test @WithMockUser(username = USER_2_USERNAME, authorities = {"export-query-data"}) - public void export_privateInvalidFormat_fails() throws UserNotFoundException, QueryStoreException, - DatabaseConnectionException, TableMalformedException, NotAllowedException, QueryMalformedException, - QueryNotFoundException, DatabaseNotFoundException, ImageNotSupportedException, IOException, - FileStorageException { + public void export_privateInvalidFormat_fails() throws QueryStoreException, NotAllowedException, + QueryMalformedException, QueryNotFoundException, DatabaseNotFoundException, ImageNotSupportedException, + IOException, FileStorageException, DataProcessingException { /* test */ - export_generic(DATABASE_2_ID, QUERY_1_ID, USER_2_ID, USER_2_PRINCIPAL, DATABASE_2, DATABASE_2_USER_2_READ_ACCESS, "application/json", HttpStatus.NOT_IMPLEMENTED); + export_generic(DATABASE_2_ID, QUERY_1_ID, USER_2_PRINCIPAL, DATABASE_2, "application/json", HttpStatus.NOT_IMPLEMENTED); } @Test @WithMockUser(username = USER_2_USERNAME, authorities = {"export-query-data"}) - public void export_privateRead_succeeds() throws UserNotFoundException, QueryStoreException, - TableMalformedException, DatabaseConnectionException, QueryMalformedException, DatabaseNotFoundException, - ImageNotSupportedException, NotAllowedException, QueryNotFoundException, FileStorageException, - IOException { + public void export_privateRead_succeeds() throws QueryStoreException, NotAllowedException, QueryMalformedException, + QueryNotFoundException, DatabaseNotFoundException, ImageNotSupportedException, IOException, + FileStorageException, DataProcessingException { /* test */ - export_generic(DATABASE_2_ID, QUERY_1_ID, USER_2_ID, USER_2_PRINCIPAL, DATABASE_2, DATABASE_2_USER_1_READ_ACCESS, null, HttpStatus.OK); + export_generic(DATABASE_2_ID, QUERY_1_ID, USER_2_PRINCIPAL, DATABASE_2, null, HttpStatus.OK); } @Test @WithMockUser(username = USER_2_USERNAME, authorities = {"export-query-data"}) - public void export_privateWriteOwn_succeeds() throws UserNotFoundException, QueryStoreException, - TableMalformedException, DatabaseConnectionException, QueryMalformedException, DatabaseNotFoundException, - ImageNotSupportedException, NotAllowedException, QueryNotFoundException, FileStorageException, - IOException { + public void export_privateWriteOwn_succeeds() throws QueryStoreException, NotAllowedException, + QueryMalformedException, QueryNotFoundException, DatabaseNotFoundException, ImageNotSupportedException, + IOException, FileStorageException, DataProcessingException { /* test */ - export_generic(DATABASE_2_ID, QUERY_1_ID, USER_2_ID, USER_2_PRINCIPAL, DATABASE_2, DATABASE_2_USER_1_WRITE_OWN_ACCESS, null, HttpStatus.OK); + export_generic(DATABASE_2_ID, QUERY_1_ID, USER_2_PRINCIPAL, DATABASE_2, null, HttpStatus.OK); } @Test @WithMockUser(username = USER_2_USERNAME, authorities = {"export-query-data"}) - public void export_privateWriteAll_succeeds() throws UserNotFoundException, QueryStoreException, - TableMalformedException, DatabaseConnectionException, QueryMalformedException, DatabaseNotFoundException, - ImageNotSupportedException, NotAllowedException, QueryNotFoundException, FileStorageException, - IOException { + public void export_privateWriteAll_succeeds() throws QueryStoreException, NotAllowedException, + QueryMalformedException, QueryNotFoundException, DatabaseNotFoundException, ImageNotSupportedException, + IOException, FileStorageException, DataProcessingException { /* test */ - export_generic(DATABASE_2_ID, QUERY_1_ID, USER_2_ID, USER_2_PRINCIPAL, DATABASE_2, DATABASE_2_USER_1_WRITE_ALL_ACCESS, null, HttpStatus.OK); + export_generic(DATABASE_2_ID, QUERY_1_ID, USER_2_PRINCIPAL, DATABASE_2, null, HttpStatus.OK); } /* ################################################################################################### */ /* ## GENERIC TEST CASES ## */ /* ################################################################################################### */ - protected void generic_execute(Long databaseId, String statement, UUID userId, Principal principal, - Database database, DatabaseAccess access) throws UserNotFoundException, - QueryStoreException, TableMalformedException, DatabaseConnectionException, - QueryMalformedException, ColumnParseException, DatabaseNotFoundException, ImageNotSupportedException, - SortException, NotAllowedException, PaginationException, KeycloakRemoteException, - at.tuwien.exception.AccessDeniedException, QueryNotFoundException { + protected void generic_execute(Long databaseId, String statement, Principal principal, Database database) + throws UserNotFoundException, QueryStoreException, TableMalformedException, QueryMalformedException, + ColumnParseException, DatabaseNotFoundException, ImageNotSupportedException, SortException, + NotAllowedException, PaginationException, at.tuwien.exception.AccessDeniedException, + QueryNotFoundException { final ExecuteStatementDto request = ExecuteStatementDto.builder() .statement(statement) .build(); @@ -481,9 +452,8 @@ public class QueryEndpointUnitTest extends BaseUnitTest { } protected void generic_reExecute(Long databaseId, Long queryId, Query query, Long resultId, - QueryResultDto result, UUID userId, Principal principal, Database database, - DatabaseAccess access) throws UserNotFoundException, QueryStoreException, - DatabaseConnectionException, QueryNotFoundException, DatabaseNotFoundException, ImageNotSupportedException, + QueryResultDto result, Principal principal, Database database) + throws QueryStoreException, QueryNotFoundException, DatabaseNotFoundException, ImageNotSupportedException, TableMalformedException, QueryMalformedException, ColumnParseException, SortException, NotAllowedException, PaginationException, at.tuwien.exception.AccessDeniedException { final Long page = 0L; @@ -507,11 +477,10 @@ public class QueryEndpointUnitTest extends BaseUnitTest { assertEquals(resultId, response.getBody().getId()); } - protected void export_generic(Long databaseId, Long queryId, UUID userId, Principal principal, - Database database, DatabaseAccess access, String accept, HttpStatus status) throws IOException, - UserNotFoundException, QueryStoreException, DatabaseConnectionException, QueryNotFoundException, - DatabaseNotFoundException, ImageNotSupportedException, TableMalformedException, QueryMalformedException, - FileStorageException, NotAllowedException { + protected void export_generic(Long databaseId, Long queryId, Principal principal, Database database, String accept, + HttpStatus status) throws IOException, QueryStoreException, QueryNotFoundException, + DatabaseNotFoundException, ImageNotSupportedException, QueryMalformedException, FileStorageException, + NotAllowedException, DataProcessingException { final ExportResource resource = ExportResource.builder() .filename("location.csv") .resource(new InputStreamResource(FileUtils.openInputStream(new File("src/test/resources/weather/location.csv")))) diff --git a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/endpoints/StoreEndpointUnitTest.java b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/endpoints/StoreEndpointUnitTest.java index 5b1278f33c47ea1553e175b7e387881aa66a0d4d..5145c8136ad2d513b0f263edafec15d5be7cf454 100644 --- a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/endpoints/StoreEndpointUnitTest.java +++ b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/endpoints/StoreEndpointUnitTest.java @@ -261,8 +261,8 @@ public class StoreEndpointUnitTest extends BaseUnitTest { @Test @WithMockUser(username = USER_1_USERNAME, authorities = "persist-query") public void persist_ownWriteAll_succeeds() throws UserNotFoundException, QueryStoreException, - NotAllowedException, DatabaseConnectionException, QueryNotFoundException, DatabaseNotFoundException, - ImageNotSupportedException, AccessDeniedException, IdentifierAlreadyPublishedException { + NotAllowedException, QueryNotFoundException, DatabaseNotFoundException, ImageNotSupportedException, + AccessDeniedException, IdentifierAlreadyPublishedException { /* mock */ when(userRepository.findByUsername(USER_1_USERNAME)) @@ -296,8 +296,7 @@ public class StoreEndpointUnitTest extends BaseUnitTest { protected QueryDto persist_generic(Long databaseId, Database database, Long queryId, Query query, UUID userId, Principal principal, DatabaseAccess access) throws DatabaseNotFoundException, UserNotFoundException, QueryStoreException, QueryNotFoundException, - ImageNotSupportedException, NotAllowedException, DatabaseConnectionException, - AccessDeniedException, IdentifierAlreadyPublishedException { + ImageNotSupportedException, NotAllowedException, AccessDeniedException, IdentifierAlreadyPublishedException { final QueryPersistDto request = QueryPersistDto.builder() .persist(true) .build(); @@ -307,8 +306,9 @@ public class StoreEndpointUnitTest extends BaseUnitTest { .thenReturn(database); when(storeService.findOne(databaseId, queryId, principal)) .thenReturn(query); - when(storeService.persist(databaseId, queryId, request)) - .thenReturn(query); + doReturn(query) + .when(storeService) + .persist(databaseId, queryId, request); if (access != null) { log.trace("mock access for database with id {} and user id {}", databaseId, userId); when(accessService.find(databaseId, userId)) diff --git a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/endpoints/TableDataEndpointUnitTest.java b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/endpoints/TableDataEndpointUnitTest.java index cf5b8dd25cee8571cbb3fe2570d235834efd73ed..933d4ba78aefde918ecbbaa7f4f63fb5c5249871 100644 --- a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/endpoints/TableDataEndpointUnitTest.java +++ b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/endpoints/TableDataEndpointUnitTest.java @@ -136,9 +136,9 @@ public class TableDataEndpointUnitTest extends BaseUnitTest { @Test @WithMockUser(username = USER_1_USERNAME, authorities = {"insert-table-data"}) - public void import_publicWriteAll_succeeds() throws UserNotFoundException, TableNotFoundException, NotAllowedException, - TableMalformedException, DatabaseConnectionException, QueryMalformedException, DatabaseNotFoundException, - ImageNotSupportedException, AccessDeniedException, DataDbSidecarException { + public void import_publicWriteAll_succeeds() throws TableNotFoundException, AccessDeniedException, + TableMalformedException, NotAllowedException, DatabaseNotFoundException, DataDbSidecarException, + DataProcessingException { /* test */ generic_import(DATABASE_3_ID, DATABASE_3, TABLE_8_ID, TABLE_8, USER_1_ID, @@ -147,9 +147,9 @@ public class TableDataEndpointUnitTest extends BaseUnitTest { @Test @WithMockUser(username = USER_1_USERNAME, authorities = {"insert-table-data"}) - public void import_privateWriteAll_succeeds() throws UserNotFoundException, TableNotFoundException, NotAllowedException, - TableMalformedException, DatabaseConnectionException, QueryMalformedException, DatabaseNotFoundException, - ImageNotSupportedException, AccessDeniedException, DataDbSidecarException { + public void import_privateWriteAll_succeeds() throws TableNotFoundException, AccessDeniedException, + TableMalformedException, NotAllowedException, DatabaseNotFoundException, DataDbSidecarException, + DataProcessingException { /* test */ generic_import(DATABASE_1_ID, DATABASE_1, TABLE_1_ID, TABLE_1, USER_1_ID, @@ -224,9 +224,8 @@ public class TableDataEndpointUnitTest extends BaseUnitTest { @Test @WithMockUser(username = USER_1_USERNAME, authorities = {"insert-table-data"}) - public void insert_publicWriteAll_succeeds() throws UserNotFoundException, TableNotFoundException, - NotAllowedException, TableMalformedException, DatabaseConnectionException, DatabaseNotFoundException, - ImageNotSupportedException, ContainerNotFoundException, AccessDeniedException { + public void insert_publicWriteAll_succeeds() throws TableNotFoundException, AccessDeniedException, + TableMalformedException, NotAllowedException, DatabaseNotFoundException { /* test */ generic_insert(DATABASE_3_ID, TABLE_8_ID, DATABASE_3, TABLE_8, USER_1_ID, @@ -235,9 +234,8 @@ public class TableDataEndpointUnitTest extends BaseUnitTest { @Test @WithMockUser(username = USER_1_USERNAME, authorities = {"insert-table-data"}) - public void insert_privateWriteAll_succeeds() throws UserNotFoundException, TableNotFoundException, - NotAllowedException, TableMalformedException, DatabaseConnectionException, DatabaseNotFoundException, - ImageNotSupportedException, ContainerNotFoundException, AccessDeniedException { + public void insert_privateWriteAll_succeeds() throws TableNotFoundException, AccessDeniedException, + TableMalformedException, NotAllowedException, DatabaseNotFoundException { /* test */ generic_insert(DATABASE_1_ID, TABLE_1_ID, DATABASE_1, TABLE_1, USER_1_ID, DATABASE_1_USER_1_WRITE_ALL_ACCESS, TABLE_1_CSV_DTO, USER_1_PRINCIPAL); @@ -245,9 +243,8 @@ public class TableDataEndpointUnitTest extends BaseUnitTest { @Test @WithMockUser(username = USER_1_USERNAME, authorities = {"insert-table-data"}) - public void insert_privateDataNull_fails() throws UserNotFoundException, TableNotFoundException, - NotAllowedException, TableMalformedException, DatabaseConnectionException, DatabaseNotFoundException, - ImageNotSupportedException, ContainerNotFoundException, AccessDeniedException { + public void insert_privateDataNull_fails() throws TableNotFoundException, AccessDeniedException, + TableMalformedException, NotAllowedException, DatabaseNotFoundException { /* test */ generic_insert(DATABASE_1_ID, TABLE_1_ID, DATABASE_1, TABLE_1, USER_1_ID, DATABASE_1_USER_1_WRITE_ALL_ACCESS, null, USER_1_PRINCIPAL); @@ -369,10 +366,9 @@ public class TableDataEndpointUnitTest extends BaseUnitTest { @MethodSource("getAll_succeeds_parameters") public void getAll_succeeds(String test, Long databaseId, Long tableId, Database database, Table table, UUID userId, DatabaseAccess access, Principal principal, Instant timestamp, Long page, Long size, - SortType sortDirection, String sortColumn) throws UserNotFoundException, - TableNotFoundException, SortException, TableMalformedException, NotAllowedException, - DatabaseConnectionException, QueryMalformedException, DatabaseNotFoundException, ImageNotSupportedException, - PaginationException, AccessDeniedException { + SortType sortDirection, String sortColumn) throws TableNotFoundException, SortException, + TableMalformedException, NotAllowedException, QueryMalformedException, DatabaseNotFoundException, + ImageNotSupportedException, PaginationException, AccessDeniedException { /* test */ generic_getAll(databaseId, tableId, database, table, userId, access, principal, timestamp, @@ -409,9 +405,8 @@ public class TableDataEndpointUnitTest extends BaseUnitTest { @MethodSource("getAll_succeeds_parameters") public void getCount_succeeds(String test, Long databaseId, Long tableId, Database database, Table table, UUID userId, DatabaseAccess access, Principal principal, Instant timestamp) - throws UserNotFoundException, TableNotFoundException, QueryStoreException, TableMalformedException, - NotAllowedException, DatabaseConnectionException, QueryMalformedException, DatabaseNotFoundException, - ImageNotSupportedException, AccessDeniedException { + throws TableNotFoundException, QueryStoreException, TableMalformedException, NotAllowedException, + QueryMalformedException, DatabaseNotFoundException, ImageNotSupportedException, AccessDeniedException { /* test */ generic_getCount(databaseId, tableId, database, table, userId, access, principal, timestamp); @@ -424,9 +419,8 @@ public class TableDataEndpointUnitTest extends BaseUnitTest { public void generic_import(Long databaseId, Database database, Long tableId, Table table, UUID userId, DatabaseAccess access, Principal principal) throws DatabaseNotFoundException, - TableNotFoundException, NotAllowedException, UserNotFoundException, TableMalformedException, - DatabaseConnectionException, QueryMalformedException, ImageNotSupportedException, - AccessDeniedException, DataDbSidecarException { + TableNotFoundException, AccessDeniedException, TableMalformedException, NotAllowedException, + DataDbSidecarException, DataProcessingException { final ImportDto request = ImportDto.builder().location("test:csv/csv_01.csv").build(); /* mock */ @@ -445,9 +439,8 @@ public class TableDataEndpointUnitTest extends BaseUnitTest { public void generic_insert(Long databaseId, Long tableId, Database database, Table table, UUID userId, DatabaseAccess access, TableCsvDto data, Principal principal) - throws DatabaseNotFoundException, TableNotFoundException, NotAllowedException, UserNotFoundException, - TableMalformedException, DatabaseConnectionException, ImageNotSupportedException, - ContainerNotFoundException, AccessDeniedException { + throws DatabaseNotFoundException, TableNotFoundException, AccessDeniedException, TableMalformedException, + NotAllowedException { /* mock */ when(databaseService.find(databaseId)) @@ -465,10 +458,9 @@ public class TableDataEndpointUnitTest extends BaseUnitTest { public void generic_getAll(Long databaseId, Long tableId, Database database, Table table, UUID userId, DatabaseAccess access, Principal principal, Instant timestamp, Long page, Long size, - SortType sortDirection, String sortColumn) throws UserNotFoundException, - TableMalformedException, NotAllowedException, PaginationException, TableNotFoundException, SortException, - DatabaseConnectionException, QueryMalformedException, DatabaseNotFoundException, ImageNotSupportedException, - AccessDeniedException { + SortType sortDirection, String sortColumn) throws TableMalformedException, + NotAllowedException, PaginationException, TableNotFoundException, SortException, QueryMalformedException, + DatabaseNotFoundException, ImageNotSupportedException, AccessDeniedException { /* mock */ when(databaseService.find(databaseId)) @@ -492,9 +484,8 @@ public class TableDataEndpointUnitTest extends BaseUnitTest { public void generic_getCount(Long databaseId, Long tableId, Database database, Table table, UUID userId, DatabaseAccess access, Principal principal, Instant timestamp) - throws UserNotFoundException, TableMalformedException, NotAllowedException, TableNotFoundException, - QueryStoreException, DatabaseConnectionException, QueryMalformedException, DatabaseNotFoundException, - ImageNotSupportedException, AccessDeniedException { + throws TableMalformedException, NotAllowedException, TableNotFoundException, QueryStoreException, + QueryMalformedException, DatabaseNotFoundException, ImageNotSupportedException, AccessDeniedException { /* mock */ when(databaseService.find(databaseId)) 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 c5501238b39c56a74be2ab1eb122e81e0377ab63..f07af86a8262d2edee475a3b3a9f789bc20999fd 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 @@ -449,11 +449,55 @@ public class TableEndpointUnitTest extends BaseUnitTest { @Test @WithMockUser(username = USER_4_USERNAME) - public void delete_privateNoRole_succeeds() throws TableNotFoundException, DatabaseNotFoundException, - at.tuwien.exception.AccessDeniedException, QueueNotFoundException, BrokerRemoteException { + public void delete_privateNoRole_fails() { /* test */ - generic_findById(DATABASE_1_ID, TABLE_1_ID, DATABASE_1, TABLE_1, USER_4_ID, USER_4_PRINCIPAL, null); + assertThrows(org.springframework.security.access.AccessDeniedException.class, () -> { + generic_delete(USER_4_PRINCIPAL, TABLE_1); + }); + } + + @Test + @WithMockUser(username = USER_1_USERNAME, authorities = {"delete-table"}) + public void delete_succeeds() throws TableNotFoundException, TableMalformedException, NotAllowedException, + QueryMalformedException, DatabaseNotFoundException, ImageNotSupportedException { + + /* test */ + generic_delete(USER_1_PRINCIPAL, TABLE_1); + } + + @Test + @WithMockUser(username = USER_3_USERNAME, authorities = {"delete-table"}) + public void delete_foreign_fails() { + + /* test */ + assertThrows(NotAllowedException.class, () -> { + generic_delete(USER_3_PRINCIPAL, TABLE_1); + }); + } + + @Test + @WithMockUser(username = USER_2_USERNAME, authorities = {"delete-foreign-table"}) + public void delete_foreign_succeeds() throws TableNotFoundException, TableMalformedException, NotAllowedException, + QueryMalformedException, DatabaseNotFoundException, ImageNotSupportedException { + + /* test */ + generic_delete(USER_2_PRINCIPAL, TABLE_1); + } + + @Test + @WithMockUser(username = USER_2_USERNAME, authorities = {"delete-table"}) + public void delete_hasIdentifiers_fails() { + final Table response = Table.builder() + .identifiers(List.of(IDENTIFIER_1)) + .owner(USER_1) + .ownedBy(USER_1_ID) + .build(); + + /* test */ + assertThrows(NotAllowedException.class, () -> { + generic_delete(USER_1_PRINCIPAL, response); + }); } /* ################################################################################################### */ @@ -494,9 +538,8 @@ public class TableEndpointUnitTest extends BaseUnitTest { protected ResponseEntity<TableBriefDto> generic_create(Long databaseId, Database database, TableCreateDto data, UUID userId, Principal principal, DatabaseAccess access) - throws DatabaseNotFoundException, NotAllowedException, UserNotFoundException, TableMalformedException, - QueryMalformedException, ImageNotSupportedException, AmqpException, TableNameExistsException, - ContainerNotFoundException, at.tuwien.exception.AccessDeniedException, TableNotFoundException { + throws DatabaseNotFoundException, NotAllowedException, TableMalformedException, QueryMalformedException, + ImageNotSupportedException, TableNameExistsException, AccessDeniedException, TableNotFoundException { /* mock */ if (database != null) { @@ -528,7 +571,8 @@ public class TableEndpointUnitTest extends BaseUnitTest { protected ResponseEntity<TableDto> generic_findById(Long databaseId, Long tableId, Database database, Table table, UUID userId, Principal principal, DatabaseAccess access) throws DatabaseNotFoundException, - TableNotFoundException, at.tuwien.exception.AccessDeniedException, QueueNotFoundException, BrokerRemoteException { + TableNotFoundException, at.tuwien.exception.AccessDeniedException, QueueNotFoundException, + BrokerRemoteException { /* mock */ if (table != null) { @@ -565,4 +609,16 @@ public class TableEndpointUnitTest extends BaseUnitTest { /* test */ return tableEndpoint.findById(databaseId, tableId, principal); } + + protected ResponseEntity<?> generic_delete(Principal principal, Table table) throws TableNotFoundException, + TableMalformedException, NotAllowedException, QueryMalformedException, DatabaseNotFoundException, + ImageNotSupportedException { + + /* mock */ + when(tableService.find(anyLong(), anyLong())) + .thenReturn(table); + + /* test */ + return tableEndpoint.delete(DATABASE_1_ID, TABLE_1_ID, principal); + } } diff --git a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/endpoints/ViewEndpointIntegrationTest.java b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/endpoints/ViewEndpointIntegrationTest.java deleted file mode 100644 index 2b4ae7503f06db8ce63c67bf9f7e9258c4c50053..0000000000000000000000000000000000000000 --- a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/endpoints/ViewEndpointIntegrationTest.java +++ /dev/null @@ -1,107 +0,0 @@ -package at.tuwien.endpoints; - -import at.tuwien.BaseUnitTest; -import at.tuwien.annotations.MockAmqp; -import at.tuwien.annotations.MockOpensearch; -import at.tuwien.api.database.ViewBriefDto; -import at.tuwien.config.MariaDbConfig; -import at.tuwien.config.MariaDbContainerConfig; -import at.tuwien.exception.*; -import at.tuwien.repository.mdb.*; -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.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.security.test.context.support.WithMockUser; -import org.springframework.test.context.junit.jupiter.SpringExtension; -import org.testcontainers.containers.MariaDBContainer; -import org.testcontainers.junit.jupiter.Container; -import org.testcontainers.junit.jupiter.Testcontainers; - -import java.sql.SQLException; -import java.util.List; - -import static org.junit.jupiter.api.Assertions.*; - -@Log4j2 -@SpringBootTest -@Testcontainers -@ExtendWith(SpringExtension.class) -@MockAmqp -@MockOpensearch -public class ViewEndpointIntegrationTest extends BaseUnitTest { - - @Autowired - private ImageRepository imageRepository; - - @Autowired - private LicenseRepository licenseRepository; - - @Autowired - private UserRepository userRepository; - - @Autowired - private ContainerRepository containerRepository; - - @Autowired - private DatabaseRepository databaseRepository; - - @Autowired - private ViewEndpoint viewEndpoint; - - @Container - private static MariaDBContainer<?> mariaDBContainer = MariaDbContainerConfig.getContainer(); - - @BeforeEach - public void beforeEach() throws SQLException { - TABLE_1.setColumns(TABLE_1_COLUMNS); - TABLE_2.setColumns(TABLE_2_COLUMNS); - TABLE_3.setColumns(TABLE_3_COLUMNS); - TABLE_4.setColumns(TABLE_4_COLUMNS); - /* metadata database */ - imageRepository.save(IMAGE_1); - licenseRepository.save(LICENSE_1); - userRepository.saveAll(List.of(USER_1, USER_2, USER_3)); - containerRepository.save(CONTAINER_1); - databaseRepository.save(DATABASE_1); - MariaDbConfig.dropAllDatabases(CONTAINER_1); - MariaDbConfig.createInitDatabase(CONTAINER_1, DATABASE_1); - } - - @Test - @WithMockUser(username = USER_1_USERNAME, authorities = {"create-database-view"}) - public void create_privateDatabasePublicView_succeeds() throws UserNotFoundException, NotAllowedException, - DatabaseConnectionException, ViewMalformedException, QueryMalformedException, DatabaseNotFoundException { - - /* test */ - final ResponseEntity<ViewBriefDto> response = viewEndpoint.create(DATABASE_1_ID, VIEW_1_CREATE_DTO, USER_1_PRINCIPAL); - assertEquals(HttpStatus.CREATED, response.getStatusCode()); - final ViewBriefDto body = response.getBody(); - assertNotNull(body); - assertEquals(VIEW_1_NAME, body.getName()); - assertEquals(VIEW_1_INTERNAL_NAME, body.getInternalName()); - assertEquals(VIEW_1_QUERY, body.getQuery()); - assertEquals(VIEW_1_PUBLIC, body.getIsPublic()); - } - - @Test - @WithMockUser(username = USER_1_USERNAME, authorities = {"create-database-view"}) - public void count_privateDatabasePublicView_succeeds() throws UserNotFoundException, DatabaseConnectionException, - QueryMalformedException, DatabaseNotFoundException, QueryStoreException, TableMalformedException, - ImageNotSupportedException, ContainerNotFoundException, ViewNotFoundException, SQLException { - final String request = "CREATE VIEW `" + VIEW_1_INTERNAL_NAME + "` AS (" + VIEW_1_QUERY + ");"; - - /* mock */ - MariaDbConfig.execute(DATABASE_1, request); - - /* test */ - final ResponseEntity<Long> response = viewEndpoint.count(DATABASE_1_ID, VIEW_1_ID, USER_1_PRINCIPAL); - assertEquals(HttpStatus.OK, response.getStatusCode()); - assertEquals(3L, response.getBody()); - } - -} diff --git a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/gateway/BrokerServiceGatewayTest.java b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/gateway/BrokerServiceGatewayTest.java deleted file mode 100644 index e73cc99df0b6af44e2d1634df68db2c1c9da95c9..0000000000000000000000000000000000000000 --- a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/gateway/BrokerServiceGatewayTest.java +++ /dev/null @@ -1,178 +0,0 @@ -package at.tuwien.gateway; - -import at.tuwien.BaseUnitTest; -import at.tuwien.annotations.MockAmqp; -import at.tuwien.annotations.MockOpensearch; -import at.tuwien.exception.BrokerRemoteException; -import at.tuwien.exception.BrokerVirtualHostModificationException; -import at.tuwien.exception.BrokerVirtualHostGrantException; -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.RestTemplate; - -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.Mockito.*; - -@Log4j2 -@SpringBootTest -@ExtendWith(SpringExtension.class) -@MockAmqp -@MockOpensearch -public class BrokerServiceGatewayTest extends BaseUnitTest { - - @MockBean - @Qualifier("brokerRestTemplate") - private RestTemplate restTemplate; - - @Autowired - private BrokerServiceGateway brokerServiceGateway; - - @Test - public void createVirtualHost_succeeds() throws BrokerVirtualHostModificationException, BrokerRemoteException { - final ResponseEntity<Void> mock = ResponseEntity.status(HttpStatus.CREATED) - .build(); - - /* mock */ - when(restTemplate.exchange(anyString(), eq(HttpMethod.POST), any(HttpEntity.class), eq(Void.class))) - .thenReturn(mock); - - /* test */ - brokerServiceGateway.createVirtualHost(VIRTUAL_HOST_CREATE_DTO); - } - - @Test - public void createVirtualHost_fails() { - final ResponseEntity<Void> mock = ResponseEntity.status(HttpStatus.NO_CONTENT) - .build(); - - /* mock */ - when(restTemplate.exchange(anyString(), eq(HttpMethod.POST), any(HttpEntity.class), eq(Void.class))) - .thenReturn(mock); - - /* test */ - assertThrows(BrokerVirtualHostModificationException.class, () -> { - brokerServiceGateway.createVirtualHost(VIRTUAL_HOST_CREATE_DTO); - }); - } - - @Test - public void grantPermission_exchangeNoRightsBefore_succeeds() throws BrokerVirtualHostGrantException, BrokerRemoteException { - final ResponseEntity<Void> mock = ResponseEntity.status(HttpStatus.CREATED) - .build(); - - /* mock */ - when(restTemplate.exchange(anyString(), eq(HttpMethod.PUT), any(HttpEntity.class), eq(Void.class))) - .thenReturn(mock); - - /* test */ - brokerServiceGateway.grantPermission(USER_1_USERNAME, VIRTUAL_HOST_EXCHANGE_UPDATE_DTO); - } - - @Test - public void grantPermission_exchangeRightsSame_succeeds() throws BrokerVirtualHostGrantException, BrokerRemoteException { - final ResponseEntity<Void> mock = ResponseEntity.status(HttpStatus.NO_CONTENT) - .build(); - - /* mock */ - when(restTemplate.exchange(anyString(), eq(HttpMethod.PUT), any(HttpEntity.class), eq(Void.class))) - .thenReturn(mock); - - /* test */ - brokerServiceGateway.grantPermission(USER_1_USERNAME, VIRTUAL_HOST_EXCHANGE_UPDATE_DTO); - } - - @Test - public void grantPermission_invalidResponseCode_fails() { - final ResponseEntity<Void> mock = ResponseEntity.status(HttpStatus.UNAUTHORIZED) - .build(); - - /* mock */ - when(restTemplate.exchange(anyString(), eq(HttpMethod.PUT), any(HttpEntity.class), eq(Void.class))) - .thenReturn(mock); - - /* test */ - assertThrows(BrokerVirtualHostGrantException.class, () -> { - brokerServiceGateway.grantPermission(USER_1_USERNAME, VIRTUAL_HOST_EXCHANGE_UPDATE_DTO); - }); - } - - @Test - public void grantPermission_virtualHostNoRightsBefore_succeeds() throws BrokerRemoteException, BrokerVirtualHostGrantException { - final ResponseEntity<Void> mock = ResponseEntity.status(HttpStatus.CREATED) - .build(); - - /* mock */ - when(restTemplate.exchange(anyString(), eq(HttpMethod.PUT), any(HttpEntity.class), eq(Void.class))) - .thenReturn(mock); - - /* test */ - brokerServiceGateway.grantPermission(USER_1_USERNAME, VIRTUAL_HOST_GRANT_DTO); - } - - @Test - public void grantPermission_virtualHostRightsSame_succeeds() throws BrokerRemoteException, BrokerVirtualHostGrantException { - final ResponseEntity<Void> mock = ResponseEntity.status(HttpStatus.NO_CONTENT) - .build(); - - /* mock */ - when(restTemplate.exchange(anyString(), eq(HttpMethod.PUT), any(HttpEntity.class), eq(Void.class))) - .thenReturn(mock); - - /* test */ - brokerServiceGateway.grantPermission(USER_1_USERNAME, VIRTUAL_HOST_GRANT_DTO); - } - - @Test - public void grantPermission_invalidResponseCode2_fails() { - final ResponseEntity<Void> mock = ResponseEntity.status(HttpStatus.ACCEPTED) - .build(); - - /* mock */ - when(restTemplate.exchange(anyString(), eq(HttpMethod.PUT), any(HttpEntity.class), eq(Void.class))) - .thenReturn(mock); - - /* test */ - assertThrows(BrokerVirtualHostGrantException.class, () -> { - brokerServiceGateway.grantPermission(USER_1_USERNAME, VIRTUAL_HOST_GRANT_DTO); - }); - } - - @Test - public void createUser_succeeds() throws BrokerRemoteException, BrokerVirtualHostModificationException { - final ResponseEntity<Void> mock = ResponseEntity.status(HttpStatus.NO_CONTENT) - .build(); - - /* mock */ - when(restTemplate.exchange(anyString(), eq(HttpMethod.PUT), any(HttpEntity.class), eq(Void.class))) - .thenReturn(mock); - - /* test */ - brokerServiceGateway.createUser(USER_1_USERNAME, USER_1_PASSWORD); - } - - @Test - public void createUser_invalidResponseCode_fails() { - final ResponseEntity<Void> mock = ResponseEntity.status(HttpStatus.ACCEPTED) - .build(); - - /* mock */ - when(restTemplate.exchange(anyString(), eq(HttpMethod.PUT), any(HttpEntity.class), eq(Void.class))) - .thenReturn(mock); - - /* test */ - assertThrows(BrokerVirtualHostModificationException.class, () -> { - brokerServiceGateway.createUser(USER_1_USERNAME, USER_1_PASSWORD); - }); - } - -} 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 new file mode 100644 index 0000000000000000000000000000000000000000..96c9a6e71bc8eebf9a3e2442d2542c765330e876 --- /dev/null +++ b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/gateway/BrokerServiceGatewayUnitTest.java @@ -0,0 +1,416 @@ +package at.tuwien.gateway; + +import at.tuwien.BaseUnitTest; +import at.tuwien.annotations.MockAmqp; +import at.tuwien.annotations.MockOpensearch; +import at.tuwien.api.amqp.ExchangeDto; +import at.tuwien.api.amqp.QueueDto; +import at.tuwien.exception.*; +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.RestClientException; +import org.springframework.web.client.RestTemplate; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.*; + +@Log4j2 +@SpringBootTest +@ExtendWith(SpringExtension.class) +@MockAmqp +@MockOpensearch +public class BrokerServiceGatewayUnitTest extends BaseUnitTest { + + @MockBean + @Qualifier("brokerRestTemplate") + private RestTemplate restTemplate; + + @Autowired + private BrokerServiceGateway brokerServiceGateway; + + @Test + public void createVirtualHost_succeeds() throws BrokerVirtualHostModificationException, BrokerRemoteException { + final ResponseEntity<Void> mock = ResponseEntity.status(HttpStatus.CREATED) + .build(); + + /* mock */ + when(restTemplate.exchange(anyString(), eq(HttpMethod.POST), any(HttpEntity.class), eq(Void.class))) + .thenReturn(mock); + + /* test */ + brokerServiceGateway.createVirtualHost(VIRTUAL_HOST_CREATE_DTO); + } + + @Test + public void createVirtualHost_fails() { + final ResponseEntity<Void> mock = ResponseEntity.status(HttpStatus.NO_CONTENT) + .build(); + + /* mock */ + when(restTemplate.exchange(anyString(), eq(HttpMethod.POST), any(HttpEntity.class), eq(Void.class))) + .thenReturn(mock); + + /* test */ + assertThrows(BrokerVirtualHostModificationException.class, () -> { + brokerServiceGateway.createVirtualHost(VIRTUAL_HOST_CREATE_DTO); + }); + } + + @Test + public void createVirtualHost_unexpected_fails() { + + /* mock */ + doThrow(RestClientException.class) + .when(restTemplate) + .exchange(anyString(), eq(HttpMethod.POST), any(HttpEntity.class), eq(Void.class)); + + /* test */ + assertThrows(BrokerRemoteException.class, () -> { + brokerServiceGateway.createVirtualHost(VIRTUAL_HOST_CREATE_DTO); + }); + } + + @Test + public void grantPermission_exchangeNoRightsBefore_succeeds() throws BrokerVirtualHostGrantException, BrokerRemoteException { + final ResponseEntity<Void> mock = ResponseEntity.status(HttpStatus.CREATED) + .build(); + + /* mock */ + when(restTemplate.exchange(anyString(), eq(HttpMethod.PUT), any(HttpEntity.class), eq(Void.class))) + .thenReturn(mock); + + /* test */ + brokerServiceGateway.grantPermission(USER_1_USERNAME, VIRTUAL_HOST_EXCHANGE_UPDATE_DTO); + } + + @Test + public void grantPermission_exchangeRightsSame_succeeds() throws BrokerVirtualHostGrantException, BrokerRemoteException { + final ResponseEntity<Void> mock = ResponseEntity.status(HttpStatus.NO_CONTENT) + .build(); + + /* mock */ + when(restTemplate.exchange(anyString(), eq(HttpMethod.PUT), any(HttpEntity.class), eq(Void.class))) + .thenReturn(mock); + + /* test */ + brokerServiceGateway.grantPermission(USER_1_USERNAME, VIRTUAL_HOST_EXCHANGE_UPDATE_DTO); + } + + @Test + public void grantPermission_invalidResponseCode_fails() { + final ResponseEntity<Void> mock = ResponseEntity.status(HttpStatus.UNAUTHORIZED) + .build(); + + /* mock */ + when(restTemplate.exchange(anyString(), eq(HttpMethod.PUT), any(HttpEntity.class), eq(Void.class))) + .thenReturn(mock); + + /* test */ + assertThrows(BrokerVirtualHostGrantException.class, () -> { + brokerServiceGateway.grantPermission(USER_1_USERNAME, VIRTUAL_HOST_EXCHANGE_UPDATE_DTO); + }); + } + + @Test + public void grantPermission_virtualHostNoRightsBefore_succeeds() throws BrokerRemoteException, BrokerVirtualHostGrantException { + final ResponseEntity<Void> mock = ResponseEntity.status(HttpStatus.CREATED) + .build(); + + /* mock */ + when(restTemplate.exchange(anyString(), eq(HttpMethod.PUT), any(HttpEntity.class), eq(Void.class))) + .thenReturn(mock); + + /* test */ + brokerServiceGateway.grantPermission(USER_1_USERNAME, VIRTUAL_HOST_GRANT_DTO); + } + + @Test + public void grantPermission_virtualHostRightsSame_succeeds() throws BrokerRemoteException, BrokerVirtualHostGrantException { + final ResponseEntity<Void> mock = ResponseEntity.status(HttpStatus.NO_CONTENT) + .build(); + + /* mock */ + when(restTemplate.exchange(anyString(), eq(HttpMethod.PUT), any(HttpEntity.class), eq(Void.class))) + .thenReturn(mock); + + /* test */ + brokerServiceGateway.grantPermission(USER_1_USERNAME, VIRTUAL_HOST_GRANT_DTO); + } + + @Test + public void grantPermission_invalidResponseCode2_fails() { + final ResponseEntity<Void> mock = ResponseEntity.status(HttpStatus.ACCEPTED) + .build(); + + /* mock */ + when(restTemplate.exchange(anyString(), eq(HttpMethod.PUT), any(HttpEntity.class), eq(Void.class))) + .thenReturn(mock); + + /* test */ + assertThrows(BrokerVirtualHostGrantException.class, () -> { + brokerServiceGateway.grantPermission(USER_1_USERNAME, VIRTUAL_HOST_GRANT_DTO); + }); + } + + @Test + public void grantPermission_unexpected_fails() { + + /* mock */ + doThrow(RestClientException.class) + .when(restTemplate) + .exchange(anyString(), eq(HttpMethod.PUT), any(HttpEntity.class), eq(Void.class)); + + /* test */ + assertThrows(BrokerRemoteException.class, () -> { + brokerServiceGateway.grantPermission(USER_1_USERNAME, VIRTUAL_HOST_GRANT_DTO); + }); + } + + @Test + public void grantPermission_unexpected2_fails() { + + /* mock */ + doThrow(RestClientException.class) + .when(restTemplate) + .exchange(anyString(), eq(HttpMethod.PUT), any(HttpEntity.class), eq(Void.class)); + + /* test */ + assertThrows(BrokerRemoteException.class, () -> { + brokerServiceGateway.grantPermission(USER_1_USERNAME, VIRTUAL_HOST_EXCHANGE_UPDATE_DTO); + }); + } + + @Test + public void createUser_succeeds() throws BrokerRemoteException, BrokerVirtualHostModificationException { + final ResponseEntity<Void> mock = ResponseEntity.status(HttpStatus.NO_CONTENT) + .build(); + + /* mock */ + when(restTemplate.exchange(anyString(), eq(HttpMethod.PUT), any(HttpEntity.class), eq(Void.class))) + .thenReturn(mock); + + /* test */ + brokerServiceGateway.createUser(USER_1_USERNAME, USER_1_PASSWORD); + } + + @Test + public void createUser_invalidResponseCode_fails() { + final ResponseEntity<Void> mock = ResponseEntity.status(HttpStatus.ACCEPTED) + .build(); + + /* mock */ + when(restTemplate.exchange(anyString(), eq(HttpMethod.PUT), any(HttpEntity.class), eq(Void.class))) + .thenReturn(mock); + + /* test */ + assertThrows(BrokerVirtualHostModificationException.class, () -> { + brokerServiceGateway.createUser(USER_1_USERNAME, USER_1_PASSWORD); + }); + } + + @Test + public void createUser_unexpected_fails() { + + /* mock */ + doThrow(RestClientException.class) + .when(restTemplate) + .exchange(anyString(), eq(HttpMethod.PUT), any(HttpEntity.class), eq(Void.class)); + + /* test */ + assertThrows(BrokerRemoteException.class, () -> { + brokerServiceGateway.createUser(USER_1_USERNAME, USER_1_PASSWORD); + }); + } + + @Test + public void findQueue_fails() { + final ResponseEntity<QueueDto> mock = ResponseEntity.status(HttpStatus.NO_CONTENT) + .build(); + + /* mock */ + when(restTemplate.exchange(anyString(), eq(HttpMethod.GET), any(HttpEntity.class), eq(QueueDto.class))) + .thenReturn(mock); + + /* test */ + assertThrows(QueueNotFoundException.class, () -> { + brokerServiceGateway.findQueue("dbrepo"); + }); + } + + @Test + public void findQueue_unexpected_fails() { + + /* mock */ + doThrow(RestClientException.class) + .when(restTemplate) + .exchange(anyString(), eq(HttpMethod.GET), any(HttpEntity.class), eq(QueueDto.class)); + + /* test */ + assertThrows(BrokerRemoteException.class, () -> { + brokerServiceGateway.findQueue("dbrepo"); + }); + } + + @Test + public void findQueue_succeeds() throws QueueNotFoundException, BrokerRemoteException { + final ResponseEntity<QueueDto> mock = ResponseEntity.status(HttpStatus.OK) + .build(); + + /* mock */ + when(restTemplate.exchange(anyString(), eq(HttpMethod.GET), any(HttpEntity.class), eq(QueueDto.class))) + .thenReturn(mock); + + /* test */ + brokerServiceGateway.findQueue("dbrepo"); + } + + @Test + public void findExchange_fails() { + final ResponseEntity<ExchangeDto> mock = ResponseEntity.status(HttpStatus.NO_CONTENT) + .build(); + + /* mock */ + when(restTemplate.exchange(anyString(), eq(HttpMethod.GET), any(HttpEntity.class), eq(ExchangeDto.class))) + .thenReturn(mock); + + /* test */ + assertThrows(ExchangeNotFoundException.class, () -> { + brokerServiceGateway.findExchange("dbrepo"); + }); + } + + @Test + public void findExchange_succeeds() throws BrokerRemoteException, ExchangeNotFoundException { + final ResponseEntity<ExchangeDto> mock = ResponseEntity.status(HttpStatus.OK) + .build(); + + /* mock */ + when(restTemplate.exchange(anyString(), eq(HttpMethod.GET), any(HttpEntity.class), eq(ExchangeDto.class))) + .thenReturn(mock); + + /* test */ + brokerServiceGateway.findExchange("dbrepo"); + } + + @Test + public void findExchange_unexpected_fails() { + + /* mock */ + doThrow(RestClientException.class) + .when(restTemplate) + .exchange(anyString(), eq(HttpMethod.GET), any(HttpEntity.class), eq(ExchangeDto.class)); + + /* test */ + assertThrows(BrokerRemoteException.class, () -> { + brokerServiceGateway.findExchange("dbrepo"); + }); + } + + @Test + public void deleteUser_succeeds() throws BrokerRemoteException, BrokerVirtualHostModificationException { + final ResponseEntity<Void> mock = ResponseEntity.status(HttpStatus.NO_CONTENT) + .build(); + + /* mock */ + when(restTemplate.exchange(anyString(), eq(HttpMethod.DELETE), any(HttpEntity.class), eq(Void.class))) + .thenReturn(mock); + + /* test */ + brokerServiceGateway.deleteUser(USER_1_USERNAME); + } + + @Test + public void deleteUser_fails() { + final ResponseEntity<Void> mock = ResponseEntity.status(HttpStatus.OK) + .build(); + + /* mock */ + when(restTemplate.exchange(anyString(), eq(HttpMethod.DELETE), any(HttpEntity.class), eq(Void.class))) + .thenReturn(mock); + + /* test */ + assertThrows(BrokerVirtualHostModificationException.class, () -> { + brokerServiceGateway.deleteUser(USER_1_USERNAME); + }); + } + + @Test + public void deleteUser_unexpected_fails() { + + /* mock */ + doThrow(RestClientException.class) + .when(restTemplate) + .exchange(anyString(), eq(HttpMethod.DELETE), any(HttpEntity.class), eq(Void.class)); + + /* test */ + assertThrows(BrokerRemoteException.class, () -> { + brokerServiceGateway.deleteUser(USER_1_USERNAME); + }); + } + + @Test + public void grantTopicPermission_succeeds() throws BrokerRemoteException, BrokerVirtualHostGrantException { + final ResponseEntity<Void> mock = ResponseEntity.status(HttpStatus.CREATED) + .build(); + + /* mock */ + when(restTemplate.exchange(anyString(), eq(HttpMethod.PUT), any(HttpEntity.class), eq(Void.class))) + .thenReturn(mock); + + /* test */ + brokerServiceGateway.grantTopicPermission(USER_1_USERNAME, USER_1_RABBITMQ_GRANT_TOPIC_DTO); + } + + @Test + public void grantTopicPermission_exists_succeeds() throws BrokerRemoteException, BrokerVirtualHostGrantException { + final ResponseEntity<Void> mock = ResponseEntity.status(HttpStatus.NO_CONTENT) + .build(); + + /* mock */ + when(restTemplate.exchange(anyString(), eq(HttpMethod.PUT), any(HttpEntity.class), eq(Void.class))) + .thenReturn(mock); + + /* test */ + brokerServiceGateway.grantTopicPermission(USER_1_USERNAME, USER_1_RABBITMQ_GRANT_TOPIC_DTO); + } + + @Test + public void grantTopicPermission_unexpected2_fails() { + final ResponseEntity<Void> mock = ResponseEntity.status(HttpStatus.BAD_GATEWAY) + .build(); + + /* mock */ + when(restTemplate.exchange(anyString(), eq(HttpMethod.PUT), any(HttpEntity.class), eq(Void.class))) + .thenReturn(mock); + + /* test */ + assertThrows(BrokerVirtualHostGrantException.class, () -> { + brokerServiceGateway.grantTopicPermission(USER_1_USERNAME, USER_1_RABBITMQ_GRANT_TOPIC_DTO); + }); + } + + @Test + public void grantTopicPermission_unexpected_fails() { + + /* mock */ + doThrow(RestClientException.class) + .when(restTemplate) + .exchange(anyString(), eq(HttpMethod.PUT), any(HttpEntity.class), eq(Void.class)); + + /* test */ + assertThrows(BrokerRemoteException.class, () -> { + brokerServiceGateway.grantTopicPermission(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 new file mode 100644 index 0000000000000000000000000000000000000000..fb898c1c18fdf1ac475f36c0023c82e0c1c7a1bb --- /dev/null +++ b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/gateway/CrossrefGatewayUnitTest.java @@ -0,0 +1,63 @@ +package at.tuwien.gateway; + +import at.tuwien.BaseUnitTest; +import at.tuwien.annotations.MockAmqp; +import at.tuwien.annotations.MockOpensearch; +import at.tuwien.api.crossref.CrossrefDto; +import at.tuwien.exception.DoiNotFoundException; +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.ResourceAccessException; +import org.springframework.web.client.RestTemplate; + +import static org.mockito.Mockito.*; + +@Log4j2 +@SpringBootTest +@ExtendWith(SpringExtension.class) +@MockAmqp +@MockOpensearch +public class CrossrefGatewayUnitTest extends BaseUnitTest { + + @MockBean + @Qualifier("keycloakRestTemplate") + private RestTemplate restTemplate; + + @Autowired + private CrossrefGateway crossrefGateway; + + @Test + public void findById_succeeds() throws DoiNotFoundException { + + /* mock */ + when(restTemplate.exchange(anyString(), eq(HttpMethod.GET), any(HttpEntity.class), eq(CrossrefDto.class))) + .thenReturn(ResponseEntity.status(HttpStatus.OK) + .build()); + + /* test */ + crossrefGateway.findById("501100004729"); + } + + @Test + public void findById_fails() throws DoiNotFoundException { + + /* mock */ + doThrow(ResourceAccessException.class) + .when(restTemplate) + .exchange(anyString(), eq(HttpMethod.GET), any(HttpEntity.class), eq(CrossrefDto.class)); + + /* test */ + crossrefGateway.findById("501100004729"); + } + +} diff --git a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/gateway/DataDbSidecarGatewayUnitTest.java b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/gateway/DataDbSidecarGatewayUnitTest.java new file mode 100644 index 0000000000000000000000000000000000000000..6a706c7e82c8077386884873a378ecb573e4e0a8 --- /dev/null +++ b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/gateway/DataDbSidecarGatewayUnitTest.java @@ -0,0 +1,124 @@ +package at.tuwien.gateway; + +import at.tuwien.BaseUnitTest; +import at.tuwien.annotations.MockAmqp; +import at.tuwien.annotations.MockOpensearch; +import at.tuwien.api.keycloak.TokenDto; +import at.tuwien.api.keycloak.UserDto; +import at.tuwien.exception.*; +import at.tuwien.gateway.impl.DataDbSidecarGatewayImpl; +import at.tuwien.gateway.impl.KeycloakGatewayImpl; +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.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.mockito.Mockito.*; + +@Log4j2 +@SpringBootTest +@ExtendWith(SpringExtension.class) +@MockAmqp +@MockOpensearch +public class DataDbSidecarGatewayUnitTest extends BaseUnitTest { + + @MockBean + @Qualifier("sidecarRestTemplate") + private RestTemplate restTemplate; + + @Autowired + private DataDbSidecarGatewayImpl dataDbSidecarGateway; + + @Test + public void importFile_succeeds() throws DataDbSidecarException, DataProcessingException { + + /* mock */ + when(restTemplate.exchange(anyString(), eq(HttpMethod.POST), any(HttpEntity.class), eq(Void.class))) + .thenReturn(ResponseEntity.status(HttpStatus.ACCEPTED) + .build()); + + /* test */ + dataDbSidecarGateway.importFile("data-db", 3305, "somefile.csv"); + } + + @Test + public void importFile_response_fails() { + + /* mock */ + when(restTemplate.exchange(anyString(), eq(HttpMethod.POST), any(HttpEntity.class), eq(Void.class))) + .thenReturn(ResponseEntity.status(HttpStatus.NO_CONTENT) + .build()); + + /* test */ + assertThrows(DataProcessingException.class, () -> { + dataDbSidecarGateway.importFile("data-db", 3305, "failed.csv"); + }); + } + + @Test + public void importFile_unexpected_fails() { + + /* mock */ + doThrow(ResourceAccessException.class) + .when(restTemplate) + .exchange(anyString(), eq(HttpMethod.POST), any(HttpEntity.class), eq(Void.class)); + + /* test */ + assertThrows(DataDbSidecarException.class, () -> { + dataDbSidecarGateway.importFile("data-db", 3305, "failed.csv"); + }); + } + + @Test + public void exportFile_succeeds() throws DataDbSidecarException, DataProcessingException { + + /* mock */ + when(restTemplate.exchange(anyString(), eq(HttpMethod.POST), any(HttpEntity.class), eq(Void.class))) + .thenReturn(ResponseEntity.status(HttpStatus.ACCEPTED) + .build()); + + /* test */ + dataDbSidecarGateway.exportFile("data-db", 3305, "somefile.csv"); + } + + @Test + public void exportFile_response_fails() { + + /* mock */ + when(restTemplate.exchange(anyString(), eq(HttpMethod.POST), any(HttpEntity.class), eq(Void.class))) + .thenReturn(ResponseEntity.status(HttpStatus.NO_CONTENT) + .build()); + + /* test */ + assertThrows(DataProcessingException.class, () -> { + dataDbSidecarGateway.exportFile("data-db", 3305, "failed.csv"); + }); + } + + @Test + public void exportFile_unexpected_fails() { + + /* mock */ + doThrow(ResourceAccessException.class) + .when(restTemplate) + .exchange(anyString(), eq(HttpMethod.POST), any(HttpEntity.class), eq(Void.class)); + + /* test */ + assertThrows(DataDbSidecarException.class, () -> { + dataDbSidecarGateway.exportFile("data-db", 3305, "failed.csv"); + }); + } + +} 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 new file mode 100644 index 0000000000000000000000000000000000000000..b54f60a52345dc3a1d3553fc7e6aa657f16bbf58 --- /dev/null +++ b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/gateway/KeycloakGatewayUnitTest.java @@ -0,0 +1,386 @@ +package at.tuwien.gateway; + +import at.tuwien.BaseUnitTest; +import at.tuwien.annotations.MockAmqp; +import at.tuwien.annotations.MockOpensearch; +import at.tuwien.api.amqp.ExchangeDto; +import at.tuwien.api.amqp.QueueDto; +import at.tuwien.api.keycloak.TokenDto; +import at.tuwien.api.keycloak.UserDto; +import at.tuwien.exception.*; +import at.tuwien.gateway.impl.KeycloakGatewayImpl; +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.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.mockito.Mockito.*; + +@Log4j2 +@SpringBootTest +@ExtendWith(SpringExtension.class) +@MockAmqp +@MockOpensearch +public class KeycloakGatewayUnitTest extends BaseUnitTest { + + @MockBean + @Qualifier("keycloakRestTemplate") + private RestTemplate restTemplate; + + @Autowired + private KeycloakGatewayImpl keycloakGateway; + + @Test + public void obtainToken_succeeds() throws KeycloakRemoteException, AccessDeniedException { + + /* mock */ + when(restTemplate.exchange(anyString(), eq(HttpMethod.POST), any(HttpEntity.class), eq(TokenDto.class))) + .thenReturn(ResponseEntity.status(HttpStatus.OK) + .body(TOKEN_DTO)); + + /* test */ + keycloakGateway.obtainToken(); + } + + @Test + public void obtainToken_noAccess_fails() { + + /* mock */ + doThrow(ResourceAccessException.class) + .when(restTemplate) + .exchange(anyString(), eq(HttpMethod.POST), any(HttpEntity.class), eq(TokenDto.class)); + + /* test */ + assertThrows(AccessDeniedException.class, () -> { + keycloakGateway.obtainToken(); + }); + } + + @Test + public void obtainToken_fails() { + + /* mock */ + doThrow(HttpServerErrorException.BadGateway.create(HttpStatus.BAD_GATEWAY, "", new HttpHeaders(), new byte[]{}, Charset.defaultCharset())) + .when(restTemplate) + .exchange(anyString(), eq(HttpMethod.POST), any(HttpEntity.class), eq(TokenDto.class)); + + /* test */ + assertThrows(KeycloakRemoteException.class, () -> { + keycloakGateway.obtainToken(); + }); + } + + @Test + public void createUser_succeeds() throws KeycloakRemoteException, AccessDeniedException, + UserEmailAlreadyExistsException, UserAlreadyExistsException { + + /* mock */ + when(restTemplate.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))) + .thenReturn(ResponseEntity.status(HttpStatus.CREATED) + .build()); + + /* test */ + keycloakGateway.createUser(USER_1_KEYCLOAK_SIGNUP_REQUEST); + } + + @Test + public void createUser_fails() { + + /* mock */ + when(restTemplate.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))) + .thenReturn(ResponseEntity.status(HttpStatus.NO_CONTENT) + .build()); + + /* test */ + assertThrows(KeycloakRemoteException.class, () -> { + keycloakGateway.createUser(USER_1_KEYCLOAK_SIGNUP_REQUEST); + }); + } + + @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(UserEmailAlreadyExistsException.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))) + .thenReturn(ResponseEntity.status(HttpStatus.OK) + .body(TOKEN_DTO)); + doThrow(HttpClientErrorException.Conflict.create(HttpStatus.CONFLICT, "same username", new HttpHeaders(), new byte[]{}, null)) + .when(restTemplate) + .exchange(anyString(), eq(HttpMethod.POST), any(HttpEntity.class), eq(Void.class)); + + /* test */ + assertThrows(UserAlreadyExistsException.class, () -> { + keycloakGateway.createUser(USER_1_KEYCLOAK_SIGNUP_REQUEST); + }); + } + + @Test + public void createUser_unexpected_fails() { + + /* mock */ + when(restTemplate.exchange(anyString(), eq(HttpMethod.POST), any(HttpEntity.class), eq(TokenDto.class))) + .thenReturn(ResponseEntity.status(HttpStatus.OK) + .body(TOKEN_DTO)); + doThrow(ResourceAccessException.class) + .when(restTemplate) + .exchange(anyString(), eq(HttpMethod.POST), any(HttpEntity.class), eq(Void.class)); + + /* test */ + assertThrows(KeycloakRemoteException.class, () -> { + keycloakGateway.createUser(USER_1_KEYCLOAK_SIGNUP_REQUEST); + }); + } + + @Test + public void createUser_unexpected2_fails() { + + /* 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())) + .when(restTemplate) + .exchange(anyString(), eq(HttpMethod.POST), any(HttpEntity.class), eq(Void.class)); + + /* test */ + assertThrows(KeycloakRemoteException.class, () -> { + keycloakGateway.createUser(USER_1_KEYCLOAK_SIGNUP_REQUEST); + }); + } + + @Test + public void deleteUser_fails() { + + /* mock */ + when(restTemplate.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))) + .thenReturn(ResponseEntity.status(HttpStatus.OK) + .build()); + + /* test */ + assertThrows(KeycloakRemoteException.class, () -> { + keycloakGateway.deleteUser(USER_1_ID); + }); + } + + @Test + public void deleteUser_succeeds() throws UserNotFoundException, KeycloakRemoteException, AccessDeniedException { + + /* mock */ + when(restTemplate.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))) + .thenReturn(ResponseEntity.status(HttpStatus.NO_CONTENT) + .build()); + + /* test */ + keycloakGateway.deleteUser(USER_1_ID); + } + + @Test + public void deleteUser_unexpected_fails() { + + /* mock */ + when(restTemplate.exchange(anyString(), eq(HttpMethod.POST), any(HttpEntity.class), eq(TokenDto.class))) + .thenReturn(ResponseEntity.status(HttpStatus.OK) + .body(TOKEN_DTO)); + doThrow(ResourceAccessException.class) + .when(restTemplate) + .exchange(anyString(), eq(HttpMethod.DELETE), any(HttpEntity.class), eq(Void.class)); + + /* test */ + assertThrows(KeycloakRemoteException.class, () -> { + keycloakGateway.deleteUser(USER_1_ID); + }); + } + + @Test + public void deleteUser_notFound_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.NotFound.create(HttpStatus.NOT_FOUND, "", new HttpHeaders(), new byte[]{}, Charset.defaultCharset())) + .when(restTemplate) + .exchange(anyString(), eq(HttpMethod.DELETE), any(HttpEntity.class), eq(Void.class)); + + /* test */ + assertThrows(UserNotFoundException.class, () -> { + keycloakGateway.deleteUser(USER_1_ID); + }); + } + + @Test + public void deleteUser_unexpected2_fails() { + + /* 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())) + .when(restTemplate) + .exchange(anyString(), eq(HttpMethod.DELETE), any(HttpEntity.class), eq(Void.class)); + + /* test */ + assertThrows(KeycloakRemoteException.class, () -> { + keycloakGateway.deleteUser(USER_1_ID); + }); + } + + @Test + public void updateUserCredentials_succeeds() throws KeycloakRemoteException, AccessDeniedException { + + /* mock */ + when(restTemplate.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))) + .thenReturn(ResponseEntity.status(HttpStatus.NO_CONTENT) + .build()); + + /* test */ + keycloakGateway.updateUserCredentials(USER_1_ID, USER_1_PASSWORD_DTO); + } + + @Test + public void updateUserCredentials_fails() { + + /* mock */ + when(restTemplate.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))) + .thenReturn(ResponseEntity.status(HttpStatus.OK) + .build()); + + /* test */ + assertThrows(KeycloakRemoteException.class, () -> { + keycloakGateway.updateUserCredentials(USER_1_ID, USER_1_PASSWORD_DTO); + }); + } + + @Test + public void updateUserCredentials_unexpected_fails() { + + /* mock */ + when(restTemplate.exchange(anyString(), eq(HttpMethod.POST), any(HttpEntity.class), eq(TokenDto.class))) + .thenReturn(ResponseEntity.status(HttpStatus.OK) + .body(TOKEN_DTO)); + doThrow(ResourceAccessException.class) + .when(restTemplate) + .exchange(anyString(), eq(HttpMethod.PUT), any(HttpEntity.class), eq(Void.class)); + + /* test */ + assertThrows(KeycloakRemoteException.class, () -> { + keycloakGateway.updateUserCredentials(USER_1_ID, USER_1_PASSWORD_DTO); + }); + } + + @Test + public void updateUserCredentials_unexpected2_fails() { + + /* 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())) + .when(restTemplate) + .exchange(anyString(), eq(HttpMethod.PUT), any(HttpEntity.class), eq(Void.class)); + + /* test */ + assertThrows(KeycloakRemoteException.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))) + .thenReturn(ResponseEntity.status(HttpStatus.OK) + .body(TOKEN_DTO)); + when(restTemplate.exchange(anyString(), eq(HttpMethod.GET), any(HttpEntity.class), eq(UserDto[].class))) + .thenReturn(ResponseEntity.status(HttpStatus.OK) + .body(new UserDto[]{})); + + /* test */ + assertThrows(UserNotFoundException.class, () -> { + keycloakGateway.findByUsername(USER_1_USERNAME); + }); + } + + @Test + public void findByUsername_remote_fails() { + + /* mock */ + when(restTemplate.exchange(anyString(), eq(HttpMethod.POST), any(HttpEntity.class), eq(TokenDto.class))) + .thenReturn(ResponseEntity.status(HttpStatus.OK) + .body(TOKEN_DTO)); + doThrow(ResourceAccessException.class) + .when(restTemplate) + .exchange(anyString(), eq(HttpMethod.GET), any(HttpEntity.class), eq(UserDto[].class)); + + /* test */ + assertThrows(KeycloakRemoteException.class, () -> { + keycloakGateway.findByUsername(USER_1_USERNAME); + }); + } + + @Test + public void findByUsername_unexpected_fails() { + + /* 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())) + .when(restTemplate) + .exchange(anyString(), eq(HttpMethod.GET), any(HttpEntity.class), eq(UserDto[].class)); + + /* test */ + assertThrows(KeycloakRemoteException.class, () -> { + keycloakGateway.findByUsername(USER_1_USERNAME); + }); + } + +} 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 new file mode 100644 index 0000000000000000000000000000000000000000..d6f27e21954d60cc9856ce2974adc8f48790ce39 --- /dev/null +++ b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/gateway/OrcidGatewayUnitTest.java @@ -0,0 +1,63 @@ +package at.tuwien.gateway; + +import at.tuwien.BaseUnitTest; +import at.tuwien.annotations.MockAmqp; +import at.tuwien.annotations.MockOpensearch; +import at.tuwien.api.orcid.OrcidDto; +import at.tuwien.exception.OrcidNotFoundException; +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.ResourceAccessException; +import org.springframework.web.client.RestTemplate; + +import static org.mockito.Mockito.*; + +@Log4j2 +@SpringBootTest +@ExtendWith(SpringExtension.class) +@MockAmqp +@MockOpensearch +public class OrcidGatewayUnitTest extends BaseUnitTest { + + @MockBean + @Qualifier("keycloakRestTemplate") + private RestTemplate restTemplate; + + @Autowired + private OrcidGateway orcidGateway; + + @Test + public void findByUrl_succeeds() throws OrcidNotFoundException { + + /* mock */ + when(restTemplate.exchange(anyString(), eq(HttpMethod.GET), any(HttpEntity.class), eq(OrcidDto.class))) + .thenReturn(ResponseEntity.status(HttpStatus.OK) + .build()); + + /* test */ + orcidGateway.findByUrl(USER_1_ORCID_URL); + } + + @Test + public void findByUrl_fails() throws OrcidNotFoundException { + + /* mock */ + doThrow(ResourceAccessException.class) + .when(restTemplate) + .exchange(anyString(), eq(HttpMethod.GET), any(HttpEntity.class), eq(OrcidDto.class)); + + /* test */ + 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 new file mode 100644 index 0000000000000000000000000000000000000000..29f6455ebff41a73cb354469f702685d408f5a44 --- /dev/null +++ b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/gateway/RorGatewayUnitTest.java @@ -0,0 +1,68 @@ +package at.tuwien.gateway; + +import at.tuwien.BaseUnitTest; +import at.tuwien.annotations.MockAmqp; +import at.tuwien.annotations.MockOpensearch; +import at.tuwien.api.keycloak.TokenDto; +import at.tuwien.api.keycloak.UserDto; +import at.tuwien.api.ror.RorDto; +import at.tuwien.exception.*; +import at.tuwien.gateway.impl.KeycloakGatewayImpl; +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.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.mockito.Mockito.*; + +@Log4j2 +@SpringBootTest +@ExtendWith(SpringExtension.class) +@MockAmqp +@MockOpensearch +public class RorGatewayUnitTest extends BaseUnitTest { + + @MockBean + @Qualifier("keycloakRestTemplate") + private RestTemplate restTemplate; + + @Autowired + private RorGateway rorGateway; + + @Test + public void findById_succeeds() throws RorNotFoundException { + + /* mock */ + when(restTemplate.exchange(anyString(), eq(HttpMethod.GET), any(HttpEntity.class), eq(RorDto.class))) + .thenReturn(ResponseEntity.status(HttpStatus.OK) + .build()); + + /* test */ + rorGateway.findById("04d836q62"); + } + + @Test + public void findById_fails() throws RorNotFoundException { + + /* mock */ + doThrow(ResourceAccessException.class) + .when(restTemplate) + .exchange(anyString(), eq(HttpMethod.GET), any(HttpEntity.class), eq(RorDto.class)); + + /* test */ + rorGateway.findById("04d836q62"); + } + +} 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 680754fd3fd5ef98dda489dbd407a324960c9484..94a97d3643140ef3de727720821c1e1b37f94bb0 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 @@ -197,7 +197,7 @@ public class PrometheusEndpointMvcTest extends BaseUnitTest { } @Test - @WithMockUser(username = USER_1_USERNAME, authorities = {"create-database", "modify-database-visibility", "modify-database-owner", "delete-database"}) + @WithMockUser(username = USER_1_USERNAME, authorities = {"create-database", "modify-database-visibility", "modify-database-owner", "delete-database", "modify-database-image"}) public void prometheusDatabaseEndpoint_succeeds() { /* mock */ @@ -231,9 +231,14 @@ public class PrometheusEndpointMvcTest extends BaseUnitTest { } catch (Exception e) { /* ignore */ } + try { + databaseEndpoint.modifyImage(DATABASE_1_ID, DatabaseModifyImageDto.builder().build(), USER_1_PRINCIPAL); + } catch (Exception e) { + /* ignore */ + } /* test */ - for (String metric : List.of("dbr_database_findall", "dbr_database_count", "dbr_database_create", "dbr_database_visibility", "dbr_database_transfer", "dbr_database_find")) { + for (String metric : List.of("dbr_database_findall", "dbr_database_count", "dbr_database_create", "dbr_database_visibility", "dbr_database_transfer", "dbr_database_find", "dbr_database_image")) { assertThat(registry) .hasObservationWithNameEqualTo(metric); } diff --git a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/AccessServiceIntegrationTest.java b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/AccessServiceIntegrationTest.java index bd8ce85f76c012828dd350d74927fc30b2fc8830..ce2a42a9cd37cc34c7709362ed097cf6d7e29183 100644 --- a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/AccessServiceIntegrationTest.java +++ b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/AccessServiceIntegrationTest.java @@ -2,6 +2,7 @@ package at.tuwien.service; import at.tuwien.BaseUnitTest; import at.tuwien.annotations.MockAmqp; +import at.tuwien.annotations.MockListeners; import at.tuwien.annotations.MockOpensearch; import at.tuwien.api.database.AccessTypeDto; import at.tuwien.api.database.DatabaseGiveAccessDto; @@ -43,6 +44,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows; @DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD) @ExtendWith(SpringExtension.class) @MockAmqp +@MockListeners @MockOpensearch public class AccessServiceIntegrationTest extends BaseUnitTest { 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 59f15c219b7da587a55a5095806fdb892b3a3e0b..b2ae2d80b3555d5d2e87851c89d368e086460800 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 @@ -2,6 +2,7 @@ package at.tuwien.service; import at.tuwien.BaseUnitTest; import at.tuwien.annotations.MockAmqp; +import at.tuwien.annotations.MockListeners; import at.tuwien.annotations.MockOpensearch; import at.tuwien.api.database.AccessTypeDto; import at.tuwien.api.database.DatabaseModifyAccessDto; @@ -33,6 +34,7 @@ import static org.mockito.Mockito.when; @SpringBootTest @ExtendWith(SpringExtension.class) @MockAmqp +@MockListeners @MockOpensearch public class AccessServiceUnitTest extends BaseUnitTest { 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 cfa8f9251d122e30299e6443ef161e4288fd3d71..83d9fe5d58580cf1343247b3ca0ac97f2f7ff783 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 @@ -2,6 +2,7 @@ package at.tuwien.service; import at.tuwien.BaseUnitTest; import at.tuwien.annotations.MockAmqp; +import at.tuwien.annotations.MockListeners; import at.tuwien.annotations.MockOpensearch; import at.tuwien.exception.*; import at.tuwien.gateway.KeycloakGateway; @@ -28,6 +29,7 @@ import org.testcontainers.junit.jupiter.Testcontainers; @SpringBootTest @ExtendWith(SpringExtension.class) @MockAmqp +@MockListeners @MockOpensearch public class AuthenticationServiceIntegrationTest extends BaseUnitTest { diff --git a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/BannerMessageServiceIntegrationTest.java b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/BannerMessageServiceIntegrationTest.java index bbf0d7d64d910add854b3c38414fbae8fe9c0e3c..8ed3f1f9d707cd9254d1eced1c1bdc712caf2a93 100644 --- a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/BannerMessageServiceIntegrationTest.java +++ b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/BannerMessageServiceIntegrationTest.java @@ -2,6 +2,7 @@ package at.tuwien.service; import at.tuwien.BaseUnitTest; import at.tuwien.annotations.MockAmqp; +import at.tuwien.annotations.MockListeners; import at.tuwien.annotations.MockOpensearch; import at.tuwien.api.maintenance.BannerMessageCreateDto; import at.tuwien.api.maintenance.BannerMessageTypeDto; @@ -32,6 +33,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows; @SpringBootTest @ExtendWith(SpringExtension.class) @MockAmqp +@MockListeners @MockOpensearch public class BannerMessageServiceIntegrationTest extends BaseUnitTest { diff --git a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/ContainerServiceIntegrationTest.java b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/ContainerServiceIntegrationTest.java index 002ddeb1300802f0a19d385d258a2cc8dc7c48d7..172c9801d6ff366500299c66aa31bee0d18fd816 100644 --- a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/ContainerServiceIntegrationTest.java +++ b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/ContainerServiceIntegrationTest.java @@ -2,6 +2,7 @@ package at.tuwien.service; import at.tuwien.BaseUnitTest; import at.tuwien.annotations.MockAmqp; +import at.tuwien.annotations.MockListeners; import at.tuwien.annotations.MockOpensearch; import at.tuwien.api.container.ContainerCreateRequestDto; import at.tuwien.entities.container.Container; @@ -27,6 +28,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows; @SpringBootTest @ExtendWith(SpringExtension.class) @MockAmqp +@MockListeners @MockOpensearch public class ContainerServiceIntegrationTest extends BaseUnitTest { diff --git a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/DataCiteIdentifierServiceIntegrationTest.java b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/DataCiteIdentifierServiceIntegrationTest.java index c548b7b2193379619e9d28f0c51eee873046bf92..4c903a82ebffe199c62bfbbfbc2677666c868e67 100644 --- a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/DataCiteIdentifierServiceIntegrationTest.java +++ b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/DataCiteIdentifierServiceIntegrationTest.java @@ -2,6 +2,7 @@ package at.tuwien.service; import at.tuwien.BaseUnitTest; import at.tuwien.annotations.MockAmqp; +import at.tuwien.annotations.MockListeners; import at.tuwien.annotations.MockOpensearch; import at.tuwien.api.datacite.DataCiteBody; import at.tuwien.api.datacite.DataCiteData; @@ -40,6 +41,7 @@ import static org.mockito.Mockito.when; @DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD) @SpringBootTest(properties = "spring.profiles.active:local,doi") @MockAmqp +@MockListeners @MockOpensearch public class DataCiteIdentifierServiceIntegrationTest extends BaseUnitTest { diff --git a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/DataCiteIdentifierServiceUnitTest.java b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/DataCiteIdentifierServiceUnitTest.java index 6bcf84616f83a5501b9dea0d1b72e77fe06cccee..afa00b5308ec786ea185c3c2d63d26d7b2d2e211 100644 --- a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/DataCiteIdentifierServiceUnitTest.java +++ b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/DataCiteIdentifierServiceUnitTest.java @@ -2,6 +2,7 @@ package at.tuwien.service; import at.tuwien.BaseUnitTest; import at.tuwien.annotations.MockAmqp; +import at.tuwien.annotations.MockListeners; import at.tuwien.annotations.MockOpensearch; import at.tuwien.api.datacite.DataCiteBody; import at.tuwien.api.datacite.DataCiteData; @@ -45,6 +46,7 @@ import static org.mockito.Mockito.when; @DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD) @SpringBootTest(properties = "spring.profiles.active:local,doi") @MockAmqp +@MockListeners @MockOpensearch public class DataCiteIdentifierServiceUnitTest extends BaseUnitTest { diff --git a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/DatabaseServiceComponentTest.java b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/DatabaseServiceComponentTest.java index 18de26397aa4991956ee5494ce6df5b098ffce49..0f53ecd57351365532d5aa82e2ff24aad6e95c00 100644 --- a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/DatabaseServiceComponentTest.java +++ b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/DatabaseServiceComponentTest.java @@ -2,6 +2,7 @@ package at.tuwien.service; import at.tuwien.BaseUnitTest; import at.tuwien.annotations.MockAmqp; +import at.tuwien.annotations.MockListeners; import at.tuwien.annotations.MockOpensearch; import at.tuwien.api.database.DatabaseCreateDto; import at.tuwien.api.database.DatabaseDto; @@ -38,6 +39,7 @@ import static org.mockito.Mockito.when; @ExtendWith(SpringExtension.class) @SpringBootTest @MockAmqp +@MockListeners @MockOpensearch public class DatabaseServiceComponentTest extends BaseUnitTest { diff --git a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/DatabaseServiceIntegrationTest.java b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/DatabaseServiceIntegrationTest.java index 91dc9c7d9ca5b69ba3f43999082b799912a1a965..c51f14874500e4763242c9e666d0321ea0dbd48c 100644 --- a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/DatabaseServiceIntegrationTest.java +++ b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/DatabaseServiceIntegrationTest.java @@ -2,6 +2,7 @@ package at.tuwien.service; import at.tuwien.BaseUnitTest; import at.tuwien.annotations.MockAmqp; +import at.tuwien.annotations.MockListeners; import at.tuwien.annotations.MockOpensearch; import at.tuwien.api.database.*; import at.tuwien.config.MariaDbConfig; @@ -47,6 +48,7 @@ import static org.mockito.Mockito.*; @ExtendWith(SpringExtension.class) @SpringBootTest @MockAmqp +@MockListeners @MockOpensearch public class DatabaseServiceIntegrationTest extends BaseUnitTest { @@ -339,7 +341,7 @@ public class DatabaseServiceIntegrationTest extends BaseUnitTest { } @Test - public void obtainMetadata_tableWithoutVersioning_succeeds() throws DatabaseUnchangedException, QueryMalformedException, + public void obtainMetadata_tableWithoutVersioning_succeeds() throws QueryMalformedException, DatabaseNotFoundException, ColumnParseException { /* test */ @@ -358,8 +360,8 @@ public class DatabaseServiceIntegrationTest extends BaseUnitTest { } @Test - public void obtainMetadata_tableWithVersioning_succeeds() throws DatabaseUnchangedException, QueryMalformedException, - DatabaseNotFoundException, ColumnParseException { + public void obtainMetadata_tableWithVersioning_succeeds() throws QueryMalformedException, DatabaseNotFoundException, + ColumnParseException { /* test */ final Database response = databaseService.obtainMetadata(DATABASE_1_ID); @@ -378,8 +380,8 @@ public class DatabaseServiceIntegrationTest extends BaseUnitTest { } @Test - public void obtainMetadata_view_succeeds() throws DatabaseUnchangedException, QueryMalformedException, - DatabaseNotFoundException, ColumnParseException { + public void obtainMetadata_view_succeeds() throws QueryMalformedException, DatabaseNotFoundException, + ColumnParseException { /* test */ final Database response = databaseService.obtainMetadata(DATABASE_1_ID); 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 576129336f66bdeb543b5e08e3b6951fa04f491c..4eea12c7f0f8b6fd2c32f0f4d8f227b5bba815e7 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 @@ -2,6 +2,7 @@ package at.tuwien.service; import at.tuwien.BaseUnitTest; import at.tuwien.annotations.MockAmqp; +import at.tuwien.annotations.MockListeners; import at.tuwien.annotations.MockOpensearch; import at.tuwien.api.database.DatabaseCreateDto; import at.tuwien.entities.database.Database; @@ -28,6 +29,7 @@ import static org.mockito.Mockito.when; @SpringBootTest @ExtendWith(SpringExtension.class) @MockAmqp +@MockListeners @MockOpensearch public class DatabaseServiceUnitTest extends BaseUnitTest { diff --git a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/EntityServiceIntegrationTest.java b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/EntityServiceIntegrationTest.java index bd015711b96f0532af788f099b450fecab980034..0fb358df3548d5b25a3d2e33f360b98071168a4c 100644 --- a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/EntityServiceIntegrationTest.java +++ b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/EntityServiceIntegrationTest.java @@ -2,6 +2,7 @@ package at.tuwien.service; import at.tuwien.BaseUnitTest; import at.tuwien.annotations.MockAmqp; +import at.tuwien.annotations.MockListeners; import at.tuwien.annotations.MockOpensearch; import at.tuwien.api.semantics.EntityDto; import at.tuwien.api.semantics.TableColumnEntityDto; @@ -25,6 +26,7 @@ import static org.junit.jupiter.api.Assertions.*; @DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD) @ExtendWith(SpringExtension.class) @MockAmqp +@MockListeners @MockOpensearch public class EntityServiceIntegrationTest extends BaseUnitTest { diff --git a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/IdentifierServiceIntegrationTest.java b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/IdentifierServiceIntegrationTest.java index a08746df1c5038295f1b91271f7ee9b66ead1931..2e5a57a87065703abc794e91c1ac0cddf0ab000c 100644 --- a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/IdentifierServiceIntegrationTest.java +++ b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/IdentifierServiceIntegrationTest.java @@ -174,11 +174,9 @@ public class IdentifierServiceIntegrationTest extends BaseUnitTest { @Test @Transactional - public void create_subsetRelatedIdentifiers_succeeds() - throws DatabaseNotFoundException, UserNotFoundException, IdentifierAlreadyExistsException, - QueryNotFoundException, IdentifierPublishingNotAllowedException, RemoteUnavailableException, - IdentifierRequestException, ViewNotFoundException, QueryStoreException, DatabaseConnectionException, - ImageNotSupportedException, IdentifierNotFoundException { + public void create_subsetRelatedIdentifiers_succeeds() throws DatabaseNotFoundException, UserNotFoundException, + QueryNotFoundException, RemoteUnavailableException, IdentifierRequestException, ViewNotFoundException, + QueryStoreException, ImageNotSupportedException { /* mock */ when(restTemplate.exchange(anyString(), any(HttpMethod.class), any(HttpEntity.class), eq(QueryDto.class))) diff --git a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/IdentifierServiceUnitTest.java b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/IdentifierServiceUnitTest.java index 3ad31da548073e7910e9c28b01725e8c78e2703f..8b67a8548f6cea349602a3a9a72880e5cf7c6f26 100644 --- a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/IdentifierServiceUnitTest.java +++ b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/IdentifierServiceUnitTest.java @@ -2,15 +2,14 @@ package at.tuwien.service; import at.tuwien.BaseUnitTest; import at.tuwien.annotations.MockAmqp; +import at.tuwien.annotations.MockListeners; import at.tuwien.annotations.MockOpensearch; import at.tuwien.api.database.query.QueryDto; import at.tuwien.api.identifier.BibliographyTypeDto; -import at.tuwien.entities.database.Database; import at.tuwien.entities.identifier.Creator; import at.tuwien.entities.identifier.Identifier; import at.tuwien.entities.identifier.NameIdentifierSchemeType; import at.tuwien.exception.*; -import at.tuwien.repository.mdb.DatabaseRepository; import at.tuwien.repository.mdb.IdentifierRepository; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -36,15 +35,13 @@ import static org.mockito.Mockito.when; @ExtendWith(SpringExtension.class) @SpringBootTest @MockAmqp +@MockListeners @MockOpensearch public class IdentifierServiceUnitTest extends BaseUnitTest { @MockBean private IdentifierRepository identifierRepository; - @MockBean - private DatabaseRepository databaseRepository; - @MockBean private DatabaseService databaseService; @@ -176,11 +173,9 @@ public class IdentifierServiceUnitTest extends BaseUnitTest { } @Test - public void create_database_succeeds() - throws DatabaseNotFoundException, UserNotFoundException, IdentifierAlreadyExistsException, - QueryNotFoundException, IdentifierPublishingNotAllowedException, RemoteUnavailableException, - IdentifierRequestException, ViewNotFoundException, QueryStoreException, DatabaseConnectionException, - ImageNotSupportedException, IdentifierNotFoundException { + public void create_database_succeeds() throws UserNotFoundException, QueryStoreException, + QueryNotFoundException, DatabaseNotFoundException, ImageNotSupportedException, RemoteUnavailableException, + IdentifierRequestException, ViewNotFoundException { /* mock */ when(databaseService.find(DATABASE_1_ID)) @@ -198,10 +193,9 @@ public class IdentifierServiceUnitTest extends BaseUnitTest { } @Test - public void create_existsSubset_succeeds() throws DatabaseNotFoundException, UserNotFoundException, - QueryStoreException, DatabaseConnectionException, QueryNotFoundException, ImageNotSupportedException, - IdentifierAlreadyExistsException, IdentifierPublishingNotAllowedException, RemoteUnavailableException, - IdentifierNotFoundException, IdentifierRequestException, ViewNotFoundException { + public void create_existsSubset_succeeds() throws UserNotFoundException, QueryStoreException, + QueryNotFoundException, DatabaseNotFoundException, ImageNotSupportedException, RemoteUnavailableException, + IdentifierRequestException, ViewNotFoundException { /* mock */ when(databaseService.find(DATABASE_2_ID)) @@ -217,10 +211,9 @@ public class IdentifierServiceUnitTest extends BaseUnitTest { } @Test - public void create_existsDatabase_succeeds() throws DatabaseNotFoundException, IdentifierAlreadyExistsException, - UserNotFoundException, QueryStoreException, DatabaseConnectionException, QueryNotFoundException, - ImageNotSupportedException, IdentifierPublishingNotAllowedException, RemoteUnavailableException, - IdentifierNotFoundException, IdentifierRequestException, ViewNotFoundException { + public void create_existsDatabase_succeeds() throws UserNotFoundException, QueryStoreException, + QueryNotFoundException, DatabaseNotFoundException, ImageNotSupportedException, RemoteUnavailableException, + IdentifierRequestException, ViewNotFoundException { /* mock */ when(databaseService.find(DATABASE_1_ID)) diff --git a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/ImageServiceIntegrationTest.java b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/ImageServiceIntegrationTest.java index 92d0c06fbf2d6c112b6c4ca999cdb5d674f58f11..07c32c520599bb795026ae9b3cef1219e878d768 100644 --- a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/ImageServiceIntegrationTest.java +++ b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/ImageServiceIntegrationTest.java @@ -2,6 +2,7 @@ package at.tuwien.service; import at.tuwien.BaseUnitTest; import at.tuwien.annotations.MockAmqp; +import at.tuwien.annotations.MockListeners; import at.tuwien.annotations.MockOpensearch; import at.tuwien.api.container.image.ImageCreateDto; import at.tuwien.exception.ImageAlreadyExistsException; @@ -28,6 +29,7 @@ import static org.junit.jupiter.api.Assertions.*; @ExtendWith(SpringExtension.class) @SpringBootTest @MockAmqp +@MockListeners @MockOpensearch public class ImageServiceIntegrationTest extends BaseUnitTest { diff --git a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/ImageServiceUnitTest.java b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/ImageServiceUnitTest.java index eaa6fc8bf9c21a020eec675d130059be3121cf19..620e66dacd3be1eeefa17187d855c7e478f18bf9 100644 --- a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/ImageServiceUnitTest.java +++ b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/ImageServiceUnitTest.java @@ -2,6 +2,7 @@ package at.tuwien.service; import at.tuwien.BaseUnitTest; import at.tuwien.annotations.MockAmqp; +import at.tuwien.annotations.MockListeners; import at.tuwien.annotations.MockOpensearch; import at.tuwien.api.container.image.ImageChangeDto; import at.tuwien.api.container.image.ImageCreateDto; @@ -29,6 +30,7 @@ import static org.mockito.Mockito.*; @ExtendWith(SpringExtension.class) @SpringBootTest @MockAmqp +@MockListeners @MockOpensearch public class ImageServiceUnitTest extends BaseUnitTest { diff --git a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/LicenseServiceIntegrationTest.java b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/LicenseServiceIntegrationTest.java index eca8d1bc914ede6e2d792970755c5e7dd681e84c..a9e4490d0925fe2268e980a32a9150d391fda1ba 100644 --- a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/LicenseServiceIntegrationTest.java +++ b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/LicenseServiceIntegrationTest.java @@ -2,6 +2,7 @@ package at.tuwien.service; import at.tuwien.BaseUnitTest; import at.tuwien.annotations.MockAmqp; +import at.tuwien.annotations.MockListeners; import at.tuwien.annotations.MockOpensearch; import at.tuwien.config.MariaDbContainerConfig; import at.tuwien.entities.database.License; @@ -28,6 +29,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows; @SpringBootTest @ExtendWith(SpringExtension.class) @MockAmqp +@MockListeners @MockOpensearch public class LicenseServiceIntegrationTest extends BaseUnitTest { diff --git a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/MessageQueueServiceIntegrationTest.java b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/MessageQueueServiceIntegrationTest.java index 7a44e6c2bb2c7631504140ca2fefad447f340d78..28ccbb19e31e55176424fb59d38f05a79b2d3ebc 100644 --- a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/MessageQueueServiceIntegrationTest.java +++ b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/MessageQueueServiceIntegrationTest.java @@ -1,6 +1,7 @@ package at.tuwien.service; import at.tuwien.BaseUnitTest; +import at.tuwien.annotations.MockListeners; import at.tuwien.annotations.MockOpensearch; import at.tuwien.api.amqp.GrantExchangePermissionsDto; import at.tuwien.api.amqp.TopicPermissionDto; @@ -39,6 +40,7 @@ import static org.junit.jupiter.api.Assertions.*; @ExtendWith(SpringExtension.class) @DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD) @MockOpensearch +@MockListeners public class MessageQueueServiceIntegrationTest extends BaseUnitTest { @Autowired diff --git a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/MetadataServiceIntegrationTest.java b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/MetadataServiceIntegrationTest.java index d4f3c634eb70abdc6943b277a9938e8ad3ded751..7ec7d0a78475e561b177dbc16d911a4d9d7a8bd5 100644 --- a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/MetadataServiceIntegrationTest.java +++ b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/MetadataServiceIntegrationTest.java @@ -2,6 +2,7 @@ package at.tuwien.service; import at.tuwien.BaseUnitTest; import at.tuwien.annotations.MockAmqp; +import at.tuwien.annotations.MockListeners; import at.tuwien.annotations.MockOpensearch; import at.tuwien.oaipmh.OaiErrorType; import at.tuwien.oaipmh.OaiListIdentifiersParameters; @@ -24,6 +25,7 @@ import static org.junit.jupiter.api.Assertions.*; @SpringBootTest @DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD) @MockAmqp +@MockListeners @MockOpensearch public class MetadataServiceIntegrationTest extends BaseUnitTest { diff --git a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/MetadataServiceUnitTest.java b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/MetadataServiceUnitTest.java index 1f06ad3c38e7490001c2e9ec52ba0a1a699565cb..f731e99d25a78f7b46851865ec5c6b883e25a479 100644 --- a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/MetadataServiceUnitTest.java +++ b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/MetadataServiceUnitTest.java @@ -2,6 +2,7 @@ package at.tuwien.service; import at.tuwien.BaseUnitTest; import at.tuwien.annotations.MockAmqp; +import at.tuwien.annotations.MockListeners; import at.tuwien.annotations.MockOpensearch; import at.tuwien.api.crossref.CrossrefDto; import at.tuwien.api.orcid.OrcidDto; @@ -38,6 +39,7 @@ import static org.mockito.Mockito.when; @ExtendWith(SpringExtension.class) @SpringBootTest @MockAmqp +@MockListeners @MockOpensearch public class MetadataServiceUnitTest extends BaseUnitTest { diff --git a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/PersistenceIntegrationTest.java b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/PersistenceIntegrationTest.java index 5da05bc8a4694d32970e6b500ef8d32267d96677..5f6e1045f57c1c257130ac4d1f770ca92107ad44 100644 --- a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/PersistenceIntegrationTest.java +++ b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/PersistenceIntegrationTest.java @@ -2,6 +2,7 @@ package at.tuwien.service; import at.tuwien.BaseUnitTest; import at.tuwien.annotations.MockAmqp; +import at.tuwien.annotations.MockListeners; import at.tuwien.annotations.MockOpensearch; import at.tuwien.exception.ImageNotFoundException; import at.tuwien.repository.mdb.ImageRepository; @@ -22,6 +23,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows; @ExtendWith(SpringExtension.class) @SpringBootTest @MockAmqp +@MockListeners @MockOpensearch public class PersistenceIntegrationTest extends BaseUnitTest { diff --git a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/QueryServiceIntegrationTest.java b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/QueryServiceIntegrationTest.java index 78298edfea16ae9b62e7f1133e6225d42b754670..77af8cb2fcd0e400b5ab5c5c0d60adad1f05993b 100644 --- a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/QueryServiceIntegrationTest.java +++ b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/QueryServiceIntegrationTest.java @@ -3,9 +3,12 @@ package at.tuwien.service; import at.tuwien.BaseUnitTest; import at.tuwien.ExportResource; import at.tuwien.annotations.MockAmqp; +import at.tuwien.annotations.MockListeners; import at.tuwien.annotations.MockOpensearch; import at.tuwien.api.database.query.ExecuteStatementDto; +import at.tuwien.api.database.query.ImportDto; import at.tuwien.api.database.query.QueryResultDto; +import at.tuwien.api.database.table.TableCsvDeleteDto; import at.tuwien.api.database.table.TableCsvDto; import at.tuwien.config.MariaDbConfig; import at.tuwien.config.MariaDbContainerConfig; @@ -17,6 +20,7 @@ import at.tuwien.repository.mdb.*; import at.tuwien.service.impl.QueryServiceImpl; import lombok.SneakyThrows; import lombok.extern.log4j.Log4j2; +import org.apache.commons.io.FileUtils; import org.apache.commons.lang3.RandomStringUtils; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -35,6 +39,7 @@ import org.testcontainers.containers.MinIOContainer; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; +import java.io.File; import java.io.IOException; import java.math.BigInteger; import java.sql.SQLException; @@ -58,6 +63,7 @@ import static org.mockito.Mockito.doNothing; @SpringBootTest @ExtendWith(SpringExtension.class) @MockAmqp +@MockListeners @MockOpensearch public class QueryServiceIntegrationTest extends BaseUnitTest { @@ -117,14 +123,14 @@ public class QueryServiceIntegrationTest extends BaseUnitTest { databaseRepository.saveAll(List.of(DATABASE_1, DATABASE_2)); /* mock */ MariaDbConfig.dropAllDatabases(CONTAINER_1); - MariaDbConfig.createInitDatabase(CONTAINER_1, DATABASE_2); MariaDbConfig.createInitDatabase(CONTAINER_1, DATABASE_1); + MariaDbConfig.createInitDatabase(CONTAINER_1, DATABASE_2); } @Test public void findAll_succeeds() throws DatabaseNotFoundException, ImageNotSupportedException, - TableMalformedException, TableNotFoundException, DatabaseConnectionException, - PaginationException, QueryMalformedException, UserNotFoundException { + TableMalformedException, TableNotFoundException, DatabaseConnectionException, PaginationException, + QueryMalformedException, UserNotFoundException { /* test */ final QueryResultDto result = queryService.tableFindAll(DATABASE_1_ID, TABLE_1_ID, Instant.now(), @@ -148,8 +154,8 @@ public class QueryServiceIntegrationTest extends BaseUnitTest { } @Test - public void selectAll_succeeds() throws TableNotFoundException, DatabaseNotFoundException, - ImageNotSupportedException, TableMalformedException, QueryMalformedException { + public void selectAll_succeeds() throws TableNotFoundException, DatabaseNotFoundException, TableMalformedException, + ImageNotSupportedException, QueryMalformedException { final Long page = 0L; final Long size = 10L; @@ -181,8 +187,26 @@ public class QueryServiceIntegrationTest extends BaseUnitTest { } @Test - public void insert_date_succeeds() throws TableNotFoundException, TableMalformedException, - DatabaseNotFoundException, SQLException { + public void insert_csv_succeeds() throws IOException, TableNotFoundException, TableMalformedException, + DatabaseNotFoundException, DataProcessingException { + final String filename = RandomStringUtils.randomAlphabetic(40) + ".csv"; + final ImportDto request = ImportDto.builder() + .quote('"') + .nullElement("NA") + .separator(';') + .location(filename) + .build(); + + /* mock */ + FileUtils.copyFile(new File("./src/test/resources/csv/weather_aus.csv"), new File("/tmp/" + filename)); + + /* test */ + queryService.insert(DATABASE_1_ID, TABLE_1_ID, request, USER_1_PRINCIPAL); + } + + @Test + public void insert_date_succeeds() throws TableNotFoundException, TableMalformedException, SQLException, + DatabaseNotFoundException { final TableCsvDto request = TableCsvDto.builder() .data(new HashMap<>() {{ put("id", 4L); @@ -291,18 +315,16 @@ public class QueryServiceIntegrationTest extends BaseUnitTest { } @Test - public void findAll_timestampMissing_succeeds() throws TableNotFoundException, DatabaseConnectionException, - TableMalformedException, DatabaseNotFoundException, ImageNotSupportedException, PaginationException, - QueryMalformedException, UserNotFoundException { + public void findAll_timestampMissing_succeeds() throws TableNotFoundException, TableMalformedException, + DatabaseNotFoundException, ImageNotSupportedException, QueryMalformedException { /* test */ queryService.tableFindAll(DATABASE_1_ID, TABLE_1_ID, null, null, null, USER_1_PRINCIPAL); } @Test - public void findAll_timestampBeforeCreation_succeeds() throws TableNotFoundException, DatabaseConnectionException, - TableMalformedException, DatabaseNotFoundException, ImageNotSupportedException, PaginationException, - QueryMalformedException, UserNotFoundException { + public void findAll_timestampBeforeCreation_succeeds() throws TableNotFoundException, TableMalformedException, + DatabaseNotFoundException, ImageNotSupportedException, QueryMalformedException { final Instant timestamp = DATABASE_1_CREATED.minus(1, ChronoUnit.SECONDS); /* test */ @@ -311,12 +333,11 @@ public class QueryServiceIntegrationTest extends BaseUnitTest { } @Test - public void execute_succeeds() throws DatabaseConnectionException, TableMalformedException, - DatabaseNotFoundException, ImageNotSupportedException, QueryMalformedException, UserNotFoundException, - QueryStoreException, ColumnParseException, InterruptedException, KeycloakRemoteException, - AccessDeniedException, QueryNotFoundException { + public void execute_succeeds() throws TableMalformedException, DatabaseNotFoundException, + ImageNotSupportedException, QueryMalformedException, UserNotFoundException, QueryStoreException, + ColumnParseException, InterruptedException, QueryNotFoundException { final ExecuteStatementDto request = ExecuteStatementDto.builder() - .statement("SELECT n.`firstname`, n.`lastname`, n.`birth`, n.`reminder`, z.`animal_name`, z.`legs` FROM `likes` l JOIN `names` n ON l.`name_id` = n.`id` JOIN `mock_view` z ON z.`id` = l.`zoo_id` ORDER BY animal_name ASC") + .statement("SELECT n.`id`, n.`firstname`, n.`lastname`, n.`birth`, n.`reminder`, z.`animal_name`, z.`legs` FROM `likes` l JOIN `names` n ON l.`name_id` = n.`id` JOIN `mock_view` z ON z.`id` = l.`zoo_id` ORDER BY id, animal_name ASC") .build(); /* pre-condition */ @@ -327,22 +348,26 @@ public class QueryServiceIntegrationTest extends BaseUnitTest { assertEquals(4L, response.getResultNumber()); assertNotNull(response.getResult()); final List<Map<String, Object>> result = response.getResult(); + assertEquals(BigInteger.valueOf(1L), result.get(0).get("id")); assertEquals(4, result.get(0).get("legs")); assertEquals("boar", result.get(0).get("animal_name")); assertEquals("Moritz", result.get(0).get("firstname")); assertEquals("Staudinger", result.get(0).get("lastname")); assertEquals("1990", result.get(0).get("birth")); assertEquals("11:22:33", result.get(0).get("reminder")); + assertEquals(BigInteger.valueOf(1L), result.get(1).get("id")); assertEquals(4, result.get(1).get("legs")); assertEquals("cavy", result.get(1).get("animal_name")); assertEquals("Moritz", result.get(1).get("firstname")); assertEquals("Staudinger", result.get(1).get("lastname")); assertEquals("1990", result.get(1).get("birth")); assertEquals("11:22:33", result.get(1).get("reminder")); + assertEquals(BigInteger.valueOf(3L), result.get(2).get("id")); assertEquals(4, result.get(2).get("legs")); assertEquals("bear", result.get(2).get("animal_name")); assertEquals("Eva", result.get(2).get("firstname")); assertEquals("Gergely", result.get(2).get("lastname")); + assertEquals(BigInteger.valueOf(4L), result.get(3).get("id")); assertEquals(4, result.get(3).get("legs")); assertEquals("bear", result.get(3).get("animal_name")); assertEquals("Cornelia", result.get(3).get("firstname")); @@ -350,10 +375,9 @@ public class QueryServiceIntegrationTest extends BaseUnitTest { } @Test - public void execute_withoutNullField_succeeds() throws DatabaseConnectionException, TableMalformedException, - DatabaseNotFoundException, ImageNotSupportedException, QueryMalformedException, UserNotFoundException, - QueryStoreException, ColumnParseException, InterruptedException, KeycloakRemoteException, - AccessDeniedException, QueryNotFoundException { + public void execute_withoutNullField_succeeds() throws TableMalformedException, DatabaseNotFoundException, + ImageNotSupportedException, QueryMalformedException, UserNotFoundException, QueryStoreException, + ColumnParseException, InterruptedException, QueryNotFoundException { final ExecuteStatementDto request = ExecuteStatementDto.builder() .statement("SELECT `location`, `lng` FROM `weather_location` WHERE `lat` IS NULL") .build(); @@ -373,10 +397,9 @@ public class QueryServiceIntegrationTest extends BaseUnitTest { } @Test - public void execute_withoutNullField2_succeeds() throws DatabaseConnectionException, TableMalformedException, - DatabaseNotFoundException, ImageNotSupportedException, QueryMalformedException, UserNotFoundException, - QueryStoreException, ColumnParseException, InterruptedException, KeycloakRemoteException, - AccessDeniedException, QueryNotFoundException { + public void execute_withoutNullField2_succeeds() throws TableMalformedException, DatabaseNotFoundException, + ImageNotSupportedException, QueryMalformedException, UserNotFoundException, QueryStoreException, + ColumnParseException, InterruptedException, QueryNotFoundException { final ExecuteStatementDto request = ExecuteStatementDto.builder() .statement("SELECT `location` FROM `weather_location` WHERE `lat` IS NULL") .build(); @@ -396,10 +419,9 @@ public class QueryServiceIntegrationTest extends BaseUnitTest { } @Test - public void execute_withNullField_succeeds() throws DatabaseConnectionException, TableMalformedException, - DatabaseNotFoundException, ImageNotSupportedException, QueryMalformedException, UserNotFoundException, - QueryStoreException, ColumnParseException, InterruptedException, KeycloakRemoteException, - AccessDeniedException, QueryNotFoundException { + public void execute_withNullField_succeeds() throws TableMalformedException, DatabaseNotFoundException, + ImageNotSupportedException, QueryMalformedException, UserNotFoundException, QueryStoreException, + ColumnParseException, InterruptedException, QueryNotFoundException { final ExecuteStatementDto request = ExecuteStatementDto.builder() .statement("SELECT `lat`, `lng` FROM `weather_location` WHERE `lat` IS NULL") .build(); @@ -415,10 +437,9 @@ public class QueryServiceIntegrationTest extends BaseUnitTest { } @Test - public void execute_aliases_succeeds() throws DatabaseConnectionException, TableMalformedException, - DatabaseNotFoundException, ImageNotSupportedException, QueryMalformedException, UserNotFoundException, - QueryStoreException, ColumnParseException, InterruptedException, KeycloakRemoteException, - AccessDeniedException, QueryNotFoundException { + public void execute_aliases_succeeds() throws TableMalformedException, DatabaseNotFoundException, + ImageNotSupportedException, QueryMalformedException, UserNotFoundException, QueryStoreException, + ColumnParseException, InterruptedException, QueryNotFoundException { final ExecuteStatementDto request = ExecuteStatementDto.builder() .statement("SELECT aus.location as a, loc.location from weather_aus aus, weather_location loc") .build(); @@ -452,10 +473,9 @@ public class QueryServiceIntegrationTest extends BaseUnitTest { } @Test - public void execute_aliasesWithDatabaseName_succeeds() throws DatabaseConnectionException, TableMalformedException, - DatabaseNotFoundException, ImageNotSupportedException, QueryMalformedException, UserNotFoundException, - QueryStoreException, ColumnParseException, InterruptedException, KeycloakRemoteException, - AccessDeniedException, QueryNotFoundException { + public void execute_aliasesWithDatabaseName_succeeds() throws TableMalformedException, DatabaseNotFoundException, + ImageNotSupportedException, QueryMalformedException, UserNotFoundException, QueryStoreException, + ColumnParseException, InterruptedException, QueryNotFoundException { final ExecuteStatementDto request = ExecuteStatementDto.builder() .statement("SELECT aus.location as a, loc.location from weather.weather_aus aus, weather.weather_location loc") .build(); @@ -519,9 +539,9 @@ public class QueryServiceIntegrationTest extends BaseUnitTest { } @Test - public void findOne_emptySet_succeeds() throws DatabaseConnectionException, DatabaseNotFoundException, - ImageNotSupportedException, QueryMalformedException, UserNotFoundException, QueryStoreException, - QueryNotFoundException, FileStorageException, SQLException, IOException { + public void findOne_emptySet_succeeds() throws DatabaseNotFoundException, ImageNotSupportedException, + QueryMalformedException, QueryStoreException, QueryNotFoundException, FileStorageException, SQLException, + IOException, DataProcessingException { final String filename = RandomStringUtils.randomAlphabetic(40) + ".csv"; final Query query = Query.builder() .id(QUERY_1_ID) @@ -549,6 +569,28 @@ public class QueryServiceIntegrationTest extends BaseUnitTest { assertNotNull(response.getResource()); } + @Test + public void delete_emptyKeySet_succeeds() throws TableNotFoundException, TableMalformedException, QueryMalformedException, + DatabaseNotFoundException, ImageNotSupportedException { + final TableCsvDeleteDto request = TableCsvDeleteDto.builder() + .keys(Map.of()) + .build(); + + /* test */ + queryService.delete(DATABASE_1_ID, TABLE_1_ID, request, USER_1_PRINCIPAL); + } + + @Test + public void delete_succeeds() throws TableNotFoundException, TableMalformedException, QueryMalformedException, + DatabaseNotFoundException, ImageNotSupportedException { + final TableCsvDeleteDto request = TableCsvDeleteDto.builder() + .keys(Map.of("id", "1")) + .build(); + + /* test */ + queryService.delete(DATABASE_1_ID, TABLE_1_ID, request, USER_1_PRINCIPAL); + } + @SneakyThrows private static Instant toInstant(String str) { final DateTimeFormatter formatter = new DateTimeFormatterBuilder() diff --git a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/QueryStoreServiceIntegrationTest.java b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/QueryStoreServiceIntegrationTest.java index 2c3fb2be2665a86f3115c8816f02a8e1be3dd5b9..5f60fdc995be3bada2ac50a468e049bcd976bb68 100644 --- a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/QueryStoreServiceIntegrationTest.java +++ b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/QueryStoreServiceIntegrationTest.java @@ -2,6 +2,7 @@ package at.tuwien.service; import at.tuwien.BaseUnitTest; import at.tuwien.annotations.MockAmqp; +import at.tuwien.annotations.MockListeners; import at.tuwien.annotations.MockOpensearch; import at.tuwien.config.MariaDbConfig; import at.tuwien.config.MariaDbContainerConfig; @@ -32,6 +33,7 @@ import java.util.List; @DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD) @ExtendWith(SpringExtension.class) @MockAmqp +@MockListeners @MockOpensearch public class QueryStoreServiceIntegrationTest extends BaseUnitTest { diff --git a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/SemanticServiceIntegrationTest.java b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/SemanticServiceIntegrationTest.java index b5746390c7abbae59b06636088af5955a68b2704..50466c77758ceb9e70050b9a549bcafefd955188 100644 --- a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/SemanticServiceIntegrationTest.java +++ b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/SemanticServiceIntegrationTest.java @@ -2,6 +2,7 @@ package at.tuwien.service; import at.tuwien.BaseUnitTest; import at.tuwien.annotations.MockAmqp; +import at.tuwien.annotations.MockListeners; import at.tuwien.annotations.MockOpensearch; import at.tuwien.entities.database.table.columns.TableColumnConcept; import at.tuwien.entities.database.table.columns.TableColumnUnit; @@ -27,6 +28,7 @@ import static org.junit.jupiter.api.Assertions.*; @DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD) @ExtendWith(SpringExtension.class) @MockAmqp +@MockListeners @MockOpensearch public class SemanticServiceIntegrationTest extends BaseUnitTest { 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 0000000000000000000000000000000000000000..b058a6986761e4b7d38df503fcd7ef54e5bf8999 --- /dev/null +++ b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/StorageServiceIntegrationTest.java @@ -0,0 +1,79 @@ +package at.tuwien.service; + +import at.tuwien.BaseUnitTest; +import at.tuwien.annotations.MockAmqp; +import at.tuwien.annotations.MockListeners; +import at.tuwien.annotations.MockOpensearch; +import at.tuwien.config.S3Config; +import at.tuwien.exception.*; +import lombok.extern.log4j.Log4j2; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.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 java.io.IOException; + +import static org.junit.jupiter.api.Assertions.*; + +@Log4j2 +@Testcontainers +@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD) +@EnableAutoConfiguration(exclude = RabbitAutoConfiguration.class) +@SpringBootTest +@ExtendWith(SpringExtension.class) +@MockAmqp +@MockListeners +@MockOpensearch +public class StorageServiceIntegrationTest extends BaseUnitTest { + + @Autowired + private S3Config s3Config; + + @Autowired + private StorageService storageService; + + @Container + private static MinIOContainer minIOContainer = new MinIOContainer("minio/minio") + .withUserName("seaweedfsadmin") + .withPassword("seaweedfsadmin"); + + @DynamicPropertySource + static void openSearchProperties(DynamicPropertyRegistry registry) { + registry.add("fda.s3.endpoint", () -> minIOContainer.getS3URL()); + } + + @BeforeEach + public void beforeEach() throws IOException { + s3Config.makeBuckets(s3Config.getS3ImportBucket()); + s3Config.uploadFile(s3Config.getS3ImportBucket(), "./src/test/resources/csv/testdata.csv", "s3_filekey"); + } + + @Test + public void deleteStaleFiles_succeeds() throws FileStorageException, InterruptedException { + + /* test */ + Thread.sleep(5000); + storageService.deleteStaleFiles(s3Config.getS3ImportBucket()); + assertFalse(s3Config.objectExists(s3Config.getS3ImportBucket(), "s3_filekey")); + } + + @Test + public void deleteStaleFiles_fails() throws FileStorageException { + + /* test */ + storageService.deleteStaleFiles(s3Config.getS3ImportBucket()); + assertTrue(s3Config.objectExists(s3Config.getS3ImportBucket(), "s3_filekey")); + } + +} diff --git a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/StoreServiceIntegrationTest.java b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/StoreServiceIntegrationTest.java index e12e7b1b6ba945c72d68f18e5e83e764b94a7956..3bb487e8e7dc0a639702ee443c1ca440e3dded37 100644 --- a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/StoreServiceIntegrationTest.java +++ b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/StoreServiceIntegrationTest.java @@ -2,6 +2,7 @@ package at.tuwien.service; import at.tuwien.BaseUnitTest; import at.tuwien.annotations.MockAmqp; +import at.tuwien.annotations.MockListeners; import at.tuwien.annotations.MockOpensearch; import at.tuwien.api.database.query.ExecuteStatementDto; import at.tuwien.api.database.query.QueryPersistDto; @@ -41,6 +42,7 @@ import static org.junit.jupiter.api.Assertions.*; @SpringBootTest @ExtendWith(SpringExtension.class) @MockAmqp +@MockListeners @MockOpensearch public class StoreServiceIntegrationTest extends BaseUnitTest { diff --git a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/TableServiceIntegrationReadTest.java b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/TableServiceIntegrationReadTest.java index c123797552a670fa1c4b9bacbbba72f41b56c5d6..4220af3f9ccb5515e5f1c79c0ad29de411dc00fe 100644 --- a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/TableServiceIntegrationReadTest.java +++ b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/TableServiceIntegrationReadTest.java @@ -2,6 +2,7 @@ package at.tuwien.service; import at.tuwien.BaseUnitTest; import at.tuwien.annotations.MockAmqp; +import at.tuwien.annotations.MockListeners; import at.tuwien.annotations.MockOpensearch; import at.tuwien.api.database.table.TableHistoryDto; import at.tuwien.config.MariaDbConfig; @@ -36,6 +37,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows; @SpringBootTest @ExtendWith(SpringExtension.class) @MockAmqp +@MockListeners @MockOpensearch public class TableServiceIntegrationReadTest extends BaseUnitTest { diff --git a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/TableServiceIntegrationWriteTest.java b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/TableServiceIntegrationWriteTest.java index 8ef2a0ce4ad5bf39739c24123b651de325383f3a..c174c3e3b7fda1be798b8629b7363b6f394a82d6 100644 --- a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/TableServiceIntegrationWriteTest.java +++ b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/TableServiceIntegrationWriteTest.java @@ -2,6 +2,7 @@ package at.tuwien.service; import at.tuwien.BaseUnitTest; import at.tuwien.annotations.MockAmqp; +import at.tuwien.annotations.MockListeners; import at.tuwien.annotations.MockOpensearch; import at.tuwien.api.database.table.TableCreateDto; import at.tuwien.api.database.table.columns.ColumnCreateDto; @@ -49,6 +50,7 @@ import static org.mockito.ArgumentMatchers.any; @SpringBootTest @ExtendWith(SpringExtension.class) @MockAmqp +@MockListeners @MockOpensearch public class TableServiceIntegrationWriteTest extends BaseUnitTest { 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 e64c3f09c03ac8536784b135c0d1dc747198a534..864153b9398cb92d6d3a7448efc93f42330d2a6d 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 @@ -2,6 +2,7 @@ package at.tuwien.service; import at.tuwien.BaseUnitTest; import at.tuwien.annotations.MockAmqp; +import at.tuwien.annotations.MockListeners; import at.tuwien.annotations.MockOpensearch; import at.tuwien.entities.database.table.columns.TableColumn; import at.tuwien.exception.DatabaseNotFoundException; @@ -31,6 +32,7 @@ import static org.mockito.Mockito.when; @SpringBootTest @ExtendWith(SpringExtension.class) @MockAmqp +@MockListeners @MockOpensearch public class TableServiceUnitTest extends BaseUnitTest { diff --git a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/UserServiceIntegrationTest.java b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/UserServiceIntegrationTest.java index ba29bea0102b62422c9f725799cf6ec00f21999c..b8ee173b94c93e7f8667067efaf4cb529fc53d48 100644 --- a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/UserServiceIntegrationTest.java +++ b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/UserServiceIntegrationTest.java @@ -2,6 +2,7 @@ package at.tuwien.service; import at.tuwien.BaseUnitTest; import at.tuwien.annotations.MockAmqp; +import at.tuwien.annotations.MockListeners; import at.tuwien.annotations.MockOpensearch; import at.tuwien.api.auth.SignupRequestDto; import at.tuwien.api.user.*; @@ -32,6 +33,7 @@ import static org.junit.jupiter.api.Assertions.*; @SpringBootTest @ExtendWith(SpringExtension.class) @MockAmqp +@MockListeners @MockOpensearch public class UserServiceIntegrationTest extends BaseUnitTest { 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 476123cde6fd79024b5a5b5114c43f37bc89a430..20583370eacf69475dce3d15ae956926bb74622f 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 @@ -2,6 +2,7 @@ package at.tuwien.service; import at.tuwien.BaseUnitTest; import at.tuwien.annotations.MockAmqp; +import at.tuwien.annotations.MockListeners; import at.tuwien.annotations.MockOpensearch; import at.tuwien.entities.user.User; import at.tuwien.exception.*; @@ -24,6 +25,7 @@ import static org.mockito.Mockito.*; @ExtendWith(SpringExtension.class) @SpringBootTest @MockAmqp +@MockListeners @MockOpensearch public class UserServiceUnitTest extends BaseUnitTest { diff --git a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/ViewServiceIntegrationTest.java b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/ViewServiceIntegrationTest.java index 7d30bca6dee18252be67715972679b9845d015c8..c1b225310ad8bdcac8495eb6fe9fd8fe9c138bd6 100644 --- a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/ViewServiceIntegrationTest.java +++ b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/ViewServiceIntegrationTest.java @@ -2,6 +2,7 @@ package at.tuwien.service; import at.tuwien.BaseUnitTest; import at.tuwien.annotations.MockAmqp; +import at.tuwien.annotations.MockListeners; import at.tuwien.api.database.ViewCreateDto; import at.tuwien.config.MariaDbConfig; import at.tuwien.config.MariaDbContainerConfig; @@ -44,6 +45,7 @@ import static org.junit.jupiter.api.Assertions.assertNull; @SpringBootTest @ExtendWith(SpringExtension.class) @MockAmqp +@MockListeners public class ViewServiceIntegrationTest extends BaseUnitTest { @Autowired diff --git a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/ViewServicePersistenceIntegrationTest.java b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/ViewServicePersistenceIntegrationTest.java index 40c9e988e3e3ab84a2ba1e2d3b700b780a1d023b..3480c1682a7cd304717f1c74db3864b788905b39 100644 --- a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/ViewServicePersistenceIntegrationTest.java +++ b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/ViewServicePersistenceIntegrationTest.java @@ -2,6 +2,7 @@ package at.tuwien.service; import at.tuwien.BaseUnitTest; import at.tuwien.annotations.MockAmqp; +import at.tuwien.annotations.MockListeners; import at.tuwien.annotations.MockOpensearch; import at.tuwien.api.database.ViewCreateDto; import at.tuwien.config.MariaDbConfig; @@ -37,6 +38,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; @SpringBootTest @ExtendWith(SpringExtension.class) @MockAmqp +@MockListeners @MockOpensearch public class ViewServicePersistenceIntegrationTest extends BaseUnitTest { 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 162b05a4e02eac8f2e15e412ab86fd75ca22ad8e..e6132055b7c79ef2c4fedd852d1995af875b5b77 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 @@ -7,9 +7,9 @@ import at.tuwien.annotations.MockOpensearch; import at.tuwien.api.database.table.TableCreateDto; import at.tuwien.api.database.table.columns.ColumnCreateDto; import at.tuwien.api.database.table.columns.ColumnTypeDto; +import at.tuwien.api.identifier.IdentifierSaveDto; import at.tuwien.entities.database.table.Table; import at.tuwien.exception.*; -import at.tuwien.repository.mdb.IdentifierRepository; import at.tuwien.service.AccessService; import at.tuwien.service.DatabaseService; import at.tuwien.service.TableService; @@ -29,7 +29,7 @@ import org.springframework.test.context.junit.jupiter.SpringExtension; import java.util.List; import java.util.stream.Stream; -import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.when; @@ -46,9 +46,6 @@ public class EndpointValidatorUnitTest extends BaseUnitTest { @MockBean private AccessService accessService; - @MockBean - private IdentifierRepository identifierRepository; - @MockBean private TableService tableService; @@ -463,4 +460,171 @@ public class EndpointValidatorUnitTest extends BaseUnitTest { }); } + @Test + public void validatePublicationDate_succeeds() { + final IdentifierSaveDto request = IdentifierSaveDto.builder() + .publicationYear(2024) + .publicationMonth(1) + .publicationDay(25) + .build(); + + /* test */ + assertTrue(endpointValidator.validatePublicationDate(request)); + } + + @Test + public void validatePublicationDate_missingDay_succeeds() { + final IdentifierSaveDto request = IdentifierSaveDto.builder() + .publicationYear(2024) + .publicationMonth(1) + .build(); + + /* test */ + assertTrue(endpointValidator.validatePublicationDate(request)); + } + + @Test + public void validatePublicationDate_missingMonth_succeeds() { + final IdentifierSaveDto request = IdentifierSaveDto.builder() + .publicationYear(2024) + .publicationDay(1) + .build(); + + /* test */ + assertTrue(endpointValidator.validatePublicationDate(request)); + } + + @Test + public void validatePublicationDate_missingDayMonth_succeeds() { + final IdentifierSaveDto request = IdentifierSaveDto.builder() + .publicationYear(2024) + .build(); + + /* test */ + assertTrue(endpointValidator.validatePublicationDate(request)); + } + + @Test + public void validatePublicationDate_yearEarly_succeeds() { + final IdentifierSaveDto request = IdentifierSaveDto.builder() + .publicationYear(1300) + .build(); + + /* test */ + assertTrue(endpointValidator.validatePublicationDate(request)); + } + + @Test + public void validatePublicationDate_yearLate_succeeds() { + final IdentifierSaveDto request = IdentifierSaveDto.builder() + .publicationYear(12345) + .build(); + + /* test */ + assertTrue(endpointValidator.validatePublicationDate(request)); + } + + @Test + public void validatePublicationDate_monthInvalid1_fails() { + final IdentifierSaveDto request = IdentifierSaveDto.builder() + .publicationYear(2024) + .publicationMonth(0) + .build(); + + /* test */ + assertFalse(endpointValidator.validatePublicationDate(request)); + } + + @Test + public void validatePublicationDate_monthInvalid2_fails() { + final IdentifierSaveDto request = IdentifierSaveDto.builder() + .publicationYear(2024) + .publicationMonth(13) + .build(); + + /* test */ + assertFalse(endpointValidator.validatePublicationDate(request)); + } + + @Test + public void validatePublicationDate_dayInvalid1_fails() { + final IdentifierSaveDto request = IdentifierSaveDto.builder() + .publicationYear(2024) + .publicationDay(0) + .build(); + + /* test */ + assertFalse(endpointValidator.validatePublicationDate(request)); + } + + @Test + public void validatePublicationDate_dayInvalid2_fails() { + final IdentifierSaveDto request = IdentifierSaveDto.builder() + .publicationYear(2024) + .publicationDay(32) + .build(); + + /* test */ + assertFalse(endpointValidator.validatePublicationDate(request)); + } + + @Test + public void validatePublicationDate_february29Invalid_fails() { + final IdentifierSaveDto request = IdentifierSaveDto.builder() + .publicationYear(2023) + .publicationMonth(2) + .publicationDay(29) + .build(); + + /* test */ + assertFalse(endpointValidator.validatePublicationDate(request)); + } + + @Test + public void validatePublicationDate_february29_succeeds() { + final IdentifierSaveDto request = IdentifierSaveDto.builder() + .publicationYear(2024) + .publicationMonth(2) + .publicationDay(29) + .build(); + + /* test */ + assertTrue(endpointValidator.validatePublicationDate(request)); + } + + @Test + public void validateOnlyMineOrWriteAccessOrHasRole_noAccess_fails() { + + /* test */ + assertFalse(endpointValidator.validateOnlyMineOrWriteAccessOrHasRole(USER_1_ID, USER_1_PRINCIPAL, null, "nobody-role")); + } + + @Test + public void validateOnlyMineOrWriteAccessOrHasRole_readAccess_fails() { + + /* test */ + assertFalse(endpointValidator.validateOnlyMineOrWriteAccessOrHasRole(USER_1_ID, USER_1_PRINCIPAL, DATABASE_1_USER_1_READ_ACCESS, "nobody-role")); + } + + @Test + public void validateOnlyMineOrWriteAccessOrHasRole_ownerOnlyWriteOwn_succeeds() { + + /* test */ + assertTrue(endpointValidator.validateOnlyMineOrWriteAccessOrHasRole(USER_1_ID, USER_1_PRINCIPAL, DATABASE_1_USER_1_WRITE_OWN_ACCESS, "nobody-role")); + } + + @Test + public void validateOnlyMineOrWriteAccessOrHasRole_notOwnerOnlyWriteOwn_fails() { + + /* test */ + assertFalse(endpointValidator.validateOnlyMineOrWriteAccessOrHasRole(USER_2_ID, USER_1_PRINCIPAL, DATABASE_1_USER_1_WRITE_OWN_ACCESS, "nobody-role")); + } + + @Test + public void validateOnlyMineOrWriteAccessOrHasRole_notOwnerWriteAll_succeeds() { + + /* test */ + assertTrue(endpointValidator.validateOnlyMineOrWriteAccessOrHasRole(USER_2_ID, USER_1_PRINCIPAL, DATABASE_1_USER_1_WRITE_ALL_ACCESS, "nobody-role")); + } + } 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 0c0fab40c34a920f03a7c48022a4b0bfe4c8de0c..0ee2f044691ace782e985fb45dcceebf396d8ce9 100644 --- a/dbrepo-metadata-service/rest-service/src/test/resources/application.properties +++ b/dbrepo-metadata-service/rest-service/src/test/resources/application.properties @@ -16,7 +16,7 @@ spring.jpa.hibernate.ddl-auto=create # logging logging.level.root=error -logging.level.at.tuwien.=debug +logging.level.at.tuwien.=info # rabbitmq spring.rabbitmq.host=localhost @@ -33,5 +33,8 @@ fda.datacite.password: test-password # keycloak fda.keycloak.endpoint: http://localhost:8080/ +# s3 +fda.s3.staleSeconds=1 + # consumers fda.consumers=2 \ No newline at end of file diff --git a/dbrepo-metadata-service/rest-service/src/test/resources/csv/weather_aus.csv b/dbrepo-metadata-service/rest-service/src/test/resources/csv/weather_aus.csv new file mode 100644 index 0000000000000000000000000000000000000000..f1c02b05a665f99e7662d44895610112104e8366 --- /dev/null +++ b/dbrepo-metadata-service/rest-service/src/test/resources/csv/weather_aus.csv @@ -0,0 +1 @@ +4;"2024-01-27";"Vienna";NA;NA \ No newline at end of file diff --git a/dbrepo-metadata-service/services/src/main/java/at/tuwien/auth/AuthTokenFilter.java b/dbrepo-metadata-service/services/src/main/java/at/tuwien/auth/AuthTokenFilter.java index 647f23867be68d0f5f953934d460185c3fff0afd..dca11b65a1e4fb5ea0fd9c1ea163ce15da76db25 100644 --- a/dbrepo-metadata-service/services/src/main/java/at/tuwien/auth/AuthTokenFilter.java +++ b/dbrepo-metadata-service/services/src/main/java/at/tuwien/auth/AuthTokenFilter.java @@ -6,6 +6,7 @@ import com.auth0.jwt.JWT; import com.auth0.jwt.JWTVerifier; import com.auth0.jwt.algorithms.Algorithm; import com.auth0.jwt.interfaces.DecodedJWT; +import com.auth0.jwt.interfaces.Verification; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; @@ -56,6 +57,10 @@ public class AuthTokenFilter extends OncePerRequestFilter { } public UserDetails verifyJwt(String token) throws ServletException { + return verifyJwt(token, true); + } + + public UserDetails verifyJwt(String token, boolean strict) throws ServletException { final KeyFactory kf; try { kf = KeyFactory.getInstance("RSA"); @@ -72,10 +77,12 @@ public class AuthTokenFilter extends OncePerRequestFilter { throw new ServletException("Provided public key is invalid", e); } final Algorithm algorithm = Algorithm.RSA256(pubKey, null); - JWTVerifier verifier = JWT.require(algorithm) - .withIssuer(issuer) - .withAudience("spring") - .build(); + Verification verification = JWT.require(algorithm) + .withAudience("spring"); + if (strict) { + verification = verification.withIssuer(issuer); + } + final JWTVerifier verifier = verification.build(); final DecodedJWT jwt = verifier.verify(token); final RealmAccessDto realmAccess = jwt.getClaim("realm_access").as(RealmAccessDto.class); return UserDetailsDto.builder() 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 new file mode 100644 index 0000000000000000000000000000000000000000..f4bfbcc820834d9ffce41cd8d37d3c6a0306d020 --- /dev/null +++ b/dbrepo-metadata-service/services/src/main/java/at/tuwien/auth/BasicAuthenticationProvider.java @@ -0,0 +1,42 @@ +package at.tuwien.auth; + +import at.tuwien.api.keycloak.TokenDto; +import at.tuwien.exception.AccessDeniedException; +import at.tuwien.exception.KeycloakRemoteException; +import at.tuwien.gateway.KeycloakGateway; +import jakarta.servlet.ServletException; +import lombok.extern.log4j.Log4j2; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.AuthenticationManager; +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.userdetails.UserDetails; +import org.springframework.stereotype.Component; + +@Log4j2 +@Component +public class BasicAuthenticationProvider implements AuthenticationManager { + + private final AuthTokenFilter authTokenFilter; + private final KeycloakGateway keycloakGateway; + + @Autowired + public BasicAuthenticationProvider(AuthTokenFilter authTokenFilter, KeycloakGateway keycloakGateway) { + this.authTokenFilter = authTokenFilter; + this.keycloakGateway = keycloakGateway; + } + + @Override + public Authentication authenticate(Authentication auth) throws AuthenticationException { + try { + final TokenDto tokenDto = keycloakGateway.obtainUserToken(auth.getName(), auth.getCredentials().toString()); + final UserDetails userDetails = authTokenFilter.verifyJwt(tokenDto.getAccessToken(), false); + log.debug("authenticated user {}", userDetails); + return new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); + } catch (AccessDeniedException | KeycloakRemoteException | ServletException e) { + throw new BadCredentialsException("Failed to authenticate with authentication service", e); + } + } +} 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 241ea9c4b538c3670f58a5807067cb2db4d35113..d47b1080ef1d0c2d69a0c163f289b27a37613503 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 @@ -21,6 +21,9 @@ public class KeycloakConfig { @Value("${fda.keycloak.password}") private String keycloakPassword; + @Value("${fda.keycloak.clientSecret}") + private String keycloakClientSecret; + @Bean("keycloakRestTemplate") public RestTemplate brokerRestTemplate() { final RestTemplate restTemplate = new RestTemplate(); diff --git a/dbrepo-metadata-service/services/src/main/java/at/tuwien/config/S3Config.java b/dbrepo-metadata-service/services/src/main/java/at/tuwien/config/S3Config.java index 6e99fc0ee2dc88a80034ae281faa9279e24d916d..3bbf37d2cf81fb4aa9d17006aa807eada2783ff6 100644 --- a/dbrepo-metadata-service/services/src/main/java/at/tuwien/config/S3Config.java +++ b/dbrepo-metadata-service/services/src/main/java/at/tuwien/config/S3Config.java @@ -27,6 +27,9 @@ public class S3Config { @Value("${fda.s3.exportBucket}") private String s3ExportBucket; + @Value("${fda.s3.staleSeconds}") + private Integer staleSeconds; + @Bean public MinioClient minioClient() { return MinioClient.builder() 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 55bbb9f8b869c2178d2c3ac25a1cf2947fbd0cf9..8fc09851fd44238a2f30e43c77478f1aa51ed3f2 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 @@ -1,6 +1,8 @@ package at.tuwien.config; import at.tuwien.auth.AuthTokenFilter; +import at.tuwien.auth.BasicAuthenticationProvider; +import at.tuwien.gateway.KeycloakGateway; import io.swagger.v3.oas.annotations.enums.SecuritySchemeType; import io.swagger.v3.oas.annotations.security.SecurityScheme; import jakarta.servlet.http.HttpServletResponse; @@ -12,6 +14,7 @@ import org.springframework.security.config.annotation.web.configuration.EnableWe import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.security.web.util.matcher.OrRequestMatcher; import org.springframework.web.cors.CorsConfiguration; @@ -27,6 +30,11 @@ import org.springframework.web.filter.CorsFilter; bearerFormat = "JWT", scheme = "bearer" ) +@SecurityScheme( + name = "basicAuth", + type = SecuritySchemeType.HTTP, + scheme = "basic" +) public class WebSecurityConfig { @Bean @@ -35,7 +43,7 @@ public class WebSecurityConfig { } @Bean - public SecurityFilterChain filterChain(HttpSecurity http) 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"), @@ -77,6 +85,9 @@ public class WebSecurityConfig { http.addFilterBefore(authTokenFilter(), UsernamePasswordAuthenticationFilter.class ); + 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 370c627901ce38693d0cd833b8bcd3446cb7a42f..8b07b0e6e40f7f4d140330b65683a4ff85c23636 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 @@ -18,14 +18,6 @@ public interface BrokerServiceGateway { void grantTopicPermission(String username, GrantExchangePermissionsDto data) throws BrokerRemoteException, BrokerVirtualHostGrantException; - /** - * Finds all active consumers on the virtual host "dbrepo". - * - * @return The list of active consumers. - * @throws BrokerRemoteException The Broker Service did not respond within the 3s timeout. - */ - List<ConsumerDto> findAllConsumers() throws BrokerRemoteException; - /** * Create virtual host at the queue service. * diff --git a/dbrepo-metadata-service/services/src/main/java/at/tuwien/gateway/CrossrefGateway.java b/dbrepo-metadata-service/services/src/main/java/at/tuwien/gateway/CrossrefGateway.java index 30bc0dc73dcc793621fc9be7a96a674a17be2a35..58e023ac9d32c3e1820969b1c73478a4d141de9d 100644 --- a/dbrepo-metadata-service/services/src/main/java/at/tuwien/gateway/CrossrefGateway.java +++ b/dbrepo-metadata-service/services/src/main/java/at/tuwien/gateway/CrossrefGateway.java @@ -4,5 +4,13 @@ import at.tuwien.api.crossref.CrossrefDto; import at.tuwien.exception.DoiNotFoundException; public interface CrossrefGateway { + + /** + * Retrieves metadata from the CrossRef funder database for a given CrossRef id. + * + * @param id The CrossRef id. + * @return The CrossRef metadata from the CrossRef funder database. + * @throws DoiNotFoundException The metadata was not found in the CrossRef funder database. + */ CrossrefDto findById(String id) throws DoiNotFoundException; } diff --git a/dbrepo-metadata-service/services/src/main/java/at/tuwien/gateway/DataDbSidecarGateway.java b/dbrepo-metadata-service/services/src/main/java/at/tuwien/gateway/DataDbSidecarGateway.java index c7dff9c9e3e8d340fbfb490398e772f51881ef67..a8eae9032a9504306448521c87f86c76bad9104a 100644 --- a/dbrepo-metadata-service/services/src/main/java/at/tuwien/gateway/DataDbSidecarGateway.java +++ b/dbrepo-metadata-service/services/src/main/java/at/tuwien/gateway/DataDbSidecarGateway.java @@ -1,9 +1,10 @@ package at.tuwien.gateway; import at.tuwien.exception.DataDbSidecarException; +import at.tuwien.exception.DataProcessingException; public interface DataDbSidecarGateway { - void importFile(String hostname, Integer port, String filename) throws DataDbSidecarException; + void importFile(String hostname, Integer port, String filename) throws DataDbSidecarException, DataProcessingException; - void exportFile(String hostname, Integer port, String filename) throws DataDbSidecarException; + void exportFile(String hostname, Integer port, String filename) throws DataDbSidecarException, DataProcessingException; } diff --git a/dbrepo-metadata-service/services/src/main/java/at/tuwien/gateway/KeycloakGateway.java b/dbrepo-metadata-service/services/src/main/java/at/tuwien/gateway/KeycloakGateway.java index 3614e43fcbbbc21c8f9808974355b97b0d6750a4..0a9dcf6b69434afe1b8c0bf4b36ab6566c78e20a 100644 --- a/dbrepo-metadata-service/services/src/main/java/at/tuwien/gateway/KeycloakGateway.java +++ b/dbrepo-metadata-service/services/src/main/java/at/tuwien/gateway/KeycloakGateway.java @@ -1,5 +1,6 @@ package at.tuwien.gateway; +import at.tuwien.api.keycloak.TokenDto; import at.tuwien.api.keycloak.UserCreateDto; import at.tuwien.api.keycloak.UserDto; import at.tuwien.api.user.UserPasswordDto; @@ -9,6 +10,8 @@ import java.util.UUID; public interface KeycloakGateway { + TokenDto obtainUserToken(String username, String password) throws AccessDeniedException, KeycloakRemoteException; + /** * Creates a user at the Authentication Service with given credentials. * diff --git a/dbrepo-metadata-service/services/src/main/java/at/tuwien/gateway/RorGateway.java b/dbrepo-metadata-service/services/src/main/java/at/tuwien/gateway/RorGateway.java index 29f226efe7ec5f3da50ba21ff47297b27dd49ae5..27603b99191708e22970c7452fc6a44337809128 100644 --- a/dbrepo-metadata-service/services/src/main/java/at/tuwien/gateway/RorGateway.java +++ b/dbrepo-metadata-service/services/src/main/java/at/tuwien/gateway/RorGateway.java @@ -5,5 +5,12 @@ import at.tuwien.exception.RorNotFoundException; public interface RorGateway { + /** + * Retrieves metadata from the ROR database for an organizational ROR id. + * + * @param id The ROR id. + * @return The metadata from the ROR database, if successful. + * @throws RorNotFoundException The ROR id was not found in the ROR database. + */ RorDto findById(String id) throws RorNotFoundException; } 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 018c66c2b3637b3e2a587da409a1bb459b3d3386..3d674e41f4ac44469f8a00e71f359979feb2a757 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 @@ -102,7 +102,7 @@ public class BrokerServiceGatewayImpl implements BrokerServiceGateway { response = restTemplate.exchange(url, HttpMethod.DELETE, new HttpEntity<>(null), Void.class); } catch (Exception e) { log.error("Failed to delete user: remote host answered unexpected: {}", e.getMessage()); - throw new BrokerRemoteException("Failed to delete user: remote host answered unexpected", e); + throw new BrokerRemoteException("Failed to delete user: remote host answered unexpected: " + e.getMessage(), e); } if (!response.getStatusCode().equals(HttpStatus.NO_CONTENT)) { log.error("Failed to delete user: {}", response.getStatusCode()); @@ -124,8 +124,8 @@ public class BrokerServiceGatewayImpl implements BrokerServiceGateway { throw new BrokerRemoteException("Failed to create permissions: remote host answered unexpected", e); } if (!response.getStatusCode().equals(HttpStatus.CREATED) && !response.getStatusCode().equals(HttpStatus.NO_CONTENT)) { - log.error("Failed to grant virtual host permissions: {}", response.getStatusCode()); - throw new BrokerVirtualHostGrantException("Failed to grant virtual host permissions"); + log.error("Failed to grant virtual host permissions at broker service"); + throw new BrokerVirtualHostGrantException("Failed to grant virtual host permissions at broker service"); } log.trace("Grant virtual host permissions for user with username {}", username); } @@ -143,29 +143,12 @@ public class BrokerServiceGatewayImpl implements BrokerServiceGateway { throw new BrokerRemoteException("Failed to grant topic permissions: remote host answered unexpected", e); } if (!response.getStatusCode().equals(HttpStatus.CREATED) && !response.getStatusCode().equals(HttpStatus.NO_CONTENT)) { - log.error("Failed to grant topic permissions: {}", response.getStatusCode()); - throw new BrokerVirtualHostGrantException("Failed to grant topic permissions"); + log.error("Failed to grant topic permissions at broker service"); + throw new BrokerVirtualHostGrantException("Failed to grant topic permissions at broker service"); } log.trace("Grant topic permissions for user with username {}", username); } - @Override - public List<ConsumerDto> findAllConsumers() throws BrokerRemoteException { - final String url = "/api/consumers/" + rabbitConfig.getVirtualHost(); - log.trace("gateway broker find all consumers, virtual host={}", rabbitConfig.getVirtualHost()); - log.debug("find consumers from url {}{}", gatewayConfig.getBrokerEndpoint(), url); - final ResponseEntity<List<ConsumerDto>> response; - try { - response = restTemplate.exchange(URI.create(url), HttpMethod.GET, HttpEntity.EMPTY, - new ParameterizedTypeReference<>() { - }); - } catch (Exception e) { - log.error("Failed to find consumers: remote host answered unexpected: {}", e.getMessage()); - throw new BrokerRemoteException("Failed to find consumers: remote host answered unexpected", e); - } - return response.getBody(); - } - @Override public QueueDto findQueue(String name) throws BrokerRemoteException, QueueNotFoundException { final String url = "/api/queues/" + rabbitConfig.getVirtualHost() + "/" + name; @@ -181,8 +164,8 @@ public class BrokerServiceGatewayImpl implements BrokerServiceGateway { throw new BrokerRemoteException("Failed to find queue: remote host answered unexpected", e); } if (!response.getStatusCode().equals(HttpStatus.OK)) { - log.error("Failed find queue: {}", response.getStatusCode()); - throw new QueueNotFoundException("Failed to find queue"); + log.error("Failed find queue at broker service"); + throw new QueueNotFoundException("Failed to find queue at broker service"); } return response.getBody(); } diff --git a/dbrepo-metadata-service/services/src/main/java/at/tuwien/gateway/impl/DataDbSidecarGatewayImpl.java b/dbrepo-metadata-service/services/src/main/java/at/tuwien/gateway/impl/DataDbSidecarGatewayImpl.java index 6e4b2b56be874c1387d408f872a39158d732a086..5a793ed008b27240b1b804e74b0c3a803708578c 100644 --- a/dbrepo-metadata-service/services/src/main/java/at/tuwien/gateway/impl/DataDbSidecarGatewayImpl.java +++ b/dbrepo-metadata-service/services/src/main/java/at/tuwien/gateway/impl/DataDbSidecarGatewayImpl.java @@ -1,12 +1,11 @@ package at.tuwien.gateway.impl; import at.tuwien.exception.DataDbSidecarException; +import at.tuwien.exception.DataProcessingException; import at.tuwien.gateway.DataDbSidecarGateway; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.http.HttpEntity; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpMethod; +import org.springframework.http.*; import org.springframework.stereotype.Service; import org.springframework.web.client.HttpServerErrorException; import org.springframework.web.client.ResourceAccessException; @@ -23,26 +22,39 @@ public class DataDbSidecarGatewayImpl implements DataDbSidecarGateway { } @Override - public void importFile(String hostname, Integer port, String filename) throws DataDbSidecarException { + public void importFile(String hostname, Integer port, String filename) throws DataDbSidecarException, + DataProcessingException { final HttpHeaders headers = new HttpHeaders(); headers.set("Accept", "application/json"); + final ResponseEntity<Void> response; try { - restTemplate.exchange("http://" + hostname + ":" + port + "/sidecar/import/" + filename, HttpMethod.POST, new HttpEntity<>(null, headers), Void.class); + response = restTemplate.exchange("http://" + hostname + ":" + port + "/sidecar/import/" + filename, + HttpMethod.POST, new HttpEntity<>(null, headers), Void.class); } catch (ResourceAccessException | HttpServerErrorException.ServiceUnavailable e) { log.error("Failed to import .csv in data-db sidecar: {}", e.getMessage()); - throw new DataDbSidecarException("Failed to import .csv in data-db sidecar: " + e.getMessage()); + throw new DataDbSidecarException("Failed to import .csv in data-db sidecar: " + e.getMessage(), e); + } + if (!response.getStatusCode().equals(HttpStatus.ACCEPTED)) { + log.error("Failed to import .csv in data-db sidecar"); + throw new DataProcessingException("Failed to import .csv in data-db sidecar"); } } @Override - public void exportFile(String hostname, Integer port, String filename) throws DataDbSidecarException { + public void exportFile(String hostname, Integer port, String filename) throws DataDbSidecarException, DataProcessingException { final HttpHeaders headers = new HttpHeaders(); headers.set("Accept", "application/json"); + final ResponseEntity<Void> response; try { - restTemplate.exchange("http://" + hostname + ":" + port + "/sidecar/export/" + filename, HttpMethod.POST, new HttpEntity<>(null, headers), Void.class); + response = restTemplate.exchange("http://" + hostname + ":" + port + "/sidecar/export/" + filename, + HttpMethod.POST, new HttpEntity<>(null, headers), Void.class); } catch (ResourceAccessException | HttpServerErrorException.ServiceUnavailable e) { log.error("Failed to export .csv in data-db sidecar: {}", e.getMessage()); - throw new DataDbSidecarException("Failed to export .csv in data-db sidecar: " + e.getMessage()); + throw new DataDbSidecarException("Failed to export .csv in data-db sidecar: " + e.getMessage(), e); + } + if (!response.getStatusCode().equals(HttpStatus.ACCEPTED)) { + log.error("Failed to export .csv in data-db sidecar"); + throw new DataProcessingException("Failed to export .csv in data-db sidecar"); } } } 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 6f29d138ffb858c526e1e18b0f76c0d27cb5d99c..62351acf6430b23bef48e6a96c27bd0d2377c5d0 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 @@ -48,10 +48,37 @@ public class KeycloakGatewayImpl implements KeycloakGateway { response = restTemplate.exchange(url, HttpMethod.POST, new HttpEntity<>(payload, headers), TokenDto.class); } catch (ResourceAccessException | HttpServerErrorException.ServiceUnavailable e) { log.error("Failed to obtain admin token: {}", e.getMessage()); - throw new AccessDeniedException("Failed to obtain admin token: " + e.getMessage()); + throw new AccessDeniedException("Failed to obtain admin token: " + e.getMessage(), e); } catch (Exception e) { - log.error("Failed to create user: remote host answered unexpected: {}", e.getMessage()); - throw new KeycloakRemoteException("Failed to create user: remote host answered unexpected", e); + log.error("Failed to obtain admin token: remote host answered unexpected: {}", e.getMessage(), e); + throw new KeycloakRemoteException("Failed to obtain admin token: remote host answered unexpected: " + e.getMessage(), e); + } + return response.getBody(); + } + + @Override + public TokenDto obtainUserToken(String username, String password) throws AccessDeniedException, KeycloakRemoteException { + 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("client_id", "dbrepo-client"); + 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); + final ResponseEntity<TokenDto> response; + try { + response = new RestTemplate() + .exchange(url, HttpMethod.POST, new HttpEntity<>(payload, headers), TokenDto.class); + } catch (ResourceAccessException | HttpServerErrorException.ServiceUnavailable e) { + log.error("Failed to obtain user token: {}", e.getMessage()); + throw new AccessDeniedException("Failed to obtain user token: " + e.getMessage(), e); + } catch (Exception e) { + log.error("Failed to obtain user token: remote host answered unexpected: {}", e.getMessage(), e); + throw new KeycloakRemoteException("Failed to obtain user token: remote host answered unexpected: " + e.getMessage(), e); } return response.getBody(); } @@ -81,7 +108,7 @@ public class KeycloakGatewayImpl implements KeycloakGateway { } } catch (Exception e) { log.error("Failed to create user: remote host answered unexpected: {}", e.getMessage()); - throw new KeycloakRemoteException("Failed to create user: remote host answered unexpected", e); + throw new KeycloakRemoteException("Failed to create user: remote host answered unexpected: " + e.getMessage(), e); } if (!response.getStatusCode().equals(HttpStatus.CREATED)) { log.error("Failed to create user: status {} was not expected", response.getStatusCode().value()); @@ -162,7 +189,7 @@ public class KeycloakGatewayImpl implements KeycloakGateway { throw new KeycloakRemoteException("Failed to find user: " + e.getMessage()); } catch (Exception e) { log.error("Failed to create user: remote host answered unexpected: {}", e.getMessage()); - throw new KeycloakRemoteException("Failed to create user: remote host answered unexpected", e); + throw new KeycloakRemoteException("Failed to create user: remote host answered unexpected: " + e.getMessage(), e); } final UserDto[] body = response.getBody(); if (body == null || body.length != 1) { 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 cf1600aea5bc8f6fb65ac85495eb3a2520a51dc3..6eeb74c3c072f684834f90228dd755bfcb959c09 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 @@ -34,7 +34,7 @@ public class RorGatewayImpl implements RorGateway { response = restTemplate.exchange(url, HttpMethod.GET, new HttpEntity<>(null, headers), RorDto.class); } catch (ResourceAccessException | HttpServerErrorException.ServiceUnavailable 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()); + throw new RorNotFoundException("Failed to retrieve ROR metadata from URL " + url + ": " + e.getMessage(), e); } return response.getBody(); } diff --git a/dbrepo-metadata-service/services/src/main/java/at/tuwien/listener/StorageListener.java b/dbrepo-metadata-service/services/src/main/java/at/tuwien/listener/StorageListener.java new file mode 100644 index 0000000000000000000000000000000000000000..88a5260387318b2b13bbfdf80ca04a41d559e2c0 --- /dev/null +++ b/dbrepo-metadata-service/services/src/main/java/at/tuwien/listener/StorageListener.java @@ -0,0 +1,15 @@ +package at.tuwien.listener; + +import at.tuwien.exception.FileStorageException; +import org.springframework.scheduling.annotation.Scheduled; + +public interface StorageListener { + + /** + * Deletes old files from the buckets used by the system in regular intervals. + * + * @throws FileStorageException The object failed to be loaded from the Storage Service. + */ + @Scheduled + void deleteStaleFiles() throws FileStorageException; +} diff --git a/dbrepo-metadata-service/services/src/main/java/at/tuwien/listener/impl/StorageListenerImpl.java b/dbrepo-metadata-service/services/src/main/java/at/tuwien/listener/impl/StorageListenerImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..73c4c9913a1e32c4874a15d7c739816c0dbed336 --- /dev/null +++ b/dbrepo-metadata-service/services/src/main/java/at/tuwien/listener/impl/StorageListenerImpl.java @@ -0,0 +1,34 @@ +package at.tuwien.listener.impl; + +import at.tuwien.config.S3Config; +import at.tuwien.exception.FileStorageException; +import at.tuwien.listener.StorageListener; +import at.tuwien.service.StorageService; +import lombok.extern.log4j.Log4j2; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import java.util.concurrent.TimeUnit; + +@Log4j2 +@Component +public class StorageListenerImpl implements StorageListener { + + final S3Config s3Config; + final StorageService storageService; + + @Autowired + public StorageListenerImpl(S3Config s3Config, StorageService storageService) { + this.s3Config = s3Config; + this.storageService = storageService; + } + + @Override + @Scheduled(fixedRateString = "${fda.s3.deleteStaleFilesRate}", timeUnit = TimeUnit.SECONDS) + public void deleteStaleFiles() throws FileStorageException { + storageService.deleteStaleFiles(s3Config.getS3ExportBucket()); + storageService.deleteStaleFiles(s3Config.getS3ImportBucket()); + } + +} 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 e013d982743af03509b703609d59298fe94d269b..59c0743a2e37a094f0107874755b3c78013163ba 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 @@ -1,12 +1,14 @@ package at.tuwien.service; import at.tuwien.api.database.DatabaseCreateDto; +import at.tuwien.api.database.DatabaseModifyImageDto; 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.Transactional; import java.security.Principal; import java.util.List; @@ -101,6 +103,16 @@ public interface DatabaseService { Database transfer(Long databaseId, DatabaseTransferDto transferDto) throws DatabaseNotFoundException, UserNotFoundException; + /** + * Modify image of database with given id. + * + * @param databaseId The database id. + * @param image The image. + * @return The database, if successful. + * @throws DatabaseNotFoundException The database was not found in the metadata database. + */ + Database modifyImage(Long databaseId, byte[] image) throws DatabaseNotFoundException; + /** * Obtain metadata from database with given id to read table and view information (schema) and write it to the metadata database for management by DBRepo. * 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 4a3032281a1de86dcc165450b474c3d89dadd066..457b9dc3e8489cfccc59901b2530805004afcf23 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 @@ -141,7 +141,7 @@ public interface IdentifierService { */ InputStreamResource exportResource(Long identifierId, Principal principal) throws IdentifierNotFoundException, QueryNotFoundException, IdentifierRequestException, QueryStoreException, QueryMalformedException, - DatabaseNotFoundException, ImageNotSupportedException, FileStorageException, DataDbSidecarException; + DatabaseNotFoundException, ImageNotSupportedException, FileStorageException, DataDbSidecarException, DataProcessingException; /** * Soft-deletes an identifier for a given id in the metadata database. Does not actually remove the entity from the diff --git a/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/QueryService.java b/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/QueryService.java index 738eddb4a6d110341d364ec3da8c93f896d0aff3..d6350915820967217193eeceffae6a359a16484b 100644 --- a/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/QueryService.java +++ b/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/QueryService.java @@ -125,7 +125,7 @@ public interface QueryService { */ ExportResource tableFindAll(Long databaseId, Long tableId, Instant timestamp, Principal principal) throws TableNotFoundException, DatabaseNotFoundException, FileStorageException, QueryMalformedException, - DataDbSidecarException; + DataDbSidecarException, DataProcessingException; /** * Select all data known in the view id tuple and return a page of specific size. @@ -161,7 +161,7 @@ public interface QueryService { */ ExportResource findOne(Long databaseId, Long queryId, Principal principal) throws DatabaseNotFoundException, ImageNotSupportedException, FileStorageException, QueryStoreException, QueryNotFoundException, - QueryMalformedException, DataDbSidecarException; + QueryMalformedException, DataDbSidecarException, DataProcessingException; /** * Count the total tuples for a given table id within a database id at a given time. @@ -242,5 +242,5 @@ public interface QueryService { * @throws DataDbSidecarException The data database sidecar failed to import the dataset. */ void insert(Long databaseId, Long tableId, ImportDto data, Principal principal) throws TableMalformedException, - DatabaseNotFoundException, TableNotFoundException, DataDbSidecarException; + DatabaseNotFoundException, TableNotFoundException, DataDbSidecarException, DataProcessingException; } 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 new file mode 100644 index 0000000000000000000000000000000000000000..52a32bd56391363f80daf525b20be78b220c8f54 --- /dev/null +++ b/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/StorageService.java @@ -0,0 +1,65 @@ +package at.tuwien.service; + +import at.tuwien.ExportResource; +import at.tuwien.exception.FileStorageException; + +import java.io.InputStream; + +public interface StorageService { + + /** + * Loads an object of a bucket from the Storage Service into an input stream. + * + * @param bucket The bucket name. + * @param key The object key. + * @return The input stream, if successful. + * @throws FileStorageException The object failed to be loaded from the Storage Service. + */ + InputStream getObject(String bucket, String key) throws FileStorageException; + + /** + * Loads an object of the default upload bucket from the Storage Service into a byte array. + * + * @param key The object key. + * @return The byte array. + * @throws FileStorageException The object failed to be loaded from the Storage Service. + */ + byte[] getBytes(String key) throws FileStorageException; + + /** + * Loads an object of a bucket from the Storage Service into a byte array. + * + * @param bucket The bucket name. + * @param key The object key. + * @return The byte array. + * @throws FileStorageException The object failed to be loaded from the Storage Service. + */ + byte[] getBytes(String bucket, String key) throws FileStorageException; + + /** + * Loads an object of the default export bucket from the Storage Service into an export resource. + * + * @param key The object key. + * @return The export resource, if successful. + * @throws FileStorageException The object failed to be loaded from the Storage Service. + */ + ExportResource getResource(String key) throws FileStorageException; + + /** + * Loads an object of a bucket from the Storage Service into an export resource. + * + * @param bucket The bucket name. + * @param key The object key. + * @return The export resource, if successful. + * @throws FileStorageException The object failed to be loaded from the Storage Service. + */ + ExportResource getResource(String bucket, String key) throws FileStorageException; + + /** + * Deletes files older than an hour from the bucket. + * + * @param bucketName The bucket name. + * @throws FileStorageException The object failed to be loaded from the Storage Service. + */ + void deleteStaleFiles(String bucketName) throws FileStorageException; +} 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 48803195a6589bd956a95df2287901e9527ceab0..e0df5026fb79604a02bfc642edd337f14e28893f 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 @@ -9,7 +9,16 @@ import java.util.List; public interface ViewService { - View findById(Long id) throws ViewNotFoundException, DatabaseNotFoundException; + /** + * Find a view of a database with id. + * + * @param databaseId The database id. + * @param viewId The view id. + * @return The view, if successful. + * @throws ViewNotFoundException The view was not found in the metadata database. + * @throws DatabaseNotFoundException The database was not found in the metadata database. + */ + View findById(Long databaseId, Long viewId) throws ViewNotFoundException, DatabaseNotFoundException; /** * Find all views by database id. 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 3dfa60dc757343d49bd197c747e9831faa4c0d44..541e1fee8a3b9033738838b2cb6991531d31b3cf 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 @@ -189,7 +189,7 @@ public class DataCiteIdentifierServiceImpl implements IdentifierService { public InputStreamResource exportResource(Long identifierId, Principal principal) throws IdentifierNotFoundException, QueryNotFoundException, FileStorageException, IdentifierRequestException, QueryStoreException, QueryMalformedException, DatabaseNotFoundException, - ImageNotSupportedException, DataDbSidecarException { + ImageNotSupportedException, DataDbSidecarException, DataProcessingException { return identifierService.exportResource(identifierId, principal); } 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 4b2724a9aa55006abe48e3c033d86b68dd7bce91..528280b1a74b1da6950a3a312b0fb79dc0b3b6d6 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 @@ -176,7 +176,7 @@ public class IdentifierServiceImpl implements IdentifierService { } case VIEW -> { log.debug("identifier type: view with id {} and database with id {}", data.getViewId(), data.getDatabaseId()); - final View view = viewService.findById(data.getViewId()); + final View view = viewService.findById(data.getDatabaseId(), data.getViewId()); entity.setViewId(view.getId()); entity.setQuery(view.getQuery()); entity.setQueryNormalized(view.getQuery()); @@ -245,7 +245,8 @@ public class IdentifierServiceImpl implements IdentifierService { @Transactional(readOnly = true) public InputStreamResource exportResource(Long identifierId, Principal principal) throws IdentifierNotFoundException, QueryNotFoundException, IdentifierRequestException, QueryStoreException, QueryMalformedException, - DatabaseNotFoundException, ImageNotSupportedException, FileStorageException, DataDbSidecarException { + DatabaseNotFoundException, ImageNotSupportedException, FileStorageException, DataDbSidecarException, + DataProcessingException { /* check */ final Identifier identifier = find(identifierId); if (identifier.getType().equals(IdentifierType.DATABASE)) { diff --git a/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/MariaDbServiceImpl.java b/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/MariaDbServiceImpl.java index 07df2b4ca5950d5f3dbbb60c8e68746044b919f6..1b675b0fac3e2ebecf7eb8349ced51f8e8f3d15a 100644 --- a/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/MariaDbServiceImpl.java +++ b/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/MariaDbServiceImpl.java @@ -1,6 +1,7 @@ package at.tuwien.service.impl; import at.tuwien.api.database.DatabaseCreateDto; +import at.tuwien.api.database.DatabaseModifyImageDto; import at.tuwien.api.database.DatabaseModifyVisibilityDto; import at.tuwien.api.database.DatabaseTransferDto; import at.tuwien.config.QueryConfig; @@ -213,34 +214,64 @@ public class MariaDbServiceImpl extends HibernateConnector implements DatabaseSe return entity; } + @Override + @Transactional + public Database modifyImage(Long databaseId, byte[] image) throws DatabaseNotFoundException { + /* check */ + final Database database = findById(databaseId); + /* update in metadata database */ + database.setImage(image); + final Database entity = databaseRepository.save(database); + /* save in open search database */ + databaseIdxRepository.save(databaseMapper.databaseToDatabaseDto(entity)); + log.info("Updated database owner of database with id {} in metadata database & search database", entity.getId()); + return entity; + } + @Override @Transactional public Database obtainMetadata(Long databaseId) throws DatabaseNotFoundException, QueryMalformedException, - DatabaseUnchangedException, ColumnParseException { + ColumnParseException { /* check */ final Database database = findById(databaseId); final List<Table> diffTables; + final List<Table> knownTables; final List<View> diffViews; final ComboPooledDataSource dataSource = getPrivilegedDataSource(database.getContainer().getImage(), database.getContainer(), database); try { final Connection connection = dataSource.getConnection(); final PreparedStatement preparedStatement0 = databaseMapper.databaseToDatabaseMetadata(connection, database); - diffTables = tableMapper.resultListToTableList(preparedStatement0.executeQuery(), database) - .stream() - .filter(table -> database.getTables() + final List<Table> tables = tableMapper.resultListToTableList(preparedStatement0.executeQuery(), database); + diffTables = tables.stream() + .filter(obtainedTable -> database.getTables() .stream() + .noneMatch(t -> t.getInternalName().equals(obtainedTable.getInternalName()))) + .toList(); + knownTables = tables.stream() + .filter(table -> diffTables.stream() .noneMatch(t -> t.getInternalName().equals(table.getInternalName()))) + .map(obtainedTable -> { + final Optional<Table> optional = database.getTables() + .stream() + .filter(t -> t.getInternalName().equals(obtainedTable.getInternalName())) + .findFirst(); + if (optional.isPresent()) { + final Table table = optional.get(); + table.setNumRows(obtainedTable.getNumRows()); + table.setDataLength(obtainedTable.getDataLength()); + table.setMaxDataLength(obtainedTable.getMaxDataLength()); + table.setAvgRowLength(obtainedTable.getAvgRowLength()); + return table; + } + return obtainedTable; + }) .toList(); - diffViews = tableMapper.resultListToViewList(preparedStatement0.executeQuery(), database) - .stream() + final List<View> views = tableMapper.resultListToViewList(preparedStatement0.executeQuery(), database); + diffViews = views.stream() .filter(view -> database.getViews() .stream() .noneMatch(v -> v.getInternalName().equals(view.getInternalName()))) .toList(); - if (diffTables.isEmpty() && diffViews.isEmpty()) { - log.debug("database with id {} does not contain any unknown tables and any unknown views", databaseId); - throw new DatabaseUnchangedException("Database with id " + databaseId + " does not contain any unknown tables and any unknown views"); - } /* default times */ final Optional<ContainerImageDate> defaultDateFormat = containerRepository.findDefaultDateFormat(); if (defaultDateFormat.isEmpty()) { @@ -255,6 +286,16 @@ public class MariaDbServiceImpl extends HibernateConnector implements DatabaseSe /* obtain table schema */ log.info("Database with id {} contains {} unknown table(s) and {} unknown view(s)", databaseId, diffTables.size(), diffViews.size()); log.debug("database with id {} misses table(s) in metadata database: {}", databaseId, diffTables.stream().map(Table::getInternalName).toList()); + database.getTables().replaceAll(table -> { + final Optional<Table> optional = knownTables.stream() + .filter(t -> t.getId().equals(table.getId())) + .findFirst(); + if (optional.isPresent()) { + log.trace("found table with id {} and merged it", table.getId()); + return optional.get(); + } + return table; + }); for (Table table : diffTables) { final PreparedStatement preparedStatement1 = queryMapper.obtainTableMetadataRawQuery(connection, table.getDatabase().getInternalName(), table.getInternalName()); table = tableMapper.resultSetTableToObtainedMetadata(preparedStatement1.executeQuery(), table, diff --git a/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/QueryServiceImpl.java b/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/QueryServiceImpl.java index b78715ee47068b3c4956e92a8ca562c792fe3ab1..775df9e82dda9c5604114faeebd840b37c9284cb 100644 --- a/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/QueryServiceImpl.java +++ b/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/QueryServiceImpl.java @@ -7,7 +7,6 @@ import at.tuwien.api.database.query.ImportDto; import at.tuwien.api.database.query.QueryResultDto; import at.tuwien.api.database.table.TableCsvDeleteDto; import at.tuwien.api.database.table.TableCsvDto; -import at.tuwien.config.S3Config; import at.tuwien.entities.container.Container; import at.tuwien.entities.database.Database; import at.tuwien.entities.database.View; @@ -18,26 +17,15 @@ import at.tuwien.gateway.DataDbSidecarGateway; import at.tuwien.mapper.QueryMapper; import at.tuwien.mapper.ViewMapper; import at.tuwien.querystore.Query; -import at.tuwien.service.DatabaseService; -import at.tuwien.service.QueryService; -import at.tuwien.service.StoreService; -import at.tuwien.service.TableService; +import at.tuwien.service.*; import com.mchange.v2.c3p0.ComboPooledDataSource; -import io.minio.GetObjectArgs; -import io.minio.MinioClient; -import io.minio.errors.*; import lombok.extern.log4j.Log4j2; import net.sf.jsqlparser.JSQLParserException; import org.apache.commons.lang3.RandomStringUtils; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.core.io.InputStreamResource; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.io.IOException; -import java.io.InputStream; -import java.security.InvalidKeyException; -import java.security.NoSuchAlgorithmException; import java.security.Principal; import java.sql.Connection; import java.sql.PreparedStatement; @@ -51,24 +39,22 @@ import java.util.List; @Service public class QueryServiceImpl extends HibernateConnector implements QueryService { - private final S3Config s3Config; private final ViewMapper viewMapper; - private final MinioClient minioClient; private final QueryMapper queryMapper; private final StoreService storeService; private final TableService tableService; + private final StorageService storageService; private final DatabaseService databaseService; private final DataDbSidecarGateway dataDbSidecarGateway; @Autowired - public QueryServiceImpl(S3Config s3Config, ViewMapper viewMapper, MinioClient minioClient, QueryMapper queryMapper, - TableService tableService, DatabaseService databaseService, StoreService storeService, + public QueryServiceImpl(ViewMapper viewMapper, QueryMapper queryMapper, TableService tableService, + StorageService storageService, DatabaseService databaseService, StoreService storeService, DataDbSidecarGateway dataDbSidecarGateway) { - this.s3Config = s3Config; this.viewMapper = viewMapper; - this.minioClient = minioClient; this.queryMapper = queryMapper; this.tableService = tableService; + this.storageService = storageService; this.storeService = storeService; this.databaseService = databaseService; this.dataDbSidecarGateway = dataDbSidecarGateway; @@ -242,7 +228,7 @@ public class QueryServiceImpl extends HibernateConnector implements QueryService @Transactional(readOnly = true) public ExportResource tableFindAll(Long databaseId, Long tableId, Instant timestamp, Principal principal) throws TableNotFoundException, DatabaseNotFoundException, FileStorageException, QueryMalformedException, - DataDbSidecarException { + DataDbSidecarException, DataProcessingException { final String filename = RandomStringUtils.randomAlphabetic(40) + ".csv"; /* find */ final Database database = databaseService.find(databaseId); @@ -264,37 +250,25 @@ public class QueryServiceImpl extends HibernateConnector implements QueryService } private ExportResource retrieveBlobAsResource(Container container, String filename) throws DataDbSidecarException, - FileStorageException { + FileStorageException, DataProcessingException { /* upload from sidecar into blob storage */ dataDbSidecarGateway.exportFile(container.getSidecarHost(), container.getSidecarPort(), filename); /* export file from blob storage */ - try { - final InputStream stream = minioClient.getObject(GetObjectArgs.builder().bucket(s3Config.getS3ExportBucket()).object(filename).build()); - log.debug("found object with key {} in bucket {}", filename, s3Config.getS3ExportBucket()); - return ExportResource.builder() - .resource(new InputStreamResource(stream)) - .filename(filename) - .build(); - } catch (ServerException | InsufficientDataException | ErrorResponseException | IOException | - NoSuchAlgorithmException | InvalidKeyException | InvalidResponseException | XmlParserException | - InternalException e) { - log.error("Failed to find object {} in bucket {}: {}", filename, s3Config.getS3ExportBucket(), e.getMessage()); - throw new FileStorageException("Failed to find object " + filename + " in bucket " + s3Config.getS3ExportBucket() + ": " + e.getMessage()); - } + return storageService.getResource(filename); } @Override @Transactional(readOnly = true) public ExportResource findOne(Long databaseId, Long queryId, Principal principal) throws DatabaseNotFoundException, ImageNotSupportedException, FileStorageException, QueryStoreException, - QueryNotFoundException, QueryMalformedException, DataDbSidecarException { + QueryNotFoundException, QueryMalformedException, DataDbSidecarException, DataProcessingException { return findOne(databaseId, queryId, principal, RandomStringUtils.randomAlphabetic(40) + ".csv"); } @Transactional(readOnly = true) public ExportResource findOne(Long databaseId, Long queryId, Principal principal, String filename) throws DatabaseNotFoundException, ImageNotSupportedException, FileStorageException, QueryStoreException, - QueryNotFoundException, QueryMalformedException, DataDbSidecarException { + QueryNotFoundException, QueryMalformedException, DataDbSidecarException, DataProcessingException { /* find */ final Database database = databaseService.find(databaseId); final Query query = storeService.findOne(databaseId, queryId, principal); @@ -372,7 +346,8 @@ public class QueryServiceImpl extends HibernateConnector implements QueryService @Override @Transactional public void insert(Long databaseId, Long tableId, ImportDto data, Principal principal) - throws TableMalformedException, DatabaseNotFoundException, TableNotFoundException, DataDbSidecarException { + throws TableMalformedException, DatabaseNotFoundException, TableNotFoundException, DataDbSidecarException, + DataProcessingException { /* find */ final Database database = databaseService.find(databaseId); final Table table = tableService.find(databaseId, tableId); diff --git a/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/SeaweedServiceImpl.java b/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/SeaweedServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..41b9de1d449ec8b77fcb56815e73a51c71b3eea9 --- /dev/null +++ b/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/SeaweedServiceImpl.java @@ -0,0 +1,123 @@ +package at.tuwien.service.impl; + +import at.tuwien.ExportResource; +import at.tuwien.config.S3Config; +import at.tuwien.exception.FileStorageException; +import at.tuwien.service.StorageService; +import io.minio.*; +import io.minio.errors.*; +import io.minio.messages.DeleteError; +import io.minio.messages.DeleteObject; +import io.minio.messages.Item; +import lombok.extern.log4j.Log4j2; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.io.InputStreamResource; +import org.springframework.stereotype.Service; + +import java.io.IOException; +import java.io.InputStream; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.time.ZonedDateTime; +import java.util.LinkedList; +import java.util.List; + +@Log4j2 +@Service +public class SeaweedServiceImpl implements StorageService { + + private final S3Config s3Config; + private final MinioClient minioClient; + + @Autowired + public SeaweedServiceImpl(S3Config s3Config, MinioClient minioClient) { + this.s3Config = s3Config; + this.minioClient = minioClient; + } + + @Override + public InputStream getObject(String bucket, String key) throws FileStorageException { + try { + return minioClient.getObject(GetObjectArgs.builder() + .bucket(bucket) + .object(key) + .build()); + } catch (ErrorResponseException | InsufficientDataException | InternalException | InvalidKeyException | + InvalidResponseException | IOException | NoSuchAlgorithmException | ServerException | + XmlParserException e) { + log.error("Failed to find object {} in bucket {}: {}", key, bucket, e.getMessage()); + throw new FileStorageException("Failed to find object " + key + " in bucket " + bucket + ": " + e.getMessage(), e); + } + } + + @Override + public byte[] getBytes(String key) throws FileStorageException { + return getBytes(s3Config.getS3ImportBucket(), key); + } + + @Override + public byte[] getBytes(String bucket, String key) throws FileStorageException { + try { + return getObject(bucket, key) + .readAllBytes(); + } catch (IOException e) { + log.error("Failed to read bytes from input stream: {}", e.getMessage()); + throw new FileStorageException("Failed to read bytes from input stream: " + e.getMessage(), e); + } + } + + @Override + public ExportResource getResource(String key) throws FileStorageException { + return getResource(s3Config.getS3ExportBucket(), key); + } + + @Override + public ExportResource getResource(String bucket, String key) throws FileStorageException { + final InputStream stream = getObject(bucket, key); + return ExportResource.builder() + .resource(new InputStreamResource(stream)) + .filename(key) + .build(); + } + + @Override + public void deleteStaleFiles(String bucketName) throws FileStorageException { + final List<Item> objects = new LinkedList<>(); + for (Result<Item> result : minioClient.listObjects(ListObjectsArgs.builder() + .bucket(bucketName) + .build())) { + try { + final Item item = result.get(); + final long diff = item.lastModified().toEpochSecond() - ZonedDateTime.now().minusSeconds(s3Config.getStaleSeconds()).toEpochSecond(); + if (diff <= 0) { + log.trace("file {} of bucket {} is due {} second(s)", item.objectName(), bucketName, diff * -1); + objects.add(item); + } else { + log.trace("file {} of bucket {} is not yet due for {} second(s)", item.objectName(), bucketName, diff); + } + } catch (ErrorResponseException | InsufficientDataException | InternalException | InvalidKeyException | + InvalidResponseException | IOException | NoSuchAlgorithmException | ServerException | + XmlParserException e) { + log.error("Failed to retrieve file infos from bucket {}: {}", bucketName, e.getMessage()); + throw new FileStorageException("Failed to retrieve file infos from bucket " + bucketName + ": " + e.getMessage(), e); + } + } + log.debug("deleting files {}", objects.stream().map(Item::objectName).toList()); + final Iterable<Result<DeleteError>> response = minioClient.removeObjects(RemoveObjectsArgs.builder() + .bucket(bucketName) + .objects(objects.stream().map(o -> new DeleteObject(o.objectName())).toList()) + .build()); + for (Result<DeleteError> result : response) { + try { + result.get(); + } catch (ServerException | InsufficientDataException | ErrorResponseException | IOException | + NoSuchAlgorithmException | InvalidKeyException | InvalidResponseException | XmlParserException | + InternalException e) { + log.error("Failed to delete file from bucket {}: {}", bucketName, e.getMessage()); + throw new FileStorageException("Failed to delete file from bucket " + bucketName + ": " + e.getMessage(), e); + } + } + log.info("Deleted {} files", objects.size()); + } + +} 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 dae45fb07ea2081bd0f48f633c24975c09ec1ee6..2a57cadf190fa111233efb30c5fa13ba384411cb 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 @@ -53,15 +53,15 @@ public class ViewServiceImpl extends HibernateConnector implements ViewService { } @Override - public View findById(Long id) throws ViewNotFoundException, DatabaseNotFoundException { - final Optional<View> optional = databaseService.find(id) + public View findById(Long databaseId, Long viewId) throws ViewNotFoundException, DatabaseNotFoundException { + final Optional<View> optional = databaseService.find(databaseId) .getViews() .stream() - .filter(v -> v.getId().equals(id)) + .filter(v -> v.getId().equals(viewId)) .findFirst(); if (optional.isEmpty()) { - log.error("Failed to find view with id {} in metadata database", id); - throw new ViewNotFoundException("Failed to find view with id " + id + " in metadata database"); + log.error("Failed to find view with id {} in metadata database", viewId); + throw new ViewNotFoundException("Failed to find view with id " + viewId + " in metadata database"); } return optional.get(); } 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 f4d85aa196fc07d53198a0d3f169e0895d7f9c70..67a2711a98bc1b93492fdfb25102daf6f9c0037b 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 @@ -24,6 +24,7 @@ import at.tuwien.api.database.table.constraints.unique.UniqueDto; import at.tuwien.api.identifier.*; import at.tuwien.api.keycloak.CredentialDto; import at.tuwien.api.keycloak.CredentialTypeDto; +import at.tuwien.api.keycloak.TokenDto; import at.tuwien.api.keycloak.UserCreateDto; import at.tuwien.api.maintenance.BannerMessageCreateDto; import at.tuwien.api.maintenance.BannerMessageTypeDto; @@ -142,7 +143,7 @@ public abstract class BaseTest { public final static String[] DEFAULT_DATABASE_HANDLING = new String[]{"default-database-handling", "update-database-access", "modify-database-visibility", "create-database", "modify-database-owner", - "delete-database-access", "check-database-access", "list-databases", + "delete-database-access", "check-database-access", "list-databases", "modify-database-image", "create-database-access", "find-database", "import-database-data"}; public final static String[] ESCALATED_DATABASE_HANDLING = new String[]{"escalated-database-handling", @@ -229,6 +230,11 @@ public abstract class BaseTest { public final static String ROLE_DEFAULT_RESEARCHER_ROLES_NAME = "default-researcher-roles"; public final static UUID ROLE_DEFAULT_RESEARCHER_ROLES_REALM_ID = REALM_DBREPO_ID; + public final static TokenDto TOKEN_DTO = TokenDto.builder() + .accessToken("ey.yee.skrr") + .scope("openid") + .build(); + public final static String USER_BROKER_USERNAME = "guest"; public final static String USER_BROKER_PASSWORD = "guest"; @@ -1346,6 +1352,7 @@ public abstract class BaseTest { .name(TABLE_1_NAME) .queueName(TABLE_1_QUEUE_NAME) .routingKey(TABLE_1_ROUTING_KEY) + .identifiers(List.of()) .columns(List.of() /* TABLE_1_COLUMNS */) .constraints(null /* TABLE_1_CONSTRAINTS */) .createdBy(USER_1_ID) @@ -1365,6 +1372,7 @@ public abstract class BaseTest { .name(TABLE_1_NAME) .queueName(TABLE_1_QUEUE_NAME) .routingKey(TABLE_1_ROUTING_KEY) + .identifiers(List.of()) .columns(List.of() /* TABLE_1_COLUMNS */) .constraints(null /* TABLE_1_CONSTRAINTS */) .createdBy(USER_1_ID) diff --git a/dbrepo-search-db/init/indices/database.json b/dbrepo-search-db/init/indices/database.json index 35be7c5ae43bafe791adc58b90023287e7160402..fedeae2384d124803048ccc602b9cc45966e47b4 100644 --- a/dbrepo-search-db/init/indices/database.json +++ b/dbrepo-search-db/init/indices/database.json @@ -583,6 +583,9 @@ "tables": { "type": "object", "properties": { + "avg_row_length": { + "type": "long" + }, "columns": { "properties": { "auto_generated": { @@ -724,6 +727,9 @@ "database_id": { "type": "keyword" }, + "data_length": { + "type": "long" + }, "description": { "type": "text" }, @@ -939,6 +945,12 @@ "name": { "type": "keyword" }, + "num_rows": { + "type": "long" + }, + "max_data_length": { + "type": "long" + }, "owner": { "type": "object", "properties": { diff --git a/dbrepo-search-service/app/__init__.py b/dbrepo-search-service/app/__init__.py index e090b89e1d966bfb61c0c511efd3a978d0fc8cff..f14faf60b41ce59a50d43365d9f0c2c03d362496 100644 --- a/dbrepo-search-service/app/__init__.py +++ b/dbrepo-search-service/app/__init__.py @@ -66,7 +66,7 @@ def create_app(config_class=Config): "info": { "title": "Database Repository Search Service API", "description": "Service that searches the search database", - "version": "latest", + "version": "__APPVERSION__", "contact": { "name": "Prof. Andreas Rauber", "email": "andreas.rauber@tuwien.ac.at" diff --git a/dbrepo-ui/README.md b/dbrepo-ui/README.md index c4f407ba90f72ae0af0398394613b55c6cea031f..fbd83426ca01b7b4acb8aad17690bf577cd73945 100644 --- a/dbrepo-ui/README.md +++ b/dbrepo-ui/README.md @@ -1,73 +1,43 @@ -# fda-ui +# User Interface -## Prerequisit +## Prerequisites -We use Ubuntu 20.04 LTS. +* Node.js 14+ ([how to install](https://www.stewright.me/2021/03/install-nodejs-14-on-ubuntu-20-04/)) +* Yarn 1.22.0+ ([how to install](https://classic.yarnpkg.com/lang/en/docs/install/#debian-stable)) -## Build environment: yarn +Install the runtime dependencies into `node_modules`: -Install yarn: - -First we need curl: - - sudo apt install curl - -Add yarn GPG-Key - - curl -sL https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add - - -Add repo - - echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list - -Install yarn - - sudo apt update && sudo apt install yarn - -Run yarn - - $ yarn - -which gives yarn install v1.22.5 - -## Nuxt +```bash +yarn install +``` -With yarn installed, install nuxt.js +## Run -See https://nuxtjs.org/ +Then, start a local development server at port 3001. The development server has a local proxy that rewrites the paths +and does not rely on the `gateway-service` (a NGINX-based proxy to bundle the REST API). - yarn add nuxt +```bash +yarn dev +``` +Visit [http://localhost:3001](http://localhost:3001) in your browser. The development server watches for changes in +`dbrepo-ui` and will reload the frontend. -## Prepare +## Configure -Configure the `.env` file for the IP and port running or run through terminal: +To change most display settings, modify the `dbrepo.config.json` in the root folder. Extend it for any configuration +that the user needs to do, e.g. change the title, logo, display a list of links. -```bash -API=http://fda-gateway-service npm --prefix ./fda-ui run dev -``` +## Test -Of course you need to add `fda-gateway-service` to your `/etc/hosts` file for Docker "DNS" to your containers: +TO run the unit tests: ```bash -172.29.0.6 fda-gateway-service +yarn run test:unit ``` -## Build Setup +Optionally, generate a coverage report: ```bash -# install dependencies -$ yarn install - -# serve with hot reload at localhost:3000 -$ yarn dev - -# build for production and launch server -$ yarn build -$ yarn start - -# generate static project -$ yarn generate -``` - -For detailed explanation on how things work, check out [Nuxt.js docs](https://nuxtjs.org). +yarn run coverage +``` \ No newline at end of file diff --git a/dbrepo-ui/api/database.service.js b/dbrepo-ui/api/database.service.js index 9c4fb14f224c25ad6e2009317c6a5c53a55c6679..c112b6982f080676245cfb076afae45c0dda7cc9 100644 --- a/dbrepo-ui/api/database.service.js +++ b/dbrepo-ui/api/database.service.js @@ -99,6 +99,20 @@ class DatabaseService { }) } + modifyImage (databaseId, payload) { + return new Promise((resolve, reject) => { + api.put(`/api/database/${databaseId}/image`, payload, { headers: { Accept: 'application/json' } }) + .then((response) => { + const database = response.data + console.debug('response database', database) + resolve(database) + }).catch((error) => { + displayError(error, 'Failed to modify database visibility') + reject(error) + }) + }) + } + modifyOwner (databaseId, username) { return new Promise((resolve, reject) => { api.put(`/api/database/${databaseId}/transfer`, { username }, { headers: { Accept: 'application/json' } }) diff --git a/dbrepo-ui/api/upload.service.js b/dbrepo-ui/api/upload.service.js index f37cf24d1f3bb7e91c9183d5ca570ff46bafa7bb..7627c37e8739091cc3391ed5fce130b9d1a04334 100644 --- a/dbrepo-ui/api/upload.service.js +++ b/dbrepo-ui/api/upload.service.js @@ -1,10 +1,11 @@ import Vue from 'vue' +import config from '../dbrepo.config.json' const tus = require('tus-js-client') class UploadService { upload (file) { return new Promise((resolve, reject) => { - const endpoint = `${location.protocol}//${location.host}/api/upload/files` + const endpoint = `http${config.upload.useSsl ? 's' : ''}://${config.upload.endpoint}:${config.upload.port}/api/upload/files` console.debug('upload endpoint', endpoint) if (!tus.isSupported) { console.error('Your browser does not support uploads!') @@ -19,7 +20,7 @@ class UploadService { filetype: file.type }, onError (error) { - console.error('Failed because: ' + error) + console.error('Failed to upload:', error) reject(error) }, onProgress (bytesUploaded, bytesTotal) { diff --git a/dbrepo-ui/components/identifier/Persist.vue b/dbrepo-ui/components/identifier/Persist.vue index e95f644bde687eebe789c19652f5873f2dd946ed..869ac3c5fc5550585693d1b198b4d3e67244b554 100644 --- a/dbrepo-ui/components/identifier/Persist.vue +++ b/dbrepo-ui/components/identifier/Persist.vue @@ -13,7 +13,7 @@ v-if="!isUpdate" class="mb-1" :loading="loading" - :disabled="!formValid || loading" + :disabled="!formValid || !validPublicationMonth || !validPublicationDay || loading" color="primary" @click="save"> <v-icon left>mdi-content-save-outline</v-icon> Create PID @@ -286,6 +286,7 @@ v-model.number="identifier.publication_day" type="number" label="Publication day" + :rules="[validPublicationDay || $t('Invalid day')]" clearable /> </v-col> <v-col cols="2"> @@ -294,6 +295,7 @@ v-model.number="identifier.publication_month" type="number" label="Publication month" + :rules="[validPublicationMonth || $t('Invalid month')]" clearable /> </v-col> <v-col cols="2"> @@ -649,6 +651,20 @@ export default { return 'Table' } return '' + }, + validPublicationDay () { + const day = this.identifier.publication_day + if (day === null) { + return true + } + return day >= 1 && day <= 31 + }, + validPublicationMonth () { + const month = this.identifier.publication_month + if (month === null) { + return true + } + return month >= 1 && month <= 12 } }, watch: { diff --git a/dbrepo-ui/components/identifier/Summary.vue b/dbrepo-ui/components/identifier/Summary.vue index e4fe690432a41cd59f84b4f3880907839787214e..a9f16bd347cc5889067dc274369868520d5fdfcd 100644 --- a/dbrepo-ui/components/identifier/Summary.vue +++ b/dbrepo-ui/components/identifier/Summary.vue @@ -97,7 +97,7 @@ import IsniIcon from '@/components/icons/IsniIcon.vue' import OrcidIcon from '@/components/icons/OrcidIcon.vue' import RorIcon from '@/components/icons/RorIcon.vue' import Banner from '@/components/identifier/Banner.vue' -import { formatDateUTC, formatLanguage } from '@/utils' +import { formatLanguage } from '@/utils' export default { components: { @@ -167,7 +167,9 @@ export default { if (this.identifier.publication_year && !this.identifier.publication_month && !this.identifier.publication_day) { return this.identifier.publication_year } else if (this.identifier.publication_year && this.identifier.publication_month && this.identifier.publication_day) { - return formatDateUTC(this.identifier.publication_year + '-' + this.identifier.publication_month + '-' + this.identifier.publication_day) + const month = this.identifier.publication_month + const day = this.identifier.publication_day + return `${this.identifier.publication_year}-${month < 9 ? '0' + month : month}-${day < 9 ? '0' + day : day}` } else { return null } diff --git a/dbrepo-ui/dbrepo.config.json b/dbrepo-ui/dbrepo.config.json index 24ddb9e90f225cc0f8091747c7130d622c4ef3e5..cbd1ca4165707e19f274d9549e074ec67e88052e 100644 --- a/dbrepo-ui/dbrepo.config.json +++ b/dbrepo-ui/dbrepo.config.json @@ -1,6 +1,6 @@ { "title": "Database Repository", - "version": "1.4.0", + "version": "1.4.1", "ssl": { "force": false }, @@ -34,6 +34,11 @@ "secret": "seaweedfsadmin" } }, + "upload": { + "endpoint": "localhost", + "port": 1080, + "useSsl": false + }, "database": { "connection": { "extraInfo": null diff --git a/dbrepo-ui/pages/database/_database_id/info.vue b/dbrepo-ui/pages/database/_database_id/info.vue index 284395ed0e7e5aa39bdec7363ee80b16ba085c97..d2a619b876765cb2a24317827b587a1c540bcea5 100644 --- a/dbrepo-ui/pages/database/_database_id/info.vue +++ b/dbrepo-ui/pages/database/_database_id/info.vue @@ -16,10 +16,12 @@ <v-list dense> <v-list-item> <v-list-item-content> - <v-list-item-title> - Database Visibility + <v-list-item-title v-if="databaseImage" class="mt-2"> + Database Image </v-list-item-title> - <v-list-item-content v-if="database" v-text="`${database.is_public ? 'Public' : 'Private'}`" /> + <v-list-item-content v-if="databaseImage"> + <v-img :src="databaseImage" alt="database image" max-width="200" max-height="200" /> + </v-list-item-content> <v-list-item-title class="mt-2"> Database Name </v-list-item-title> @@ -28,6 +30,14 @@ Database Internal Name </v-list-item-title> <v-list-item-content v-if="database" v-text="database.internal_name" /> + <v-list-item-title> + Database Visibility + </v-list-item-title> + <v-list-item-content v-if="database" v-text="`${database.is_public ? 'Public' : 'Private'}`" /> + <v-list-item-title> + Database Size + </v-list-item-title> + <v-list-item-content v-if="databaseSize" v-text="databaseSize" /> <v-list-item-title class="mt-2"> Database Owner </v-list-item-title> @@ -102,7 +112,7 @@ <script> import DatabaseToolbar from '@/components/database/DatabaseToolbar.vue' -import { formatTimestampUTCLabel } from '@/utils' +import { formatTimestampUTCLabel, sizeToHumanLabel } from '@/utils' import DatabaseMapper from '@/api/database.mapper' import Summary from '@/components/identifier/Summary' import Select from '@/components/identifier/Select' @@ -225,7 +235,24 @@ export default { }, databaseExtraInfo () { return this.$config.databaseExtraInfo + }, + databaseSize () { + if (!this.database) { + return null + } + let sum = 0 + this.database.tables.forEach((t) => { sum += t.data_length }) + return sizeToHumanLabel(sum) + }, + databaseImage () { + if (!this.database || !this.database.image) { + return null + } + return `data:image/webp;base64,${this.database.image}` } + }, + methods: { + sizeToHumanLabel } } </script> diff --git a/dbrepo-ui/pages/database/_database_id/settings.vue b/dbrepo-ui/pages/database/_database_id/settings.vue index 8734d197c9ed18bad2e77ed70983ec36ae994136..77c5cccb82c02fcc558027690ad62691cdf52e59 100644 --- a/dbrepo-ui/pages/database/_database_id/settings.vue +++ b/dbrepo-ui/pages/database/_database_id/settings.vue @@ -4,6 +4,48 @@ <v-progress-linear v-if="loading" /> <v-tabs-items v-model="tab"> <v-tab-item> + <v-card v-if="canModifyImage" flat tile> + <v-card-title>Image</v-card-title> + <v-card-text> + <v-row dense> + <v-col> + The image will be displayed in a box with maximum dimensions 200x200 pixels. + </v-col> + </v-row> + <v-row dense> + <v-col sm="6"> + <v-file-input + v-model="fileModel" + accept="image/*" + hint="max. 1MB file size" + persistent-hint + clearable + :loading="loadingUpload" + :show-size="1000" + counter + label="Teaser Image" + @change="uploadFile" /> + </v-col> + </v-row> + <v-btn + small + class="black--text mt-4" + :loading="loadingImage" + @click="updateDatabaseImage"> + Modify Image + </v-btn> + <v-btn + v-if="database.image" + small + color="warning" + class="black--text mt-4" + :loading="loadingDeleteImage" + @click="removeDatabaseImage"> + Remove Image + </v-btn> + </v-card-text> + </v-card> + <v-divider /> <v-card v-if="isOwner" flat tile> <v-card-title>Access</v-card-title> <v-data-table @@ -100,6 +142,7 @@ import DatabaseToolbar from '@/components/database/DatabaseToolbar.vue' import EditAccess from '@/components/dialogs/EditAccess.vue' import DatabaseService from '@/api/database.service' import UserService from '@/api/user.service' +import UploadService from '@/api/upload.service' export default { components: { @@ -114,6 +157,10 @@ export default { accessType: null, users: [], loading: false, + loadingUpload: false, + loadingImage: false, + loadingDeleteImage: false, + fileModel: null, loadingUsers: false, editAccessDialog: false, editVisibilityDialog: false, @@ -123,6 +170,9 @@ export default { modifyOwner: { id: null }, + modifyImage: { + key: null + }, visibility: [ { text: 'Public', value: true }, { text: 'Private', value: false } @@ -214,6 +264,12 @@ export default { return false } return this.roles.includes('create-database-access') + }, + canModifyImage () { + if (!this.isOwner) { + return false + } + return this.roles.includes('modify-database-image') } }, watch: { @@ -254,6 +310,50 @@ export default { this.loading = false }) }, + uploadFile () { + this.loadingUpload = true + UploadService.upload(this.fileModel) + .then((metadata) => { + console.debug('uploaded image', metadata) + this.modifyImage.key = metadata.s3key + this.loadingUpload = false + }) + .finally(() => { + this.loadingUpload = false + }) + }, + updateDatabaseImage () { + this.loadingImage = true + DatabaseService.modifyImage(this.$route.params.database_id, this.modifyImage) + .then(() => { + this.$toast.success('Updated image successfully') + this.$store.dispatch('reloadDatabase') + this.loadingImage = false + }) + .catch(() => { + this.$toast.error('Failed to modify image') + this.loadingImage = false + }) + .finally(() => { + this.loadingImage = false + }) + }, + removeDatabaseImage () { + this.loadingDeleteImage = true + DatabaseService.modifyImage(this.$route.params.database_id, { key: null }) + .then(() => { + this.$toast.success('Removed image successfully') + this.$store.dispatch('reloadDatabase') + this.loadingDeleteImage = false + }) + .catch(() => { + this.$toast.error('Failed to delete image') + this.loadingDeleteImage = false + }) + .finally(() => { + this.loadingDeleteImage = false + }) + }, updateDatabaseOwner () { this.loading = true DatabaseService.modifyOwner(this.$route.params.database_id, this.modifyOwner.username) diff --git a/dbrepo-ui/pages/database/_database_id/table/_table_id/import.vue b/dbrepo-ui/pages/database/_database_id/table/_table_id/import.vue index 15ce462653c2ee36976c7edec4f94e92d775e85d..980b440ddd6beaa3a89283e350f5b6a657bc88da 100644 --- a/dbrepo-ui/pages/database/_database_id/table/_table_id/import.vue +++ b/dbrepo-ui/pages/database/_database_id/table/_table_id/import.vue @@ -205,7 +205,8 @@ export default { this.$toast.success('Successfully imported data') this.$router.push(`/database/${this.$route.params.database_id}/table/${this.$route.params.table_id}`) }) - .catch(() => { + .catch((error) => { + this.$toast.error('Failed to import data', error) this.loading = false }) .finally(() => { diff --git a/dbrepo-ui/pages/database/_database_id/table/_table_id/info.vue b/dbrepo-ui/pages/database/_database_id/table/_table_id/info.vue index d4b0620414afd29196250e01d1c8b150fa1b7b67..bff92a1fb5cebc7f18e6bc11dedf968816d7fc8d 100644 --- a/dbrepo-ui/pages/database/_database_id/table/_table_id/info.vue +++ b/dbrepo-ui/pages/database/_database_id/table/_table_id/info.vue @@ -17,9 +17,15 @@ <v-list-item-title> Table ID </v-list-item-title> - <v-list-item-content> - <span v-if="table && table.id">{{ table.id }}</span> - </v-list-item-content> + <v-list-item-content v-if="table && table.id" v-text="table.id" /> + <v-list-item-title v-if="table && table.data_length"> + Table Size + </v-list-item-title> + <v-list-item-content v-if="table && table.data_length" v-text="sizeToHumanLabel(table.data_length)" /> + <v-list-item-title v-if="table && table.num_rows"> + Table Rows + </v-list-item-title> + <v-list-item-content v-if="table && table.num_rows" v-text="table.num_rows" /> <v-list-item-title v-if="hasDescription" class="mt-2"> Table Description </v-list-item-title> @@ -109,7 +115,7 @@ import TableToolbar from '@/components/table/TableToolbar.vue' import Select from '@/components/identifier/Select' import Summary from '@/components/identifier/Summary' -import { formatTimestampUTCLabel } from '@/utils' +import { formatTimestampUTCLabel, sizeToHumanLabel } from '@/utils' import UserBadge from '@/components/UserBadge.vue' export default { @@ -228,6 +234,7 @@ export default { } }, methods: { + sizeToHumanLabel, amqpBadgeText (port) { if (port === 5672) { return 'insecure' diff --git a/dbrepo-ui/pages/database/_database_id/table/import.vue b/dbrepo-ui/pages/database/_database_id/table/import.vue index adc353bedae47e6e407a04acef396e74cc3db710..a013fcff66234e4bf2a5348714140e35a5263944 100644 --- a/dbrepo-ui/pages/database/_database_id/table/import.vue +++ b/dbrepo-ui/pages/database/_database_id/table/import.vue @@ -341,8 +341,8 @@ export default { resolve(metadata) }) .catch((error) => { + this.$toast.error('Failed to import data', error) this.loading = false - this.$toast.error('Failed to upload file') reject(error) }) .finally(() => { diff --git a/dbrepo-ui/utils/index.js b/dbrepo-ui/utils/index.js index 3e08f463448d549aa1ab6d455fa3c3b8046557e8..c626b026d0d0469fd3bb79a670c9676a4e522ac3 100644 --- a/dbrepo-ui/utils/index.js +++ b/dbrepo-ui/utils/index.js @@ -1046,6 +1046,32 @@ function timestampsToHumanDifference (date1, date2) { return moment.duration(other.diff(date)).humanize(true) } +function sizeToHumanLabel (num) { + let number = Number(num) + if (!number) { + return '0' + } + if (number < 1000) { + return `${Math.floor(number)} B` + } + number = number / 1000 + if (number < 1000) { + return `${Math.floor(number)} kB` + } + number = number / 1000 + if (number < 1000) { + return `${Math.floor(number)} MB` + } + number = number / 1000 + if (number < 1000) { + return `${number} GB` + } + number = number / 1000 + if (number < 1000) { + return `${number} TB` + } +} + module.exports = { notEmpty, formatTimestamp, @@ -1061,5 +1087,6 @@ module.exports = { formatBinaryStream, languages, formatLanguage, - timestampsToHumanDifference + timestampsToHumanDifference, + sizeToHumanLabel } diff --git a/docker-compose.yml b/docker-compose.yml index d783a4b77f19e284f156ec33d33f23ff985555f2..1fb934e77561d750f9d7bff2b25dae1d8a79b8c9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -150,6 +150,7 @@ services: KEYCLOAK_HOST: "${KEYCLOAK_HOST:-http://authentication-service:8080}" KEYCLOAK_ADMIN: "${KEYCLOAK_ADMIN:-fda}" KEYCLOAK_ADMIN_PASSWORD: "${KEYCLOAK_ADMIN_PASSWORD:-fda}" + KEYCLOAK_CLIENT_SECRET: "${KEYCLOAK_CLIENT_SECRET:-MUwRc7yfXSJwX8AdRMWaQC3Nep1VjwgG}" DATACITE_URL: "${DATACITE_URL:-https://api.test.datacite.org}" DATACITE_PREFIX: "${DATACITE_PREFIX:-}" DATACITE_USERNAME: "${DATACITE_USERNAME:-}" @@ -159,6 +160,7 @@ services: S3_SECRET_ACCESS_KEY: "${STORAGE_PASSWORD:-seaweedfsadmin}" S3_IMPORT_BUCKET: "${STORAGE_IMPORT_BUCKET:-dbrepo-upload}" S3_EXPORT_BUCKET: "${STORAGE_EXPORT_BUCKET:-dbrepo-download}" + DELETE_STALE_FILES_RATE: "${DELETE_STALE_FILES_RATE:-60}" MIRROR_RATE: ${METADATA_SERVICE_MIRROR_RATE:-60} OBTAIN_METADATA_RATE: ${METADATA_SERVICE_OBTAIN_METADATA_RATE:-60} DELETE_STALE_QUERIES_RATE: ${METADATA_SERVICE_DELETE_STALE_QUERIES_RATE:-60} diff --git a/helm-charts/dbrepo/Chart.yaml b/helm-charts/dbrepo/Chart.yaml index 59295782271c8f78c4796f8c32cd313807897e57..79a5fd1fa2cac445afa3c7d661a17589c7999129 100644 --- a/helm-charts/dbrepo/Chart.yaml +++ b/helm-charts/dbrepo/Chart.yaml @@ -16,38 +16,38 @@ home: https://www.ifs.tuwien.ac.at/infrastructures/dbrepo/ icon: https://gitlab.phaidra.org/fair-data-austria-db-repository/fda-services/-/raw/master/.docs/images/signet_white.png dependencies: - name: opensearch - alias: search-db + alias: searchDb version: 2.15.0 # app version 2.10.0 repository: https://opensearch-project.github.io/helm-charts/ - name: opensearch-dashboards - alias: search-db-dashboard + alias: searchDbDashboard version: 2.13.0 # app version 2.10.0 repository: https://opensearch-project.github.io/helm-charts/ - name: keycloak - alias: auth-service + alias: authService version: 17.3.3 repository: https://charts.bitnami.com/bitnami - name: mariadb-galera - alias: data-db + alias: dataDb version: 11.0.1 repository: https://charts.bitnami.com/bitnami - name: mariadb-galera - alias: metadata-db + alias: metadataDb version: 11.0.1 repository: https://charts.bitnami.com/bitnami - name: postgresql-ha - alias: auth-db + alias: authDb version: 12.1.7 repository: https://charts.bitnami.com/bitnami - name: rabbitmq - alias: broker-service + alias: brokerService version: 12.5.1 repository: https://charts.bitnami.com/bitnami - name: fluent-bit - alias: log-service + alias: logService version: 0.40.0 repository: https://fluent.github.io/helm-charts - name: seaweedfs - alias: storage-service + alias: storageService version: 3.59.4 repository: https://seaweedfs.github.io/seaweedfs/helm diff --git a/helm-charts/dbrepo/README.md b/helm-charts/dbrepo/README.md index e8cdbdc8001b12877da4fbbe55d88e1f48262e11..6311b5e8b65a7b11a48c43481c432365b070b333 100644 --- a/helm-charts/dbrepo/README.md +++ b/helm-charts/dbrepo/README.md @@ -18,6 +18,8 @@ helm install my-release "oci://s210.dl.hpc.tuwien.ac.at/dbrepo/helm/dbrepo" --va * Kubernetes 1.24+ * Kubernetes 3.8.0+ * PV provisioner support in the underlying infrastructure +* Ingress support in the underlying infrastructure +* TLS certificate provisioner support in the underlying infrastructure, e.g. [cert-manager](https://cert-manager.io/) ## Installing the Chart @@ -44,10 +46,201 @@ The command removes all the Kubernetes components associated with the chart and ### Common parameters -| Name | Description | Value | -|-----------------|--------------------------------------|-----------------| -| `namespace` | Namespace which DBRepo is running in | `""` | -| `hostname` | The hostname for ingress rules | `""` | -| `strategyType` | Deployments update strategy | `RollingUpdate` | -| `clusterDomain` | Internal cluster domain | `cluster.local` | +| Name | Description | Value | +|-----------------|---------------------------------------|-----------------| +| `namespace` | Namespace which DBRepo is running in. | `""` | +| `hostname` | The hostname for ingress rules. | `""` | +| `strategyType` | Deployments update strategy. | `RollingUpdate` | +| `clusterDomain` | Internal cluster domain. | `cluster.local` | + +### Metadata Database + +The Metadata Database uses the [Bitnami MariaDB Galera](https://artifacthub.io/packages/helm/bitnami/mariadb-galera) +Helm chart. See their documentation for the remaining overridden values. + +| Name | Description | Value | +|----------------------------|-------------------------------------------|---------------| +| `metadataDb.host` | Hostname. | `metadata-db` | +| `metadataDb.jdbcExtraArgs` | Extra arguments for the JDBC connections. | `""` | + +### Authentication Service + +The Auth Service uses the [Bitnami Keycloak](https://artifacthub.io/packages/helm/bitnami/keycloak) Helm chart. See +their documentation for the remaining overridden values. + +| Name | Description | Value | +|-----------------------------|-----------------------------------------------------------------|------------------------------------| +| `authService.client.id` | Client id. This value is publicly known. | `dbrepo-client` | +| `authService.client.secret` | Client secret. This value should never be known outside DBRepo. | `MUwRc7yfXSJwX8AdRMWaQC3Nep1VjwgG` | + +### Auth Database + +The Auth Database uses the [Bitnami PostgreSQL HA](https://artifacthub.io/packages/helm/bitnami/postgresql-ha) Helm +chart. See their documentation for the remaining overridden values. + +| Name | Description | Value | +|---------------|--------------------------------------|------------------| +| `authDb.host` | Hostname. Needed for other services. | `auth-db-pgpool` | +| `authDB.port` | Port. Needed for other services. | `5432` | + +### Data Database + +The Data Database uses the [Bitnami MariaDB Galera](https://artifacthub.io/packages/helm/bitnami/mariadb-galera) +Helm chart. See their documentation for the remaining overridden values. It is important to note that the Data Database +uses a sidecar to import/export files from the Storage Service. + +### Search Database + +The Search Database uses +the [OpenSearch](https://artifacthub.io/packages/helm/opensearch-project-helm-charts/opensearch) Helm +chart. See their documentation for the remaining overridden values. + +| Name | Description | Value | +|-------------------|--------------------------------------|-------------| +| `searchDb.host` | Hostname. Needed for other services. | `search-db` | +| `authDB.port` | Port. Needed for other services. | `9200` | +| `authDB.username` | Username. Needed for other services. | `admin` | +| `authDB.password` | Password. Needed for other services. | `admin` | + +### Search Database Dashboard + +The Search Database Dashboard uses +the [OpenSearch](https://artifacthub.io/packages/helm/opensearch-project-helm-charts/opensearch-dashboards) Helm +chart. See their documentation for the remaining overridden values. + +### Upload Service + +| Name | Description | Value | +|----------------------------------|----------------------------------------|-------------------| +| `uploadService.enabled` | Enables/disabled the deployment. | `true` | +| `uploadService.image.registry` | Registry to pull the image | `docker.io` | +| `uploadService.image.repository` | Repository to pull the image | `tusproject/tusd` | +| `uploadService.image.tag` | Tag of the image. | `v1.12` | +| `uploadService.replicaCount` | Number of replicas for the deployment. | `2` | + +### Broker Service + +The Broker Service uses the [Bitnami RabbitMQ](https://artifacthub.io/packages/helm/bitnami/rabbitmq) +Helm chart. See their documentation for the remaining overridden values. + +| Name | Description | Value | +|-----------------------------------|-------------------------------------------------------------------------|-------------------------------| +| `brokerService.url` | Admin API endpoint. Needed for other services. | `http://broker-service:15672` | +| `brokerService.host` | Service hostname. Needed for other services. | `broker-service` | +| `brokerService.port` | Service port. Needed for other services. | `5672` | +| `brokerService.virtualHost` | Virtual host on RabbitMQ. Needed for other services. | `dbrepo` | +| `brokerService.queueName` | Queue name on RabbitMQ. Needed for other services. | `dbrepo` | +| `brokerService.exchangeName` | Exchange name on RabbitMQ. Needed for other services. | `dbrepo` | +| `brokerService.routingKey` | Route binding for queue to exchange defined. Needed for other services. | `dbrepo.#` | +| `brokerService.connectionTimeout` | Connection timeout. Needed for other services. | `60000` | + +### Analyse Service + +| Name | Description | Value | +|-----------------------------------|----------------------------------------|----------------------------| +| `analyseService.enabled` | Enables/disabled the deployment. | `true` | +| `analyseService.image.registry` | Registry to pull the image | `s210.dl.hpc.tuwien.ac.at` | +| `analyseService.image.repository` | Repository to pull the image | `dbrepo/analyse-service` | +| `analyseService.image.tag` | Tag of the image. | `1.4.1` | +| `analyseService.image.pullPolicy` | Image pull policy on deployments | `Always` | +| `analyseService.image.debug` | Enables/disabled the debug logging. | `false` | +| `analyseService.replicaCount` | Number of replicas for the deployment. | `2` | + +### Metadata Service + +| Name | Description | Value | +|--------------------------------------------|----------------------------------------------------------------------------------|----------------------------| +| `metadataService.enabled` | Enables/disabled the deployment. | `true` | +| `metadataService.image.registry` | Registry to pull the image | `s210.dl.hpc.tuwien.ac.at` | +| `metadataService.image.repository` | Repository to pull the image | `dbrepo/metadata-service` | +| `metadataService.image.tag` | Tag of the image. | `1.4.1` | +| `metadataService.image.pullPolicy` | Image pull policy on deployments | `Always` | +| `metadataService.image.debug` | Enables/disabled the debug logging. | `false` | +| `metadataService.adminEmail` | E-Mail address of the administrator displayed for OAI-PMH. | `noreply@example.com` | +| `metadataService.authService.url` | Url to the Auth Service. | `http://auth-service` | +| `metadataService.website` | Url to redirect PIDs to. | `http://example.com` | +| `metadataService.repositoryName` | Repository name for OAI-PMH. | `Database Repository` | +| `metadataService.datacite.enabled` | Enable/disable DataCite Fabrica DOI minting. | `false` | +| `metadataService.datacite.url` | DataCite Fabrica API endpoint. | `https://api.datacite.org` | +| `metadataService.datacite.prefix` | DataCite Fabrica DOI prefix. | `""` | +| `metadataService.datacite.username` | DataCite Fabrica API username. | `""` | +| `metadataService.datacite.password` | DataCite Fabrica API password. | `""` | +| `metadataService.rates.deleteStaleFiles` | Interval rate to delete stale files in the Storage Service. | `60` | +| `metadataService.rates.mirror` | Interval rate to mirror to the Search Database. | `60` | +| `metadataService.rates.obtainMetadata` | Interval rate to obtain metadata from the Data Database. | `60` | +| `metadataService.rates.deleteStaleQueries` | Interval rate to delete stale queries from the Query Store in the Data Database. | `60` | +| `metadataService.replicaCount` | Number of replicas for the deployment. | `2` | + +### Data Service + +| Name | Description | Value | +|-----------------------------------------|--------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `metadataService.enabled` | Enables/disabled the deployment. | `true` | +| `metadataService.image.registry` | Registry to pull the image | `s210.dl.hpc.tuwien.ac.at` | +| `metadataService.image.repository` | Repository to pull the image | `dbrepo/data-service` | +| `metadataService.image.tag` | Tag of the image. | `1.4.1` | +| `metadataService.image.pullPolicy` | Image pull policy on deployments | `Always` | +| `metadataService.image.debug` | Enables/disabled the debug logging. | `false` | +| `metadataService.jwt.pubkey` | The JWT pubkey to verify JWT signature. | `MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqqnHQ2BWWW9vDNLRCcxD++xZg/16oqMo/c1l+lcFEjjAIJjJp/HqrPYU/U9GvquGE6PbVFtTzW1KcKawOW+FJNOA3CGo8Q1TFEfz43B8rZpKsFbJKvQGVv1Z4HaKPvLUm7iMm8Hv91cLduuoWx6Q3DPe2vg13GKKEZe7UFghF+0T9u8EKzA/XqQ0OiICmsmYPbwvf9N3bCKsB/Y10EYmZRb8IhCoV9mmO5TxgWgiuNeCTtNCv2ePYqL/U0WvyGFW0reasIK8eg3KrAUj8DpyOgPOVBn3lBGf+3KFSYi+0bwZbJZWqbC/Xlk20Go1YfeJPRIt7ImxD27R/lNjgDO/MwIDAQAB` | +| `metadataService.consumerConcurrentMin` | The number of concurrent consumers (minimum). | `1` | +| `metadataService.consumerConcurrentMax` | The number of concurrent consumers (maximum). | `5` | +| `metadataService.requeueRejected` | Requeue rejected tuples into the Broker Service. | `false` | +| `metadataService.replicaCount` | Number of replicas for the deployment. | `2` | + +### Search Service + +| Name | Description | Value | +|----------------------------------|----------------------------------------|----------------------------| +| `searchService.enabled` | Enables/disabled the deployment. | `true` | +| `searchService.image.registry` | Registry to pull the image | `s210.dl.hpc.tuwien.ac.at` | +| `searchService.image.repository` | Repository to pull the image | `dbrepo/search-service` | +| `searchService.image.tag` | Tag of the image. | `1.4.1` | +| `searchService.image.pullPolicy` | Image pull policy on deployments | `Always` | +| `searchService.image.debug` | Enables/disabled the debug logging. | `false` | +| `searchService.replicaCount` | Number of replicas for the deployment. | `2` | + +### Storage Service + +The Storage Service uses the [SeaweedFS](https://artifacthub.io/packages/helm/seaweedfs/seaweedfs) +Helm chart. See their documentation for the remaining overridden values. + +| Name | Description | Value | +|-----------------|---------------------------------------------|------------------| +| `auth.username` | Username for S3. Needed for other services. | `seaweedfsadmin` | +| `auth.password` | Password for S3. Needed for other services. | `seaweedfsadmin` | + +### User Interface + +To replace the placeholder values in +the [`dbrepo.config.json`](https://gitlab.phaidra.org/fair-data-austria-db-repository/fda-services/-/blob/release-v1.4/dbrepo-ui/dbrepo.config.json) +you need to create a ConfigMap `my-config` and mount the `dbrepo.config.json` into `/app/dbrepo.config.json`: + +```yaml +ui: + extraVolumes: + - name: config-map + configMap: + name: my-config + extraVolumeMounts: + - name: config-map + mountPath: /dbrepo.config.json + subPath: dbrepo.config.json + readOnly: true + ... +``` +| Name | Description | Value | +|------------------------|----------------------------------------|----------------------------| +| `ui.enabled` | Enables/disabled the deployment. | `enabled` | +| `ui.image.registry` | Registry to pull the image | `s210.dl.hpc.tuwien.ac.at` | +| `ui.image.repository` | Repository to pull the image | `dbrepo/ui` | +| `ui.image.tag` | Tag of the image. | `1.4.1` | +| `ui.image.pullPolicy` | Image pull policy on deployments | `Always` | +| `ui.replicaCount` | Number of replicas for the deployment. | `2` | +| `ui.extraVolumes` | List of extra volumes. | `[]` | +| `ui.extraVolumeMounts` | List of extra volume mounts. | `[]` | + +## Ingress + +The deployment depends on ingress, by default ingress is configured +for [NGINX Ingress Controller](https://github.com/kubernetes/ingress-nginx) with annotations. \ No newline at end of file diff --git a/helm-charts/dbrepo/templates/auth-service/secret.yaml b/helm-charts/dbrepo/templates/auth-service/secret.yaml index c6cc5911d9418808623fdce9608181fcecbece2a..bae6e2036a7375bb1af928a1521b8022a801cd63 100644 --- a/helm-charts/dbrepo/templates/auth-service/secret.yaml +++ b/helm-charts/dbrepo/templates/auth-service/secret.yaml @@ -4,8 +4,8 @@ metadata: name: auth-service-secret namespace: {{ .Values.namespace }} stringData: - db-host: "{{ index .Values "auth-db" "host" }}" - db-port: "{{ index .Values "auth-db" "port" }}" - db-name: "{{ index .Values "auth-db" "postgresql" "database" }}" - db-username: "{{ index .Values "auth-db" "postgresql" "username" }}" - db-password: "{{ index .Values "auth-db" "postgresql" "password" }}" + db-host: "{{ .Values.authDb.host }}" + db-port: "{{ .Values.authDb.port }}" + db-name: "{{ .Values.authDb.postgresql.database }}" + db-username: "{{ .Values.authDb.postgresql.username }}" + db-password: "{{ .Values.authDb.postgresql.password }}" diff --git a/helm-charts/dbrepo/templates/data-db/pvc.yaml b/helm-charts/dbrepo/templates/data-db/pvc.yaml index 0e55aa4a0a1d9fa1f69706aadae269d985401bdd..0d5c5c49af15f9603f2869a31387f94c174e8203 100644 --- a/helm-charts/dbrepo/templates/data-db/pvc.yaml +++ b/helm-charts/dbrepo/templates/data-db/pvc.yaml @@ -3,7 +3,7 @@ kind: PersistentVolumeClaim metadata: name: data-db-shared spec: - storageClassName: {{ index .Values "data-db" "persistence" "sharedStorageClass" }} + storageClassName: {{ .Values.dataDb.persistence.sharedStorageClass }} accessModes: - ReadWriteMany resources: diff --git a/helm-charts/dbrepo/templates/data-service/secret.yaml b/helm-charts/dbrepo/templates/data-service/secret.yaml index 52fbb2e3a9f70ab51a89be64639e8eb3c79fac94..3386171d49f7e942130842cbfdb1864125fe6c27 100644 --- a/helm-charts/dbrepo/templates/data-service/secret.yaml +++ b/helm-charts/dbrepo/templates/data-service/secret.yaml @@ -7,25 +7,25 @@ metadata: name: data-service-secret namespace: {{ .Values.namespace }} stringData: - metadata-db: "{{ index .Values "metadata-db" "db" "name" }}" - metadata-host: "{{ index .Values "metadata-db" "host" }}" - metadata-username: "{{ index .Values "metadata-db" "rootUser" "user" }}" - metadata-password: "{{ index .Values "metadata-db" "rootUser" "password" }}" - metadata-jdbc-extra-args: "{{ index .Values "metadata-db" "jdbcExtraArgs" }}" - search-username: "{{ index .Values "search-db" "username" }}" - search-password: "{{ index .Values "search-db" "password" }}" + metadata-db: "{{ .Values.metadataDb.db.name }}" + metadata-host: "{{ .Values.metadataDb.host }}" + metadata-username: "{{ .Values.metadataDb.rootUser.user }}" + metadata-password: "{{ .Values.metadataDb.rootUser.password }}" + metadata-jdbc-extra-args: "{{ .Values.metadataDb.jdbcExtraArgs }}" + search-username: "{{ .Values.searchDb.username }}" + search-password: "{{ .Values.searchDb.password }}" jwt-issuer: "{{ $jwtIssuer }}" jwt-pubkey: "{{ .Values.dataService.jwt.pubkey }}" - broker-username: "{{ index .Values "broker-service" "auth" "username" }}" - broker-password: "{{ index .Values "broker-service" "auth" "password" }}" + broker-username: "{{ .Values.brokerService.auth.username }}" + broker-password: "{{ .Values.brokerService.auth.password }}" min-concurrent-consumers: "{{ .Values.dataService.consumerConcurrentMin }}" max-concurrent-consumers: "{{ .Values.dataService.consumerConcurrentMax }}" requeue-rejected: "{{ .Values.dataService.requeueRejected }}" log-level: "{{ ternary "debug" "info" .Values.dataService.image.debug }}" - broker-host: "{{ index .Values "broker-service" "host" }}" - broker-port: "{{ index .Values "broker-service" "port" }}" - broker-virtualhost: "{{ index .Values "broker-service" "virtualHost" }}" - queue-name: "{{ index .Values "broker-service" "queueName" }}" - exchange-name: "{{ index .Values "broker-service" "exchangeName" }}" - routing-key: "{{ index .Values "broker-service" "routingKey" }}" - connection-timeout: "{{ index .Values "broker-service" "connectionTimeout" }}" \ No newline at end of file + broker-host: "{{ .Values.brokerService.host }}" + broker-port: "{{ .Values.brokerService.port }}" + broker-virtualhost: "{{ .Values.brokerService.virtualHost }}" + queue-name: "{{ .Values.brokerService.queueName }}" + exchange-name: "{{ .Values.brokerService.exchangeName }}" + routing-key: "{{ .Values.brokerService.routingKey }}" + connection-timeout: "{{ .Values.brokerService.connectionTimeout }}" \ No newline at end of file diff --git a/helm-charts/dbrepo/templates/metadata-service/deployment.yaml b/helm-charts/dbrepo/templates/metadata-service/deployment.yaml index a74271436d67ec3c2e5c7c401a41fe80e606c5cf..144a970c57d360c1f615fa39554f52b94381485f 100644 --- a/helm-charts/dbrepo/templates/metadata-service/deployment.yaml +++ b/helm-charts/dbrepo/templates/metadata-service/deployment.yaml @@ -129,6 +129,11 @@ spec: secretKeyRef: name: metadata-service-secret key: keycloak-admin-password + - name: KEYCLOAK_CLIENT_SECRET + valueFrom: + secretKeyRef: + name: metadata-service-secret + key: keycloak-client-secret - name: JWT_ISSUER valueFrom: secretKeyRef: @@ -239,6 +244,11 @@ spec: secretKeyRef: name: metadata-service-secret key: s3-export-bucket + - name: DELETE_STALE_FILES_RATE + valueFrom: + secretKeyRef: + name: metadata-service-secret + key: delete-stale-files-rate - name: MIRROR_RATE valueFrom: secretKeyRef: diff --git a/helm-charts/dbrepo/templates/metadata-service/secret.yaml b/helm-charts/dbrepo/templates/metadata-service/secret.yaml index a8ebcba53df13966383988c4a2fda4c6f22746e3..3a139a74e8739d6669d04806f44b769f03113924 100644 --- a/helm-charts/dbrepo/templates/metadata-service/secret.yaml +++ b/helm-charts/dbrepo/templates/metadata-service/secret.yaml @@ -9,24 +9,25 @@ metadata: stringData: admin-email: "{{ .Values.metadataService.adminEmail }}" base-url: "{{ .Values.hostname }}" - broker-endpoint: "{{ index .Values "broker-service" "url" }}" - broker-host: "{{ index .Values "broker-service" "host" }}" - broker-port: "{{ index .Values "broker-service" "port" }}" + broker-endpoint: "{{ .Values.brokerService.url }}" + broker-host: "{{ .Values.brokerService.host }}" + broker-port: "{{ .Values.brokerService.port }}" gateway-endpoint: "{{ .Values.hostname }}" website: "{{ .Values.metadataService.website }}" - search-username: "{{ index .Values "search-db" "username" }}" - search-password: "{{ index .Values "search-db" "password" }}" - broker-username: "{{ index .Values "broker-service" "auth" "username" }}" - broker-password: "{{ index .Values "broker-service" "auth" "password" }}" + search-username: "{{ .Values.searchDb.username }}" + search-password: "{{ .Values.searchDb.password }}" + broker-username: "{{ .Values.brokerService.auth.username }}" + broker-password: "{{ .Values.brokerService.auth.password }}" log-level: "{{ ternary "trace" "info" .Values.metadataService.image.debug }}" - metadata-db: "{{ index .Values "metadata-db" "db" "name" }}" - metadata-host: "{{ index .Values "metadata-db" "host" }}" - metadata-username: "{{ index .Values "metadata-db" "rootUser" "user" }}" - metadata-password: "{{ index .Values "metadata-db" "rootUser" "password" }}" - metadata-jdbc-extra-args: "{{ index .Values "metadata-db" "jdbcExtraArgs" }}" + metadata-db: "{{ .Values.metadataDb.db.name }}" + metadata-host: "{{ .Values.metadataDb.host }}" + metadata-username: "{{ .Values.metadataDb.rootUser.user }}" + metadata-password: "{{ .Values.metadataDb.rootUser.password }}" + metadata-jdbc-extra-args: "{{ .Values.metadataDb.jdbcExtraArgs }}" keycloak-host: "{{ .Values.metadataService.authService.url }}" - keycloak-admin: "{{ index .Values "auth-service" "auth" "adminUser" }}" - keycloak-admin-password: "{{ index .Values "auth-service" "auth" "adminPassword" }}" + keycloak-admin: "{{ .Values.authService.auth.adminUser }}" + keycloak-admin-password: "{{ .Values.authService.auth.adminPassword }}" + keycloak-client-secret: "{{ .Values.authService.client.secret }}" datacite-url: "{{ .Values.metadataService.datacite.url }}" datacite-prefix: "{{ .Values.metadataService.datacite.prefix | toString }}" datacite-username: "{{ .Values.metadataService.datacite.username }}" @@ -34,19 +35,20 @@ stringData: repository-name: "{{ .Values.metadataService.repositoryName }}" pid-base: "{{ $pidBase }}" jwt-issuer: "{{ $jwtIssuer }}" - broker-virtualhost: "{{ index .Values "broker-service" "virtualHost" }}" - queue-name: "{{ index .Values "broker-service" "queueName" }}" - exchange-name: "{{ index .Values "broker-service" "exchangeName" }}" - routing-key: "{{ index .Values "broker-service" "routingKey" }}" - connection-timeout: "{{ index .Values "broker-service" "connectionTimeout" }}" + broker-virtualhost: "{{ .Values.brokerService.virtualHost }}" + queue-name: "{{ .Values.brokerService.queueName }}" + exchange-name: "{{ .Values.brokerService.exchangeName }}" + routing-key: "{{ .Values.brokerService.routingKey }}" + connection-timeout: "{{ .Values.brokerService.connectionTimeout }}" min-concurrent-consumers: "{{ .Values.dataService.consumerConcurrentMin }}" max-concurrent-consumers: "{{ .Values.dataService.consumerConcurrentMax }}" requeue-rejected: "{{ .Values.dataService.requeueRejected }}" s3-storage-endpoint: http://storage-service-s3:9000 - s3-access-key-id: seaweedfsadmin - s3-secret-access-key: seaweedfsadmin - s3-import-bucket: dbrepo-upload - s3-export-bucket: dbrepo-download + s3-access-key-id: "{{ .Values.storageService.s3.auth.username }}" + s3-secret-access-key: "{{ .Values.storageService.s3.auth.password }}" + s3-import-bucket: "dbrepo-upload" + s3-export-bucket: "dbrepo-download" + delete-stale-files-rate: {{ .Values.metadataService.rates.deleteStaleFiles | quote }} mirror-rate: {{ .Values.metadataService.rates.mirror | quote }} obtain-metadata-rate: {{ .Values.metadataService.rates.obtainMetadata | quote }} delete-stale-queries-rate: {{ .Values.metadataService.rates.deleteStaleQueries | quote }} diff --git a/helm-charts/dbrepo/templates/search-db-dashboard/secret.yaml b/helm-charts/dbrepo/templates/search-db-dashboard/secret.yaml index c38c243c6c9b34b8aa01fc9ccf6a747e4066aa04..f7caf1d29290312437cf014e2766d8b8eabcd226 100644 --- a/helm-charts/dbrepo/templates/search-db-dashboard/secret.yaml +++ b/helm-charts/dbrepo/templates/search-db-dashboard/secret.yaml @@ -2,7 +2,7 @@ apiVersion: v1 kind: Secret metadata: name: search-db-dashboard-secret - namespace: {{ $.Values.namespace }} + namespace: {{ .Values.namespace }} stringData: opensearch_dashboards.yml: | server: @@ -17,5 +17,5 @@ stringData: opensearch: ssl: verificationMode: none - username: {{ index .Values "search-db" "username" }} - password: {{ index .Values "search-db" "password" }} + username: {{ .Values.searchDb.username }} + password: {{ .Values.searchDb.password }} diff --git a/helm-charts/dbrepo/templates/search-service/secret.yaml b/helm-charts/dbrepo/templates/search-service/secret.yaml index ed2881fe17a0ec16c9d2875c6d3b9abab935ac0b..de636d510424d2250fe598463c4eb4b6b6be53b2 100644 --- a/helm-charts/dbrepo/templates/search-service/secret.yaml +++ b/helm-charts/dbrepo/templates/search-service/secret.yaml @@ -5,8 +5,8 @@ metadata: name: search-service-secret namespace: {{ .Values.namespace }} stringData: - opensearch-host: "{{ index .Values "search-db" "host" }}" - opensearch-port: "{{ index .Values "search-db" "port" }}" - opensearch-username: "{{ index .Values "search-db" "username" }}" - opensearch-password: "{{ index .Values "search-db" "password" }}" + opensearch-host: "{{ .Values.searchDb.host }}" + opensearch-port: "{{ .Values.searchDb.port }}" + opensearch-username: "{{ .Values.searchDb.username }}" + opensearch-password: "{{ .Values.searchDb.password }}" log-level: "{{ ternary "DEBUG" "INFO" .Values.searchService.image.debug }}" diff --git a/helm-charts/dbrepo/templates/storage-service/secret.yaml b/helm-charts/dbrepo/templates/storage-service/secret.yaml index 525323c623e127e23957b5b74b9c83f8d9980f0b..baf13a16ad937e93c364e6fbd4e1a594c10c227c 100644 --- a/helm-charts/dbrepo/templates/storage-service/secret.yaml +++ b/helm-charts/dbrepo/templates/storage-service/secret.yaml @@ -14,8 +14,8 @@ stringData: "name": "admin", "credentials": [ { - "accessKey": "{{ index .Values "storage-service" "s3" "auth" "username" }}", - "secretKey": "{{ index .Values "storage-service" "s3" "auth" "password" }}" + "accessKey": "{{ .Values.storageService.s3.auth.username }}", + "secretKey": "{{ .Values.storageService.s3.auth.password }}" } ], "actions": [ diff --git a/helm-charts/dbrepo/templates/ui/configmap.yaml b/helm-charts/dbrepo/templates/ui/configmap.yaml index 932fcfac905f6921950198e6c9f0c3836d96f7bc..2307a33b23adb36311d138700fbefc9554ace6ad 100644 --- a/helm-charts/dbrepo/templates/ui/configmap.yaml +++ b/helm-charts/dbrepo/templates/ui/configmap.yaml @@ -8,28 +8,30 @@ metadata: data: dbrepo.config.json: | { - "title": "{{ .Values.ui.title }}", - "version": "{{ .Values.ui.version }}", + "title": "Database Repository", + "version": "1.4.1", "ssl": { - "force": "{{ .Values.ui.forceSsl }}" + "force": false }, "logo": { - "path": "{{ .Values.ui.logo }}" + "path": "/logo.svg" }, "icon": { - "path": "{{ .Values.ui.icon }}" + "path": "/favicon.ico" }, "appleTouchIcon": { - "path": "{{ .Values.ui.appleTouchIcon }}" + "path": "/apple-touch-icon.png" }, "api": { - "useSsl": "{{ .Values.ui.forceSsl }}" + "useSsl": false }, "broker": { "connection": { - "host": "{{ .Values.ui.broker.host }}", - "ports": {{ .Values.ui.broker.ports | toJson }}, - "extraInfo": "{{ .Values.ui.broker.extraInfo}}" + "host": "localhost", + "ports": [ + 5672 + ], + "extraInfo": null } }, "storage": { @@ -41,9 +43,14 @@ data: "secret": "seaweedfsadmin" } }, + "upload": { + "endpoint": "localhost", + "port": 1080, + "useSsl": false + }, "database": { "connection": { - "extraInfo": "128.130.0.0/15" + "extraInfo": null } }, "keycloak": { @@ -58,38 +65,22 @@ data: }, "pid": { "default": { - "publisher": "TU Wien" + "publisher": "Example University" } }, "doi": { - "url": "{{ .Values.ui.datacite.url }}" + "url": "https://doi.org" }, "pages": { "login": { - "links": [ - { - "text": "OpenSearch Admin", - "blank": true, - "href": "{{ printf "https://%s/admin/dashboard" .Values.hostname }}" - }, - { - "text": "RabbitMQ Admin", - "blank": true, - "href": "{{ printf "https://%s/admin/broker/" .Values.hostname }}" - }, - { - "text": "Keycloak Admin", - "blank": true, - "href": "{{ printf "https://%s/api/auth/" .Values.hostname }}" - } - ] + "links": [] }, "information": { "links": [ { "text": "Online Documentation", "blank": true, - "href": "https://www.ifs.tuwien.ac.at/infrastructures/dbrepo/latest/" + "href": "https://www.ifs.tuwien.ac.at/infrastructures/dbrepo/" }, { "text": "Sourcecode Documentation", @@ -107,9 +98,9 @@ data: "href": "https://test.dbrepo.tuwien.ac.at/" }, { - "text": "Demo Instance (Docker Compose)", + "text": "Pilot Instance (Docker Compose)", "blank": true, - "href": "https://dbrepo2.ec.tuwien.ac.at/" + "href": "https://dbrepo1.ec.tuwien.ac.at/" }, { "text": "Paper", diff --git a/helm-charts/dbrepo/templates/upload-service/secret.yaml b/helm-charts/dbrepo/templates/upload-service/secret.yaml index 1d94dc2ebdcc8ecfda0b934fc6fb2849ba1ba535..ba309cc7ec41b7390f46c5e12d0a2893e31bee18 100644 --- a/helm-charts/dbrepo/templates/upload-service/secret.yaml +++ b/helm-charts/dbrepo/templates/upload-service/secret.yaml @@ -6,7 +6,7 @@ metadata: name: upload-service-secret namespace: {{ .Values.namespace }} stringData: - aws-access-key-id: "{{ index .Values "storage-service" "s3" "auth" "username" }}" - aws-secret-access-key: "{{ index .Values "storage-service" "s3" "auth" "password" }}" + aws-access-key-id: "{{ .Values.storageService.s3.auth.username }}" + aws-secret-access-key: "{{ .Values.storageService.s3.auth.password }}" aws-region: "default" {{- end }} \ No newline at end of file diff --git a/helm-charts/dbrepo/values.yaml b/helm-charts/dbrepo/values.yaml index a477b725a9e870d7ab9b98a3346be26f380e6850..793a27405d5c1151556f15013bff5df898b56ae7 100644 --- a/helm-charts/dbrepo/values.yaml +++ b/helm-charts/dbrepo/values.yaml @@ -6,7 +6,7 @@ strategyType: RollingUpdate clusterDomain: cluster.local -metadata-db: +metadataDb: fullnameOverride: metadata-db image: debug: false @@ -18,7 +18,7 @@ metadata-db: db: name: fda metrics: - enabled: true + enabled: false galera: mariabackup: user: mariabackup @@ -27,7 +27,7 @@ metadata-db: service: type: ClusterIP annotations: { } - #loadBalancerIP: 1.2.3.4 + loadBalancerIP: "" loadBalancerSourceRanges: [ ] persistence: enabled: false @@ -35,7 +35,7 @@ metadata-db: storageClass: default replicaCount: 3 # uneven 3,5,7 -auth-service: +authService: fullnameOverride: auth-service image: debug: false @@ -71,7 +71,7 @@ auth-service: mountPath: /opt/bitnami/keycloak/data/import replicaCount: 2 -auth-db: +authDb: fullnameOverride: auth-db host: auth-db-pgpool port: 5432 @@ -89,14 +89,14 @@ auth-db: service: type: ClusterIP annotations: { } - #loadBalancerIP: 1.2.3.4 + loadBalancerIP: "" loadBalancerSourceRanges: [ ] persistence: enabled: false size: 10Gi storageClass: default -data-db: +dataDb: fullnameOverride: data-db image: debug: false @@ -112,7 +112,7 @@ data-db: password: mariabackup sidecars: - name: sidecar - image: s210.dl.hpc.tuwien.ac.at/dbrepo/data-db-sidecar:latest + image: s210.dl.hpc.tuwien.ac.at/dbrepo/data-db-sidecar:1.4.1 imagePullPolicy: Always securityContext: runAsUser: 1001 @@ -156,7 +156,7 @@ data-db: sharedStorageClass: default replicaCount: 3 # uneven -search-db: +searchDb: fullnameOverride: search-db host: search-db port: 9200 @@ -232,12 +232,12 @@ search-db: ".opendistro-asynchronous-search-response*", ] -search-db-dashboard: +searchDbDashboard: fullnameOverride: search-db-dashboard opensearchHosts: http://search-db:9200 extraInitContainers: - name: init - image: s210.dl.hpc.tuwien.ac.at/dbrepo/search-db-init:latest + image: s210.dl.hpc.tuwien.ac.at/dbrepo/search-db-init:1.4.1 imagePullPolicy: Always env: - name: OPENSEARCH_HOST @@ -267,7 +267,7 @@ uploadService: tag: v1.12 replicaCount: 2 -broker-service: +brokerService: fullnameOverride: broker-service image: debug: true @@ -344,7 +344,7 @@ analyseService: image: registry: s210.dl.hpc.tuwien.ac.at repository: dbrepo/analyse-service - tag: "latest" + tag: "1.4.1" pullPolicy: Always debug: false replicaCount: 2 @@ -354,7 +354,7 @@ metadataService: image: registry: s210.dl.hpc.tuwien.ac.at repository: dbrepo/metadata-service - tag: "latest" + tag: "1.4.1" pullPolicy: Always debug: false adminEmail: noreply@example.com @@ -369,6 +369,7 @@ metadataService: username: "" password: "" rates: + deleteStaleFiles: 60 mirror: 60 obtainMetadata: 60 deleteStaleQueries: 60 @@ -379,7 +380,7 @@ dataService: image: registry: s210.dl.hpc.tuwien.ac.at repository: dbrepo/data-service - tag: "latest" + tag: "1.4.1" pullPolicy: Always debug: false jwt: @@ -394,12 +395,12 @@ searchService: image: registry: s210.dl.hpc.tuwien.ac.at repository: dbrepo/search-service - tag: "latest" + tag: "1.4.1" pullPolicy: Always debug: false replicaCount: 2 -storage-service: +storageService: fullnameOverride: storage-service master: enabled: true @@ -430,7 +431,7 @@ storage-service: username: seaweedfsadmin password: seaweedfsadmin -log-service: +logService: fullnameOverride: log-service config: outputs: | @@ -468,24 +469,8 @@ ui: image: registry: s210.dl.hpc.tuwien.ac.at repository: dbrepo/ui - tag: "latest" + tag: "1.4.1" pullPolicy: Always - logo: /logo.svg - icon: /favicon.ico - appleTouchIcon: /apple-touch-logo.png - version: 1.4.0 - title: Database Repository - datacite: - url: https://doi.org - keycloak: - url: /api/auth/admin/ - broker: - url: /admin/broker/ - host: broker-service - extraInfo: "" - ports: - - 5672 - forceSsl: false replicaCount: 2 extraVolumes: [ ] # - name: images-map diff --git a/mkdocs.yml b/mkdocs.yml index ae8084fafc40d7832f7bf97b9965008a4ea55fd6..14affa17a9a3a1ebb0687936f79c7895f73c3b19 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -22,7 +22,7 @@ nav: - Storage Service: system-services-storage.md - Upload Service: system-services-upload.md - Databases: - - Auth Database: system-databases-auth.md + - Authentication Database: system-databases-authentication.md - Data Database: system-databases-data.md - Metadata Database: system-databases-metadata.md - Search Database: system-databases-search.md @@ -33,8 +33,10 @@ nav: - Overview: usage-overview.md - Services: - Analyse Service: usage-analyse.md - - Authentication Service: usage-auth.md + - Authentication Service: usage-authentication.md - Broker Service: usage-broker.md + - Metadata Service: usage-metadata.md + - Search Service: usage-search.md - Storage Service: usage-storage.md - Upload Service: usage-upload.md - publications.md