From 86eade8a643c121a6c6ab9d6647c8a1c7a354870 Mon Sep 17 00:00:00 2001 From: Martin Weise <martin.weise@tuwien.ac.at> Date: Wed, 12 Jun 2024 22:31:40 +0000 Subject: [PATCH] Dev --- .docs/.swagger/api.base.yaml | 7 +- .docs/.swagger/api.yaml | 979 +++++++++----- dbrepo-analyse-service/app.py | 22 +- .../as-yml/analyse_datatypes.yml | 2 +- .../as-yml/analyse_keys.yml | 4 +- .../as-yml/analyse_table_stat.yml | 41 - .../at/tuwien/endpoints/AccessEndpoint.java | 45 +- .../at/tuwien/endpoints/DatabaseEndpoint.java | 6 +- .../at/tuwien/endpoints/SubsetEndpoint.java | 54 +- .../at/tuwien/endpoints/TableEndpoint.java | 92 +- .../at/tuwien/endpoints/ViewEndpoint.java | 22 +- .../endpoint/AccessEndpointUnitTest.java | 23 +- .../endpoint/DatabaseEndpointUnitTest.java | 4 - .../endpoint/TableEndpointUnitTest.java | 7 +- .../tuwien/endpoint/ViewEndpointUnitTest.java | 1 - .../tuwien/mvc/PrometheusEndpointMvcTest.java | 2 +- .../at/tuwien/endpoints/AccessEndpoint.java | 37 +- .../at/tuwien/endpoints/ConceptEndpoint.java | 5 +- .../tuwien/endpoints/ContainerEndpoint.java | 41 +- .../at/tuwien/endpoints/DatabaseEndpoint.java | 46 +- .../tuwien/endpoints/IdentifierEndpoint.java | 61 +- .../at/tuwien/endpoints/ImageEndpoint.java | 24 +- .../at/tuwien/endpoints/LicenseEndpoint.java | 8 +- .../at/tuwien/endpoints/MessageEndpoint.java | 32 +- .../at/tuwien/endpoints/MetadataEndpoint.java | 10 +- .../at/tuwien/endpoints/OntologyEndpoint.java | 34 +- .../at/tuwien/endpoints/TableEndpoint.java | 54 +- .../at/tuwien/endpoints/UnitEndpoint.java | 4 +- .../at/tuwien/endpoints/UserEndpoint.java | 58 +- .../at/tuwien/endpoints/ViewEndpoint.java | 36 +- .../endpoints/AccessEndpointUnitTest.java | 5 +- .../endpoints/ContainerEndpointUnitTest.java | 2 +- ...Test.java => MessageEndpointUnitTest.java} | 14 +- .../endpoints/UserEndpointUnitTest.java | 4 +- .../tuwien/mvc/PrometheusEndpointMvcTest.java | 2 +- .../java/at/tuwien/service/AccessService.java | 5 +- .../service/impl/AccessServiceImpl.java | 18 +- dbrepo-search-service/app.py | 5 +- docker-compose.yml | 1 + helm/dbrepo/values.yaml | 8 + lib/python/dbrepo/RestClient.py | 1122 ++++++++++++----- lib/python/dbrepo/api/dto.py | 13 + lib/python/dbrepo/api/exceptions.py | 28 + lib/python/tests/test_unit_identifier.py | 67 +- lib/python/tests/test_unit_query.py | 158 +-- lib/python/tests/test_unit_table.py | 62 +- lib/python/tests/test_unit_user.py | 41 +- lib/python/tests/test_unit_view.py | 10 - 48 files changed, 2007 insertions(+), 1319 deletions(-) delete mode 100644 dbrepo-analyse-service/as-yml/analyse_table_stat.yml rename dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/endpoints/{MaintenanceEndpointUnitTest.java => MessageEndpointUnitTest.java} (91%) diff --git a/.docs/.swagger/api.base.yaml b/.docs/.swagger/api.base.yaml index aa83c853ce..c7b01fab0e 100644 --- a/.docs/.swagger/api.base.yaml +++ b/.docs/.swagger/api.base.yaml @@ -10,13 +10,16 @@ components: scheme: bearer type: http externalDocs: - description: Sourcecode Documentation + description: Project Website url: https://www.ifs.tuwien.ac.at/infrastructures/dbrepo/ info: contact: email: andreas.rauber@tuwien.ac.at name: Prof. Andreas Rauber - description: The REST API + description: | + The merged REST API of DBRepo for users, developers and data stewards to be accessed publicly. Have a look at + the [source code](https://gitlab.phaidra.org/fair-data-austria-db-repository/fda-services) for non-public endpoints + that are used between the services themselves. license: name: Apache 2.0 url: https://www.apache.org/licenses/LICENSE-2.0 diff --git a/.docs/.swagger/api.yaml b/.docs/.swagger/api.yaml index 7ba582ed3f..c2b5b17fd4 100644 --- a/.docs/.swagger/api.yaml +++ b/.docs/.swagger/api.yaml @@ -3,7 +3,15 @@ info: contact: email: andreas.rauber@tuwien.ac.at name: Prof. Andreas Rauber - description: The REST API + description: > + The merged REST API of DBRepo for users, developers and data stewards to be + accessed publicly. Have a look at + + the [source + code](https://gitlab.phaidra.org/fair-data-austria-db-repository/fda-services) + for non-public endpoints + + that are used between the services themselves. license: name: Apache 2.0 url: 'https://www.apache.org/licenses/LICENSE-2.0' @@ -15,14 +23,16 @@ servers: - description: Local Instance url: 'http://localhost' externalDocs: - description: Sourcecode Documentation + description: Project Website url: 'https://www.ifs.tuwien.ac.at/infrastructures/dbrepo/' paths: /api/analyse/datatypes: get: consumes: - application/json - description: This is a simple API which returns the datatypes of a (path) csv file + description: >- + Determines MySQL 8 datatypes of a given dataset. Requires role + `table-semantic-analyse`. operationId: analyse_datatypes parameters: - example: filename_s3_key @@ -87,8 +97,8 @@ paths: consumes: - application/json description: >- - This is a simple API which returns the primary keys + ranking of a - (path) csv file + Determines primary keys of a given dataset. Requires role + `table-semantic-analyse`. operationId: analyse_keys parameters: - example: filename_s3_key @@ -133,14 +143,18 @@ paths: security: - bearerAuth: [] - basicAuth: [] - summary: Determine primary keys + summary: Determine keys tags: - analyse-endpoint '/api/database/{databaseId}/view/{viewId}/data': get: tags: - view-endpoint - summary: Retrieve view data + summary: Get view data + description: >- + Gets data from a view of a database. For private databases, the user + needs at least *READ* access to the associated database. Requires role + `view-database-view-data`. operationId: getData parameters: - name: databaseId @@ -176,6 +190,15 @@ paths: responses: '200': description: Retrieved view data + headers: + Access-Control-Expose-Headers: + description: Expose `X-Count` custom header + required: true + style: simple + X-Count: + description: Number of rows + required: true + style: simple content: application/json: schema: @@ -216,7 +239,11 @@ paths: head: tags: - view-endpoint - summary: Retrieve view data + summary: Get view data + description: >- + Gets data from a view of a database. For private databases, the user + needs at least *READ* access to the associated database. Requires role + `view-database-view-data`. operationId: getData_1 parameters: - name: databaseId @@ -252,6 +279,15 @@ paths: responses: '200': description: Retrieved view data + headers: + Access-Control-Expose-Headers: + description: Expose `X-Count` custom header + required: true + style: simple + X-Count: + description: Number of rows + required: true + style: simple content: application/json: schema: @@ -293,7 +329,13 @@ paths: get: tags: - table-endpoint - summary: Retrieve table data + summary: Get table data + description: >- + Gets data from a table with id. For a table in a private database, the + user needs to have at least *READ* access to the associated database. + Requests with HTTP method **GET** return the full dataset, requests with + HTTP method **HEAD** only the number of tuples in the `X-Count` header. + Requires role `view-table-data`. operationId: getData_2 parameters: - name: databaseId @@ -328,7 +370,16 @@ paths: format: int64 responses: '200': - description: Retrieved table data + description: Get table data + headers: + Access-Control-Expose-Headers: + description: Expose `X-Count` custom header + required: true + style: simple + X-Count: + description: Number of rows + required: true + style: simple content: application/json: schema: @@ -339,6 +390,12 @@ paths: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' + '403': + description: Not allowed to get table data + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorDto' '404': description: Failed to find table in metadata database content: @@ -357,10 +414,11 @@ paths: put: tags: - table-endpoint - summary: Update a raw data tuple + summary: Update tuple description: >- - Updates a raw data tuple in a table with at least WRITE_OWN access. Then - update the table statistics. + Updates a data tuple into a table, then the table statistics are + updated. The user needs to have at least *WRITE_OWN* access to the + associated database. Requires role `insert-table-data`. operationId: updateRawTuple parameters: - name: databaseId @@ -414,10 +472,11 @@ paths: post: tags: - table-endpoint - summary: Insert a raw data tuple + summary: Insert tuple description: >- - Inserts a raw data tuple into a table with at least WRITE_OWN access. - Then update the table statistics. + Inserts a data tuple into a table, then the table statistics are + updated. The user needs to have at least *WRITE_OWN* access to the + associated database. Requires role `insert-table-data`. operationId: insertRawTuple parameters: - name: databaseId @@ -473,10 +532,11 @@ paths: delete: tags: - table-endpoint - summary: Delete table data + summary: Delete tuple description: >- - Deletes a raw data tuple in a table with at least WRITE_OWN access. Then - update the table statistics. + Deletes a data tuple into a table, then the table statistics are + updated. The user needs to have at least *WRITE_OWN* access to the + associated database. Requires role `delete-table-data`. operationId: deleteRawTuple parameters: - name: databaseId @@ -530,7 +590,13 @@ paths: head: tags: - table-endpoint - summary: Retrieve table data + summary: Get table data + description: >- + Gets data from a table with id. For a table in a private database, the + user needs to have at least *READ* access to the associated database. + Requests with HTTP method **GET** return the full dataset, requests with + HTTP method **HEAD** only the number of tuples in the `X-Count` header. + Requires role `view-table-data`. operationId: getData_3 parameters: - name: databaseId @@ -565,7 +631,16 @@ paths: format: int64 responses: '200': - description: Retrieved table data + description: Get table data + headers: + Access-Control-Expose-Headers: + description: Expose `X-Count` custom header + required: true + style: simple + X-Count: + description: Number of rows + required: true + style: simple content: application/json: schema: @@ -576,6 +651,12 @@ paths: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' + '403': + description: Not allowed to get table data + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorDto' '404': description: Failed to find table in metadata database content: @@ -595,7 +676,13 @@ paths: get: tags: - subset-endpoint - summary: Retrieved subset data + summary: Get subset data + description: >- + Gets data of subset with id. For private databases, the user needs at + least *READ* access to the associated database. Requests with HTTP + method **GET** return the subset dataset, requests with HTTP method + **HEAD** only the number of rows in the subset dataset in the `X-Count` + header operationId: getData_4 parameters: - name: databaseId @@ -625,12 +712,21 @@ paths: responses: '200': description: Retrieved subset data + headers: + Access-Control-Expose-Headers: + description: Expose `X-Count` custom header + required: true + style: simple + X-Count: + description: Number of rows + required: true + style: simple content: application/json: schema: $ref: '#/components/schemas/QueryResultDto' '400': - description: Malformed select query + description: Invalid pagination content: application/json: schema: @@ -661,7 +757,13 @@ paths: head: tags: - subset-endpoint - summary: Retrieved subset data + summary: Get subset data + description: >- + Gets data of subset with id. For private databases, the user needs at + least *READ* access to the associated database. Requests with HTTP + method **GET** return the subset dataset, requests with HTTP method + **HEAD** only the number of rows in the subset dataset in the `X-Count` + header operationId: getData_5 parameters: - name: databaseId @@ -691,12 +793,21 @@ paths: responses: '200': description: Retrieved subset data + headers: + Access-Control-Expose-Headers: + description: Expose `X-Count` custom header + required: true + style: simple + X-Count: + description: Number of rows + required: true + style: simple content: application/json: schema: $ref: '#/components/schemas/QueryResultDto' '400': - description: Malformed select query + description: Invalid pagination content: application/json: schema: @@ -729,6 +840,7 @@ paths: tags: - subset-endpoint summary: Persist subset + description: Persists a subset with id. Requires role `persist-query`. operationId: persist parameters: - name: databaseId @@ -795,10 +907,12 @@ paths: post: tags: - table-endpoint - summary: Import data from a dataset + summary: Import dataset description: >- - Deletes a raw data tuple in a table with at least WRITE_OWN access. Then - update the table statistics. + Imports a dataset in a table. Then update the table statistics. The user + needs to have at least *WRITE_OWN* access to the associated database + when importing into a owned table. Otherwise *WRITE_ALL* access in + needed. Requires role `insert-table-data`. operationId: importDataset parameters: - name: databaseId @@ -854,6 +968,10 @@ paths: tags: - subset-endpoint summary: Find subsets + description: >- + Finds subsets in the query store. The result can be optionally filtered + by setting `persisted`. When set to *true*, only persisted queries are + returned, otherwise only non-persisted queries are returned. operationId: list parameters: - name: databaseId @@ -873,7 +991,9 @@ paths: content: application/json: schema: - type: string + type: array + items: + $ref: '#/components/schemas/QueryDto' '403': description: Not allowed to find subsets content: @@ -901,6 +1021,9 @@ paths: tags: - subset-endpoint summary: Create subset + description: >- + Creates a subset in the query store of the data database. Requires role + `execute-query` operationId: create parameters: - name: databaseId @@ -985,7 +1108,10 @@ paths: get: tags: - table-endpoint - summary: Generate table statistic + summary: Get table statistic + description: >- + Gets basic statistical properties (min, max, mean, median, std.dev) of + numerical columns of a table with id. operationId: statistic parameters: - name: databaseId @@ -1029,10 +1155,11 @@ paths: get: tags: - table-endpoint - summary: Find table history + summary: Get history description: >- - Lists the insert/delete operations performed. Authentication is only - required for tables in private databases + Gets the insert/delete operations history performed. For tables in + private databases, the user needs to have at least *READ* access to the + associated database. operationId: getHistory parameters: - name: databaseId @@ -1059,9 +1186,11 @@ paths: content: application/json: schema: - type: string + type: array + items: + $ref: '#/components/schemas/TableHistoryDto' '400': - description: Invalid pagination request + description: 'Invalid pagination size request, must be > 0' content: application/json: schema: @@ -1091,7 +1220,11 @@ paths: get: tags: - table-endpoint - summary: Export table data + summary: Get table data + description: >- + Gets data from table with id as downloadable file. For tables in private + databases, the user needs to have at least *READ* access to the + associated database. operationId: exportData parameters: - name: databaseId @@ -1152,6 +1285,10 @@ paths: tags: - subset-endpoint summary: Find subset + description: >- + Finds a subset in the data database. Requests with HTTP header + `Accept=application/json` return the metadata, requests with HTTP header + `Accept=text/csv` return the data as downloadable file. operationId: findById parameters: - name: databaseId @@ -1220,6 +1357,10 @@ paths: tags: - database-endpoint summary: List databases + description: >- + Lists all databases in the metadata database. Requests with HTTP method + **GET** return the list of databases, requests with HTTP method **HEAD** + only the number in the `X-Count` header. operationId: list1 parameters: - name: internal_name @@ -1230,6 +1371,15 @@ paths: responses: '200': description: List of databases + headers: + Access-Control-Expose-Headers: + description: Expose `X-Count` custom header + required: true + style: simple + X-Count: + description: Number of databases + required: true + style: simple content: application/json: schema: @@ -1240,6 +1390,9 @@ paths: tags: - database-endpoint summary: Create database + description: >- + Creates a database in the container with id. Requires roles + `create-database`. operationId: create_5 requestBody: content: @@ -1299,6 +1452,10 @@ paths: tags: - database-endpoint summary: List databases + description: >- + Lists all databases in the metadata database. Requests with HTTP method + **GET** return the list of databases, requests with HTTP method **HEAD** + only the number in the `X-Count` header. operationId: list_1 parameters: - name: internal_name @@ -1309,6 +1466,15 @@ paths: responses: '200': description: List of databases + headers: + Access-Control-Expose-Headers: + description: Expose `X-Count` custom header + required: true + style: simple + X-Count: + description: Number of databases + required: true + style: simple content: application/json: schema: @@ -1319,7 +1485,13 @@ paths: get: tags: - access-endpoint - summary: Check access to some database + summary: Find/Check access + description: >- + Finds or checks access of a user with given id to a database with given + id. Requests with HTTP method **GET** return the access object, requests + with HTTP method **HEAD** only the status. When the user has at least + *READ* access, the status 200 is returned, 403 otherwise. Requires role + `check-database-access` or `admin`. operationId: find parameters: - name: databaseId @@ -1359,7 +1531,10 @@ paths: put: tags: - access-endpoint - summary: Modify access to some database + summary: Modify access + description: >- + Modifies access of a user with given id to database with given id. + Requires role `update-database-access`. operationId: update_4 parameters: - name: databaseId @@ -1382,7 +1557,7 @@ paths: required: true responses: '202': - description: Modify access succeeded + description: Modified access '400': description: Modify access query or database connection is malformed content: @@ -1423,7 +1598,10 @@ paths: post: tags: - access-endpoint - summary: Give access to some database + summary: Give access + description: >- + Give a user with given id access to some database with given id. + Requires role `create-database-access`. operationId: create_8 parameters: - name: databaseId @@ -1447,6 +1625,10 @@ paths: responses: '202': description: Granting access succeeded + content: + application/json: + schema: + $ref: '#/components/schemas/DatabaseAccessDto' '400': description: Granting access query or database connection is malformed content: @@ -1465,12 +1647,6 @@ paths: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - '405': - description: Granting access not permitted - content: - application/json: - schema: - $ref: '#/components/schemas/ApiErrorDto' '502': description: Access could not be created due to connection error content: @@ -1489,7 +1665,10 @@ paths: delete: tags: - access-endpoint - summary: Revoke access to some database + summary: Delete access + description: >- + Delete access of a user with id to a database with id. Requires role + `delete-database-access`. operationId: revoke parameters: - name: databaseId @@ -1506,7 +1685,7 @@ paths: format: uuid responses: '202': - description: Revoked access successfully + description: Deleted access '400': description: Modify access query or database connection is malformed content: @@ -1543,7 +1722,13 @@ paths: head: tags: - access-endpoint - summary: Check access to some database + summary: Find/Check access + description: >- + Finds or checks access of a user with given id to a database with given + id. Requests with HTTP method **GET** return the access object, requests + with HTTP method **HEAD** only the status. When the user has at least + *READ* access, the status 200 is returned, 403 otherwise. Requires role + `check-database-access` or `admin`. operationId: find_1 parameters: - name: databaseId @@ -1584,7 +1769,8 @@ paths: get: tags: - user-endpoint - summary: Get a user info + summary: Get user + description: Gets user with id from the metadata database. Requires authentication. operationId: find_2 parameters: - name: userId @@ -1618,7 +1804,8 @@ paths: put: tags: - user-endpoint - summary: Modify user information + summary: Update user + description: Updates user with id. Requires role `modify-user-information`. operationId: modify parameters: - name: userId @@ -1665,7 +1852,8 @@ paths: put: tags: - user-endpoint - summary: Modify user password + summary: Update user password + description: Updates password of user with id. Requires authentication. operationId: password parameters: - name: userId @@ -1683,10 +1871,12 @@ paths: responses: '202': description: Modified user password + '400': + description: Invalid password payload content: application/json: schema: - $ref: '#/components/schemas/UserDto' + $ref: '#/components/schemas/ApiErrorDto' '403': description: Not allowed to change foreign user password content: @@ -1718,7 +1908,8 @@ paths: put: tags: - user-endpoint - summary: Refresh user token + summary: Refresh token + description: Refreshes user token by refresh token. operationId: refreshToken requestBody: content: @@ -1733,12 +1924,18 @@ paths: application/json: schema: $ref: '#/components/schemas/TokenDto' - '403': + '400': description: Invalid refresh token content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' + '403': + description: Not allowed + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorDto' '502': description: Connection to auth service failed content: @@ -1748,7 +1945,8 @@ paths: post: tags: - user-endpoint - summary: Obtain user token + summary: Create token + description: Creates a user token via the auth service. operationId: getToken requestBody: content: @@ -1763,6 +1961,12 @@ paths: application/json: schema: $ref: '#/components/schemas/TokenDto' + '400': + description: Invalid login request + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorDto' '403': description: Not allowed to get token content: @@ -1799,7 +2003,8 @@ paths: get: tags: - ontology-endpoint - summary: Find one ontology + summary: Find ontology + description: Finds an ontology with id in the metadata database. operationId: find_3 parameters: - name: ontologyId @@ -1824,7 +2029,8 @@ paths: put: tags: - ontology-endpoint - summary: Update an ontology + summary: Update ontology + description: Updates an ontology with id. Requires role `update-ontology`. operationId: update parameters: - name: ontologyId @@ -1858,7 +2064,8 @@ paths: delete: tags: - ontology-endpoint - summary: Delete an ontology + summary: Delete ontology + description: Deletes an ontology with given id. Requires role `delete-ontology`. operationId: delete parameters: - name: ontologyId @@ -1885,7 +2092,8 @@ paths: put: tags: - message-endpoint - summary: Update maintenance message + summary: Update message + description: Updates a message with id. Requires role `update-maintenance-message`. operationId: update_1 parameters: - name: messageId @@ -1919,7 +2127,8 @@ paths: delete: tags: - message-endpoint - summary: Delete maintenance message + summary: Delete message + description: Deletes a message with id. Requires role `delete-maintenance-message`. operationId: delete_1 parameters: - name: messageId @@ -1946,7 +2155,8 @@ paths: get: tags: - image-endpoint - summary: Find some image + summary: Find image + description: Finds a container image in the metadata database. operationId: findById1 parameters: - name: imageId @@ -1971,7 +2181,10 @@ paths: put: tags: - image-endpoint - summary: Update some image + summary: Update image + description: >- + Updates container image in the metadata database. Requires role + `modify-image`. operationId: update_2 parameters: - name: imageId @@ -2005,7 +2218,10 @@ paths: delete: tags: - image-endpoint - summary: Delete some image + summary: Delete image + description: >- + Deletes a container image in the metadata database. Requires role + `delete-image`. operationId: delete_2 parameters: - name: imageId @@ -2030,7 +2246,10 @@ paths: get: tags: - identifier-endpoint - summary: Find some identifier + summary: Find identifier + description: >- + Finds an identifier with id. The response format depends on the HTTP + `Accept` header set on the request. operationId: find_6 parameters: - name: identifierId @@ -2112,6 +2331,11 @@ paths: tags: - identifier-endpoint summary: Save identifier + description: >- + Saves an identifier with id as a draft identifier. Identifiers can only + be created for objects the user has at least *READ* access in the + associated database (requires role `create-identifier`) or for any + object in any database (requires role `create-foreign-identifier`). operationId: save parameters: - name: identifierId @@ -2151,12 +2375,6 @@ paths: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - '405': - description: Creating identifier not permitted - content: - application/json: - schema: - $ref: '#/components/schemas/ApiErrorDto' '502': description: Connection to search service failed content: @@ -2175,7 +2393,8 @@ paths: delete: tags: - identifier-endpoint - summary: Delete some identifier + summary: Delete identifier + description: Deletes an identifier with id. Requires role `delete-identifier`. operationId: delete_3 parameters: - name: identifierId @@ -2187,10 +2406,6 @@ paths: responses: '202': description: Deleted identifier - content: - '*/*': - schema: - type: object '403': description: Deleting identifier not permitted content: @@ -2223,6 +2438,9 @@ paths: tags: - identifier-endpoint summary: Publish identifier + description: >- + Publishes an identifier with id. A published identifier cannot be + changed anymore. Requires role `publish-identifier`. operationId: publish parameters: - name: identifierId @@ -2256,12 +2474,6 @@ paths: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - '405': - description: Creating identifier not permitted - content: - application/json: - schema: - $ref: '#/components/schemas/ApiErrorDto' '502': description: Connection to search service failed content: @@ -2282,6 +2494,9 @@ paths: tags: - database-endpoint summary: Update database visibility + description: >- + Updates the database with id on the visibility. Only the database owner + can perform this operation. Requires role `modify-database-visibility`. operationId: visibility parameters: - name: databaseId @@ -2303,6 +2518,12 @@ paths: application/json: schema: $ref: '#/components/schemas/DatabaseDto' + '400': + description: The visibility payload is malformed + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorDto' '403': description: Visibility modification is not permitted content: @@ -2334,7 +2555,8 @@ paths: get: tags: - table-endpoint - summary: Get information about table + summary: Find table + description: Finds a table with id. operationId: findById_2 parameters: - name: databaseId @@ -2369,13 +2591,13 @@ paths: schema: $ref: '#/components/schemas/ApiErrorDto' '502': - description: Connection to search service failed + description: Failed to establish connection with broker service content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' '503': - description: Failed to save in search service + description: Failed to obtain queue information from broker service content: application/json: schema: @@ -2386,7 +2608,11 @@ paths: put: tags: - table-endpoint - summary: Update table statistics + summary: Update statistics + description: >- + Updates basic statistical properties (min, max, mean, median, std.dev) + for numerical columns in a table with id. Requires role + `update-table-statistic` operationId: updateStatistic parameters: - name: databaseId @@ -2434,7 +2660,11 @@ paths: delete: tags: - table-endpoint - summary: Delete a table + summary: Delete table + description: >- + Deletes a table with id. Only the owner of a table can perform this + action (requires role `delete-table`) or anyone can delete a table + (requires role `delete-foreign-table`). operationId: delete_5 parameters: - name: databaseId @@ -2489,7 +2719,12 @@ paths: put: tags: - table-endpoint - summary: Update a table column semantic mapping + summary: Update semantics + description: >- + Updates column semantics of a table column with id. Only the table owner + with at least *READ* access to the associated database can update the + column semantics (requires role `modify-table-column-semantics`) or + foreign table columns if role `modify-foreign-table-column-semantics`. operationId: update_3 parameters: - name: databaseId @@ -2563,6 +2798,9 @@ paths: tags: - database-endpoint summary: Update database owner + description: >- + Updates the database with id on the owner. Only the database owner can + perform this operation. Requires role `modify-database-owner`. operationId: transfer parameters: - name: databaseId @@ -2584,6 +2822,12 @@ paths: application/json: schema: $ref: '#/components/schemas/DatabaseDto' + '400': + description: Owner payload is malformed + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorDto' '403': description: Transfer of ownership is not permitted content: @@ -2615,7 +2859,11 @@ paths: put: tags: - database-endpoint - summary: Refresh database views metadata + summary: Update database view schemas + description: >- + Updates the database with id with generated metadata from view that are + not yet known to the database. Only the database owner can perform this + operation. Requires role `find-database`. operationId: refreshViewMetadata parameters: - name: databaseId @@ -2662,7 +2910,11 @@ paths: put: tags: - database-endpoint - summary: Refresh database tables metadata + summary: Update database table schemas + description: >- + Updates the database with id with generated metadata from tables that + are not yet known to the database. Only the database owner can perform + this operation. Requires role `find-database`. operationId: refreshTableMetadata parameters: - name: databaseId @@ -2715,7 +2967,10 @@ paths: put: tags: - database-endpoint - summary: Update database image + summary: Update database preview image + description: >- + Updates the database with id on the preview image. Only the database + owner can perform this operation. Requires role `modify-database-image`. operationId: modifyImage parameters: - name: databaseId @@ -2774,7 +3029,8 @@ paths: get: tags: - user-endpoint - summary: Find all users + summary: List users + description: Lists users known to the metadata database. operationId: findAll responses: '200': @@ -2789,6 +3045,9 @@ paths: tags: - user-endpoint summary: Create user + description: >- + Creates a user in the auth service and metadata database. Requires that + no credentials are sent in the request. operationId: create1 requestBody: content: @@ -2802,7 +3061,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/UserBriefDto' + $ref: '#/components/schemas/UserDto' '400': description: Parameters are not well-formed (likely email) content: @@ -2841,21 +3100,25 @@ paths: get: tags: - ontology-endpoint - summary: List all ontologies + summary: List ontologies + description: Lists all ontologies known to the metadata database. operationId: findAll_2 responses: '200': - description: List all ontologies + description: List ontologies content: application/json: schema: type: array items: - $ref: '#/components/schemas/OntologyDto' + $ref: '#/components/schemas/OntologyBriefDto' post: tags: - ontology-endpoint - summary: Register a new ontology + summary: Create ontology + description: >- + Creates an ontology in the metadata database. Requires role + `create-ontology`. operationId: create_1 requestBody: content: @@ -2877,14 +3140,20 @@ paths: get: tags: - message-endpoint - summary: Find maintenance messages + summary: List messages + description: >- + Lists messages known to the metadata database. Messages can be filtered + be filtered with the optional `active` parameter. If set to *true*, only + active messages (that is, messages whose end time has not been reached) + will be returned. Otherwise only inactive messages are returned. If not + set, active and inactive messages are returned. operationId: list_2 parameters: - - name: filter + - name: active in: query required: false schema: - type: string + type: boolean responses: '200': description: List messages @@ -2897,7 +3166,10 @@ paths: post: tags: - message-endpoint - summary: Create maintenance message + summary: Create message + description: >- + Creates a message in the metadata database. Requires role + `create-maintenance-message`. operationId: create_2 requestBody: content: @@ -2919,7 +3191,8 @@ paths: get: tags: - image-endpoint - summary: Find all images + summary: List images + description: Lists all container images known to the metadata database. operationId: findAll_3 responses: '200': @@ -2929,11 +3202,14 @@ paths: schema: type: array items: - $ref: '#/components/schemas/ContainerImage' + $ref: '#/components/schemas/ImageBriefDto' post: tags: - image-endpoint summary: Create image + description: >- + Creates a container image in the metadata database. Requires role + `create-image`. operationId: create_3 requestBody: content: @@ -2967,7 +3243,8 @@ paths: get: tags: - identifier-endpoint - summary: Find all identifiers + summary: List identifiers + description: Lists all identifiers known to the metadata database operationId: findAll_4 parameters: - name: dbid @@ -3005,10 +3282,14 @@ paths: content: application/json: schema: - type: string + type: array + items: + $ref: '#/components/schemas/ConceptDto' application/ld+json: schema: - type: string + type: array + items: + $ref: '#/components/schemas/LdDatasetDto' '406': description: 'Identifier could not be exported, the requested style is not known' content: @@ -3018,7 +3299,12 @@ paths: post: tags: - identifier-endpoint - summary: Draft identifier + summary: Create identifier + description: >- + Create an identifier with id to create a draft identifier. Identifiers + can only be created for objects the user has at least *READ* access in + the associated database (requires role `create-identifier`) or for any + object in any database (requires role `create-foreign-identifier`). operationId: create_4 requestBody: content: @@ -3051,12 +3337,6 @@ paths: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - '405': - description: Creating identifier not permitted - content: - application/json: - schema: - $ref: '#/components/schemas/ApiErrorDto' '502': description: Connection to search service failed content: @@ -3076,7 +3356,8 @@ paths: get: tags: - view-endpoint - summary: Find all views + summary: List views + description: Lists views known to the metadata database. operationId: findAll_5 parameters: - name: databaseId @@ -3106,7 +3387,8 @@ paths: post: tags: - view-endpoint - summary: Create a view + summary: Create view + description: Creates a view. Requires role `create-database-view`. operationId: create_6 parameters: - name: databaseId @@ -3134,12 +3416,6 @@ paths: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - '401': - description: Credentials missing - content: - application/json: - schema: - $ref: '#/components/schemas/ApiErrorDto' '403': description: Credentials missing content: @@ -3152,12 +3428,6 @@ paths: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - '405': - description: Create view is not permitted - content: - application/json: - schema: - $ref: '#/components/schemas/ApiErrorDto' '423': description: Create view resulted in an invalid query statement content: @@ -3183,7 +3453,8 @@ paths: get: tags: - table-endpoint - summary: List all tables + summary: List tables + description: Lists all tables known to the metadata database. operationId: list_4 parameters: - name: databaseId @@ -3219,7 +3490,8 @@ paths: post: tags: - table-endpoint - summary: Create a table + summary: Create table + description: Creates a table in the database with id. Requires role `create-table`. operationId: create_7 parameters: - name: databaseId @@ -3284,7 +3556,8 @@ paths: get: tags: - container-endpoint - summary: Find all containers + summary: List containers + description: List all containers in the metadata database. operationId: findAll_6 parameters: - name: limit @@ -3301,11 +3574,14 @@ paths: schema: type: array items: - type: string + $ref: '#/components/schemas/ContainerBriefDto' post: tags: - container-endpoint summary: Create container + description: >- + Creates a container in the metadata database. Requires role + `create-container`. operationId: create_9 requestBody: content: @@ -3319,7 +3595,19 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/ContainerBriefDto' + $ref: '#/components/schemas/ContainerDto' + '400': + description: Container payload malformed + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorDto' + '403': + description: 'Create container not permitted, need authority `create-container`' + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorDto' '404': description: Container image or user could not be found content: @@ -3339,7 +3627,8 @@ paths: get: tags: - unit-endpoint - summary: List semantic units + summary: List units + description: Lists units known to the metadata database. operationId: findAll_1 responses: '200': @@ -3355,6 +3644,9 @@ paths: tags: - ontology-endpoint summary: Find entities + description: >- + Finds semantic entities by label or uri in an ontology with id. Requires + role `execute-semantic-query`. operationId: find_4 parameters: - name: ontologyId @@ -3413,7 +3705,7 @@ paths: get: tags: - metadata-endpoint - summary: Get the record + summary: Get record operationId: identify_1_1_1_1 parameters: - name: verb @@ -3432,7 +3724,8 @@ paths: get: tags: - message-endpoint - summary: Find one maintenance message + summary: Find message + description: Finds a message with id in the metadata database. operationId: find_5 parameters: - name: messageId @@ -3458,7 +3751,8 @@ paths: get: tags: - license-endpoint - summary: Get all licenses + summary: List licenses + description: Lists licenses known to the metadata database. operationId: list_3 responses: '200': @@ -3468,12 +3762,15 @@ paths: schema: type: array items: - type: string + $ref: '#/components/schemas/LicenseDto' /api/identifier/retrieve: get: tags: - identifier-endpoint - summary: Retrieve metadata from identifier + summary: Retrieve PID metadata + description: >- + Retrieves Persistent Identifier (PID) metadata from external endpoints. + Supported PIDs are: ORCID, ROR, DOI. operationId: retrieve parameters: - name: url @@ -3498,7 +3795,8 @@ paths: get: tags: - database-endpoint - summary: Find some database + summary: Find database + description: Finds a database with id. operationId: findById_1 parameters: - name: databaseId @@ -3539,7 +3837,8 @@ paths: get: tags: - view-endpoint - summary: Find one view + summary: Get view + description: Gets a view with id in the metadata database. operationId: find_7 parameters: - name: databaseId @@ -3579,7 +3878,8 @@ paths: delete: tags: - view-endpoint - summary: Delete one view + summary: Delete view + description: Deletes a view with id. Requires role `delete-database-view`. operationId: delete_4 parameters: - name: databaseId @@ -3597,6 +3897,10 @@ paths: responses: '202': description: Delete view successfully + content: + '*/*': + schema: + $ref: '#/components/schemas/View' '400': description: Delete view query is malformed content: @@ -3615,12 +3919,6 @@ paths: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - '405': - description: Delete view is not permitted - content: - application/json: - schema: - $ref: '#/components/schemas/ApiErrorDto' '423': description: Delete view resulted in an invalid query statement content: @@ -3646,7 +3944,10 @@ paths: get: tags: - table-endpoint - summary: Suggest table semantics + summary: Suggest semantics + description: >- + Suggests semantic concepts for a table. Requires role + `table-semantic-analyse`. operationId: analyseTable parameters: - name: databaseId @@ -3669,7 +3970,7 @@ paths: schema: type: array items: - $ref: '#/components/schemas/TableColumnEntityDto' + $ref: '#/components/schemas/EntityDto' '400': description: Failed to parse statistic in search service content: @@ -3701,7 +4002,8 @@ paths: get: tags: - table-endpoint - summary: Suggest table column semantics + summary: Suggest semantics + description: Suggests column semantics. Requires role `table-semantic-analyse`. operationId: analyseTableColumn parameters: - name: databaseId @@ -3756,7 +4058,8 @@ paths: get: tags: - container-endpoint - summary: Find some container + summary: Find container + description: Finds a container in the metadata database. operationId: findById_3 parameters: - name: containerId @@ -3781,7 +4084,10 @@ paths: delete: tags: - container-endpoint - summary: Delete some container + summary: Delete container + description: >- + Deletes a container in the metadata database. Requires role + `delete-container`. operationId: delete_6 parameters: - name: containerId @@ -3792,11 +4098,13 @@ paths: format: int64 responses: '202': - description: Deleted container successfully + description: Deleted container + '403': + description: 'Create container not permitted, need authority `delete-container`' content: - '*/*': + application/json: schema: - type: object + $ref: '#/components/schemas/ApiErrorDto' '404': description: Container not found content: @@ -3810,11 +4118,12 @@ paths: get: tags: - concept-endpoint - summary: List semantic concepts + summary: List concepts + description: List all semantic concepts known to the metadata database operationId: findAll_7 responses: '200': - description: Find all semantic concepts + description: List concepts content: application/json: schema: @@ -4092,29 +4401,6 @@ components: column_name: type: string type: object - QueryResultDto: - required: - - headers - - id - - result - type: object - properties: - result: - type: array - items: - type: object - additionalProperties: - type: object - headers: - type: array - items: - type: object - additionalProperties: - type: integer - format: int32 - id: - type: integer - format: int64 ApiErrorDto: required: - code @@ -4201,6 +4487,29 @@ components: code: type: string example: error.service.code + QueryResultDto: + required: + - headers + - id + - result + type: object + properties: + result: + type: array + items: + type: object + additionalProperties: + type: object + headers: + type: array + items: + type: object + additionalProperties: + type: integer + format: int32 + id: + type: integer + format: int64 TupleUpdateDto: required: - data @@ -5323,6 +5632,23 @@ components: rows: type: integer format: int64 + TableHistoryDto: + required: + - event + - timestamp + - total + type: object + properties: + timestamp: + type: string + format: date-time + example: '2021-03-12T15:26:21.000Z' + event: + type: string + total: + type: integer + format: int64 + example: 1 TupleDeleteDto: required: - keys @@ -7950,58 +8276,33 @@ components: privileged_password: type: string description: Password of privileged user - ContainerBriefDto: + OntologyBriefDto: required: - - created - - hash - id - - image - - internal_name - - name - - running + - prefix + - rdf + - sparql + - uri type: object properties: id: type: integer format: int64 - hash: + uri: type: string - example: f829dd8a884182d0da846f365dee1221fd16610a14c81b8f9f295ff162749e50 - name: + example: 'http://www.wikidata.org/' + prefix: type: string - example: Air Quality - image: - $ref: '#/components/schemas/ImageBriefDto' - running: + example: wd + sparql: type: boolean example: true - created: - type: string - format: date-time - example: '2021-03-12T15:26:21.000Z' - internal_name: - type: string - example: air-quality - ImageBriefDto: - required: - - id - - jdbc_method - - name - - version - type: object - properties: - id: - type: integer - format: int64 - name: - type: string - example: mariadb - version: - type: string - example: '10.5' - jdbc_method: + rdf: + type: boolean + example: false + uri_pattern: type: string - example: mariadb + example: 'http://www.wikidata.org/entity/.*' EntityDto: required: - label @@ -8030,14 +8331,14 @@ components: type: string resumptionToken: type: string + parametersString: + type: string fromDate: type: string format: date-time untilDate: type: string format: date-time - parametersString: - type: string BannerMessageDto: required: - id @@ -8071,6 +8372,149 @@ components: type: string format: date-time example: '2021-03-12T15:26:21.000Z' + ImageBriefDto: + required: + - id + - jdbc_method + - name + - version + type: object + properties: + id: + type: integer + format: int64 + name: + type: string + example: mariadb + version: + type: string + example: '10.5' + jdbc_method: + type: string + example: mariadb + LdCreatorDto: + required: + - '@type' + - name + type: object + properties: + name: + type: string + sameAs: + type: string + givenName: + type: string + familyName: + type: string + '@type': + type: string + LdDatasetDto: + required: + - '@context' + - '@type' + - citation + - creator + - description + - hasPart + - identifier + - name + - temporalCoverage + - url + - version + type: object + properties: + name: + type: string + description: + type: string + url: + type: string + identifier: + type: array + items: + type: string + license: + type: string + creator: + type: array + items: + $ref: '#/components/schemas/LdCreatorDto' + citation: + type: string + hasPart: + type: array + items: + $ref: '#/components/schemas/LdDatasetDto' + temporalCoverage: + type: string + version: + type: string + format: date-time + '@context': + type: string + '@type': + type: string + TableColumnEntityDto: + required: + - column_id + - database_id + - table_id + - uri + type: object + properties: + uri: + type: string + example: 'https://www.wikidata.org/entity/Q1686799' + label: + type: string + example: Apache Jena + description: + type: string + example: open source semantic web framework for Java + database_id: + type: integer + format: int64 + example: 1 + table_id: + type: integer + format: int64 + example: 1 + column_id: + type: integer + format: int64 + example: 1 + ContainerBriefDto: + required: + - created + - hash + - id + - image + - internal_name + - name + - running + type: object + properties: + id: + type: integer + format: int64 + hash: + type: string + example: f829dd8a884182d0da846f365dee1221fd16610a14c81b8f9f295ff162749e50 + name: + type: string + example: Air Quality + image: + $ref: '#/components/schemas/ImageBriefDto' + running: + type: boolean + example: true + created: + type: string + format: date-time + example: '2021-03-12T15:26:21.000Z' + internal_name: + type: string + example: air-quality Constraints: type: object properties: @@ -9510,97 +9954,6 @@ components: d: type: integer format: int64 - LdCreatorDto: - required: - - '@type' - - name - type: object - properties: - name: - type: string - sameAs: - type: string - givenName: - type: string - familyName: - type: string - '@type': - type: string - LdDatasetDto: - required: - - '@context' - - '@type' - - citation - - creator - - description - - hasPart - - identifier - - name - - temporalCoverage - - url - - version - type: object - properties: - name: - type: string - description: - type: string - url: - type: string - identifier: - type: array - items: - type: string - license: - type: string - creator: - type: array - items: - $ref: '#/components/schemas/LdCreatorDto' - citation: - type: string - hasPart: - type: array - items: - $ref: '#/components/schemas/LdDatasetDto' - temporalCoverage: - type: string - version: - type: string - format: date-time - '@context': - type: string - '@type': - type: string - TableColumnEntityDto: - required: - - column_id - - database_id - - table_id - - uri - type: object - properties: - uri: - type: string - example: 'https://www.wikidata.org/entity/Q1686799' - label: - type: string - example: Apache Jena - description: - type: string - example: open source semantic web framework for Java - database_id: - type: integer - format: int64 - example: 1 - table_id: - type: integer - format: int64 - example: 1 - column_id: - type: integer - format: int64 - example: 1 IndexDto: properties: results: diff --git a/dbrepo-analyse-service/app.py b/dbrepo-analyse-service/app.py index 0e8a10bf1d..61c866982b 100644 --- a/dbrepo-analyse-service/app.py +++ b/dbrepo-analyse-service/app.py @@ -1,17 +1,15 @@ -import json +import os import logging from typing import Any, List -import os from json import dumps import requests.exceptions from dbrepo.api.dto import ApiError -from flasgger import LazyJSONEncoder, Swagger -from flask_httpauth import HTTPBasicAuth, MultiAuth, HTTPTokenAuth -from flasgger.utils import swag_from +from flasgger import LazyJSONEncoder, Swagger, swag_from from flask import Flask, Response, request from flask_cors import CORS +from flask_httpauth import HTTPBasicAuth, MultiAuth, HTTPTokenAuth from prometheus_flask_exporter import PrometheusMetrics from botocore.exceptions import ClientError @@ -52,14 +50,14 @@ app = Flask(__name__) cors = CORS(app, resources={r"/api/*": {"origins": "*"}}) +metrics = PrometheusMetrics(app) +metrics.info("app_info", "Application info", version="0.0.1") +app.config["SWAGGER"] = {"openapi": "3.0.1", "title": "Swagger UI", "uiversion": 3} + token_auth = HTTPTokenAuth(scheme='Bearer') basic_auth = HTTPBasicAuth() auth = MultiAuth(token_auth, basic_auth) -metrics = PrometheusMetrics(app) -metrics.info("app_info", "Application info", version="1.4.4") -app.config["SWAGGER"] = {"openapi": "3.0.1", "title": "Swagger UI", "uiversion": 3} - swagger_config = { "headers": [], "specs": [ @@ -165,7 +163,7 @@ template = { }, "externalDocs": { "description": "Sourcecode Documentation", - "url": "https://www.ifs.tuwien.ac.at/infrastructures/dbrepo/__APPVERSION__/" + "url": "https://www.ifs.tuwien.ac.at/infrastructures/dbrepo/1.4.4/" }, "servers": [ { @@ -185,7 +183,7 @@ app.config["JWT_ALGORITHM"] = "HS256" app.config["JWT_PUBKEY"] = '-----BEGIN PUBLIC KEY-----\n' + os.getenv("JWT_PUBKEY", "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqqnHQ2BWWW9vDNLRCcxD++xZg/16oqMo/c1l+lcFEjjAIJjJp/HqrPYU/U9GvquGE6PbVFtTzW1KcKawOW+FJNOA3CGo8Q1TFEfz43B8rZpKsFbJKvQGVv1Z4HaKPvLUm7iMm8Hv91cLduuoWx6Q3DPe2vg13GKKEZe7UFghF+0T9u8EKzA/XqQ0OiICmsmYPbwvf9N3bCKsB/Y10EYmZRb8IhCoV9mmO5TxgWgiuNeCTtNCv2ePYqL/U0WvyGFW0reasIK8eg3KrAUj8DpyOgPOVBn3lBGf+3KFSYi+0bwZbJZWqbC/Xlk20Go1YfeJPRIt7ImxD27R/lNjgDO/MwIDAQAB") + '\n-----END PUBLIC KEY-----' app.config["AUTH_SERVICE_ENDPOINT"] = os.getenv("AUTH_SERVICE_ENDPOINT", "http://localhost/api/auth") -app.config["AUTH_SERVICE_CLIENT"] = os.getenv("AUTH_SERVICE_CLIENT", "dbrepo") +app.config["AUTH_SERVICE_CLIENT"] = os.getenv("AUTH_SERVICE_CLIENT", "dbrepo-client") app.config["AUTH_SERVICE_CLIENT_SECRET"] = os.getenv("AUTH_SERVICE_CLIENT_SECRET", "MUwRc7yfXSJwX8AdRMWaQC3Nep1VjwgG") app.config["ADMIN_USERNAME"] = os.getenv('ADMIN_USERNAME', 'admin') app.config["ADMIN_PASSWORD"] = os.getenv('ADMIN_PASSWORD', 'admin') @@ -253,7 +251,7 @@ def analyse_datatypes(): if filename is None or separator is None: return Response( - json.dumps({'success': False, 'message': "Missing required query parameters 'filename' and 'separator'"}), + dumps({'success': False, 'message': "Missing required query parameters 'filename' and 'separator'"}), mimetype="application/json"), 400 try: diff --git a/dbrepo-analyse-service/as-yml/analyse_datatypes.yml b/dbrepo-analyse-service/as-yml/analyse_datatypes.yml index ae52198766..14529bb34b 100644 --- a/dbrepo-analyse-service/as-yml/analyse_datatypes.yml +++ b/dbrepo-analyse-service/as-yml/analyse_datatypes.yml @@ -2,7 +2,7 @@ tags: - analyse-endpoint summary: "Determine datatypes" operationId: analyse_datatypes -description: "This is a simple API which returns the datatypes of a (path) csv file" +description: "Determines MySQL 8 datatypes of a given dataset. Requires role `table-semantic-analyse`." consumes: - "application/json" produces: diff --git a/dbrepo-analyse-service/as-yml/analyse_keys.yml b/dbrepo-analyse-service/as-yml/analyse_keys.yml index da4f0bbca0..fb760930f4 100644 --- a/dbrepo-analyse-service/as-yml/analyse_keys.yml +++ b/dbrepo-analyse-service/as-yml/analyse_keys.yml @@ -1,8 +1,8 @@ tags: - analyse-endpoint -summary: "Determine primary keys" +summary: "Determine keys" operationId: analyse_keys -description: "This is a simple API which returns the primary keys + ranking of a (path) csv file" +description: "Determines primary keys of a given dataset. Requires role `table-semantic-analyse`." consumes: - "application/json" produces: diff --git a/dbrepo-analyse-service/as-yml/analyse_table_stat.yml b/dbrepo-analyse-service/as-yml/analyse_table_stat.yml deleted file mode 100644 index 8639d4dd92..0000000000 --- a/dbrepo-analyse-service/as-yml/analyse_table_stat.yml +++ /dev/null @@ -1,41 +0,0 @@ -tags: - - analyse-endpoint -summary: Determine table statistics -operationId: analyse_table_stat -parameters: - - name: database_id - in: path - required: true - example: 1 - schema: - type: integer - format: int64 - - name: table_id - in: path - required: true - example: 1 - schema: - type: integer - format: int64 -security: - - bearerAuth: [ ] - - basicAuth: [ ] -responses: - 202: - description: Determined statistics - content: - application/json: - schema: - $ref: '#/components/schemas/TableStats' - 400: - description: "Missing parameters" - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorDto' - 404: - description: "Table not found" - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorDto' diff --git a/dbrepo-data-service/rest-service/src/main/java/at/tuwien/endpoints/AccessEndpoint.java b/dbrepo-data-service/rest-service/src/main/java/at/tuwien/endpoints/AccessEndpoint.java index 4b58c5de33..95332db436 100644 --- a/dbrepo-data-service/rest-service/src/main/java/at/tuwien/endpoints/AccessEndpoint.java +++ b/dbrepo-data-service/rest-service/src/main/java/at/tuwien/endpoints/AccessEndpoint.java @@ -43,7 +43,8 @@ public class AccessEndpoint { @PostMapping("/{userId}") @PreAuthorize("hasAuthority('admin')") - @Operation(summary = "Give access to some database", security = {@SecurityRequirement(name = "basicAuth")}, + @Operation(summary = "Give access", + security = {@SecurityRequirement(name = "basicAuth")}, hidden = true) @ApiResponses(value = { @ApiResponse(responseCode = "202", @@ -74,11 +75,11 @@ public class AccessEndpoint { mediaType = "application/json", schema = @Schema(implementation = ApiErrorDto.class))}), }) - public ResponseEntity<?> create(@NotBlank @PathVariable("databaseId") Long databaseId, - @NotBlank @PathVariable("userId") UUID userId, - @Valid @RequestBody UpdateDatabaseAccessDto data) - throws NotAllowedException, QueryMalformedException, DatabaseNotFoundException, RemoteUnavailableException, - UserNotFoundException, DatabaseMalformedException, ServiceException { + public ResponseEntity<Void> create(@NotBlank @PathVariable("databaseId") Long databaseId, + @NotBlank @PathVariable("userId") UUID userId, + @Valid @RequestBody UpdateDatabaseAccessDto data) + throws NotAllowedException, DatabaseUnavailableException, DatabaseNotFoundException, + RemoteUnavailableException, UserNotFoundException, DatabaseMalformedException, ServiceException { log.debug("endpoint give access to database, databaseId={}, userId={}", databaseId, userId); final PrivilegedDatabaseDto database = metadataServiceGateway.getDatabaseById(databaseId); final PrivilegedUserDto user = metadataServiceGateway.getPrivilegedUserById(userId); @@ -91,18 +92,19 @@ public class AccessEndpoint { return ResponseEntity.accepted() .build(); } catch (SQLException e) { - throw new QueryMalformedException(e); + log.error("Failed to establish connection to database: {}", e.getMessage()); + throw new DatabaseUnavailableException("Failed to establish connection to database", e); } } @PutMapping("/{userId}") @PreAuthorize("hasAuthority('admin')") - @Operation(summary = "Update access to some database", security = {@SecurityRequirement(name = "basicAuth")}, + @Operation(summary = "Update access", + security = {@SecurityRequirement(name = "basicAuth")}, hidden = true) @ApiResponses(value = { @ApiResponse(responseCode = "202", - description = "Update access succeeded", - content = {@Content}), + description = "Update access succeeded"), @ApiResponse(responseCode = "400", description = "Update access query or database connection is malformed", content = {@Content( @@ -129,10 +131,10 @@ public class AccessEndpoint { mediaType = "application/json", schema = @Schema(implementation = ApiErrorDto.class))}), }) - public ResponseEntity<?> update(@NotBlank @PathVariable("databaseId") Long databaseId, - @NotBlank @PathVariable("userId") UUID userId, - @Valid @RequestBody UpdateDatabaseAccessDto access) throws NotAllowedException, - QueryMalformedException, DatabaseNotFoundException, RemoteUnavailableException, UserNotFoundException, + public ResponseEntity<Void> update(@NotBlank @PathVariable("databaseId") Long databaseId, + @NotBlank @PathVariable("userId") UUID userId, + @Valid @RequestBody UpdateDatabaseAccessDto access) throws NotAllowedException, + DatabaseUnavailableException, DatabaseNotFoundException, RemoteUnavailableException, UserNotFoundException, DatabaseMalformedException, ServiceException { log.debug("endpoint modify access to database, databaseId={}, userId={}, access.type={}", databaseId, userId, access.getType()); @@ -147,13 +149,15 @@ public class AccessEndpoint { return ResponseEntity.accepted() .build(); } catch (SQLException e) { - throw new QueryMalformedException(e); + log.error("Failed to establish connection to database: {}", e.getMessage()); + throw new DatabaseUnavailableException("Failed to establish connection to database", e); } } @DeleteMapping("/{userId}") @PreAuthorize("hasAuthority('admin')") - @Operation(summary = "Revoke access to some database", security = {@SecurityRequirement(name = "basicAuth")}, + @Operation(summary = "Revoke access", + security = {@SecurityRequirement(name = "basicAuth")}, hidden = true) @ApiResponses(value = { @ApiResponse(responseCode = "202", @@ -184,9 +188,9 @@ public class AccessEndpoint { mediaType = "application/json", schema = @Schema(implementation = ApiErrorDto.class))}), }) - public ResponseEntity<?> revoke(@NotBlank @PathVariable("databaseId") Long databaseId, - @NotBlank @PathVariable("userId") UUID userId) throws NotAllowedException, - QueryMalformedException, DatabaseNotFoundException, RemoteUnavailableException, UserNotFoundException, + public ResponseEntity<Void> revoke(@NotBlank @PathVariable("databaseId") Long databaseId, + @NotBlank @PathVariable("userId") UUID userId) throws NotAllowedException, + DatabaseUnavailableException, DatabaseNotFoundException, RemoteUnavailableException, UserNotFoundException, DatabaseMalformedException, ServiceException { log.debug("endpoint revoke access to database, databaseId={}, userId={}", databaseId, userId); final PrivilegedDatabaseDto database = metadataServiceGateway.getDatabaseById(databaseId); @@ -200,7 +204,8 @@ public class AccessEndpoint { return ResponseEntity.accepted() .build(); } catch (SQLException e) { - throw new QueryMalformedException(e); + log.error("Failed to establish connection to database: {}", e.getMessage()); + throw new DatabaseUnavailableException("Failed to establish connection to database", e); } } diff --git a/dbrepo-data-service/rest-service/src/main/java/at/tuwien/endpoints/DatabaseEndpoint.java b/dbrepo-data-service/rest-service/src/main/java/at/tuwien/endpoints/DatabaseEndpoint.java index bd32093068..5397ba1584 100644 --- a/dbrepo-data-service/rest-service/src/main/java/at/tuwien/endpoints/DatabaseEndpoint.java +++ b/dbrepo-data-service/rest-service/src/main/java/at/tuwien/endpoints/DatabaseEndpoint.java @@ -54,7 +54,8 @@ public class DatabaseEndpoint { @PostMapping @PreAuthorize("hasAuthority('admin')") - @Operation(summary = "Create database", security = {@SecurityRequirement(name = "basicAuth")}, + @Operation(summary = "Create database", + security = {@SecurityRequirement(name = "basicAuth")}, hidden = true) @ApiResponses(value = { @ApiResponse(responseCode = "201", @@ -108,7 +109,8 @@ public class DatabaseEndpoint { @PutMapping("/{databaseId}") @PreAuthorize("hasAuthority('admin')") - @Operation(summary = "Update user password in database", security = {@SecurityRequirement(name = "basicAuth")}, + @Operation(summary = "Update user password", + security = {@SecurityRequirement(name = "basicAuth")}, hidden = true) @ApiResponses(value = { @ApiResponse(responseCode = "202", diff --git a/dbrepo-data-service/rest-service/src/main/java/at/tuwien/endpoints/SubsetEndpoint.java b/dbrepo-data-service/rest-service/src/main/java/at/tuwien/endpoints/SubsetEndpoint.java index 0d4b53b92c..e75cd571eb 100644 --- a/dbrepo-data-service/rest-service/src/main/java/at/tuwien/endpoints/SubsetEndpoint.java +++ b/dbrepo-data-service/rest-service/src/main/java/at/tuwien/endpoints/SubsetEndpoint.java @@ -1,6 +1,7 @@ package at.tuwien.endpoints; import at.tuwien.ExportResourceDto; +import at.tuwien.api.database.DatabaseDto; import at.tuwien.api.database.internal.PrivilegedDatabaseDto; import at.tuwien.api.database.query.ExecuteStatementDto; import at.tuwien.api.database.query.QueryDto; @@ -14,6 +15,8 @@ import at.tuwien.utils.UserUtil; import at.tuwien.validation.EndpointValidator; import io.micrometer.observation.annotation.Observed; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.headers.Header; +import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; @@ -57,13 +60,15 @@ public class SubsetEndpoint { @GetMapping @Observed(name = "dbrepo_subset_list") - @Operation(summary = "Find subsets", security = {@SecurityRequirement(name = "basicAuth"), @SecurityRequirement(name = "bearerAuth")}) + @Operation(summary = "Find subsets", + description = "Finds subsets in the query store. The result can be optionally filtered by setting `persisted`. When set to *true*, only persisted queries are returned, otherwise only non-persisted queries are returned.", + security = {@SecurityRequirement(name = "basicAuth"), @SecurityRequirement(name = "bearerAuth")}) @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Found subsets", content = {@Content( mediaType = "application/json", - schema = @Schema(implementation = QueryDto[].class))}), + array = @ArraySchema(schema = @Schema(implementation = QueryDto.class)))}), @ApiResponse(responseCode = "403", description = "Not allowed to find subsets", content = {@Content( @@ -85,8 +90,7 @@ public class SubsetEndpoint { Principal principal) throws DatabaseUnavailableException, DatabaseNotFoundException, RemoteUnavailableException, QueryNotFoundException, NotAllowedException, ServiceException { - log.debug("endpoint find subsets in database, databaseId={}, filterPersisted={}, principal.name={}", databaseId, - filterPersisted, principal != null ? principal.getName() : null); + log.debug("endpoint find subsets in database, databaseId={}, filterPersisted={}", databaseId, filterPersisted); final PrivilegedDatabaseDto database = metadataServiceGateway.getDatabaseById(databaseId); if (!database.getIsPublic()) { if (principal == null) { @@ -102,13 +106,15 @@ public class SubsetEndpoint { log.error("Failed to establish connection to database: {}", e.getMessage()); throw new DatabaseUnavailableException("Failed to establish connection to database: " + e.getMessage(), e); } - log.info("Found {} subsets in data database", queries.size()); + log.info("Found {} subset(s)", queries.size()); return ResponseEntity.ok(queries); } @GetMapping("/{subsetId}") @Observed(name = "dbrepo_subset_find") - @Operation(summary = "Find subset", security = {@SecurityRequirement(name = "basicAuth"), @SecurityRequirement(name = "bearerAuth")}) + @Operation(summary = "Find subset", + description = "Finds a subset in the data database. Requests with HTTP header `Accept=application/json` return the metadata, requests with HTTP header `Accept=text/csv` return the data as downloadable file.", + security = {@SecurityRequirement(name = "basicAuth"), @SecurityRequirement(name = "bearerAuth")}) @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Found subset", @@ -171,12 +177,12 @@ public class SubsetEndpoint { } /* parameters */ if (timestamp == null) { - log.debug("timestamp not set: default to now"); timestamp = Instant.now(); + log.debug("timestamp not set: default to {}", timestamp); } if (accept == null) { - log.debug("accept header not set: default to application/json"); accept = MediaType.APPLICATION_JSON_VALUE; + log.debug("accept header not set: default to {}", accept); } switch (accept) { case MediaType.APPLICATION_JSON_VALUE: @@ -205,7 +211,9 @@ public class SubsetEndpoint { @PostMapping @Observed(name = "dbrepo_subset_create") @PreAuthorize("hasAuthority('execute-query')") - @Operation(summary = "Create subset", security = {@SecurityRequirement(name = "basicAuth"), @SecurityRequirement(name = "bearerAuth")}) + @Operation(summary = "Create subset", + description = "Creates a subset in the query store of the data database. Requires role `execute-query`", + security = {@SecurityRequirement(name = "basicAuth"), @SecurityRequirement(name = "bearerAuth")}) @ApiResponses(value = { @ApiResponse(responseCode = "201", description = "Created subset", @@ -253,24 +261,24 @@ public class SubsetEndpoint { QueryNotFoundException, StorageUnavailableException, QueryMalformedException, SidecarExportException, StorageNotFoundException, QueryStoreInsertException, TableMalformedException, PaginationException, QueryNotSupportedException, NotAllowedException, UserNotFoundException, ServiceException { - log.debug("endpoint create subset in database, databaseId={}, data.statement={}, principal.name={}, page={}, size={}, timestamp={}", - databaseId, data.getStatement(), principal.getName(), page, size, timestamp); + log.debug("endpoint create subset in database, databaseId={}, data.statement={}, principal.name={}, page={}, " + + "size={}, timestamp={}", databaseId, data.getStatement(), principal.getName(), page, size, timestamp); /* check */ endpointValidator.validateDataParams(page, size); endpointValidator.validateForbiddenStatements(data.getStatement()); metadataServiceGateway.getAccess(databaseId, UserUtil.getId(principal)); /* parameters */ if (page == null) { - log.debug("page not set: default to 0"); page = 0L; + log.debug("page not set: default to {}", page); } if (size == null) { - log.debug("size not set: default to 10"); size = 10L; + log.debug("size not set: default to {}", size); } if (timestamp == null) { - log.debug("timestamp not set: default to now"); timestamp = Instant.now(); + log.debug("timestamp not set: default to {}", timestamp); } /* create */ final PrivilegedDatabaseDto database = metadataServiceGateway.getDatabaseById(databaseId); @@ -282,22 +290,26 @@ public class SubsetEndpoint { log.error("Failed to establish connection to database: {}", e.getMessage()); throw new DatabaseUnavailableException("Failed to establish connection to database: " + e.getMessage(), e); } - log.info("Created subset with id {} in data database", queryResult.getId()); + log.info("Created subset with id: {}", queryResult.getId()); return ResponseEntity.status(HttpStatus.CREATED) .body(queryResult); } @RequestMapping(value = "/{subsetId}/data", method = {RequestMethod.GET, RequestMethod.HEAD}) @Observed(name = "dbrepo_subset_data") - @Operation(summary = "Retrieved subset data", security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) + @Operation(summary = "Get subset data", + description = "Gets data of subset with id. For private databases, the user needs at least *READ* access to the associated database. Requests with HTTP method **GET** return the subset dataset, requests with HTTP method **HEAD** only the number of rows in the subset dataset in the `X-Count` header", + security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Retrieved subset data", + headers = {@Header(name = "X-Count", description = "Number of rows", schema = @Schema(implementation = Long.class), required = true), + @Header(name = "Access-Control-Expose-Headers", description = "Expose `X-Count` custom header", schema = @Schema(implementation = String.class), required = true)}, content = {@Content( mediaType = "application/json", schema = @Schema(implementation = QueryResultDto.class))}), @ApiResponse(responseCode = "400", - description = "Malformed select query", + description = "Invalid pagination", content = {@Content( mediaType = "application/json", schema = @Schema(implementation = ApiErrorDto.class))}), @@ -339,12 +351,12 @@ public class SubsetEndpoint { } /* parameters */ if (page == null) { - log.debug("page not set: default to 0"); page = 0L; + log.debug("page not set: default to {}", page); } if (size == null) { - log.debug("size not set: default to 10"); size = 10L; + log.debug("size not set: default to {}", size); } try { final QueryDto query = subsetService.findById(database, subsetId); @@ -372,7 +384,9 @@ public class SubsetEndpoint { @PutMapping("/{queryId}") @PreAuthorize("hasAuthority('persist-query')") @Observed(name = "dbrepo_subset_persist") - @Operation(summary = "Persist subset", security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) + @Operation(summary = "Persist subset", + description = "Persists a subset with id. Requires role `persist-query`.", + security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) @ApiResponses(value = { @ApiResponse(responseCode = "202", description = "Persisted subset", diff --git a/dbrepo-data-service/rest-service/src/main/java/at/tuwien/endpoints/TableEndpoint.java b/dbrepo-data-service/rest-service/src/main/java/at/tuwien/endpoints/TableEndpoint.java index b03621c85e..a0200609a6 100644 --- a/dbrepo-data-service/rest-service/src/main/java/at/tuwien/endpoints/TableEndpoint.java +++ b/dbrepo-data-service/rest-service/src/main/java/at/tuwien/endpoints/TableEndpoint.java @@ -5,6 +5,7 @@ import at.tuwien.api.database.DatabaseAccessDto; import at.tuwien.api.database.DatabaseDto; import at.tuwien.api.database.internal.PrivilegedDatabaseDto; import at.tuwien.api.database.query.ImportCsvDto; +import at.tuwien.api.database.query.QueryDto; import at.tuwien.api.database.query.QueryResultDto; import at.tuwien.api.database.table.*; import at.tuwien.api.database.table.internal.PrivilegedTableDto; @@ -17,6 +18,8 @@ import at.tuwien.utils.UserUtil; import at.tuwien.validation.EndpointValidator; import io.micrometer.observation.annotation.Observed; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.headers.Header; +import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; @@ -59,7 +62,8 @@ public class TableEndpoint { @PostMapping @PreAuthorize("hasAuthority('admin')") - @Operation(summary = "Create table", security = {@SecurityRequirement(name = "basicAuth")}, + @Operation(summary = "Create table", + security = {@SecurityRequirement(name = "basicAuth")}, hidden = true) @ApiResponses(value = { @ApiResponse(responseCode = "201", @@ -105,7 +109,8 @@ public class TableEndpoint { @DeleteMapping("/{tableId}") @PreAuthorize("hasAuthority('admin')") - @Operation(summary = "Delete table", security = {@SecurityRequirement(name = "basicAuth")}, + @Operation(summary = "Delete table", + security = {@SecurityRequirement(name = "basicAuth")}, hidden = true) @ApiResponses(value = { @ApiResponse(responseCode = "202", @@ -147,10 +152,14 @@ public class TableEndpoint { @RequestMapping(value = "/{tableId}/data", method = {RequestMethod.GET, RequestMethod.HEAD}) @Observed(name = "dbrepo_table_data_list") - @Operation(summary = "Retrieve table data", security = {@SecurityRequirement(name = "basicAuth"), @SecurityRequirement(name = "bearerAuth")}) + @Operation(summary = "Get table data", + description = "Gets data from a table with id. For a table in a private database, the user needs to have at least *READ* access to the associated database. Requests with HTTP method **GET** return the full dataset, requests with HTTP method **HEAD** only the number of tuples in the `X-Count` header.", + security = {@SecurityRequirement(name = "basicAuth"), @SecurityRequirement(name = "bearerAuth")}) @ApiResponses(value = { @ApiResponse(responseCode = "200", - description = "Retrieved table data", + description = "Get table data", + headers = {@Header(name = "X-Count", description = "Number of rows", schema = @Schema(implementation = Long.class), required = true), + @Header(name = "Access-Control-Expose-Headers", description = "Expose `X-Count` custom header", schema = @Schema(implementation = String.class), required = true)}, content = {@Content( mediaType = "application/json", schema = @Schema(implementation = QueryResultDto.class))}), @@ -159,6 +168,11 @@ public class TableEndpoint { content = {@Content( mediaType = "application/json", schema = @Schema(implementation = ApiErrorDto.class))}), + @ApiResponse(responseCode = "403", + description = "Not allowed to get table data", + content = {@Content( + mediaType = "application/json", + schema = @Schema(implementation = ApiErrorDto.class))}), @ApiResponse(responseCode = "404", description = "Failed to find table in metadata database", content = {@Content( @@ -174,26 +188,36 @@ public class TableEndpoint { @NotBlank @PathVariable("tableId") Long tableId, @RequestParam(required = false) Instant timestamp, @RequestParam(required = false) Long page, - @RequestParam(required = false) Long size) + @RequestParam(required = false) Long size, + Principal principal) throws DatabaseUnavailableException, RemoteUnavailableException, TableNotFoundException, - TableMalformedException, PaginationException, QueryMalformedException, ServiceException { + TableMalformedException, PaginationException, QueryMalformedException, ServiceException, + NotAllowedException { log.debug("endpoint find table data, databaseId={}, tableId={}, timestamp={}, page={}, size={}", databaseId, tableId, timestamp, page, size); endpointValidator.validateDataParams(page, size); /* parameters */ if (page == null) { - log.debug("page not set: default to 0"); page = 0L; + log.debug("page not set: default to {}", page); } if (size == null) { - log.debug("size not set: default to 10"); size = 10L; + log.debug("size not set: default to {}", size); } if (timestamp == null) { - log.debug("timestamp not set: default to now"); timestamp = Instant.now(); + log.debug("timestamp not set: default to {}", timestamp); } final PrivilegedTableDto table = metadataServiceGateway.getTableById(databaseId, tableId); + if (!table.getIsPublic()) { + if (principal == null) { + log.error("Failed find table data: authentication required"); + throw new NotAllowedException("Failed to find table data: authentication required"); + } + final DatabaseAccessDto access = metadataServiceGateway.getAccess(databaseId, UserUtil.getId(principal)); + endpointValidator.validateOnlyWriteOwnOrWriteAllAccess(access.getType(), table.getOwner().getId(), UserUtil.getId(principal)); + } final HttpHeaders headers = new HttpHeaders(); headers.set("Access-Control-Expose-Headers", "X-Count"); try { @@ -211,8 +235,8 @@ public class TableEndpoint { @PostMapping("/{tableId}/data") @PreAuthorize("hasAuthority('insert-table-data')") @Observed(name = "dbrepo_table_data_create") - @Operation(summary = "Insert a raw data tuple", - description = "Inserts a raw data tuple into a table with at least WRITE_OWN access. Then update the table statistics.", + @Operation(summary = "Insert tuple", + description = "Inserts a data tuple into a table, then the table statistics are updated. The user needs to have at least *WRITE_OWN* access to the associated database. Requires role `insert-table-data`.", security = {@SecurityRequirement(name = "basicAuth"), @SecurityRequirement(name = "bearerAuth")}) @ApiResponses(value = { @ApiResponse(responseCode = "201", @@ -263,8 +287,8 @@ public class TableEndpoint { @PutMapping("/{tableId}/data") @PreAuthorize("hasAuthority('insert-table-data')") @Observed(name = "dbrepo_table_data_update") - @Operation(summary = "Update a raw data tuple", - description = "Updates a raw data tuple in a table with at least WRITE_OWN access. Then update the table statistics.", + @Operation(summary = "Update tuple", + description = "Updates a data tuple into a table, then the table statistics are updated. The user needs to have at least *WRITE_OWN* access to the associated database. Requires role `insert-table-data`.", security = {@SecurityRequirement(name = "basicAuth"), @SecurityRequirement(name = "bearerAuth")}) @ApiResponses(value = { @ApiResponse(responseCode = "202", @@ -315,8 +339,8 @@ public class TableEndpoint { @DeleteMapping("/{tableId}/data") @PreAuthorize("hasAuthority('delete-table-data')") @Observed(name = "dbrepo_table_data_delete") - @Operation(summary = "Delete table data", - description = "Deletes a raw data tuple in a table with at least WRITE_OWN access. Then update the table statistics.", + @Operation(summary = "Delete tuple", + description = "Deletes a data tuple into a table, then the table statistics are updated. The user needs to have at least *WRITE_OWN* access to the associated database. Requires role `delete-table-data`.", security = {@SecurityRequirement(name = "basicAuth"), @SecurityRequirement(name = "bearerAuth")}) @ApiResponses(value = { @ApiResponse(responseCode = "202", @@ -366,17 +390,17 @@ public class TableEndpoint { @GetMapping("/{tableId}/history") @Observed(name = "dbrepo_table_data_history") - @Operation(summary = "Find table history", - description = "Lists the insert/delete operations performed. Authentication is only required for tables in private databases", + @Operation(summary = "Get history", + description = "Gets the insert/delete operations history performed. For tables in private databases, the user needs to have at least *READ* access to the associated database.", security = {@SecurityRequirement(name = "basicAuth"), @SecurityRequirement(name = "bearerAuth")}) @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Found table history", content = {@Content( mediaType = "application/json", - schema = @Schema(implementation = TableHistoryDto[].class))}), + array = @ArraySchema(schema = @Schema(implementation = TableHistoryDto.class)))}), @ApiResponse(responseCode = "400", - description = "Invalid pagination request", + description = "Invalid pagination size request, must be > 0", content = {@Content( mediaType = "application/json", schema = @Schema(implementation = ApiErrorDto.class))}), @@ -431,13 +455,14 @@ public class TableEndpoint { @GetMapping @PreAuthorize("hasAuthority('admin')") @Observed(name = "dbrepo_table_schema_list") - @Operation(summary = "Find table schemas", hidden = true) + @Operation(summary = "Find tables", + hidden = true) @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Got table schemas", content = {@Content( mediaType = "application/json", - schema = @Schema(implementation = TableDto[].class))}), + array = @ArraySchema(schema = @Schema(implementation = TableDto.class)))}), @ApiResponse(responseCode = "400", description = "Schema data malformed", content = {@Content( @@ -479,7 +504,9 @@ public class TableEndpoint { @GetMapping("/{tableId}/export") @Observed(name = "dbrepo_table_data_export") - @Operation(summary = "Export table data", security = {@SecurityRequirement(name = "basicAuth"), @SecurityRequirement(name = "bearerAuth")}) + @Operation(summary = "Get table data", + description = "Gets data from table with id as downloadable file. For tables in private databases, the user needs to have at least *READ* access to the associated database.", + security = {@SecurityRequirement(name = "basicAuth"), @SecurityRequirement(name = "bearerAuth")}) @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Exported table data", @@ -515,6 +542,11 @@ public class TableEndpoint { NotAllowedException, StorageUnavailableException, QueryMalformedException, SidecarExportException, StorageNotFoundException, ServiceException { log.debug("endpoint find table history, databaseId={}, tableId={}, timestamp={}", databaseId, tableId, timestamp); + /* parameters */ + if (timestamp == null) { + timestamp = Instant.now(); + log.debug("timestamp not set: default to {}", timestamp); + } final PrivilegedTableDto table = metadataServiceGateway.getTableById(databaseId, tableId); if (!table.getIsPublic()) { if (principal == null) { @@ -523,11 +555,6 @@ public class TableEndpoint { } metadataServiceGateway.getAccess(databaseId, UserUtil.getId(principal)); } - /* parameters */ - if (timestamp == null) { - log.debug("timestamp not set: default to now"); - timestamp = Instant.now(); - } try { final HttpHeaders headers = new HttpHeaders(); final ExportResourceDto resource = tableService.exportDataset(table, timestamp); @@ -546,8 +573,8 @@ public class TableEndpoint { @PostMapping("/{tableId}/data/import") @Observed(name = "dbrepo_table_data_import") @PreAuthorize("hasAuthority('insert-table-data')") - @Operation(summary = "Import data from a dataset", - description = "Deletes a raw data tuple in a table with at least WRITE_OWN access. Then update the table statistics.", + @Operation(summary = "Import dataset", + description = "Imports a dataset in a table. Then update the table statistics. The user needs to have at least *WRITE_OWN* access to the associated database when importing into a owned table. Otherwise *WRITE_ALL* access in needed. Requires role `insert-table-data`.", security = {@SecurityRequirement(name = "basicAuth"), @SecurityRequirement(name = "bearerAuth")}) @ApiResponses(value = { @ApiResponse(responseCode = "202", @@ -585,12 +612,12 @@ public class TableEndpoint { final DatabaseAccessDto access = metadataServiceGateway.getAccess(databaseId, UserUtil.getId(principal)); endpointValidator.validateOnlyWriteOwnOrWriteAllAccess(access.getType(), table.getOwner().getId(), UserUtil.getId(principal)); if (data.getNullElement() == null) { - log.debug("null element not present, default to empty string"); data.setNullElement(""); + log.debug("null element not present, default to empty string"); } if (data.getLineTermination() == null) { - log.debug("line termination not present, default to \\r\\n"); data.setLineTermination("\\r\\n"); + log.debug("line termination not present, default to {}", data.getLineTermination()); } try { tableService.importDataset(table, data); @@ -605,7 +632,8 @@ public class TableEndpoint { @GetMapping("/{tableId}/statistic") @Observed(name = "dbrepo_table_statistic") - @Operation(summary = "Generate table statistic") + @Operation(summary = "Get table statistic", + description = "Gets basic statistical properties (min, max, mean, median, std.dev) of numerical columns of a table with id.") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Generated table statistic", diff --git a/dbrepo-data-service/rest-service/src/main/java/at/tuwien/endpoints/ViewEndpoint.java b/dbrepo-data-service/rest-service/src/main/java/at/tuwien/endpoints/ViewEndpoint.java index 64eea4ebd0..3212b699a2 100644 --- a/dbrepo-data-service/rest-service/src/main/java/at/tuwien/endpoints/ViewEndpoint.java +++ b/dbrepo-data-service/rest-service/src/main/java/at/tuwien/endpoints/ViewEndpoint.java @@ -12,6 +12,7 @@ import at.tuwien.utils.UserUtil; import at.tuwien.validation.EndpointValidator; import io.micrometer.observation.annotation.Observed; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.headers.Header; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; @@ -55,7 +56,8 @@ public class ViewEndpoint { @GetMapping @PreAuthorize("hasAuthority('admin')") @Observed(name = "dbrepo_view_schema_list") - @Operation(summary = "Find view schemas", hidden = true) + @Operation(summary = "Find views", + hidden = true) @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Found view schemas", @@ -104,7 +106,8 @@ public class ViewEndpoint { @PostMapping @PreAuthorize("hasAuthority('admin')") - @Operation(summary = "Create view", security = {@SecurityRequirement(name = "basicAuth"), @SecurityRequirement(name = "bearerAuth")}, + @Operation(summary = "Create view", + security = {@SecurityRequirement(name = "basicAuth"), @SecurityRequirement(name = "bearerAuth")}, hidden = true) @ApiResponses(value = { @ApiResponse(responseCode = "202", @@ -149,7 +152,8 @@ public class ViewEndpoint { @DeleteMapping("/{viewId}") @PreAuthorize("hasAuthority('admin')") - @Operation(summary = "Delete view", security = {@SecurityRequirement(name = "basicAuth"), @SecurityRequirement(name = "bearerAuth")}, + @Operation(summary = "Delete view", + security = {@SecurityRequirement(name = "basicAuth"), @SecurityRequirement(name = "bearerAuth")}, hidden = true) @ApiResponses(value = { @ApiResponse(responseCode = "202", @@ -193,10 +197,14 @@ public class ViewEndpoint { @RequestMapping(value = "/{viewId}/data", method = {RequestMethod.GET, RequestMethod.HEAD}) @Observed(name = "dbrepo_view_data") - @Operation(summary = "Retrieve view data", security = {@SecurityRequirement(name = "basicAuth"), @SecurityRequirement(name = "bearerAuth")}) + @Operation(summary = "Get view data", + description = "Gets data from a view of a database. For private databases, the user needs at least *READ* access to the associated database. Requires role `view-database-view-data`.", + security = {@SecurityRequirement(name = "basicAuth"), @SecurityRequirement(name = "bearerAuth")}) @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Retrieved view data", + headers = {@Header(name = "X-Count", description = "Number of rows", schema = @Schema(implementation = Long.class), required = true), + @Header(name = "Access-Control-Expose-Headers", description = "Expose `X-Count` custom header", schema = @Schema(implementation = String.class), required = true)}, content = {@Content( mediaType = "application/json", schema = @Schema(implementation = QueryResultDto.class))}), @@ -241,16 +249,16 @@ public class ViewEndpoint { endpointValidator.validateDataParams(page, size); /* parameters */ if (page == null) { - log.debug("page not set: default to 0"); page = 0L; + log.debug("page not set: default to {}", page); } if (size == null) { - log.debug("size not set: default to 10"); size = 10L; + log.debug("size not set: default to {}", size); } if (timestamp == null) { - log.debug("timestamp not set: default to now"); timestamp = Instant.now(); + log.debug("timestamp not set: default to {}", timestamp); } final PrivilegedViewDto view = metadataServiceGateway.getViewById(databaseId, viewId); if (!view.getIsPublic()) { diff --git a/dbrepo-data-service/rest-service/src/test/java/at/tuwien/endpoint/AccessEndpointUnitTest.java b/dbrepo-data-service/rest-service/src/test/java/at/tuwien/endpoint/AccessEndpointUnitTest.java index 544f3f0d17..0a69255f10 100644 --- a/dbrepo-data-service/rest-service/src/test/java/at/tuwien/endpoint/AccessEndpointUnitTest.java +++ b/dbrepo-data-service/rest-service/src/test/java/at/tuwien/endpoint/AccessEndpointUnitTest.java @@ -1,5 +1,7 @@ package at.tuwien.endpoint; +import at.tuwien.api.database.internal.PrivilegedDatabaseDto; +import at.tuwien.api.user.UserDto; import at.tuwien.endpoints.AccessEndpoint; import at.tuwien.exception.*; import at.tuwien.gateway.MetadataServiceGateway; @@ -15,9 +17,10 @@ import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.context.junit.jupiter.SpringExtension; +import java.sql.SQLException; + import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.Mockito.doThrow; -import static org.mockito.Mockito.when; +import static org.mockito.Mockito.*; @Log4j2 @SpringBootTest @@ -28,10 +31,10 @@ public class AccessEndpointUnitTest extends AbstractUnitTest { private AccessEndpoint accessEndpoint; @MockBean - private AccessService accessService; + private MetadataServiceGateway metadataServiceGateway; @MockBean - private MetadataServiceGateway metadataServiceGateway; + private AccessService accessService; @BeforeEach public void beforeEach() { @@ -40,7 +43,7 @@ public class AccessEndpointUnitTest extends AbstractUnitTest { @Test @WithMockUser(username = USER_LOCAL_ADMIN_USERNAME, authorities = {"admin"}) - public void create_succeeds() throws UserNotFoundException, NotAllowedException, QueryMalformedException, + public void create_succeeds() throws UserNotFoundException, NotAllowedException, DatabaseUnavailableException, DatabaseNotFoundException, RemoteUnavailableException, DatabaseMalformedException, ServiceException { /* mock */ @@ -117,7 +120,7 @@ public class AccessEndpointUnitTest extends AbstractUnitTest { @Test @WithMockUser(username = USER_LOCAL_ADMIN_USERNAME, authorities = {"admin"}) public void update_succeeds() throws DatabaseNotFoundException, RemoteUnavailableException, UserNotFoundException, - NotAllowedException, QueryMalformedException, DatabaseMalformedException, ServiceException { + NotAllowedException, DatabaseUnavailableException, DatabaseMalformedException, ServiceException { /* mock */ when(metadataServiceGateway.getDatabaseById(DATABASE_1_ID)) @@ -175,14 +178,18 @@ public class AccessEndpointUnitTest extends AbstractUnitTest { @Test @WithMockUser(username = USER_LOCAL_ADMIN_USERNAME, authorities = {"admin"}) - public void revoke_succeeds() throws UserNotFoundException, NotAllowedException, QueryMalformedException, - DatabaseNotFoundException, RemoteUnavailableException, DatabaseMalformedException, ServiceException { + public void revoke_succeeds() throws UserNotFoundException, NotAllowedException, DatabaseUnavailableException, + DatabaseNotFoundException, RemoteUnavailableException, DatabaseMalformedException, ServiceException, + SQLException { /* mock */ when(metadataServiceGateway.getDatabaseById(DATABASE_1_ID)) .thenReturn(DATABASE_1_PRIVILEGED_DTO); when(metadataServiceGateway.getPrivilegedUserById(USER_1_ID)) .thenReturn(USER_1_PRIVILEGED_DTO); + doNothing() + .when(accessService) + .delete(any(PrivilegedDatabaseDto.class), any(UserDto.class)); /* test */ accessEndpoint.revoke(DATABASE_1_ID, USER_1_ID); diff --git a/dbrepo-data-service/rest-service/src/test/java/at/tuwien/endpoint/DatabaseEndpointUnitTest.java b/dbrepo-data-service/rest-service/src/test/java/at/tuwien/endpoint/DatabaseEndpointUnitTest.java index 21769ff5eb..aa424e3aa6 100644 --- a/dbrepo-data-service/rest-service/src/test/java/at/tuwien/endpoint/DatabaseEndpointUnitTest.java +++ b/dbrepo-data-service/rest-service/src/test/java/at/tuwien/endpoint/DatabaseEndpointUnitTest.java @@ -5,7 +5,6 @@ import at.tuwien.api.user.PrivilegedUserDto; import at.tuwien.endpoints.DatabaseEndpoint; import at.tuwien.exception.*; import at.tuwien.gateway.MetadataServiceGateway; -import at.tuwien.mapper.MetadataMapper; import at.tuwien.service.AccessService; import at.tuwien.service.DatabaseService; import at.tuwien.service.SubsetService; @@ -41,9 +40,6 @@ public class DatabaseEndpointUnitTest extends AbstractUnitTest { @MockBean private AccessService accessService; - @MockBean - private MetadataMapper metadataMapper; - @MockBean private DatabaseService databaseService; diff --git a/dbrepo-data-service/rest-service/src/test/java/at/tuwien/endpoint/TableEndpointUnitTest.java b/dbrepo-data-service/rest-service/src/test/java/at/tuwien/endpoint/TableEndpointUnitTest.java index 62375e2ab4..022b043caa 100644 --- a/dbrepo-data-service/rest-service/src/test/java/at/tuwien/endpoint/TableEndpointUnitTest.java +++ b/dbrepo-data-service/rest-service/src/test/java/at/tuwien/endpoint/TableEndpointUnitTest.java @@ -140,7 +140,8 @@ public class TableEndpointUnitTest extends AbstractUnitTest { @Test @WithAnonymousUser public void getData_succeeds() throws DatabaseUnavailableException, TableNotFoundException, TableMalformedException, - SQLException, QueryMalformedException, RemoteUnavailableException, PaginationException, ServiceException { + SQLException, QueryMalformedException, RemoteUnavailableException, PaginationException, ServiceException, + NotAllowedException { /* mock */ when(metadataServiceGateway.getTableById(DATABASE_3_ID, TABLE_8_ID)) @@ -151,7 +152,7 @@ public class TableEndpointUnitTest extends AbstractUnitTest { .thenReturn(TABLE_8_DATA_DTO); /* test */ - final ResponseEntity<QueryResultDto> response = tableEndpoint.getData(DATABASE_3_ID, TABLE_8_ID, null, null, null); + final ResponseEntity<QueryResultDto> response = tableEndpoint.getData(DATABASE_3_ID, TABLE_8_ID, null, null, null, null); assertEquals(HttpStatus.OK, response.getStatusCode()); assertNotNull(response.getHeaders().get("X-Count")); assertEquals(1, response.getHeaders().get("X-Count").size()); @@ -174,7 +175,7 @@ public class TableEndpointUnitTest extends AbstractUnitTest { /* test */ assertThrows(TableNotFoundException.class, () -> { - tableEndpoint.getData(DATABASE_3_ID, TABLE_8_ID, null, null, null); + tableEndpoint.getData(DATABASE_3_ID, TABLE_8_ID, null, null, null, null); }); } diff --git a/dbrepo-data-service/rest-service/src/test/java/at/tuwien/endpoint/ViewEndpointUnitTest.java b/dbrepo-data-service/rest-service/src/test/java/at/tuwien/endpoint/ViewEndpointUnitTest.java index af4767b049..9f7ad136a8 100644 --- a/dbrepo-data-service/rest-service/src/test/java/at/tuwien/endpoint/ViewEndpointUnitTest.java +++ b/dbrepo-data-service/rest-service/src/test/java/at/tuwien/endpoint/ViewEndpointUnitTest.java @@ -17,7 +17,6 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.context.junit.jupiter.SpringExtension; diff --git a/dbrepo-data-service/rest-service/src/test/java/at/tuwien/mvc/PrometheusEndpointMvcTest.java b/dbrepo-data-service/rest-service/src/test/java/at/tuwien/mvc/PrometheusEndpointMvcTest.java index 803632c078..9d42c1a54d 100644 --- a/dbrepo-data-service/rest-service/src/test/java/at/tuwien/mvc/PrometheusEndpointMvcTest.java +++ b/dbrepo-data-service/rest-service/src/test/java/at/tuwien/mvc/PrometheusEndpointMvcTest.java @@ -171,7 +171,7 @@ public class PrometheusEndpointMvcTest extends AbstractUnitTest { /* mock */ try { - tableEndpoint.getData(DATABASE_1_ID, TABLE_1_ID, null, null, null); + tableEndpoint.getData(DATABASE_1_ID, TABLE_1_ID, null, null, null, null); } catch (Exception e) { /* ignore */ } 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 17c1e20978..1a85308940 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 @@ -56,11 +56,15 @@ public class AccessEndpoint { @Transactional @Observed(name = "dbrepo_access_give") @PreAuthorize("hasAuthority('create-database-access')") - @Operation(summary = "Give access to some database", security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) + @Operation(summary = "Give access", + description = "Give a user with given id access to some database with given id. Requires role `create-database-access`.", + security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) @ApiResponses(value = { @ApiResponse(responseCode = "202", description = "Granting access succeeded", - content = {@Content}), + content = {@Content( + mediaType = "application/json", + schema = @Schema(implementation = DatabaseAccessDto.class))}), @ApiResponse(responseCode = "400", description = "Granting access query or database connection is malformed", content = {@Content( @@ -76,11 +80,6 @@ public class AccessEndpoint { content = {@Content( mediaType = "application/json", schema = @Schema(implementation = ApiErrorDto.class))}), - @ApiResponse(responseCode = "405", - description = "Granting access not permitted", - content = {@Content( - mediaType = "application/json", - schema = @Schema(implementation = ApiErrorDto.class))}), @ApiResponse(responseCode = "502", description = "Access could not be created due to connection error", content = {@Content( @@ -92,7 +91,7 @@ public class AccessEndpoint { mediaType = "application/json", schema = @Schema(implementation = ApiErrorDto.class))}), }) - public ResponseEntity<?> create(@NotBlank @PathVariable("databaseId") Long databaseId, + public ResponseEntity<DatabaseAccessDto> create(@NotBlank @PathVariable("databaseId") Long databaseId, @NotBlank @PathVariable("userId") UUID userId, @Valid @RequestBody UpdateDatabaseAccessDto data, @NotNull Principal principal) throws NotAllowedException, ServiceException, @@ -122,11 +121,12 @@ public class AccessEndpoint { @Transactional @Observed(name = "dbrepo_access_modify") @PreAuthorize("hasAuthority('update-database-access')") - @Operation(summary = "Modify access to some database", security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) + @Operation(summary = "Modify access", + description = "Modifies access of a user with given id to database with given id. Requires role `update-database-access`.", + security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) @ApiResponses(value = { @ApiResponse(responseCode = "202", - description = "Modify access succeeded", - content = {@Content}), + description = "Modified access"), @ApiResponse(responseCode = "400", description = "Modify access query or database connection is malformed", content = {@Content( @@ -153,7 +153,7 @@ public class AccessEndpoint { mediaType = "application/json", schema = @Schema(implementation = ApiErrorDto.class))}), }) - public ResponseEntity<?> update(@NotBlank @PathVariable("databaseId") Long databaseId, + public ResponseEntity<Void> update(@NotBlank @PathVariable("databaseId") Long databaseId, @NotBlank @PathVariable("userId") UUID userId, @Valid @RequestBody UpdateDatabaseAccessDto data, @NotNull Principal principal) throws NotAllowedException, @@ -177,7 +177,9 @@ public class AccessEndpoint { @Transactional(readOnly = true) @Observed(name = "dbrepo_access_get") @PreAuthorize("hasAuthority('check-database-access') or hasAuthority('admin')") - @Operation(summary = "Check access to some database", security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) + @Operation(summary = "Find/Check access", + description = "Finds or checks access of a user with given id to a database with given id. Requests with HTTP method **GET** return the access object, requests with HTTP method **HEAD** only the status. When the user has at least *READ* access, the status 200 is returned, 403 otherwise. Requires role `check-database-access` or `admin`.", + security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Found database access", @@ -220,11 +222,12 @@ public class AccessEndpoint { @Transactional @Observed(name = "dbrepo_access_delete") @PreAuthorize("hasAuthority('delete-database-access')") - @Operation(summary = "Revoke access to some database", security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) + @Operation(summary = "Delete access", + description = "Delete access of a user with id to a database with id. Requires role `delete-database-access`.", + security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) @ApiResponses(value = { @ApiResponse(responseCode = "202", - description = "Revoked access successfully", - content = {@Content}), + description = "Deleted access"), @ApiResponse(responseCode = "400", description = "Modify access query or database connection is malformed", content = {@Content( @@ -251,7 +254,7 @@ public class AccessEndpoint { mediaType = "application/json", schema = @Schema(implementation = ApiErrorDto.class))}), }) - public ResponseEntity<?> revoke(@NotBlank @PathVariable("databaseId") Long databaseId, + public ResponseEntity<Void> revoke(@NotBlank @PathVariable("databaseId") Long databaseId, @NotBlank @PathVariable("userId") UUID userId, @NotNull Principal principal) throws NotAllowedException, ServiceException, ServiceConnectionException, DatabaseNotFoundException, UserNotFoundException, AccessNotFoundException, diff --git a/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/ConceptEndpoint.java b/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/ConceptEndpoint.java index cb58e62def..44a592658e 100644 --- a/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/ConceptEndpoint.java +++ b/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/ConceptEndpoint.java @@ -36,10 +36,11 @@ public class ConceptEndpoint { @GetMapping @Transactional(readOnly = true) @Observed(name = "dbrepo_semantic_concepts_findall") - @Operation(summary = "List semantic concepts") + @Operation(summary = "List concepts", + description = "List all semantic concepts known to the metadata database") @ApiResponses(value = { @ApiResponse(responseCode = "200", - description = "Find all semantic concepts", + description = "List concepts", content = {@Content( mediaType = "application/json", array = @ArraySchema(schema = @Schema(implementation = ConceptDto.class)))}), 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 9d6e24801b..77d35ec498 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 @@ -54,13 +54,14 @@ public class ContainerEndpoint { @GetMapping @Transactional(readOnly = true) @Observed(name = "dbrepo_container_findall") - @Operation(summary = "Find all containers") + @Operation(summary = "List containers", + description = "List all containers in the metadata database.") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "List containers", content = {@Content( mediaType = "application/json", - array = @ArraySchema(schema = @Schema(implementation = ContainerBriefDto[].class)))}), + array = @ArraySchema(schema = @Schema(implementation = ContainerBriefDto.class)))}), }) public ResponseEntity<List<ContainerBriefDto>> findAll(@RequestParam(required = false) Integer limit) { log.debug("endpoint find all containers, limit={}", limit); @@ -77,13 +78,25 @@ public class ContainerEndpoint { @Transactional @Observed(name = "dbrepo_container_create") @PreAuthorize("hasAuthority('create-container')") - @Operation(summary = "Create container", security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) + @Operation(summary = "Create container", + description = "Creates a container in the metadata database. Requires role `create-container`.", + security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) @ApiResponses(value = { @ApiResponse(responseCode = "201", description = "Created a new container", content = {@Content( mediaType = "application/json", - schema = @Schema(implementation = ContainerBriefDto.class))}), + schema = @Schema(implementation = ContainerDto.class))}), + @ApiResponse(responseCode = "400", + description = "Container payload malformed", + content = {@Content( + mediaType = "application/json", + schema = @Schema(implementation = ApiErrorDto.class))}), + @ApiResponse(responseCode = "403", + description = "Create container not permitted, need authority `create-container`", + content = {@Content( + mediaType = "application/json", + schema = @Schema(implementation = ApiErrorDto.class))}), @ApiResponse(responseCode = "404", description = "Container image or user could not be found", content = {@Content( @@ -95,11 +108,11 @@ public class ContainerEndpoint { mediaType = "application/json", schema = @Schema(implementation = ApiErrorDto.class))}), }) - public ResponseEntity<ContainerBriefDto> create(@Valid @RequestBody ContainerCreateDto data) + public ResponseEntity<ContainerDto> create(@Valid @RequestBody ContainerCreateDto data) throws ImageNotFoundException, ContainerAlreadyExistsException { log.debug("endpoint create container, data={}", data); final Container container = containerService.create(data); - final ContainerBriefDto dto = metadataMapper.containerToDatabaseContainerBriefDto(container); + final ContainerDto dto = metadataMapper.containerToContainerDto(container); log.trace("create container resulted in container {}", dto); return ResponseEntity.status(HttpStatus.CREATED) .body(dto); @@ -108,7 +121,8 @@ public class ContainerEndpoint { @GetMapping("/{containerId}") @Transactional(readOnly = true) @Observed(name = "dbrepo_container_find") - @Operation(summary = "Find some container") + @Operation(summary = "Find container", + description = "Finds a container in the metadata database.") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Found container", @@ -147,17 +161,24 @@ public class ContainerEndpoint { @Transactional @Observed(name = "dbrepo_container_delete") @PreAuthorize("hasAuthority('delete-container')") - @Operation(summary = "Delete some container", security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) + @Operation(summary = "Delete container", + description = "Deletes a container in the metadata database. Requires role `delete-container`.", + security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) @ApiResponses(value = { @ApiResponse(responseCode = "202", - description = "Deleted container successfully"), + description = "Deleted container"), + @ApiResponse(responseCode = "403", + description = "Create container not permitted, need authority `delete-container`", + content = {@Content( + mediaType = "application/json", + schema = @Schema(implementation = ApiErrorDto.class))}), @ApiResponse(responseCode = "404", description = "Container not found", content = {@Content( mediaType = "application/json", schema = @Schema(implementation = ApiErrorDto.class))}), }) - public ResponseEntity<?> delete(@NotNull @PathVariable("containerId") Long containerId) throws ContainerNotFoundException { + public ResponseEntity<Void> delete(@NotNull @PathVariable("containerId") Long containerId) throws ContainerNotFoundException { log.debug("endpoint delete container, containerId={}", containerId); final Container container = containerService.find(containerId); containerService.remove(container); 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 b5248ed232..36cf03bb37 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 @@ -12,6 +12,7 @@ import at.tuwien.mapper.MetadataMapper; import at.tuwien.service.*; import io.micrometer.observation.annotation.Observed; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.headers.Header; import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; @@ -65,10 +66,13 @@ public class DatabaseEndpoint { @RequestMapping(method = {RequestMethod.GET, RequestMethod.HEAD}) @Transactional(readOnly = true) @Observed(name = "dbrepo_database_findall") - @Operation(summary = "List databases") + @Operation(summary = "List databases", + description = "Lists all databases in the metadata database. Requests with HTTP method **GET** return the list of databases, requests with HTTP method **HEAD** only the number in the `X-Count` header.") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "List of databases", + headers = {@Header(name = "X-Count", description = "Number of databases", schema = @Schema(implementation = Long.class), required = true), + @Header(name = "Access-Control-Expose-Headers", description = "Expose `X-Count` custom header", schema = @Schema(implementation = String.class), required = true)}, content = {@Content( mediaType = "application/json", array = @ArraySchema(schema = @Schema(implementation = DatabaseDto.class)))}), @@ -101,7 +105,9 @@ public class DatabaseEndpoint { @Transactional(rollbackFor = Exception.class) @PreAuthorize("hasAuthority('create-database')") @Observed(name = "dbrepo_database_create") - @Operation(summary = "Create database", security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) + @Operation(summary = "Create database", + description = "Creates a database in the container with id. Requires roles `create-database`.", + security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) @ApiResponses(value = { @ApiResponse(responseCode = "201", description = "Created a new database", @@ -152,10 +158,12 @@ public class DatabaseEndpoint { } @PutMapping("/{databaseId}/metadata/table") - @Transactional(rollbackFor = {SearchServiceException.class, SearchServiceConnectionException.class, DatabaseNotFoundException.class}) + @Transactional(rollbackFor = {Exception.class}) @PreAuthorize("hasAuthority('find-database')") @Observed(name = "dbrepo_tables_refresh") - @Operation(summary = "Refresh database tables metadata", security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) + @Operation(summary = "Update database table schemas", + description = "Updates the database with id with generated metadata from tables that are not yet known to the database. Only the database owner can perform this operation. Requires role `find-database`.", + security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Refreshed database tables metadata", @@ -206,7 +214,9 @@ public class DatabaseEndpoint { @Transactional(rollbackFor = {SearchServiceException.class, SearchServiceConnectionException.class, DatabaseNotFoundException.class}) @PreAuthorize("hasAuthority('find-database')") @Observed(name = "dbrepo_views_refresh") - @Operation(summary = "Refresh database views metadata", security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) + @Operation(summary = "Update database view schemas", + description = "Updates the database with id with generated metadata from view that are not yet known to the database. Only the database owner can perform this operation. Requires role `find-database`.", + security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Refreshed database views metadata", @@ -252,13 +262,20 @@ public class DatabaseEndpoint { @Transactional @PreAuthorize("hasAuthority('modify-database-visibility')") @Observed(name = "dbrepo_database_visibility") - @Operation(summary = "Update database visibility", security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) + @Operation(summary = "Update database visibility", + description = "Updates the database with id on the visibility. Only the database owner can perform this operation. Requires role `modify-database-visibility`.", + security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) @ApiResponses(value = { @ApiResponse(responseCode = "202", description = "Visibility modified successfully", content = {@Content( mediaType = "application/json", schema = @Schema(implementation = DatabaseDto.class))}), + @ApiResponse(responseCode = "400", + description = "The visibility payload is malformed", + content = {@Content( + mediaType = "application/json", + schema = @Schema(implementation = ApiErrorDto.class))}), @ApiResponse(responseCode = "403", description = "Visibility modification is not permitted", content = {@Content( @@ -299,13 +316,20 @@ public class DatabaseEndpoint { @Transactional @PreAuthorize("hasAuthority('modify-database-owner')") @Observed(name = "dbrepo_database_transfer") - @Operation(summary = "Update database owner", security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) + @Operation(summary = "Update database owner", + description = "Updates the database with id on the owner. Only the database owner can perform this operation. Requires role `modify-database-owner`.", + security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) @ApiResponses(value = { @ApiResponse(responseCode = "202", description = "Transfer of ownership was successful", content = {@Content( mediaType = "application/json", schema = @Schema(implementation = DatabaseDto.class))}), + @ApiResponse(responseCode = "400", + description = "Owner payload is malformed", + content = {@Content( + mediaType = "application/json", + schema = @Schema(implementation = ApiErrorDto.class))}), @ApiResponse(responseCode = "404", description = "Database or user could not be found", content = {@Content( @@ -349,7 +373,9 @@ public class DatabaseEndpoint { @Transactional @PreAuthorize("hasAuthority('modify-database-image')") @Observed(name = "dbrepo_database_image") - @Operation(summary = "Update database image", security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) + @Operation(summary = "Update database preview image", + description = "Updates the database with id on the preview image. Only the database owner can perform this operation. Requires role `modify-database-image`.", + security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) @ApiResponses(value = { @ApiResponse(responseCode = "202", description = "Modify of image was successful", @@ -407,7 +433,9 @@ public class DatabaseEndpoint { @GetMapping("/{databaseId}") @Transactional(readOnly = true) @Observed(name = "dbrepo_database_find") - @Operation(summary = "Find some database", security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) + @Operation(summary = "Find database", + description = "Finds a database with id.", + 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/IdentifierEndpoint.java b/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/IdentifierEndpoint.java index c1bf712840..781e5c44e8 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 @@ -1,6 +1,7 @@ package at.tuwien.endpoints; import at.tuwien.api.database.query.QueryDto; +import at.tuwien.api.database.table.columns.concepts.ConceptDto; import at.tuwien.api.error.ApiErrorDto; import at.tuwien.api.identifier.*; import at.tuwien.api.identifier.ld.LdDatasetDto; @@ -22,6 +23,7 @@ import at.tuwien.utils.UserUtil; import at.tuwien.validation.EndpointValidator; import io.micrometer.observation.annotation.Observed; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; @@ -86,13 +88,16 @@ public class IdentifierEndpoint { @GetMapping(produces = {MediaType.APPLICATION_JSON_VALUE, "application/ld+json"}) @Transactional(readOnly = true) @Observed(name = "dbrepo_identifier_list") - @Operation(summary = "Find all identifiers") + @Operation(summary = "List identifiers", + description = "Lists all identifiers known to the metadata database") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Found identifiers successfully", content = { - @Content(mediaType = "application/json", schema = @Schema(implementation = IdentifierDto[].class)), - @Content(mediaType = "application/ld+json", schema = @Schema(implementation = LdDatasetDto[].class)) + @Content(mediaType = "application/json", + array = @ArraySchema(schema = @Schema(implementation = ConceptDto.class))), + @Content(mediaType = "application/ld+json", + array = @ArraySchema(schema = @Schema(implementation = LdDatasetDto.class))) }), @ApiResponse(responseCode = "406", description = "Identifier could not be exported, the requested style is not known", @@ -104,7 +109,8 @@ public class IdentifierEndpoint { @Valid @RequestParam(value = "qid", required = false) Long qid, @Valid @RequestParam(value = "vid", required = false) Long vid, @Valid @RequestParam(value = "tid", required = false) Long tid, - @RequestHeader(HttpHeaders.ACCEPT) String accept) throws FormatNotAvailableException { + @RequestHeader(HttpHeaders.ACCEPT) String accept) + throws FormatNotAvailableException { log.debug("endpoint find identifiers, dbid={}, qid={}, vid={}, tid={}, accept={}", dbid, qid, vid, tid, accept); final List<Identifier> identifiers = identifierService.findAll() .stream() @@ -141,7 +147,8 @@ public class IdentifierEndpoint { "text/bibliography; style=ieee", "text/bibliography; style=bibtex"}) @Transactional(readOnly = true) @Observed(name = "dbrepo_identifier_find") - @Operation(summary = "Find some identifier") + @Operation(summary = "Find identifier", + description = "Finds an identifier with id. The response format depends on the HTTP `Accept` header set on the request.") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Found identifier successfully", @@ -265,7 +272,9 @@ public class IdentifierEndpoint { @Transactional @Observed(name = "dbrepo_identifier_delete") @PreAuthorize("hasAuthority('delete-identifier')") - @Operation(summary = "Delete some identifier", security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) + @Operation(summary = "Delete identifier", + description = "Deletes an identifier with id. Requires role `delete-identifier`.", + security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) @ApiResponses(value = { @ApiResponse(responseCode = "202", description = "Deleted identifier"), @@ -290,7 +299,7 @@ public class IdentifierEndpoint { mediaType = "application/json", schema = @Schema(implementation = ApiErrorDto.class))}), }) - public ResponseEntity<?> delete(@NotNull @PathVariable("identifierId") Long identifierId) + public ResponseEntity<Void> delete(@NotNull @PathVariable("identifierId") Long identifierId) throws IdentifierNotFoundException, NotAllowedException, ServiceException, ServiceConnectionException, DatabaseNotFoundException, SearchServiceException, SearchServiceConnectionException { log.debug("endpoint delete identifier, identifierId={}", identifierId); @@ -300,7 +309,6 @@ public class IdentifierEndpoint { throw new NotAllowedException("Failed to delete identifier: already published"); } identifierService.delete(identifier); - log.info("Deleted identifier with pid: {}", identifierId); return ResponseEntity.accepted() .build(); } @@ -309,7 +317,9 @@ public class IdentifierEndpoint { @Transactional @Observed(name = "dbrepo_identifier_publish") @PreAuthorize("hasAuthority('publish-identifier')") - @Operation(summary = "Publish identifier", security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) + @Operation(summary = "Publish identifier", + description = "Publishes an identifier with id. A published identifier cannot be changed anymore. Requires role `publish-identifier`.", + security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) @ApiResponses(value = { @ApiResponse(responseCode = "202", description = "Published identifier", @@ -331,11 +341,6 @@ public class IdentifierEndpoint { content = {@Content( mediaType = "application/json", schema = @Schema(implementation = ApiErrorDto.class))}), - @ApiResponse(responseCode = "405", - description = "Creating identifier not permitted", - content = {@Content( - mediaType = "application/json", - schema = @Schema(implementation = ApiErrorDto.class))}), @ApiResponse(responseCode = "502", description = "Connection to search service failed", content = {@Content( @@ -351,7 +356,7 @@ public class IdentifierEndpoint { throws SearchServiceException, DatabaseNotFoundException, SearchServiceConnectionException, MalformedException, ServiceConnectionException, IdentifierNotFoundException { log.debug("endpoint publish identifier, identifierId={}", identifierId); - final Identifier identifier = identifierService.find(identifierId); + identifierService.find(identifierId); return ResponseEntity.status(HttpStatus.CREATED) .body(metadataMapper.identifierToIdentifierDto(identifierService.publish(identifierId))); } @@ -360,7 +365,9 @@ public class IdentifierEndpoint { @Transactional(rollbackFor = {Exception.class}) @Observed(name = "dbrepo_identifier_save") @PreAuthorize("hasAuthority('create-identifier') or hasAuthority('create-foreign-identifier')") - @Operation(summary = "Save identifier", security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) + @Operation(summary = "Save identifier", + description = "Saves an identifier with id as a draft identifier. Identifiers can only be created for objects the user has at least *READ* access in the associated database (requires role `create-identifier`) or for any object in any database (requires role `create-foreign-identifier`).", + security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) @ApiResponses(value = { @ApiResponse(responseCode = "202", description = "Saved identifier", @@ -382,11 +389,6 @@ public class IdentifierEndpoint { content = {@Content( mediaType = "application/json", schema = @Schema(implementation = ApiErrorDto.class))}), - @ApiResponse(responseCode = "405", - description = "Creating identifier not permitted", - content = {@Content( - mediaType = "application/json", - schema = @Schema(implementation = ApiErrorDto.class))}), @ApiResponse(responseCode = "502", description = "Connection to search service failed", content = {@Content( @@ -485,7 +487,9 @@ public class IdentifierEndpoint { @Transactional(rollbackFor = {Exception.class}) @Observed(name = "dbrepo_identifier_create") @PreAuthorize("hasAuthority('create-identifier') or hasAuthority('create-foreign-identifier')") - @Operation(summary = "Draft identifier", security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) + @Operation(summary = "Create identifier", + description = "Create an identifier with id to create a draft identifier. Identifiers can only be created for objects the user has at least *READ* access in the associated database (requires role `create-identifier`) or for any object in any database (requires role `create-foreign-identifier`).", + security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) @ApiResponses(value = { @ApiResponse(responseCode = "201", description = "Drafted identifier", @@ -507,11 +511,6 @@ public class IdentifierEndpoint { content = {@Content( mediaType = "application/json", schema = @Schema(implementation = ApiErrorDto.class))}), - @ApiResponse(responseCode = "405", - description = "Creating identifier not permitted", - content = {@Content( - mediaType = "application/json", - schema = @Schema(implementation = ApiErrorDto.class))}), @ApiResponse(responseCode = "502", description = "Connection to search service failed", content = {@Content( @@ -532,10 +531,9 @@ public class IdentifierEndpoint { final Database database = databaseService.findById(data.getDatabaseId()); final User user = userService.findByUsername(principal.getName()); /* check access */ - DatabaseAccess access = null; try { - access = accessService.find(database, user); - log.trace("found access: {}", access); + final DatabaseAccess access = accessService.find(database, user); + log.trace("found access: {}", access.getType()); } catch (AccessNotFoundException e) { if (!UserUtil.hasRole(principal, "create-foreign-identifier")) { log.error("Failed to create identifier: insufficient role"); @@ -549,7 +547,8 @@ public class IdentifierEndpoint { @GetMapping("/retrieve") @Observed(name = "dbrepo_identifier_retrieve") - @Operation(summary = "Retrieve metadata from identifier") + @Operation(summary = "Retrieve PID metadata", + description = "Retrieves Persistent Identifier (PID) metadata from external endpoints. Supported PIDs are: ORCID, ROR, DOI.") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Retrieved metadata from identifier", 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 d295cc7a11..088dc2981b 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 @@ -52,13 +52,14 @@ public class ImageEndpoint { @GetMapping @Transactional(readOnly = true) @Observed(name = "dbrepo_image_findall") - @Operation(summary = "Find all images") + @Operation(summary = "List images", + description = "Lists all container images known to the metadata database.") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "List images", content = {@Content( mediaType = "application/json", - array = @ArraySchema(schema = @Schema(implementation = ContainerImage.class)))}), + array = @ArraySchema(schema = @Schema(implementation = ImageBriefDto.class)))}), }) public ResponseEntity<List<ImageBriefDto>> findAll() { log.debug("endpoint find all images"); @@ -66,14 +67,16 @@ public class ImageEndpoint { return ResponseEntity.ok() .body(containers.stream() .map(metadataMapper::containerImageToImageBriefDto) - .collect(Collectors.toList())); + .toList()); } @PostMapping @Transactional @Observed(name = "dbrepo_image_create") @PreAuthorize("hasAuthority('create-image')") - @Operation(summary = "Create image", security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) + @Operation(summary = "Create image", + description = "Creates a container image in the metadata database. Requires role `create-image`.", + security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) @ApiResponses(value = { @ApiResponse(responseCode = "201", description = "Created image", @@ -109,7 +112,8 @@ public class ImageEndpoint { @GetMapping("/{imageId}") @Transactional(readOnly = true) @Observed(name = "dbrepo_image_find") - @Operation(summary = "Find some image") + @Operation(summary = "Find image", + description = "Finds a container image in the metadata database.") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Found image", @@ -135,7 +139,9 @@ public class ImageEndpoint { @Transactional @Observed(name = "dbrepo_image_update") @PreAuthorize("hasAuthority('modify-image')") - @Operation(summary = "Update some image", security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) + @Operation(summary = "Update image", + description = "Updates container image in the metadata database. Requires role `modify-image`.", + security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) @ApiResponses(value = { @ApiResponse(responseCode = "202", description = "Updated image successfully", @@ -164,7 +170,9 @@ public class ImageEndpoint { @Transactional @Observed(name = "dbrepo_image_delete") @PreAuthorize("hasAuthority('delete-image')") - @Operation(summary = "Delete some image", security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) + @Operation(summary = "Delete image", + description = "Deletes a container image in the metadata database. Requires role `delete-image`.", + security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) @ApiResponses(value = { @ApiResponse(responseCode = "202", description = "Deleted image successfully", @@ -175,7 +183,7 @@ public class ImageEndpoint { mediaType = "application/json", schema = @Schema(implementation = ApiErrorDto.class))}), }) - public ResponseEntity<?> delete(@NotNull @PathVariable("imageId") Long imageId) throws ImageNotFoundException { + public ResponseEntity<Void> delete(@NotNull @PathVariable("imageId") Long imageId) throws ImageNotFoundException { log.debug("endpoint delete image, id={}", imageId); final ContainerImage image = imageService.find(imageId); imageService.delete(image); diff --git a/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/LicenseEndpoint.java b/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/LicenseEndpoint.java index e2b47905c9..75998b03a5 100644 --- a/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/LicenseEndpoint.java +++ b/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/LicenseEndpoint.java @@ -41,21 +41,21 @@ public class LicenseEndpoint { @GetMapping @Transactional(readOnly = true) @Observed(name = "dbrepo_license_findall") - @Operation(summary = "Get all licenses") + @Operation(summary = "List licenses", + description = "Lists licenses known to the metadata database.") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "List of licenses", content = {@Content( mediaType = "application/json", - array = @ArraySchema(schema = @Schema(implementation = LicenseDto[].class)))}), + array = @ArraySchema(schema = @Schema(implementation = LicenseDto.class)))}), }) public ResponseEntity<List<LicenseDto>> list() { log.debug("endpoint list licenses"); final List<LicenseDto> licenses = licenseService.findAll() .stream() .map(metadataMapper::licenseToLicenseDto) - .collect(Collectors.toList()); - log.trace("list licenses resulted in licenses {}", licenses); + .toList(); return ResponseEntity.status(HttpStatus.OK) .body(licenses); } diff --git a/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/MessageEndpoint.java b/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/MessageEndpoint.java index 62677967b0..0a1cddf5df 100644 --- a/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/MessageEndpoint.java +++ b/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/MessageEndpoint.java @@ -45,7 +45,8 @@ public class MessageEndpoint { @GetMapping @Observed(name = "dbrepo_maintenance_findall") - @Operation(summary = "Find maintenance messages") + @Operation(summary = "List messages", + description = "Lists messages known to the metadata database. Messages can be filtered be filtered with the optional `active` parameter. If set to *true*, only active messages (that is, messages whose end time has not been reached) will be returned. Otherwise only inactive messages are returned. If not set, active and inactive messages are returned.") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "List messages", @@ -53,10 +54,10 @@ public class MessageEndpoint { mediaType = "application/json", array = @ArraySchema(schema = @Schema(implementation = BannerMessageDto.class)))}), }) - public ResponseEntity<List<BannerMessageDto>> list(@RequestParam(required = false) String filter) { - log.debug("endpoint list active maintenance messages"); + public ResponseEntity<List<BannerMessageDto>> list(@RequestParam(required = false) Boolean active) { + log.debug("endpoint list messages, active={}", active); List<BannerMessageDto> dtos; - if (filter.equals("active")) { + if (active != null && active) { dtos = bannerMessageService.getActive() .stream() .map(metadataMapper::bannerMessageToBannerMessageDto) @@ -67,13 +68,14 @@ public class MessageEndpoint { .map(metadataMapper::bannerMessageToBannerMessageDto) .toList(); } - log.trace("list maintenance messages results in dtos {}", dtos); + log.info("List messages resulted in {} message(s)", dtos.size()); return ResponseEntity.ok(dtos); } @GetMapping("/message/{messageId}") @Observed(name = "dbrepo_maintenance_find") - @Operation(summary = "Find one maintenance message") + @Operation(summary = "Find message", + description = "Finds a message with id in the metadata database.") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Get messages", @@ -88,16 +90,17 @@ public class MessageEndpoint { }) public ResponseEntity<BannerMessageDto> find(@NotNull @PathVariable("messageId") Long messageId) throws MessageNotFoundException { - log.debug("endpoint find one maintenance messages"); + log.debug("endpoint find one maintenance message, messageId={}", messageId); final BannerMessageDto dto = metadataMapper.bannerMessageToBannerMessageDto(bannerMessageService.find(messageId)); - log.trace("find one maintenance message results in dto {}", dto); return ResponseEntity.ok(dto); } @PostMapping @Observed(name = "dbrepo_maintenance_create") - @Operation(summary = "Create maintenance message", security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) @PreAuthorize("hasAuthority('create-maintenance-message')") + @Operation(summary = "Create message", + description = "Creates a message in the metadata database. Requires role `create-maintenance-message`.", + security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) @ApiResponses(value = { @ApiResponse(responseCode = "201", description = "Created message", @@ -115,8 +118,10 @@ public class MessageEndpoint { @PutMapping("/{messageId}") @Observed(name = "dbrepo_maintenance_update") - @Operation(summary = "Update maintenance message", security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) @PreAuthorize("hasAuthority('update-maintenance-message')") + @Operation(summary = "Update message", + description = "Updates a message with id. Requires role `update-maintenance-message`.", + security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) @ApiResponses(value = { @ApiResponse(responseCode = "202", description = "Updated message", @@ -142,8 +147,10 @@ public class MessageEndpoint { @DeleteMapping("/{messageId}") @Observed(name = "dbrepo_maintenance_delete") - @Operation(summary = "Delete maintenance message", security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) @PreAuthorize("hasAuthority('delete-maintenance-message')") + @Operation(summary = "Delete message", + description = "Deletes a message with id. Requires role `delete-maintenance-message`.", + security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) @ApiResponses(value = { @ApiResponse(responseCode = "202", description = "Deleted message", @@ -154,7 +161,8 @@ public class MessageEndpoint { mediaType = "application/json", schema = @Schema(implementation = ApiErrorDto.class))}), }) - public ResponseEntity<?> delete(@NotNull @PathVariable("messageId") Long messageId) throws MessageNotFoundException { + public ResponseEntity<Void> delete(@NotNull @PathVariable("messageId") Long messageId) + throws MessageNotFoundException { log.debug("endpoint delete maintenance message, messageId={}", messageId); final BannerMessage message = bannerMessageService.find(messageId); bannerMessageService.delete(message); diff --git a/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/MetadataEndpoint.java b/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/MetadataEndpoint.java index 18bf1c3e62..462ca9df97 100644 --- a/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/MetadataEndpoint.java +++ b/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/MetadataEndpoint.java @@ -44,7 +44,7 @@ public class MetadataEndpoint { @ExampleObject(value = "ListMetadataFormats"), }) @Observed(name = "dbrepo_oai_identify") - @Operation(summary = "Identify the repository") + @Operation(summary = "Identify repository") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "List containers", @@ -57,7 +57,7 @@ public class MetadataEndpoint { @GetMapping(params = "verb=Identify", produces = MediaType.TEXT_XML_VALUE) @Observed(name = "dbrepo_oai_identify") - @Operation(summary = "Identify the repository") + @Operation(summary = "Identify repository") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "List containers", @@ -72,7 +72,7 @@ public class MetadataEndpoint { @GetMapping(params = "verb=ListIdentifiers", produces = MediaType.TEXT_XML_VALUE) @Observed(name = "dbrepo_oai_identifiers_list") - @Operation(summary = "List the identifiers") + @Operation(summary = "List identifiers") public ResponseEntity<String> listIdentifiers(OaiListIdentifiersParameters parameters) { log.debug("endpoint list identifiers, verb=ListIdentifiers, parameters={}", parameters); final String xml = metadataService.listIdentifiers(parameters); @@ -82,7 +82,7 @@ public class MetadataEndpoint { @GetMapping(params = "verb=GetRecord", produces = MediaType.TEXT_XML_VALUE) @Observed(name = "dbrepo_oai_record_get") - @Operation(summary = "Get the record") + @Operation(summary = "Get record") public ResponseEntity<String> getRecord(OaiRecordParameters parameters) { log.debug("endpoint get record, verb=GetRecord, parameters={}", parameters); final List<String> supportedMetadataFormats = List.of("oai_dc", "oai_datacite"); @@ -117,7 +117,7 @@ public class MetadataEndpoint { @GetMapping(params = "verb=ListMetadataFormats", produces = MediaType.TEXT_XML_VALUE) @Observed(name = "dbrepo_oai_metadataformats_list") - @Operation(summary = "List the metadata formats") + @Operation(summary = "List metadata formats") public ResponseEntity<String> listMetadataFormats() { log.debug("endpoint list metadata formats, verb=ListMetadataFormats"); final String xml = metadataService.listMetadataFormats(); 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 8d626db323..cd17c7ac0b 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 @@ -39,7 +39,7 @@ public class OntologyEndpoint { private final OntologyService ontologyService; @Autowired - public OntologyEndpoint(EntityService entityService, MetadataMapper metadataMapper, + public OntologyEndpoint(EntityService entityService, MetadataMapper metadataMapper, OntologyService ontologyService) { this.entityService = entityService; this.metadataMapper = metadataMapper; @@ -48,13 +48,14 @@ public class OntologyEndpoint { @GetMapping @Observed(name = "dbrepo_ontologies_findall") - @Operation(summary = "List all ontologies") + @Operation(summary = "List ontologies", + description = "Lists all ontologies known to the metadata database.") @ApiResponses(value = { @ApiResponse(responseCode = "200", - description = "List all ontologies", + description = "List ontologies", content = {@Content( mediaType = "application/json", - array = @ArraySchema(schema = @Schema(implementation = OntologyDto.class)))}), + array = @ArraySchema(schema = @Schema(implementation = OntologyBriefDto.class)))}), }) public ResponseEntity<List<OntologyBriefDto>> findAll() { log.debug("endpoint find all ontologies"); @@ -62,13 +63,13 @@ public class OntologyEndpoint { .stream() .map(metadataMapper::ontologyToOntologyBriefDto) .toList(); - log.trace("create ontology resulted in dtos {}", dtos); return ResponseEntity.ok(dtos); } @GetMapping("/{ontologyId}") @Observed(name = "dbrepo_ontologies_find") - @Operation(summary = "Find one ontology") + @Operation(summary = "Find ontology", + description = "Finds an ontology with id in the metadata database.") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Find one ontology", @@ -85,14 +86,15 @@ public class OntologyEndpoint { throws OntologyNotFoundException { log.debug("endpoint find all ontologies, ontologyId={}", ontologyId); final OntologyDto dto = metadataMapper.ontologyToOntologyDto(ontologyService.find(ontologyId)); - log.trace("create ontology resulted in dto {}", dto); return ResponseEntity.ok(dto); } @PostMapping @PreAuthorize("hasAuthority('create-ontology')") @Observed(name = "dbrepo_ontologies_create") - @Operation(summary = "Register a new ontology", security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) + @Operation(summary = "Create ontology", + description = "Creates an ontology in the metadata database. Requires role `create-ontology`.", + security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) @ApiResponses(value = { @ApiResponse(responseCode = "201", description = "Registered ontology successfully", @@ -104,7 +106,6 @@ public class OntologyEndpoint { @NotNull Principal principal) { log.debug("endpoint create ontology, data={}", data); final OntologyDto dto = metadataMapper.ontologyToOntologyDto(ontologyService.create(data, principal)); - log.trace("create ontology resulted in dto {}", dto); return ResponseEntity.status(HttpStatus.CREATED) .body(dto); } @@ -112,7 +113,9 @@ public class OntologyEndpoint { @PutMapping("/{ontologyId}") @PreAuthorize("hasAuthority('update-ontology')") @Observed(name = "dbrepo_ontologies_update") - @Operation(summary = "Update an ontology", security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) + @Operation(summary = "Update ontology", + description = "Updates an ontology with id. Requires role `update-ontology`.", + security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) @ApiResponses(value = { @ApiResponse(responseCode = "202", description = "Updated ontology successfully", @@ -131,7 +134,6 @@ public class OntologyEndpoint { log.debug("endpoint update ontology, data={}", data); final Ontology ontology = ontologyService.find(ontologyId); final OntologyDto dto = metadataMapper.ontologyToOntologyDto(ontologyService.update(ontology, data)); - log.trace("update ontology resulted in dto {}", dto); return ResponseEntity.accepted() .body(dto); } @@ -139,7 +141,9 @@ public class OntologyEndpoint { @DeleteMapping("/{ontologyId}") @PreAuthorize("hasAuthority('delete-ontology')") @Observed(name = "dbrepo_ontologies_delete") - @Operation(summary = "Delete an ontology", security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) + @Operation(summary = "Delete ontology", + description = "Deletes an ontology with given id. Requires role `delete-ontology`.", + security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) @ApiResponses(value = { @ApiResponse(responseCode = "202", description = "Deleted ontology successfully", @@ -151,7 +155,7 @@ public class OntologyEndpoint { mediaType = "application/json", schema = @Schema(implementation = ApiErrorDto.class))}), }) - public ResponseEntity<?> delete(@NotNull @PathVariable("ontologyId") Long ontologyId) + public ResponseEntity<Void> delete(@NotNull @PathVariable("ontologyId") Long ontologyId) throws OntologyNotFoundException { log.debug("endpoint delete ontology, ontologyId={}", ontologyId); final Ontology ontology = ontologyService.find(ontologyId); @@ -163,7 +167,9 @@ public class OntologyEndpoint { @GetMapping("/{ontologyId}/entity") @PreAuthorize("hasAuthority('execute-semantic-query')") @Observed(name = "dbrepo_ontologies_entities_find") - @Operation(summary = "Find entities", security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) + @Operation(summary = "Find entities", + description = "Finds semantic entities by label or uri in an ontology with id. Requires role `execute-semantic-query`.", + 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/TableEndpoint.java b/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/TableEndpoint.java index c87b4039c1..0577ef7232 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 @@ -77,7 +77,9 @@ public class TableEndpoint { @GetMapping @Transactional(readOnly = true) @Observed(name = "dbrepo_tables_findall") - @Operation(summary = "List all tables", security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) + @Operation(summary = "List tables", + description = "Lists all tables known to the metadata database.", + security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "List tables", @@ -114,13 +116,15 @@ public class TableEndpoint { @Transactional(readOnly = true) @PreAuthorize("hasAuthority('table-semantic-analyse')") @Observed(name = "dbrepo_semantic_table_analyse") - @Operation(summary = "Suggest table semantics", security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) + @Operation(summary = "Suggest semantics", + description = "Suggests semantic concepts for a table. Requires role `table-semantic-analyse`.", + security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Suggested table semantics successfully", content = {@Content( mediaType = "application/json", - array = @ArraySchema(schema = @Schema(implementation = TableColumnEntityDto.class)))}), + array = @ArraySchema(schema = @Schema(implementation = EntityDto.class)))}), @ApiResponse(responseCode = "400", description = "Failed to parse statistic in search service", content = {@Content( @@ -148,7 +152,6 @@ public class TableEndpoint { log.debug("endpoint analyse table semantics, databaseId={}, tableId={}", databaseId, tableId); final Table table = tableService.findById(databaseId, tableId); final List<EntityDto> dtos = entityService.suggestByTable(table); - log.trace("analyse table semantics resulted in dtos {}", dtos); return ResponseEntity.ok() .body(dtos); } @@ -157,7 +160,9 @@ public class TableEndpoint { @Transactional @PreAuthorize("hasAuthority('update-table-statistic') or hasAuthority('admin')") @Observed(name = "dbrepo_statistic_table_update") - @Operation(summary = "Update table statistics", security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) + @Operation(summary = "Update statistics", + description = "Updates basic statistical properties (min, max, mean, median, std.dev) for numerical columns in a table with id. Requires role `update-table-statistic`", + security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) @ApiResponses(value = { @ApiResponse(responseCode = "202", description = "Updated table statistics successfully"), @@ -197,7 +202,9 @@ public class TableEndpoint { @Transactional @PreAuthorize("hasAuthority('modify-table-column-semantics') or hasAuthority('modify-foreign-table-column-semantics')") @Observed(name = "dbrepo_semantics_column_save") - @Operation(summary = "Update a table column semantic mapping", security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) + @Operation(summary = "Update semantics", + description = "Updates column semantics of a table column with id. Only the table owner with at least *READ* access to the associated database can update the column semantics (requires role `modify-table-column-semantics`) or foreign table columns if role `modify-foreign-table-column-semantics`.", + security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) @ApiResponses(value = { @ApiResponse(responseCode = "202", description = "Updated column semantics successfully", @@ -258,7 +265,9 @@ public class TableEndpoint { @Transactional(readOnly = true) @PreAuthorize("hasAuthority('table-semantic-analyse')") @Observed(name = "dbrepo_semantic_column_analyse") - @Operation(summary = "Suggest table column semantics", security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) + @Operation(summary = "Suggest semantics", + description = "Suggests column semantics. Requires role `table-semantic-analyse`.", + security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Suggested table column semantics successfully", @@ -289,16 +298,17 @@ public class TableEndpoint { final Table table = tableService.findById(databaseId, tableId); TableColumn column = tableService.findColumnById(table, columnId); final List<TableColumnEntityDto> dtos = entityService.suggestByColumn(column); - log.trace("analyse table semantics resulted in dtos {}", dtos); return ResponseEntity.ok() .body(dtos); } @PostMapping - @Transactional(rollbackFor = {ServiceConnectionException.class, DatabaseNotFoundException.class, ServiceException.class}) + @Transactional(rollbackFor = {Exception.class}) @PreAuthorize("hasAuthority('create-table')") @Observed(name = "dbrepo_table_create") - @Operation(summary = "Create a table", security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) + @Operation(summary = "Create table", + description = "Creates a table in the database with id. Requires role `create-table`.", + security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) @ApiResponses(value = { @ApiResponse(responseCode = "201", description = "Created a new table", @@ -357,7 +367,7 @@ public class TableEndpoint { } final Table table = tableService.createTable(database, data, principal); final TableDto dto = metadataMapper.customTableToTableDto(table); - log.debug("create table resulted in table.id={}", dto.getId()); + log.info("Created table with id {}", dto.getId()); return ResponseEntity.status(HttpStatus.CREATED) .body(dto); } @@ -365,7 +375,9 @@ public class TableEndpoint { @GetMapping("/{tableId}") @Transactional(readOnly = true) @Observed(name = "dbrepo_tables_find") - @Operation(summary = "Get information about table", security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) + @Operation(summary = "Find table", + description = "Finds a table with id.", + security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Find table successfully", @@ -383,12 +395,12 @@ public class TableEndpoint { mediaType = "application/json", schema = @Schema(implementation = ApiErrorDto.class))}), @ApiResponse(responseCode = "502", - description = "Connection to search service failed", + description = "Failed to establish connection with broker service", content = {@Content( mediaType = "application/json", schema = @Schema(implementation = ApiErrorDto.class))}), @ApiResponse(responseCode = "503", - description = "Failed to save in search service", + description = "Failed to obtain queue information from broker service", content = {@Content( mediaType = "application/json", schema = @Schema(implementation = ApiErrorDto.class))}), @@ -418,7 +430,6 @@ public class TableEndpoint { headers.set("Access-Control-Expose-Headers", "X-Username X-Password X-Host X-Port X-Type X-Database X-Sidecar-Host X-Sidecar-Port"); } } - log.trace("find table resulted in table {}", dto); return ResponseEntity.status(HttpStatus.OK) .headers(headers) .body(dto); @@ -428,11 +439,12 @@ public class TableEndpoint { @Transactional @PreAuthorize("hasAuthority('delete-table') or hasAuthority('delete-foreign-table')") @Observed(name = "dbrepo_table_delete") - @Operation(summary = "Delete a table", security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) + @Operation(summary = "Delete table", + description = "Deletes a table with id. Only the owner of a table can perform this action (requires role `delete-table`) or anyone can delete a table (requires role `delete-foreign-table`).", + security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) @ApiResponses(value = { @ApiResponse(responseCode = "202", - description = "Delete table successfully", - content = {@Content}), + description = "Delete table successfully"), @ApiResponse(responseCode = "400", description = "Delete table query resulted in an invalid query statement", content = {@Content( @@ -459,9 +471,9 @@ public class TableEndpoint { mediaType = "application/json", schema = @Schema(implementation = ApiErrorDto.class))}), }) - public ResponseEntity<?> delete(@NotNull @PathVariable("databaseId") Long databaseId, - @NotNull @PathVariable("tableId") Long tableId, - @NotNull Principal principal) throws NotAllowedException, + public ResponseEntity<Void> delete(@NotNull @PathVariable("databaseId") Long databaseId, + @NotNull @PathVariable("tableId") Long tableId, + @NotNull Principal principal) throws NotAllowedException, ServiceException, ServiceConnectionException, TableNotFoundException, DatabaseNotFoundException, SearchServiceException, SearchServiceConnectionException { log.debug("endpoint delete table, databaseId={}, tableId={}", databaseId, tableId); diff --git a/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/UnitEndpoint.java b/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/UnitEndpoint.java index 71dfa78cef..c992f151b5 100644 --- a/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/UnitEndpoint.java +++ b/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/UnitEndpoint.java @@ -36,7 +36,8 @@ public class UnitEndpoint { @GetMapping @Transactional(readOnly = true) @Observed(name = "dbrepo_semantic_units_findall") - @Operation(summary = "List semantic units") + @Operation(summary = "List units", + description = "Lists units known to the metadata database.") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Find all semantic units", @@ -50,7 +51,6 @@ public class UnitEndpoint { .stream() .map(metadataMapper::tableColumnUnitToUnitDto) .toList(); - log.trace("Find all units resulted in dtos {}", dtos); return ResponseEntity.ok() .body(dtos); } 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 fb9ddc0096..1ca013ac2c 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 @@ -59,7 +59,8 @@ public class UserEndpoint { @GetMapping @Transactional(readOnly = true) @Observed(name = "dbrepo_users_list") - @Operation(summary = "Find all users") + @Operation(summary = "List users", + description = "Lists users known to the metadata database.") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "List users", @@ -73,21 +74,21 @@ public class UserEndpoint { .stream() .map(userMapper::userToUserBriefDto) .toList(); - log.trace("find all users resulted in users {}", users); return ResponseEntity.ok(users); } @PostMapping - @Transactional(rollbackFor = {ServiceException.class, ServiceConnectionException.class}) + @Transactional(rollbackFor = {Exception.class}) @PreAuthorize("!isAuthenticated()") @Observed(name = "dbrepo_user_create") - @Operation(summary = "Create user") + @Operation(summary = "Create user", + description = "Creates a user in the auth service and metadata database. Requires that no credentials are sent in the request.") @ApiResponses(value = { @ApiResponse(responseCode = "201", description = "Created user", content = {@Content( mediaType = "application/json", - schema = @Schema(implementation = UserBriefDto.class))}), + schema = @Schema(implementation = UserDto.class))}), @ApiResponse(responseCode = "400", description = "Parameters are not well-formed (likely email)", content = {@Content(mediaType = "application/json")}), @@ -117,30 +118,35 @@ public class UserEndpoint { mediaType = "application/json", schema = @Schema(implementation = ApiErrorDto.class))}), }) - public ResponseEntity<UserBriefDto> create(@NotNull @Valid @RequestBody SignupRequestDto data) + public ResponseEntity<UserDto> create(@NotNull @Valid @RequestBody SignupRequestDto data) throws UserExistsException, EmailExistsException, ServiceException, ServiceConnectionException, UserNotFoundException { - log.debug("endpoint create a user, data.username={}", data.getUsername()); + log.debug("endpoint create user, data.username={}", data.getUsername()); userService.validateUsernameNotExists(data.getUsername()); userService.validateEmailNotExists(data.getEmail()); authenticationService.create(data); final at.tuwien.api.keycloak.UserDto keycloakUserDto = authenticationService.findByUsername(data.getUsername()); final User user = userService.create(data, keycloakUserDto.getId()); - final UserBriefDto dto = userMapper.userToUserBriefDto(user); - log.trace("create user resulted in dto {}", dto); + log.info("Created user with id: {}", user.getId()); return ResponseEntity.status(HttpStatus.CREATED) - .body(dto); + .body(userMapper.userToUserDto(user)); } @PostMapping("/token") @Observed(name = "dbrepo_user_token") - @Operation(summary = "Obtain user token") + @Operation(summary = "Create token", + description = "Creates a user token via the auth service.") @ApiResponses(value = { @ApiResponse(responseCode = "202", description = "Obtained user token", content = {@Content( mediaType = "application/json", schema = @Schema(implementation = TokenDto.class))}), + @ApiResponse(responseCode = "400", + description = "Invalid login request", + content = {@Content( + mediaType = "application/json", + schema = @Schema(implementation = ApiErrorDto.class))}), @ApiResponse(responseCode = "403", description = "Not allowed to get token", content = {@Content( @@ -192,18 +198,24 @@ public class UserEndpoint { @PutMapping("/token") @Observed(name = "dbrepo_user_refresh_token") - @Operation(summary = "Refresh user token") + @Operation(summary = "Refresh token", + description = "Refreshes user token by refresh token.") @ApiResponses(value = { @ApiResponse(responseCode = "202", description = "Refreshed user token", content = {@Content( mediaType = "application/json", schema = @Schema(implementation = TokenDto.class))}), - @ApiResponse(responseCode = "403", + @ApiResponse(responseCode = "400", description = "Invalid refresh token", content = {@Content( mediaType = "application/json", schema = @Schema(implementation = ApiErrorDto.class))}), + @ApiResponse(responseCode = "403", + description = "Not allowed", + content = {@Content( + mediaType = "application/json", + schema = @Schema(implementation = ApiErrorDto.class))}), @ApiResponse(responseCode = "502", description = "Connection to auth service failed", content = {@Content( @@ -223,7 +235,9 @@ public class UserEndpoint { @Transactional(readOnly = true) @PreAuthorize("isAuthenticated()") @Observed(name = "dbrepo_user_find") - @Operation(summary = "Get a user info", security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) + @Operation(summary = "Get user", + description = "Gets user with id from the metadata database. Requires authentication.", + security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Found user", @@ -262,7 +276,9 @@ public class UserEndpoint { @Transactional @PreAuthorize("hasAuthority('modify-user-information')") @Observed(name = "dbrepo_user_modify") - @Operation(summary = "Modify user information", security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) + @Operation(summary = "Update user", + description = "Updates user with id. Requires role `modify-user-information`.", + security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) @ApiResponses(value = { @ApiResponse(responseCode = "202", description = "Modified user information", @@ -305,13 +321,17 @@ public class UserEndpoint { @Transactional @PreAuthorize("isAuthenticated()") @Observed(name = "dbrepo_user_password_modify") - @Operation(summary = "Modify user password", security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) + @Operation(summary = "Update user password", + description = "Updates password of user with id. Requires authentication.", + security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) @ApiResponses(value = { @ApiResponse(responseCode = "202", - description = "Modified user password", + description = "Modified user password"), + @ApiResponse(responseCode = "400", + description = "Invalid password payload", content = {@Content( mediaType = "application/json", - schema = @Schema(implementation = UserDto.class))}), + schema = @Schema(implementation = ApiErrorDto.class))}), @ApiResponse(responseCode = "403", description = "Not allowed to change foreign user password", content = {@Content( @@ -333,7 +353,7 @@ public class UserEndpoint { mediaType = "application/json", schema = @Schema(implementation = ApiErrorDto.class))}), }) - public ResponseEntity<?> password(@NotNull @PathVariable("userId") UUID userId, + public ResponseEntity<Void> password(@NotNull @PathVariable("userId") UUID userId, @NotNull @Valid @RequestBody UserPasswordDto data, @NotNull Principal principal) throws NotAllowedException, ServiceException, ServiceConnectionException, UserNotFoundException, DatabaseNotFoundException { 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 701e3172fb..775b117bc3 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 @@ -59,7 +59,9 @@ public class ViewEndpoint { @GetMapping @Transactional(readOnly = true) @Observed(name = "dbrepo_views_findall") - @Operation(summary = "Find all views", security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) + @Operation(summary = "List views", + description = "Lists views known to the metadata database.", + security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Find views successfully", @@ -90,7 +92,9 @@ public class ViewEndpoint { @Transactional @PreAuthorize("hasAuthority('create-database-view')") @Observed(name = "dbrepo_view_create") - @Operation(summary = "Create a view", security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) + @Operation(summary = "Create view", + description = "Creates a view. Requires role `create-database-view`.", + security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) @ApiResponses(value = { @ApiResponse(responseCode = "201", description = "Create view successfully", @@ -102,11 +106,6 @@ public class ViewEndpoint { content = {@Content( mediaType = "application/json", schema = @Schema(implementation = ApiErrorDto.class))}), - @ApiResponse(responseCode = "401", - description = "Credentials missing", - content = {@Content( - mediaType = "application/json", - schema = @Schema(implementation = ApiErrorDto.class))}), @ApiResponse(responseCode = "403", description = "Credentials missing", content = {@Content( @@ -117,11 +116,6 @@ public class ViewEndpoint { content = {@Content( mediaType = "application/json", schema = @Schema(implementation = ApiErrorDto.class))}), - @ApiResponse(responseCode = "405", - description = "Create view is not permitted", - content = {@Content( - mediaType = "application/json", - schema = @Schema(implementation = ApiErrorDto.class))}), @ApiResponse(responseCode = "423", description = "Create view resulted in an invalid query statement", content = {@Content( @@ -161,7 +155,9 @@ public class ViewEndpoint { @GetMapping("/{viewId}") @Transactional(readOnly = true) @Observed(name = "dbrepo_view_find") - @Operation(summary = "Find one view", security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) + @Operation(summary = "Get view", + description = "Gets a view with id in the metadata database.", + security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Find view successfully", @@ -208,11 +204,12 @@ public class ViewEndpoint { @Transactional @PreAuthorize("hasAuthority('delete-database-view')") @Observed(name = "dbrepo_view_delete") - @Operation(summary = "Delete one view", security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) + @Operation(summary = "Delete view", + description = "Deletes a view with id. Requires role `delete-database-view`.", + security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) @ApiResponses(value = { @ApiResponse(responseCode = "202", - description = "Delete view successfully", - content = {@Content}), + description = "Delete view successfully"), @ApiResponse(responseCode = "400", description = "Delete view query is malformed", content = {@Content( @@ -228,11 +225,6 @@ public class ViewEndpoint { content = {@Content( mediaType = "application/json", schema = @Schema(implementation = ApiErrorDto.class))}), - @ApiResponse(responseCode = "405", - description = "Delete view is not permitted", - content = {@Content( - mediaType = "application/json", - schema = @Schema(implementation = ApiErrorDto.class))}), @ApiResponse(responseCode = "423", description = "Delete view resulted in an invalid query statement", content = {@Content( @@ -249,7 +241,7 @@ public class ViewEndpoint { mediaType = "application/json", schema = @Schema(implementation = ApiErrorDto.class))}), }) - public ResponseEntity<?> delete(@NotNull @PathVariable("databaseId") Long databaseId, + public ResponseEntity<View> delete(@NotNull @PathVariable("databaseId") Long databaseId, @NotNull @PathVariable("viewId") Long viewId, @NotNull Principal principal) throws NotAllowedException, ServiceException, ServiceConnectionException, DatabaseNotFoundException, ViewNotFoundException, SearchServiceException, diff --git a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/endpoints/AccessEndpointUnitTest.java b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/endpoints/AccessEndpointUnitTest.java index 0444a76690..16c212a546 100644 --- a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/endpoints/AccessEndpointUnitTest.java +++ b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/endpoints/AccessEndpointUnitTest.java @@ -78,9 +78,8 @@ public class AccessEndpointUnitTest extends AbstractUnitTest { SearchServiceConnectionException { /* mock */ - doNothing() - .when(accessService) - .create(eq(DATABASE_1), eq(USER_2), any(AccessTypeDto.class)); + when(accessService.create(eq(DATABASE_1), eq(USER_2), any(AccessTypeDto.class))) + .thenReturn(DATABASE_1_USER_1_READ_ACCESS); /* test */ generic_create(USER_2_PRINCIPAL, USER_2_ID, USER_2_USERNAME, USER_2); diff --git a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/endpoints/ContainerEndpointUnitTest.java b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/endpoints/ContainerEndpointUnitTest.java index 1296346660..cb230377f3 100644 --- a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/endpoints/ContainerEndpointUnitTest.java +++ b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/endpoints/ContainerEndpointUnitTest.java @@ -239,7 +239,7 @@ public class ContainerEndpointUnitTest extends AbstractUnitTest { .thenReturn(CONTAINER_1); /* test */ - final ResponseEntity<ContainerBriefDto> response = containerEndpoint.create(data); + final ResponseEntity<ContainerDto> response = containerEndpoint.create(data); assertEquals(HttpStatus.CREATED, response.getStatusCode()); assertNotNull(response.getBody()); } diff --git a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/endpoints/MaintenanceEndpointUnitTest.java b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/endpoints/MessageEndpointUnitTest.java similarity index 91% rename from dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/endpoints/MaintenanceEndpointUnitTest.java rename to dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/endpoints/MessageEndpointUnitTest.java index b05e32e92e..cea67bc510 100644 --- a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/endpoints/MaintenanceEndpointUnitTest.java +++ b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/endpoints/MessageEndpointUnitTest.java @@ -27,13 +27,13 @@ import static org.mockito.Mockito.*; @Log4j2 @SpringBootTest @ExtendWith(SpringExtension.class) -public class MaintenanceEndpointUnitTest extends AbstractUnitTest { +public class MessageEndpointUnitTest extends AbstractUnitTest { @MockBean private BannerMessageService bannerMessageService; @Autowired - private MessageEndpoint maintenanceEndpoint; + private MessageEndpoint messageEndpoint; @Test @WithAnonymousUser @@ -198,7 +198,7 @@ public class MaintenanceEndpointUnitTest extends AbstractUnitTest { .thenReturn(List.of(BANNER_MESSAGE_1, BANNER_MESSAGE_2)); /* test */ - final ResponseEntity<List<BannerMessageDto>> response = maintenanceEndpoint.list(""); + final ResponseEntity<List<BannerMessageDto>> response = messageEndpoint.list(null); assertEquals(HttpStatus.OK, response.getStatusCode()); assertNotNull(response.getBody()); } @@ -216,7 +216,7 @@ public class MaintenanceEndpointUnitTest extends AbstractUnitTest { } /* test */ - final ResponseEntity<BannerMessageDto> response = maintenanceEndpoint.find(messageId); + final ResponseEntity<BannerMessageDto> response = messageEndpoint.find(messageId); assertEquals(HttpStatus.OK, response.getStatusCode()); assertNotNull(response.getBody()); } @@ -228,7 +228,7 @@ public class MaintenanceEndpointUnitTest extends AbstractUnitTest { .thenReturn(message); /* test */ - final ResponseEntity<BannerMessageDto> response = maintenanceEndpoint.create(data); + final ResponseEntity<BannerMessageDto> response = messageEndpoint.create(data); assertEquals(HttpStatus.CREATED, response.getStatusCode()); assertNotNull(response.getBody()); } @@ -243,7 +243,7 @@ public class MaintenanceEndpointUnitTest extends AbstractUnitTest { .thenReturn(message); /* test */ - final ResponseEntity<BannerMessageDto> response = maintenanceEndpoint.update(messageId, data); + final ResponseEntity<BannerMessageDto> response = messageEndpoint.update(messageId, data); assertEquals(HttpStatus.ACCEPTED, response.getStatusCode()); assertNotNull(response.getBody()); } @@ -264,7 +264,7 @@ public class MaintenanceEndpointUnitTest extends AbstractUnitTest { .delete(message); /* test */ - final ResponseEntity<?> response = maintenanceEndpoint.delete(messageId); + final ResponseEntity<?> response = messageEndpoint.delete(messageId); assertEquals(HttpStatus.ACCEPTED, response.getStatusCode()); assertNull(response.getBody()); } diff --git a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/endpoints/UserEndpointUnitTest.java b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/endpoints/UserEndpointUnitTest.java index b7db83d321..dfc2615b29 100644 --- a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/endpoints/UserEndpointUnitTest.java +++ b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/endpoints/UserEndpointUnitTest.java @@ -259,9 +259,9 @@ public class UserEndpointUnitTest extends AbstractUnitTest { .create(any(SignupRequestDto.class)); /* test */ - final ResponseEntity<UserBriefDto> response = userEndpoint.create(data); + final ResponseEntity<UserDto> response = userEndpoint.create(data); assertEquals(HttpStatus.CREATED, response.getStatusCode()); - final UserBriefDto body = response.getBody(); + final UserDto body = response.getBody(); assertNotNull(body); } 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 44b924f396..23aa393e9f 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 @@ -352,7 +352,7 @@ public class PrometheusEndpointMvcTest extends AbstractUnitTest { /* mock */ try { - messageEndpoint.list(""); + messageEndpoint.list(null); } catch (Exception e) { /* ignore */ } diff --git a/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/AccessService.java b/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/AccessService.java index a013a25ce1..d5a4d03092 100644 --- a/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/AccessService.java +++ b/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/AccessService.java @@ -25,7 +25,7 @@ public interface AccessService { * * @param database The database. * @param user The user. - * @return The database access. + * @return The database access, if successful. * @throws AccessNotFoundException The access was not found in the metadata database. */ DatabaseAccess find(Database database, User user) throws AccessNotFoundException; @@ -36,11 +36,12 @@ public interface AccessService { * @param database The database. * @param access The access. * @param user The user. + * @return The database access, if successful. * @throws ServiceException The data service responded with unexpected behavior. * @throws ServiceConnectionException The connection with the data service could not be established. * @throws DatabaseNotFoundException The database was not found in the metadata/search database. */ - void create(Database database, User user, AccessTypeDto access) throws ServiceException, ServiceConnectionException, + DatabaseAccess create(Database database, User user, AccessTypeDto access) throws ServiceException, ServiceConnectionException, DatabaseNotFoundException, SearchServiceException, SearchServiceConnectionException; /** diff --git a/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/AccessServiceImpl.java b/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/AccessServiceImpl.java index 5de1366e9f..e1e6924e76 100644 --- a/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/AccessServiceImpl.java +++ b/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/AccessServiceImpl.java @@ -62,23 +62,25 @@ public class AccessServiceImpl implements AccessService { @Override @Transactional - public void create(Database database, User user, AccessTypeDto access) throws ServiceException, + public DatabaseAccess create(Database database, User user, AccessTypeDto type) throws ServiceException, ServiceConnectionException, DatabaseNotFoundException, SearchServiceException, SearchServiceConnectionException { /* create in data database */ - dataServiceGateway.createAccess(database.getId(), user.getId(), access); + dataServiceGateway.createAccess(database.getId(), user.getId(), type); /* create in metadata database */ + final DatabaseAccess access = DatabaseAccess.builder() + .hdbid(database.getId()) + .database(database) + .huserid(user.getId()) + .type(metadataMapper.accessTypeDtoToAccessType(type)) + .build(); database.getAccesses() - .add(DatabaseAccess.builder() - .hdbid(database.getId()) - .database(database) - .huserid(user.getId()) - .type(metadataMapper.accessTypeDtoToAccessType(access)) - .build()); + .add(access); database = databaseRepository.save(database); /* create in search service */ searchServiceGateway.update(database); log.info("Created access to database with id {}", database.getId()); + return access; } @Override diff --git a/dbrepo-search-service/app.py b/dbrepo-search-service/app.py index 5d3c816ffd..460f0c7eba 100644 --- a/dbrepo-search-service/app.py +++ b/dbrepo-search-service/app.py @@ -13,7 +13,6 @@ from flask_httpauth import HTTPTokenAuth, HTTPBasicAuth, MultiAuth from opensearchpy import TransportError, NotFoundError from prometheus_flask_exporter import PrometheusMetrics from pydantic import ValidationError -from logging.config import dictConfig from clients.keycloak_client import User, KeycloakClient from clients.opensearch_client import OpenSearchClient @@ -21,6 +20,8 @@ from clients.opensearch_client import OpenSearchClient logging.addLevelName(level=logging.NOTSET, levelName='TRACE') logging.basicConfig(level=logging.DEBUG) +from logging.config import dictConfig + # logging configuration dictConfig({ 'version': 1, @@ -191,7 +192,7 @@ template = { } swagger = Swagger(app, config=swagger_config, template=template) -app.config["GATEWAY_ENDPOINT"] = os.getenv("GATEWAY_ENDPOINT", "http://localhost") +app.config["GATEWAY_SERVICE_ENDPOINT"] = os.getenv("GATEWAY_SERVICE_ENDPOINT", "http://localhost") app.config["JWT_ALGORITHM"] = "HS256" app.config["JWT_PUBKEY"] = '-----BEGIN PUBLIC KEY-----\n' + os.getenv("JWT_PUBKEY", "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqqnHQ2BWWW9vDNLRCcxD++xZg/16oqMo/c1l+lcFEjjAIJjJp/HqrPYU/U9GvquGE6PbVFtTzW1KcKawOW+FJNOA3CGo8Q1TFEfz43B8rZpKsFbJKvQGVv1Z4HaKPvLUm7iMm8Hv91cLduuoWx6Q3DPe2vg13GKKEZe7UFghF+0T9u8EKzA/XqQ0OiICmsmYPbwvf9N3bCKsB/Y10EYmZRb8IhCoV9mmO5TxgWgiuNeCTtNCv2ePYqL/U0WvyGFW0reasIK8eg3KrAUj8DpyOgPOVBn3lBGf+3KFSYi+0bwZbJZWqbC/Xlk20Go1YfeJPRIt7ImxD27R/lNjgDO/MwIDAQAB") + '\n-----END PUBLIC KEY-----' diff --git a/docker-compose.yml b/docker-compose.yml index 17389cad12..48d62373b2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -269,6 +269,7 @@ services: AUTH_SERVICE_CLIENT_SECRET: ${AUTH_SERVICE_CLIENT:-MUwRc7yfXSJwX8AdRMWaQC3Nep1VjwgG} AUTH_SERVICE_ENDPOINT: ${AUTH_SERVICE_ENDPOINT:-http://auth-service:8080} COLLECTION: ${COLLECTION:-['database','table','column','identifier','unit','concept','user','view']} + GATEWAY_SERVICE_ENDPOINT: ${GATEWAY_SERVICE_ENDPOINT:-http://gateway-service} OPENSEARCH_HOST: ${OPENSEARCH_HOST:-search-db} OPENSEARCH_PORT: ${OPENSEARCH_PORT:-9200} OPENSEARCH_USERNAME: ${OPENSEARCH_USERNAME:-admin} diff --git a/helm/dbrepo/values.yaml b/helm/dbrepo/values.yaml index dc8cd7bdba..15e6888d17 100644 --- a/helm/dbrepo/values.yaml +++ b/helm/dbrepo/values.yaml @@ -336,6 +336,14 @@ uploadservice: image: repository: tusproject/tusd tag: v1.12 + securityContext: + allowPrivilegeEscalation: false + runAsNonRoot: true + seccompProfile: + type: RuntimeDefault + capabilities: + drop: + - ALL containerArgs: - "--base-path=/api/upload/files/" - "-s3-endpoint=http://storageservice-s3:9000" diff --git a/lib/python/dbrepo/RestClient.py b/lib/python/dbrepo/RestClient.py index 22790681d8..6813acd095 100644 --- a/lib/python/dbrepo/RestClient.py +++ b/lib/python/dbrepo/RestClient.py @@ -11,7 +11,7 @@ from dbrepo.UploadClient import UploadClient from dbrepo.api.dto import * from dbrepo.api.exceptions import ResponseCodeError, UsernameExistsError, EmailExistsError, NotExistsError, \ ForbiddenError, MalformedError, NameExistsError, QueryStoreError, MetadataConsistencyError, ExternalSystemError, \ - AuthenticationError, UploadError + AuthenticationError, UploadError, FormatNotAvailable, RequestError, ServiceError, ServiceConnectionError class RestClient: @@ -45,6 +45,8 @@ class RestClient: self.secure = os.environ.get('REST_API_SECURE') == 'True' else: self.secure = secure + logging.debug( + f'initialized rest client with endpoint={self.endpoint}, username={username}, verify_ssl={secure}') def _wrapper(self, method: str, url: str, params: [(str,)] = None, payload=None, headers: dict = None, force_auth: bool = False, stream: bool = False) -> requests.Response: @@ -93,14 +95,18 @@ class RestClient: def get_jwt_auth(self, username: str = None, password: str = None) -> JwtAuth: """ - Obtains a JWT auth object from the Auth Service containing e.g. the access token and refresh token. + Obtains a JWT auth object from the auth service containing e.g. the access token and refresh token. - :param username: The username used to authenticate with the Auth Service. Optional. Default: username from the `RestClient` constructor. - :param password: The password used to authenticate with the Auth Service. Optional. Default: password from the `RestClient` constructor. + :param username: The username used to authenticate with the auth service. Optional. Default: username from the `RestClient` constructor. + :param password: The password used to authenticate with the auth service. Optional. Default: password from the `RestClient` constructor. - :returns: JWT auth object from the Auth Service, if successful. + :returns: JWT auth object from the auth service, if successful. - :raises ForbiddenError: If something went wrong with the authentication. + :raises MalformedError: If the payload was rejected by the service. + :raises ForbiddenError: If something went wrong with the authorization. + :raises AuthenticationError: If something went wrong with the authentication. + :raises ServiceConnectionError: If something went wrong with connection to the auth service. + :raises ServiceError: If something went wrong with obtaining the information in the auth service. :raises ResponseCodeError: If something went wrong with the authentication. """ if username is None: @@ -112,9 +118,45 @@ class RestClient: if response.status_code == 202: body = response.json() return JwtAuth.model_validate(body) + if response.status_code == 400: + raise MalformedError(f'Failed to get JWT: {response.text}') if response.status_code == 403: - raise ForbiddenError(f'Failed to get JWT auth') - raise ResponseCodeError(f'Failed to get JWT auth: response code: {response.status_code} is not 202 (ACCEPTED)') + raise ForbiddenError(f'Failed to get JWT: not allowed') + if response.status_code == 428: + raise AuthenticationError(f'Failed to get JWT: account not fully setup (requires password change?)') + if response.status_code == 502: + raise ServiceConnectionError(f'Failed to get JWT: failed to establish connection with auth service') + if response.status_code == 503: + raise ServiceError(f'Failed to get JWT: failed to get user in auth service') + raise ResponseCodeError(f'Failed to get JWT: response code: {response.status_code} is not ' + f'202 (ACCEPTED): {response.text}') + + def refresh_jwt_auth(self, refresh_token: str) -> JwtAuth: + """ + Refreshes a JWT auth object from the auth service containing e.g. the access token and refresh token. + + :param refresh_token: The refresh token. + + :returns: JWT auth object from the auth service, if successful. + + :raises MalformedError: If the payload was rejected by the service. + :raises ForbiddenError: If something went wrong with the authorization. + :raises ServiceConnectionError: If something went wrong with the connection to the auth service. + :raises ResponseCodeError: If something went wrong with the authentication. + """ + url = f'{self.endpoint}/api/user/token' + response = self._wrapper(method="put", url=url, payload={"refresh_token": refresh_token}) + if response.status_code == 202: + body = response.json() + return JwtAuth.model_validate(body) + if response.status_code == 400: + raise MalformedError(f'Failed to refresh JWT: {response.text}') + if response.status_code == 403: + raise ForbiddenError(f'Failed to refresh JWT: not allowed') + if response.status_code == 502: + raise ServiceConnectionError(f'Failed to refresh JWT: failed to establish connection with auth service') + raise ResponseCodeError(f'Failed to refresh JWT: response code: {response.status_code} is not ' + f'202 (ACCEPTED): {response.text}') def whoami(self) -> str | None: """ @@ -141,7 +183,24 @@ class RestClient: if response.status_code == 200: body = response.json() return TypeAdapter(List[UserBrief]).validate_python(body) - raise ResponseCodeError(f'Failed to find users: response code: {response.status_code} is not 200 (OK)') + raise ResponseCodeError(f'Failed to find users: response code: {response.status_code} is not ' + f'200 (OK): {response.text}') + + def get_units(self) -> List[Unit]: + """ + Get all units known to the metadata database. + + :returns: List of units, if successful. + + :raises ResponseCodeError: If something went wrong with the retrieval. + """ + url = f'/api/unit' + response = self._wrapper(method="get", url=url) + if response.status_code == 200: + body = response.json() + return TypeAdapter(List[Unit]).validate_python(body) + raise ResponseCodeError(f'Failed to find units: response code: {response.status_code} is not ' + f'200 (OK): {response.text}') def get_user(self, user_id: str) -> User: """ @@ -150,17 +209,20 @@ class RestClient: :returns: The user, if successful. :raises ResponseCodeError: If something went wrong with the retrieval. - :raises NotExistsError: If theuser does not exist. + :raises ForbiddenError: If something went wrong with the authorization. + :raises NotExistsError: If the user does not exist. """ url = f'/api/user/{user_id}' response = self._wrapper(method="get", url=url) if response.status_code == 200: body = response.json() return User.model_validate(body) + if response.status_code == 403: + raise ForbiddenError(f'Failed to find user: not allowed') if response.status_code == 404: - raise NotExistsError(f'Failed to find user with id {user_id}') - raise ResponseCodeError( - f'Failed to find user with id {user_id}: response code: {response.status_code} is not 200 (OK)') + raise NotExistsError(f'Failed to find user: not found') + raise ResponseCodeError(f'Failed to find user: response code: {response.status_code} is not ' + f'200 (OK): {response.text}') def create_user(self, username: str, password: str, email: str) -> UserBrief: """ @@ -172,11 +234,13 @@ class RestClient: :returns: The user, if successful. - :raises ResponseCodeError: If something went wrong with the creation. + :raises MalformedError: If the payload was rejected by the service. :raises UsernameExistsError: The username exists already. - :raises ForbiddenError: If the action is not allowed. - :raises NotExistsError: If thedefault role was not found. + :raises ForbiddenError: If something went wrong with the authorization. + :raises NotExistsError: If the created user was not found in the auth service. :raises EmailExistsError: The email exists already. + :raises ServiceConnectionError: If something went wrong with connection to the auth service. + :raises ServiceError: If something went wrong with obtaining the information in the auth service. """ url = f'/api/user' response = self._wrapper(method="post", url=url, @@ -184,16 +248,20 @@ class RestClient: if response.status_code == 201: body = response.json() return UserBrief.model_validate(body) - if response.status_code == 403: - raise ForbiddenError(f'Failed to update user password: not allowed') + if response.status_code == 400: + raise MalformedError(f'Failed to create user: {response.text}') if response.status_code == 404: - raise NotExistsError(f'Failed to create user: default role not found') + raise NotExistsError(f'Failed to create user: failed to find created user in auth service') if response.status_code == 409: raise UsernameExistsError(f'Failed to create user: user with username exists') if response.status_code == 417: raise EmailExistsError(f'Failed to create user: user with e-mail exists') - raise ResponseCodeError( - f'Failed to create user: response code: {response.status_code} is not 201 (CREATED)') + if response.status_code == 502: + raise ServiceConnectionError(f'Failed to create user: failed to establish connection with auth service') + if response.status_code == 503: + raise ServiceError(f'Failed to create user: failed to create in auth service') + raise ResponseCodeError(f'Failed to create user: response code: {response.status_code} is not ' + f'201 (CREATED): {response.text}') def update_user(self, user_id: str, theme: str, language: str, firstname: str = None, lastname: str = None, affiliation: str = None, orcid: str = None) -> User: @@ -210,9 +278,10 @@ class RestClient: :returns: The user, if successful. + :raises MalformedError: If the payload was rejected by the service. + :raises ForbiddenError: If something went wrong with the authorization. + :raises NotExistsError: If the user does not exist. :raises ResponseCodeError: If something went wrong with the update. - :raises ForbiddenError: If the action is not allowed. - :raises NotExistsError: If theuser does not exist. """ url = f'/api/user/{user_id}' response = self._wrapper(method="put", url=url, force_auth=True, @@ -222,15 +291,13 @@ class RestClient: body = response.json() return User.model_validate(body) if response.status_code == 400: - raise ResponseCodeError(f'Failed to update user: invalid values') + raise MalformedError(f'Failed to update user: {response.text}') if response.status_code == 403: raise ForbiddenError(f'Failed to update user password: not allowed') if response.status_code == 404: raise NotExistsError(f'Failed to update user: user not found') - if response.status_code == 405: - raise ForbiddenError(f'Failed to update user: foreign user') - raise ResponseCodeError( - f'Failed to update user: response code: {response.status_code} is not 202 (ACCEPTED)') + raise ResponseCodeError(f'Failed to update user: response code: {response.status_code} is not ' + f'202 (ACCEPTED): {response.text}') def update_user_password(self, user_id: str, password: str) -> User: """ @@ -241,9 +308,12 @@ class RestClient: :returns: The user, if successful. + :raises MalformedError: If the payload was rejected by the service. + :raises ForbiddenError: If something went wrong with the authorization. + :raises NotExistsError: If the user does not exist. + :raises ServiceConnectionError: If something went wrong with connection to the auth service. + :raises ServiceError: If something went wrong with obtaining the information in the auth service. :raises ResponseCodeError: If something went wrong with the update. - :raises ForbiddenError: If the action is not allowed. - :raises NotExistsError: If theuser does not exist. """ url = f'/api/user/{user_id}/password' response = self._wrapper(method="put", url=url, force_auth=True, payload=UpdateUserPassword(password=password)) @@ -251,17 +321,18 @@ class RestClient: body = response.json() return User.model_validate(body) if response.status_code == 400: - raise ResponseCodeError(f'Failed to update user password: invalid values') + raise MalformedError(f'Failed to update user password: {response.text}') if response.status_code == 403: raise ForbiddenError(f'Failed to update user password: not allowed') if response.status_code == 404: raise NotExistsError(f'Failed to update user password: not found') - if response.status_code == 405: - raise ForbiddenError(f'Failed to update user password: foreign user') + if response.status_code == 502: + raise ServiceConnectionError( + f'Failed to update user password: failed to establish connection with auth service') if response.status_code == 503: - raise ResponseCodeError(f'Failed to update user password: keycloak error') - raise ResponseCodeError( - f'Failed to update user theme: response code: {response.status_code} is not 202 (ACCEPTED)') + raise ServiceError(f'Failed to update user password: failed to update in auth service') + raise ResponseCodeError(f'Failed to update user theme: response code: {response.status_code} is not ' + f'202 (ACCEPTED): {response.text}') def get_containers(self) -> List[ContainerBrief]: """ @@ -276,7 +347,8 @@ class RestClient: if response.status_code == 200: body = response.json() return TypeAdapter(List[ContainerBrief]).validate_python(body) - raise ResponseCodeError(f'Failed to find containers: response code: {response.status_code} is not 200 (OK)') + raise ResponseCodeError(f'Failed to find containers: response code: {response.status_code} is not ' + f'200 (OK): {response.text}') def get_container(self, container_id: int) -> Container: """ @@ -294,7 +366,8 @@ class RestClient: return Container.model_validate(body) if response.status_code == 404: raise NotExistsError(f'Failed to get container: not found') - raise ResponseCodeError(f'Failed to get container: response code: {response.status_code} is not 200 (OK)') + raise ResponseCodeError(f'Failed to get container: response code: {response.status_code} is not ' + f'200 (OK): {response.text}') def get_databases(self) -> List[Database]: """ @@ -309,7 +382,8 @@ class RestClient: if response.status_code == 200: body = response.json() return TypeAdapter(List[Database]).validate_python(body) - raise ResponseCodeError(f'Failed to find databases: response code: {response.status_code} is not 200 (OK)') + raise ResponseCodeError(f'Failed to find databases: response code: {response.status_code} is not ' + f'200 (OK): {response.text}') def get_databases_count(self) -> int: """ @@ -322,7 +396,8 @@ class RestClient: response = self._wrapper(method="head", url=url) if response.status_code == 200: return int(response.headers.get("x-count")) - raise ResponseCodeError(f'Failed to find databases: response code: {response.status_code} is not 200 (OK)') + raise ResponseCodeError(f'Failed to find databases: response code: {response.status_code} is not ' + f'200 (OK): {response.text}') def get_database(self, database_id: int) -> Database: """ @@ -332,6 +407,9 @@ class RestClient: :returns: The database, if successful. + :raises NotExistsError: If the container does not exist. + :raises ServiceConnectionError: If something went wrong with connection to the broker service. + :raises ServiceError: If something went wrong with obtaining the information in the broker service. :raises ResponseCodeError: If something went wrong with the retrieval. """ url = f'/api/database/{database_id}' @@ -340,9 +418,13 @@ class RestClient: body = response.json() return Database.model_validate(body) if response.status_code == 404: - raise NotExistsError(f'Failed to find database with id {database_id}') - raise ResponseCodeError( - f'Failed to find database with id {database_id}: response code: {response.status_code} is not 200 (OK)') + raise NotExistsError(f'Failed to find database: not found') + if response.status_code == 502: + raise ServiceConnectionError(f'Failed to find database: failed to establish connection with broker service') + if response.status_code == 503: + raise ServiceError(f'Failed to find database: failed to obtain queue metadata from broker service') + raise ResponseCodeError(f'Failed to find database: response code: {response.status_code} is not ' + f'200 (OK): {response.text}') def create_database(self, name: str, container_id: int, is_public: bool) -> Database: """ @@ -355,9 +437,13 @@ class RestClient: :returns: The database, if successful. - :raises ResponseCodeError: If something went wrong with the retrieval. - :raises ForbiddenError: If the action is not allowed. + :raises MalformedError: If the payload was rejected by the service. + :raises ForbiddenError: If something went wrong with the authorization. :raises NotExistsError: If the container does not exist. + :raises QueryStoreError: If something went wrong with the query store. + :raises ServiceConnectionError: If something went wrong with connection to the search service. + :raises ServiceError: If something went wrong with obtaining the information in the search service. + :raises ResponseCodeError: If something went wrong with the retrieval. """ url = f'/api/database' response = self._wrapper(method="post", url=url, force_auth=True, @@ -365,12 +451,68 @@ class RestClient: if response.status_code == 201: body = response.json() return Database.model_validate(body) + if response.status_code == 400: + raise MalformedError(f'Failed to create database: {response.text}') if response.status_code == 403: raise ForbiddenError(f'Failed to create database: not allowed') if response.status_code == 404: raise NotExistsError(f'Failed to create database: container not found') - raise ResponseCodeError( - f'Failed to create database: response code: {response.status_code} is not 201 (CREATED)') + if response.status_code == 409: + raise QueryStoreError(f'Failed to create database: failed to create query store in data database') + if response.status_code == 502: + raise ServiceConnectionError(f'Failed to create database: failed to establish connection to search service') + if response.status_code == 503: + raise ServiceError(f'Failed to create database: failed to create in search service') + raise ResponseCodeError(f'Failed to create database: response code: {response.status_code} is not ' + f'201 (CREATED): {response.text}') + + def create_container(self, name: str, host: str, image_id: int, sidecar_host: str, sidecar_port: int, + privileged_username: str, privileged_password: str, port: int = None, ui_host: str = None, + ui_port: int = None) -> Container: + """ + Register a container instance executing a given container image. Note that this does not create a container, + but only saves it in the metadata database to be used within DBRepo. The container still needs to be created + through e.g. `docker run image:tag -d`. + + :param name: The container name. + :param host: The container hostname. + :param image_id: The container image id. + :param sidecar_host: The container sidecar hostname. + :param sidecar_port: The container sidecar port. + :param privileged_username: The container privileged user username. + :param privileged_password: The container privileged user password. + :param port: The container port bound to the host. Optional. + :param ui_host: The container hostname displayed in the user interface. Optional. Default: value of `host` + :param ui_port: The container port displayed in the user interface. Optional. Default: `default_port` of image. + + :returns: The container, if successful. + + :raises MalformedError: If the payload was rejected by the service. + :raises ForbiddenError: If something went wrong with the authorization. + :raises NotExistsError: If the container does not exist. + :raises NameExistsError: If a container with this name already exists. + :raises ResponseCodeError: If something went wrong with the retrieval. + """ + url = f'/api/container' + response = self._wrapper(method="post", url=url, force_auth=True, + payload=CreateContainer(name=name, host=host, image_id=image_id, + sidecar_host=sidecar_host, sidecar_port=sidecar_port, + privileged_username=privileged_username, + privileged_password=privileged_password, port=port, + ui_host=ui_host, ui_port=ui_port)) + if response.status_code == 201: + body = response.json() + return Container.model_validate(body) + if response.status_code == 400: + raise MalformedError(f'Failed to create container: {response.text}') + if response.status_code == 403: + raise ForbiddenError(f'Failed to create container: not allowed') + if response.status_code == 404: + raise NotExistsError(f'Failed to create container: container not found') + if response.status_code == 409: + raise NameExistsError(f'Failed to create container: container name already exists') + raise ResponseCodeError(f'Failed to create container: response code: {response.status_code} is not ' + f'201 (CREATED): {response.text}') def update_database_visibility(self, database_id: int, is_public: bool) -> Database: """ @@ -382,19 +524,29 @@ class RestClient: :returns: The database, if successful. - :raises ResponseCodeError: If something went wrong with the retrieval. - :raises ForbiddenError: If the action is not allowed. - :raises NotExistsError: If thedatabase does not exist. + :raises MalformedError: If the payload was rejected by the service. + :raises ForbiddenError: If something went wrong with the authorization. + :raises NotExistsError: If the database does not exist. + :raises ServiceConnectionError: If something went wrong with connection to the search service. + :raises ServiceError: If something went wrong with obtaining the information in the search service. + :raises ResponseCodeError: If something went wrong with the update. """ url = f'/api/database/{database_id}' response = self._wrapper(method="put", url=url, force_auth=True, payload=ModifyVisibility(is_public=is_public)) if response.status_code == 202: body = response.json() return Database.model_validate(body) + if response.status_code == 400: + raise MalformedError(f'Failed to update database visibility: {response.text}') if response.status_code == 403: raise ForbiddenError(f'Failed to update database visibility: not allowed') if response.status_code == 404: raise NotExistsError(f'Failed to update database visibility: not found') + if response.status_code == 502: + raise ServiceConnectionError( + f'Failed to update database visibility: failed to establish connection to search service') + if response.status_code == 503: + raise ServiceError(f'Failed to update database visibility: failed to update in search service') raise ResponseCodeError( f'Failed to update database visibility: response code: {response.status_code} is not 202 (ACCEPTED)') @@ -407,21 +559,71 @@ class RestClient: :returns: The database, if successful. - :raises ResponseCodeError: If something went wrong with the retrieval. - :raises NotExistsError: If thedatabase does not exist. + :raises MalformedError: If the payload was rejected by the service. + :raises ForbiddenError: If something went wrong with the authorization. + :raises NotExistsError: If the database does not exist. + :raises ServiceConnectionError: If something went wrong with connection to the search service. + :raises ServiceError: If something went wrong with obtaining the information in the search service. + :raises ResponseCodeError: If something went wrong with the update. """ url = f'/api/database/{database_id}/owner' response = self._wrapper(method="put", url=url, force_auth=True, payload=ModifyOwner(id=user_id)) if response.status_code == 202: body = response.json() return Database.model_validate(body) + if response.status_code == 400: + raise MalformedError(f'Failed to update database visibility: {response.text}') if response.status_code == 403: raise ForbiddenError(f'Failed to update database visibility: not allowed') if response.status_code == 404: raise NotExistsError(f'Failed to update database visibility: not found') + if response.status_code == 502: + raise ServiceConnectionError( + f'Failed to update database visibility: failed to establish connection to search service') + if response.status_code == 503: + raise ServiceError( + f'Failed to update database visibility: failed to update in search service') raise ResponseCodeError( f'Failed to update database visibility: response code: {response.status_code} is not 202 (ACCEPTED)') + def update_database_schema(self, database_id: int) -> Database: + """ + Updates the database table and view metadata of a database with given database id. + + :param database_id: The database id. + + :returns: The updated database, if successful. + + :raises MalformedError: If the payload was rejected by the service. + :raises ForbiddenError: If something went wrong with the authorization. + :raises NotExistsError: If the database does not exist. + :raises ServiceConnectionError: If something went wrong with connection to the data service. + :raises ServiceError: If something went wrong with obtaining the information in the data service. + :raises ResponseCodeError: If something went wrong with the update. + """ + url = f'/api/database/{database_id}/metadata/table' + response = self._wrapper(method="put", url=url, force_auth=True) + if response.status_code == 200: + response.json() + url = f'/api/database/{database_id}/metadata/view' + response = self._wrapper(method="put", url=url, force_auth=True) + if response.status_code == 200: + body = response.json() + return Database.model_validate(body) + if response.status_code == 400: + raise MalformedError(f'Failed to update database schema: {response.text}') + if response.status_code == 403: + raise ForbiddenError(f'Failed to update database schema: not allowed') + if response.status_code == 404: + raise NotExistsError(f'Failed to update database schema: not found') + if response.status_code == 502: + raise ServiceConnectionError( + f'Failed to update database schema: failed to establish connection to search service') + if response.status_code == 503: + raise ServiceError(f'Failed to update database schema: failed to update in search service') + raise ResponseCodeError( + f'Failed to update database schema: response code: {response.status_code} is not 200 (OK)') + def create_table(self, database_id: int, name: str, columns: List[CreateTableColumn], constraints: CreateTableConstraints, description: str = None) -> Table: """ @@ -435,11 +637,13 @@ class RestClient: :returns: The table, if successful. - :raises ResponseCodeError: If something went wrong with the retrieval. - :raises NameExistsError: If a table with this name already exists. - :raises ForbiddenError: If the action is not allowed. :raises MalformedError: If the payload is rejected by the service. - :raises NotExistsError: If the container does not exist. + :raises ForbiddenError: If something went wrong with the authorization. + :raises NotExistsError: If the database does not exist. + :raises NameExistsError: If a table with this name already exists. + :raises ServiceConnectionError: If something went wrong with connection to the data service. + :raises ServiceError: If something went wrong with obtaining the information in the data service. + :raises ResponseCodeError: If something went wrong with the creation. """ url = f'/api/database/{database_id}/table' response = self._wrapper(method="post", url=url, force_auth=True, @@ -449,15 +653,19 @@ class RestClient: body = response.json() return Table.model_validate(body) if response.status_code == 400: - raise MalformedError(f'Failed to create table: service rejected malformed payload') + raise MalformedError(f'Failed to create table: {response.text}') if response.status_code == 403: raise ForbiddenError(f'Failed to create table: not allowed') if response.status_code == 404: - raise NotExistsError(f'Failed to create table: container not found') + raise NotExistsError(f'Failed to create table: not found') if response.status_code == 409: raise NameExistsError(f'Failed to create table: table name exists') - raise ResponseCodeError( - f'Failed to create table: response code: {response.status_code} is not 201 (CREATED)') + if response.status_code == 502: + raise ServiceConnectionError(f'Failed to create table: failed to establish connection to data service') + if response.status_code == 503: + raise ServiceError(f'Failed to create table: failed to create table in data service') + raise ResponseCodeError(f'Failed to create table: response code: {response.status_code} is not ' + f'201 (CREATED): {response.text}') def get_tables(self, database_id: int) -> List[TableBrief]: """ @@ -467,6 +675,8 @@ class RestClient: :returns: List of tables, if successful. + :raises ForbiddenError: If something went wrong with the authorization. + :raises NotExistsError: If the database does not exist. :raises ResponseCodeError: If something went wrong with the retrieval. """ url = f'/api/database/{database_id}/table' @@ -474,7 +684,12 @@ class RestClient: if response.status_code == 200: body = response.json() return TypeAdapter(List[TableBrief]).validate_python(body) - raise ResponseCodeError(f'Failed to find tables: response code: {response.status_code} is not 200 (OK)') + if response.status_code == 403: + raise ForbiddenError(f'Failed to get tables: not allowed') + if response.status_code == 404: + raise NotExistsError(f'Failed to get tables: database not found') + raise ResponseCodeError(f'Failed to get tables: response code: {response.status_code} is not ' + f'200 (OK): {response.text}') def get_table(self, database_id: int, table_id: int) -> Table: """ @@ -485,9 +700,11 @@ class RestClient: :returns: List of tables, if successful. + :raises ForbiddenError: If something went wrong with the authorization. + :raises NotExistsError: If the table does not exist. + :raises ServiceConnectionError: If something went wrong with connection to the metadata service. + :raises ServiceError: If something went wrong with obtaining the information in the metadata service. :raises ResponseCodeError: If something went wrong with the retrieval. - :raises ForbiddenError: If the action is not allowed. - :raises NotExistsError: If thecontainer does not exist. """ url = f'/api/database/{database_id}/table/{table_id}' response = self._wrapper(method="get", url=url) @@ -498,7 +715,12 @@ class RestClient: raise ForbiddenError(f'Failed to find table: not allowed') if response.status_code == 404: raise NotExistsError(f'Failed to find table: not found') - raise ResponseCodeError(f'Failed to find table: response code: {response.status_code} is not 200 (OK)') + if response.status_code == 502: + raise ServiceConnectionError(f'Failed to find table: failed to establish connection to broker service') + if response.status_code == 503: + raise ServiceError(f'Failed to find table: failed to obtain queue information from broker service') + raise ResponseCodeError(f'Failed to find table: response code: {response.status_code} is not ' + f'200 (OK): {response.text}') def delete_table(self, database_id: int, table_id: int) -> None: """ @@ -507,19 +729,55 @@ class RestClient: :param database_id: The database id. :param table_id: The table id. - :raises ResponseCodeError: If something went wrong with the retrieval. - :raises ForbiddenError: If the action is not allowed. - :raises NotExistsError: If thecontainer does not exist. + :raises MalformedError: If the payload is rejected by the service. + :raises ForbiddenError: If something went wrong with the authorization. + :raises NotExistsError: If the container does not exist. + :raises ServiceConnectionError: If something went wrong with connection to the data service. + :raises ServiceError: If something went wrong with obtaining the information in the data service. + :raises ResponseCodeError: If something went wrong with the deletion. """ url = f'/api/database/{database_id}/table/{table_id}' response = self._wrapper(method="delete", url=url, force_auth=True) if response.status_code == 202: return + if response.status_code == 400: + raise MalformedError(f'Failed to delete table: {response.text}') if response.status_code == 403: raise ForbiddenError(f'Failed to delete table: not allowed') if response.status_code == 404: raise NotExistsError(f'Failed to delete table: not found') - raise ResponseCodeError(f'Failed to delete table: response code: {response.status_code} is not 202 (ACCEPTED)') + if response.status_code == 502: + raise ServiceConnectionError(f'Failed to delete table: failed to establish connection to search service') + if response.status_code == 503: + raise ServiceError(f'Failed to delete table: failed to delete in search service') + raise ResponseCodeError(f'Failed to delete table: response code: {response.status_code} is not ' + f'202 (ACCEPTED): {response.text}') + + def delete_container(self, container_id: int) -> None: + """ + Deletes a container with given id. Note that this does not delete the container, but deletes the entry in the + metadata database. The container still needs to be removed, e.g. `docker container stop hash` and then + `docker container rm hash`. + + :param container_id: The container id. + + :raises MalformedError: If the payload is rejected by the service. + :raises ForbiddenError: If something went wrong with the authorization. + :raises NotExistsError: If the container does not exist. + :raises ResponseCodeError: If something went wrong with the deletion. + """ + url = f'/api/container/{container_id}' + response = self._wrapper(method="delete", url=url, force_auth=True) + if response.status_code == 202: + return + if response.status_code == 400: + raise MalformedError(f'Failed to delete container: {response.text}') + if response.status_code == 403: + raise ForbiddenError(f'Failed to delete container: not allowed') + if response.status_code == 404: + raise NotExistsError(f'Failed to delete container: not found') + raise ResponseCodeError(f'Failed to delete container: response code: {response.status_code} is not ' + f'202 (ACCEPTED): {response.text}') def get_table_metadata(self, database_id: int) -> Database: """ @@ -527,9 +785,9 @@ class RestClient: :param database_id: The database id. + :raises ForbiddenError: If something went wrong with the authorization. + :raises NotExistsError: If the table does not exist. :raises ResponseCodeError: If something went wrong with the retrieval. - :raises ForbiddenError: If the action is not allowed. - :raises NotExistsError: If the container does not exist. """ url = f'/api/database/{database_id}/metadata/table' response = self._wrapper(method="put", url=url, force_auth=True) @@ -540,7 +798,38 @@ class RestClient: raise ForbiddenError(f'Failed to get tables metadata: not allowed') if response.status_code == 404: raise NotExistsError(f'Failed to get tables metadata: not found') - raise ResponseCodeError(f'Failed to get tables metadata: response code: {response.status_code} is not 200 (OK)') + raise ResponseCodeError(f'Failed to get tables metadata: response code: {response.status_code} is not ' + f'200 (OK): {response.text}') + + def get_table_history(self, database_id: int, table_id: int, size: int = 100) -> Database: + """ + Get the table history of insert/delete operations. + + :param database_id: The database id. + :param table_id: The table id. + :param size: The number of operations. Optional. Default: 100. + + :raises MalformedError: If the payload is rejected by the service. + :raises ForbiddenError: If something went wrong with the authorization. + :raises NotExistsError: If the table does not exist. + :raises ServiceError: If something went wrong with obtaining the information in the data service. + :raises ResponseCodeError: If something went wrong with the retrieval. + """ + url = f'/api/database/{database_id}/table/{table_id}/history?size={size}' + response = self._wrapper(method="get", url=url, force_auth=True) + if response.status_code == 200: + body = response.json() + return Database.model_validate(body) + if response.status_code == 400: + raise MalformedError(f'Failed to get table history: {response.text}') + if response.status_code == 403: + raise ForbiddenError(f'Failed to get table history: not allowed') + if response.status_code == 404: + raise NotExistsError(f'Failed to get table history: not found') + if response.status_code == 503: + raise ServiceError(f'Failed to get table history: failed to establish connection with metadata service') + raise ResponseCodeError(f'Failed to get table history: response code: {response.status_code} is not ' + f'200 (OK): {response.text}') def get_views(self, database_id: int) -> List[View]: """ @@ -550,20 +839,18 @@ class RestClient: :returns: The list of views, if successful. + :raises NotExistsError: If the container does not exist. :raises ResponseCodeError: If something went wrong with the retrieval. - :raises ForbiddenError: If the action is not allowed. - :raises NotExistsError: If thecontainer does not exist. """ url = f'/api/database/{database_id}/view' response = self._wrapper(method="get", url=url) if response.status_code == 200: body = response.json() return TypeAdapter(List[View]).validate_python(body) - if response.status_code == 403: - raise ForbiddenError(f'Failed to find views: not allowed') if response.status_code == 404: raise NotExistsError(f'Failed to find views: not found') - raise ResponseCodeError(f'Failed to find views: response code: {response.status_code} is not 200 (OK)') + raise ResponseCodeError(f'Failed to find views: response code: {response.status_code} is not ' + f'200 (OK): {response.text}') def get_view(self, database_id: int, view_id: int) -> View: """ @@ -574,9 +861,9 @@ class RestClient: :returns: The view, if successful. + :raises ForbiddenError: If something went wrong with the authorization. + :raises NotExistsError: If the container does not exist. :raises ResponseCodeError: If something went wrong with the retrieval. - :raises ForbiddenError: If the action is not allowed. - :raises NotExistsError: If thecontainer does not exist. """ url = f'/api/database/{database_id}/view/{view_id}' response = self._wrapper(method="get", url=url) @@ -587,7 +874,8 @@ class RestClient: raise ForbiddenError(f'Failed to find view: not allowed') if response.status_code == 404: raise NotExistsError(f'Failed to find view: not found') - raise ResponseCodeError(f'Failed to find view: response code: {response.status_code} is not 200 (OK)') + raise ResponseCodeError(f'Failed to find view: response code: {response.status_code} is not ' + f'200 (OK): {response.text}') def create_view(self, database_id: int, name: str, query: str, is_public: bool) -> View: """ @@ -601,9 +889,13 @@ class RestClient: :returns: The created view, if successful. + :raises MalformedError: If the payload is rejected by the service. + :raises ForbiddenError: If something went wrong with the authorization. + :raises NotExistsError: If the database does not exist. + :raises ExternalSystemError: If the mapped view creation query is erroneous. + :raises ServiceConnectionError: If something went wrong with connection to the search service. + :raises ServiceError: If something went wrong with obtaining the information in the search service. :raises ResponseCodeError: If something went wrong with the retrieval. - :raises ForbiddenError: If the action is not allowed. - :raises NotExistsError: If thecontainer does not exist. """ url = f'/api/database/{database_id}/view' response = self._wrapper(method="post", url=url, force_auth=True, @@ -611,13 +903,20 @@ class RestClient: if response.status_code == 201: body = response.json() return View.model_validate(body) - if response.status_code == 400 or response.status_code == 423: - raise MalformedError(f'Failed to create view: service rejected malformed payload') - if response.status_code == 403 or response.status_code == 405: + if response.status_code == 400: + raise MalformedError(f'Failed to create view: {response.text}') + if response.status_code == 403: raise ForbiddenError(f'Failed to create view: not allowed') if response.status_code == 404: raise NotExistsError(f'Failed to create view: not found') - raise ResponseCodeError(f'Failed to create view: response code: {response.status_code} is not 201 (CREATED)') + if response.status_code == 423: + raise ExternalSystemError(f'Failed to create view: mapped invalid query: {response.text}') + if response.status_code == 502: + raise ServiceConnectionError(f'Failed to create view: failed to establish connection to search service') + if response.status_code == 503: + raise ServiceError(f'Failed to create view: failed to save in search service') + raise ResponseCodeError(f'Failed to create view: response code: {response.status_code} is not ' + f'201 (CREATED): {response.text}') def delete_view(self, database_id: int, view_id: int) -> None: """ @@ -626,21 +925,32 @@ class RestClient: :param database_id: The database id. :param view_id: The view id. - :raises ResponseCodeError: If something went wrong with the retrieval. - :raises ForbiddenError: If the action is not allowed. - :raises NotExistsError: If thecontainer does not exist. + :raises MalformedError: If the payload is rejected by the service. + :raises ForbiddenError: If something went wrong with the authorization. + :raises NotExistsError: If the container does not exist. + :raises ExternalSystemError: If the mapped view deletion query is erroneous. + :raises ServiceConnectionError: If something went wrong with connection to the search service. + :raises ServiceError: If something went wrong with obtaining the information in the search service. + :raises ResponseCodeError: If something went wrong with the deletion. """ url = f'/api/database/{database_id}/view/{view_id}' response = self._wrapper(method="delete", url=url, force_auth=True) if response.status_code == 202: return - if response.status_code == 400 or response.status_code == 423: - raise MalformedError(f'Failed to delete view: service rejected malformed payload') - if response.status_code == 403 or response.status_code == 405: + if response.status_code == 400: + raise MalformedError(f'Failed to delete view: {response.text}') + if response.status_code == 403: raise ForbiddenError(f'Failed to delete view: not allowed') if response.status_code == 404: raise NotExistsError(f'Failed to delete view: not found') - raise ResponseCodeError(f'Failed to delete view: response code: {response.status_code} is not 202 (ACCEPTED)') + if response.status_code == 423: + raise ExternalSystemError(f'Failed to delete view: mapped invalid delete query') + if response.status_code == 502: + raise ServiceConnectionError(f'Failed to delete view: failed to establish connection to search service') + if response.status_code == 503: + raise ServiceError(f'Failed to delete view: failed to save in search service') + raise ResponseCodeError(f'Failed to delete view: response code: {response.status_code} is not ' + f'202 (ACCEPTED): {response.text}') def get_view_data(self, database_id: int, view_id: int, page: int = 0, size: int = 10, df: bool = False) -> Result | DataFrame: @@ -655,9 +965,12 @@ class RestClient: :returns: The result of the view query, if successful. - :raises ResponseCodeError: If something went wrong with the retrieval. - :raises ForbiddenError: If the action is not allowed. + :raises MalformedError: If the payload is rejected by the service. + :raises ForbiddenError: If something went wrong with the authorization. :raises NotExistsError: If the view does not exist. + :raises ExternalSystemError: If the mapped view selection query is erroneous. + :raises ServiceError: If something went wrong with obtaining the information in the metadata service. + :raises ResponseCodeError: If something went wrong with the retrieval. """ url = f'/api/database/{database_id}/view/{view_id}/data' params = [] @@ -672,12 +985,18 @@ class RestClient: return DataFrame.from_records(res.result) return res if response.status_code == 400: - raise MalformedError(f'Failed to get view data: service rejected malformed payload') + raise MalformedError(f'Failed to get view data: {response.text}') if response.status_code == 403: raise ForbiddenError(f'Failed to get view data: not allowed') if response.status_code == 404: raise NotExistsError(f'Failed to get view data: not found') - raise ResponseCodeError(f'Failed to get view data: response code: {response.status_code} is not 200 (OK)') + if response.status_code == 409: + raise ExternalSystemError(f'Failed to get view data: mapping failed: {response.text}') + if response.status_code == 503: + raise ServiceError(f'Failed to get view data: data service failed to establish connection to ' + f'metadata service') + raise ResponseCodeError(f'Failed to get view data: response code: {response.status_code} is not ' + f'200 (OK):{response.text}') def get_views_metadata(self, database_id: int) -> Database: """ @@ -685,9 +1004,9 @@ class RestClient: :param database_id: The database id. - :raises ResponseCodeError: If something went wrong with the retrieval. - :raises ForbiddenError: If the action is not allowed. + :raises ForbiddenError: If something went wrong with the authorization. :raises NotExistsError: If the container does not exist. + :raises ResponseCodeError: If something went wrong with the retrieval. """ url = f'/api/database/{database_id}/metadata/view' response = self._wrapper(method="put", url=url, force_auth=True) @@ -698,7 +1017,8 @@ class RestClient: raise ForbiddenError(f'Failed to get views metadata: not allowed') if response.status_code == 404: raise NotExistsError(f'Failed to get views metadata: not found') - raise ResponseCodeError(f'Failed to get views metadata: response code: {response.status_code} is not 200 (OK)') + raise ResponseCodeError(f'Failed to get views metadata: response code: {response.status_code} is not ' + f'200 (OK): {response.text}') def get_table_data(self, database_id: int, table_id: int, page: int = 0, size: int = 10, timestamp: datetime.datetime = None, df: bool = False) -> Result | DataFrame: @@ -714,10 +1034,11 @@ class RestClient: :returns: The result of the view query, if successful. - :raises ResponseCodeError: If something went wrong with the retrieval. - :raises ForbiddenError: If the action is not allowed. + :raises MalformedError: If the payload is rejected by the service. + :raises ForbiddenError: If something went wrong with the authorization. :raises NotExistsError: If the table does not exist. - :raises QueryStoreError: If the result set could not be counted. + :raises ServiceError: If something went wrong with obtaining the information in the metadata service. + :raises ResponseCodeError: If something went wrong with the retrieval. """ url = f'/api/database/{database_id}/table/{table_id}/data' params = [] @@ -734,14 +1055,16 @@ class RestClient: return DataFrame.from_records(res.result) return res if response.status_code == 400: - raise MalformedError(f'Failed to get table data: service rejected malformed payload') + raise MalformedError(f'Failed to get table data: {response.text}') if response.status_code == 403: raise ForbiddenError(f'Failed to get table data: not allowed') if response.status_code == 404: raise NotExistsError(f'Failed to get table data: not found') - if response.status_code == 409: - raise QueryStoreError(f'Failed to get table data: service rejected result count') - raise ResponseCodeError(f'Failed to get table data: response code: {response.status_code} is not 200 (OK)') + if response.status_code == 503: + raise ServiceError(f'Failed to get table data: data service failed to establish connection to ' + f'metadata service') + raise ResponseCodeError(f'Failed to get table data: response code: {response.status_code} is not ' + f'200 (OK): {response.text}') def create_table_data(self, database_id: int, table_id: int, data: dict) -> None: """ @@ -751,23 +1074,27 @@ class RestClient: :param table_id: The table id. :param data: The data dictionary to be inserted into the table with the form column=value of the table. - :raises ResponseCodeError: If something went wrong with the insert. - :raises ForbiddenError: If the action is not allowed. + :raises MalformedError: If the payload is rejected by the service (e.g. LOB could not be imported). + :raises ForbiddenError: If something went wrong with the authorization. :raises NotExistsError: If the table does not exist. - :raises MalformedError: If the payload is rejected by the service (e.g. LOB data could not be imported). + :raises ServiceError: If something went wrong with obtaining the information in the metadata service. + :raises ResponseCodeError: If something went wrong with the insert. """ url = f'/api/database/{database_id}/table/{table_id}/data' response = self._wrapper(method="post", url=url, force_auth=True, payload=CreateData(data=data)) if response.status_code == 201: return - if response.status_code == 400 or response.status_code == 410: - raise MalformedError(f'Failed to insert table data: service rejected malformed payload') + if response.status_code == 400: + raise MalformedError(f'Failed to insert table data: {response.text}') if response.status_code == 403: raise ForbiddenError(f'Failed to insert table data: not allowed') if response.status_code == 404: raise NotExistsError(f'Failed to insert table data: not found') - raise ResponseCodeError( - f'Failed to insert table data: response code: {response.status_code} is not 201 (CREATED)') + if response.status_code == 503: + raise ServiceError( + f'Failed to insert table data: data service failed to establish connection to metadata service') + raise ResponseCodeError(f'Failed to insert table data: response code: {response.status_code} is not ' + f'201 (CREATED): {response.text}') def import_table_data(self, database_id: int, table_id: int, separator: str, file_path: str, quote: str = None, skip_lines: int = 0, false_encoding: str = None, @@ -787,10 +1114,11 @@ class RestClient: :param null_encoding: The encoding of null. Optional. :param line_encoding: The encoding of the line termination. Optional. Default: CR (Windows). - :raises ResponseCodeError: If something went wrong with the insert. - :raises ForbiddenError: If the action is not allowed. + :raises MalformedError: If the payload is rejected by the service (e.g. LOB could not be imported). + :raises ForbiddenError: If something went wrong with the authorization. :raises NotExistsError: If the table does not exist. - :raises MalformedError: If the payload is rejected by the service (e.g. LOB data could not be imported). + :raises ServiceError: If something went wrong with obtaining the information in the metadata service. + :raises ResponseCodeError: If something went wrong with the insert. """ client = UploadClient(endpoint=f"{self.endpoint}/api/upload/files") filename = client.upload(file_path=file_path) @@ -803,15 +1131,16 @@ class RestClient: if response.status_code == 202: return if response.status_code == 400: - raise MalformedError(f'Failed to import table data: service rejected malformed payload') + raise MalformedError(f'Failed to import table data: {response.text}') if response.status_code == 403: raise ForbiddenError(f'Failed to import table data: not allowed') if response.status_code == 404: raise NotExistsError(f'Failed to import table data: not found') - if response.status_code == 409 or response.status_code == 422: - raise ExternalSystemError(f'Failed to import table data: sidecar rejected the import') - raise ResponseCodeError( - f'Failed to import table data: response code: {response.status_code} is not 202 (ACCEPTED)') + if response.status_code == 503: + raise ServiceError( + f'Failed to insert table data: data service failed to establish connection to metadata service') + raise ResponseCodeError(f'Failed to import table data: response code: {response.status_code} is not ' + f'202 (ACCEPTED): {response.text}') def analyse_datatypes(self, file_path: str, separator: str, enum: bool = None, enum_tol: int = None, upload: bool = True) -> DatatypeAnalysis: @@ -827,9 +1156,9 @@ class RestClient: :returns: The determined data types, if successful. - :raises ResponseCodeError: If something went wrong with the analysis. :raises MalformedError: If the payload is rejected by the service. :raises NotExistsError: If the file was not found by the Analyse Service. + :raises ResponseCodeError: If something went wrong with the analysis. """ if upload: client = UploadClient(endpoint=f"{self.endpoint}/api/upload/files") @@ -847,12 +1176,12 @@ class RestClient: if response.status_code == 202: body = response.json() return DatatypeAnalysis.model_validate(body) - if response.status_code == 400 or response.status_code == 500: - raise MalformedError(f'Failed to analyse data types: service rejected malformed payload') + if response.status_code == 400: + raise MalformedError(f'Failed to analyse data types: {response.text}') if response.status_code == 404: - raise NotExistsError(f'Failed to analyse data types: failed to find file in Storage Service') - raise ResponseCodeError( - f'Failed to analyse data types: response code: {response.status_code} is not 202 (ACCEPTED)') + raise NotExistsError(f'Failed to analyse data types: failed to find file in storage service') + raise ResponseCodeError(f'Failed to analyse data types: response code: {response.status_code} is not ' + f'202 (ACCEPTED): {response.text}') def analyse_keys(self, file_path: str, separator: str, upload: bool = True) -> KeyAnalysis: """ @@ -865,9 +1194,9 @@ class RestClient: :returns: The determined ranking of the primary key candidates, if successful. - :raises ResponseCodeError: If something went wrong with the analysis. :raises MalformedError: If the payload is rejected by the service. :raises NotExistsError: If the file was not found by the Analyse Service. + :raises ResponseCodeError: If something went wrong with the analysis. """ if upload: client = UploadClient(endpoint=f"{self.endpoint}/api/upload/files") @@ -883,12 +1212,12 @@ class RestClient: if response.status_code == 202: body = response.json() return KeyAnalysis.model_validate(body) - if response.status_code == 400 or response.status_code == 500: - raise MalformedError(f'Failed to analyse data types: service rejected malformed payload') + if response.status_code == 400: + raise MalformedError(f'Failed to analyse data keys: {response.text}') if response.status_code == 404: - raise NotExistsError(f'Failed to analyse data types: failed to find file in Storage Service') - raise ResponseCodeError( - f'Failed to analyse data types: response code: {response.status_code} is not 202 (ACCEPTED)') + raise NotExistsError(f'Failed to analyse data keys: failed to find file in Storage Service') + raise ResponseCodeError(f'Failed to analyse data types: response code: {response.status_code} is not ' + f'202 (ACCEPTED): {response.text}') def analyse_table_statistics(self, database_id: int, table_id: int) -> TableStatistics: """ @@ -899,9 +1228,11 @@ class RestClient: :returns: The table statistics, if successful. - :raises ResponseCodeError: If something went wrong with the analysis. :raises MalformedError: If the payload is rejected by the service. :raises NotExistsError: If the file was not found by the Analyse Service. + :raises ServiceConnectionError: If something went wrong with connection to the metadata service. + :raises ServiceError: If something went wrong with obtaining the information in the search service. + :raises ResponseCodeError: If something went wrong with the analysis. """ url = f'/api/analyse/database/{database_id}/table/{table_id}/statistics' response = self._wrapper(method="get", url=url) @@ -909,11 +1240,16 @@ class RestClient: body = response.json() return TableStatistics.model_validate(body) if response.status_code == 400: - raise MalformedError(f'Failed to analyse table statistics: service rejected malformed payload') + raise MalformedError(f'Failed to analyse table statistics: {response.text}') if response.status_code == 404: raise NotExistsError(f'Failed to analyse table statistics: separator error') - raise ResponseCodeError( - f'Failed to analyse table statistics: response code: {response.status_code} is not 202 (ACCEPTED)') + if response.status_code == 502: + raise NotExistsError( + f'Failed to analyse table statistics: data service failed to establish connection to metadata service') + if response.status_code == 503: + raise ServiceError(f'Failed to analyse table statistics: failed to save statistic in search service') + raise ResponseCodeError(f'Failed to analyse table statistics: response code: {response.status_code} is not ' + f'202 (ACCEPTED): {response.text}') def update_table_data(self, database_id: int, table_id: int, data: dict, keys: dict) -> None: """ @@ -924,23 +1260,27 @@ class RestClient: :param data: The data dictionary to be updated into the table with the form column=value of the table. :param keys: The key dictionary matching the rows in the form column=value. - :raises ResponseCodeError: If something went wrong with the update. - :raises ForbiddenError: If the action is not allowed. - :raises NotExistsError: If the table does not exist. :raises MalformedError: If the payload is rejected by the service (e.g. LOB data could not be imported). + :raises ForbiddenError: If something went wrong with the authorization. + :raises NotExistsError: If the table does not exist. + :raises ServiceError: If something went wrong with obtaining the information in the metadata service. + :raises ResponseCodeError: If something went wrong with the update. """ url = f'/api/database/{database_id}/table/{table_id}/data' response = self._wrapper(method="put", url=url, force_auth=True, payload=UpdateData(data=data, keys=keys)) if response.status_code == 202: return - if response.status_code == 400 or response.status_code == 410: - raise MalformedError(f'Failed to update table data: service rejected malformed payload') + if response.status_code == 400: + raise MalformedError(f'Failed to update table data: {response.text}') if response.status_code == 403: raise ForbiddenError(f'Failed to update table data: not allowed') if response.status_code == 404: raise NotExistsError(f'Failed to update table data: not found') - raise ResponseCodeError( - f'Failed to update table data: response code: {response.status_code} is not 202 (ACCEPTED)') + if response.status_code == 503: + raise ServiceError( + f'Failed to update table data: data service failed to establish connection to metadata service') + raise ResponseCodeError(f'Failed to update table data: response code: {response.status_code} is not ' + f'202 (ACCEPTED): {response.text}') def delete_table_data(self, database_id: int, table_id: int, keys: dict) -> None: """ @@ -950,23 +1290,27 @@ class RestClient: :param table_id: The table id. :param keys: The key dictionary matching the rows in the form column=value. - :raises ResponseCodeError: If something went wrong with the deletion. - :raises ForbiddenError: If the action is not allowed. - :raises NotExistsError: If the table does not exist. :raises MalformedError: If the payload is rejected by the service. + :raises ForbiddenError: If something went wrong with the authorization. + :raises NotExistsError: If the table does not exist. + :raises ServiceError: If something went wrong with obtaining the information in the metadata service. + :raises ResponseCodeError: If something went wrong with the deletion. """ url = f'/api/database/{database_id}/table/{table_id}/data' response = self._wrapper(method="delete", url=url, force_auth=True, payload=DeleteData(keys=keys)) if response.status_code == 202: return if response.status_code == 400: - raise MalformedError(f'Failed to delete table data: service rejected malformed payload') + raise MalformedError(f'Failed to delete table data: {response.text}') if response.status_code == 403: raise ForbiddenError(f'Failed to delete table data: not allowed') if response.status_code == 404: raise NotExistsError(f'Failed to delete table data: not found') - raise ResponseCodeError( - f'Failed to delete table data: response code: {response.status_code} is not 202 (ACCEPTED)') + if response.status_code == 503: + raise ServiceError( + f'Failed to update table data: data service failed to establish connection to metadata service') + raise ResponseCodeError(f'Failed to delete table data: response code: {response.status_code} is not ' + f'202 (ACCEPTED): {response.text}') def get_table_data_count(self, database_id: int, table_id: int, page: int = 0, size: int = 10, timestamp: datetime.datetime = None) -> int: @@ -981,10 +1325,12 @@ class RestClient: :returns: The result of the view query, if successful. - :raises ResponseCodeError: If something went wrong with the retrieval. - :raises ForbiddenError: If the action is not allowed. + :raises MalformedError: If the payload is rejected by the service. + :raises ForbiddenError: If something went wrong with the authorization. :raises NotExistsError: If the table does not exist. - :raises QueryStoreError: If the result set could not be counted. + :raises ExternalSystemError: If the mapped view selection query is erroneous. + :raises ServiceError: If something went wrong with obtaining the information in the metadata service. + :raises ResponseCodeError: If something went wrong with the retrieval. """ url = f'/api/database/{database_id}/table/{table_id}/data' if page is not None and size is not None: @@ -999,14 +1345,18 @@ class RestClient: if response.status_code == 200: return int(response.headers.get('X-Count')) if response.status_code == 400: - raise MalformedError(f'Failed to get table data: service rejected malformed payload') + raise MalformedError(f'Failed to count table data: {response.text}') if response.status_code == 403: - raise ForbiddenError(f'Failed to get table data: not allowed') + raise ForbiddenError(f'Failed to count table data: not allowed') if response.status_code == 404: - raise NotExistsError(f'Failed to get table data: not found') + raise NotExistsError(f'Failed to count table data: not found') if response.status_code == 409: - raise QueryStoreError(f'Failed to get table data: service rejected result count') - raise ResponseCodeError(f'Failed to get table data: response code: {response.status_code} is not 200 (OK)') + raise ExternalSystemError(f'Failed to count table data: mapping failed: {response.text}') + if response.status_code == 503: + raise ServiceError( + f'Failed to count table data: data service failed to establish connection to metadata service') + raise ResponseCodeError(f'Failed to count table data: response code: {response.status_code} is not ' + f'200 (OK): {response.text}') def get_view_data_count(self, database_id: int, view_id: int) -> int: """ @@ -1017,22 +1367,30 @@ class RestClient: :returns: The result count of the view query, if successful. - :raises ResponseCodeError: If something went wrong with the retrieval. :raises MalformedError: If the payload is rejected by the service. - :raises ForbiddenError: If the action is not allowed. - :raises NotExistsError: If thecontainer does not exist. + :raises ForbiddenError: If something went wrong with the authorization. + :raises NotExistsError: If the view does not exist. + :raises ExternalSystemError: If the mapped view selection query is erroneous. + :raises ServiceError: If something went wrong with obtaining the information in the metadata service. + :raises ResponseCodeError: If something went wrong with the retrieval. """ url = f'/api/database/{database_id}/view/{view_id}/data' response = self._wrapper(method="head", url=url) if response.status_code == 200: return int(response.headers.get('X-Count')) if response.status_code == 400: - raise MalformedError(f'Failed to get view data count: service rejected malformed payload') + raise MalformedError(f'Failed to count view data: {response.text}') if response.status_code == 403: - raise ForbiddenError(f'Failed to get view data count: not allowed') + raise ForbiddenError(f'Failed to count view data: not allowed') if response.status_code == 404: - raise NotExistsError(f'Failed to get view data count: not found') - raise ResponseCodeError(f'Failed to get view data count: response code: {response.status_code} is not 200 (OK)') + raise NotExistsError(f'Failed to count view data: not found') + if response.status_code == 409: + raise ExternalSystemError(f'Failed to count view data: mapping failed: {response.text}') + if response.status_code == 503: + raise ServiceError( + f'Failed to count view data: data service failed to establish connection to metadata service') + raise ResponseCodeError(f'Failed to count view data: response code: {response.status_code} is not ' + f'200 (OK): {response.text}') def get_database_access(self, database_id: int) -> AccessType: """ @@ -1042,9 +1400,9 @@ class RestClient: :returns: The access type, if successful. + :raises ForbiddenError: If something went wrong with the authorization. + :raises NotExistsError: If the container does not exist. :raises ResponseCodeError: If something went wrong with the retrieval. - :raises ForbiddenError: If the action is not allowed. - :raises NotExistsError: If thecontainer does not exist. """ url = f'/api/database/{database_id}/access' response = self._wrapper(method="get", url=url) @@ -1055,7 +1413,31 @@ class RestClient: raise ForbiddenError(f'Failed to get database access: not allowed') if response.status_code == 404: raise NotExistsError(f'Failed to get database access: not found') - raise ResponseCodeError(f'Failed to get database access: response code: {response.status_code} is not 200 (OK)') + raise ResponseCodeError(f'Failed to get database access: response code: {response.status_code} is not ' + f'200 (OK): {response.text}') + + def check_database_access(self, database_id: int) -> bool: + """ + Checks access of a view in a database with given database id and view id. + + :param database_id: The database id. + + :returns: The access type, if successful. + + :raises ForbiddenError: If something went wrong with the authorization. + :raises NotExistsError: If the container does not exist. + :raises ResponseCodeError: If something went wrong with the retrieval. + """ + url = f'/api/database/{database_id}/access' + response = self._wrapper(method="get", url=url) + if response.status_code == 200: + return True + if response.status_code == 403: + return False + if response.status_code == 404: + raise NotExistsError(f'Failed to check database access: not found') + raise ResponseCodeError(f'Failed to check database access: response code: {response.status_code} is not ' + f'200 (OK): {response.text}') def create_database_access(self, database_id: int, user_id: str, type: AccessType) -> AccessType: """ @@ -1067,10 +1449,12 @@ class RestClient: :returns: The access type, if successful. - :raises ResponseCodeError: If something went wrong with the retrieval. :raises MalformedError: If the payload is rejected by the service. - :raises ForbiddenError: If the action is not allowed. - :raises NotExistsError: If thedatabase or user does not exist. + :raises ForbiddenError: If something went wrong with the authorization. + :raises NotExistsError: If the database or user does not exist. + :raises ServiceConnectionError: If something went wrong with connection to the data service. + :raises ServiceError: If something went wrong with obtaining the information in the data service. + :raises ResponseCodeError: If something went wrong with the retrieval. """ url = f'/api/database/{database_id}/access/{user_id}' response = self._wrapper(method="post", url=url, force_auth=True, payload=CreateAccess(type=type)) @@ -1078,13 +1462,18 @@ class RestClient: body = response.json() return DatabaseAccess.model_validate(body).type if response.status_code == 400: - raise MalformedError(f'Failed to create database access: service rejected malformed payload') - if response.status_code == 403 or response.status_code == 405: + raise MalformedError(f'Failed to create database access: {response.text}') + if response.status_code == 403: raise ForbiddenError(f'Failed to create database access: not allowed') if response.status_code == 404: raise NotExistsError(f'Failed to create database access: not found') - raise ResponseCodeError( - f'Failed to create database access: response code: {response.status_code} is not 202 (ACCEPTED)') + if response.status_code == 502: + raise ServiceConnectionError( + f'Failed to create database access: failed to establish connection to data service') + if response.status_code == 503: + raise ServiceError(f'Failed to create database access: failed to create access in data service') + raise ResponseCodeError(f'Failed to create database access: response code: {response.status_code} is not ' + f'202 (ACCEPTED): {response.text}') def update_database_access(self, database_id: int, user_id: str, type: AccessType) -> AccessType: """ @@ -1096,10 +1485,12 @@ class RestClient: :returns: The access type, if successful. - :raises ResponseCodeError: If something went wrong with the retrieval. :raises MalformedError: If the payload is rejected by the service. - :raises ForbiddenError: If the action is not allowed. - :raises NotExistsError: If thedatabase or user does not exist. + :raises ForbiddenError: If something went wrong with the authorization. + :raises NotExistsError: If the database or user does not exist. + :raises ServiceConnectionError: If something went wrong with connection to the data service. + :raises ServiceError: If something went wrong with obtaining the information in the data service. + :raises ResponseCodeError: If something went wrong with the retrieval. """ url = f'/api/database/{database_id}/access/{user_id}' response = self._wrapper(method="put", url=url, force_auth=True, payload=UpdateAccess(type=type)) @@ -1107,13 +1498,18 @@ class RestClient: body = response.json() return DatabaseAccess.model_validate(body).type if response.status_code == 400: - raise MalformedError(f'Failed to update database access: service rejected malformed payload') - if response.status_code == 403 or response.status_code == 405: + raise MalformedError(f'Failed to update database access: {response.text}') + if response.status_code == 403: raise ForbiddenError(f'Failed to update database access: not allowed') if response.status_code == 404: raise NotExistsError(f'Failed to update database access: not found') - raise ResponseCodeError( - f'Failed to update database access: response code: {response.status_code} is not 202 (ACCEPTED)') + if response.status_code == 502: + raise ServiceConnectionError( + f'Failed to update database access: failed to establish connection to data service') + if response.status_code == 503: + raise ServiceError(f'Failed to update database access: failed to update access in data service') + raise ResponseCodeError(f'Failed to update database access: response code: {response.status_code} is not ' + f'202 (ACCEPTED): {response.text}') def delete_database_access(self, database_id: int, user_id: str) -> None: """ @@ -1122,25 +1518,32 @@ class RestClient: :param database_id: The database id. :param user_id: The user id. - :raises ResponseCodeError: If something went wrong with the retrieval. :raises MalformedError: If the payload is rejected by the service. - :raises ForbiddenError: If the action is not allowed. - :raises NotExistsError: If thedatabase or user does not exist. + :raises ForbiddenError: If something went wrong with the authorization. + :raises NotExistsError: If the database or user does not exist. + :raises ServiceConnectionError: If something went wrong with connection to the data service. + :raises ServiceError: If something went wrong with obtaining the information in the data service. + :raises ResponseCodeError: If something went wrong with the retrieval. """ url = f'/api/database/{database_id}/access/{user_id}' response = self._wrapper(method="delete", url=url, force_auth=True) if response.status_code == 202: return if response.status_code == 400: - raise MalformedError(f'Failed to delete database access: service rejected malformed payload') - if response.status_code == 403 or response.status_code == 405: + raise MalformedError(f'Failed to delete database access: {response.text}') + if response.status_code == 403: raise ForbiddenError(f'Failed to delete database access: not allowed') if response.status_code == 404: raise NotExistsError(f'Failed to delete database access: not found') - raise ResponseCodeError( - f'Failed to delete database access: response code: {response.status_code} is not 201 (CREATED)') + if response.status_code == 502: + raise ServiceConnectionError( + f'Failed to delete database access: failed to establish connection to data service') + if response.status_code == 503: + raise ServiceError(f'Failed to delete database access: failed to delete access in data service') + raise ResponseCodeError(f'Failed to delete database access: response code: {response.status_code} is not ' + f'201 (CREATED): {response.text}') - def execute_query(self, database_id: int, query: str, page: int = 0, size: int = 10, + def create_subset(self, database_id: int, query: str, page: int = 0, size: int = 10, timestamp: datetime.datetime = datetime.datetime.now()) -> Result: """ Executes a SQL query in a database where the current user has at least read access with given database id. The @@ -1151,16 +1554,17 @@ class RestClient: :param query: The query statement. :param page: The result pagination number. Optional. Default: 0. :param size: The result pagination size. Optional. Default: 10. - :param timestamp: The query execution time. Optional. + :param timestamp: The query execution time. Optional. Default: now. :returns: The result set, if successful. - :raises ResponseCodeError: If something went wrong with the retrieval. :raises MalformedError: If the payload is rejected by the service. - :raises ForbiddenError: If the action is not allowed. + :raises ForbiddenError: If something went wrong with the authorization. :raises NotExistsError: If the database, table or user does not exist. :raises QueryStoreError: The query store rejected the query. - :raises MetadataConsistencyError: The service failed to parse columns from the metadata database. + :raises FormatNotAvailable: The subset query contains non-supported keywords. + :raises ServiceError: If something went wrong with obtaining the information in the data service. + :raises ResponseCodeError: If something went wrong with the retrieval. """ url = f'/api/database/{database_id}/subset' if page is not None and size is not None: @@ -1171,25 +1575,27 @@ class RestClient: body = response.json() return Result.model_validate(body) if response.status_code == 400: - raise MalformedError(f'Failed to execute query: service rejected malformed payload') + raise MalformedError(f'Failed to create subset: {response.text}') if response.status_code == 403: - raise ForbiddenError(f'Failed to execute query: not allowed') + raise ForbiddenError(f'Failed to create subset: not allowed') if response.status_code == 404: - raise NotExistsError(f'Failed to execute query: not found') - if response.status_code == 409: - raise QueryStoreError(f'Failed to execute query: query store rejected query') + raise NotExistsError(f'Failed to create subset: not found') if response.status_code == 417: - raise MetadataConsistencyError(f'Failed to execute query: service expected other metadata') - raise ResponseCodeError( - f'Failed to execute query: response code: {response.status_code} is not 202 (ACCEPTED)') + raise QueryStoreError(f'Failed to create subset: query store rejected query') + if response.status_code == 501: + raise FormatNotAvailable(f'Failed to create subset: contains non-supported keywords: {response.text}') + if response.status_code == 503: + raise ServiceError(f'Failed to create subset: failed to establish connection with data database') + raise ResponseCodeError(f'Failed to create subset: response code: {response.status_code} is not ' + f'201 (CREATED): {response.text}') - def get_query_data(self, database_id: int, query_id: int, page: int = 0, size: int = 10, - df: bool = False) -> Result | DataFrame: + def get_subset_data(self, database_id: int, subset_id: int, page: int = 0, size: int = 10, + df: bool = False) -> Result | DataFrame: """ Re-executes a query in a database with given database id and query id. :param database_id: The database id. - :param query_id: The query id. + :param subset_id: The subset id. :param page: The result pagination number. Optional. Default: 0. :param size: The result pagination size. Optional. Default: 10. :param size: The result pagination size. Optional. Default: 10. @@ -1197,15 +1603,14 @@ class RestClient: :returns: The result set, if successful. - :raises ResponseCodeError: If something went wrong with the retrieval. :raises MalformedError: If the payload is rejected by the service. - :raises ForbiddenError: If the action is not allowed. + :raises ForbiddenError: If something went wrong with the authorization. :raises NotExistsError: If the database, query or user does not exist. - :raises QueryStoreError: The query store rejected the query. - :raises MetadataConsistencyError: The service failed to parse columns from the metadata database. + :raises ServiceError: If something went wrong with obtaining the information in the data service. + :raises ResponseCodeError: If something went wrong with the retrieval. """ headers = {} - url = f'/api/database/{database_id}/subset/{query_id}/data' + url = f'/api/database/{database_id}/subset/{subset_id}/data' if page is not None and size is not None: url += f'?page={page}&size={size}' response = self._wrapper(method="get", url=url, headers=headers) @@ -1216,85 +1621,80 @@ class RestClient: return DataFrame.from_records(res.result) return res if response.status_code == 400: - raise MalformedError(f'Failed to re-execute query: service rejected malformed payload') - if response.status_code == 403 or response.status_code == 405: - raise ForbiddenError(f'Failed to re-execute query: not allowed') + raise MalformedError(f'Failed to get query data: {response.text}') + if response.status_code == 403: + raise ForbiddenError(f'Failed to get query data: not allowed') if response.status_code == 404: - raise NotExistsError(f'Failed to re-execute query: not found') - if response.status_code == 409: - raise QueryStoreError(f'Failed to re-execute query: query store rejected query') - if response.status_code == 417: - raise MetadataConsistencyError(f'Failed to re-execute query: service expected other metadata') - raise ResponseCodeError( - f'Failed to re-execute query: response code: {response.status_code} is not 200 (OK)') + raise NotExistsError(f'Failed to get query data: not found') + if response.status_code == 503: + raise ServiceError(f'Failed to get query data: failed to establish connection with data database') + raise ResponseCodeError(f'Failed to get query data: response code: {response.status_code} is not ' + f'200 (OK): {response.text}') - def get_query_data_count(self, database_id: int, query_id: int, page: int = 0, size: int = 10) -> int: + def get_subset_data_count(self, database_id: int, subset_id: int, page: int = 0, size: int = 10) -> int: """ Re-executes a query in a database with given database id and query id and only counts the results. :param database_id: The database id. - :param query_id: The query id. + :param subset_id: The subset id. :param page: The result pagination number. Optional. Default: 0. :param size: The result pagination size. Optional. Default: 10. :returns: The result set, if successful. - :raises ResponseCodeError: If something went wrong with the retrieval. :raises MalformedError: If the payload is rejected by the service. - :raises ForbiddenError: If the action is not allowed. + :raises ForbiddenError: If something went wrong with the authorization. :raises NotExistsError: If the database, query or user does not exist. - :raises QueryStoreError: The query store rejected the query. - :raises MetadataConsistencyError: The service failed to parse columns from the metadata database. + :raises ServiceError: If something went wrong with obtaining the information in the data service. + :raises ResponseCodeError: If something went wrong with the retrieval. """ - url = f'/api/database/{database_id}/subset/{query_id}/data' + url = f'/api/database/{database_id}/subset/{subset_id}/data' if page is not None and size is not None: url += f'?page={page}&size={size}' response = self._wrapper(method="head", url=url) if response.status_code == 200: return int(response.headers.get('X-Count')) if response.status_code == 400: - raise MalformedError(f'Failed to re-execute query: service rejected malformed payload') - if response.status_code == 403 or response.status_code == 405: - raise ForbiddenError(f'Failed to re-execute query: not allowed') + raise MalformedError(f'Failed to get query count: {response.text}') + if response.status_code == 403: + raise ForbiddenError(f'Failed to get query count: not allowed') if response.status_code == 404: - raise NotExistsError(f'Failed to re-execute query: not found') - if response.status_code == 409: - raise QueryStoreError(f'Failed to re-execute query: query store rejected query') - if response.status_code == 417: - raise MetadataConsistencyError(f'Failed to re-execute query: service expected other metadata') + raise NotExistsError(f'Failed to get query count: not found') + if response.status_code == 503: + raise ServiceError(f'Failed to get query count: failed to establish connection with data database') raise ResponseCodeError( - f'Failed to re-execute query: response code: {response.status_code} is not 200 (OK)') + f'Failed to get query count: response code: {response.status_code} is not 200 (OK)') - def get_query(self, database_id: int, query_id: int) -> Query: + def get_subset(self, database_id: int, subset_id: int) -> Query: """ Get query from a database with given database id and query id. :param database_id: The database id. - :param query_id: The query id. + :param subset_id: The subset id. :returns: The query, if successful. + :raises ForbiddenError: If something went wrong with the authorization. + :raises NotExistsError: If the database, query or user does not exist. + :raises FormatNotAvailable: If the service could not represent the output. + :raises ServiceError: If something went wrong with obtaining the information in the data service. :raises ResponseCodeError: If something went wrong with the retrieval. - :raises ForbiddenError: If the action is not allowed. - :raises NotExistsError: If thedatabase, query or user does not exist. - :raises QueryStoreError: The query store rejected the query. - :raises MetadataConsistencyError: The service failed to parse columns from the metadata database. """ - url = f'/api/database/{database_id}/subset/{query_id}' + url = f'/api/database/{database_id}/subset/{subset_id}' response = self._wrapper(method="get", url=url) if response.status_code == 200: body = response.json() return Query.model_validate(body) + if response.status_code == 403: + raise ForbiddenError(f'Failed to find subset: not allowed') if response.status_code == 404: - raise NotExistsError(f'Failed to find query: not found') - if response.status_code == 403 or response.status_code == 405: - raise ForbiddenError(f'Failed to find query: not allowed') - if response.status_code == 417: - raise MetadataConsistencyError(f'Failed to find query: service expected other metadata') - if response.status_code == 501 or response.status_code == 503 or response.status_code == 504: - raise QueryStoreError(f'Failed to find query: query store rejected query') - raise ResponseCodeError( - f'Failed to find query: response code: {response.status_code} is not 200 (OK)') + raise NotExistsError(f'Failed to find subset: not found') + if response.status_code == 406: + raise FormatNotAvailable(f'Failed to find subset: failed to provide acceptable representation') + if response.status_code == 503: + raise ServiceError(f'Failed to find subset: failed to establish connection with data database') + raise ResponseCodeError(f'Failed to find subset: response code: {response.status_code} is not ' + f'200 (OK): {response.text}') def get_queries(self, database_id: int) -> List[Query]: """ @@ -1304,63 +1704,66 @@ class RestClient: :returns: List of queries, if successful. + :raises ForbiddenError: If something went wrong with the authorization. + :raises NotExistsError: If the database or user does not exist. + :raises ServiceError: If something went wrong with obtaining the information in the data service. :raises ResponseCodeError: If something went wrong with the retrieval. - :raises MalformedError: If the query is rejected by the service. - :raises ForbiddenError: If the action is not allowed. - :raises NotExistsError: If thedatabase or user does not exist. - :raises QueryStoreError: The query store rejected the query. """ url = f'/api/database/{database_id}/subset' response = self._wrapper(method="get", url=url) if response.status_code == 200: body = response.json() return TypeAdapter(List[Query]).validate_python(body) - if response.status_code == 403 or response.status_code == 405: + if response.status_code == 403: raise ForbiddenError(f'Failed to find queries: not allowed') if response.status_code == 404: raise NotExistsError(f'Failed to find queries: not found') - if response.status_code == 423: - raise MalformedError(f'Failed to find queries: service rejected malformed query') - if response.status_code == 501 or response.status_code == 503 or response.status_code == 504: - raise QueryStoreError(f'Failed to find queries: query store rejected query') - raise ResponseCodeError( - f'Failed to find query: response code: {response.status_code} is not 200 (OK)') + if response.status_code == 503: + raise ServiceError(f'Failed to find queries: failed to establish connection with data database') + raise ResponseCodeError(f'Failed to find query: response code: {response.status_code} is not ' + f'200 (OK): {response.text}') - def update_query(self, database_id: int, query_id: int, persist: bool) -> Query: + def update_subset(self, database_id: int, subset_id: int, persist: bool) -> Query: """ - Update query from a database with given database id and query id. + Save query or mark it for deletion (at a later time) in a database with given database id and query id. :param database_id: The database id. - :param query_id: The query id. - :param persist: If set to true, the query will be saved and visible in the User Interface, otherwise the query \ - is marked for deletion in the future and not visible in the User Interface. + :param subset_id: The subset id. + :param persist: If set to true, the query will be saved and visible in the user interface, otherwise the query \ + is marked for deletion in the future and not visible in the user interface. :returns: The query, if successful. - :raises ResponseCodeError: If something went wrong with the retrieval. - :raises ForbiddenError: If the action is not allowed. - :raises NotExistsError: If thedatabase or user does not exist. + :raises MalformedError: If the payload is rejected by the service. + :raises ForbiddenError: If something went wrong with the authorization. + :raises NotExistsError: If the database or user does not exist. :raises QueryStoreError: The query store rejected the update. + :raises ServiceError: If something went wrong with obtaining the information in the data service. + :raises ResponseCodeError: If something went wrong with the retrieval. """ - url = f'/api/database/{database_id}/subset/{query_id}' + url = f'/api/database/{database_id}/subset/{subset_id}' response = self._wrapper(method="put", url=url, force_auth=True, payload=UpdateQuery(persist=persist)) if response.status_code == 202: body = response.json() return Query.model_validate(body) - if response.status_code == 403 or response.status_code == 405: + if response.status_code == 400: + raise MalformedError(f'Failed to update query: {response.text}') + if response.status_code == 403: raise ForbiddenError(f'Failed to update query: not allowed') if response.status_code == 404: raise NotExistsError(f'Failed to update query: not found') - if response.status_code == 412: + if response.status_code == 417: raise QueryStoreError(f'Failed to update query: query store rejected update') - raise ResponseCodeError( - f'Failed to update query: response code: {response.status_code} is not 200 (OK)') + if response.status_code == 503: + raise ServiceError(f'Failed to update query: failed to establish connection with data database') + raise ResponseCodeError(f'Failed to update query: response code: {response.status_code} is not ' + f'202 (ACCEPTED): {response.text}') def create_identifier(self, database_id: int, type: IdentifierType, titles: List[CreateIdentifierTitle], publisher: str, creators: List[CreateIdentifierCreator], publication_year: int, descriptions: List[CreateIdentifierDescription] = None, funders: List[CreateIdentifierFunder] = None, licenses: List[License] = None, - language: Language = None, query_id: int = None, view_id: int = None, table_id: int = None, + language: Language = None, subset_id: int = None, view_id: int = None, table_id: int = None, publication_day: int = None, publication_month: int = None, related_identifiers: List[CreateRelatedIdentifier] = None) -> Identifier: """ @@ -1376,7 +1779,7 @@ class RestClient: :param funders: The funders(s) of the created identifier. Optional. :param licenses: The license(s) of the created identifier. Optional. :param language: The language of the created identifier. Optional. - :param query_id: The query id of the created identifier. Required when type=SUBSET, otherwise invalid. Optional. + :param subset_id: The subset id of the created identifier. Required when type=SUBSET, otherwise invalid. Optional. :param view_id: The view id of the created identifier. Required when type=VIEW, otherwise invalid. Optional. :param table_id: The table id of the created identifier. Required when type=TABLE, otherwise invalid. Optional. :param publication_day: The publication day of the created identifier. Optional. @@ -1385,16 +1788,17 @@ class RestClient: :returns: The identifier, if successful. - :raises ResponseCodeError: If something went wrong with the creation of the identifier. - :raises ForbiddenError: If the action is not allowed. :raises MalformedError: If the payload is rejected by the service. + :raises ForbiddenError: If something went wrong with the authorization. :raises NotExistsError: If the database, table/view/subset or user does not exist. - :raises ExternalSystemError: If the external system (DataCite) refused communication with the service. + :raises ServiceConnectionError: If something went wrong with connection to the search service. + :raises ServiceError: If something went wrong with obtaining the information in the search service. + :raises ResponseCodeError: If something went wrong with the creation of the identifier. """ url = f'/api/identifier' payload = CreateIdentifier(database_id=database_id, type=type, titles=titles, publisher=publisher, creators=creators, publication_year=publication_year, descriptions=descriptions, - funders=funders, licenses=licenses, language=language, query_id=query_id, + funders=funders, licenses=licenses, language=language, subset_id=subset_id, view_id=view_id, table_id=table_id, publication_day=publication_day, publication_month=publication_month, related_identifiers=related_identifiers) response = self._wrapper(method="post", url=url, force_auth=True, payload=payload) @@ -1402,21 +1806,24 @@ class RestClient: body = response.json() return Identifier.model_validate(body) if response.status_code == 400: - raise MalformedError(f'Failed to create identifier: service rejected malformed payload') - if response.status_code == 403 or response.status_code == 405: + raise MalformedError(f'Failed to create identifier: {response.text}') + if response.status_code == 403: raise ForbiddenError(f'Failed to create identifier: not allowed') if response.status_code == 404: raise NotExistsError(f'Failed to create identifier: not found') + if response.status_code == 502: + raise ServiceConnectionError( + f'Failed to create identifier: failed to establish connection with search service') if response.status_code == 503: - raise ExternalSystemError(f'Failed to create identifier: external system rejected communication') - raise ResponseCodeError( - f'Failed to create identifier: response code: {response.status_code} is not 201 (CREATED)') + raise ServiceError(f'Failed to create identifier: failed to save in search service') + raise ResponseCodeError(f'Failed to create identifier: response code: {response.status_code} is not ' + f'201 (CREATED): {response.text}') def save_identifier(self, identifier_id: int, database_id: int, type: IdentifierType, titles: List[CreateIdentifierTitle], publisher: str, creators: List[CreateIdentifierCreator], publication_year: int, descriptions: List[CreateIdentifierDescription] = None, funders: List[CreateIdentifierFunder] = None, licenses: List[License] = None, - language: Language = None, query_id: int = None, view_id: int = None, table_id: int = None, + language: Language = None, subset_id: int = None, view_id: int = None, table_id: int = None, publication_day: int = None, publication_month: int = None, related_identifiers: List[CreateRelatedIdentifier] = None) -> Identifier: """ @@ -1433,7 +1840,7 @@ class RestClient: :param funders: The funders(s) of the created identifier. Optional. :param licenses: The license(s) of the created identifier. Optional. :param language: The language of the created identifier. Optional. - :param query_id: The query id of the created identifier. Required when type=SUBSET, otherwise invalid. Optional. + :param subset_id: The subset id of the created identifier. Required when type=SUBSET, otherwise invalid. Optional. :param view_id: The view id of the created identifier. Required when type=VIEW, otherwise invalid. Optional. :param table_id: The table id of the created identifier. Required when type=TABLE, otherwise invalid. Optional. :param publication_day: The publication day of the created identifier. Optional. @@ -1442,32 +1849,36 @@ class RestClient: :returns: The identifier, if successful. - :raises ResponseCodeError: If something went wrong with the creation of the identifier. - :raises ForbiddenError: If the action is not allowed. :raises MalformedError: If the payload is rejected by the service. + :raises ForbiddenError: If something went wrong with the authorization. :raises NotExistsError: If the database, table/view/subset or user does not exist. - :raises ExternalSystemError: If the external system (DataCite) refused communication with the service. + :raises ServiceConnectionError: If something went wrong with connection to the search service. + :raises ServiceError: If something went wrong with obtaining the information in the search service. + :raises ResponseCodeError: If something went wrong with the creation of the identifier. """ url = f'/api/identifier/{identifier_id}' payload = CreateIdentifier(database_id=database_id, type=type, titles=titles, publisher=publisher, creators=creators, publication_year=publication_year, descriptions=descriptions, - funders=funders, licenses=licenses, language=language, query_id=query_id, + funders=funders, licenses=licenses, language=language, subset_id=subset_id, view_id=view_id, table_id=table_id, publication_day=publication_day, publication_month=publication_month, related_identifiers=related_identifiers) response = self._wrapper(method="put", url=url, force_auth=True, payload=payload) - if response.status_code == 201: + if response.status_code == 202: body = response.json() return Identifier.model_validate(body) if response.status_code == 400: - raise MalformedError(f'Failed to save identifier: service rejected malformed payload') - if response.status_code == 403 or response.status_code == 405: + raise MalformedError(f'Failed to save identifier: {response.text}') + if response.status_code == 403: raise ForbiddenError(f'Failed to save identifier: not allowed') if response.status_code == 404: raise NotExistsError(f'Failed to save identifier: not found') + if response.status_code == 502: + raise ServiceConnectionError( + f'Failed to save identifier: failed to establish connection with search service') if response.status_code == 503: - raise ExternalSystemError(f'Failed to save identifier: external system rejected communication') - raise ResponseCodeError( - f'Failed to save identifier: response code: {response.status_code} is not 202 (ACCEPTED)') + raise ServiceError(f'Failed to save identifier: failed to update in search service') + raise ResponseCodeError(f'Failed to save identifier: response code: {response.status_code} is not ' + f'202 (ACCEPTED): {response.text}') def publish_identifier(self, identifier_id: int) -> Identifier: """ @@ -1477,87 +1888,102 @@ class RestClient: :returns: The identifier, if successful. - :raises ResponseCodeError: If something went wrong with the creation of the identifier. - :raises ForbiddenError: If the action is not allowed. :raises MalformedError: If the payload is rejected by the service. + :raises ForbiddenError: If something went wrong with the authorization. :raises NotExistsError: If the database, table/view/subset or user does not exist. - :raises ExternalSystemError: If the external system (DataCite) refused communication with the service. + :raises ServiceConnectionError: If something went wrong with connection to the search service. + :raises ServiceError: If something went wrong with obtaining the information in the search service. + :raises ResponseCodeError: If something went wrong with the creation of the identifier. """ url = f'/api/identifier/{identifier_id}/publish' response = self._wrapper(method="put", url=url, force_auth=True) - if response.status_code == 201: + if response.status_code == 202: body = response.json() return Identifier.model_validate(body) if response.status_code == 400: - raise MalformedError(f'Failed to publish identifier: service rejected malformed payload') - if response.status_code == 403 or response.status_code == 405: + raise MalformedError(f'Failed to publish identifier: {response.text}') + if response.status_code == 403: raise ForbiddenError(f'Failed to publish identifier: not allowed') if response.status_code == 404: raise NotExistsError(f'Failed to publish identifier: not found') + if response.status_code == 502: + raise ServiceConnectionError( + f'Failed to publish identifier: failed to establish connection with search service') if response.status_code == 503: - raise ExternalSystemError(f'Failed to publish identifier: external system rejected communication') - raise ResponseCodeError( - f'Failed to publish identifier: response code: {response.status_code} is not 201 (CREATED)') + raise ServiceError(f'Failed to publish identifier: failed to update in search service') + raise ResponseCodeError(f'Failed to publish identifier: response code: {response.status_code} is not ' + f'202 (ACCEPTED): {response.text}') - def suggest_identifier(self, uri: str) -> Identifier: + def get_licenses(self) -> List[License]: """ - Suggest identifier metadata for a given identifier URI. Example: ROR, ORCID, ISNI, GND, DOI. - - :param uri: The identifier URI. - - :returns: The identifier, if successful. + Get list of licenses allowed. - :raises ResponseCodeError: If something went wrong with the suggestion of the identifier. - :raises NotExistsError: If no metadata can be found or the identifier type is not supported. + :returns: List of licenses, if successful. """ - url = f'/api/identifier?url={uri}' + url = f'/api/database/license' response = self._wrapper(method="get", url=url) if response.status_code == 200: body = response.json() - return Identifier.model_validate(body) - if response.status_code == 404: - raise NotExistsError(f'Failed to suggest identifier: not found or not supported') - raise ResponseCodeError(f'Failed to suggest identifier: response code: {response.status_code} is not 200 (OK)') + return TypeAdapter(List[License]).validate_python(body) + raise ResponseCodeError(f'Failed to get licenses: response code: {response.status_code} is not ' + f'200 (OK): {response.text}') - def get_licenses(self) -> List[License]: + def get_concepts(self) -> List[Concept]: """ - Get list of licenses allowed. + Get list of concepts known to the metadata database. - :returns: List of licenses, if successful. + :returns: List of concepts, if successful. """ - url = f'/api/database/license' + url = f'/api/concept' response = self._wrapper(method="get", url=url) if response.status_code == 200: body = response.json() - return TypeAdapter(List[License]).validate_python(body) - raise ResponseCodeError(f'Failed to get licenses: response code: {response.status_code} is not 200 (OK)') + return TypeAdapter(List[Concept]).validate_python(body) + raise ResponseCodeError(f'Failed to get concepts: response code: {response.status_code} is not ' + f'200 (OK): {response.text}') - def get_identifiers(self, ld: bool = False) -> List[Identifier] | str: + def get_identifiers(self, database_id: int = None, subset_id: int = None, view_id: int = None, + table_id: int = None) -> List[Identifier] | str: """ - Get list of identifiers. + Get list of identifiers, filter by the remaining optional arguments. - :param ld: If set to true, identifiers are requested as JSON-LD. Optional. Default: false. + :param database_id: The database id. Optional. + :param subset_id: The subset id. Optional. Requires `database_id` to be set. + :param view_id: The view id. Optional. Requires `database_id` to be set. + :param table_id: The table id. Optional. Requires `database_id` to be set. :returns: List of identifiers, if successful. - :raises ResponseCodeError: If something went wrong with the retrieval of the identifiers. :raises NotExistsError: If the accept header is neither application/json nor application/ld+json. + :raises FormatNotAvailable: If the service could not represent the output. + :raises ResponseCodeError: If something went wrong with the retrieval of the identifiers. """ - url = f'/api/pid' - headers = None - if ld: - headers = {'Accept': 'application/ld+json'} - response = self._wrapper(method="get", url=url, headers=headers) + url = f'/api/identifiers' + if database_id is not None: + url += f'?dbid={database_id}' + if subset_id is not None: + if database_id is None: + raise RequestError(f'Filtering by subset_id requires database_id to be set') + url += f'&qid={subset_id}' + if view_id is not None: + if database_id is None: + raise RequestError(f'Filtering by view_id requires database_id to be set') + url += f'&vid={view_id}' + if table_id is not None: + if database_id is None: + raise RequestError(f'Filtering by table_id requires database_id to be set') + url += f'&tid={table_id}' + response = self._wrapper(method="get", url=url, headers={'Accept': 'application/json'}) if response.status_code == 200: - if ld: - return response.json() - else: - body = response.json() - return TypeAdapter(List[Identifier]).validate_python(body) + body = response.json() + return TypeAdapter(List[Identifier]).validate_python(body) + if response.status_code == 404: + raise NotExistsError(f'Failed to get identifiers: requested style is not known') if response.status_code == 406: raise MalformedError( f'Failed to get identifiers: accept header must be application/json or application/ld+json') - raise ResponseCodeError(f'Failed to get identifiers: response code: {response.status_code} is not 200 (OK)') + raise ResponseCodeError(f'Failed to get identifiers: response code: {response.status_code} is not ' + f'200 (OK): {response.text}') def update_table_column(self, database_id: int, table_id: int, column_id: int, concept_uri: str = None, unit_uri: str = None) -> Column: @@ -1572,8 +1998,12 @@ class RestClient: :returns: The column, if successful. - :raises ResponseCodeError: If something went wrong with the retrieval of the identifiers. + :raises MalformedError: If the payload is rejected by the service. + :raises ForbiddenError: If something went wrong with the authorization. :raises NotExistsError: If the accept header is neither application/json nor application/ld+json. + :raises ServiceConnectionError: If something went wrong with connection to the search service. + :raises ServiceError: If something went wrong with obtaining the information in the search service. + :raises ResponseCodeError: If something went wrong with the retrieval of the identifiers. """ url = f'/api/database/{database_id}/table/{table_id}/column/{column_id}' response = self._wrapper(method="put", url=url, force_auth=True, @@ -1582,9 +2012,13 @@ class RestClient: body = response.json() return Column.model_validate(body) if response.status_code == 400: - raise MalformedError(f'Failed to update column: service rejected malformed payload') + raise MalformedError(f'Failed to update column: {response.text}') if response.status_code == 403: raise ForbiddenError(f'Failed to update colum: not allowed') if response.status_code == 404: raise NotExistsError(f'Failed to update colum: not found') + if response.status_code == 502: + raise ServiceConnectionError(f'Failed to update colum: failed to establish connection to search service') + if response.status_code == 503: + raise ServiceError(f'Failed to update colum: failed to save in search service') raise ResponseCodeError(f'Failed to update colum: response code: {response.status_code} is not 202 (ACCEPTED)') diff --git a/lib/python/dbrepo/api/dto.py b/lib/python/dbrepo/api/dto.py index 5eae072f35..4656a5220d 100644 --- a/lib/python/dbrepo/api/dto.py +++ b/lib/python/dbrepo/api/dto.py @@ -56,6 +56,19 @@ class CreateDatabase(BaseModel): is_public: bool +class CreateContainer(BaseModel): + name: str + host: str + image_id: int + sidecar_host: str + sidecar_port: int + privileged_username: str + privileged_password: str + ui_host: Optional[str] = None + ui_port: Optional[int] = None + port: Optional[int] = None + + class CreateUser(BaseModel): username: str email: str diff --git a/lib/python/dbrepo/api/exceptions.py b/lib/python/dbrepo/api/exceptions.py index a606a4fc7b..9aeb83d93b 100644 --- a/lib/python/dbrepo/api/exceptions.py +++ b/lib/python/dbrepo/api/exceptions.py @@ -61,6 +61,13 @@ class MetadataConsistencyError(Exception): pass +class FormatNotAvailable(Exception): + """ + The service cannot provide the result in the requested representation. + """ + pass + + class ExternalSystemError(Exception): """ The service could not communicate with the external system. @@ -75,8 +82,29 @@ class AuthenticationError(Exception): pass +class ServiceConnectionError(Exception): + """ + The service failed to establish connection. + """ + pass + + +class ServiceError(Exception): + """ + The service failed to perform the requested action. + """ + pass + + class UploadError(Exception): """ The upload was not successful. """ pass + + +class RequestError(Exception): + """ + The request cannot be sent. + """ + pass diff --git a/lib/python/tests/test_unit_identifier.py b/lib/python/tests/test_unit_identifier.py index b64816731d..2832f0e799 100644 --- a/lib/python/tests/test_unit_identifier.py +++ b/lib/python/tests/test_unit_identifier.py @@ -9,8 +9,7 @@ from dbrepo.api.dto import Identifier, IdentifierType, CreateIdentifierTitle, Cr IdentifierCreator, IdentifierTitle, IdentifierDescription, CreateIdentifierDescription, Language, \ CreateIdentifierFunder, CreateRelatedIdentifier, RelatedIdentifierRelation, RelatedIdentifierType, IdentifierFunder, \ RelatedIdentifier, UserBrief, IdentifierStatusType -from dbrepo.api.exceptions import MalformedError, ForbiddenError, NotExistsError, ExternalSystemError, \ - AuthenticationError +from dbrepo.api.exceptions import MalformedError, ForbiddenError, NotExistsError, AuthenticationError class IdentifierUnitTest(unittest.TestCase): @@ -100,22 +99,6 @@ class IdentifierUnitTest(unittest.TestCase): except NotExistsError: pass - def test_create_identifier_not_found_fails(self): - with requests_mock.Mocker() as mock: - # mock - mock.post('/api/identifier', status_code=503) - # test - try: - client = RestClient(username="a", password="b") - response = client.create_identifier( - database_id=1, type=IdentifierType.VIEW, - titles=[CreateIdentifierTitle(title='Test Title')], - descriptions=[CreateIdentifierDescription(description='Test')], - publisher='TU Wien', publication_year=2024, - creators=[CreateIdentifierCreator(creator_name='Carberry, Josiah')]) - except ExternalSystemError: - pass - def test_create_identifier_not_auth_fails(self): with requests_mock.Mocker() as mock: # mock @@ -131,38 +114,6 @@ class IdentifierUnitTest(unittest.TestCase): except AuthenticationError: pass - def test_suggest_identifier_succeeds(self): - with requests_mock.Mocker() as mock: - exp = Identifier(id=10, - database_id=1, - publication_year=2024, - publisher='TU Wien', - titles=[IdentifierTitle(id=10, title='Test Title')], - descriptions=[IdentifierDescription(id=10, description='Test')], - created=datetime.datetime(2024, 1, 1, 0, 0, 0, 0, datetime.timezone.utc), - last_modified=datetime.datetime(2024, 1, 1, 0, 0, 0, 0, datetime.timezone.utc), - type=IdentifierType.VIEW, - creators=[IdentifierCreator(id=5, creator_name='Carberry, Josiah', - name_identifier='https://orcid.org/0000-0002-1825-0097')], - status=IdentifierStatusType.DRAFT, - creator=UserBrief(id='8638c043-5145-4be8-a3e4-4b79991b0a16', username='mweise') - ) - # mock - mock.get('/api/identifier?url=https://orcid.org/0000-0002-1825-0097', json=exp.model_dump()) - # test - response = RestClient().suggest_identifier("https://orcid.org/0000-0002-1825-0097") - self.assertEqual(exp, response) - - def test_suggest_identifier_not_found_fails(self): - with requests_mock.Mocker() as mock: - # mock - mock.get('/api/identifier?url=https://orcid.org/0000-0002-1825-0097', status_code=404) - # test - try: - response = RestClient().suggest_identifier("https://orcid.org/0000-0002-1825-0097") - except NotExistsError: - pass - def test_get_identifiers_succeeds(self): with requests_mock.Mocker() as mock: exp = [Identifier(id=10, @@ -184,25 +135,11 @@ class IdentifierUnitTest(unittest.TestCase): status=IdentifierStatusType.PUBLISHED, creator=UserBrief(id='8638c043-5145-4be8-a3e4-4b79991b0a16', username='mweise'))] # mock - mock.get('/api/pid', json=[exp[0].model_dump()], headers={"Accept": "application/json"}) + mock.get('/api/identifiers', json=[exp[0].model_dump()], headers={"Accept": "application/json"}) # test response = RestClient().get_identifiers() self.assertEqual(exp, response) - def test_get_identifiers_ld_json_succeeds(self): - with requests_mock.Mocker() as mock: - exp = [{"@context": "https://schema.org/", "@type": "Dataset", "url": "http://localhost/database/2/info", - "citation": "http://localhost/pid/2", "hasPart": [], "version": "2024-03-21T12:05:46.000Z", - "name": "sdfsdf", "description": "sfsdf", "identifier": ["http://localhost/pid/2"], - "license": "https://creativecommons.org/licenses/by/4.0/legalcode", "creator": [ - {"name": "Weise, Martin", "@type": "Person", "sameAs": "https://orcid.org/0000-0003-4216-302X", - "givenName": "Martin", "familyName": "Weise"}], "temporalCoverage": 2024}] - # mock - mock.get('/api/pid', json=exp, headers={"Accept": "application/ld+json"}) - # test - response = RestClient().get_identifiers(ld=True) - self.assertEqual(exp, response) - if __name__ == "__main__": unittest.main() diff --git a/lib/python/tests/test_unit_query.py b/lib/python/tests/test_unit_query.py index 628a0914b2..d2de6f8278 100644 --- a/lib/python/tests/test_unit_query.py +++ b/lib/python/tests/test_unit_query.py @@ -15,7 +15,7 @@ from dbrepo.api.exceptions import MalformedError, NotExistsError, ForbiddenError class QueryUnitTest(unittest.TestCase): - def test_execute_query_succeeds(self): + def test_create_subset_succeeds(self): with requests_mock.Mocker() as mock: exp = Result(result=[{'id': 1, 'username': 'foo'}, {'id': 2, 'username': 'bar'}], headers=[{'id': 0, 'username': 1}], @@ -24,77 +24,53 @@ class QueryUnitTest(unittest.TestCase): mock.post('/api/database/1/subset', json=exp.model_dump(), status_code=201) # test client = RestClient(username="a", password="b") - response = client.execute_query(database_id=1, page=0, size=10, + response = client.create_subset(database_id=1, page=0, size=10, query="SELECT id, username FROM some_table WHERE id IN (1,2)") self.assertEqual(exp, response) - def test_execute_query_malformed_fails(self): + def test_create_subset_malformed_fails(self): with requests_mock.Mocker() as mock: # mock mock.post('/api/database/1/subset', status_code=400) # test try: client = RestClient(username="a", password="b") - response = client.execute_query(database_id=1, + response = client.create_subset(database_id=1, query="SELECT id, username FROM some_table WHERE id IN (1,2)") except MalformedError: pass - def test_execute_query_not_allowed_fails(self): + def test_create_subset_not_allowed_fails(self): with requests_mock.Mocker() as mock: # mock mock.post('/api/database/1/subset', status_code=403) # test try: client = RestClient(username="a", password="b") - response = client.execute_query(database_id=1, + response = client.create_subset(database_id=1, query="SELECT id, username FROM some_table WHERE id IN (1,2)") except ForbiddenError: pass - def test_execute_query_not_found_fails(self): + def test_create_subset_not_found_fails(self): with requests_mock.Mocker() as mock: # mock mock.post('/api/database/1/subset', status_code=404) # test try: client = RestClient(username="a", password="b") - response = client.execute_query(database_id=1, + response = client.create_subset(database_id=1, query="SELECT id, username FROM some_table WHERE id IN (1,2)") except NotExistsError: pass - def test_execute_query_not_valid_fails(self): - with requests_mock.Mocker() as mock: - # mock - mock.post('/api/database/1/subset', status_code=409) - # test - try: - client = RestClient(username="a", password="b") - response = client.execute_query(database_id=1, - query="SELECT id, username FROM some_table WHERE id IN (1,2)") - except QueryStoreError: - pass - - def test_execute_query_not_expected_fails(self): - with requests_mock.Mocker() as mock: - # mock - mock.post('/api/database/1/subset', status_code=417) - # test - try: - client = RestClient(username="a", password="b") - response = client.execute_query(database_id=1, - query="SELECT id, username FROM some_table WHERE id IN (1,2)") - except MetadataConsistencyError: - pass - - def test_execute_query_not_auth_fails(self): + def test_create_subset_not_auth_fails(self): with requests_mock.Mocker() as mock: # mock mock.post('/api/database/1/subset', status_code=417) # test try: - response = RestClient().execute_query(database_id=1, + response = RestClient().create_subset(database_id=1, query="SELECT id, username FROM some_table WHERE id IN (1,2)") except AuthenticationError: pass @@ -119,7 +95,7 @@ class QueryUnitTest(unittest.TestCase): # mock mock.get('/api/database/1/subset/6', json=exp.model_dump()) # test - response = RestClient().get_query(database_id=1, query_id=6) + response = RestClient().get_subset(database_id=1, subset_id=6) self.assertEqual(exp, response) def test_find_query_not_allowed_fails(self): @@ -128,7 +104,7 @@ class QueryUnitTest(unittest.TestCase): mock.get('/api/database/1/subset/6', status_code=403) # test try: - response = RestClient().get_query(database_id=1, query_id=6) + response = RestClient().get_subset(database_id=1, subset_id=6) except ForbiddenError: pass @@ -138,30 +114,10 @@ class QueryUnitTest(unittest.TestCase): mock.get('/api/database/1/subset/6', status_code=404) # test try: - response = RestClient().get_query(database_id=1, query_id=6) + response = RestClient().get_subset(database_id=1, subset_id=6) except NotExistsError: pass - def test_find_query_not_valid_fails(self): - with requests_mock.Mocker() as mock: - # mock - mock.get('/api/database/1/subset/6', status_code=501) - # test - try: - response = RestClient().get_query(database_id=1, query_id=6) - except QueryStoreError: - pass - - def test_find_query_not_expected_fails(self): - with requests_mock.Mocker() as mock: - # mock - mock.get('/api/database/1/subset/6', status_code=417) - # test - try: - response = RestClient().get_query(database_id=1, query_id=6) - except MetadataConsistencyError: - pass - def test_get_queries_empty_succeeds(self): with requests_mock.Mocker() as mock: exp = [] @@ -214,27 +170,7 @@ class QueryUnitTest(unittest.TestCase): except NotExistsError: pass - def test_get_queries_not_valid_fails(self): - with requests_mock.Mocker() as mock: - # mock - mock.get('/api/database/1/subset', status_code=501) - # test - try: - response = RestClient().get_queries(database_id=1) - except QueryStoreError: - pass - - def test_get_queries_malformed_fails(self): - with requests_mock.Mocker() as mock: - # mock - mock.get('/api/database/1/subset', status_code=423) - # test - try: - response = RestClient().get_queries(database_id=1) - except MalformedError: - pass - - def test_get_query_data_succeeds(self): + def test_get_subset_data_succeeds(self): with requests_mock.Mocker() as mock: exp = Result(result=[{'id': 1, 'username': 'foo'}, {'id': 2, 'username': 'bar'}], headers=[{'id': 0, 'username': 1}], @@ -242,10 +178,10 @@ class QueryUnitTest(unittest.TestCase): # mock mock.get('/api/database/1/subset/6/data', json=exp.model_dump()) # test - response = RestClient().get_query_data(database_id=1, query_id=6) + response = RestClient().get_subset_data(database_id=1, subset_id=6) self.assertEqual(exp, response) - def test_get_query_data_dataframe_succeeds(self): + def test_get_subset_data_dataframe_succeeds(self): with requests_mock.Mocker() as mock: res = Result(result=[{'id': 1, 'username': 'foo'}, {'id': 2, 'username': 'bar'}], headers=[{'id': 0, 'username': 1}], @@ -254,99 +190,59 @@ class QueryUnitTest(unittest.TestCase): # mock mock.get('/api/database/1/subset/6/data', json=res.model_dump()) # test - response = RestClient().get_query_data(database_id=1, query_id=6, df=True) + response = RestClient().get_subset_data(database_id=1, subset_id=6, df=True) self.assertEqual(exp.shape, response.shape) self.assertTrue(DataFrame.equals(exp, response)) - def test_get_query_data_not_allowed_fails(self): + def test_get_subset_data_not_allowed_fails(self): with requests_mock.Mocker() as mock: # mock mock.get('/api/database/1/subset/6/data', status_code=403) # test try: - response = RestClient().get_query_data(database_id=1, query_id=6) + response = RestClient().get_subset_data(database_id=1, subset_id=6) except ForbiddenError: pass - def test_get_query_data_not_found_fails(self): + def test_get_subset_data_not_found_fails(self): with requests_mock.Mocker() as mock: # mock mock.get('/api/database/1/subset/6/data', status_code=404) # test try: - response = RestClient().get_query_data(database_id=1, query_id=6) + response = RestClient().get_subset_data(database_id=1, subset_id=6) except NotExistsError: pass - def test_get_query_data_not_valid_fails(self): - with requests_mock.Mocker() as mock: - # mock - mock.get('/api/database/1/subset/6/data', status_code=409) - # test - try: - response = RestClient().get_query_data(database_id=1, query_id=6) - except QueryStoreError: - pass - - def test_get_query_data_not_consistent_fails(self): - with requests_mock.Mocker() as mock: - # mock - mock.get('/api/database/1/subset/6/data', status_code=417) - # test - try: - response = RestClient().get_query_data(database_id=1, query_id=6) - except MetadataConsistencyError: - pass - - def test_get_query_data_count_succeeds(self): + def test_get_subset_data_count_succeeds(self): with requests_mock.Mocker() as mock: exp = 2 # mock mock.head('/api/database/1/subset/6/data', headers={'X-Count': str(exp)}) # test - response = RestClient().get_query_data_count(database_id=1, query_id=6) + response = RestClient().get_subset_data_count(database_id=1, subset_id=6) self.assertEqual(exp, response) - def test_get_query_data_count_not_allowed_fails(self): + def test_get_subset_data_count_not_allowed_fails(self): with requests_mock.Mocker() as mock: # mock mock.head('/api/database/1/subset/6/data', status_code=403) # test try: - response = RestClient().get_query_data_count(database_id=1, query_id=6) + response = RestClient().get_subset_data_count(database_id=1, subset_id=6) except ForbiddenError: pass - def test_get_query_data_count_not_found_fails(self): + def test_get_subset_data_count_not_found_fails(self): with requests_mock.Mocker() as mock: # mock mock.head('/api/database/1/subset/6/data', status_code=404) # test try: - response = RestClient().get_query_data_count(database_id=1, query_id=6) + response = RestClient().get_subset_data_count(database_id=1, subset_id=6) except NotExistsError: pass - def test_get_query_data_count_not_valid_fails(self): - with requests_mock.Mocker() as mock: - # mock - mock.head('/api/database/1/subset/6/data', status_code=409) - # test - try: - response = RestClient().get_query_data_count(database_id=1, query_id=6) - except QueryStoreError: - pass - - def test_get_query_data_count_not_consistent_fails(self): - with requests_mock.Mocker() as mock: - # mock - mock.head('/api/database/1/subset/6/data', status_code=417) - # test - try: - response = RestClient().get_query_data_count(database_id=1, query_id=6) - except MetadataConsistencyError: - pass - if __name__ == "__main__": unittest.main() diff --git a/lib/python/tests/test_unit_table.py b/lib/python/tests/test_unit_table.py index 5dff01582e..0be3a4a9fb 100644 --- a/lib/python/tests/test_unit_table.py +++ b/lib/python/tests/test_unit_table.py @@ -10,7 +10,7 @@ from pandas import DataFrame from dbrepo.api.dto import Table, CreateTableConstraints, UserAttributes, User, Column, Constraints, ColumnType, Result, \ Concept, Unit, TableStatistics, ColumnStatistic, PrimaryKey, TableMinimal, ColumnMinimal, TableBrief, UserBrief from dbrepo.api.exceptions import MalformedError, ForbiddenError, NotExistsError, NameExistsError, QueryStoreError, \ - AuthenticationError + AuthenticationError, ExternalSystemError class TableUnitTest(unittest.TestCase): @@ -294,16 +294,6 @@ class TableUnitTest(unittest.TestCase): except NotExistsError: pass - def test_get_table_data_not_countable_fails(self): - with requests_mock.Mocker() as mock: - # mock - mock.get('/api/database/1/table/9/data', status_code=409) - # test - try: - response = RestClient().get_table_data(database_id=1, table_id=9) - except QueryStoreError: - pass - def test_get_table_data_count_succeeds(self): with requests_mock.Mocker() as mock: exp = 2 @@ -350,7 +340,7 @@ class TableUnitTest(unittest.TestCase): # test try: response = RestClient().get_table_data_count(database_id=1, table_id=9) - except QueryStoreError: + except ExternalSystemError: pass def test_create_table_data_succeeds(self): @@ -398,29 +388,6 @@ class TableUnitTest(unittest.TestCase): except NotExistsError: pass - def test_create_table_data_not_lob_fails(self): - with requests_mock.Mocker() as mock: - # mock - mock.post('/api/database/1/table/9/data', status_code=410) - # test - try: - client = RestClient(username="a", password="b") - client.create_table_data(database_id=1, table_id=9, - data={'name': 'Josiah', 'age': 45, 'gender': 'male'}) - except MalformedError: - pass - - def test_create_table_data_not_auth_fails(self): - with requests_mock.Mocker() as mock: - # mock - mock.post('/api/database/1/table/9/data', status_code=410) - # test - try: - RestClient().create_table_data(database_id=1, table_id=9, - data={'name': 'Josiah', 'age': 45, 'gender': 'male'}) - except AuthenticationError: - pass - def test_update_table_data_succeeds(self): with requests_mock.Mocker() as mock: # mock @@ -470,31 +437,6 @@ class TableUnitTest(unittest.TestCase): except NotExistsError: pass - def test_update_table_data_not_lob_fails(self): - with requests_mock.Mocker() as mock: - # mock - mock.put('/api/database/1/table/9/data', status_code=410) - # test - try: - client = RestClient(username="a", password="b") - client.update_table_data(database_id=1, table_id=9, - data={'name': 'Josiah', 'age': 45, 'gender': 'male'}, - keys={'id': 1}) - except MalformedError: - pass - - def test_update_table_data_not_auth_fails(self): - with requests_mock.Mocker() as mock: - # mock - mock.put('/api/database/1/table/9/data', status_code=410) - # test - try: - RestClient().update_table_data(database_id=1, table_id=9, - data={'name': 'Josiah', 'age': 45, 'gender': 'male'}, - keys={'id': 1}) - except AuthenticationError: - pass - def test_delete_table_data_succeeds(self): with requests_mock.Mocker() as mock: # mock diff --git a/lib/python/tests/test_unit_user.py b/lib/python/tests/test_unit_user.py index 08133fa6f0..0d12ecf48f 100644 --- a/lib/python/tests/test_unit_user.py +++ b/lib/python/tests/test_unit_user.py @@ -5,7 +5,7 @@ import requests_mock from dbrepo.RestClient import RestClient from dbrepo.api.dto import User, UserAttributes, UserBrief from dbrepo.api.exceptions import ResponseCodeError, UsernameExistsError, EmailExistsError, NotExistsError, \ - ForbiddenError, AuthenticationError + ForbiddenError, AuthenticationError, MalformedError, ServiceError class UserUnitTest(unittest.TestCase): @@ -66,18 +66,7 @@ class UserUnitTest(unittest.TestCase): # test try: response = RestClient().create_user(username='mweise', password='s3cr3t', email='mweise@example.com') - except ResponseCodeError as e: - pass - - def test_create_user_not_allowed_fails(self): - with requests_mock.Mocker() as mock: - exp = UserBrief(id='8638c043-5145-4be8-a3e4-4b79991b0a16', username='mweise') - # mock - mock.post('http://gateway-service/api/user', json=exp.model_dump(), status_code=403) - # test - try: - response = RestClient().create_user(username='mweise', password='s3cr3t', email='mweise@example.com') - except ForbiddenError as e: + except MalformedError as e: pass def test_create_user_username_exists_fails(self): @@ -171,18 +160,6 @@ class UserUnitTest(unittest.TestCase): except NotExistsError as e: pass - def test_update_user_foreign_fails(self): - with requests_mock.Mocker() as mock: - # mock - mock.put('http://gateway-service/api/user/8638c043-5145-4be8-a3e4-4b79991b0a16', status_code=405) - # test - try: - client = RestClient(username="a", password="b") - response = client.update_user(user_id='8638c043-5145-4be8-a3e4-4b79991b0a16', firstname='Martin', - language='en', theme='light') - except ForbiddenError as e: - pass - def test_update_user_not_auth_fails(self): with requests_mock.Mocker() as mock: # mock @@ -231,18 +208,6 @@ class UserUnitTest(unittest.TestCase): except NotExistsError as e: pass - def test_update_user_password_foreign_fails(self): - with requests_mock.Mocker() as mock: - # mock - mock.put('http://gateway-service/api/user/8638c043-5145-4be8-a3e4-4b79991b0a16/password', status_code=405) - # test - try: - client = RestClient(username="a", password="b") - response = client.update_user_password(user_id='8638c043-5145-4be8-a3e4-4b79991b0a16', - password='s3cr3t1n0rm4t10n') - except ForbiddenError as e: - pass - def test_update_user_password_keycloak_fails(self): with requests_mock.Mocker() as mock: # mock @@ -252,7 +217,7 @@ class UserUnitTest(unittest.TestCase): client = RestClient(username="a", password="b") response = client.update_user_password(user_id='8638c043-5145-4be8-a3e4-4b79991b0a16', password='s3cr3t1n0rm4t10n') - except ResponseCodeError as e: + except ServiceError as e: pass def test_update_user_password_not_auth_fails(self): diff --git a/lib/python/tests/test_unit_view.py b/lib/python/tests/test_unit_view.py index 476f047370..19a88be85a 100644 --- a/lib/python/tests/test_unit_view.py +++ b/lib/python/tests/test_unit_view.py @@ -44,16 +44,6 @@ class ViewUnitTest(unittest.TestCase): response = RestClient().get_views(database_id=1) self.assertEqual(exp, response) - def test_get_views_not_allowed_fails(self): - with requests_mock.Mocker() as mock: - # mock - mock.get('/api/database/1/view', status_code=403) - # test - try: - response = RestClient().get_views(database_id=1) - except ForbiddenError: - pass - def test_get_views_not_found_fails(self): with requests_mock.Mocker() as mock: # mock -- GitLab