You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by mi...@apache.org on 2021/12/06 01:14:16 UTC
[superset] branch master updated: fix: Allows PUT and DELETE only for owners of dashboard filter state (#17644)
This is an automated email from the ASF dual-hosted git repository.
michaelsmolina pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git
The following commit(s) were added to refs/heads/master by this push:
new 2ae83fa fix: Allows PUT and DELETE only for owners of dashboard filter state (#17644)
2ae83fa is described below
commit 2ae83fac8623acd20f92e9f441ce03793354e0a1
Author: Michael S. Molina <70...@users.noreply.github.com>
AuthorDate: Sun Dec 5 22:13:09 2021 -0300
fix: Allows PUT and DELETE only for owners of dashboard filter state (#17644)
* fix: Allows PUT and DELETE only for owners of dashboard filter state
* Converts the values to TypedDict
* Fixes variable name
---
.../dashboards/filter_state/commands/create.py | 10 +-
.../dashboards/filter_state/commands/delete.py | 16 +-
.../filter_state/commands/{delete.py => entry.py} | 16 +-
superset/dashboards/filter_state/commands/get.py | 9 +-
.../dashboards/filter_state/commands/update.py | 20 +-
superset/key_value/api.py | 9 +-
superset/key_value/commands/create.py | 10 +-
superset/key_value/commands/delete.py | 8 +-
superset/key_value/commands/exceptions.py | 5 +
superset/key_value/commands/get.py | 4 +-
superset/key_value/commands/update.py | 10 +-
.../dashboards/filter_state/api_tests.py | 280 ++++++++++++---------
12 files changed, 236 insertions(+), 161 deletions(-)
diff --git a/superset/dashboards/filter_state/commands/create.py b/superset/dashboards/filter_state/commands/create.py
index 4b8a789..045b2fa 100644
--- a/superset/dashboards/filter_state/commands/create.py
+++ b/superset/dashboards/filter_state/commands/create.py
@@ -16,17 +16,23 @@
# under the License.
from typing import Optional
+from flask_appbuilder.security.sqla.models import User
+
from superset.dashboards.dao import DashboardDAO
+from superset.dashboards.filter_state.commands.entry import Entry
from superset.extensions import cache_manager
from superset.key_value.commands.create import CreateKeyValueCommand
from superset.key_value.utils import cache_key
class CreateFilterStateCommand(CreateKeyValueCommand):
- def create(self, resource_id: int, key: str, value: str) -> Optional[bool]:
+ def create(
+ self, actor: User, resource_id: int, key: str, value: str
+ ) -> Optional[bool]:
dashboard = DashboardDAO.get_by_id_or_slug(str(resource_id))
if dashboard:
+ entry: Entry = {"owner": actor.get_user_id(), "value": value}
return cache_manager.filter_state_cache.set(
- cache_key(resource_id, key), value
+ cache_key(resource_id, key), entry
)
return False
diff --git a/superset/dashboards/filter_state/commands/delete.py b/superset/dashboards/filter_state/commands/delete.py
index 89ee287..0f27911 100644
--- a/superset/dashboards/filter_state/commands/delete.py
+++ b/superset/dashboards/filter_state/commands/delete.py
@@ -16,15 +16,27 @@
# under the License.
from typing import Optional
+from flask_appbuilder.security.sqla.models import User
+
from superset.dashboards.dao import DashboardDAO
+from superset.dashboards.filter_state.commands.entry import Entry
from superset.extensions import cache_manager
from superset.key_value.commands.delete import DeleteKeyValueCommand
+from superset.key_value.commands.exceptions import KeyValueAccessDeniedError
from superset.key_value.utils import cache_key
class DeleteFilterStateCommand(DeleteKeyValueCommand):
- def delete(self, resource_id: int, key: str) -> Optional[bool]:
+ def delete(self, actor: User, resource_id: int, key: str) -> Optional[bool]:
dashboard = DashboardDAO.get_by_id_or_slug(str(resource_id))
if dashboard:
- return cache_manager.filter_state_cache.delete(cache_key(resource_id, key))
+ entry: Entry = cache_manager.filter_state_cache.get(
+ cache_key(resource_id, key)
+ )
+ if entry:
+ if entry["owner"] != actor.get_user_id():
+ raise KeyValueAccessDeniedError()
+ return cache_manager.filter_state_cache.delete(
+ cache_key(resource_id, key)
+ )
return False
diff --git a/superset/dashboards/filter_state/commands/delete.py b/superset/dashboards/filter_state/commands/entry.py
similarity index 58%
copy from superset/dashboards/filter_state/commands/delete.py
copy to superset/dashboards/filter_state/commands/entry.py
index 89ee287..4ac6f03 100644
--- a/superset/dashboards/filter_state/commands/delete.py
+++ b/superset/dashboards/filter_state/commands/entry.py
@@ -14,17 +14,9 @@
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
-from typing import Optional
+from typing import TypedDict
-from superset.dashboards.dao import DashboardDAO
-from superset.extensions import cache_manager
-from superset.key_value.commands.delete import DeleteKeyValueCommand
-from superset.key_value.utils import cache_key
-
-class DeleteFilterStateCommand(DeleteKeyValueCommand):
- def delete(self, resource_id: int, key: str) -> Optional[bool]:
- dashboard = DashboardDAO.get_by_id_or_slug(str(resource_id))
- if dashboard:
- return cache_manager.filter_state_cache.delete(cache_key(resource_id, key))
- return False
+class Entry(TypedDict):
+ owner: int
+ value: str
diff --git a/superset/dashboards/filter_state/commands/get.py b/superset/dashboards/filter_state/commands/get.py
index ea9dc9d..d95f6b0 100644
--- a/superset/dashboards/filter_state/commands/get.py
+++ b/superset/dashboards/filter_state/commands/get.py
@@ -17,6 +17,7 @@
from typing import Optional
from superset.dashboards.dao import DashboardDAO
+from superset.dashboards.filter_state.commands.entry import Entry
from superset.extensions import cache_manager
from superset.key_value.commands.get import GetKeyValueCommand
from superset.key_value.utils import cache_key
@@ -26,8 +27,10 @@ class GetFilterStateCommand(GetKeyValueCommand):
def get(self, resource_id: int, key: str, refreshTimeout: bool) -> Optional[str]:
dashboard = DashboardDAO.get_by_id_or_slug(str(resource_id))
if dashboard:
- value = cache_manager.filter_state_cache.get(cache_key(resource_id, key))
+ entry: Entry = cache_manager.filter_state_cache.get(
+ cache_key(resource_id, key)
+ )
if refreshTimeout:
- cache_manager.filter_state_cache.set(key, value)
- return value
+ cache_manager.filter_state_cache.set(key, entry)
+ return entry["value"]
return None
diff --git a/superset/dashboards/filter_state/commands/update.py b/superset/dashboards/filter_state/commands/update.py
index a432df6..1412348 100644
--- a/superset/dashboards/filter_state/commands/update.py
+++ b/superset/dashboards/filter_state/commands/update.py
@@ -16,17 +16,31 @@
# under the License.
from typing import Optional
+from flask_appbuilder.security.sqla.models import User
+
from superset.dashboards.dao import DashboardDAO
+from superset.dashboards.filter_state.commands.entry import Entry
from superset.extensions import cache_manager
+from superset.key_value.commands.exceptions import KeyValueAccessDeniedError
from superset.key_value.commands.update import UpdateKeyValueCommand
from superset.key_value.utils import cache_key
class UpdateFilterStateCommand(UpdateKeyValueCommand):
- def update(self, resource_id: int, key: str, value: str) -> Optional[bool]:
+ def update(
+ self, actor: User, resource_id: int, key: str, value: str
+ ) -> Optional[bool]:
dashboard = DashboardDAO.get_by_id_or_slug(str(resource_id))
if dashboard:
- return cache_manager.filter_state_cache.set(
- cache_key(resource_id, key), value
+ entry: Entry = cache_manager.filter_state_cache.get(
+ cache_key(resource_id, key)
)
+ if entry:
+ user_id = actor.get_user_id()
+ if entry["owner"] != user_id:
+ raise KeyValueAccessDeniedError()
+ new_entry: Entry = {"owner": actor.get_user_id(), "value": value}
+ return cache_manager.filter_state_cache.set(
+ cache_key(resource_id, key), new_entry
+ )
return False
diff --git a/superset/key_value/api.py b/superset/key_value/api.py
index d67852a..2dc3400 100644
--- a/superset/key_value/api.py
+++ b/superset/key_value/api.py
@@ -29,6 +29,7 @@ from superset.dashboards.commands.exceptions import (
DashboardNotFoundError,
)
from superset.exceptions import InvalidPayloadFormatError
+from superset.key_value.commands.exceptions import KeyValueAccessDeniedError
from superset.key_value.schemas import KeyValuePostSchema, KeyValuePutSchema
logger = logging.getLogger(__name__)
@@ -64,7 +65,7 @@ class KeyValueRestApi(BaseApi, ABC):
return self.response(201, key=key)
except ValidationError as error:
return self.response_400(message=error.messages)
- except DashboardAccessDeniedError:
+ except (DashboardAccessDeniedError, KeyValueAccessDeniedError):
return self.response_403()
except DashboardNotFoundError:
return self.response_404()
@@ -80,7 +81,7 @@ class KeyValueRestApi(BaseApi, ABC):
return self.response(200, message="Value updated successfully.")
except ValidationError as error:
return self.response_400(message=error.messages)
- except DashboardAccessDeniedError:
+ except (DashboardAccessDeniedError, KeyValueAccessDeniedError):
return self.response_403()
except DashboardNotFoundError:
return self.response_404()
@@ -91,7 +92,7 @@ class KeyValueRestApi(BaseApi, ABC):
if not value:
return self.response_404()
return self.response(200, value=value)
- except DashboardAccessDeniedError:
+ except (DashboardAccessDeniedError, KeyValueAccessDeniedError):
return self.response_403()
except DashboardNotFoundError:
return self.response_404()
@@ -102,7 +103,7 @@ class KeyValueRestApi(BaseApi, ABC):
if not result:
return self.response_404()
return self.response(200, message="Deleted successfully")
- except DashboardAccessDeniedError:
+ except (DashboardAccessDeniedError, KeyValueAccessDeniedError):
return self.response_403()
except DashboardNotFoundError:
return self.response_404()
diff --git a/superset/key_value/commands/create.py b/superset/key_value/commands/create.py
index 361a3ad..3c56f73 100644
--- a/superset/key_value/commands/create.py
+++ b/superset/key_value/commands/create.py
@@ -31,16 +31,16 @@ logger = logging.getLogger(__name__)
class CreateKeyValueCommand(BaseCommand, ABC):
def __init__(
- self, user: User, resource_id: int, value: str,
+ self, actor: User, resource_id: int, value: str,
):
- self._actor = user
+ self._actor = actor
self._resource_id = resource_id
self._value = value
def run(self) -> Model:
try:
key = token_urlsafe(48)
- self.create(self._resource_id, key, self._value)
+ self.create(self._actor, self._resource_id, key, self._value)
return key
except SQLAlchemyError as ex:
logger.exception("Error running create command")
@@ -50,5 +50,7 @@ class CreateKeyValueCommand(BaseCommand, ABC):
pass
@abstractmethod
- def create(self, resource_id: int, key: str, value: str) -> Optional[bool]:
+ def create(
+ self, actor: User, resource_id: int, key: str, value: str
+ ) -> Optional[bool]:
...
diff --git a/superset/key_value/commands/delete.py b/superset/key_value/commands/delete.py
index 9990c44..9c885d0 100644
--- a/superset/key_value/commands/delete.py
+++ b/superset/key_value/commands/delete.py
@@ -29,14 +29,14 @@ logger = logging.getLogger(__name__)
class DeleteKeyValueCommand(BaseCommand, ABC):
- def __init__(self, user: User, resource_id: int, key: str):
- self._actor = user
+ def __init__(self, actor: User, resource_id: int, key: str):
+ self._actor = actor
self._resource_id = resource_id
self._key = key
def run(self) -> Model:
try:
- return self.delete(self._resource_id, self._key)
+ return self.delete(self._actor, self._resource_id, self._key)
except SQLAlchemyError as ex:
logger.exception("Error running delete command")
raise KeyValueDeleteFailedError() from ex
@@ -45,5 +45,5 @@ class DeleteKeyValueCommand(BaseCommand, ABC):
pass
@abstractmethod
- def delete(self, resource_id: int, key: str) -> Optional[bool]:
+ def delete(self, actor: User, resource_id: int, key: str) -> Optional[bool]:
...
diff --git a/superset/key_value/commands/exceptions.py b/superset/key_value/commands/exceptions.py
index f8c9dbd..780f705 100644
--- a/superset/key_value/commands/exceptions.py
+++ b/superset/key_value/commands/exceptions.py
@@ -20,6 +20,7 @@ from superset.commands.exceptions import (
CommandException,
CreateFailedError,
DeleteFailedError,
+ ForbiddenError,
UpdateFailedError,
)
@@ -38,3 +39,7 @@ class KeyValueDeleteFailedError(DeleteFailedError):
class KeyValueUpdateFailedError(UpdateFailedError):
message = _("An error occurred while updating the value.")
+
+
+class KeyValueAccessDeniedError(ForbiddenError):
+ message = _("You don't have permission to modify the value.")
diff --git a/superset/key_value/commands/get.py b/superset/key_value/commands/get.py
index 32991fa..be56050 100644
--- a/superset/key_value/commands/get.py
+++ b/superset/key_value/commands/get.py
@@ -30,8 +30,8 @@ logger = logging.getLogger(__name__)
class GetKeyValueCommand(BaseCommand, ABC):
- def __init__(self, user: User, resource_id: int, key: str):
- self._actor = user
+ def __init__(self, actor: User, resource_id: int, key: str):
+ self._actor = actor
self._resource_id = resource_id
self._key = key
diff --git a/superset/key_value/commands/update.py b/superset/key_value/commands/update.py
index dac91da..3f322c7 100644
--- a/superset/key_value/commands/update.py
+++ b/superset/key_value/commands/update.py
@@ -30,16 +30,16 @@ logger = logging.getLogger(__name__)
class UpdateKeyValueCommand(BaseCommand, ABC):
def __init__(
- self, user: User, resource_id: int, key: str, value: str,
+ self, actor: User, resource_id: int, key: str, value: str,
):
- self._actor = user
+ self._actor = actor
self._resource_id = resource_id
self._key = key
self._value = value
def run(self) -> Model:
try:
- return self.update(self._resource_id, self._key, self._value)
+ return self.update(self._actor, self._resource_id, self._key, self._value)
except SQLAlchemyError as ex:
logger.exception("Error running update command")
raise KeyValueUpdateFailedError() from ex
@@ -48,5 +48,7 @@ class UpdateKeyValueCommand(BaseCommand, ABC):
pass
@abstractmethod
- def update(self, resource_id: int, key: str, value: str) -> Optional[bool]:
+ def update(
+ self, actor: User, resource_id: int, key: str, value: str
+ ) -> Optional[bool]:
...
diff --git a/tests/integration_tests/dashboards/filter_state/api_tests.py b/tests/integration_tests/dashboards/filter_state/api_tests.py
index 1c2beef..02af14c 100644
--- a/tests/integration_tests/dashboards/filter_state/api_tests.py
+++ b/tests/integration_tests/dashboards/filter_state/api_tests.py
@@ -15,143 +15,181 @@
# specific language governing permissions and limitations
# under the License.
import json
-from typing import Any
from unittest.mock import patch
import pytest
-from flask.testing import FlaskClient
+from flask_appbuilder.security.sqla.models import User
from sqlalchemy.orm import Session
from superset import app
from superset.dashboards.commands.exceptions import DashboardAccessDeniedError
+from superset.dashboards.filter_state.commands.entry import Entry
from superset.extensions import cache_manager
from superset.key_value.utils import cache_key
from superset.models.dashboard import Dashboard
+from tests.integration_tests.base_tests import login
from tests.integration_tests.fixtures.world_bank_dashboard import (
load_world_bank_dashboard_with_slices,
)
from tests.integration_tests.test_app import app
-dashboardId = 985374
key = "test-key"
value = "test"
-class FilterStateTests:
- @pytest.fixture
- def client(self):
- with app.test_client() as client:
- with app.app_context():
- yield client
-
- @pytest.fixture
- def dashboard_id(self, load_world_bank_dashboard_with_slices) -> int:
- with app.app_context() as ctx:
- session: Session = ctx.app.appbuilder.get_session
- dashboard = session.query(Dashboard).filter_by(slug="world_health").one()
- return dashboard.id
-
- @pytest.fixture
- def cache(self, dashboard_id):
- app.config["FILTER_STATE_CACHE_CONFIG"] = {"CACHE_TYPE": "SimpleCache"}
- cache_manager.init_app(app)
- cache_manager.filter_state_cache.set(cache_key(dashboard_id, key), value)
-
- def setUp(self):
- self.login(username="admin")
-
- def test_post(self, client, dashboard_id: int):
- payload = {
- "value": value,
- }
- resp = client.post(
- f"api/v1/dashboard/{dashboard_id}/filter_state", json=payload
- )
- assert resp.status_code == 201
-
- def test_post_bad_request(self, client, dashboard_id: int):
- payload = {
- "value": 1234,
- }
- resp = client.put(
- f"api/v1/dashboard/{dashboard_id}/filter_state/{key}/", json=payload
- )
- assert resp.status_code == 400
-
- @patch("superset.security.SupersetSecurityManager.raise_for_dashboard_access")
- def test_post_access_denied(
- self, client, mock_raise_for_dashboard_access, dashboard_id: int
- ):
- mock_raise_for_dashboard_access.side_effect = DashboardAccessDeniedError()
- payload = {
- "value": value,
- }
- resp = client.post(
- f"api/v1/dashboard/{dashboard_id}/filter_state", json=payload
- )
- assert resp.status_code == 403
-
- def test_put(self, client, dashboard_id: int):
- payload = {
- "value": "new value",
- }
- resp = client.put(
- f"api/v1/dashboard/{dashboard_id}/filter_state/{key}/", json=payload
- )
- assert resp.status_code == 200
-
- def test_put_bad_request(self, client, dashboard_id: int):
- payload = {
- "value": 1234,
- }
- resp = client.put(
- f"api/v1/dashboard/{dashboard_id}/filter_state/{key}/", json=payload
- )
- assert resp.status_code == 400
-
- @patch("superset.security.SupersetSecurityManager.raise_for_dashboard_access")
- def test_put_access_denied(
- self, client, mock_raise_for_dashboard_access, dashboard_id: int
- ):
- mock_raise_for_dashboard_access.side_effect = DashboardAccessDeniedError()
- payload = {
- "value": "new value",
- }
- resp = client.put(
- f"api/v1/dashboard/{dashboard_id}/filter_state/{key}/", json=payload
- )
- assert resp.status_code == 403
-
- def test_get_key_not_found(self, client):
- resp = client.get("unknown-key")
- assert resp.status_code == 404
-
- def test_get_dashboard_not_found(self, client):
- resp = client.get(f"api/v1/dashboard/{-1}/filter_state/{key}/")
- assert resp.status_code == 404
-
- def test_get(self, client, dashboard_id: int):
- resp = client.get(f"api/v1/dashboard/{dashboard_id}/filter_state/{key}/")
- assert resp.status_code == 200
- data = json.loads(resp.data.decode("utf-8"))
- assert value == data.get("value")
-
- @patch("superset.security.SupersetSecurityManager.raise_for_dashboard_access")
- def test_get_access_denied(
- self, client, mock_raise_for_dashboard_access, dashboard_id
- ):
- mock_raise_for_dashboard_access.side_effect = DashboardAccessDeniedError()
- resp = client.get(f"api/v1/dashboard/{dashboard_id}/filter_state/{key}/")
- assert resp.status_code == 403
-
- def test_delete(self, client, dashboard_id: int):
- resp = client.delete(f"api/v1/dashboard/{dashboard_id}/filter_state/{key}/")
- assert resp.status_code == 200
-
- @patch("superset.security.SupersetSecurityManager.raise_for_dashboard_access")
- def test_delete_access_denied(
- self, client, mock_raise_for_dashboard_access, dashboard_id: int
- ):
- mock_raise_for_dashboard_access.side_effect = DashboardAccessDeniedError()
- resp = client.delete(f"api/v1/dashboard/{dashboard_id}/filter_state/{key}/")
- assert resp.status_code == 403
+@pytest.fixture
+def client():
+ with app.test_client() as client:
+ with app.app_context():
+ yield client
+
+
+@pytest.fixture
+def dashboard_id(load_world_bank_dashboard_with_slices) -> int:
+ with app.app_context() as ctx:
+ session: Session = ctx.app.appbuilder.get_session
+ dashboard = session.query(Dashboard).filter_by(slug="world_health").one()
+ return dashboard.id
+
+
+@pytest.fixture
+def admin_id() -> int:
+ with app.app_context() as ctx:
+ session: Session = ctx.app.appbuilder.get_session
+ admin = session.query(User).filter_by(username="admin").one_or_none()
+ return admin.id
+
+
+@pytest.fixture(autouse=True)
+def cache(dashboard_id, admin_id):
+ app.config["FILTER_STATE_CACHE_CONFIG"] = {"CACHE_TYPE": "SimpleCache"}
+ cache_manager.init_app(app)
+ entry: Entry = {"owner": admin_id, "value": value}
+ cache_manager.filter_state_cache.set(cache_key(dashboard_id, key), entry)
+
+
+def test_post(client, dashboard_id: int):
+ login(client, "admin")
+ payload = {
+ "value": value,
+ }
+ resp = client.post(f"api/v1/dashboard/{dashboard_id}/filter_state", json=payload)
+ assert resp.status_code == 201
+
+
+def test_post_bad_request(client, dashboard_id: int):
+ login(client, "admin")
+ payload = {
+ "value": 1234,
+ }
+ resp = client.put(
+ f"api/v1/dashboard/{dashboard_id}/filter_state/{key}/", json=payload
+ )
+ assert resp.status_code == 400
+
+
+@patch("superset.security.SupersetSecurityManager.raise_for_dashboard_access")
+def test_post_access_denied(mock_raise_for_dashboard_access, client, dashboard_id: int):
+ login(client, "admin")
+ mock_raise_for_dashboard_access.side_effect = DashboardAccessDeniedError()
+ payload = {
+ "value": value,
+ }
+ resp = client.post(f"api/v1/dashboard/{dashboard_id}/filter_state", json=payload)
+ assert resp.status_code == 403
+
+
+def test_put(client, dashboard_id: int):
+ login(client, "admin")
+ payload = {
+ "value": "new value",
+ }
+ resp = client.put(
+ f"api/v1/dashboard/{dashboard_id}/filter_state/{key}/", json=payload
+ )
+ assert resp.status_code == 200
+
+
+def test_put_bad_request(client, dashboard_id: int):
+ login(client, "admin")
+ payload = {
+ "value": 1234,
+ }
+ resp = client.put(
+ f"api/v1/dashboard/{dashboard_id}/filter_state/{key}/", json=payload
+ )
+ assert resp.status_code == 400
+
+
+@patch("superset.security.SupersetSecurityManager.raise_for_dashboard_access")
+def test_put_access_denied(mock_raise_for_dashboard_access, client, dashboard_id: int):
+ login(client, "admin")
+ mock_raise_for_dashboard_access.side_effect = DashboardAccessDeniedError()
+ payload = {
+ "value": "new value",
+ }
+ resp = client.put(
+ f"api/v1/dashboard/{dashboard_id}/filter_state/{key}/", json=payload
+ )
+ assert resp.status_code == 403
+
+
+def test_put_not_owner(client, dashboard_id: int):
+ login(client, "gamma")
+ payload = {
+ "value": "new value",
+ }
+ resp = client.put(
+ f"api/v1/dashboard/{dashboard_id}/filter_state/{key}/", json=payload
+ )
+ assert resp.status_code == 403
+
+
+def test_get_key_not_found(client):
+ login(client, "admin")
+ resp = client.get("unknown-key")
+ assert resp.status_code == 404
+
+
+def test_get_dashboard_not_found(client):
+ login(client, "admin")
+ resp = client.get(f"api/v1/dashboard/{-1}/filter_state/{key}/")
+ assert resp.status_code == 404
+
+
+def test_get(client, dashboard_id: int):
+ login(client, "admin")
+ resp = client.get(f"api/v1/dashboard/{dashboard_id}/filter_state/{key}/")
+ assert resp.status_code == 200
+ data = json.loads(resp.data.decode("utf-8"))
+ assert value == data.get("value")
+
+
+@patch("superset.security.SupersetSecurityManager.raise_for_dashboard_access")
+def test_get_access_denied(mock_raise_for_dashboard_access, client, dashboard_id):
+ login(client, "admin")
+ mock_raise_for_dashboard_access.side_effect = DashboardAccessDeniedError()
+ resp = client.get(f"api/v1/dashboard/{dashboard_id}/filter_state/{key}/")
+ assert resp.status_code == 403
+
+
+def test_delete(client, dashboard_id: int):
+ login(client, "admin")
+ resp = client.delete(f"api/v1/dashboard/{dashboard_id}/filter_state/{key}/")
+ assert resp.status_code == 200
+
+
+@patch("superset.security.SupersetSecurityManager.raise_for_dashboard_access")
+def test_delete_access_denied(
+ mock_raise_for_dashboard_access, client, dashboard_id: int
+):
+ login(client, "admin")
+ mock_raise_for_dashboard_access.side_effect = DashboardAccessDeniedError()
+ resp = client.delete(f"api/v1/dashboard/{dashboard_id}/filter_state/{key}/")
+ assert resp.status_code == 403
+
+
+def test_delete_not_owner(client, dashboard_id: int):
+ login(client, "gamma")
+ resp = client.delete(f"api/v1/dashboard/{dashboard_id}/filter_state/{key}/")
+ assert resp.status_code == 403