Skip to content

Commit

Permalink
fix: Allow dataset owners to explore their datasets (apache#20382)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
reesercollins authored and codyml committed Jul 7, 2022
1 parent 4abe8c7 commit c9b0f65
Show file tree
Hide file tree
Showing 6 changed files with 37 additions and 10 deletions.
17 changes: 15 additions & 2 deletions superset/charts/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@
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
Expand Down Expand Up @@ -77,6 +78,18 @@ def apply(self, query: Query, value: Any) -> Query:
return query
perms = security_manager.user_view_menu_names("datasource_access")
schema_perms = security_manager.user_view_menu_names("schema_access")
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))
or_(
self.model.perm.in_(perms),
self.model.schema_perm.in_(schema_perms),
models.SqlaTable.id.in_(owner_ids_query),
)
)
6 changes: 5 additions & 1 deletion superset/security/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -1093,6 +1093,7 @@ def raise_for_access(
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:
Expand Down Expand Up @@ -1123,7 +1124,9 @@ def raise_for_access(

# 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_)
Expand All @@ -1149,6 +1152,7 @@ def raise_for_access(
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)
Expand Down
3 changes: 2 additions & 1 deletion superset/views/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
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 (
Expand Down Expand Up @@ -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

Expand Down
5 changes: 3 additions & 2 deletions tests/integration_tests/base_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
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
Expand Down Expand Up @@ -252,7 +252,7 @@ def get_database_by_name(database_name: str = "main") -> Database:

@staticmethod
def get_datasource_mock() -> BaseDatasource:
datasource = Mock()
datasource = MagicMock()
results = Mock()
results.query = Mock()
results.status = Mock()
Expand All @@ -266,6 +266,7 @@ def get_datasource_mock() -> BaseDatasource:
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(
Expand Down
10 changes: 8 additions & 2 deletions tests/integration_tests/security_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -906,20 +906,25 @@ def test_can_access_table(self, mock_raise_for_access):

@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
security_manager.raise_for_access(datasource=datasource)

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"
)
Expand All @@ -928,6 +933,7 @@ def test_raise_for_access_query(self, mock_can_access):
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)
Expand Down
6 changes: 4 additions & 2 deletions tests/unit_tests/explore/utils_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand Down

0 comments on commit c9b0f65

Please sign in to comment.