Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Allows PUT and DELETE only for owners of dashboard filter state #17644

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions superset/dashboards/filter_state/commands/create.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
16 changes: 14 additions & 2 deletions superset/dashboards/filter_state/commands/delete.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
22 changes: 22 additions & 0 deletions superset/dashboards/filter_state/commands/entry.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
from typing import TypedDict


class Entry(TypedDict):
owner: int
value: str
9 changes: 6 additions & 3 deletions superset/dashboards/filter_state/commands/get.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
20 changes: 17 additions & 3 deletions superset/dashboards/filter_state/commands/update.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
9 changes: 5 additions & 4 deletions superset/key_value/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
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__)
Expand Down Expand Up @@ -64,7 +65,7 @@ def post(self, pk: int) -> Response:
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()
Expand All @@ -80,7 +81,7 @@ def put(self, pk: int, key: str) -> Response:
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()
Expand All @@ -91,7 +92,7 @@ def get(self, pk: int, key: str) -> Response:
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()
Expand All @@ -102,7 +103,7 @@ def delete(self, pk: int, key: str) -> Response:
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()
Expand Down
10 changes: 6 additions & 4 deletions superset/key_value/commands/create.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,16 +31,16 @@

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")
Expand All @@ -50,5 +50,7 @@ def validate(self) -> None:
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]:
...
8 changes: 4 additions & 4 deletions superset/key_value/commands/delete.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,14 @@


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
Expand All @@ -45,5 +45,5 @@ def validate(self) -> None:
pass

@abstractmethod
def delete(self, resource_id: int, key: str) -> Optional[bool]:
def delete(self, actor: User, resource_id: int, key: str) -> Optional[bool]:
...
5 changes: 5 additions & 0 deletions superset/key_value/commands/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
CommandException,
CreateFailedError,
DeleteFailedError,
ForbiddenError,
UpdateFailedError,
)

Expand All @@ -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.")
4 changes: 2 additions & 2 deletions superset/key_value/commands/get.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@


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

Expand Down
10 changes: 6 additions & 4 deletions superset/key_value/commands/update.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,16 +30,16 @@

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
Expand All @@ -48,5 +48,7 @@ def validate(self) -> None:
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]:
...
Loading