diff --git a/docs/static/resources/openapi.json b/docs/static/resources/openapi.json index 7729d413d..ad299216f 100644 --- a/docs/static/resources/openapi.json +++ b/docs/static/resources/openapi.json @@ -1834,10 +1834,10 @@ "type": "string" }, "last_saved_by": { - "$ref": "#/components/schemas/ChartDataRestApi.get_list.User3" + "$ref": "#/components/schemas/ChartDataRestApi.get_list.User1" }, "owners": { - "$ref": "#/components/schemas/ChartDataRestApi.get_list.User1" + "$ref": "#/components/schemas/ChartDataRestApi.get_list.User3" }, "params": { "nullable": true, @@ -1925,16 +1925,11 @@ "last_name": { "maxLength": 64, "type": "string" - }, - "username": { - "maxLength": 64, - "type": "string" } }, "required": [ "first_name", - "last_name", - "username" + "last_name" ], "type": "object" }, @@ -1972,11 +1967,16 @@ "last_name": { "maxLength": 64, "type": "string" + }, + "username": { + "maxLength": 64, + "type": "string" } }, "required": [ "first_name", - "last_name" + "last_name", + "username" ], "type": "object" }, @@ -2627,10 +2627,10 @@ "type": "string" }, "last_saved_by": { - "$ref": "#/components/schemas/ChartRestApi.get_list.User3" + "$ref": "#/components/schemas/ChartRestApi.get_list.User1" }, "owners": { - "$ref": "#/components/schemas/ChartRestApi.get_list.User1" + "$ref": "#/components/schemas/ChartRestApi.get_list.User3" }, "params": { "nullable": true, @@ -2718,16 +2718,11 @@ "last_name": { "maxLength": 64, "type": "string" - }, - "username": { - "maxLength": 64, - "type": "string" } }, "required": [ "first_name", - "last_name", - "username" + "last_name" ], "type": "object" }, @@ -2765,11 +2760,16 @@ "last_name": { "maxLength": 64, "type": "string" + }, + "username": { + "maxLength": 64, + "type": "string" } }, "required": [ "first_name", - "last_name" + "last_name", + "username" ], "type": "object" }, @@ -3420,7 +3420,7 @@ "readOnly": true }, "created_by": { - "$ref": "#/components/schemas/DashboardRestApi.get_list.User2" + "$ref": "#/components/schemas/DashboardRestApi.get_list.User1" }, "created_on_delta_humanized": { "readOnly": true @@ -3446,7 +3446,7 @@ "type": "string" }, "owners": { - "$ref": "#/components/schemas/DashboardRestApi.get_list.User1" + "$ref": "#/components/schemas/DashboardRestApi.get_list.User2" }, "position_json": { "nullable": true, @@ -3519,6 +3519,27 @@ "type": "object" }, "DashboardRestApi.get_list.User1": { + "properties": { + "first_name": { + "maxLength": 64, + "type": "string" + }, + "id": { + "format": "int32", + "type": "integer" + }, + "last_name": { + "maxLength": 64, + "type": "string" + } + }, + "required": [ + "first_name", + "last_name" + ], + "type": "object" + }, + "DashboardRestApi.get_list.User2": { "properties": { "email": { "maxLength": 64, @@ -3549,27 +3570,6 @@ ], "type": "object" }, - "DashboardRestApi.get_list.User2": { - "properties": { - "first_name": { - "maxLength": 64, - "type": "string" - }, - "id": { - "format": "int32", - "type": "integer" - }, - "last_name": { - "maxLength": 64, - "type": "string" - } - }, - "required": [ - "first_name", - "last_name" - ], - "type": "object" - }, "DashboardRestApi.post": { "properties": { "certification_details": { @@ -4293,6 +4293,18 @@ }, "type": "object" }, + "DatabaseSchemaAccessForFileUploadResponse": { + "properties": { + "schemas": { + "description": "The list of schemas allowed for the database to upload information", + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object" + }, "DatabaseTablesResponse": { "properties": { "extra": { @@ -4917,7 +4929,7 @@ "$ref": "#/components/schemas/DatasetRestApi.get.TableColumn" }, "created_by": { - "$ref": "#/components/schemas/DatasetRestApi.get.User2" + "$ref": "#/components/schemas/DatasetRestApi.get.User1" }, "created_on": { "format": "date-time", @@ -4981,7 +4993,7 @@ "type": "integer" }, "owners": { - "$ref": "#/components/schemas/DatasetRestApi.get.User1" + "$ref": "#/components/schemas/DatasetRestApi.get.User2" }, "schema": { "maxLength": 255, @@ -5190,6 +5202,23 @@ "type": "object" }, "DatasetRestApi.get.User1": { + "properties": { + "first_name": { + "maxLength": 64, + "type": "string" + }, + "last_name": { + "maxLength": 64, + "type": "string" + } + }, + "required": [ + "first_name", + "last_name" + ], + "type": "object" + }, + "DatasetRestApi.get.User2": { "properties": { "first_name": { "maxLength": 64, @@ -5215,23 +5244,6 @@ ], "type": "object" }, - "DatasetRestApi.get.User2": { - "properties": { - "first_name": { - "maxLength": 64, - "type": "string" - }, - "last_name": { - "maxLength": 64, - "type": "string" - } - }, - "required": [ - "first_name", - "last_name" - ], - "type": "object" - }, "DatasetRestApi.get_list": { "properties": { "changed_by": { @@ -6971,7 +6983,7 @@ "type": "integer" }, "created_by": { - "$ref": "#/components/schemas/ReportScheduleRestApi.get_list.User2" + "$ref": "#/components/schemas/ReportScheduleRestApi.get_list.User1" }, "created_on": { "format": "date-time", @@ -7021,7 +7033,7 @@ "type": "string" }, "owners": { - "$ref": "#/components/schemas/ReportScheduleRestApi.get_list.User1" + "$ref": "#/components/schemas/ReportScheduleRestApi.get_list.User2" }, "recipients": { "$ref": "#/components/schemas/ReportScheduleRestApi.get_list.ReportRecipients" @@ -7082,10 +7094,6 @@ "maxLength": 64, "type": "string" }, - "id": { - "format": "int32", - "type": "integer" - }, "last_name": { "maxLength": 64, "type": "string" @@ -7103,6 +7111,10 @@ "maxLength": 64, "type": "string" }, + "id": { + "format": "int32", + "type": "integer" + }, "last_name": { "maxLength": 64, "type": "string" @@ -15301,6 +15313,50 @@ ] } }, + "/api/v1/database/{pk}/schemas_access_for_file_upload/": { + "get": { + "parameters": [ + { + "in": "path", + "name": "pk", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DatabaseSchemaAccessForFileUploadResponse" + } + } + }, + "description": "The list of the database schemas where to upload information" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "security": [ + { + "jwt": [] + } + ], + "summary": "The list of the database schemas where to upload information", + "tags": [ + "Database" + ] + } + }, "/api/v1/database/{pk}/select_star/{table_name}/": { "get": { "description": "Get database select star for table", diff --git a/superset/constants.py b/superset/constants.py index 69a7bc7a4..7007e77e3 100644 --- a/superset/constants.py +++ b/superset/constants.py @@ -145,6 +145,7 @@ MODEL_API_RW_METHOD_PERMISSION_MAP = { "delete_ssh_tunnel": "write", "get_updated_since": "read", "stop_query": "read", + "schemas_access_for_file_upload": "read", "get_objects": "read", "get_all_objects": "read", "add_objects": "write", diff --git a/superset/databases/api.py b/superset/databases/api.py index f9be5e7e9..233990ccd 100644 --- a/superset/databases/api.py +++ b/superset/databases/api.py @@ -65,6 +65,7 @@ from superset.databases.schemas import ( DatabasePostSchema, DatabasePutSchema, DatabaseRelatedObjectsResponse, + DatabaseSchemaAccessForFileUploadResponse, DatabaseTablesResponse, DatabaseTestConnectionSchema, DatabaseValidateParametersSchema, @@ -120,6 +121,7 @@ class DatabaseRestApi(BaseSupersetModelRestApi): "validate_parameters", "validate_sql", "delete_ssh_tunnel", + "schemas_access_for_file_upload", } resource_name = "database" class_permission_name = "Database" @@ -222,6 +224,7 @@ class DatabaseRestApi(BaseSupersetModelRestApi): openapi_spec_tag = "Database" openapi_spec_component_schemas = ( DatabaseFunctionNamesResponse, + DatabaseSchemaAccessForFileUploadResponse, DatabaseRelatedObjectsResponse, DatabaseTablesResponse, DatabaseTestConnectionSchema, @@ -814,7 +817,7 @@ class DatabaseRestApi(BaseSupersetModelRestApi): ) except NoSuchTableError: self.incr_stats("error", self.select_star.__name__) - return self.response(404, message="Table not found in the database") + return self.response(404, message="Table not found on the database") self.incr_stats("success", self.select_star.__name__) return self.response(200, result=result) @@ -1453,3 +1456,52 @@ class DatabaseRestApi(BaseSupersetModelRestApi): exc_info=True, ) return self.response_400(message=str(ex)) + + @expose("//schemas_access_for_file_upload/") + @protect() + @safe + @statsd_metrics + @event_logger.log_this_with_context( + action=lambda self, *args, **kwargs: f"{self.__class__.__name__}" + f".schemas_access_for_file_upload", + log_to_statsd=False, + ) + def schemas_access_for_file_upload(self, pk: int) -> Response: + """The list of the database schemas where to upload information + --- + get: + summary: + The list of the database schemas where to upload information + parameters: + - in: path + name: pk + schema: + type: integer + responses: + 200: + description: The list of the database schemas where to upload information + content: + application/json: + schema: + $ref: "#/components/schemas/DatabaseSchemaAccessForFileUploadResponse" + 401: + $ref: '#/components/responses/401' + 404: + $ref: '#/components/responses/404' + 500: + $ref: '#/components/responses/500' + """ + database = DatabaseDAO.find_by_id(pk) + if not database: + return self.response_404() + + schemas_allowed = database.get_schema_access_for_file_upload() + # the list schemas_allowed should not be empty here + # and the list schemas_allowed_processed returned from security_manager + # should not be empty either, + # otherwise the database should have been filtered out + # in CsvToDatabaseForm + schemas_allowed_processed = security_manager.get_schemas_accessible_by_user( + database, schemas_allowed, True + ) + return self.response(200, schemas=schemas_allowed_processed) diff --git a/superset/databases/schemas.py b/superset/databases/schemas.py index 288408969..7a1e99404 100644 --- a/superset/databases/schemas.py +++ b/superset/databases/schemas.py @@ -808,3 +808,10 @@ def encrypted_field_properties(self, field: Any, **_) -> Dict[str, Any]: # type if self.openapi_version.major > 2: ret["x-encrypted-extra"] = True return ret + + +class DatabaseSchemaAccessForFileUploadResponse(Schema): + schemas = fields.List( + fields.String(), + description="The list of schemas allowed for the database to upload information", + ) diff --git a/superset/templates/superset/form_view/database_schemas_selector.html b/superset/templates/superset/form_view/database_schemas_selector.html index b9efb68d7..ac827c133 100644 --- a/superset/templates/superset/form_view/database_schemas_selector.html +++ b/superset/templates/superset/form_view/database_schemas_selector.html @@ -32,12 +32,11 @@ under the License. function update_schemas_allowed_for_csv_upload(db_id) { $.ajax({ method: "GET", - url: "/superset/schemas_access_for_file_upload", - data: {db_id: db_id}, + url: `/api/v1/database/${db_id}/schemas_access_for_file_upload/`, dataType: 'json', contentType: "application/json; charset=utf-8" }).done(function (data) { - change_schema_field_in_formview(data) + change_schema_field_in_formview(data ? data.schemas : []) }).fail(function (error) { var errorMsg = error.responseJSON.error; alert("ERROR: " + errorMsg); diff --git a/superset/views/core.py b/superset/views/core.py index 7c5609ca2..3bd0ec651 100755 --- a/superset/views/core.py +++ b/superset/views/core.py @@ -2747,6 +2747,7 @@ class Superset(BaseSupersetView): # pylint: disable=too-many-public-methods @has_access_api @event_logger.log_this @expose("/schemas_access_for_file_upload") + @deprecated() def schemas_access_for_file_upload(self) -> FlaskResponse: """ This method exposes an API endpoint to diff --git a/tests/integration_tests/databases/api_tests.py b/tests/integration_tests/databases/api_tests.py index 3859c0be5..a30a95188 100644 --- a/tests/integration_tests/databases/api_tests.py +++ b/tests/integration_tests/databases/api_tests.py @@ -3619,3 +3619,44 @@ class TestDatabaseApi(SupersetTestCase): return self.assertEqual(rv.status_code, 422) self.assertIn("Kaboom!", response["errors"][0]["message"]) + + @mock.patch( + "superset.security.SupersetSecurityManager.get_schemas_accessible_by_user" + ) + @mock.patch("superset.security.SupersetSecurityManager.can_access_database") + @mock.patch("superset.security.SupersetSecurityManager.can_access_all_datasources") + def test_schemas_access_for_csv_upload_not_found_endpoint( + self, + mock_can_access_all_datasources, + mock_can_access_database, + mock_schemas_accessible, + ): + self.login(username="gamma") + self.create_fake_db() + mock_can_access_database.return_value = False + mock_schemas_accessible.return_value = ["this_schema_is_allowed_too"] + rv = self.client.get(f"/api/v1/database/120ff/schemas_access_for_file_upload") + self.assertEqual(rv.status_code, 404) + self.delete_fake_db() + + @mock.patch( + "superset.security.SupersetSecurityManager.get_schemas_accessible_by_user" + ) + @mock.patch("superset.security.SupersetSecurityManager.can_access_database") + @mock.patch("superset.security.SupersetSecurityManager.can_access_all_datasources") + def test_schemas_access_for_csv_upload_endpoint( + self, + mock_can_access_all_datasources, + mock_can_access_database, + mock_schemas_accessible, + ): + self.login(username="admin") + dbobj = self.create_fake_db() + mock_can_access_all_datasources.return_value = False + mock_can_access_database.return_value = False + mock_schemas_accessible.return_value = ["this_schema_is_allowed_too"] + data = self.get_json_resp( + url=f"/api/v1/database/{dbobj.id}/schemas_access_for_file_upload" + ) + assert data == {"schemas": ["this_schema_is_allowed_too"]} + self.delete_fake_db()