feat(dao): admin can remove self from object owners (#15149)

This commit is contained in:
Ville Brofeldt 2021-08-13 12:42:48 +03:00 committed by GitHub
parent 517a678cd7
commit d6f9c48aa1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 138 additions and 51 deletions

View File

@ -28,15 +28,15 @@ from superset.charts.commands.exceptions import (
DashboardsNotFoundValidationError, DashboardsNotFoundValidationError,
) )
from superset.charts.dao import ChartDAO from superset.charts.dao import ChartDAO
from superset.commands.base import BaseCommand from superset.commands.base import BaseCommand, CreateMixin
from superset.commands.utils import get_datasource_by_id, populate_owners from superset.commands.utils import get_datasource_by_id
from superset.dao.exceptions import DAOCreateFailedError from superset.dao.exceptions import DAOCreateFailedError
from superset.dashboards.dao import DashboardDAO from superset.dashboards.dao import DashboardDAO
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class CreateChartCommand(BaseCommand): class CreateChartCommand(CreateMixin, BaseCommand):
def __init__(self, user: User, data: Dict[str, Any]): def __init__(self, user: User, data: Dict[str, Any]):
self._actor = user self._actor = user
self._properties = data.copy() self._properties = data.copy()
@ -73,7 +73,7 @@ class CreateChartCommand(BaseCommand):
self._properties["dashboards"] = dashboards self._properties["dashboards"] = dashboards
try: try:
owners = populate_owners(self._actor, owner_ids) owners = self.populate_owners(self._actor, owner_ids)
self._properties["owners"] = owners self._properties["owners"] = owners
except ValidationError as ex: except ValidationError as ex:
exceptions.append(ex) exceptions.append(ex)

View File

@ -31,8 +31,8 @@ from superset.charts.commands.exceptions import (
DatasourceTypeUpdateRequiredValidationError, DatasourceTypeUpdateRequiredValidationError,
) )
from superset.charts.dao import ChartDAO from superset.charts.dao import ChartDAO
from superset.commands.base import BaseCommand from superset.commands.base import BaseCommand, UpdateMixin
from superset.commands.utils import get_datasource_by_id, populate_owners from superset.commands.utils import get_datasource_by_id
from superset.dao.exceptions import DAOUpdateFailedError from superset.dao.exceptions import DAOUpdateFailedError
from superset.dashboards.dao import DashboardDAO from superset.dashboards.dao import DashboardDAO
from superset.exceptions import SupersetSecurityException from superset.exceptions import SupersetSecurityException
@ -42,7 +42,7 @@ from superset.views.base import check_ownership
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class UpdateChartCommand(BaseCommand): class UpdateChartCommand(UpdateMixin, BaseCommand):
def __init__(self, user: User, model_id: int, data: Dict[str, Any]): def __init__(self, user: User, model_id: int, data: Dict[str, Any]):
self._actor = user self._actor = user
self._model_id = model_id self._model_id = model_id
@ -100,7 +100,7 @@ class UpdateChartCommand(BaseCommand):
# Validate/Populate owner # Validate/Populate owner
try: try:
owners = populate_owners(self._actor, owner_ids) owners = self.populate_owners(self._actor, owner_ids)
self._properties["owners"] = owners self._properties["owners"] = owners
except ValidationError as ex: except ValidationError as ex:
exceptions.append(ex) exceptions.append(ex)

View File

@ -15,7 +15,11 @@
# specific language governing permissions and limitations # specific language governing permissions and limitations
# under the License. # under the License.
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from typing import Any from typing import Any, List, Optional
from flask_appbuilder.security.sqla.models import User
from superset.commands.utils import populate_owners
class BaseCommand(ABC): class BaseCommand(ABC):
@ -37,3 +41,38 @@ class BaseCommand(ABC):
Will raise exception if validation fails Will raise exception if validation fails
:raises: CommandException :raises: CommandException
""" """
class CreateMixin:
@staticmethod
def populate_owners(
user: User, 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)
class UpdateMixin:
@staticmethod
def populate_owners(
user: User, 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)

View File

@ -29,17 +29,25 @@ from superset.datasets.commands.exceptions import DatasetNotFoundError
from superset.extensions import db, security_manager from superset.extensions import db, security_manager
def populate_owners(user: User, owner_ids: Optional[List[int]] = None) -> List[User]: 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 Helper function for commands, will fetch all users from owners id's
Can raise ValidationError :param user: current user
:param user: The current user :param owner_ids: list of owners by id's
:param owner_ids: A 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
:returns: Final list of owners
""" """
owner_ids = owner_ids or []
owners = list() owners = list()
if not owner_ids: if not owner_ids and default_to_user:
return [user] return [user]
if user.id not in owner_ids: if user.id not in owner_ids and "admin" not in [
role.name.lower() for role in user.roles
]:
# make sure non-admins can't remove themselves as owner by mistake
owners.append(user) owners.append(user)
for owner_id in owner_ids: for owner_id in owner_ids:
owner = security_manager.get_user_by_id(owner_id) owner = security_manager.get_user_by_id(owner_id)

View File

@ -21,8 +21,8 @@ from flask_appbuilder.models.sqla import Model
from flask_appbuilder.security.sqla.models import User from flask_appbuilder.security.sqla.models import User
from marshmallow import ValidationError from marshmallow import ValidationError
from superset.commands.base import BaseCommand from superset.commands.base import BaseCommand, CreateMixin
from superset.commands.utils import populate_owners, populate_roles from superset.commands.utils import populate_roles
from superset.dao.exceptions import DAOCreateFailedError from superset.dao.exceptions import DAOCreateFailedError
from superset.dashboards.commands.exceptions import ( from superset.dashboards.commands.exceptions import (
DashboardCreateFailedError, DashboardCreateFailedError,
@ -34,7 +34,7 @@ from superset.dashboards.dao import DashboardDAO
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class CreateDashboardCommand(BaseCommand): class CreateDashboardCommand(CreateMixin, BaseCommand):
def __init__(self, user: User, data: Dict[str, Any]): def __init__(self, user: User, data: Dict[str, Any]):
self._actor = user self._actor = user
self._properties = data.copy() self._properties = data.copy()
@ -60,7 +60,7 @@ class CreateDashboardCommand(BaseCommand):
exceptions.append(DashboardSlugExistsValidationError()) exceptions.append(DashboardSlugExistsValidationError())
try: try:
owners = populate_owners(self._actor, owner_ids) owners = self.populate_owners(self._actor, owner_ids)
self._properties["owners"] = owners self._properties["owners"] = owners
except ValidationError as ex: except ValidationError as ex:
exceptions.append(ex) exceptions.append(ex)

View File

@ -21,8 +21,8 @@ from flask_appbuilder.models.sqla import Model
from flask_appbuilder.security.sqla.models import User from flask_appbuilder.security.sqla.models import User
from marshmallow import ValidationError from marshmallow import ValidationError
from superset.commands.base import BaseCommand from superset.commands.base import BaseCommand, UpdateMixin
from superset.commands.utils import populate_owners, populate_roles from superset.commands.utils import populate_roles
from superset.dao.exceptions import DAOUpdateFailedError from superset.dao.exceptions import DAOUpdateFailedError
from superset.dashboards.commands.exceptions import ( from superset.dashboards.commands.exceptions import (
DashboardForbiddenError, DashboardForbiddenError,
@ -39,7 +39,7 @@ from superset.views.base import check_ownership
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class UpdateDashboardCommand(BaseCommand): class UpdateDashboardCommand(UpdateMixin, BaseCommand):
def __init__(self, user: User, model_id: int, data: Dict[str, Any]): def __init__(self, user: User, model_id: int, data: Dict[str, Any]):
self._actor = user self._actor = user
self._model_id = model_id self._model_id = model_id
@ -80,7 +80,7 @@ class UpdateDashboardCommand(BaseCommand):
if owners_ids is None: if owners_ids is None:
owners_ids = [owner.id for owner in self._model.owners] owners_ids = [owner.id for owner in self._model.owners]
try: try:
owners = populate_owners(self._actor, owners_ids) owners = self.populate_owners(self._actor, owners_ids)
self._properties["owners"] = owners self._properties["owners"] = owners
except ValidationError as ex: except ValidationError as ex:
exceptions.append(ex) exceptions.append(ex)

View File

@ -22,8 +22,7 @@ from flask_appbuilder.security.sqla.models import User
from marshmallow import ValidationError from marshmallow import ValidationError
from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.exc import SQLAlchemyError
from superset.commands.base import BaseCommand from superset.commands.base import BaseCommand, CreateMixin
from superset.commands.utils import populate_owners
from superset.dao.exceptions import DAOCreateFailedError from superset.dao.exceptions import DAOCreateFailedError
from superset.datasets.commands.exceptions import ( from superset.datasets.commands.exceptions import (
DatabaseNotFoundValidationError, DatabaseNotFoundValidationError,
@ -38,7 +37,7 @@ from superset.extensions import db, security_manager
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class CreateDatasetCommand(BaseCommand): class CreateDatasetCommand(CreateMixin, BaseCommand):
def __init__(self, user: User, data: Dict[str, Any]): def __init__(self, user: User, data: Dict[str, Any]):
self._actor = user self._actor = user
self._properties = data.copy() self._properties = data.copy()
@ -90,7 +89,7 @@ class CreateDatasetCommand(BaseCommand):
exceptions.append(TableNotFoundValidationError(table_name)) exceptions.append(TableNotFoundValidationError(table_name))
try: try:
owners = populate_owners(self._actor, owner_ids) owners = self.populate_owners(self._actor, owner_ids)
self._properties["owners"] = owners self._properties["owners"] = owners
except ValidationError as ex: except ValidationError as ex:
exceptions.append(ex) exceptions.append(ex)

View File

@ -22,8 +22,7 @@ from flask_appbuilder.models.sqla import Model
from flask_appbuilder.security.sqla.models import User from flask_appbuilder.security.sqla.models import User
from marshmallow import ValidationError from marshmallow import ValidationError
from superset.commands.base import BaseCommand from superset.commands.base import BaseCommand, UpdateMixin
from superset.commands.utils import populate_owners
from superset.connectors.sqla.models import SqlaTable from superset.connectors.sqla.models import SqlaTable
from superset.dao.exceptions import DAOUpdateFailedError from superset.dao.exceptions import DAOUpdateFailedError
from superset.datasets.commands.exceptions import ( from superset.datasets.commands.exceptions import (
@ -47,7 +46,7 @@ from superset.views.base import check_ownership
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class UpdateDatasetCommand(BaseCommand): class UpdateDatasetCommand(UpdateMixin, BaseCommand):
def __init__( def __init__(
self, self,
user: User, user: User,
@ -101,7 +100,7 @@ class UpdateDatasetCommand(BaseCommand):
exceptions.append(DatabaseChangeValidationError()) exceptions.append(DatabaseChangeValidationError())
# Validate/Populate owner # Validate/Populate owner
try: try:
owners = populate_owners(self._actor, owner_ids) owners = self.populate_owners(self._actor, owner_ids)
self._properties["owners"] = owners self._properties["owners"] = owners
except ValidationError as ex: except ValidationError as ex:
exceptions.append(ex) exceptions.append(ex)

View File

@ -22,7 +22,7 @@ from flask_appbuilder.models.sqla import Model
from flask_appbuilder.security.sqla.models import User from flask_appbuilder.security.sqla.models import User
from marshmallow import ValidationError from marshmallow import ValidationError
from superset.commands.utils import populate_owners from superset.commands.base import CreateMixin
from superset.dao.exceptions import DAOCreateFailedError from superset.dao.exceptions import DAOCreateFailedError
from superset.databases.dao import DatabaseDAO from superset.databases.dao import DatabaseDAO
from superset.models.reports import ReportScheduleType from superset.models.reports import ReportScheduleType
@ -40,7 +40,7 @@ from superset.reports.dao import ReportScheduleDAO
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class CreateReportScheduleCommand(BaseReportScheduleCommand): class CreateReportScheduleCommand(CreateMixin, BaseReportScheduleCommand):
def __init__(self, user: User, data: Dict[str, Any]): def __init__(self, user: User, data: Dict[str, Any]):
self._actor = user self._actor = user
self._properties = data.copy() self._properties = data.copy()
@ -90,7 +90,7 @@ class CreateReportScheduleCommand(BaseReportScheduleCommand):
) )
try: try:
owners = populate_owners(self._actor, owner_ids) owners = self.populate_owners(self._actor, owner_ids)
self._properties["owners"] = owners self._properties["owners"] = owners
except ValidationError as ex: except ValidationError as ex:
exceptions.append(ex) exceptions.append(ex)

View File

@ -22,7 +22,7 @@ from flask_appbuilder.models.sqla import Model
from flask_appbuilder.security.sqla.models import User from flask_appbuilder.security.sqla.models import User
from marshmallow import ValidationError from marshmallow import ValidationError
from superset.commands.utils import populate_owners from superset.commands.base import UpdateMixin
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.exceptions import SupersetSecurityException
@ -42,7 +42,7 @@ from superset.views.base import check_ownership
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class UpdateReportScheduleCommand(BaseReportScheduleCommand): class UpdateReportScheduleCommand(UpdateMixin, BaseReportScheduleCommand):
def __init__(self, user: User, model_id: int, data: Dict[str, Any]): def __init__(self, user: User, model_id: int, data: Dict[str, Any]):
self._actor = user self._actor = user
self._model_id = model_id self._model_id = model_id
@ -117,7 +117,7 @@ class UpdateReportScheduleCommand(BaseReportScheduleCommand):
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]
try: try:
owners = populate_owners(self._actor, owner_ids) owners = self.populate_owners(self._actor, owner_ids)
self._properties["owners"] = owners self._properties["owners"] = owners
except ValidationError as ex: except ValidationError as ex:
exceptions.append(ex) exceptions.append(ex)

View File

@ -568,7 +568,7 @@ class TestChartApi(SupersetTestCase, ApiOwnersTestCaseMixin, InsertChartMixin):
self.assertEqual(model.created_by, admin) self.assertEqual(model.created_by, admin)
self.assertEqual(model.slice_name, "title1_changed") self.assertEqual(model.slice_name, "title1_changed")
self.assertEqual(model.description, "description1") self.assertEqual(model.description, "description1")
self.assertIn(admin, model.owners) self.assertNotIn(admin, model.owners)
self.assertIn(gamma, model.owners) self.assertIn(gamma, model.owners)
self.assertEqual(model.viz_type, "viz_type1") self.assertEqual(model.viz_type, "viz_type1")
self.assertEqual(model.params, """{"a": 1}""") self.assertEqual(model.params, """{"a": 1}""")
@ -580,20 +580,39 @@ class TestChartApi(SupersetTestCase, ApiOwnersTestCaseMixin, InsertChartMixin):
db.session.delete(model) db.session.delete(model)
db.session.commit() db.session.commit()
def test_update_chart_new_owner(self): def test_update_chart_new_owner_not_admin(self):
""" """
Chart API: Test update set new owner to current user Chart API: Test update set new owner implicitly adds logged in owner
"""
gamma = self.get_user("gamma")
alpha = self.get_user("alpha")
chart_id = self.insert_chart("title", [alpha.id], 1).id
chart_data = {"slice_name": "title1_changed", "owners": [gamma.id]}
self.login(username="alpha")
uri = f"api/v1/chart/{chart_id}"
rv = self.put_assert_metric(uri, chart_data, "put")
self.assertEqual(rv.status_code, 200)
model = db.session.query(Slice).get(chart_id)
self.assertIn(alpha, model.owners)
self.assertIn(gamma, model.owners)
db.session.delete(model)
db.session.commit()
def test_update_chart_new_owner_admin(self):
"""
Chart API: Test update set new owner as admin to other than current user
""" """
gamma = self.get_user("gamma") gamma = self.get_user("gamma")
admin = self.get_user("admin") admin = self.get_user("admin")
chart_id = self.insert_chart("title", [gamma.id], 1).id chart_id = self.insert_chart("title", [admin.id], 1).id
chart_data = {"slice_name": "title1_changed"} chart_data = {"slice_name": "title1_changed", "owners": [gamma.id]}
self.login(username="admin") self.login(username="admin")
uri = f"api/v1/chart/{chart_id}" uri = f"api/v1/chart/{chart_id}"
rv = self.put_assert_metric(uri, chart_data, "put") rv = self.put_assert_metric(uri, chart_data, "put")
self.assertEqual(rv.status_code, 200) self.assertEqual(rv.status_code, 200)
model = db.session.query(Slice).get(chart_id) model = db.session.query(Slice).get(chart_id)
self.assertIn(admin, model.owners) self.assertNotIn(admin, model.owners)
self.assertIn(gamma, model.owners)
db.session.delete(model) db.session.delete(model)
db.session.commit() db.session.commit()

View File

@ -1109,7 +1109,7 @@ class TestDashboardApi(SupersetTestCase, ApiOwnersTestCaseMixin, InsertChartMixi
for slice in slices: for slice in slices:
self.assertIn(user_alpha1, slice.owners) self.assertIn(user_alpha1, slice.owners)
self.assertIn(user_alpha2, slice.owners) self.assertIn(user_alpha2, slice.owners)
self.assertIn(admin, slice.owners) self.assertNotIn(admin, slice.owners)
# Revert owners on slice # Revert owners on slice
slice.owners = [] slice.owners = []
db.session.commit() db.session.commit()
@ -1149,22 +1149,45 @@ class TestDashboardApi(SupersetTestCase, ApiOwnersTestCaseMixin, InsertChartMixi
db.session.delete(model) db.session.delete(model)
db.session.commit() db.session.commit()
def test_update_dashboard_new_owner(self): def test_update_dashboard_new_owner_not_admin(self):
""" """
Dashboard API: Test update set new owner to current user Dashboard API: Test update set new owner implicitly adds logged in owner
""" """
gamma_id = self.get_user("gamma").id gamma = self.get_user("gamma")
alpha = self.get_user("alpha")
dashboard_id = self.insert_dashboard("title1", "slug1", [alpha.id]).id
dashboard_data = {"dashboard_title": "title1_changed", "owners": [gamma.id]}
self.login(username="alpha")
uri = f"api/v1/dashboard/{dashboard_id}"
rv = self.client.put(uri, json=dashboard_data)
self.assertEqual(rv.status_code, 200)
model = db.session.query(Dashboard).get(dashboard_id)
self.assertIn(gamma, model.owners)
self.assertIn(alpha, model.owners)
for slc in model.slices:
self.assertIn(gamma, slc.owners)
self.assertIn(alpha, slc.owners)
db.session.delete(model)
db.session.commit()
def test_update_dashboard_new_owner_admin(self):
"""
Dashboard API: Test update set new owner as admin to other than current user
"""
gamma = self.get_user("gamma")
admin = self.get_user("admin") admin = self.get_user("admin")
dashboard_id = self.insert_dashboard("title1", "slug1", [gamma_id]).id dashboard_id = self.insert_dashboard("title1", "slug1", [admin.id]).id
dashboard_data = {"dashboard_title": "title1_changed"} dashboard_data = {"dashboard_title": "title1_changed", "owners": [gamma.id]}
self.login(username="admin") self.login(username="admin")
uri = f"api/v1/dashboard/{dashboard_id}" uri = f"api/v1/dashboard/{dashboard_id}"
rv = self.client.put(uri, json=dashboard_data) rv = self.client.put(uri, json=dashboard_data)
self.assertEqual(rv.status_code, 200) self.assertEqual(rv.status_code, 200)
model = db.session.query(Dashboard).get(dashboard_id) model = db.session.query(Dashboard).get(dashboard_id)
self.assertIn(admin, model.owners) self.assertIn(gamma, model.owners)
self.assertNotIn(admin, model.owners)
for slc in model.slices: for slc in model.slices:
self.assertIn(admin, slc.owners) self.assertIn(gamma, slc.owners)
self.assertNotIn(admin, slc.owners)
db.session.delete(model) db.session.delete(model)
db.session.commit() db.session.commit()