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.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.models.slice import Slice
from superset.views.base import BaseFilter
@ -77,6 +78,18 @@ class ChartFilter(BaseFilter): # pylint: disable=too-few-public-methods
return query
perms = security_manager.user_view_menu_names("datasource_access")
schema_perms = security_manager.user_view_menu_names("schema_access")
return query.filter(
or_(self.model.perm.in_(perms), self.model.schema_perm.in_(schema_perms))
owner_ids_query = (
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.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:
@ -1123,7 +1124,9 @@ class SupersetSecurityManager( # pylint: disable=too-many-public-methods
# Access to any datasource is suffice.
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
else:
denied.add(table_)
@ -1149,6 +1152,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 (
should_check_dashboard_access
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
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 (
@ -426,7 +427,7 @@ def is_slice_in_container(
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"""
return obj and user in obj.owners

View File

@ -21,7 +21,7 @@ import imp
import json
from contextlib import contextmanager
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 pytest
@ -252,7 +252,7 @@ class SupersetTestCase(TestCase):
@staticmethod
def get_datasource_mock() -> BaseDatasource:
datasource = Mock()
datasource = MagicMock()
results = Mock()
results.query = Mock()
results.status = Mock()
@ -266,6 +266,7 @@ class SupersetTestCase(TestCase):
datasource.database = Mock()
datasource.database.db_engine_spec = Mock()
datasource.database.db_engine_spec.mutate_expression_label = lambda x: x
datasource.owners = MagicMock()
return datasource
def get_resp(

View File

@ -906,7 +906,10 @@ class TestSecurityManager(SupersetTestCase):
@patch("superset.security.SupersetSecurityManager.can_access")
@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()
mock_can_access_schema.return_value = True
@ -914,12 +917,14 @@ class TestSecurityManager(SupersetTestCase):
mock_can_access.return_value = False
mock_can_access_schema.return_value = False
mock_is_owner.return_value = False
with self.assertRaises(SupersetSecurityException):
security_manager.raise_for_access(datasource=datasource)
@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(
database=get_example_database(), schema="bar", sql="SELECT * FROM foo"
)
@ -928,6 +933,7 @@ class TestSecurityManager(SupersetTestCase):
security_manager.raise_for_access(query=query)
mock_can_access.return_value = False
mock_is_owner.return_value = False
with self.assertRaises(SupersetSecurityException):
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.explore.utils import check_datasource_access
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,
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_owner, return_value=False)
mocker.patch(can_access, return_value=False)