fix: Allow dataset owners to explore their datasets (#20382)

* fix: Allow dataset owners to explore their datasets

* Re-order imports

* Give owners security manager permissions to their datasets

* Update test suite

* Add SqlaTable to is_owner types

* Add owners to datasource mock

* Fix VSCode import error

* Fix merge error
This commit is contained in:
Reese 2022-07-06 10:27:50 -04:00 committed by GitHub
parent b39a3d8f78
commit f9109583ce
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 38 additions and 11 deletions

View File

@ -20,7 +20,8 @@ from flask_babel import lazy_gettext as _
from sqlalchemy import and_, or_ from sqlalchemy import and_, or_
from sqlalchemy.orm.query import Query from sqlalchemy.orm.query import Query
from superset import security_manager from superset import db, security_manager
from superset.connectors.sqla import models
from superset.connectors.sqla.models import SqlaTable from superset.connectors.sqla.models import SqlaTable
from superset.models.slice import Slice from superset.models.slice import Slice
from superset.views.base import BaseFilter from superset.views.base import BaseFilter
@ -77,6 +78,18 @@ class ChartFilter(BaseFilter): # pylint: disable=too-few-public-methods
return query return query
perms = security_manager.user_view_menu_names("datasource_access") perms = security_manager.user_view_menu_names("datasource_access")
schema_perms = security_manager.user_view_menu_names("schema_access") schema_perms = security_manager.user_view_menu_names("schema_access")
return query.filter( owner_ids_query = (
or_(self.model.perm.in_(perms), self.model.schema_perm.in_(schema_perms)) db.session.query(models.SqlaTable.id)
.join(models.SqlaTable.owners)
.filter(
security_manager.user_model.id
== security_manager.user_model.get_user_id()
)
)
return query.filter(
or_(
self.model.perm.in_(perms),
self.model.schema_perm.in_(schema_perms),
models.SqlaTable.id.in_(owner_ids_query),
)
) )

View File

@ -1093,6 +1093,7 @@ class SupersetSecurityManager( # pylint: disable=too-many-public-methods
from superset.connectors.sqla.models import SqlaTable from superset.connectors.sqla.models import SqlaTable
from superset.extensions import feature_flag_manager from superset.extensions import feature_flag_manager
from superset.sql_parse import Table from superset.sql_parse import Table
from superset.views.utils import is_owner
if database and table or query: if database and table or query:
if query: if query:
@ -1123,7 +1124,9 @@ class SupersetSecurityManager( # pylint: disable=too-many-public-methods
# Access to any datasource is suffice. # Access to any datasource is suffice.
for datasource_ in datasources: for datasource_ in datasources:
if self.can_access("datasource_access", datasource_.perm): if self.can_access(
"datasource_access", datasource_.perm
) or is_owner(datasource_, getattr(g, "user", None)):
break break
else: else:
denied.add(table_) denied.add(table_)
@ -1149,6 +1152,7 @@ class SupersetSecurityManager( # pylint: disable=too-many-public-methods
if not ( if not (
self.can_access_schema(datasource) self.can_access_schema(datasource)
or self.can_access("datasource_access", datasource.perm or "") or self.can_access("datasource_access", datasource.perm or "")
or is_owner(datasource, getattr(g, "user", None))
or ( or (
should_check_dashboard_access should_check_dashboard_access
and self.can_access_based_on_dashboard(datasource) and self.can_access_based_on_dashboard(datasource)

View File

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

View File

@ -21,7 +21,7 @@ import imp
import json import json
from contextlib import contextmanager from contextlib import contextmanager
from typing import Any, Dict, Union, List, Optional from typing import Any, Dict, Union, List, Optional
from unittest.mock import Mock, patch from unittest.mock import Mock, patch, MagicMock
import pandas as pd import pandas as pd
import pytest import pytest
@ -252,7 +252,7 @@ class SupersetTestCase(TestCase):
@staticmethod @staticmethod
def get_datasource_mock() -> BaseDatasource: def get_datasource_mock() -> BaseDatasource:
datasource = Mock() datasource = MagicMock()
results = Mock() results = Mock()
results.query = Mock() results.query = Mock()
results.status = Mock() results.status = Mock()
@ -266,6 +266,7 @@ class SupersetTestCase(TestCase):
datasource.database = Mock() datasource.database = Mock()
datasource.database.db_engine_spec = Mock() datasource.database.db_engine_spec = Mock()
datasource.database.db_engine_spec.mutate_expression_label = lambda x: x datasource.database.db_engine_spec.mutate_expression_label = lambda x: x
datasource.owners = MagicMock()
return datasource return datasource
def get_resp( def get_resp(

View File

@ -906,7 +906,10 @@ class TestSecurityManager(SupersetTestCase):
@patch("superset.security.SupersetSecurityManager.can_access") @patch("superset.security.SupersetSecurityManager.can_access")
@patch("superset.security.SupersetSecurityManager.can_access_schema") @patch("superset.security.SupersetSecurityManager.can_access_schema")
def test_raise_for_access_datasource(self, mock_can_access_schema, mock_can_access): @patch("superset.views.utils.is_owner")
def test_raise_for_access_datasource(
self, mock_can_access_schema, mock_can_access, mock_is_owner
):
datasource = self.get_datasource_mock() datasource = self.get_datasource_mock()
mock_can_access_schema.return_value = True mock_can_access_schema.return_value = True
@ -914,12 +917,14 @@ class TestSecurityManager(SupersetTestCase):
mock_can_access.return_value = False mock_can_access.return_value = False
mock_can_access_schema.return_value = False mock_can_access_schema.return_value = False
mock_is_owner.return_value = False
with self.assertRaises(SupersetSecurityException): with self.assertRaises(SupersetSecurityException):
security_manager.raise_for_access(datasource=datasource) security_manager.raise_for_access(datasource=datasource)
@patch("superset.security.SupersetSecurityManager.can_access") @patch("superset.security.SupersetSecurityManager.can_access")
def test_raise_for_access_query(self, mock_can_access): @patch("superset.views.utils.is_owner")
def test_raise_for_access_query(self, mock_can_access, mock_is_owner):
query = Mock( query = Mock(
database=get_example_database(), schema="bar", sql="SELECT * FROM foo" database=get_example_database(), schema="bar", sql="SELECT * FROM foo"
) )
@ -928,6 +933,7 @@ class TestSecurityManager(SupersetTestCase):
security_manager.raise_for_access(query=query) security_manager.raise_for_access(query=query)
mock_can_access.return_value = False mock_can_access.return_value = False
mock_is_owner.return_value = False
with self.assertRaises(SupersetSecurityException): with self.assertRaises(SupersetSecurityException):
security_manager.raise_for_access(query=query) security_manager.raise_for_access(query=query)

View File

@ -271,7 +271,7 @@ def test_query_has_access(mocker: MockFixture, app_context: AppContext) -> None:
) )
def test_query_no_access(mocker: MockFixture, app_context: AppContext) -> None: def test_query_no_access(mocker: MockFixture, client, app_context: AppContext) -> None:
from superset.connectors.sqla.models import SqlaTable from superset.connectors.sqla.models import SqlaTable
from superset.explore.utils import check_datasource_access from superset.explore.utils import check_datasource_access
from superset.models.core import Database from superset.models.core import Database
@ -282,7 +282,9 @@ def test_query_no_access(mocker: MockFixture, app_context: AppContext) -> None:
query_find_by_id, query_find_by_id,
return_value=Query(database=Database(), sql="select * from foo"), return_value=Query(database=Database(), sql="select * from foo"),
) )
mocker.patch(query_datasources_by_name, return_value=[SqlaTable()]) table = SqlaTable()
table.owners = []
mocker.patch(query_datasources_by_name, return_value=[table])
mocker.patch(is_user_admin, return_value=False) mocker.patch(is_user_admin, return_value=False)
mocker.patch(is_owner, return_value=False) mocker.patch(is_owner, return_value=False)
mocker.patch(can_access, return_value=False) mocker.patch(can_access, return_value=False)