Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

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

Merged
merged 3 commits into from
Aug 13, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions superset/charts/commands/create.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,15 @@
DashboardsNotFoundValidationError,
)
from superset.charts.dao import ChartDAO
from superset.commands.base import BaseCommand
from superset.commands.utils import get_datasource_by_id, populate_owners
from superset.commands.base import BaseCommand, CreateMixin
from superset.commands.utils import get_datasource_by_id
from superset.dao.exceptions import DAOCreateFailedError
from superset.dashboards.dao import DashboardDAO

logger = logging.getLogger(__name__)


class CreateChartCommand(BaseCommand):
class CreateChartCommand(CreateMixin, BaseCommand):
def __init__(self, user: User, data: Dict[str, Any]):
self._actor = user
self._properties = data.copy()
Expand Down Expand Up @@ -73,7 +73,7 @@ def validate(self) -> None:
self._properties["dashboards"] = dashboards

try:
owners = populate_owners(self._actor, owner_ids)
owners = self.populate_owners(self._actor, owner_ids)
self._properties["owners"] = owners
except ValidationError as ex:
exceptions.append(ex)
Expand Down
8 changes: 4 additions & 4 deletions superset/charts/commands/update.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@
DatasourceTypeUpdateRequiredValidationError,
)
from superset.charts.dao import ChartDAO
from superset.commands.base import BaseCommand
from superset.commands.utils import get_datasource_by_id, populate_owners
from superset.commands.base import BaseCommand, UpdateMixin
from superset.commands.utils import get_datasource_by_id
from superset.dao.exceptions import DAOUpdateFailedError
from superset.dashboards.dao import DashboardDAO
from superset.exceptions import SupersetSecurityException
Expand All @@ -42,7 +42,7 @@
logger = logging.getLogger(__name__)


class UpdateChartCommand(BaseCommand):
class UpdateChartCommand(UpdateMixin, BaseCommand):
def __init__(self, user: User, model_id: int, data: Dict[str, Any]):
self._actor = user
self._model_id = model_id
Expand Down Expand Up @@ -100,7 +100,7 @@ def validate(self) -> None:

# Validate/Populate owner
try:
owners = populate_owners(self._actor, owner_ids)
owners = self.populate_owners(self._actor, owner_ids)
self._properties["owners"] = owners
except ValidationError as ex:
exceptions.append(ex)
Expand Down
41 changes: 40 additions & 1 deletion superset/commands/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,11 @@
# specific language governing permissions and limitations
# under the License.
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):
Expand All @@ -37,3 +41,38 @@ def validate(self) -> None:
Will raise exception if validation fails
:raises: CommandException
"""


class CreateMixin:
amitmiran137 marked this conversation as resolved.
Show resolved Hide resolved
@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)
20 changes: 14 additions & 6 deletions superset/commands/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,17 +29,25 @@
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
Can raise ValidationError
:param user: The current user
:param owner_ids: A List of owners by 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
:returns: Final list of owners
"""
owner_ids = owner_ids or []
amitmiran137 marked this conversation as resolved.
Show resolved Hide resolved
owners = list()
if not owner_ids:
if not owner_ids and default_to_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
]:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A bit out of scope, but there's an is_user_admin() on superset.views.base, seems misplaced and it probably would make more sense to place it on security manager. Also it uses g.user.
Another concern is that we may have a previous problem here regarding anonymous users (public access)

# make sure non-admins can't remove themselves as owner by mistake
owners.append(user)
for owner_id in owner_ids:
owner = security_manager.get_user_by_id(owner_id)
Expand Down
8 changes: 4 additions & 4 deletions superset/dashboards/commands/create.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@
from flask_appbuilder.security.sqla.models import User
from marshmallow import ValidationError

from superset.commands.base import BaseCommand
from superset.commands.utils import populate_owners, populate_roles
from superset.commands.base import BaseCommand, CreateMixin
from superset.commands.utils import populate_roles
from superset.dao.exceptions import DAOCreateFailedError
from superset.dashboards.commands.exceptions import (
DashboardCreateFailedError,
Expand All @@ -34,7 +34,7 @@
logger = logging.getLogger(__name__)


class CreateDashboardCommand(BaseCommand):
class CreateDashboardCommand(CreateMixin, BaseCommand):
def __init__(self, user: User, data: Dict[str, Any]):
self._actor = user
self._properties = data.copy()
Expand All @@ -60,7 +60,7 @@ def validate(self) -> None:
exceptions.append(DashboardSlugExistsValidationError())

try:
owners = populate_owners(self._actor, owner_ids)
owners = self.populate_owners(self._actor, owner_ids)
self._properties["owners"] = owners
except ValidationError as ex:
exceptions.append(ex)
Expand Down
8 changes: 4 additions & 4 deletions superset/dashboards/commands/update.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@
from flask_appbuilder.security.sqla.models import User
from marshmallow import ValidationError

from superset.commands.base import BaseCommand
from superset.commands.utils import populate_owners, populate_roles
from superset.commands.base import BaseCommand, UpdateMixin
from superset.commands.utils import populate_roles
from superset.dao.exceptions import DAOUpdateFailedError
from superset.dashboards.commands.exceptions import (
DashboardForbiddenError,
Expand All @@ -39,7 +39,7 @@
logger = logging.getLogger(__name__)


class UpdateDashboardCommand(BaseCommand):
class UpdateDashboardCommand(UpdateMixin, BaseCommand):
def __init__(self, user: User, model_id: int, data: Dict[str, Any]):
self._actor = user
self._model_id = model_id
Expand Down Expand Up @@ -80,7 +80,7 @@ def validate(self) -> None:
if owners_ids is None:
owners_ids = [owner.id for owner in self._model.owners]
try:
owners = populate_owners(self._actor, owners_ids)
owners = self.populate_owners(self._actor, owners_ids)
self._properties["owners"] = owners
except ValidationError as ex:
exceptions.append(ex)
Expand Down
7 changes: 3 additions & 4 deletions superset/datasets/commands/create.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,7 @@
from marshmallow import ValidationError
from sqlalchemy.exc import SQLAlchemyError

from superset.commands.base import BaseCommand
from superset.commands.utils import populate_owners
from superset.commands.base import BaseCommand, CreateMixin
from superset.dao.exceptions import DAOCreateFailedError
from superset.datasets.commands.exceptions import (
DatabaseNotFoundValidationError,
Expand All @@ -38,7 +37,7 @@
logger = logging.getLogger(__name__)


class CreateDatasetCommand(BaseCommand):
class CreateDatasetCommand(CreateMixin, BaseCommand):
def __init__(self, user: User, data: Dict[str, Any]):
self._actor = user
self._properties = data.copy()
Expand Down Expand Up @@ -90,7 +89,7 @@ def validate(self) -> None:
exceptions.append(TableNotFoundValidationError(table_name))

try:
owners = populate_owners(self._actor, owner_ids)
owners = self.populate_owners(self._actor, owner_ids)
self._properties["owners"] = owners
except ValidationError as ex:
exceptions.append(ex)
Expand Down
7 changes: 3 additions & 4 deletions superset/datasets/commands/update.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,7 @@
from flask_appbuilder.security.sqla.models import User
from marshmallow import ValidationError

from superset.commands.base import BaseCommand
from superset.commands.utils import populate_owners
from superset.commands.base import BaseCommand, UpdateMixin
from superset.connectors.sqla.models import SqlaTable
from superset.dao.exceptions import DAOUpdateFailedError
from superset.datasets.commands.exceptions import (
Expand All @@ -47,7 +46,7 @@
logger = logging.getLogger(__name__)


class UpdateDatasetCommand(BaseCommand):
class UpdateDatasetCommand(UpdateMixin, BaseCommand):
def __init__(
self,
user: User,
Expand Down Expand Up @@ -101,7 +100,7 @@ def validate(self) -> None:
exceptions.append(DatabaseChangeValidationError())
# Validate/Populate owner
try:
owners = populate_owners(self._actor, owner_ids)
owners = self.populate_owners(self._actor, owner_ids)
self._properties["owners"] = owners
except ValidationError as ex:
exceptions.append(ex)
Expand Down
6 changes: 3 additions & 3 deletions superset/reports/commands/create.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
from flask_appbuilder.security.sqla.models import User
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.databases.dao import DatabaseDAO
from superset.models.reports import ReportScheduleType
Expand All @@ -40,7 +40,7 @@
logger = logging.getLogger(__name__)


class CreateReportScheduleCommand(BaseReportScheduleCommand):
class CreateReportScheduleCommand(CreateMixin, BaseReportScheduleCommand):
def __init__(self, user: User, data: Dict[str, Any]):
self._actor = user
self._properties = data.copy()
Expand Down Expand Up @@ -90,7 +90,7 @@ def validate(self) -> None:
)

try:
owners = populate_owners(self._actor, owner_ids)
owners = self.populate_owners(self._actor, owner_ids)
self._properties["owners"] = owners
except ValidationError as ex:
exceptions.append(ex)
Expand Down
6 changes: 3 additions & 3 deletions superset/reports/commands/update.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
from flask_appbuilder.security.sqla.models import User
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.databases.dao import DatabaseDAO
from superset.exceptions import SupersetSecurityException
Expand All @@ -42,7 +42,7 @@
logger = logging.getLogger(__name__)


class UpdateReportScheduleCommand(BaseReportScheduleCommand):
class UpdateReportScheduleCommand(UpdateMixin, BaseReportScheduleCommand):
def __init__(self, user: User, model_id: int, data: Dict[str, Any]):
self._actor = user
self._model_id = model_id
Expand Down Expand Up @@ -117,7 +117,7 @@ def validate(self) -> None:
if owner_ids is None:
owner_ids = [owner.id for owner in self._model.owners]
try:
owners = populate_owners(self._actor, owner_ids)
owners = self.populate_owners(self._actor, owner_ids)
self._properties["owners"] = owners
except ValidationError as ex:
exceptions.append(ex)
Expand Down
31 changes: 25 additions & 6 deletions tests/integration_tests/charts/api_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -568,7 +568,7 @@ def test_update_chart(self):
self.assertEqual(model.created_by, admin)
self.assertEqual(model.slice_name, "title1_changed")
self.assertEqual(model.description, "description1")
self.assertIn(admin, model.owners)
self.assertNotIn(admin, model.owners)
self.assertIn(gamma, model.owners)
self.assertEqual(model.viz_type, "viz_type1")
self.assertEqual(model.params, """{"a": 1}""")
Expand All @@ -580,20 +580,39 @@ def test_update_chart(self):
db.session.delete(model)
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")
admin = self.get_user("admin")
chart_id = self.insert_chart("title", [gamma.id], 1).id
chart_data = {"slice_name": "title1_changed"}
chart_id = self.insert_chart("title", [admin.id], 1).id
chart_data = {"slice_name": "title1_changed", "owners": [gamma.id]}
self.login(username="admin")
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(admin, model.owners)
self.assertNotIn(admin, model.owners)
self.assertIn(gamma, model.owners)
db.session.delete(model)
db.session.commit()

Expand Down
Loading