fix(reports): apply owners security validation (#12035)

* fix(reports): apply owners security validation

* fix pylint
This commit is contained in:
Daniel Vaz Gaspar
2020-12-15 08:43:31 +00:00
committed by GitHub
parent 329dcc314e
commit 20b1aa7d6c
6 changed files with 122 additions and 1 deletions

View File

@@ -34,6 +34,7 @@ from superset.reports.commands.exceptions import (
ReportScheduleBulkDeleteFailedError, ReportScheduleBulkDeleteFailedError,
ReportScheduleCreateFailedError, ReportScheduleCreateFailedError,
ReportScheduleDeleteFailedError, ReportScheduleDeleteFailedError,
ReportScheduleForbiddenError,
ReportScheduleInvalidError, ReportScheduleInvalidError,
ReportScheduleNotFoundError, ReportScheduleNotFoundError,
ReportScheduleUpdateFailedError, ReportScheduleUpdateFailedError,
@@ -192,6 +193,8 @@ class ReportScheduleRestApi(BaseSupersetModelRestApi):
properties: properties:
message: message:
type: string type: string
403:
$ref: '#/components/responses/403'
404: 404:
$ref: '#/components/responses/404' $ref: '#/components/responses/404'
422: 422:
@@ -204,6 +207,8 @@ class ReportScheduleRestApi(BaseSupersetModelRestApi):
return self.response(200, message="OK") return self.response(200, message="OK")
except ReportScheduleNotFoundError as ex: except ReportScheduleNotFoundError as ex:
return self.response_404() return self.response_404()
except ReportScheduleForbiddenError:
return self.response_403()
except ReportScheduleDeleteFailedError as ex: except ReportScheduleDeleteFailedError as ex:
logger.error( logger.error(
"Error deleting report schedule %s: %s", "Error deleting report schedule %s: %s",
@@ -278,7 +283,7 @@ class ReportScheduleRestApi(BaseSupersetModelRestApi):
@safe @safe
@statsd_metrics @statsd_metrics
@permission_name("put") @permission_name("put")
def put(self, pk: int) -> Response: def put(self, pk: int) -> Response: # pylint: disable=too-many-return-statements
"""Updates an Report Schedule """Updates an Report Schedule
--- ---
put: put:
@@ -313,6 +318,8 @@ class ReportScheduleRestApi(BaseSupersetModelRestApi):
$ref: '#/components/responses/400' $ref: '#/components/responses/400'
401: 401:
$ref: '#/components/responses/401' $ref: '#/components/responses/401'
403:
$ref: '#/components/responses/403'
404: 404:
$ref: '#/components/responses/404' $ref: '#/components/responses/404'
500: 500:
@@ -332,6 +339,8 @@ class ReportScheduleRestApi(BaseSupersetModelRestApi):
return self.response_404() return self.response_404()
except ReportScheduleInvalidError as ex: except ReportScheduleInvalidError as ex:
return self.response_422(message=ex.normalized_messages()) return self.response_422(message=ex.normalized_messages())
except ReportScheduleForbiddenError:
return self.response_403()
except ReportScheduleUpdateFailedError as ex: except ReportScheduleUpdateFailedError as ex:
logger.error( logger.error(
"Error updating report %s: %s", self.__class__.__name__, str(ex) "Error updating report %s: %s", self.__class__.__name__, str(ex)
@@ -368,6 +377,8 @@ class ReportScheduleRestApi(BaseSupersetModelRestApi):
type: string type: string
401: 401:
$ref: '#/components/responses/401' $ref: '#/components/responses/401'
403:
$ref: '#/components/responses/403'
404: 404:
$ref: '#/components/responses/404' $ref: '#/components/responses/404'
422: 422:
@@ -388,5 +399,7 @@ class ReportScheduleRestApi(BaseSupersetModelRestApi):
) )
except ReportScheduleNotFoundError: except ReportScheduleNotFoundError:
return self.response_404() return self.response_404()
except ReportScheduleForbiddenError:
return self.response_403()
except ReportScheduleBulkDeleteFailedError as ex: except ReportScheduleBulkDeleteFailedError as ex:
return self.response_422(message=str(ex)) return self.response_422(message=str(ex))

View File

@@ -21,12 +21,15 @@ from flask_appbuilder.security.sqla.models import User
from superset.commands.base import BaseCommand from superset.commands.base import BaseCommand
from superset.dao.exceptions import DAODeleteFailedError from superset.dao.exceptions import DAODeleteFailedError
from superset.exceptions import SupersetSecurityException
from superset.models.reports import ReportSchedule from superset.models.reports import ReportSchedule
from superset.reports.commands.exceptions import ( from superset.reports.commands.exceptions import (
ReportScheduleBulkDeleteFailedError, ReportScheduleBulkDeleteFailedError,
ReportScheduleForbiddenError,
ReportScheduleNotFoundError, ReportScheduleNotFoundError,
) )
from superset.reports.dao import ReportScheduleDAO from superset.reports.dao import ReportScheduleDAO
from superset.views.base import check_ownership
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -51,3 +54,10 @@ class BulkDeleteReportScheduleCommand(BaseCommand):
self._models = ReportScheduleDAO.find_by_ids(self._model_ids) self._models = ReportScheduleDAO.find_by_ids(self._model_ids)
if not self._models or len(self._models) != len(self._model_ids): if not self._models or len(self._models) != len(self._model_ids):
raise ReportScheduleNotFoundError() raise ReportScheduleNotFoundError()
# Check ownership
for model in self._models:
try:
check_ownership(model)
except SupersetSecurityException:
raise ReportScheduleForbiddenError()

View File

@@ -22,12 +22,15 @@ from flask_appbuilder.security.sqla.models import User
from superset.commands.base import BaseCommand from superset.commands.base import BaseCommand
from superset.dao.exceptions import DAODeleteFailedError from superset.dao.exceptions import DAODeleteFailedError
from superset.exceptions import SupersetSecurityException
from superset.models.reports import ReportSchedule from superset.models.reports import ReportSchedule
from superset.reports.commands.exceptions import ( from superset.reports.commands.exceptions import (
ReportScheduleDeleteFailedError, ReportScheduleDeleteFailedError,
ReportScheduleForbiddenError,
ReportScheduleNotFoundError, ReportScheduleNotFoundError,
) )
from superset.reports.dao import ReportScheduleDAO from superset.reports.dao import ReportScheduleDAO
from superset.views.base import check_ownership
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -52,3 +55,9 @@ class DeleteReportScheduleCommand(BaseCommand):
self._model = ReportScheduleDAO.find_by_id(self._model_id) self._model = ReportScheduleDAO.find_by_id(self._model_id)
if not self._model: if not self._model:
raise ReportScheduleNotFoundError() raise ReportScheduleNotFoundError()
# Check ownership
try:
check_ownership(self._model)
except SupersetSecurityException:
raise ReportScheduleForbiddenError()

View File

@@ -21,6 +21,7 @@ from superset.commands.exceptions import (
CommandInvalidError, CommandInvalidError,
CreateFailedError, CreateFailedError,
DeleteFailedError, DeleteFailedError,
ForbiddenError,
ValidationError, ValidationError,
) )
@@ -172,3 +173,7 @@ class ReportScheduleStateNotFoundError(CommandException):
class ReportScheduleUnexpectedError(CommandException): class ReportScheduleUnexpectedError(CommandException):
message = _("Report schedule unexpected error") message = _("Report schedule unexpected error")
class ReportScheduleForbiddenError(ForbiddenError):
message = _("Changing this report is forbidden")

View File

@@ -25,16 +25,19 @@ from marshmallow import ValidationError
from superset.commands.utils import populate_owners from superset.commands.utils import populate_owners
from superset.dao.exceptions import DAOUpdateFailedError from superset.dao.exceptions import DAOUpdateFailedError
from superset.databases.dao import DatabaseDAO from superset.databases.dao import DatabaseDAO
from superset.exceptions import SupersetSecurityException
from superset.models.reports import ReportSchedule, ReportScheduleType from superset.models.reports import ReportSchedule, ReportScheduleType
from superset.reports.commands.base import BaseReportScheduleCommand from superset.reports.commands.base import BaseReportScheduleCommand
from superset.reports.commands.exceptions import ( from superset.reports.commands.exceptions import (
DatabaseNotFoundValidationError, DatabaseNotFoundValidationError,
ReportScheduleForbiddenError,
ReportScheduleInvalidError, ReportScheduleInvalidError,
ReportScheduleNameUniquenessValidationError, ReportScheduleNameUniquenessValidationError,
ReportScheduleNotFoundError, ReportScheduleNotFoundError,
ReportScheduleUpdateFailedError, ReportScheduleUpdateFailedError,
) )
from superset.reports.dao import ReportScheduleDAO from superset.reports.dao import ReportScheduleDAO
from superset.views.base import check_ownership
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -93,6 +96,12 @@ class UpdateReportScheduleCommand(BaseReportScheduleCommand):
self._properties["validator_config_json"] self._properties["validator_config_json"]
) )
# Check ownership
try:
check_ownership(self._model)
except SupersetSecurityException:
raise ReportScheduleForbiddenError()
# Validate/Populate owner # Validate/Populate owner
if owner_ids is None: if owner_ids is None:
owner_ids = [owner.id for owner in self._model.owners] owner_ids = [owner.id for owner in self._model.owners]

View File

@@ -96,6 +96,26 @@ class TestReportSchedulesApi(SupersetTestCase):
db.session.delete(report_schedule) db.session.delete(report_schedule)
db.session.commit() db.session.commit()
@pytest.fixture()
def create_alpha_users(self):
with self.create_app().app_context():
users = [
self.create_user(
"alpha1", "password", "Alpha", email="alpha1@superset.org"
),
self.create_user(
"alpha2", "password", "Alpha", email="alpha2@superset.org"
),
]
yield users
# rollback changes (assuming cascade delete)
for user in users:
db.session.delete(user)
db.session.commit()
@pytest.mark.usefixtures("create_report_schedules") @pytest.mark.usefixtures("create_report_schedules")
def test_get_report_schedule(self): def test_get_report_schedule(self):
""" """
@@ -656,6 +676,26 @@ class TestReportSchedulesApi(SupersetTestCase):
data = json.loads(rv.data.decode("utf-8")) data = json.loads(rv.data.decode("utf-8"))
assert data == {"message": {"dashboard": "Dashboard does not exist"}} assert data == {"message": {"dashboard": "Dashboard does not exist"}}
@pytest.mark.usefixtures("create_report_schedules")
@pytest.mark.usefixtures("create_alpha_users")
def test_update_report_not_owned(self):
"""
ReportSchedule API: Test update report not owned
"""
report_schedule = (
db.session.query(ReportSchedule)
.filter(ReportSchedule.name == "name2")
.one_or_none()
)
self.login(username="alpha2", password="password")
report_schedule_data = {
"active": False,
}
uri = f"api/v1/report/{report_schedule.id}"
rv = self.put_assert_metric(uri, report_schedule_data, "put")
self.assertEqual(rv.status_code, 403)
@pytest.mark.usefixtures("create_report_schedules") @pytest.mark.usefixtures("create_report_schedules")
def test_delete_report_schedule(self): def test_delete_report_schedule(self):
""" """
@@ -698,6 +738,23 @@ class TestReportSchedulesApi(SupersetTestCase):
rv = self.client.delete(uri) rv = self.client.delete(uri)
assert rv.status_code == 404 assert rv.status_code == 404
@pytest.mark.usefixtures("create_report_schedules")
@pytest.mark.usefixtures("create_alpha_users")
def test_delete_report_not_owned(self):
"""
ReportSchedule API: Test delete try not owned
"""
report_schedule = (
db.session.query(ReportSchedule)
.filter(ReportSchedule.name == "name2")
.one_or_none()
)
self.login(username="alpha2", password="password")
uri = f"api/v1/report/{report_schedule.id}"
rv = self.client.delete(uri)
self.assertEqual(rv.status_code, 403)
@pytest.mark.usefixtures("create_report_schedules") @pytest.mark.usefixtures("create_report_schedules")
def test_bulk_delete_report_schedule(self): def test_bulk_delete_report_schedule(self):
""" """
@@ -737,6 +794,24 @@ class TestReportSchedulesApi(SupersetTestCase):
rv = self.client.delete(uri) rv = self.client.delete(uri)
assert rv.status_code == 404 assert rv.status_code == 404
@pytest.mark.usefixtures("create_report_schedules")
@pytest.mark.usefixtures("create_alpha_users")
def test_bulk_delete_report_not_owned(self):
"""
ReportSchedule API: Test bulk delete try not owned
"""
report_schedule = (
db.session.query(ReportSchedule)
.filter(ReportSchedule.name == "name2")
.one_or_none()
)
report_schedules_ids = [report_schedule.id]
self.login(username="alpha2", password="password")
uri = f"api/v1/report/?q={prison.dumps(report_schedules_ids)}"
rv = self.client.delete(uri)
self.assertEqual(rv.status_code, 403)
@pytest.mark.usefixtures("create_report_schedules") @pytest.mark.usefixtures("create_report_schedules")
def test_get_list_report_schedule_logs(self): def test_get_list_report_schedule_logs(self):
""" """