fix: Refactor ownership checks and ensure consistency (#20499)

Co-authored-by: John Bodley <john.bodley@airbnb.com>
This commit is contained in:
John Bodley 2022-07-07 11:04:27 -07:00 committed by GitHub
parent e7b965a3b2
commit f0ca158989
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
107 changed files with 614 additions and 807 deletions

View File

@ -303,7 +303,7 @@ ignored-modules=numpy,pandas,alembic.op,sqlalchemy,alembic.context,flask_appbuil
# List of class names for which member attributes should not be checked (useful
# for classes with dynamically set attributes). This supports the use of
# qualified names.
ignored-classes=contextlib.closing,optparse.Values,thread._local,_thread._local,sqlalchemy.orm.scoping.scoped_session
ignored-classes=contextlib.closing,optparse.Values,thread._local,_thread._local
# List of members which are set dynamically and missed by pylint inference
# system, and so shouldn't trigger E1101 when accessed. Python regular

View File

@ -17,7 +17,7 @@
import logging
from typing import Any, Dict
from flask import g, request, Response
from flask import request, Response
from flask_appbuilder.api import expose, permission_name, protect, rison, safe
from flask_appbuilder.api.schemas import get_item_schema, get_list_schema
from flask_appbuilder.models.sqla.interface import SQLAInterface
@ -306,7 +306,7 @@ class AnnotationRestApi(BaseSupersetModelRestApi):
except ValidationError as error:
return self.response_400(message=error.messages)
try:
new_model = CreateAnnotationCommand(g.user, item).run()
new_model = CreateAnnotationCommand(item).run()
return self.response(201, id=new_model.id, result=item)
except AnnotationLayerNotFoundError as ex:
return self.response_400(message=str(ex))
@ -381,7 +381,7 @@ class AnnotationRestApi(BaseSupersetModelRestApi):
except ValidationError as error:
return self.response_400(message=error.messages)
try:
new_model = UpdateAnnotationCommand(g.user, annotation_id, item).run()
new_model = UpdateAnnotationCommand(annotation_id, item).run()
return self.response(200, id=new_model.id, result=item)
except (AnnotationNotFoundError, AnnotationLayerNotFoundError):
return self.response_404()
@ -438,7 +438,7 @@ class AnnotationRestApi(BaseSupersetModelRestApi):
$ref: '#/components/responses/500'
"""
try:
DeleteAnnotationCommand(g.user, annotation_id).run()
DeleteAnnotationCommand(annotation_id).run()
return self.response(200, message="OK")
except AnnotationNotFoundError:
return self.response_404()
@ -495,7 +495,7 @@ class AnnotationRestApi(BaseSupersetModelRestApi):
"""
item_ids = kwargs["rison"]
try:
BulkDeleteAnnotationCommand(g.user, item_ids).run()
BulkDeleteAnnotationCommand(item_ids).run()
return self.response(
200,
message=ngettext(

View File

@ -17,8 +17,6 @@
import logging
from typing import List, Optional
from flask_appbuilder.security.sqla.models import User
from superset.annotation_layers.annotations.commands.exceptions import (
AnnotationBulkDeleteFailedError,
AnnotationNotFoundError,
@ -32,8 +30,7 @@ logger = logging.getLogger(__name__)
class BulkDeleteAnnotationCommand(BaseCommand):
def __init__(self, user: User, model_ids: List[int]):
self._actor = user
def __init__(self, model_ids: List[int]):
self._model_ids = model_ids
self._models: Optional[List[Annotation]] = None

View File

@ -19,7 +19,6 @@ from datetime import datetime
from typing import Any, Dict, List, Optional
from flask_appbuilder.models.sqla import Model
from flask_appbuilder.security.sqla.models import User
from marshmallow import ValidationError
from superset.annotation_layers.annotations.commands.exceptions import (
@ -38,8 +37,7 @@ logger = logging.getLogger(__name__)
class CreateAnnotationCommand(BaseCommand):
def __init__(self, user: User, data: Dict[str, Any]):
self._actor = user
def __init__(self, data: Dict[str, Any]):
self._properties = data.copy()
def run(self) -> Model:

View File

@ -18,7 +18,6 @@ import logging
from typing import Optional
from flask_appbuilder.models.sqla import Model
from flask_appbuilder.security.sqla.models import User
from superset.annotation_layers.annotations.commands.exceptions import (
AnnotationDeleteFailedError,
@ -33,8 +32,7 @@ logger = logging.getLogger(__name__)
class DeleteAnnotationCommand(BaseCommand):
def __init__(self, user: User, model_id: int):
self._actor = user
def __init__(self, model_id: int):
self._model_id = model_id
self._model: Optional[Annotation] = None

View File

@ -19,7 +19,6 @@ from datetime import datetime
from typing import Any, Dict, List, Optional
from flask_appbuilder.models.sqla import Model
from flask_appbuilder.security.sqla.models import User
from marshmallow import ValidationError
from superset.annotation_layers.annotations.commands.exceptions import (
@ -40,8 +39,7 @@ logger = logging.getLogger(__name__)
class UpdateAnnotationCommand(BaseCommand):
def __init__(self, user: User, model_id: int, data: Dict[str, Any]):
self._actor = user
def __init__(self, model_id: int, data: Dict[str, Any]):
self._model_id = model_id
self._properties = data.copy()
self._model: Optional[Annotation] = None

View File

@ -17,7 +17,7 @@
import logging
from typing import Any
from flask import g, request, Response
from flask import request, Response
from flask_appbuilder.api import expose, permission_name, protect, rison, safe
from flask_appbuilder.models.sqla.interface import SQLAInterface
from flask_babel import ngettext
@ -151,7 +151,7 @@ class AnnotationLayerRestApi(BaseSupersetModelRestApi):
$ref: '#/components/responses/500'
"""
try:
DeleteAnnotationLayerCommand(g.user, pk).run()
DeleteAnnotationLayerCommand(pk).run()
return self.response(200, message="OK")
except AnnotationLayerNotFoundError:
return self.response_404()
@ -216,7 +216,7 @@ class AnnotationLayerRestApi(BaseSupersetModelRestApi):
except ValidationError as error:
return self.response_400(message=error.messages)
try:
new_model = CreateAnnotationLayerCommand(g.user, item).run()
new_model = CreateAnnotationLayerCommand(item).run()
return self.response(201, id=new_model.id, result=item)
except AnnotationLayerNotFoundError as ex:
return self.response_400(message=str(ex))
@ -288,7 +288,7 @@ class AnnotationLayerRestApi(BaseSupersetModelRestApi):
except ValidationError as error:
return self.response_400(message=error.messages)
try:
new_model = UpdateAnnotationLayerCommand(g.user, pk, item).run()
new_model = UpdateAnnotationLayerCommand(pk, item).run()
return self.response(200, id=new_model.id, result=item)
except AnnotationLayerNotFoundError:
return self.response_404()
@ -346,7 +346,7 @@ class AnnotationLayerRestApi(BaseSupersetModelRestApi):
"""
item_ids = kwargs["rison"]
try:
BulkDeleteAnnotationLayerCommand(g.user, item_ids).run()
BulkDeleteAnnotationLayerCommand(item_ids).run()
return self.response(
200,
message=ngettext(

View File

@ -17,8 +17,6 @@
import logging
from typing import List, Optional
from flask_appbuilder.security.sqla.models import User
from superset.annotation_layers.commands.exceptions import (
AnnotationLayerBulkDeleteFailedError,
AnnotationLayerBulkDeleteIntegrityError,
@ -33,8 +31,7 @@ logger = logging.getLogger(__name__)
class BulkDeleteAnnotationLayerCommand(BaseCommand):
def __init__(self, user: User, model_ids: List[int]):
self._actor = user
def __init__(self, model_ids: List[int]):
self._model_ids = model_ids
self._models: Optional[List[AnnotationLayer]] = None

View File

@ -18,7 +18,6 @@ import logging
from typing import Any, Dict, List
from flask_appbuilder.models.sqla import Model
from flask_appbuilder.security.sqla.models import User
from marshmallow import ValidationError
from superset.annotation_layers.commands.exceptions import (
@ -34,8 +33,7 @@ logger = logging.getLogger(__name__)
class CreateAnnotationLayerCommand(BaseCommand):
def __init__(self, user: User, data: Dict[str, Any]):
self._actor = user
def __init__(self, data: Dict[str, Any]):
self._properties = data.copy()
def run(self) -> Model:

View File

@ -18,7 +18,6 @@ import logging
from typing import Optional
from flask_appbuilder.models.sqla import Model
from flask_appbuilder.security.sqla.models import User
from superset.annotation_layers.commands.exceptions import (
AnnotationLayerDeleteFailedError,
@ -34,8 +33,7 @@ logger = logging.getLogger(__name__)
class DeleteAnnotationLayerCommand(BaseCommand):
def __init__(self, user: User, model_id: int):
self._actor = user
def __init__(self, model_id: int):
self._model_id = model_id
self._model: Optional[AnnotationLayer] = None

View File

@ -18,7 +18,6 @@ import logging
from typing import Any, Dict, List, Optional
from flask_appbuilder.models.sqla import Model
from flask_appbuilder.security.sqla.models import User
from marshmallow import ValidationError
from superset.annotation_layers.commands.exceptions import (
@ -36,8 +35,7 @@ logger = logging.getLogger(__name__)
class UpdateAnnotationLayerCommand(BaseCommand):
def __init__(self, user: User, model_id: int, data: Dict[str, Any]):
self._actor = user
def __init__(self, model_id: int, data: Dict[str, Any]):
self._model_id = model_id
self._properties = data.copy()
self._model: Optional[AnnotationLayer] = None

View File

@ -21,7 +21,7 @@ from io import BytesIO
from typing import Any, Optional
from zipfile import ZipFile
from flask import g, redirect, request, Response, send_file, url_for
from flask import redirect, request, Response, send_file, url_for
from flask_appbuilder.api import expose, protect, rison, safe
from flask_appbuilder.hooks import before_request
from flask_appbuilder.models.sqla.interface import SQLAInterface
@ -285,7 +285,7 @@ class ChartRestApi(BaseSupersetModelRestApi):
except ValidationError as error:
return self.response_400(message=error.messages)
try:
new_model = CreateChartCommand(g.user, item).run()
new_model = CreateChartCommand(item).run()
return self.response(201, id=new_model.id, result=item)
except ChartInvalidError as ex:
return self.response_422(message=ex.normalized_messages())
@ -356,7 +356,7 @@ class ChartRestApi(BaseSupersetModelRestApi):
except ValidationError as error:
return self.response_400(message=error.messages)
try:
changed_model = UpdateChartCommand(g.user, pk, item).run()
changed_model = UpdateChartCommand(pk, item).run()
response = self.response(200, id=changed_model.id, result=item)
except ChartNotFoundError:
response = self.response_404()
@ -416,7 +416,7 @@ class ChartRestApi(BaseSupersetModelRestApi):
$ref: '#/components/responses/500'
"""
try:
DeleteChartCommand(g.user, pk).run()
DeleteChartCommand(pk).run()
return self.response(200, message="OK")
except ChartNotFoundError:
return self.response_404()
@ -476,7 +476,7 @@ class ChartRestApi(BaseSupersetModelRestApi):
"""
item_ids = kwargs["rison"]
try:
BulkDeleteChartCommand(g.user, item_ids).run()
BulkDeleteChartCommand(item_ids).run()
return self.response(
200,
message=ngettext(

View File

@ -17,9 +17,9 @@
import logging
from typing import List, Optional
from flask_appbuilder.security.sqla.models import User
from flask_babel import lazy_gettext as _
from superset import security_manager
from superset.charts.commands.exceptions import (
ChartBulkDeleteFailedError,
ChartBulkDeleteFailedReportsExistError,
@ -32,14 +32,12 @@ from superset.commands.exceptions import DeleteFailedError
from superset.exceptions import SupersetSecurityException
from superset.models.slice import Slice
from superset.reports.dao import ReportScheduleDAO
from superset.views.base import check_ownership
logger = logging.getLogger(__name__)
class BulkDeleteChartCommand(BaseCommand):
def __init__(self, user: User, model_ids: List[int]):
self._actor = user
def __init__(self, model_ids: List[int]):
self._model_ids = model_ids
self._models: Optional[List[Slice]] = None
@ -66,6 +64,6 @@ class BulkDeleteChartCommand(BaseCommand):
# Check ownership
for model in self._models:
try:
check_ownership(model)
security_manager.raise_for_ownership(model)
except SupersetSecurityException as ex:
raise ChartForbiddenError() from ex

View File

@ -18,8 +18,8 @@ import logging
from datetime import datetime
from typing import Any, Dict, List, Optional
from flask import g
from flask_appbuilder.models.sqla import Model
from flask_appbuilder.security.sqla.models import User
from marshmallow import ValidationError
from superset.charts.commands.exceptions import (
@ -37,15 +37,14 @@ logger = logging.getLogger(__name__)
class CreateChartCommand(CreateMixin, BaseCommand):
def __init__(self, user: User, data: Dict[str, Any]):
self._actor = user
def __init__(self, data: Dict[str, Any]):
self._properties = data.copy()
def run(self) -> Model:
self.validate()
try:
self._properties["last_saved_at"] = datetime.now()
self._properties["last_saved_by"] = self._actor
self._properties["last_saved_by"] = g.user
chart = ChartDAO.create(self._properties)
except DAOCreateFailedError as ex:
logger.exception(ex.exception)
@ -73,7 +72,7 @@ class CreateChartCommand(CreateMixin, BaseCommand):
self._properties["dashboards"] = dashboards
try:
owners = self.populate_owners(self._actor, owner_ids)
owners = self.populate_owners(owner_ids)
self._properties["owners"] = owners
except ValidationError as ex:
exceptions.append(ex)

View File

@ -18,9 +18,9 @@ import logging
from typing import Optional
from flask_appbuilder.models.sqla import Model
from flask_appbuilder.security.sqla.models import User
from flask_babel import lazy_gettext as _
from superset import security_manager
from superset.charts.commands.exceptions import (
ChartDeleteFailedError,
ChartDeleteFailedReportsExistError,
@ -34,14 +34,12 @@ from superset.exceptions import SupersetSecurityException
from superset.models.dashboard import Dashboard
from superset.models.slice import Slice
from superset.reports.dao import ReportScheduleDAO
from superset.views.base import check_ownership
logger = logging.getLogger(__name__)
class DeleteChartCommand(BaseCommand):
def __init__(self, user: User, model_id: int):
self._actor = user
def __init__(self, model_id: int):
self._model_id = model_id
self._model: Optional[Slice] = None
@ -69,6 +67,6 @@ class DeleteChartCommand(BaseCommand):
)
# Check ownership
try:
check_ownership(self._model)
security_manager.raise_for_ownership(self._model)
except SupersetSecurityException as ex:
raise ChartForbiddenError() from ex

View File

@ -18,10 +18,11 @@ import logging
from datetime import datetime
from typing import Any, Dict, List, Optional
from flask import g
from flask_appbuilder.models.sqla import Model
from flask_appbuilder.security.sqla.models import User
from marshmallow import ValidationError
from superset import security_manager
from superset.charts.commands.exceptions import (
ChartForbiddenError,
ChartInvalidError,
@ -37,7 +38,6 @@ from superset.dao.exceptions import DAOUpdateFailedError
from superset.dashboards.dao import DashboardDAO
from superset.exceptions import SupersetSecurityException
from superset.models.slice import Slice
from superset.views.base import check_ownership
logger = logging.getLogger(__name__)
@ -49,8 +49,7 @@ def is_query_context_update(properties: Dict[str, Any]) -> bool:
class UpdateChartCommand(UpdateMixin, BaseCommand):
def __init__(self, user: User, model_id: int, data: Dict[str, Any]):
self._actor = user
def __init__(self, model_id: int, data: Dict[str, Any]):
self._model_id = model_id
self._properties = data.copy()
self._model: Optional[Slice] = None
@ -60,7 +59,7 @@ class UpdateChartCommand(UpdateMixin, BaseCommand):
try:
if self._properties.get("query_context_generation") is None:
self._properties["last_saved_at"] = datetime.now()
self._properties["last_saved_by"] = self._actor
self._properties["last_saved_by"] = g.user
chart = ChartDAO.update(self._model, self._properties)
except DAOUpdateFailedError as ex:
logger.exception(ex.exception)
@ -88,8 +87,8 @@ class UpdateChartCommand(UpdateMixin, BaseCommand):
# ownership so the update can be performed by report workers
if not is_query_context_update(self._properties):
try:
check_ownership(self._model)
owners = self.populate_owners(self._actor, owner_ids)
security_manager.raise_for_ownership(self._model)
owners = self.populate_owners(owner_ids)
self._properties["owners"] = owners
except SupersetSecurityException as ex:
raise ChartForbiddenError() from ex

View File

@ -45,34 +45,28 @@ class BaseCommand(ABC):
class CreateMixin: # pylint: disable=too-few-public-methods
@staticmethod
def populate_owners(
user: User, owner_ids: Optional[List[int]] = None
) -> List[User]:
def populate_owners(owner_ids: Optional[List[int]] = None) -> List[User]:
"""
Populate list of owners, defaulting to the current user if `owner_ids` is
undefined or empty. If current user is missing in `owner_ids`, current user
is added unless belonging to the Admin role.
:param user: current user
:param owner_ids: list of owners by id's
:raises OwnersNotFoundValidationError: if at least one owner can't be resolved
:returns: Final list of owners
"""
return populate_owners(user, owner_ids, default_to_user=True)
return populate_owners(owner_ids, default_to_user=True)
class UpdateMixin: # pylint: disable=too-few-public-methods
@staticmethod
def populate_owners(
user: User, owner_ids: Optional[List[int]] = None
) -> List[User]:
def populate_owners(owner_ids: Optional[List[int]] = None) -> List[User]:
"""
Populate list of owners. If current user is missing in `owner_ids`, current user
is added unless belonging to the Admin role.
:param user: current user
:param owner_ids: list of owners by id's
:raises OwnersNotFoundValidationError: if at least one owner can't be resolved
:returns: Final list of owners
"""
return populate_owners(user, owner_ids, default_to_user=False)
return populate_owners(owner_ids, default_to_user=False)

View File

@ -18,8 +18,10 @@ from __future__ import annotations
from typing import List, Optional, TYPE_CHECKING
from flask import g
from flask_appbuilder.security.sqla.models import Role, User
from superset import security_manager
from superset.commands.exceptions import (
DatasourceNotFoundValidationError,
OwnersNotFoundValidationError,
@ -27,21 +29,20 @@ from superset.commands.exceptions import (
)
from superset.dao.exceptions import DatasourceNotFound
from superset.datasource.dao import DatasourceDAO
from superset.extensions import db, security_manager
from superset.utils.core import DatasourceType
from superset.extensions import db
from superset.utils.core import DatasourceType, get_user_id
if TYPE_CHECKING:
from superset.connectors.base.models import BaseDatasource
def populate_owners(
user: User,
owner_ids: Optional[List[int]],
default_to_user: bool,
) -> List[User]:
"""
Helper function for commands, will fetch all users from owners id's
:param user: current user
:param owner_ids: list of owners by id's
:param default_to_user: make user the owner if `owner_ids` is None or empty
:raises OwnersNotFoundValidationError: if at least one owner id can't be resolved
@ -50,12 +51,10 @@ def populate_owners(
owner_ids = owner_ids or []
owners = []
if not owner_ids and default_to_user:
return [user]
if user.id not in owner_ids and "admin" not in [
role.name.lower() for role in user.roles
]:
return [g.user]
if not (security_manager.is_admin() or get_user_id() in owner_ids):
# make sure non-admins can't remove themselves as owner by mistake
owners.append(user)
owners.append(g.user)
for owner_id in owner_ids:
owner = security_manager.get_user_by_id(owner_id)
if not owner:

View File

@ -1,25 +0,0 @@
# 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 __future__ import annotations
from superset import conf, security_manager
def is_user_admin() -> bool:
user_roles = [role.name.lower() for role in security_manager.get_user_roles()]
admin_role = conf.get("AUTH_ROLE_ADMIN").lower()
return admin_role in user_roles

View File

@ -60,9 +60,10 @@ class SelectDataRequired(DataRequired): # pylint: disable=too-few-public-method
field_flags = ()
class TableColumnInlineView(
CompactCRUDMixin, SupersetModelView
): # pylint: disable=too-many-ancestors
class TableColumnInlineView( # pylint: disable=too-many-ancestors
CompactCRUDMixin,
SupersetModelView,
):
datamodel = SQLAInterface(models.TableColumn)
# TODO TODO, review need for this on related_views
class_permission_name = "Dataset"
@ -194,9 +195,10 @@ class TableColumnInlineView(
edit_form_extra_fields = add_form_extra_fields
class SqlMetricInlineView(
CompactCRUDMixin, SupersetModelView
): # pylint: disable=too-many-ancestors
class SqlMetricInlineView( # pylint: disable=too-many-ancestors
CompactCRUDMixin,
SupersetModelView,
):
datamodel = SQLAInterface(models.SqlMetric)
class_permission_name = "Dataset"
method_permission_name = MODEL_VIEW_RW_METHOD_PERMISSION_MAP
@ -278,9 +280,9 @@ class RowLevelSecurityListWidget(
super().__init__(**kwargs)
class RowLevelSecurityFiltersModelView(
class RowLevelSecurityFiltersModelView( # pylint: disable=too-many-ancestors
SupersetModelView, DeleteMixin
): # pylint: disable=too-many-ancestors
):
datamodel = SQLAInterface(models.RowLevelSecurityFilter)
list_widget = cast(SupersetListWidget, RowLevelSecurityListWidget)

View File

@ -17,7 +17,7 @@
import logging
from typing import Any
from flask import g, Response
from flask import Response
from flask_appbuilder.api import expose, protect, rison, safe
from flask_appbuilder.models.sqla.interface import SQLAInterface
from flask_babel import ngettext
@ -130,7 +130,7 @@ class CssTemplateRestApi(BaseSupersetModelRestApi):
"""
item_ids = kwargs["rison"]
try:
BulkDeleteCssTemplateCommand(g.user, item_ids).run()
BulkDeleteCssTemplateCommand(item_ids).run()
return self.response(
200,
message=ngettext(

View File

@ -17,8 +17,6 @@
import logging
from typing import List, Optional
from flask_appbuilder.security.sqla.models import User
from superset.commands.base import BaseCommand
from superset.css_templates.commands.exceptions import (
CssTemplateBulkDeleteFailedError,
@ -32,8 +30,7 @@ logger = logging.getLogger(__name__)
class BulkDeleteCssTemplateCommand(BaseCommand):
def __init__(self, user: User, model_ids: List[int]):
self._actor = user
def __init__(self, model_ids: List[int]):
self._model_ids = model_ids
self._models: Optional[List[CssTemplate]] = None

View File

@ -23,7 +23,7 @@ from io import BytesIO
from typing import Any, Callable, Optional
from zipfile import is_zipfile, ZipFile
from flask import g, make_response, redirect, request, Response, send_file, url_for
from flask import make_response, redirect, request, Response, send_file, url_for
from flask_appbuilder import permission_name
from flask_appbuilder.api import expose, protect, rison, safe
from flask_appbuilder.hooks import before_request
@ -504,7 +504,7 @@ class DashboardRestApi(BaseSupersetModelRestApi):
except ValidationError as error:
return self.response_400(message=error.messages)
try:
new_model = CreateDashboardCommand(g.user, item).run()
new_model = CreateDashboardCommand(item).run()
return self.response(201, id=new_model.id, result=item)
except DashboardInvalidError as ex:
return self.response_422(message=ex.normalized_messages())
@ -577,7 +577,7 @@ class DashboardRestApi(BaseSupersetModelRestApi):
except ValidationError as error:
return self.response_400(message=error.messages)
try:
changed_model = UpdateDashboardCommand(g.user, pk, item).run()
changed_model = UpdateDashboardCommand(pk, item).run()
last_modified_time = changed_model.changed_on.replace(
microsecond=0
).timestamp()
@ -644,7 +644,7 @@ class DashboardRestApi(BaseSupersetModelRestApi):
$ref: '#/components/responses/500'
"""
try:
DeleteDashboardCommand(g.user, pk).run()
DeleteDashboardCommand(pk).run()
return self.response(200, message="OK")
except DashboardNotFoundError:
return self.response_404()
@ -704,7 +704,7 @@ class DashboardRestApi(BaseSupersetModelRestApi):
"""
item_ids = kwargs["rison"]
try:
BulkDeleteDashboardCommand(g.user, item_ids).run()
BulkDeleteDashboardCommand(item_ids).run()
return self.response(
200,
message=ngettext(
@ -942,6 +942,7 @@ class DashboardRestApi(BaseSupersetModelRestApi):
dashboards = DashboardDAO.find_by_ids(requested_ids)
if not dashboards:
return self.response_404()
favorited_dashboard_ids = DashboardDAO.favorited_ids(dashboards)
res = [
{"id": request_id, "value": request_id in favorited_dashboard_ids}

View File

@ -17,9 +17,9 @@
import logging
from typing import List, Optional
from flask_appbuilder.security.sqla.models import User
from flask_babel import lazy_gettext as _
from superset import security_manager
from superset.commands.base import BaseCommand
from superset.commands.exceptions import DeleteFailedError
from superset.dashboards.commands.exceptions import (
@ -32,14 +32,12 @@ from superset.dashboards.dao import DashboardDAO
from superset.exceptions import SupersetSecurityException
from superset.models.dashboard import Dashboard
from superset.reports.dao import ReportScheduleDAO
from superset.views.base import check_ownership
logger = logging.getLogger(__name__)
class BulkDeleteDashboardCommand(BaseCommand):
def __init__(self, user: User, model_ids: List[int]):
self._actor = user
def __init__(self, model_ids: List[int]):
self._model_ids = model_ids
self._models: Optional[List[Dashboard]] = None
@ -67,6 +65,6 @@ class BulkDeleteDashboardCommand(BaseCommand):
# Check ownership
for model in self._models:
try:
check_ownership(model)
security_manager.raise_for_ownership(model)
except SupersetSecurityException as ex:
raise DashboardForbiddenError() from ex

View File

@ -18,7 +18,6 @@ import logging
from typing import Any, Dict, List, Optional
from flask_appbuilder.models.sqla import Model
from flask_appbuilder.security.sqla.models import User
from marshmallow import ValidationError
from superset.commands.base import BaseCommand, CreateMixin
@ -35,8 +34,7 @@ logger = logging.getLogger(__name__)
class CreateDashboardCommand(CreateMixin, BaseCommand):
def __init__(self, user: User, data: Dict[str, Any]):
self._actor = user
def __init__(self, data: Dict[str, Any]):
self._properties = data.copy()
def run(self) -> Model:
@ -60,7 +58,7 @@ class CreateDashboardCommand(CreateMixin, BaseCommand):
exceptions.append(DashboardSlugExistsValidationError())
try:
owners = self.populate_owners(self._actor, owner_ids)
owners = self.populate_owners(owner_ids)
self._properties["owners"] = owners
except ValidationError as ex:
exceptions.append(ex)

View File

@ -18,9 +18,9 @@ import logging
from typing import Optional
from flask_appbuilder.models.sqla import Model
from flask_appbuilder.security.sqla.models import User
from flask_babel import lazy_gettext as _
from superset import security_manager
from superset.commands.base import BaseCommand
from superset.dao.exceptions import DAODeleteFailedError
from superset.dashboards.commands.exceptions import (
@ -33,14 +33,12 @@ from superset.dashboards.dao import DashboardDAO
from superset.exceptions import SupersetSecurityException
from superset.models.dashboard import Dashboard
from superset.reports.dao import ReportScheduleDAO
from superset.views.base import check_ownership
logger = logging.getLogger(__name__)
class DeleteDashboardCommand(BaseCommand):
def __init__(self, user: User, model_id: int):
self._actor = user
def __init__(self, model_id: int):
self._model_id = model_id
self._model: Optional[Dashboard] = None
@ -67,6 +65,6 @@ class DeleteDashboardCommand(BaseCommand):
)
# Check ownership
try:
check_ownership(self._model)
security_manager.raise_for_ownership(self._model)
except SupersetSecurityException as ex:
raise DashboardForbiddenError() from ex

View File

@ -19,9 +19,9 @@ import logging
from typing import Any, Dict, List, Optional
from flask_appbuilder.models.sqla import Model
from flask_appbuilder.security.sqla.models import User
from marshmallow import ValidationError
from superset import security_manager
from superset.commands.base import BaseCommand, UpdateMixin
from superset.commands.utils import populate_roles
from superset.dao.exceptions import DAOUpdateFailedError
@ -36,14 +36,12 @@ from superset.dashboards.dao import DashboardDAO
from superset.exceptions import SupersetSecurityException
from superset.extensions import db
from superset.models.dashboard import Dashboard
from superset.views.base import check_ownership
logger = logging.getLogger(__name__)
class UpdateDashboardCommand(UpdateMixin, BaseCommand):
def __init__(self, user: User, model_id: int, data: Dict[str, Any]):
self._actor = user
def __init__(self, model_id: int, data: Dict[str, Any]):
self._model_id = model_id
self._properties = data.copy()
self._model: Optional[Dashboard] = None
@ -77,7 +75,7 @@ class UpdateDashboardCommand(UpdateMixin, BaseCommand):
raise DashboardNotFoundError()
# Check ownership
try:
check_ownership(self._model)
security_manager.raise_for_ownership(self._model)
except SupersetSecurityException as ex:
raise DashboardForbiddenError() from ex
@ -89,7 +87,7 @@ class UpdateDashboardCommand(UpdateMixin, BaseCommand):
if owners_ids is None:
owners_ids = [owner.id for owner in self._model.owners]
try:
owners = self.populate_owners(self._actor, owners_ids)
owners = self.populate_owners(owners_ids)
self._properties["owners"] = owners
except ValidationError as ex:
exceptions.append(ex)

View File

@ -17,7 +17,7 @@
import logging
from typing import Any, cast
from flask import g, request, Response
from flask import request, Response
from flask_appbuilder.api import (
expose,
get_list_schema,
@ -243,7 +243,7 @@ class FilterSetRestApi(BaseSupersetModelRestApi):
"""
try:
item = self.add_model_schema.load(request.json)
new_model = CreateFilterSetCommand(g.user, dashboard_id, item).run()
new_model = CreateFilterSetCommand(dashboard_id, item).run()
return self.response(
201, **self.show_model_schema.dump(new_model, many=False)
)
@ -314,7 +314,7 @@ class FilterSetRestApi(BaseSupersetModelRestApi):
"""
try:
item = self.edit_model_schema.load(request.json)
changed_model = UpdateFilterSetCommand(g.user, dashboard_id, pk, item).run()
changed_model = UpdateFilterSetCommand(dashboard_id, pk, item).run()
return self.response(
200, **self.show_model_schema.dump(changed_model, many=False)
)
@ -374,7 +374,7 @@ class FilterSetRestApi(BaseSupersetModelRestApi):
$ref: '#/components/responses/500'
"""
try:
changed_model = DeleteFilterSetCommand(g.user, dashboard_id, pk).run()
changed_model = DeleteFilterSetCommand(dashboard_id, pk).run()
return self.response(200, id=changed_model.id)
except ValidationError as error:
return self.response_400(message=error.messages)

View File

@ -18,10 +18,9 @@ import logging
from typing import cast, Optional
from flask_appbuilder.models.sqla import Model
from flask_appbuilder.security.sqla.models import User
from superset import security_manager
from superset.common.not_authrized_object import NotAuthorizedException
from superset.common.request_contexed_based import is_user_admin
from superset.dashboards.commands.exceptions import DashboardNotFoundError
from superset.dashboards.dao import DashboardDAO
from superset.dashboards.filter_sets.commands.exceptions import (
@ -31,6 +30,7 @@ from superset.dashboards.filter_sets.commands.exceptions import (
from superset.dashboards.filter_sets.consts import USER_OWNER_TYPE
from superset.models.dashboard import Dashboard
from superset.models.filter_set import FilterSet
from superset.utils.core import get_user_id
logger = logging.getLogger(__name__)
@ -41,9 +41,7 @@ class BaseFilterSetCommand:
_filter_set_id: Optional[int]
_filter_set: Optional[FilterSet]
def __init__(self, user: User, dashboard_id: int):
self._actor = user
self._is_actor_admin = is_user_admin()
def __init__(self, dashboard_id: int):
self._dashboard_id = dashboard_id
def run(self) -> Model:
@ -54,9 +52,6 @@ class BaseFilterSetCommand:
if not self._dashboard:
raise DashboardNotFoundError()
def is_user_dashboard_owner(self) -> bool:
return self._is_actor_admin or self._dashboard.is_actor_owner()
def validate_exist_filter_use_cases_set(self) -> None: # pylint: disable=C0103
self._validate_filter_set_exists_and_set_when_exists()
self.check_ownership()
@ -70,15 +65,15 @@ class BaseFilterSetCommand:
def check_ownership(self) -> None:
try:
if not self._is_actor_admin:
if not security_manager.is_admin():
filter_set: FilterSet = cast(FilterSet, self._filter_set)
if filter_set.owner_type == USER_OWNER_TYPE:
if self._actor.id != filter_set.owner_id:
if get_user_id() != filter_set.owner_id:
raise FilterSetForbiddenError(
str(self._filter_set_id),
"The user is not the owner of the filter_set",
)
elif not self.is_user_dashboard_owner():
elif not security_manager.is_owner(self._dashboard):
raise FilterSetForbiddenError(
str(self._filter_set_id),
"The user is not an owner of the filter_set's dashboard",

View File

@ -17,9 +17,7 @@
import logging
from typing import Any, Dict
from flask import g
from flask_appbuilder.models.sqla import Model
from flask_appbuilder.security.sqla.models import User
from superset import security_manager
from superset.dashboards.filter_sets.commands.base import BaseFilterSetCommand
@ -35,14 +33,15 @@ from superset.dashboards.filter_sets.consts import (
OWNER_TYPE_FIELD,
)
from superset.dashboards.filter_sets.dao import FilterSetDAO
from superset.utils.core import get_user_id
logger = logging.getLogger(__name__)
class CreateFilterSetCommand(BaseFilterSetCommand):
# pylint: disable=C0103
def __init__(self, user: User, dashboard_id: int, data: Dict[str, Any]):
super().__init__(user, dashboard_id)
def __init__(self, dashboard_id: int, data: Dict[str, Any]):
super().__init__(dashboard_id)
self._properties = data.copy()
def run(self) -> Model:
@ -61,13 +60,13 @@ class CreateFilterSetCommand(BaseFilterSetCommand):
def _validate_owner_id_exists(self) -> None:
owner_id = self._properties[OWNER_ID_FIELD]
if not (g.user.id == owner_id or security_manager.get_user_by_id(owner_id)):
if not (get_user_id() == owner_id or security_manager.get_user_by_id(owner_id)):
raise FilterSetCreateFailedError(
str(self._dashboard_id), "owner_id does not exists"
)
def _validate_user_is_the_dashboard_owner(self) -> None:
if not self.is_user_dashboard_owner():
if not security_manager.is_owner(self._dashboard):
raise UserIsNotDashboardOwnerError(str(self._dashboard_id))
def _validate_owner_id_is_dashboard_id(self) -> None:

View File

@ -17,7 +17,6 @@
import logging
from flask_appbuilder.models.sqla import Model
from flask_appbuilder.security.sqla.models import User
from superset.dao.exceptions import DAODeleteFailedError
from superset.dashboards.filter_sets.commands.base import BaseFilterSetCommand
@ -32,8 +31,8 @@ logger = logging.getLogger(__name__)
class DeleteFilterSetCommand(BaseFilterSetCommand):
def __init__(self, user: User, dashboard_id: int, filter_set_id: int):
super().__init__(user, dashboard_id)
def __init__(self, dashboard_id: int, filter_set_id: int):
super().__init__(dashboard_id)
self._filter_set_id = filter_set_id
def run(self) -> Model:

View File

@ -18,7 +18,6 @@ import logging
from typing import Any, Dict
from flask_appbuilder.models.sqla import Model
from flask_appbuilder.security.sqla.models import User
from superset.dao.exceptions import DAOUpdateFailedError
from superset.dashboards.filter_sets.commands.base import BaseFilterSetCommand
@ -32,10 +31,8 @@ logger = logging.getLogger(__name__)
class UpdateFilterSetCommand(BaseFilterSetCommand):
def __init__(
self, user: User, dashboard_id: int, filter_set_id: int, data: Dict[str, Any]
):
super().__init__(user, dashboard_id)
def __init__(self, dashboard_id: int, filter_set_id: int, data: Dict[str, Any]):
super().__init__(dashboard_id)
self._filter_set_id = filter_set_id
self._properties = data.copy()

View File

@ -18,13 +18,14 @@ from __future__ import annotations
from typing import Any, TYPE_CHECKING
from flask import g
from sqlalchemy import and_, or_
from superset import security_manager
from superset.dashboards.filter_sets.consts import DASHBOARD_OWNER_TYPE, USER_OWNER_TYPE
from superset.models.dashboard import dashboard_user
from superset.models.filter_set import FilterSet
from superset.views.base import BaseFilter, is_user_admin
from superset.utils.core import get_user_id
from superset.views.base import BaseFilter
if TYPE_CHECKING:
from sqlalchemy.orm.query import Query
@ -32,9 +33,8 @@ if TYPE_CHECKING:
class FilterSetFilter(BaseFilter): # pylint: disable=too-few-public-methods)
def apply(self, query: Query, value: Any) -> Query:
if is_user_admin():
if security_manager.is_admin():
return query
current_user_id = g.user.id
filter_set_ids_by_dashboard_owners = ( # pylint: disable=C0103
query.from_self(FilterSet.id)
@ -42,7 +42,7 @@ class FilterSetFilter(BaseFilter): # pylint: disable=too-few-public-methods)
.filter(
and_(
FilterSet.owner_type == DASHBOARD_OWNER_TYPE,
dashboard_user.c.user_id == current_user_id,
dashboard_user.c.user_id == get_user_id(),
)
)
)
@ -51,7 +51,7 @@ class FilterSetFilter(BaseFilter): # pylint: disable=too-few-public-methods)
or_(
and_(
FilterSet.owner_type == USER_OWNER_TYPE,
FilterSet.owner_id == current_user_id,
FilterSet.owner_id == get_user_id(),
),
FilterSet.id.in_(filter_set_ids_by_dashboard_owners),
)

View File

@ -20,17 +20,17 @@ from flask import session
from superset.dashboards.filter_state.commands.utils import check_access
from superset.extensions import cache_manager
from superset.key_value.utils import get_owner, random_key
from superset.key_value.utils import random_key
from superset.temporary_cache.commands.create import CreateTemporaryCacheCommand
from superset.temporary_cache.commands.entry import Entry
from superset.temporary_cache.commands.parameters import CommandParameters
from superset.temporary_cache.utils import cache_key
from superset.utils.core import get_user_id
class CreateFilterStateCommand(CreateTemporaryCacheCommand):
def create(self, cmd_params: CommandParameters) -> str:
resource_id = cmd_params.resource_id
actor = cmd_params.actor
tab_id = cmd_params.tab_id
contextual_key = cache_key(session.get("_id"), tab_id, resource_id)
key = cache_manager.filter_state_cache.get(contextual_key)
@ -38,7 +38,7 @@ class CreateFilterStateCommand(CreateTemporaryCacheCommand):
key = random_key()
value = cast(str, cmd_params.value) # schema ensures that value is not optional
check_access(resource_id)
entry: Entry = {"owner": get_owner(actor), "value": value}
entry: Entry = {"owner": get_user_id(), "value": value}
cache_manager.filter_state_cache.set(cache_key(resource_id, key), entry)
cache_manager.filter_state_cache.set(contextual_key, key)
return key

View File

@ -18,23 +18,22 @@ from flask import session
from superset.dashboards.filter_state.commands.utils import check_access
from superset.extensions import cache_manager
from superset.key_value.utils import get_owner
from superset.temporary_cache.commands.delete import DeleteTemporaryCacheCommand
from superset.temporary_cache.commands.entry import Entry
from superset.temporary_cache.commands.exceptions import TemporaryCacheAccessDeniedError
from superset.temporary_cache.commands.parameters import CommandParameters
from superset.temporary_cache.utils import cache_key
from superset.utils.core import get_user_id
class DeleteFilterStateCommand(DeleteTemporaryCacheCommand):
def delete(self, cmd_params: CommandParameters) -> bool:
resource_id = cmd_params.resource_id
actor = cmd_params.actor
key = cache_key(resource_id, cmd_params.key)
check_access(resource_id)
entry: Entry = cache_manager.filter_state_cache.get(key)
if entry:
if entry["owner"] != get_owner(actor):
if entry["owner"] != get_user_id():
raise TemporaryCacheAccessDeniedError()
tab_id = cmd_params.tab_id
contextual_key = cache_key(session.get("_id"), tab_id, resource_id)

View File

@ -20,23 +20,23 @@ from flask import session
from superset.dashboards.filter_state.commands.utils import check_access
from superset.extensions import cache_manager
from superset.key_value.utils import get_owner, random_key
from superset.key_value.utils import random_key
from superset.temporary_cache.commands.entry import Entry
from superset.temporary_cache.commands.exceptions import TemporaryCacheAccessDeniedError
from superset.temporary_cache.commands.parameters import CommandParameters
from superset.temporary_cache.commands.update import UpdateTemporaryCacheCommand
from superset.temporary_cache.utils import cache_key
from superset.utils.core import get_user_id
class UpdateFilterStateCommand(UpdateTemporaryCacheCommand):
def update(self, cmd_params: CommandParameters) -> Optional[str]:
resource_id = cmd_params.resource_id
actor = cmd_params.actor
key = cmd_params.key
value = cast(str, cmd_params.value) # schema ensures that value is not optional
check_access(resource_id)
entry: Entry = cache_manager.filter_state_cache.get(cache_key(resource_id, key))
owner = get_owner(actor)
owner = get_user_id()
if entry:
if entry["owner"] != owner:
raise TemporaryCacheAccessDeniedError()

View File

@ -30,7 +30,7 @@ from superset.models.embedded_dashboard import EmbeddedDashboard
from superset.models.slice import Slice
from superset.security.guest_token import GuestTokenResourceType, GuestUser
from superset.utils.core import get_user_id
from superset.views.base import BaseFilter, is_user_admin
from superset.views.base import BaseFilter
from superset.views.base_api import BaseFavoriteFilter
@ -98,7 +98,7 @@ class DashboardAccessFilter(BaseFilter): # pylint: disable=too-few-public-metho
"""
def apply(self, query: Query, value: Any) -> Query:
if is_user_admin():
if security_manager.is_admin():
return query
datasource_perms = security_manager.user_view_menu_names("datasource_access")

View File

@ -16,7 +16,7 @@
# under the License.
import logging
from flask import g, request, Response
from flask import request, Response
from flask_appbuilder.api import BaseApi, expose, protect, safe
from marshmallow import ValidationError
@ -104,7 +104,6 @@ class DashboardPermalinkRestApi(BaseApi):
try:
state = self.add_model_schema.load(request.json)
key = CreateDashboardPermalinkCommand(
actor=g.user,
dashboard_id=pk,
state=state,
).run()
@ -162,7 +161,7 @@ class DashboardPermalinkRestApi(BaseApi):
$ref: '#/components/responses/500'
"""
try:
value = GetDashboardPermalinkCommand(actor=g.user, key=key).run()
value = GetDashboardPermalinkCommand(key=key).run()
if not value:
return self.response_404()
return self.response(200, **value)

View File

@ -16,7 +16,6 @@
# under the License.
import logging
from flask_appbuilder.security.sqla.models import User
from sqlalchemy.exc import SQLAlchemyError
from superset.dashboards.dao import DashboardDAO
@ -32,11 +31,9 @@ logger = logging.getLogger(__name__)
class CreateDashboardPermalinkCommand(BaseDashboardPermalinkCommand):
def __init__(
self,
actor: User,
dashboard_id: str,
state: DashboardPermalinkState,
):
self.actor = actor
self.dashboard_id = dashboard_id
self.state = state
@ -49,7 +46,6 @@ class CreateDashboardPermalinkCommand(BaseDashboardPermalinkCommand):
"state": self.state,
}
key = CreateKeyValueCommand(
actor=self.actor,
resource=self.resource,
value=value,
).run()

View File

@ -17,7 +17,6 @@
import logging
from typing import Optional
from flask_appbuilder.security.sqla.models import User
from sqlalchemy.exc import SQLAlchemyError
from superset.dashboards.commands.exceptions import DashboardNotFoundError
@ -33,8 +32,7 @@ logger = logging.getLogger(__name__)
class GetDashboardPermalinkCommand(BaseDashboardPermalinkCommand):
def __init__(self, actor: User, key: str):
self.actor = actor
def __init__(self, key: str):
self.key = key
def run(self) -> Optional[DashboardPermalinkValue]:

View File

@ -22,7 +22,7 @@ from io import BytesIO
from typing import Any, Dict, List, Optional
from zipfile import ZipFile
from flask import g, request, Response, send_file
from flask import request, Response, send_file
from flask_appbuilder.api import expose, protect, rison, safe
from flask_appbuilder.models.sqla.interface import SQLAInterface
from marshmallow import ValidationError
@ -261,7 +261,7 @@ class DatabaseRestApi(BaseSupersetModelRestApi):
except ValidationError as error:
return self.response_400(message=error.messages)
try:
new_model = CreateDatabaseCommand(g.user, item).run()
new_model = CreateDatabaseCommand(item).run()
# Return censored version for sqlalchemy URI
item["sqlalchemy_uri"] = new_model.sqlalchemy_uri
item["expose_in_sqllab"] = new_model.expose_in_sqllab
@ -342,7 +342,7 @@ class DatabaseRestApi(BaseSupersetModelRestApi):
except ValidationError as error:
return self.response_400(message=error.messages)
try:
changed_model = UpdateDatabaseCommand(g.user, pk, item).run()
changed_model = UpdateDatabaseCommand(pk, item).run()
# Return censored version for sqlalchemy URI
item["sqlalchemy_uri"] = changed_model.sqlalchemy_uri
if changed_model.parameters:
@ -404,7 +404,7 @@ class DatabaseRestApi(BaseSupersetModelRestApi):
$ref: '#/components/responses/500'
"""
try:
DeleteDatabaseCommand(g.user, pk).run()
DeleteDatabaseCommand(pk).run()
return self.response(200, message="OK")
except DatabaseNotFoundError:
return self.response_404()
@ -706,7 +706,7 @@ class DatabaseRestApi(BaseSupersetModelRestApi):
# This validates custom Schema with custom validations
except ValidationError as error:
return self.response_400(message=error.messages)
TestConnectionDatabaseCommand(g.user, item).run()
TestConnectionDatabaseCommand(item).run()
return self.response(200, message="OK")
@expose("/<int:pk>/related_objects/", methods=["GET"])
@ -1174,6 +1174,6 @@ class DatabaseRestApi(BaseSupersetModelRestApi):
]
raise InvalidParametersError(errors) from ex
command = ValidateDatabaseParametersCommand(g.user, payload)
command = ValidateDatabaseParametersCommand(payload)
command.run()
return self.response(200, message="OK")

View File

@ -18,7 +18,6 @@ import logging
from typing import Any, Dict, List, Optional
from flask_appbuilder.models.sqla import Model
from flask_appbuilder.security.sqla.models import User
from marshmallow import ValidationError
from superset.commands.base import BaseCommand
@ -38,8 +37,7 @@ logger = logging.getLogger(__name__)
class CreateDatabaseCommand(BaseCommand):
def __init__(self, user: User, data: Dict[str, Any]):
self._actor = user
def __init__(self, data: Dict[str, Any]):
self._properties = data.copy()
def run(self) -> Model:
@ -47,7 +45,7 @@ class CreateDatabaseCommand(BaseCommand):
try:
# Test connection before starting create transaction
TestConnectionDatabaseCommand(self._actor, self._properties).run()
TestConnectionDatabaseCommand(self._properties).run()
except Exception as ex:
event_logger.log_with_context(
action=f"db_creation_failed.{ex.__class__.__name__}",

View File

@ -18,7 +18,6 @@ import logging
from typing import Optional
from flask_appbuilder.models.sqla import Model
from flask_appbuilder.security.sqla.models import User
from flask_babel import lazy_gettext as _
from superset.commands.base import BaseCommand
@ -37,8 +36,7 @@ logger = logging.getLogger(__name__)
class DeleteDatabaseCommand(BaseCommand):
def __init__(self, user: User, model_id: int):
self._actor = user
def __init__(self, model_id: int):
self._model_id = model_id
self._model: Optional[Database] = None

View File

@ -20,7 +20,6 @@ from contextlib import closing
from typing import Any, Dict, Optional
from flask import current_app as app
from flask_appbuilder.security.sqla.models import User
from flask_babel import gettext as _
from func_timeout import func_timeout, FunctionTimedOut
from sqlalchemy.engine import Engine
@ -39,14 +38,12 @@ from superset.errors import ErrorLevel, SupersetErrorType
from superset.exceptions import SupersetSecurityException, SupersetTimeoutException
from superset.extensions import event_logger
from superset.models.core import Database
from superset.utils.core import override_user
logger = logging.getLogger(__name__)
class TestConnectionDatabaseCommand(BaseCommand):
def __init__(self, user: User, data: Dict[str, Any]):
self._actor = user
def __init__(self, data: Dict[str, Any]):
self._properties = data.copy()
self._model: Optional[Database] = None
@ -77,47 +74,41 @@ class TestConnectionDatabaseCommand(BaseCommand):
database.set_sqlalchemy_uri(uri)
database.db_engine_spec.mutate_db_for_connection_test(database)
with override_user(self._actor):
engine = database.get_sqla_engine()
event_logger.log_with_context(
action="test_connection_attempt",
engine=database.db_engine_spec.__name__,
engine = database.get_sqla_engine()
event_logger.log_with_context(
action="test_connection_attempt",
engine=database.db_engine_spec.__name__,
)
def ping(engine: Engine) -> bool:
with closing(engine.raw_connection()) as conn:
return engine.dialect.do_ping(conn)
try:
alive = func_timeout(
int(app.config["TEST_DATABASE_CONNECTION_TIMEOUT"].total_seconds()),
ping,
args=(engine,),
)
def ping(engine: Engine) -> bool:
with closing(engine.raw_connection()) as conn:
return engine.dialect.do_ping(conn)
try:
alive = func_timeout(
int(
app.config[
"TEST_DATABASE_CONNECTION_TIMEOUT"
].total_seconds()
),
ping,
args=(engine,),
)
except (sqlite3.ProgrammingError, RuntimeError):
# SQLite can't run on a separate thread, so ``func_timeout`` fails
# RuntimeError catches the equivalent error from duckdb.
alive = engine.dialect.do_ping(engine)
except FunctionTimedOut as ex:
raise SupersetTimeoutException(
error_type=SupersetErrorType.CONNECTION_DATABASE_TIMEOUT,
message=(
"Please check your connection details and database settings, "
"and ensure that your database is accepting connections, "
"then try connecting again."
),
level=ErrorLevel.ERROR,
extra={"sqlalchemy_uri": database.sqlalchemy_uri},
) from ex
except Exception: # pylint: disable=broad-except
alive = False
if not alive:
raise DBAPIError(None, None, None)
except (sqlite3.ProgrammingError, RuntimeError):
# SQLite can't run on a separate thread, so ``func_timeout`` fails
# RuntimeError catches the equivalent error from duckdb.
alive = engine.dialect.do_ping(engine)
except FunctionTimedOut as ex:
raise SupersetTimeoutException(
error_type=SupersetErrorType.CONNECTION_DATABASE_TIMEOUT,
message=(
"Please check your connection details and database settings, "
"and ensure that your database is accepting connections, "
"then try connecting again."
),
level=ErrorLevel.ERROR,
extra={"sqlalchemy_uri": database.sqlalchemy_uri},
) from ex
except Exception: # pylint: disable=broad-except
alive = False
if not alive:
raise DBAPIError(None, None, None)
# Log succesful connection test with engine
event_logger.log_with_context(

View File

@ -18,7 +18,6 @@ import logging
from typing import Any, Dict, List, Optional
from flask_appbuilder.models.sqla import Model
from flask_appbuilder.security.sqla.models import User
from marshmallow import ValidationError
from superset.commands.base import BaseCommand
@ -38,8 +37,7 @@ logger = logging.getLogger(__name__)
class UpdateDatabaseCommand(BaseCommand):
def __init__(self, user: User, model_id: int, data: Dict[str, Any]):
self._actor = user
def __init__(self, model_id: int, data: Dict[str, Any]):
self._properties = data.copy()
self._model_id = model_id
self._model: Optional[Database] = None

View File

@ -18,7 +18,6 @@ import json
from contextlib import closing
from typing import Any, Dict, Optional
from flask_appbuilder.security.sqla.models import User
from flask_babel import gettext as __
from superset.commands.base import BaseCommand
@ -35,14 +34,12 @@ from superset.db_engine_specs.base import BasicParametersMixin
from superset.errors import ErrorLevel, SupersetError, SupersetErrorType
from superset.extensions import event_logger
from superset.models.core import Database
from superset.utils.core import override_user
BYPASS_VALIDATION_ENGINES = {"bigquery"}
class ValidateDatabaseParametersCommand(BaseCommand):
def __init__(self, user: User, parameters: Dict[str, Any]):
self._actor = user
def __init__(self, parameters: Dict[str, Any]):
self._properties = parameters.copy()
self._model: Optional[Database] = None
@ -117,22 +114,21 @@ class ValidateDatabaseParametersCommand(BaseCommand):
database.set_sqlalchemy_uri(sqlalchemy_uri)
database.db_engine_spec.mutate_db_for_connection_test(database)
with override_user(self._actor):
engine = database.get_sqla_engine()
try:
with closing(engine.raw_connection()) as conn:
alive = engine.dialect.do_ping(conn)
except Exception as ex:
url = make_url_safe(sqlalchemy_uri)
context = {
"hostname": url.host,
"password": url.password,
"port": url.port,
"username": url.username,
"database": url.database,
}
errors = database.db_engine_spec.extract_errors(ex, context)
raise DatabaseTestConnectionFailedError(errors) from ex
engine = database.get_sqla_engine()
try:
with closing(engine.raw_connection()) as conn:
alive = engine.dialect.do_ping(conn)
except Exception as ex:
url = make_url_safe(sqlalchemy_uri)
context = {
"hostname": url.host,
"password": url.password,
"port": url.port,
"username": url.username,
"database": url.database,
}
errors = database.db_engine_spec.extract_errors(ex, context)
raise DatabaseTestConnectionFailedError(errors) from ex
if not alive:
raise DatabaseOfflineError(

View File

@ -23,7 +23,7 @@ from zipfile import is_zipfile, ZipFile
import simplejson
import yaml
from flask import g, make_response, request, Response, send_file
from flask import make_response, request, Response, send_file
from flask_appbuilder.api import expose, protect, rison, safe
from flask_appbuilder.models.sqla.interface import SQLAInterface
from flask_babel import ngettext
@ -264,7 +264,7 @@ class DatasetRestApi(BaseSupersetModelRestApi):
return self.response_400(message=error.messages)
try:
new_model = CreateDatasetCommand(g.user, item).run()
new_model = CreateDatasetCommand(item).run()
return self.response(201, id=new_model.id, result=item)
except DatasetInvalidError as ex:
return self.response_422(message=ex.normalized_messages())
@ -344,11 +344,9 @@ class DatasetRestApi(BaseSupersetModelRestApi):
except ValidationError as error:
return self.response_400(message=error.messages)
try:
changed_model = UpdateDatasetCommand(
g.user, pk, item, override_columns
).run()
changed_model = UpdateDatasetCommand(pk, item, override_columns).run()
if override_columns:
RefreshDatasetCommand(g.user, pk).run()
RefreshDatasetCommand(pk).run()
response = self.response(200, id=changed_model.id, result=item)
except DatasetNotFoundError:
response = self.response_404()
@ -407,7 +405,7 @@ class DatasetRestApi(BaseSupersetModelRestApi):
$ref: '#/components/responses/500'
"""
try:
DeleteDatasetCommand(g.user, pk).run()
DeleteDatasetCommand(pk).run()
return self.response(200, message="OK")
except DatasetNotFoundError:
return self.response_404()
@ -547,7 +545,7 @@ class DatasetRestApi(BaseSupersetModelRestApi):
$ref: '#/components/responses/500'
"""
try:
RefreshDatasetCommand(g.user, pk).run()
RefreshDatasetCommand(pk).run()
return self.response(200, message="OK")
except DatasetNotFoundError:
return self.response_404()
@ -671,7 +669,7 @@ class DatasetRestApi(BaseSupersetModelRestApi):
"""
item_ids = kwargs["rison"]
try:
BulkDeleteDatasetCommand(g.user, item_ids).run()
BulkDeleteDatasetCommand(item_ids).run()
return self.response(
200,
message=ngettext(
@ -812,7 +810,7 @@ class DatasetRestApi(BaseSupersetModelRestApi):
"""
try:
force = parse_boolean_string(request.args.get("force"))
rv = SamplesDatasetCommand(g.user, pk, force).run()
rv = SamplesDatasetCommand(pk, force).run()
response_data = simplejson.dumps(
{"result": rv},
default=json_int_dttm_ser,

View File

@ -16,7 +16,7 @@
# under the License.
import logging
from flask import g, Response
from flask import Response
from flask_appbuilder.api import expose, permission_name, protect, safe
from flask_appbuilder.models.sqla.interface import SQLAInterface
@ -91,7 +91,7 @@ class DatasetColumnsRestApi(BaseSupersetModelRestApi):
$ref: '#/components/responses/500'
"""
try:
DeleteDatasetColumnCommand(g.user, pk, column_id).run()
DeleteDatasetColumnCommand(pk, column_id).run()
return self.response(200, message="OK")
except DatasetColumnNotFoundError:
return self.response_404()

View File

@ -18,8 +18,8 @@ import logging
from typing import Optional
from flask_appbuilder.models.sqla import Model
from flask_appbuilder.security.sqla.models import User
from superset import security_manager
from superset.commands.base import BaseCommand
from superset.connectors.sqla.models import TableColumn
from superset.dao.exceptions import DAODeleteFailedError
@ -30,14 +30,12 @@ from superset.datasets.columns.commands.exceptions import (
)
from superset.datasets.dao import DatasetDAO
from superset.exceptions import SupersetSecurityException
from superset.views.base import check_ownership
logger = logging.getLogger(__name__)
class DeleteDatasetColumnCommand(BaseCommand):
def __init__(self, user: User, dataset_id: int, model_id: int):
self._actor = user
def __init__(self, dataset_id: int, model_id: int):
self._dataset_id = dataset_id
self._model_id = model_id
self._model: Optional[TableColumn] = None
@ -60,6 +58,6 @@ class DeleteDatasetColumnCommand(BaseCommand):
raise DatasetColumnNotFoundError()
# Check ownership
try:
check_ownership(self._model)
security_manager.raise_for_ownership(self._model)
except SupersetSecurityException as ex:
raise DatasetColumnForbiddenError() from ex

View File

@ -17,8 +17,7 @@
import logging
from typing import List, Optional
from flask_appbuilder.security.sqla.models import User
from superset import security_manager
from superset.commands.base import BaseCommand
from superset.commands.exceptions import DeleteFailedError
from superset.connectors.sqla.models import SqlaTable
@ -29,15 +28,13 @@ from superset.datasets.commands.exceptions import (
)
from superset.datasets.dao import DatasetDAO
from superset.exceptions import SupersetSecurityException
from superset.extensions import db, security_manager
from superset.views.base import check_ownership
from superset.extensions import db
logger = logging.getLogger(__name__)
class BulkDeleteDatasetCommand(BaseCommand):
def __init__(self, user: User, model_ids: List[int]):
self._actor = user
def __init__(self, model_ids: List[int]):
self._model_ids = model_ids
self._models: Optional[List[SqlaTable]] = None
@ -84,6 +81,6 @@ class BulkDeleteDatasetCommand(BaseCommand):
# Check ownership
for model in self._models:
try:
check_ownership(model)
security_manager.raise_for_ownership(model)
except SupersetSecurityException as ex:
raise DatasetForbiddenError() from ex

View File

@ -18,7 +18,6 @@ import logging
from typing import Any, Dict, List, Optional
from flask_appbuilder.models.sqla import Model
from flask_appbuilder.security.sqla.models import User
from marshmallow import ValidationError
from sqlalchemy.exc import SQLAlchemyError
@ -38,8 +37,7 @@ logger = logging.getLogger(__name__)
class CreateDatasetCommand(CreateMixin, BaseCommand):
def __init__(self, user: User, data: Dict[str, Any]):
self._actor = user
def __init__(self, data: Dict[str, Any]):
self._properties = data.copy()
def run(self) -> Model:
@ -89,7 +87,7 @@ class CreateDatasetCommand(CreateMixin, BaseCommand):
exceptions.append(TableNotFoundValidationError(table_name))
try:
owners = self.populate_owners(self._actor, owner_ids)
owners = self.populate_owners(owner_ids)
self._properties["owners"] = owners
except ValidationError as ex:
exceptions.append(ex)

View File

@ -18,9 +18,9 @@ import logging
from typing import Optional
from flask_appbuilder.models.sqla import Model
from flask_appbuilder.security.sqla.models import User
from sqlalchemy.exc import SQLAlchemyError
from superset import security_manager
from superset.commands.base import BaseCommand
from superset.connectors.sqla.models import SqlaTable
from superset.dao.exceptions import DAODeleteFailedError
@ -31,15 +31,13 @@ from superset.datasets.commands.exceptions import (
)
from superset.datasets.dao import DatasetDAO
from superset.exceptions import SupersetSecurityException
from superset.extensions import db, security_manager
from superset.views.base import check_ownership
from superset.extensions import db
logger = logging.getLogger(__name__)
class DeleteDatasetCommand(BaseCommand):
def __init__(self, user: User, model_id: int):
self._actor = user
def __init__(self, model_id: int):
self._model_id = model_id
self._model: Optional[SqlaTable] = None
@ -85,6 +83,6 @@ class DeleteDatasetCommand(BaseCommand):
raise DatasetNotFoundError()
# Check ownership
try:
check_ownership(self._model)
security_manager.raise_for_ownership(self._model)
except SupersetSecurityException as ex:
raise DatasetForbiddenError() from ex

View File

@ -18,8 +18,8 @@ import logging
from typing import Optional
from flask_appbuilder.models.sqla import Model
from flask_appbuilder.security.sqla.models import User
from superset import security_manager
from superset.commands.base import BaseCommand
from superset.connectors.sqla.models import SqlaTable
from superset.datasets.commands.exceptions import (
@ -29,14 +29,12 @@ from superset.datasets.commands.exceptions import (
)
from superset.datasets.dao import DatasetDAO
from superset.exceptions import SupersetSecurityException
from superset.views.base import check_ownership
logger = logging.getLogger(__name__)
class RefreshDatasetCommand(BaseCommand):
def __init__(self, user: User, model_id: int):
self._actor = user
def __init__(self, model_id: int):
self._model_id = model_id
self._model: Optional[SqlaTable] = None
@ -58,6 +56,6 @@ class RefreshDatasetCommand(BaseCommand):
raise DatasetNotFoundError()
# Check ownership
try:
check_ownership(self._model)
security_manager.raise_for_ownership(self._model)
except SupersetSecurityException as ex:
raise DatasetForbiddenError() from ex

View File

@ -17,8 +17,7 @@
import logging
from typing import Any, Dict, Optional
from flask_appbuilder.security.sqla.models import User
from superset import security_manager
from superset.commands.base import BaseCommand
from superset.common.chart_data import ChartDataResultType
from superset.common.query_context_factory import QueryContextFactory
@ -33,14 +32,12 @@ from superset.datasets.commands.exceptions import (
from superset.datasets.dao import DatasetDAO
from superset.exceptions import SupersetSecurityException
from superset.utils.core import QueryStatus
from superset.views.base import check_ownership
logger = logging.getLogger(__name__)
class SamplesDatasetCommand(BaseCommand):
def __init__(self, user: User, model_id: int, force: bool):
self._actor = user
def __init__(self, model_id: int, force: bool):
self._model_id = model_id
self._force = force
self._model: Optional[SqlaTable] = None
@ -78,6 +75,6 @@ class SamplesDatasetCommand(BaseCommand):
raise DatasetNotFoundError()
# Check ownership
try:
check_ownership(self._model)
security_manager.raise_for_ownership(self._model)
except SupersetSecurityException as ex:
raise DatasetForbiddenError() from ex

View File

@ -19,9 +19,9 @@ from collections import Counter
from typing import Any, Dict, List, Optional
from flask_appbuilder.models.sqla import Model
from flask_appbuilder.security.sqla.models import User
from marshmallow import ValidationError
from superset import security_manager
from superset.commands.base import BaseCommand, UpdateMixin
from superset.connectors.sqla.models import SqlaTable
from superset.dao.exceptions import DAOUpdateFailedError
@ -41,7 +41,6 @@ from superset.datasets.commands.exceptions import (
)
from superset.datasets.dao import DatasetDAO
from superset.exceptions import SupersetSecurityException
from superset.views.base import check_ownership
logger = logging.getLogger(__name__)
@ -49,12 +48,10 @@ logger = logging.getLogger(__name__)
class UpdateDatasetCommand(UpdateMixin, BaseCommand):
def __init__(
self,
user: User,
model_id: int,
data: Dict[str, Any],
override_columns: bool = False,
):
self._actor = user
self._model_id = model_id
self._properties = data.copy()
self._model: Optional[SqlaTable] = None
@ -83,7 +80,7 @@ class UpdateDatasetCommand(UpdateMixin, BaseCommand):
raise DatasetNotFoundError()
# Check ownership
try:
check_ownership(self._model)
security_manager.raise_for_ownership(self._model)
except SupersetSecurityException as ex:
raise DatasetForbiddenError() from ex
@ -99,7 +96,7 @@ class UpdateDatasetCommand(UpdateMixin, BaseCommand):
exceptions.append(DatabaseChangeValidationError())
# Validate/Populate owner
try:
owners = self.populate_owners(self._actor, owner_ids)
owners = self.populate_owners(owner_ids)
self._properties["owners"] = owners
except ValidationError as ex:
exceptions.append(ex)

View File

@ -17,7 +17,6 @@
import logging
from typing import Any, Dict, List, Optional
from flask import current_app
from sqlalchemy.exc import SQLAlchemyError
from superset.connectors.sqla.models import SqlaTable, SqlMetric, TableColumn
@ -36,14 +35,6 @@ class DatasetDAO(BaseDAO): # pylint: disable=too-many-public-methods
model_cls = SqlaTable
base_filter = DatasourceFilter
@staticmethod
def get_owner_by_id(owner_id: int) -> Optional[object]:
return (
db.session.query(current_app.appbuilder.sm.user_model)
.filter_by(id=owner_id)
.one_or_none()
)
@staticmethod
def get_database_by_id(database_id: int) -> Optional[Database]:
try:

View File

@ -16,7 +16,7 @@
# under the License.
import logging
from flask import g, Response
from flask import Response
from flask_appbuilder.api import expose, permission_name, protect, safe
from flask_appbuilder.models.sqla.interface import SQLAInterface
@ -91,7 +91,7 @@ class DatasetMetricRestApi(BaseSupersetModelRestApi):
$ref: '#/components/responses/500'
"""
try:
DeleteDatasetMetricCommand(g.user, pk, metric_id).run()
DeleteDatasetMetricCommand(pk, metric_id).run()
return self.response(200, message="OK")
except DatasetMetricNotFoundError:
return self.response_404()

View File

@ -18,8 +18,8 @@ import logging
from typing import Optional
from flask_appbuilder.models.sqla import Model
from flask_appbuilder.security.sqla.models import User
from superset import security_manager
from superset.commands.base import BaseCommand
from superset.connectors.sqla.models import SqlMetric
from superset.dao.exceptions import DAODeleteFailedError
@ -30,14 +30,12 @@ from superset.datasets.metrics.commands.exceptions import (
DatasetMetricNotFoundError,
)
from superset.exceptions import SupersetSecurityException
from superset.views.base import check_ownership
logger = logging.getLogger(__name__)
class DeleteDatasetMetricCommand(BaseCommand):
def __init__(self, user: User, dataset_id: int, model_id: int):
self._actor = user
def __init__(self, dataset_id: int, model_id: int):
self._dataset_id = dataset_id
self._model_id = model_id
self._model: Optional[SqlMetric] = None
@ -60,6 +58,6 @@ class DeleteDatasetMetricCommand(BaseCommand):
raise DatasetMetricNotFoundError()
# Check ownership
try:
check_ownership(self._model)
security_manager.raise_for_ownership(self._model)
except SupersetSecurityException as ex:
raise DatasetMetricForbiddenError() from ex

View File

@ -54,7 +54,6 @@ class GetExploreCommand(BaseCommand, ABC):
self,
params: CommandParameters,
) -> None:
self._actor = params.actor
self._permalink_key = params.permalink_key
self._form_data_key = params.form_data_key
self._dataset_id = params.dataset_id
@ -66,7 +65,7 @@ class GetExploreCommand(BaseCommand, ABC):
initial_form_data = {}
if self._permalink_key is not None:
command = GetExplorePermalinkCommand(self._actor, self._permalink_key)
command = GetExplorePermalinkCommand(self._permalink_key)
permalink_value = command.run()
if not permalink_value:
raise ExplorePermalinkGetFailedError()
@ -76,9 +75,7 @@ class GetExploreCommand(BaseCommand, ABC):
if url_params:
initial_form_data["url_params"] = dict(url_params)
elif self._form_data_key:
parameters = FormDataCommandParameters(
actor=self._actor, key=self._form_data_key
)
parameters = FormDataCommandParameters(key=self._form_data_key)
value = GetFormDataCommand(parameters).run()
initial_form_data = json.loads(value) if value else {}

View File

@ -16,7 +16,7 @@
# under the License.
import logging
from flask import g, request, Response
from flask import request, Response
from flask_appbuilder.api import BaseApi, expose, protect, safe
from marshmallow import ValidationError
@ -102,7 +102,6 @@ class ExploreFormDataRestApi(BaseApi):
item = self.add_model_schema.load(request.json)
tab_id = request.args.get("tab_id")
args = CommandParameters(
actor=g.user,
datasource_id=item["datasource_id"],
datasource_type=item["datasource_type"],
chart_id=item.get("chart_id"),
@ -173,7 +172,6 @@ class ExploreFormDataRestApi(BaseApi):
item = self.edit_model_schema.load(request.json)
tab_id = request.args.get("tab_id")
args = CommandParameters(
actor=g.user,
datasource_id=item["datasource_id"],
datasource_type=item["datasource_type"],
chart_id=item.get("chart_id"),
@ -233,7 +231,7 @@ class ExploreFormDataRestApi(BaseApi):
$ref: '#/components/responses/500'
"""
try:
args = CommandParameters(actor=g.user, key=key)
args = CommandParameters(key=key)
form_data = GetFormDataCommand(args).run()
if not form_data:
return self.response_404()
@ -285,7 +283,7 @@ class ExploreFormDataRestApi(BaseApi):
$ref: '#/components/responses/500'
"""
try:
args = CommandParameters(actor=g.user, key=key)
args = CommandParameters(key=key)
result = DeleteFormDataCommand(args).run()
if not result:
return self.response_404()

View File

@ -24,10 +24,10 @@ from superset.explore.form_data.commands.parameters import CommandParameters
from superset.explore.form_data.commands.state import TemporaryExploreState
from superset.explore.form_data.commands.utils import check_access
from superset.extensions import cache_manager
from superset.key_value.utils import get_owner, random_key
from superset.key_value.utils import random_key
from superset.temporary_cache.commands.exceptions import TemporaryCacheCreateFailedError
from superset.temporary_cache.utils import cache_key
from superset.utils.core import DatasourceType
from superset.utils.core import DatasourceType, get_user_id
from superset.utils.schema import validate_json
logger = logging.getLogger(__name__)
@ -44,9 +44,8 @@ class CreateFormDataCommand(BaseCommand):
datasource_type = self._cmd_params.datasource_type
chart_id = self._cmd_params.chart_id
tab_id = self._cmd_params.tab_id
actor = self._cmd_params.actor
form_data = self._cmd_params.form_data
check_access(datasource_id, chart_id, actor, datasource_type)
check_access(datasource_id, chart_id, datasource_type)
contextual_key = cache_key(
session.get("_id"), tab_id, datasource_id, chart_id, datasource_type
)
@ -55,7 +54,7 @@ class CreateFormDataCommand(BaseCommand):
key = random_key()
if form_data:
state: TemporaryExploreState = {
"owner": get_owner(actor),
"owner": get_user_id(),
"datasource_id": datasource_id,
"datasource_type": DatasourceType(datasource_type),
"chart_id": chart_id,

View File

@ -26,13 +26,12 @@ from superset.explore.form_data.commands.parameters import CommandParameters
from superset.explore.form_data.commands.state import TemporaryExploreState
from superset.explore.form_data.commands.utils import check_access
from superset.extensions import cache_manager
from superset.key_value.utils import get_owner
from superset.temporary_cache.commands.exceptions import (
TemporaryCacheAccessDeniedError,
TemporaryCacheDeleteFailedError,
)
from superset.temporary_cache.utils import cache_key
from superset.utils.core import DatasourceType
from superset.utils.core import DatasourceType, get_user_id
logger = logging.getLogger(__name__)
@ -43,7 +42,6 @@ class DeleteFormDataCommand(BaseCommand, ABC):
def run(self) -> bool:
try:
actor = self._cmd_params.actor
key = self._cmd_params.key
state: TemporaryExploreState = cache_manager.explore_form_data_cache.get(
key
@ -52,8 +50,8 @@ class DeleteFormDataCommand(BaseCommand, ABC):
datasource_id: int = state["datasource_id"]
chart_id: Optional[int] = state["chart_id"]
datasource_type = DatasourceType(state["datasource_type"])
check_access(datasource_id, chart_id, actor, datasource_type)
if state["owner"] != get_owner(actor):
check_access(datasource_id, chart_id, datasource_type)
if state["owner"] != get_user_id():
raise TemporaryCacheAccessDeniedError()
tab_id = self._cmd_params.tab_id
contextual_key = cache_key(

View File

@ -40,7 +40,6 @@ class GetFormDataCommand(BaseCommand, ABC):
def run(self) -> Optional[str]:
try:
actor = self._cmd_params.actor
key = self._cmd_params.key
state: TemporaryExploreState = cache_manager.explore_form_data_cache.get(
key
@ -49,7 +48,6 @@ class GetFormDataCommand(BaseCommand, ABC):
check_access(
state["datasource_id"],
state["chart_id"],
actor,
DatasourceType(state["datasource_type"]),
)
if self._refresh_timeout:

View File

@ -17,14 +17,11 @@
from dataclasses import dataclass
from typing import Optional
from flask_appbuilder.security.sqla.models import User
from superset.utils.core import DatasourceType
@dataclass
class CommandParameters:
actor: User
datasource_type: DatasourceType = DatasourceType.TABLE
datasource_id: int = 0
chart_id: int = 0

View File

@ -26,13 +26,13 @@ from superset.explore.form_data.commands.parameters import CommandParameters
from superset.explore.form_data.commands.state import TemporaryExploreState
from superset.explore.form_data.commands.utils import check_access
from superset.extensions import cache_manager
from superset.key_value.utils import get_owner, random_key
from superset.key_value.utils import random_key
from superset.temporary_cache.commands.exceptions import (
TemporaryCacheAccessDeniedError,
TemporaryCacheUpdateFailedError,
)
from superset.temporary_cache.utils import cache_key
from superset.utils.core import DatasourceType
from superset.utils.core import DatasourceType, get_user_id
from superset.utils.schema import validate_json
logger = logging.getLogger(__name__)
@ -51,14 +51,13 @@ class UpdateFormDataCommand(BaseCommand, ABC):
datasource_id = self._cmd_params.datasource_id
chart_id = self._cmd_params.chart_id
datasource_type = self._cmd_params.datasource_type
actor = self._cmd_params.actor
key = self._cmd_params.key
form_data = self._cmd_params.form_data
check_access(datasource_id, chart_id, actor, datasource_type)
check_access(datasource_id, chart_id, datasource_type)
state: TemporaryExploreState = cache_manager.explore_form_data_cache.get(
key
)
owner = get_owner(actor)
owner = get_user_id()
if state and form_data:
if state["owner"] != owner:
raise TemporaryCacheAccessDeniedError()

View File

@ -16,8 +16,6 @@
# under the License.
from typing import Optional
from flask_appbuilder.security.sqla.models import User
from superset.charts.commands.exceptions import (
ChartAccessDeniedError,
ChartNotFoundError,
@ -37,11 +35,10 @@ from superset.utils.core import DatasourceType
def check_access(
datasource_id: int,
chart_id: Optional[int],
actor: User,
datasource_type: DatasourceType,
) -> None:
try:
explore_check_access(datasource_id, chart_id, actor, datasource_type)
explore_check_access(datasource_id, chart_id, datasource_type)
except (ChartNotFoundError, DatasetNotFoundError) as ex:
raise TemporaryCacheResourceNotFoundError from ex
except (ChartAccessDeniedError, DatasetAccessDeniedError) as ex:

View File

@ -16,7 +16,7 @@
# under the License.
import logging
from flask import g, request, Response
from flask import request, Response
from flask_appbuilder.api import BaseApi, expose, protect, safe
from marshmallow import ValidationError
@ -100,7 +100,7 @@ class ExplorePermalinkRestApi(BaseApi):
"""
try:
state = self.add_model_schema.load(request.json)
key = CreateExplorePermalinkCommand(actor=g.user, state=state).run()
key = CreateExplorePermalinkCommand(state=state).run()
http_origin = request.headers.environ.get("HTTP_ORIGIN")
url = f"{http_origin}/superset/explore/p/{key}/"
return self.response(201, key=key, url=url)
@ -156,7 +156,7 @@ class ExplorePermalinkRestApi(BaseApi):
$ref: '#/components/responses/500'
"""
try:
value = GetExplorePermalinkCommand(actor=g.user, key=key).run()
value = GetExplorePermalinkCommand(key=key).run()
if not value:
return self.response_404()
return self.response(200, **value)

View File

@ -17,7 +17,6 @@
import logging
from typing import Any, Dict, Optional
from flask_appbuilder.security.sqla.models import User
from sqlalchemy.exc import SQLAlchemyError
from superset.explore.permalink.commands.base import BaseExplorePermalinkCommand
@ -31,8 +30,7 @@ logger = logging.getLogger(__name__)
class CreateExplorePermalinkCommand(BaseExplorePermalinkCommand):
def __init__(self, actor: User, state: Dict[str, Any]):
self.actor = actor
def __init__(self, state: Dict[str, Any]):
self.chart_id: Optional[int] = state["formData"].get("slice_id")
self.datasource: str = state["formData"]["datasource"]
self.state = state
@ -43,9 +41,7 @@ class CreateExplorePermalinkCommand(BaseExplorePermalinkCommand):
d_id, d_type = self.datasource.split("__")
datasource_id = int(d_id)
datasource_type = DatasourceType(d_type)
check_chart_access(
datasource_id, self.chart_id, self.actor, datasource_type
)
check_chart_access(datasource_id, self.chart_id, datasource_type)
value = {
"chartId": self.chart_id,
"datasourceId": datasource_id,
@ -54,7 +50,6 @@ class CreateExplorePermalinkCommand(BaseExplorePermalinkCommand):
"state": self.state,
}
command = CreateKeyValueCommand(
actor=self.actor,
resource=self.resource,
value=value,
)

View File

@ -17,7 +17,6 @@
import logging
from typing import Optional
from flask_appbuilder.security.sqla.models import User
from sqlalchemy.exc import SQLAlchemyError
from superset.datasets.commands.exceptions import DatasetNotFoundError
@ -34,8 +33,7 @@ logger = logging.getLogger(__name__)
class GetExplorePermalinkCommand(BaseExplorePermalinkCommand):
def __init__(self, actor: User, key: str):
self.actor = actor
def __init__(self, key: str):
self.key = key
def run(self) -> Optional[ExplorePermalinkValue]:
@ -55,7 +53,7 @@ class GetExplorePermalinkCommand(BaseExplorePermalinkCommand):
datasource_type = DatasourceType(
value.get("datasourceType", DatasourceType.TABLE)
)
check_chart_access(datasource_id, chart_id, self.actor, datasource_type)
check_chart_access(datasource_id, chart_id, datasource_type)
return value
return None
except (

View File

@ -16,8 +16,6 @@
# under the License.
from typing import Optional
from flask_appbuilder.security.sqla.models import User
from superset import security_manager
from superset.charts.commands.exceptions import (
ChartAccessDeniedError,
@ -36,8 +34,6 @@ from superset.datasets.commands.exceptions import (
from superset.datasets.dao import DatasetDAO
from superset.queries.dao import QueryDAO
from superset.utils.core import DatasourceType
from superset.views.base import is_user_admin
from superset.views.utils import is_owner
def check_dataset_access(dataset_id: int) -> Optional[bool]:
@ -80,7 +76,6 @@ def check_datasource_access(
def check_access(
datasource_id: int,
chart_id: Optional[int],
actor: User,
datasource_type: DatasourceType,
) -> Optional[bool]:
check_datasource_access(datasource_id, datasource_type)
@ -88,11 +83,9 @@ def check_access(
return True
chart = ChartDAO.find_by_id(chart_id)
if chart:
can_access_chart = (
is_user_admin()
or is_owner(chart, actor)
or security_manager.can_access("can_read", "Chart")
)
can_access_chart = security_manager.is_owner(
chart
) or security_manager.can_access("can_read", "Chart")
if can_access_chart:
return True
raise ChartAccessDeniedError()

View File

@ -20,7 +20,6 @@ from datetime import datetime
from typing import Any, Optional, Union
from uuid import UUID
from flask_appbuilder.security.sqla.models import User
from sqlalchemy.exc import SQLAlchemyError
from superset import db
@ -28,23 +27,21 @@ from superset.commands.base import BaseCommand
from superset.key_value.exceptions import KeyValueCreateFailedError
from superset.key_value.models import KeyValueEntry
from superset.key_value.types import Key, KeyValueResource
from superset.utils.core import get_user_id
logger = logging.getLogger(__name__)
class CreateKeyValueCommand(BaseCommand):
actor: Optional[User]
resource: KeyValueResource
value: Any
key: Optional[Union[int, UUID]]
expires_on: Optional[datetime]
# pylint: disable=too-many-arguments
def __init__(
self,
resource: KeyValueResource,
value: Any,
actor: Optional[User] = None,
key: Optional[Union[int, UUID]] = None,
expires_on: Optional[datetime] = None,
):
@ -53,13 +50,11 @@ class CreateKeyValueCommand(BaseCommand):
:param resource: the resource (dashboard, chart etc)
:param value: the value to persist in the key-value store
:param actor: the user performing the command
:param key: id of entry (autogenerated if undefined)
:param expires_on: entry expiration time
:return: the key associated with the persisted value
"""
self.resource = resource
self.actor = actor
self.value = value
self.key = key
self.expires_on = expires_on
@ -80,9 +75,7 @@ class CreateKeyValueCommand(BaseCommand):
resource=self.resource.value,
value=pickle.dumps(self.value),
created_on=datetime.now(),
created_by_fk=None
if self.actor is None or self.actor.is_anonymous
else self.actor.id,
created_by_fk=get_user_id(),
expires_on=self.expires_on,
)
if self.key is not None:

View File

@ -21,7 +21,6 @@ from datetime import datetime
from typing import Any, Optional, Union
from uuid import UUID
from flask_appbuilder.security.sqla.models import User
from sqlalchemy.exc import SQLAlchemyError
from superset import db
@ -30,24 +29,22 @@ from superset.key_value.exceptions import KeyValueUpdateFailedError
from superset.key_value.models import KeyValueEntry
from superset.key_value.types import Key, KeyValueResource
from superset.key_value.utils import get_filter
from superset.utils.core import get_user_id
logger = logging.getLogger(__name__)
class UpdateKeyValueCommand(BaseCommand):
actor: Optional[User]
resource: KeyValueResource
value: Any
key: Union[int, UUID]
expires_on: Optional[datetime]
# pylint: disable=too-many-argumentsåå
def __init__(
self,
resource: KeyValueResource,
key: Union[int, UUID],
value: Any,
actor: Optional[User] = None,
expires_on: Optional[datetime] = None,
):
"""
@ -56,11 +53,9 @@ class UpdateKeyValueCommand(BaseCommand):
:param resource: the resource (dashboard, chart etc)
:param key: the key to update
:param value: the value to persist in the key-value store
:param actor: the user performing the command
:param expires_on: entry expiration time
:return: the key associated with the updated value
"""
self.actor = actor
self.resource = resource
self.key = key
self.value = value
@ -89,9 +84,7 @@ class UpdateKeyValueCommand(BaseCommand):
entry.value = pickle.dumps(self.value)
entry.expires_on = self.expires_on
entry.changed_on = datetime.now()
entry.changed_by_fk = (
None if self.actor is None or self.actor.is_anonymous else self.actor.id
)
entry.changed_by_fk = get_user_id()
db.session.merge(entry)
db.session.commit()
return Key(id=entry.id, uuid=entry.uuid)

View File

@ -21,7 +21,6 @@ from datetime import datetime
from typing import Any, Optional, Union
from uuid import UUID
from flask_appbuilder.security.sqla.models import User
from sqlalchemy.exc import SQLAlchemyError
from superset import db
@ -31,24 +30,22 @@ from superset.key_value.exceptions import KeyValueUpdateFailedError
from superset.key_value.models import KeyValueEntry
from superset.key_value.types import Key, KeyValueResource
from superset.key_value.utils import get_filter
from superset.utils.core import get_user_id
logger = logging.getLogger(__name__)
class UpsertKeyValueCommand(BaseCommand):
actor: Optional[User]
resource: KeyValueResource
value: Any
key: Union[int, UUID]
expires_on: Optional[datetime]
# pylint: disable=too-many-arguments
def __init__(
self,
resource: KeyValueResource,
key: Union[int, UUID],
value: Any,
actor: Optional[User] = None,
expires_on: Optional[datetime] = None,
):
"""
@ -58,11 +55,9 @@ class UpsertKeyValueCommand(BaseCommand):
:param key: the key to update
:param value: the value to persist in the key-value store
:param key_type: the type of the key to update
:param actor: the user performing the command
:param expires_on: entry expiration time
:return: the key associated with the updated value
"""
self.actor = actor
self.resource = resource
self.key = key
self.value = value
@ -91,16 +86,13 @@ class UpsertKeyValueCommand(BaseCommand):
entry.value = pickle.dumps(self.value)
entry.expires_on = self.expires_on
entry.changed_on = datetime.now()
entry.changed_by_fk = (
None if self.actor is None or self.actor.is_anonymous else self.actor.id
)
entry.changed_by_fk = get_user_id()
db.session.merge(entry)
db.session.commit()
return Key(entry.id, entry.uuid)
return CreateKeyValueCommand(
resource=self.resource,
value=self.value,
actor=self.actor,
key=self.key,
expires_on=self.expires_on,
).run()

View File

@ -18,11 +18,10 @@ from __future__ import annotations
from hashlib import md5
from secrets import token_urlsafe
from typing import Optional, Union
from typing import Union
from uuid import UUID
import hashids
from flask_appbuilder.security.sqla.models import User
from flask_babel import gettext as _
from superset.key_value.exceptions import KeyValueParseKeyError
@ -64,7 +63,3 @@ def get_uuid_namespace(seed: str) -> UUID:
md5_obj = md5()
md5_obj.update(seed.encode("utf-8"))
return UUID(md5_obj.hexdigest())
def get_owner(user: User) -> Optional[int]:
return user.id if not user.is_anonymous else None

View File

@ -23,7 +23,6 @@ from functools import partial
from typing import Any, Callable, Dict, List, Set, Tuple, Type, Union
import sqlalchemy as sqla
from flask import g
from flask_appbuilder import Model
from flask_appbuilder.models.decorators import renders
from flask_appbuilder.security.sqla.models import User
@ -47,7 +46,6 @@ from sqlalchemy.sql import join, select
from sqlalchemy.sql.elements import BinaryExpression
from superset import app, db, is_feature_enabled, security_manager
from superset.common.request_contexed_based import is_user_admin
from superset.connectors.base.models import BaseDatasource
from superset.connectors.sqla.models import SqlaTable, SqlMetric, TableColumn
from superset.datasource.dao import DatasourceDAO
@ -59,6 +57,7 @@ from superset.models.tags import DashboardUpdater
from superset.models.user_attributes import UserAttribute
from superset.tasks.thumbnails import cache_dashboard_thumbnail
from superset.utils import core as utils
from superset.utils.core import get_user_id
from superset.utils.decorators import debounce
from superset.utils.hashing import md5_sha_from_str
from superset.utils.urls import get_url_path
@ -203,15 +202,14 @@ class Dashboard(Model, AuditMixinNullable, ImportExportMixin):
@property
def filter_sets_lst(self) -> Dict[int, FilterSet]:
if is_user_admin():
if security_manager.is_admin():
return self._filter_sets
current_user = g.user.id
filter_sets_by_owner_type: Dict[str, List[Any]] = {"Dashboard": [], "User": []}
for fs in self._filter_sets:
filter_sets_by_owner_type[fs.owner_type].append(fs)
user_filter_sets = list(
filter(
lambda filter_set: filter_set.owner_id == current_user,
lambda filter_set: filter_set.owner_id == get_user_id(),
filter_sets_by_owner_type["User"],
)
)
@ -445,11 +443,6 @@ class Dashboard(Model, AuditMixinNullable, ImportExportMixin):
qry = session.query(Dashboard).filter(id_or_slug_filter(id_or_slug))
return qry.one_or_none()
def is_actor_owner(self) -> bool:
if g.user is None or g.user.is_anonymous or not g.user.is_authenticated:
return False
return g.user.id in set(map(lambda user: user.id, self.owners))
def id_or_slug_filter(id_or_slug: str) -> BinaryExpression:
if id_or_slug.isdigit():

View File

@ -192,7 +192,7 @@ class SavedQueryRestApi(BaseSupersetModelRestApi):
"""
item_ids = kwargs["rison"]
try:
BulkDeleteSavedQueryCommand(g.user, item_ids).run()
BulkDeleteSavedQueryCommand(item_ids).run()
return self.response(
200,
message=ngettext(

View File

@ -17,8 +17,6 @@
import logging
from typing import List, Optional
from flask_appbuilder.security.sqla.models import User
from superset.commands.base import BaseCommand
from superset.dao.exceptions import DAODeleteFailedError
from superset.models.dashboard import Dashboard
@ -32,8 +30,7 @@ logger = logging.getLogger(__name__)
class BulkDeleteSavedQueryCommand(BaseCommand):
def __init__(self, user: User, model_ids: List[int]):
self._actor = user
def __init__(self, model_ids: List[int]):
self._model_ids = model_ids
self._models: Optional[List[Dashboard]] = None

View File

@ -17,7 +17,7 @@
import logging
from typing import Any, Optional
from flask import g, request, Response
from flask import request, Response
from flask_appbuilder.api import expose, permission_name, protect, rison, safe
from flask_appbuilder.hooks import before_request
from flask_appbuilder.models.sqla.interface import SQLAInterface
@ -266,7 +266,7 @@ class ReportScheduleRestApi(BaseSupersetModelRestApi):
$ref: '#/components/responses/500'
"""
try:
DeleteReportScheduleCommand(g.user, pk).run()
DeleteReportScheduleCommand(pk).run()
return self.response(200, message="OK")
except ReportScheduleNotFoundError:
return self.response_404()
@ -340,7 +340,7 @@ class ReportScheduleRestApi(BaseSupersetModelRestApi):
except ValidationError as error:
return self.response_400(message=error.messages)
try:
new_model = CreateReportScheduleCommand(g.user, item).run()
new_model = CreateReportScheduleCommand(item).run()
return self.response(201, id=new_model.id, result=item)
except ReportScheduleNotFoundError as ex:
return self.response_400(message=str(ex))
@ -421,7 +421,7 @@ class ReportScheduleRestApi(BaseSupersetModelRestApi):
except ValidationError as error:
return self.response_400(message=error.messages)
try:
new_model = UpdateReportScheduleCommand(g.user, pk, item).run()
new_model = UpdateReportScheduleCommand(pk, item).run()
return self.response(200, id=new_model.id, result=item)
except ReportScheduleNotFoundError:
return self.response_404()
@ -483,7 +483,7 @@ class ReportScheduleRestApi(BaseSupersetModelRestApi):
"""
item_ids = kwargs["rison"]
try:
BulkDeleteReportScheduleCommand(g.user, item_ids).run()
BulkDeleteReportScheduleCommand(item_ids).run()
return self.response(
200,
message=ngettext(

View File

@ -17,8 +17,7 @@
import logging
from typing import List, Optional
from flask_appbuilder.security.sqla.models import User
from superset import security_manager
from superset.commands.base import BaseCommand
from superset.dao.exceptions import DAODeleteFailedError
from superset.exceptions import SupersetSecurityException
@ -29,14 +28,12 @@ from superset.reports.commands.exceptions import (
ReportScheduleNotFoundError,
)
from superset.reports.dao import ReportScheduleDAO
from superset.views.base import check_ownership
logger = logging.getLogger(__name__)
class BulkDeleteReportScheduleCommand(BaseCommand):
def __init__(self, user: User, model_ids: List[int]):
self._actor = user
def __init__(self, model_ids: List[int]):
self._model_ids = model_ids
self._models: Optional[List[ReportSchedule]] = None
@ -58,6 +55,6 @@ class BulkDeleteReportScheduleCommand(BaseCommand):
# Check ownership
for model in self._models:
try:
check_ownership(model)
security_manager.raise_for_ownership(model)
except SupersetSecurityException as ex:
raise ReportScheduleForbiddenError() from ex

View File

@ -19,7 +19,6 @@ import logging
from typing import Any, Dict, List, Optional
from flask_appbuilder.models.sqla import Model
from flask_appbuilder.security.sqla.models import User
from marshmallow import ValidationError
from superset.commands.base import CreateMixin
@ -42,8 +41,7 @@ logger = logging.getLogger(__name__)
class CreateReportScheduleCommand(CreateMixin, BaseReportScheduleCommand):
def __init__(self, user: User, data: Dict[str, Any]):
self._actor = user
def __init__(self, data: Dict[str, Any]):
self._properties = data.copy()
def run(self) -> Model:
@ -63,7 +61,6 @@ class CreateReportScheduleCommand(CreateMixin, BaseReportScheduleCommand):
creation_method = self._properties.get("creation_method")
chart_id = self._properties.get("chart")
dashboard_id = self._properties.get("dashboard")
user_id = self._actor.id
# Validate type is required
if not report_type:
@ -99,7 +96,7 @@ class CreateReportScheduleCommand(CreateMixin, BaseReportScheduleCommand):
if (
creation_method != ReportCreationMethod.ALERTS_REPORTS
and not ReportScheduleDAO.validate_unique_creation_method(
user_id, dashboard_id, chart_id
dashboard_id, chart_id
)
):
raise ReportScheduleCreationMethodUniquenessValidationError()
@ -110,7 +107,7 @@ class CreateReportScheduleCommand(CreateMixin, BaseReportScheduleCommand):
)
try:
owners = self.populate_owners(self._actor, owner_ids)
owners = self.populate_owners(owner_ids)
self._properties["owners"] = owners
except ValidationError as ex:
exceptions.append(ex)

View File

@ -18,8 +18,8 @@ import logging
from typing import Optional
from flask_appbuilder.models.sqla import Model
from flask_appbuilder.security.sqla.models import User
from superset import security_manager
from superset.commands.base import BaseCommand
from superset.dao.exceptions import DAODeleteFailedError
from superset.exceptions import SupersetSecurityException
@ -30,14 +30,12 @@ from superset.reports.commands.exceptions import (
ReportScheduleNotFoundError,
)
from superset.reports.dao import ReportScheduleDAO
from superset.views.base import check_ownership
logger = logging.getLogger(__name__)
class DeleteReportScheduleCommand(BaseCommand):
def __init__(self, user: User, model_id: int):
self._actor = user
def __init__(self, model_id: int):
self._model_id = model_id
self._model: Optional[ReportSchedule] = None
@ -58,6 +56,6 @@ class DeleteReportScheduleCommand(BaseCommand):
# Check ownership
try:
check_ownership(self._model)
security_manager.raise_for_ownership(self._model)
except SupersetSecurityException as ex:
raise ReportScheduleForbiddenError() from ex

View File

@ -19,9 +19,9 @@ import logging
from typing import Any, Dict, List, Optional
from flask_appbuilder.models.sqla import Model
from flask_appbuilder.security.sqla.models import User
from marshmallow import ValidationError
from superset import security_manager
from superset.commands.base import UpdateMixin
from superset.dao.exceptions import DAOUpdateFailedError
from superset.databases.dao import DatabaseDAO
@ -37,14 +37,12 @@ from superset.reports.commands.exceptions import (
ReportScheduleUpdateFailedError,
)
from superset.reports.dao import ReportScheduleDAO
from superset.views.base import check_ownership
logger = logging.getLogger(__name__)
class UpdateReportScheduleCommand(UpdateMixin, BaseReportScheduleCommand):
def __init__(self, user: User, model_id: int, data: Dict[str, Any]):
self._actor = user
def __init__(self, model_id: int, data: Dict[str, Any]):
self._model_id = model_id
self._properties = data.copy()
self._model: Optional[ReportSchedule] = None
@ -113,7 +111,7 @@ class UpdateReportScheduleCommand(UpdateMixin, BaseReportScheduleCommand):
# Check ownership
try:
check_ownership(self._model)
security_manager.raise_for_ownership(self._model)
except SupersetSecurityException as ex:
raise ReportScheduleForbiddenError() from ex
@ -121,7 +119,7 @@ class UpdateReportScheduleCommand(UpdateMixin, BaseReportScheduleCommand):
if owner_ids is None:
owner_ids = [owner.id for owner in self._model.owners]
try:
owners = self.populate_owners(self._actor, owner_ids)
owners = self.populate_owners(owner_ids)
self._properties["owners"] = owners
except ValidationError as ex:
exceptions.append(ex)

View File

@ -33,6 +33,7 @@ from superset.models.reports import (
ReportScheduleType,
ReportState,
)
from superset.utils.core import get_user_id
logger = logging.getLogger(__name__)
@ -116,14 +117,14 @@ class ReportScheduleDAO(BaseDAO):
@staticmethod
def validate_unique_creation_method(
user_id: int, dashboard_id: Optional[int] = None, chart_id: Optional[int] = None
dashboard_id: Optional[int] = None, chart_id: Optional[int] = None
) -> bool:
"""
Validate if the user already has a chart or dashboard
with a report attached form the self subscribe reports
"""
query = db.session.query(ReportSchedule).filter_by(created_by_fk=user_id)
query = db.session.query(ReportSchedule).filter_by(created_by_fk=get_user_id())
if dashboard_id is not None:
query = query.filter(ReportSchedule.dashboard_id == dashboard_id)

View File

@ -1093,7 +1093,6 @@ class SupersetSecurityManager( # pylint: disable=too-many-public-methods
from superset.connectors.sqla.models import SqlaTable
from superset.extensions import feature_flag_manager
from superset.sql_parse import Table
from superset.views.utils import is_owner
if database and table or query:
if query:
@ -1126,7 +1125,7 @@ class SupersetSecurityManager( # pylint: disable=too-many-public-methods
for datasource_ in datasources:
if self.can_access(
"datasource_access", datasource_.perm
) or is_owner(datasource_, getattr(g, "user", None)):
) or self.is_owner(datasource_):
break
else:
denied.add(table_)
@ -1152,7 +1151,7 @@ class SupersetSecurityManager( # pylint: disable=too-many-public-methods
if not (
self.can_access_schema(datasource)
or self.can_access("datasource_access", datasource.perm or "")
or is_owner(datasource, getattr(g, "user", None))
or self.is_owner(datasource)
or (
should_check_dashboard_access
and self.can_access_based_on_dashboard(datasource)
@ -1327,8 +1326,6 @@ class SupersetSecurityManager( # pylint: disable=too-many-public-methods
# pylint: disable=import-outside-toplevel
from superset import is_feature_enabled
from superset.dashboards.commands.exceptions import DashboardAccessDeniedError
from superset.views.base import is_user_admin
from superset.views.utils import is_owner
def has_rbac_access() -> bool:
return (not is_feature_enabled("DASHBOARD_RBAC")) or any(
@ -1341,8 +1338,8 @@ class SupersetSecurityManager( # pylint: disable=too-many-public-methods
can_access = self.has_guest_access(dashboard)
else:
can_access = (
is_user_admin()
or is_owner(dashboard, g.user)
self.is_admin()
or self.is_owner(dashboard)
or (dashboard.published and has_rbac_access())
or (not dashboard.published and not dashboard.roles)
)
@ -1520,3 +1517,69 @@ class SupersetSecurityManager( # pylint: disable=too-many-public-methods
if str(resource["id"]) == str(dashboard.embedded[0].uuid):
return True
return False
def raise_for_ownership(self, resource: Model) -> None:
"""
Raise an exception if the user does not own the resource.
Note admins are deemed owners of all resources.
:param resource: The dashboard, dataste, chart, etc. resource
:raises SupersetSecurityException: If the current user is not an owner
"""
# pylint: disable=import-outside-toplevel
from superset import db
if self.is_admin():
return
# Set of wners that works across ORM models.
owners: List[User] = []
orig_resource = db.session.query(resource.__class__).get(resource.id)
if orig_resource:
if hasattr(resource, "owners"):
owners += orig_resource.owners
if hasattr(resource, "owner"):
owners.append(orig_resource.owner)
if hasattr(resource, "created_by"):
owners.append(orig_resource.created_by)
if g.user.is_anonymous or g.user not in owners:
raise SupersetSecurityException(
SupersetError(
error_type=SupersetErrorType.MISSING_OWNERSHIP_ERROR,
message=f"You don't have the rights to alter [{resource}]",
level=ErrorLevel.ERROR,
)
)
def is_owner(self, resource: Model) -> bool:
"""
Returns True if the current user is an owner of the resource, False otherwise.
:param resource: The dashboard, dataste, chart, etc. resource
:returns: Whethe the current user is an owner of the resource
"""
try:
self.raise_for_ownership(resource)
except SupersetSecurityException:
return False
return True
def is_admin(self) -> bool:
"""
Returns True if the current user is an admin user, False otherwise.
:returns: Whehther the current user is an admin user
"""
return current_app.config["AUTH_ROLE_ADMIN"] in [
role.name for role in self.get_user_roles()
]

View File

@ -20,7 +20,7 @@ from typing import Any
from apispec import APISpec
from apispec.exceptions import DuplicateComponentNameError
from flask import g, request, Response
from flask import request, Response
from flask_appbuilder.api import BaseApi
from marshmallow import ValidationError
@ -70,9 +70,7 @@ class TemporaryCacheRestApi(BaseApi, ABC):
try:
item = self.add_model_schema.load(request.json)
tab_id = request.args.get("tab_id")
args = CommandParameters(
actor=g.user, resource_id=pk, value=item["value"], tab_id=tab_id
)
args = CommandParameters(resource_id=pk, value=item["value"], tab_id=tab_id)
key = self.get_create_command()(args).run()
return self.response(201, key=key)
except ValidationError as ex:
@ -88,7 +86,6 @@ class TemporaryCacheRestApi(BaseApi, ABC):
item = self.edit_model_schema.load(request.json)
tab_id = request.args.get("tab_id")
args = CommandParameters(
actor=g.user,
resource_id=pk,
key=key,
value=item["value"],
@ -105,7 +102,7 @@ class TemporaryCacheRestApi(BaseApi, ABC):
def get(self, pk: int, key: str) -> Response:
try:
args = CommandParameters(actor=g.user, resource_id=pk, key=key)
args = CommandParameters(resource_id=pk, key=key)
value = self.get_get_command()(args).run()
if not value:
return self.response_404()
@ -117,7 +114,7 @@ class TemporaryCacheRestApi(BaseApi, ABC):
def delete(self, pk: int, key: str) -> Response:
try:
args = CommandParameters(actor=g.user, resource_id=pk, key=key)
args = CommandParameters(resource_id=pk, key=key)
result = self.get_delete_command()(args).run()
if not result:
return self.response_404()

View File

@ -17,12 +17,9 @@
from dataclasses import dataclass
from typing import Optional
from flask_appbuilder.security.sqla.models import User
@dataclass
class CommandParameters:
actor: User
resource_id: int
tab_id: Optional[int] = None
key: Optional[str] = None

View File

@ -25,9 +25,10 @@ from superset.views.base import DeleteMixin, SupersetModelView
from superset.views.core import DAR
class AccessRequestsModelView(
SupersetModelView, DeleteMixin
): # pylint: disable=too-many-ancestors
class AccessRequestsModelView( # pylint: disable=too-many-ancestors
SupersetModelView,
DeleteMixin,
):
datamodel = SQLAInterface(DAR)
include_route_methods = RouteMethod.CRUD_SET
list_columns = [

View File

@ -47,9 +47,10 @@ class StartEndDttmValidator: # pylint: disable=too-few-public-methods
)
class AnnotationModelView(
SupersetModelView, CompactCRUDMixin
): # pylint: disable=too-many-ancestors
class AnnotationModelView( # pylint: disable=too-many-ancestors
SupersetModelView,
CompactCRUDMixin,
):
datamodel = SQLAInterface(Annotation)
include_route_methods = RouteMethod.CRUD_SET | {"annotation"}

View File

@ -38,7 +38,6 @@ from flask_appbuilder import BaseView, Model, ModelView
from flask_appbuilder.actions import action
from flask_appbuilder.forms import DynamicForm
from flask_appbuilder.models.sqla.filters import BaseFilter
from flask_appbuilder.security.sqla.models import User
from flask_appbuilder.widgets import ListWidget
from flask_babel import get_locale, gettext as __, lazy_gettext as _
from flask_jwt_extended.exceptions import NoAuthorizationError
@ -270,11 +269,6 @@ def create_table_permissions(table: models.SqlaTable) -> None:
security_manager.add_permission_view_menu("schema_access", table.schema_perm)
def is_user_admin() -> bool:
user_roles = [role.name.lower() for role in list(security_manager.get_user_roles())]
return "admin" in user_roles
class BaseSupersetView(BaseView):
@staticmethod
def json_response(obj: Any, status: int = 200) -> FlaskResponse:
@ -644,53 +638,6 @@ class CsvResponse(Response):
default_mimetype = "text/csv"
def check_ownership(obj: Any, raise_if_false: bool = True) -> bool:
"""Meant to be used in `pre_update` hooks on models to enforce ownership
Admin have all access, and other users need to be referenced on either
the created_by field that comes with the ``AuditMixin``, or in a field
named ``owners`` which is expected to be a one-to-many with the User
model. It is meant to be used in the ModelView's pre_update hook in
which raising will abort the update.
"""
if not obj:
return False
security_exception = SupersetSecurityException(
SupersetError(
error_type=SupersetErrorType.MISSING_OWNERSHIP_ERROR,
message="You don't have the rights to alter [{}]".format(obj),
level=ErrorLevel.ERROR,
)
)
if g.user.is_anonymous:
if raise_if_false:
raise security_exception
return False
if is_user_admin():
return True
scoped_session = db.create_scoped_session()
orig_obj = scoped_session.query(obj.__class__).filter_by(id=obj.id).first()
# Making a list of owners that works across ORM models
owners: List[User] = []
if hasattr(orig_obj, "owners"):
owners += orig_obj.owners
if hasattr(orig_obj, "owner"):
owners += [orig_obj.owner]
if hasattr(orig_obj, "created_by"):
owners += [orig_obj.created_by]
owner_names = [o.username for o in owners if o]
if g.user and hasattr(g.user, "username") and g.user.username in owner_names:
return True
if raise_if_false:
raise security_exception
return False
def bind_field(
_: Any, form: DynamicForm, unbound_field: UnboundField, options: Dict[Any, Any]
) -> Field:

View File

@ -21,16 +21,12 @@ from flask_appbuilder import expose, has_access
from flask_appbuilder.models.sqla.interface import SQLAInterface
from flask_babel import lazy_gettext as _
from superset import security_manager
from superset.constants import MODEL_VIEW_RW_METHOD_PERMISSION_MAP, RouteMethod
from superset.models.slice import Slice
from superset.superset_typing import FlaskResponse
from superset.utils import core as utils
from superset.views.base import (
check_ownership,
common_bootstrap_payload,
DeleteMixin,
SupersetModelView,
)
from superset.views.base import common_bootstrap_payload, DeleteMixin, SupersetModelView
from superset.views.chart.mixin import SliceMixin
from superset.views.utils import bootstrap_user_data
@ -53,10 +49,10 @@ class SliceModelView(
def pre_update(self, item: "SliceModelView") -> None:
utils.validate_json(item.params)
check_ownership(item)
security_manager.raise_for_ownership(item)
def pre_delete(self, item: "SliceModelView") -> None:
check_ownership(item)
security_manager.raise_for_ownership(item)
@expose("/add", methods=["GET", "POST"])
@has_access

View File

@ -140,7 +140,6 @@ from superset.utils.decorators import check_dashboard_access
from superset.views.base import (
api,
BaseSupersetView,
check_ownership,
common_bootstrap_payload,
create_table_permissions,
CsvResponse,
@ -164,7 +163,6 @@ from superset.views.utils import (
get_datasource_info,
get_form_data,
get_viz,
is_owner,
sanitize_datasource_data,
)
from superset.viz import BaseViz
@ -368,8 +366,8 @@ class Superset(BaseSupersetView): # pylint: disable=too-many-public-methods
return json_error_response(err)
# check if you can approve
if security_manager.can_access_all_datasources() or check_ownership(
datasource, raise_if_false=False
if security_manager.can_access_all_datasources() or security_manager.is_owner(
datasource
):
# can by done by admin only
if role_to_grant:
@ -758,7 +756,7 @@ class Superset(BaseSupersetView): # pylint: disable=too-many-public-methods
form_data_key = request.args.get("form_data_key")
if key is not None:
command = GetExplorePermalinkCommand(g.user, key)
command = GetExplorePermalinkCommand(key)
try:
permalink_value = command.run()
if permalink_value:
@ -775,7 +773,7 @@ class Superset(BaseSupersetView): # pylint: disable=too-many-public-methods
flash(__("Error: %(msg)s", msg=ex.message), "danger")
return redirect("/chart/list/")
elif form_data_key:
parameters = CommandParameters(actor=g.user, key=form_data_key)
parameters = CommandParameters(key=form_data_key)
value = GetFormDataCommand(parameters).run()
initial_form_data = json.loads(value) if value else {}
@ -857,7 +855,7 @@ class Superset(BaseSupersetView): # pylint: disable=too-many-public-methods
# slc perms
slice_add_perm = security_manager.can_access("can_write", "Chart")
slice_overwrite_perm = is_owner(slc, g.user) if slc else False
slice_overwrite_perm = security_manager.is_owner(slc) if slc else False
slice_download_perm = security_manager.can_access("can_csv", "Superset")
form_data["datasource"] = str(datasource_id) + "__" + cast(str, datasource_type)
@ -1050,7 +1048,7 @@ class Superset(BaseSupersetView): # pylint: disable=too-many-public-methods
.one(),
)
# check edit dashboard permissions
dash_overwrite_perm = check_ownership(dash, raise_if_false=False)
dash_overwrite_perm = security_manager.is_owner(dash)
if not dash_overwrite_perm:
return json_error_response(
_("You don't have the rights to ")
@ -1297,7 +1295,7 @@ class Superset(BaseSupersetView): # pylint: disable=too-many-public-methods
"""Save a dashboard's metadata"""
session = db.session()
dash = session.query(Dashboard).get(dashboard_id)
check_ownership(dash, raise_if_false=True)
security_manager.raise_for_ownership(dash)
data = json.loads(request.form["data"])
# client-side send back last_modified_time which was set when
# the dashboard was open. it was use to avoid mid-air collision.
@ -1340,7 +1338,7 @@ class Superset(BaseSupersetView): # pylint: disable=too-many-public-methods
data = json.loads(request.form["data"])
session = db.session()
dash = session.query(Dashboard).get(dashboard_id)
check_ownership(dash, raise_if_false=True)
security_manager.raise_for_ownership(dash)
new_slices = session.query(Slice).filter(Slice.id.in_(data["slice_ids"]))
dash.slices += new_slices
session.merge(dash)
@ -1664,7 +1662,7 @@ class Superset(BaseSupersetView): # pylint: disable=too-many-public-methods
def user_slices(self, user_id: Optional[int] = None) -> FlaskResponse:
"""List of slices a user owns, created, modified or faved"""
if not user_id:
user_id = cast(int, g.user.id)
user_id = cast(int, get_user_id())
error_obj = self.get_user_activity_access_error(user_id)
if error_obj:
return error_obj
@ -1717,7 +1715,7 @@ class Superset(BaseSupersetView): # pylint: disable=too-many-public-methods
def created_slices(self, user_id: Optional[int] = None) -> FlaskResponse:
"""List of slices created by this user"""
if not user_id:
user_id = cast(int, g.user.id)
user_id = cast(int, get_user_id())
error_obj = self.get_user_activity_access_error(user_id)
if error_obj:
return error_obj
@ -1748,7 +1746,7 @@ class Superset(BaseSupersetView): # pylint: disable=too-many-public-methods
def fave_slices(self, user_id: Optional[int] = None) -> FlaskResponse:
"""Favorite slices for a user"""
if user_id is None:
user_id = g.user.id
user_id = cast(int, get_user_id())
error_obj = self.get_user_activity_access_error(user_id)
if error_obj:
return error_obj
@ -1957,8 +1955,8 @@ class Superset(BaseSupersetView): # pylint: disable=too-many-public-methods
f"/superset/request_access/?dashboard_id={dashboard.id}"
)
dash_edit_perm = check_ownership(
dashboard, raise_if_false=False
dash_edit_perm = security_manager.is_owner(
dashboard
) and security_manager.can_access("can_save_dash", "Superset")
edit_mode = (
request.args.get(utils.ReservedUrlParameters.EDIT_MODE.value) == "true"
@ -1994,7 +1992,7 @@ class Superset(BaseSupersetView): # pylint: disable=too-many-public-methods
key: str,
) -> FlaskResponse:
try:
value = GetDashboardPermalinkCommand(g.user, key).run()
value = GetDashboardPermalinkCommand(key).run()
except DashboardPermalinkGetFailedError as ex:
flash(__("Error: %(msg)s", msg=ex.message), "danger")
return redirect("/dashboard/list/")

View File

@ -25,9 +25,10 @@ from superset.superset_typing import FlaskResponse
from superset.views.base import DeleteMixin, SupersetModelView
class CssTemplateModelView(
SupersetModelView, DeleteMixin
): # pylint: disable=too-many-ancestors
class CssTemplateModelView( # pylint: disable=too-many-ancestors
SupersetModelView,
DeleteMixin,
):
datamodel = SQLAInterface(models.CssTemplate)
include_route_methods = RouteMethod.CRUD_SET

View File

@ -16,8 +16,8 @@
# under the License.
from flask_babel import lazy_gettext as _
from ...dashboards.filters import DashboardAccessFilter
from ..base import check_ownership
from superset import security_manager
from superset.dashboards.filters import DashboardAccessFilter
class DashboardMixin: # pylint: disable=too-few-public-methods
@ -90,4 +90,4 @@ class DashboardMixin: # pylint: disable=too-few-public-methods
}
def pre_delete(self, item: "DashboardMixin") -> None: # pylint: disable=no-self-use
check_ownership(item)
security_manager.raise_for_ownership(item)

View File

@ -33,7 +33,6 @@ from superset.superset_typing import FlaskResponse
from superset.utils import core as utils
from superset.views.base import (
BaseSupersetView,
check_ownership,
common_bootstrap_payload,
DeleteMixin,
generate_download_headers,
@ -97,12 +96,11 @@ class DashboardModelView(
item.owners.append(g.user)
utils.validate_json(item.json_metadata)
utils.validate_json(item.position_json)
owners = list(item.owners)
for slc in item.slices:
slc.owners = list(set(owners) | set(slc.owners))
slc.owners = list(set(item.owners) | set(slc.owners))
def pre_update(self, item: "DashboardModelView") -> None:
check_ownership(item)
security_manager.raise_for_ownership(item)
self.pre_add(item)

View File

@ -18,7 +18,7 @@ import json
from collections import Counter
from typing import Any
from flask import g, request
from flask import request
from flask_appbuilder import expose
from flask_appbuilder.api import rison
from flask_appbuilder.security.decorators import has_access_api
@ -27,7 +27,7 @@ from marshmallow import ValidationError
from sqlalchemy.exc import NoSuchTableError
from sqlalchemy.orm.exc import NoResultFound
from superset import db, event_logger
from superset import db, event_logger, security_manager
from superset.commands.utils import populate_owners
from superset.connectors.sqla.models import SqlaTable
from superset.connectors.sqla.utils import get_physical_table_metadata
@ -37,14 +37,12 @@ from superset.datasets.commands.exceptions import (
)
from superset.datasource.dao import DatasourceDAO
from superset.exceptions import SupersetException, SupersetSecurityException
from superset.extensions import security_manager
from superset.models.core import Database
from superset.superset_typing import FlaskResponse
from superset.utils.core import DatasourceType
from superset.views.base import (
api,
BaseSupersetView,
check_ownership,
handle_api_exception,
json_error_response,
)
@ -84,13 +82,12 @@ class Datasource(BaseSupersetView):
if "owners" in datasource_dict and orm_datasource.owner_class is not None:
# Check ownership
try:
check_ownership(orm_datasource)
security_manager.raise_for_ownership(orm_datasource)
except SupersetSecurityException as ex:
raise DatasetForbiddenError() from ex
user = security_manager.get_user_by_id(g.user.id)
datasource_dict["owners"] = populate_owners(
user, datasource_dict["owners"], default_to_user=False
datasource_dict["owners"], default_to_user=False
)
duplicates = [

View File

@ -26,7 +26,10 @@ from superset.views.base import SupersetModelView
from . import LogMixin
class LogModelView(LogMixin, SupersetModelView): # pylint: disable=too-many-ancestors
class LogModelView( # pylint: disable=too-many-ancestors
LogMixin,
SupersetModelView,
):
datamodel = SQLAInterface(models.Log)
include_route_methods = {RouteMethod.LIST, RouteMethod.SHOW}
class_permission_name = "Log"

View File

@ -36,9 +36,10 @@ from .base import BaseSupersetView, DeleteMixin, json_success, SupersetModelView
logger = logging.getLogger(__name__)
class SavedQueryView(
SupersetModelView, DeleteMixin
): # pylint: disable=too-many-ancestors
class SavedQueryView( # pylint: disable=too-many-ancestors
SupersetModelView,
DeleteMixin,
):
datamodel = SQLAInterface(SavedQuery)
include_route_methods = RouteMethod.CRUD_SET

View File

@ -32,7 +32,6 @@ from sqlalchemy.orm.exc import NoResultFound
import superset.models.core as models
from superset import app, dataframe, db, result_set, viz
from superset.common.db_query_status import QueryStatus
from superset.connectors.sqla.models import SqlaTable
from superset.datasource.dao import DatasourceDAO
from superset.errors import ErrorLevel, SupersetError, SupersetErrorType
from superset.exceptions import (
@ -427,11 +426,6 @@ def is_slice_in_container(
return False
def is_owner(obj: Union[Dashboard, Slice, SqlaTable], user: User) -> bool:
"""Check if user is owner of the slice"""
return obj and user in obj.owners
def check_resource_permissions(
check_perms: Callable[..., Any],
) -> Callable[..., Any]:

View File

@ -350,58 +350,60 @@ class TestImportChartsCommand(SupersetTestCase):
class TestChartsCreateCommand(SupersetTestCase):
@patch("superset.views.base.g")
@patch("superset.utils.core.g")
@patch("superset.charts.commands.create.g")
@patch("superset.security.manager.g")
@pytest.mark.usefixtures("load_energy_table_with_slice")
def test_create_v1_response(self, mock_sm_g, mock_g):
def test_create_v1_response(self, mock_sm_g, mock_c_g, mock_u_g):
"""Test that the create chart command creates a chart"""
actor = security_manager.find_user(username="admin")
mock_g.user = mock_sm_g.user = actor
user = security_manager.find_user(username="admin")
mock_u_g.user = mock_c_g.user = mock_sm_g.user = user
chart_data = {
"slice_name": "new chart",
"description": "new description",
"owners": [actor.id],
"owners": [user.id],
"viz_type": "new_viz_type",
"params": json.dumps({"viz_type": "new_viz_type"}),
"cache_timeout": 1000,
"datasource_id": 1,
"datasource_type": "table",
}
command = CreateChartCommand(actor, chart_data)
command = CreateChartCommand(chart_data)
chart = command.run()
chart = db.session.query(Slice).get(chart.id)
assert chart.viz_type == "new_viz_type"
json_params = json.loads(chart.params)
assert json_params == {"viz_type": "new_viz_type"}
assert chart.slice_name == "new chart"
assert chart.owners == [actor]
assert chart.owners == [user]
db.session.delete(chart)
db.session.commit()
class TestChartsUpdateCommand(SupersetTestCase):
@patch("superset.views.base.g")
@patch("superset.charts.commands.update.g")
@patch("superset.utils.core.g")
@patch("superset.security.manager.g")
@pytest.mark.usefixtures("load_energy_table_with_slice")
def test_update_v1_response(self, mock_sm_g, mock_g):
def test_update_v1_response(self, mock_sm_g, mock_c_g, mock_u_g):
"""Test that a chart command updates properties"""
pk = db.session.query(Slice).all()[0].id
actor = security_manager.find_user(username="admin")
mock_g.user = mock_sm_g.user = actor
user = security_manager.find_user(username="admin")
mock_u_g.user = mock_c_g.user = mock_sm_g.user = user
model_id = pk
json_obj = {
"description": "test for update",
"cache_timeout": None,
"owners": [actor.id],
"owners": [user.id],
}
command = UpdateChartCommand(actor, model_id, json_obj)
command = UpdateChartCommand(model_id, json_obj)
last_saved_before = db.session.query(Slice).get(pk).last_saved_at
command.run()
chart = db.session.query(Slice).get(pk)
assert chart.last_saved_at != last_saved_before
assert chart.last_saved_by == actor
assert chart.last_saved_by == user
@patch("superset.views.base.g")
@patch("superset.utils.core.g")
@patch("superset.security.manager.g")
@pytest.mark.usefixtures("load_energy_table_with_slice")
def test_query_context_update_command(self, mock_sm_g, mock_g):
@ -415,14 +417,14 @@ class TestChartsUpdateCommand(SupersetTestCase):
chart.owners = [admin]
db.session.commit()
actor = security_manager.find_user(username="alpha")
mock_g.user = mock_sm_g.user = actor
user = security_manager.find_user(username="alpha")
mock_g.user = mock_sm_g.user = user
query_context = json.dumps({"foo": "bar"})
json_obj = {
"query_context_generation": True,
"query_context": query_context,
}
command = UpdateChartCommand(actor, pk, json_obj)
command = UpdateChartCommand(pk, json_obj)
command.run()
chart = db.session.query(Slice).get(pk)
assert chart.query_context == query_context

View File

@ -70,10 +70,11 @@ class TestCreateDatabaseCommand(SupersetTestCase):
@mock.patch(
"superset.databases.commands.test_connection.event_logger.log_with_context"
)
def test_create_duplicate_error(self, mock_logger):
@mock.patch("superset.utils.core.g")
def test_create_duplicate_error(self, mock_g, mock_logger):
example_db = get_example_database()
mock_g.user = security_manager.find_user("admin")
command = CreateDatabaseCommand(
security_manager.find_user("admin"),
{"database_name": example_db.database_name},
)
with pytest.raises(DatabaseInvalidError) as excinfo:
@ -90,8 +91,10 @@ class TestCreateDatabaseCommand(SupersetTestCase):
@mock.patch(
"superset.databases.commands.test_connection.event_logger.log_with_context"
)
def test_multiple_error_logging(self, mock_logger):
command = CreateDatabaseCommand(security_manager.find_user("admin"), {})
@mock.patch("superset.utils.core.g")
def test_multiple_error_logging(self, mock_g, mock_logger):
mock_g.user = security_manager.find_user("admin")
command = CreateDatabaseCommand({})
with pytest.raises(DatabaseInvalidError) as excinfo:
command.run()
assert str(excinfo.value) == ("Database parameters are invalid.")
@ -643,15 +646,17 @@ class TestTestConnectionDatabaseCommand(SupersetTestCase):
@mock.patch(
"superset.databases.commands.test_connection.event_logger.log_with_context"
)
def test_connection_db_exception(self, mock_event_logger, mock_get_sqla_engine):
@mock.patch("superset.utils.core.g")
def test_connection_db_exception(
self, mock_g, mock_event_logger, mock_get_sqla_engine
):
"""Test to make sure event_logger is called when an exception is raised"""
database = get_example_database()
mock_g.user = security_manager.find_user("admin")
mock_get_sqla_engine.side_effect = Exception("An error has occurred!")
db_uri = database.sqlalchemy_uri_decrypted
json_payload = {"sqlalchemy_uri": db_uri}
command_without_db_name = TestConnectionDatabaseCommand(
security_manager.find_user("admin"), json_payload
)
command_without_db_name = TestConnectionDatabaseCommand(json_payload)
with pytest.raises(DatabaseTestConnectionUnexpectedError) as excinfo:
command_without_db_name.run()
@ -664,19 +669,19 @@ class TestTestConnectionDatabaseCommand(SupersetTestCase):
@mock.patch(
"superset.databases.commands.test_connection.event_logger.log_with_context"
)
@mock.patch("superset.utils.core.g")
def test_connection_do_ping_exception(
self, mock_event_logger, mock_get_sqla_engine
self, mock_g, mock_event_logger, mock_get_sqla_engine
):
"""Test to make sure do_ping exceptions gets captured"""
database = get_example_database()
mock_g.user = security_manager.find_user("admin")
mock_get_sqla_engine.return_value.dialect.do_ping.side_effect = Exception(
"An error has occurred!"
)
db_uri = database.sqlalchemy_uri_decrypted
json_payload = {"sqlalchemy_uri": db_uri}
command_without_db_name = TestConnectionDatabaseCommand(
security_manager.find_user("admin"), json_payload
)
command_without_db_name = TestConnectionDatabaseCommand(json_payload)
with pytest.raises(DatabaseTestConnectionFailedError) as excinfo:
command_without_db_name.run()
@ -689,15 +694,17 @@ class TestTestConnectionDatabaseCommand(SupersetTestCase):
@mock.patch(
"superset.databases.commands.test_connection.event_logger.log_with_context"
)
def test_connection_do_ping_timeout(self, mock_event_logger, mock_func_timeout):
@mock.patch("superset.utils.core.g")
def test_connection_do_ping_timeout(
self, mock_g, mock_event_logger, mock_func_timeout
):
"""Test to make sure do_ping exceptions gets captured"""
database = get_example_database()
mock_g.user = security_manager.find_user("admin")
mock_func_timeout.side_effect = FunctionTimedOut("Time out")
db_uri = database.sqlalchemy_uri_decrypted
json_payload = {"sqlalchemy_uri": db_uri}
command_without_db_name = TestConnectionDatabaseCommand(
security_manager.find_user("admin"), json_payload
)
command_without_db_name = TestConnectionDatabaseCommand(json_payload)
with pytest.raises(SupersetTimeoutException) as excinfo:
command_without_db_name.run()
@ -711,20 +718,20 @@ class TestTestConnectionDatabaseCommand(SupersetTestCase):
@mock.patch(
"superset.databases.commands.test_connection.event_logger.log_with_context"
)
@mock.patch("superset.utils.core.g")
def test_connection_superset_security_connection(
self, mock_event_logger, mock_get_sqla_engine
self, mock_g, mock_event_logger, mock_get_sqla_engine
):
"""Test to make sure event_logger is called when security
connection exc is raised"""
database = get_example_database()
mock_g.user = security_manager.find_user("admin")
mock_get_sqla_engine.side_effect = SupersetSecurityException(
SupersetError(error_type=500, message="test", level="info")
)
db_uri = database.sqlalchemy_uri_decrypted
json_payload = {"sqlalchemy_uri": db_uri}
command_without_db_name = TestConnectionDatabaseCommand(
security_manager.find_user("admin"), json_payload
)
command_without_db_name = TestConnectionDatabaseCommand(json_payload)
with pytest.raises(DatabaseSecurityUnsafeError) as excinfo:
command_without_db_name.run()
@ -736,17 +743,19 @@ class TestTestConnectionDatabaseCommand(SupersetTestCase):
@mock.patch(
"superset.databases.commands.test_connection.event_logger.log_with_context"
)
def test_connection_db_api_exc(self, mock_event_logger, mock_get_sqla_engine):
@mock.patch("superset.utils.core.g")
def test_connection_db_api_exc(
self, mock_g, mock_event_logger, mock_get_sqla_engine
):
"""Test to make sure event_logger is called when DBAPIError is raised"""
database = get_example_database()
mock_g.user = security_manager.find_user("admin")
mock_get_sqla_engine.side_effect = DBAPIError(
statement="error", params={}, orig={}
)
db_uri = database.sqlalchemy_uri_decrypted
json_payload = {"sqlalchemy_uri": db_uri}
command_without_db_name = TestConnectionDatabaseCommand(
security_manager.find_user("admin"), json_payload
)
command_without_db_name = TestConnectionDatabaseCommand(json_payload)
with pytest.raises(DatabaseTestConnectionFailedError) as excinfo:
command_without_db_name.run()
@ -778,7 +787,7 @@ def test_validate(DatabaseDAO, is_port_open, is_hostname_valid, app_context):
"query": {},
},
}
command = ValidateDatabaseParametersCommand(None, payload)
command = ValidateDatabaseParametersCommand(payload)
command.run()
@ -802,7 +811,7 @@ def test_validate_partial(is_port_open, is_hostname_valid, app_context):
"query": {},
},
}
command = ValidateDatabaseParametersCommand(None, payload)
command = ValidateDatabaseParametersCommand(payload)
with pytest.raises(SupersetErrorsException) as excinfo:
command.run()
assert excinfo.value.errors == [
@ -841,7 +850,7 @@ def test_validate_partial_invalid_hostname(is_hostname_valid, app_context):
"query": {},
},
}
command = ValidateDatabaseParametersCommand(None, payload)
command = ValidateDatabaseParametersCommand(payload)
with pytest.raises(SupersetErrorsException) as excinfo:
command.run()
assert excinfo.value.errors == [

Some files were not shown because too many files have changed in this diff Show More