mirror of https://github.com/apache/superset.git
fix: dataset name change and permission change (#21161)
* fix: dataset name change and permission change
This commit is contained in:
parent
0c87ff783a
commit
3f2e894af3
|
@ -2266,11 +2266,11 @@ class SqlaTable(Model, BaseDatasource): # pylint: disable=too-many-public-metho
|
|||
|
||||
For more context: https://github.com/apache/superset/issues/14909
|
||||
"""
|
||||
security_manager.set_perm(mapper, connection, sqla_table)
|
||||
security_manager.dataset_after_insert(mapper, connection, sqla_table)
|
||||
sqla_table.write_shadow_dataset()
|
||||
|
||||
@staticmethod
|
||||
def after_delete( # pylint: disable=unused-argument
|
||||
def after_delete(
|
||||
mapper: Mapper,
|
||||
connection: Connection,
|
||||
sqla_table: "SqlaTable",
|
||||
|
@ -2287,6 +2287,7 @@ class SqlaTable(Model, BaseDatasource): # pylint: disable=too-many-public-metho
|
|||
|
||||
For more context: https://github.com/apache/superset/issues/14909
|
||||
"""
|
||||
security_manager.dataset_after_delete(mapper, connection, sqla_table)
|
||||
session = inspect(sqla_table).session
|
||||
dataset = (
|
||||
session.query(NewDataset).filter_by(uuid=sqla_table.uuid).one_or_none()
|
||||
|
@ -2313,7 +2314,7 @@ class SqlaTable(Model, BaseDatasource): # pylint: disable=too-many-public-metho
|
|||
For more context: https://github.com/apache/superset/issues/14909
|
||||
"""
|
||||
# set permissions
|
||||
security_manager.set_perm(mapper, connection, sqla_table)
|
||||
security_manager.dataset_after_update(mapper, connection, sqla_table)
|
||||
|
||||
inspector = inspect(sqla_table)
|
||||
session = inspector.session
|
||||
|
|
|
@ -32,6 +32,7 @@ from superset.databases.commands.exceptions import (
|
|||
from superset.databases.dao import DatabaseDAO
|
||||
from superset.extensions import db, security_manager
|
||||
from superset.models.core import Database
|
||||
from superset.utils.core import DatasourceType
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
@ -58,8 +59,10 @@ class UpdateDatabaseCommand(BaseCommand):
|
|||
except Exception as ex:
|
||||
db.session.rollback()
|
||||
raise DatabaseConnectionFailedError() from ex
|
||||
|
||||
# Update database schema permissions
|
||||
new_schemas: List[str] = []
|
||||
|
||||
for schema in schemas:
|
||||
old_view_menu_name = security_manager.get_schema_perm(
|
||||
old_database_name, schema
|
||||
|
@ -73,6 +76,10 @@ class UpdateDatabaseCommand(BaseCommand):
|
|||
# Update the schema permission if the database name changed
|
||||
if schema_pvm and old_database_name != database.database_name:
|
||||
schema_pvm.view_menu.name = new_view_menu_name
|
||||
|
||||
self._propagate_schema_permissions(
|
||||
old_view_menu_name, new_view_menu_name
|
||||
)
|
||||
else:
|
||||
new_schemas.append(schema)
|
||||
for schema in new_schemas:
|
||||
|
@ -86,6 +93,33 @@ class UpdateDatabaseCommand(BaseCommand):
|
|||
raise DatabaseUpdateFailedError() from ex
|
||||
return database
|
||||
|
||||
@staticmethod
|
||||
def _propagate_schema_permissions(
|
||||
old_view_menu_name: str, new_view_menu_name: str
|
||||
) -> None:
|
||||
from superset.connectors.sqla.models import ( # pylint: disable=import-outside-toplevel
|
||||
SqlaTable,
|
||||
)
|
||||
from superset.models.slice import ( # pylint: disable=import-outside-toplevel
|
||||
Slice,
|
||||
)
|
||||
|
||||
# Update schema_perm on all datasets
|
||||
datasets = (
|
||||
db.session.query(SqlaTable)
|
||||
.filter(SqlaTable.schema_perm == old_view_menu_name)
|
||||
.all()
|
||||
)
|
||||
for dataset in datasets:
|
||||
dataset.schema_perm = new_view_menu_name
|
||||
charts = db.session.query(Slice).filter(
|
||||
Slice.datasource_type == DatasourceType.TABLE,
|
||||
Slice.datasource_id == dataset.id,
|
||||
)
|
||||
# Update schema_perm on all charts
|
||||
for chart in charts:
|
||||
chart.schema_perm = new_view_menu_name
|
||||
|
||||
def validate(self) -> None:
|
||||
exceptions: List[ValidationError] = []
|
||||
# Validate/populate model exists
|
||||
|
|
|
@ -31,7 +31,7 @@ from superset.datasets.commands.exceptions import (
|
|||
TableNotFoundValidationError,
|
||||
)
|
||||
from superset.datasets.dao import DatasetDAO
|
||||
from superset.extensions import db, security_manager
|
||||
from superset.extensions import db
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
@ -47,15 +47,6 @@ class CreateDatasetCommand(CreateMixin, BaseCommand):
|
|||
dataset = DatasetDAO.create(self._properties, commit=False)
|
||||
# Updates columns and metrics from the dataset
|
||||
dataset.fetch_metadata(commit=False)
|
||||
# Add datasource access permission
|
||||
security_manager.add_permission_view_menu(
|
||||
"datasource_access", dataset.get_perm()
|
||||
)
|
||||
# Add schema access permission if exists
|
||||
if dataset.schema:
|
||||
security_manager.add_permission_view_menu(
|
||||
"schema_access", dataset.schema_perm
|
||||
)
|
||||
db.session.commit()
|
||||
except (SQLAlchemyError, DAOCreateFailedError) as ex:
|
||||
logger.warning(ex, exc_info=True)
|
||||
|
|
|
@ -45,30 +45,6 @@ class DeleteDatasetCommand(BaseCommand):
|
|||
self.validate()
|
||||
try:
|
||||
dataset = DatasetDAO.delete(self._model, commit=False)
|
||||
|
||||
view_menu = (
|
||||
security_manager.find_view_menu(self._model.get_perm())
|
||||
if self._model
|
||||
else None
|
||||
)
|
||||
|
||||
if view_menu:
|
||||
permission_views = (
|
||||
db.session.query(security_manager.permissionview_model)
|
||||
.filter_by(view_menu=view_menu)
|
||||
.all()
|
||||
)
|
||||
|
||||
for permission_view in permission_views:
|
||||
db.session.delete(permission_view)
|
||||
if view_menu:
|
||||
db.session.delete(view_menu)
|
||||
else:
|
||||
if not view_menu:
|
||||
logger.error(
|
||||
"Could not find the data access permission for the dataset",
|
||||
exc_info=True,
|
||||
)
|
||||
db.session.commit()
|
||||
except (SQLAlchemyError, DAODeleteFailedError) as ex:
|
||||
logger.exception(ex)
|
||||
|
|
|
@ -806,7 +806,7 @@ class Database(
|
|||
return sqla_url.get_dialect()()
|
||||
|
||||
|
||||
sqla.event.listen(Database, "after_insert", security_manager.set_perm)
|
||||
sqla.event.listen(Database, "after_insert", security_manager.database_after_insert)
|
||||
sqla.event.listen(Database, "after_update", security_manager.database_after_update)
|
||||
sqla.event.listen(Database, "after_delete", security_manager.database_after_delete)
|
||||
|
||||
|
|
|
@ -79,12 +79,18 @@ from superset.security.guest_token import (
|
|||
GuestTokenUser,
|
||||
GuestUser,
|
||||
)
|
||||
from superset.utils.core import DatasourceName, get_user_id, RowLevelSecurityFilterType
|
||||
from superset.utils.core import (
|
||||
DatasourceName,
|
||||
DatasourceType,
|
||||
get_user_id,
|
||||
RowLevelSecurityFilterType,
|
||||
)
|
||||
from superset.utils.urls import get_url_host
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from superset.common.query_context import QueryContext
|
||||
from superset.connectors.base.models import BaseDatasource
|
||||
from superset.connectors.sqla.models import SqlaTable
|
||||
from superset.models.core import Database
|
||||
from superset.models.dashboard import Dashboard
|
||||
from superset.models.sql_lab import Query
|
||||
|
@ -943,16 +949,89 @@ class SupersetSecurityManager( # pylint: disable=too-many-public-methods
|
|||
|
||||
return pvm.permission.name in {"can_override_role_permissions", "can_approve"}
|
||||
|
||||
def database_after_insert(
|
||||
self,
|
||||
mapper: Mapper,
|
||||
connection: Connection,
|
||||
target: "Database",
|
||||
) -> None:
|
||||
"""
|
||||
Handles permissions when a database is created.
|
||||
Triggered by a SQLAlchemy after_insert event.
|
||||
|
||||
We need to create:
|
||||
- The database PVM
|
||||
|
||||
:param mapper: The SQLA mapper
|
||||
:param connection: The SQLA connection
|
||||
:param target: The changed database object
|
||||
:return:
|
||||
"""
|
||||
self._insert_pvm_on_sqla_event(
|
||||
mapper, connection, "database_access", target.get_perm()
|
||||
)
|
||||
|
||||
def database_after_delete(
|
||||
self,
|
||||
mapper: Mapper,
|
||||
connection: Connection,
|
||||
target: "Database",
|
||||
) -> None:
|
||||
"""
|
||||
Handles permissions update when a database is deleted.
|
||||
Triggered by a SQLAlchemy after_delete event.
|
||||
|
||||
We need to delete:
|
||||
- The database PVM
|
||||
|
||||
:param mapper: The SQLA mapper
|
||||
:param connection: The SQLA connection
|
||||
:param target: The changed database object
|
||||
:return:
|
||||
"""
|
||||
self._delete_vm_database_access(
|
||||
mapper, connection, target.id, target.database_name
|
||||
)
|
||||
|
||||
def database_after_update(
|
||||
self,
|
||||
mapper: Mapper,
|
||||
connection: Connection,
|
||||
target: "Database",
|
||||
) -> None:
|
||||
"""
|
||||
Handles all permissions update when a database is changed.
|
||||
Triggered by a SQLAlchemy after_update event.
|
||||
|
||||
We need to update:
|
||||
- The database PVM
|
||||
- All datasets PVMs that reference the db, and it's local perm name
|
||||
- All datasets local schema perm that reference the db.
|
||||
- All charts local perm related with said datasets
|
||||
- All charts local schema perm related with said datasets
|
||||
|
||||
:param mapper: The SQLA mapper
|
||||
:param connection: The SQLA connection
|
||||
:param target: The changed database object
|
||||
:return:
|
||||
"""
|
||||
# Check if database name has changed
|
||||
state = inspect(target)
|
||||
history = state.get_history("database_name", True)
|
||||
if not history.has_changes() or not history.deleted:
|
||||
return
|
||||
|
||||
old_database_name = history.deleted[0]
|
||||
# update database access permission
|
||||
self._update_vm_database_access(mapper, connection, old_database_name, target)
|
||||
# update datasource access
|
||||
self._update_vm_datasources_access(
|
||||
mapper, connection, old_database_name, target
|
||||
)
|
||||
# Note schema permissions are updated at the API level
|
||||
# (database.commands.update). Since we need to fetch all existing schemas from
|
||||
# the db
|
||||
|
||||
def _delete_vm_database_access(
|
||||
self,
|
||||
mapper: Mapper,
|
||||
|
@ -960,29 +1039,11 @@ class SupersetSecurityManager( # pylint: disable=too-many-public-methods
|
|||
database_id: int,
|
||||
database_name: str,
|
||||
) -> None:
|
||||
view_menu_table = self.viewmenu_model.__table__ # pylint: disable=no-member
|
||||
permission_view_menu_table = (
|
||||
self.permissionview_model.__table__ # pylint: disable=no-member
|
||||
)
|
||||
view_menu_name = self.get_database_perm(database_id, database_name)
|
||||
# Clean database access permission
|
||||
db_pvm = self.find_permission_view_menu("database_access", view_menu_name)
|
||||
if not db_pvm:
|
||||
logger.warning(
|
||||
"Could not find previous database permission %s",
|
||||
view_menu_name,
|
||||
)
|
||||
return
|
||||
connection.execute(
|
||||
permission_view_menu_table.delete().where(
|
||||
permission_view_menu_table.c.id == db_pvm.id
|
||||
)
|
||||
self._delete_pvm_on_sqla_event(
|
||||
mapper, connection, "database_access", view_menu_name
|
||||
)
|
||||
self.on_permission_after_delete(mapper, connection, db_pvm)
|
||||
connection.execute(
|
||||
view_menu_table.delete().where(view_menu_table.c.id == db_pvm.view_menu_id)
|
||||
)
|
||||
|
||||
# Clean database schema permissions
|
||||
schema_pvms = (
|
||||
self.get_session.query(self.permissionview_model)
|
||||
|
@ -993,17 +1054,7 @@ class SupersetSecurityManager( # pylint: disable=too-many-public-methods
|
|||
.all()
|
||||
)
|
||||
for schema_pvm in schema_pvms:
|
||||
connection.execute(
|
||||
permission_view_menu_table.delete().where(
|
||||
permission_view_menu_table.c.id == schema_pvm.id
|
||||
)
|
||||
)
|
||||
self.on_permission_after_delete(mapper, connection, schema_pvm)
|
||||
connection.execute(
|
||||
view_menu_table.delete().where(
|
||||
view_menu_table.c.id == schema_pvm.view_menu_id
|
||||
)
|
||||
)
|
||||
self._delete_pvm_on_sqla_event(mapper, connection, pvm=schema_pvm)
|
||||
|
||||
def _update_vm_database_access(
|
||||
self,
|
||||
|
@ -1012,6 +1063,15 @@ class SupersetSecurityManager( # pylint: disable=too-many-public-methods
|
|||
old_database_name: str,
|
||||
target: "Database",
|
||||
) -> Optional[ViewMenu]:
|
||||
"""
|
||||
Helper method that Updates all database access permission
|
||||
when a database name changes.
|
||||
|
||||
:param connection: Current connection (called on SQLAlchemy event listener scope)
|
||||
:param old_database_name: the old database name
|
||||
:param target: The database object
|
||||
:return: A list of changed view menus (permission resource names)
|
||||
"""
|
||||
view_menu_table = self.viewmenu_model.__table__ # pylint: disable=no-member
|
||||
new_database_name = target.database_name
|
||||
old_view_menu_name = self.get_database_perm(target.id, old_database_name)
|
||||
|
@ -1022,6 +1082,9 @@ class SupersetSecurityManager( # pylint: disable=too-many-public-methods
|
|||
"Could not find previous database permission %s",
|
||||
old_view_menu_name,
|
||||
)
|
||||
self._insert_pvm_on_sqla_event(
|
||||
mapper, connection, "database_access", new_view_menu_name
|
||||
)
|
||||
return None
|
||||
new_updated_pvm = self.find_permission_view_menu(
|
||||
"database_access", new_view_menu_name
|
||||
|
@ -1053,11 +1116,12 @@ class SupersetSecurityManager( # pylint: disable=too-many-public-methods
|
|||
target: "Database",
|
||||
) -> List[ViewMenu]:
|
||||
"""
|
||||
Updates all datasource access permission when a database name changes
|
||||
Helper method that Updates all datasource access permission
|
||||
when a database name changes.
|
||||
|
||||
:param connection: Current connection (called on SQLAlchemy event listener scope)
|
||||
:param old_database_name: the old database name
|
||||
:param target: The new database name
|
||||
:param target: The database object
|
||||
:return: A list of changed view menus (permission resource names)
|
||||
"""
|
||||
from superset.connectors.sqla.models import ( # pylint: disable=import-outside-toplevel
|
||||
|
@ -1092,6 +1156,9 @@ class SupersetSecurityManager( # pylint: disable=too-many-public-methods
|
|||
.where(view_menu_table.c.name == old_dataset_vm_name)
|
||||
.values(name=new_dataset_vm_name)
|
||||
)
|
||||
# After update refresh
|
||||
new_dataset_view_menu = self.find_view_menu(new_dataset_vm_name)
|
||||
|
||||
# Update dataset (SqlaTable perm field)
|
||||
connection.execute(
|
||||
sqlatable_table.update()
|
||||
|
@ -1108,28 +1175,390 @@ class SupersetSecurityManager( # pylint: disable=too-many-public-methods
|
|||
.values(perm=new_dataset_vm_name)
|
||||
)
|
||||
self.on_view_menu_after_update(mapper, connection, new_dataset_view_menu)
|
||||
updated_view_menus.append(self.find_view_menu(new_dataset_view_menu))
|
||||
updated_view_menus.append(new_dataset_view_menu)
|
||||
return updated_view_menus
|
||||
|
||||
def database_after_update(
|
||||
def dataset_after_insert(
|
||||
self,
|
||||
mapper: Mapper,
|
||||
connection: Connection,
|
||||
target: "Database",
|
||||
target: "SqlaTable",
|
||||
) -> None:
|
||||
# Check if database name has changed
|
||||
state = inspect(target)
|
||||
history = state.get_history("database_name", True)
|
||||
if not history.has_changes() or not history.deleted:
|
||||
return
|
||||
"""
|
||||
Handles permission creation when a dataset is inserted.
|
||||
Triggered by a SQLAlchemy after_insert event.
|
||||
|
||||
old_database_name = history.deleted[0]
|
||||
# update database access permission
|
||||
self._update_vm_database_access(mapper, connection, old_database_name, target)
|
||||
# update datasource access
|
||||
self._update_vm_datasources_access(
|
||||
mapper, connection, old_database_name, target
|
||||
We need to create:
|
||||
- The dataset PVM and set local and schema perm
|
||||
|
||||
:param mapper: The SQLA mapper
|
||||
:param connection: The SQLA connection
|
||||
:param target: The changed dataset object
|
||||
:return:
|
||||
"""
|
||||
try:
|
||||
dataset_perm = target.get_perm()
|
||||
except DatasetInvalidPermissionEvaluationException:
|
||||
logger.warning("Dataset has no database refusing to set permission")
|
||||
return
|
||||
dataset_table = target.__table__
|
||||
|
||||
self._insert_pvm_on_sqla_event(
|
||||
mapper, connection, "datasource_access", dataset_perm
|
||||
)
|
||||
if target.perm != dataset_perm:
|
||||
target.perm = dataset_perm
|
||||
connection.execute(
|
||||
dataset_table.update()
|
||||
.where(dataset_table.c.id == target.id)
|
||||
.values(perm=dataset_perm)
|
||||
)
|
||||
|
||||
if target.schema:
|
||||
dataset_schema_perm = self.get_schema_perm(
|
||||
target.database.database_name, target.schema
|
||||
)
|
||||
self._insert_pvm_on_sqla_event(
|
||||
mapper, connection, "schema_access", dataset_schema_perm
|
||||
)
|
||||
target.schema_perm = dataset_schema_perm
|
||||
connection.execute(
|
||||
dataset_table.update()
|
||||
.where(dataset_table.c.id == target.id)
|
||||
.values(schema_perm=dataset_schema_perm)
|
||||
)
|
||||
|
||||
def dataset_after_delete(
|
||||
self,
|
||||
mapper: Mapper,
|
||||
connection: Connection,
|
||||
target: "SqlaTable",
|
||||
) -> None:
|
||||
"""
|
||||
Handles permissions update when a dataset is deleted.
|
||||
Triggered by a SQLAlchemy after_delete event.
|
||||
|
||||
We need to delete:
|
||||
- The dataset PVM
|
||||
|
||||
:param mapper: The SQLA mapper
|
||||
:param connection: The SQLA connection
|
||||
:param target: The changed dataset object
|
||||
:return:
|
||||
"""
|
||||
dataset_vm_name = self.get_dataset_perm(
|
||||
target.id, target.table_name, target.database.database_name
|
||||
)
|
||||
self._delete_pvm_on_sqla_event(
|
||||
mapper, connection, "datasource_access", dataset_vm_name
|
||||
)
|
||||
|
||||
def dataset_after_update(
|
||||
self,
|
||||
mapper: Mapper,
|
||||
connection: Connection,
|
||||
target: "SqlaTable",
|
||||
) -> None:
|
||||
"""
|
||||
Handles all permissions update when a dataset is changed.
|
||||
Triggered by a SQLAlchemy after_update event.
|
||||
|
||||
We need to update:
|
||||
- The dataset PVM and local perm
|
||||
- All charts local perm related with said datasets
|
||||
- All charts local schema perm related with said datasets
|
||||
|
||||
:param mapper: The SQLA mapper
|
||||
:param connection: The SQLA connection
|
||||
:param target: The changed dataset object
|
||||
:return:
|
||||
"""
|
||||
# Check if watched fields have changed
|
||||
state = inspect(target)
|
||||
history_database = state.get_history("database_id", True)
|
||||
history_table_name = state.get_history("table_name", True)
|
||||
history_schema = state.get_history("schema", True)
|
||||
|
||||
# When database name changes
|
||||
if history_database.has_changes() and history_database.deleted:
|
||||
new_dataset_vm_name = self.get_dataset_perm(
|
||||
target.id, target.table_name, target.database.database_name
|
||||
)
|
||||
self._update_dataset_perm(
|
||||
mapper, connection, target.perm, new_dataset_vm_name, target
|
||||
)
|
||||
|
||||
# Updates schema permissions
|
||||
new_dataset_schema_name = self.get_schema_perm(
|
||||
target.database.database_name, target.schema
|
||||
)
|
||||
self._update_dataset_schema_perm(
|
||||
mapper,
|
||||
connection,
|
||||
new_dataset_schema_name,
|
||||
target,
|
||||
)
|
||||
|
||||
# When table name changes
|
||||
if history_table_name.has_changes() and history_table_name.deleted:
|
||||
old_dataset_name = history_table_name.deleted[0]
|
||||
new_dataset_vm_name = self.get_dataset_perm(
|
||||
target.id, target.table_name, target.database.database_name
|
||||
)
|
||||
old_dataset_vm_name = self.get_dataset_perm(
|
||||
target.id, old_dataset_name, target.database.database_name
|
||||
)
|
||||
self._update_dataset_perm(
|
||||
mapper, connection, old_dataset_vm_name, new_dataset_vm_name, target
|
||||
)
|
||||
|
||||
# When schema changes
|
||||
if history_schema.has_changes() and history_schema.deleted:
|
||||
new_dataset_schema_name = self.get_schema_perm(
|
||||
target.database.database_name, target.schema
|
||||
)
|
||||
self._update_dataset_schema_perm(
|
||||
mapper,
|
||||
connection,
|
||||
new_dataset_schema_name,
|
||||
target,
|
||||
)
|
||||
|
||||
def _update_dataset_schema_perm(
|
||||
self,
|
||||
mapper: Mapper,
|
||||
connection: Connection,
|
||||
new_schema_permission_name: Optional[str],
|
||||
target: "SqlaTable",
|
||||
) -> None:
|
||||
"""
|
||||
Helper method that is called by SQLAlchemy events on datasets to update
|
||||
a new schema permission name, propagates the name change to datasets and charts.
|
||||
|
||||
If the schema permission name does not exist already has a PVM,
|
||||
creates a new one.
|
||||
|
||||
:param mapper: The SQLA event mapper
|
||||
:param connection: The SQLA connection
|
||||
:param new_schema_permission_name: The new schema permission name that changed
|
||||
:param target: Dataset that was updated
|
||||
:return:
|
||||
"""
|
||||
from superset.connectors.sqla.models import ( # pylint: disable=import-outside-toplevel
|
||||
SqlaTable,
|
||||
)
|
||||
from superset.models.slice import ( # pylint: disable=import-outside-toplevel
|
||||
Slice,
|
||||
)
|
||||
|
||||
sqlatable_table = SqlaTable.__table__ # pylint: disable=no-member
|
||||
chart_table = Slice.__table__ # pylint: disable=no-member
|
||||
|
||||
# insert new schema PVM if it does not exist
|
||||
self._insert_pvm_on_sqla_event(
|
||||
mapper, connection, "schema_access", new_schema_permission_name
|
||||
)
|
||||
|
||||
# Update dataset (SqlaTable schema_perm field)
|
||||
connection.execute(
|
||||
sqlatable_table.update()
|
||||
.where(
|
||||
sqlatable_table.c.id == target.id,
|
||||
)
|
||||
.values(schema_perm=new_schema_permission_name)
|
||||
)
|
||||
|
||||
# Update charts (Slice schema_perm field)
|
||||
connection.execute(
|
||||
chart_table.update()
|
||||
.where(
|
||||
chart_table.c.datasource_id == target.id,
|
||||
chart_table.c.datasource_type == DatasourceType.TABLE,
|
||||
)
|
||||
.values(schema_perm=new_schema_permission_name)
|
||||
)
|
||||
|
||||
def _update_dataset_perm( # pylint: disable=too-many-arguments
|
||||
self,
|
||||
mapper: Mapper,
|
||||
connection: Connection,
|
||||
old_permission_name: Optional[str],
|
||||
new_permission_name: Optional[str],
|
||||
target: "SqlaTable",
|
||||
) -> None:
|
||||
"""
|
||||
Helper method that is called by SQLAlchemy events on datasets to update
|
||||
a permission name change, propagates the name change to VM, datasets and charts.
|
||||
|
||||
:param mapper:
|
||||
:param connection:
|
||||
:param old_permission_name
|
||||
:param new_permission_name:
|
||||
:param target:
|
||||
:return:
|
||||
"""
|
||||
from superset.connectors.sqla.models import ( # pylint: disable=import-outside-toplevel
|
||||
SqlaTable,
|
||||
)
|
||||
from superset.models.slice import ( # pylint: disable=import-outside-toplevel
|
||||
Slice,
|
||||
)
|
||||
|
||||
view_menu_table = self.viewmenu_model.__table__ # pylint: disable=no-member
|
||||
sqlatable_table = SqlaTable.__table__ # pylint: disable=no-member
|
||||
chart_table = Slice.__table__ # pylint: disable=no-member
|
||||
|
||||
new_dataset_view_menu = self.find_view_menu(new_permission_name)
|
||||
if new_dataset_view_menu:
|
||||
return
|
||||
# Update VM
|
||||
connection.execute(
|
||||
view_menu_table.update()
|
||||
.where(view_menu_table.c.name == old_permission_name)
|
||||
.values(name=new_permission_name)
|
||||
)
|
||||
# VM changed, so call hook
|
||||
new_dataset_view_menu = self.find_view_menu(new_permission_name)
|
||||
self.on_view_menu_after_update(mapper, connection, new_dataset_view_menu)
|
||||
# Update dataset (SqlaTable perm field)
|
||||
connection.execute(
|
||||
sqlatable_table.update()
|
||||
.where(
|
||||
sqlatable_table.c.id == target.id,
|
||||
)
|
||||
.values(perm=new_permission_name)
|
||||
)
|
||||
# Update charts (Slice perm field)
|
||||
connection.execute(
|
||||
chart_table.update()
|
||||
.where(
|
||||
chart_table.c.datasource_type == DatasourceType.TABLE,
|
||||
chart_table.c.datasource_id == target.id,
|
||||
)
|
||||
.values(perm=new_permission_name)
|
||||
)
|
||||
|
||||
def _delete_pvm_on_sqla_event( # pylint: disable=too-many-arguments
|
||||
self,
|
||||
mapper: Mapper,
|
||||
connection: Connection,
|
||||
permission_name: Optional[str] = None,
|
||||
view_menu_name: Optional[str] = None,
|
||||
pvm: Optional[PermissionView] = None,
|
||||
) -> None:
|
||||
"""
|
||||
Helper method that is called by SQLAlchemy events.
|
||||
Deletes a PVM.
|
||||
|
||||
:param mapper: The SQLA event mapper
|
||||
:param connection: The SQLA connection
|
||||
:param permission_name: e.g.: datasource_access, schema_access
|
||||
:param view_menu_name: e.g. [db1].[public]
|
||||
:param pvm: Can be called with the actual PVM already
|
||||
:return:
|
||||
"""
|
||||
view_menu_table = self.viewmenu_model.__table__ # pylint: disable=no-member
|
||||
permission_view_menu_table = (
|
||||
self.permissionview_model.__table__ # pylint: disable=no-member
|
||||
)
|
||||
|
||||
if not pvm:
|
||||
pvm = self.find_permission_view_menu(permission_name, view_menu_name)
|
||||
if not pvm:
|
||||
return
|
||||
# Delete Any Role to PVM association
|
||||
connection.execute(
|
||||
assoc_permissionview_role.delete().where(
|
||||
assoc_permissionview_role.c.permission_view_id == pvm.id
|
||||
)
|
||||
)
|
||||
# Delete the database access PVM
|
||||
connection.execute(
|
||||
permission_view_menu_table.delete().where(
|
||||
permission_view_menu_table.c.id == pvm.id
|
||||
)
|
||||
)
|
||||
self.on_permission_view_after_delete(mapper, connection, pvm)
|
||||
connection.execute(
|
||||
view_menu_table.delete().where(view_menu_table.c.id == pvm.view_menu_id)
|
||||
)
|
||||
|
||||
def _insert_pvm_on_sqla_event(
|
||||
self,
|
||||
mapper: Mapper,
|
||||
connection: Connection,
|
||||
permission_name: str,
|
||||
view_menu_name: Optional[str],
|
||||
) -> None:
|
||||
"""
|
||||
Helper method that is called by SQLAlchemy events.
|
||||
Inserts a new PVM (if it does not exist already)
|
||||
|
||||
:param mapper: The SQLA event mapper
|
||||
:param connection: The SQLA connection
|
||||
:param permission_name: e.g.: datasource_access, schema_access
|
||||
:param view_menu_name: e.g. [db1].[public]
|
||||
:return:
|
||||
"""
|
||||
permission_table = self.permission_model.__table__ # pylint: disable=no-member
|
||||
view_menu_table = self.viewmenu_model.__table__ # pylint: disable=no-member
|
||||
permission_view_table = (
|
||||
self.permissionview_model.__table__ # pylint: disable=no-member
|
||||
)
|
||||
if not view_menu_name:
|
||||
return
|
||||
pvm = self.find_permission_view_menu(permission_name, view_menu_name)
|
||||
if pvm:
|
||||
return
|
||||
permission = self.find_permission(permission_name)
|
||||
view_menu = self.find_view_menu(view_menu_name)
|
||||
if not permission:
|
||||
connection.execute(permission_table.insert().values(name=permission_name))
|
||||
permission = self.find_permission(permission_name)
|
||||
self.on_permission_after_insert(mapper, connection, permission)
|
||||
if not view_menu:
|
||||
connection.execute(view_menu_table.insert().values(name=view_menu_name))
|
||||
view_menu = self.find_view_menu(view_menu_name)
|
||||
self.on_view_menu_after_insert(mapper, connection, view_menu)
|
||||
connection.execute(
|
||||
permission_view_table.insert().values(
|
||||
permission_id=permission.id, view_menu_id=view_menu.id
|
||||
)
|
||||
)
|
||||
permission = self.find_permission_view_menu(permission_name, view_menu_name)
|
||||
self.on_permission_view_after_insert(mapper, connection, permission)
|
||||
|
||||
def on_role_after_update(
|
||||
self, mapper: Mapper, connection: Connection, target: Role
|
||||
) -> None:
|
||||
"""
|
||||
Hook that allows for further custom operations when a Role update
|
||||
is created by SQLAlchemy events.
|
||||
|
||||
On SQLAlchemy after_insert events, we cannot
|
||||
create new view_menu's using a session, so any SQLAlchemy events hooked to
|
||||
`ViewMenu` will not trigger an after_insert.
|
||||
|
||||
:param mapper: The table mapper
|
||||
:param connection: The DB-API connection
|
||||
:param target: The mapped instance being changed
|
||||
"""
|
||||
|
||||
def on_view_menu_after_insert(
|
||||
self, mapper: Mapper, connection: Connection, target: ViewMenu
|
||||
) -> None:
|
||||
"""
|
||||
Hook that allows for further custom operations when a new ViewMenu
|
||||
is created by set_perm.
|
||||
|
||||
On SQLAlchemy after_insert events, we cannot
|
||||
create new view_menu's using a session, so any SQLAlchemy events hooked to
|
||||
`ViewMenu` will not trigger an after_insert.
|
||||
|
||||
:param mapper: The table mapper
|
||||
:param connection: The DB-API connection
|
||||
:param target: The mapped instance being persisted
|
||||
"""
|
||||
|
||||
def on_view_menu_after_update(
|
||||
self, mapper: Mapper, connection: Connection, target: ViewMenu
|
||||
|
@ -1147,18 +1576,6 @@ class SupersetSecurityManager( # pylint: disable=too-many-public-methods
|
|||
:param target: The mapped instance being persisted
|
||||
"""
|
||||
|
||||
def on_permission_after_delete(
|
||||
self, mapper: Mapper, connection: Connection, target: Permission
|
||||
) -> None:
|
||||
"""
|
||||
Hook that allows for further custom operations when a permission
|
||||
is deleted by sqlalchemy events.
|
||||
|
||||
:param mapper: The table mapper
|
||||
:param connection: The DB-API connection
|
||||
:param target: The mapped instance being persisted
|
||||
"""
|
||||
|
||||
def on_permission_after_insert(
|
||||
self, mapper: Mapper, connection: Connection, target: Permission
|
||||
) -> None:
|
||||
|
@ -1175,30 +1592,14 @@ class SupersetSecurityManager( # pylint: disable=too-many-public-methods
|
|||
:param target: The mapped instance being persisted
|
||||
"""
|
||||
|
||||
def on_view_menu_after_insert(
|
||||
self, mapper: Mapper, connection: Connection, target: ViewMenu
|
||||
) -> None:
|
||||
"""
|
||||
Hook that allows for further custom operations when a new ViewMenu
|
||||
is created by set_perm.
|
||||
|
||||
Since set_perm is executed by SQLAlchemy after_insert events, we cannot
|
||||
create new view_menu's using a session, so any SQLAlchemy events hooked to
|
||||
`ViewMenu` will not trigger an after_insert.
|
||||
|
||||
:param mapper: The table mapper
|
||||
:param connection: The DB-API connection
|
||||
:param target: The mapped instance being persisted
|
||||
"""
|
||||
|
||||
def on_permission_view_after_insert(
|
||||
self, mapper: Mapper, connection: Connection, target: PermissionView
|
||||
) -> None:
|
||||
"""
|
||||
Hook that allows for further custom operations when a new PermissionView
|
||||
is created by set_perm.
|
||||
is created by SQLAlchemy events.
|
||||
|
||||
Since set_perm is executed by SQLAlchemy after_insert events, we cannot
|
||||
On SQLAlchemy after_insert events, we cannot
|
||||
create new pvms using a session, so any SQLAlchemy events hooked to
|
||||
`PermissionView` will not trigger an after_insert.
|
||||
|
||||
|
@ -1207,98 +1608,21 @@ class SupersetSecurityManager( # pylint: disable=too-many-public-methods
|
|||
:param target: The mapped instance being persisted
|
||||
"""
|
||||
|
||||
def set_perm(
|
||||
self, mapper: Mapper, connection: Connection, target: "BaseDatasource"
|
||||
def on_permission_view_after_delete(
|
||||
self, mapper: Mapper, connection: Connection, target: PermissionView
|
||||
) -> None:
|
||||
"""
|
||||
Set the datasource permissions.
|
||||
Hook that allows for further custom operations when a new PermissionView
|
||||
is delete by SQLAlchemy events.
|
||||
|
||||
On SQLAlchemy after_delete events, we cannot
|
||||
delete pvms using a session, so any SQLAlchemy events hooked to
|
||||
`PermissionView` will not trigger an after_delete.
|
||||
|
||||
:param mapper: The table mapper
|
||||
:param connection: The DB-API connection
|
||||
:param target: The mapped instance being persisted
|
||||
"""
|
||||
try:
|
||||
target_get_perm = target.get_perm()
|
||||
except DatasetInvalidPermissionEvaluationException:
|
||||
logger.warning("Dataset has no database refusing to set permission")
|
||||
return
|
||||
permission_table = self.permission_model.__table__ # pylint: disable=no-member
|
||||
view_menu_table = self.viewmenu_model.__table__ # pylint: disable=no-member
|
||||
link_table = target.__table__
|
||||
if target.perm != target_get_perm:
|
||||
connection.execute(
|
||||
link_table.update()
|
||||
.where(link_table.c.id == target.id)
|
||||
.values(perm=target_get_perm)
|
||||
)
|
||||
connection.execute(
|
||||
permission_table.update()
|
||||
.where(permission_table.c.name == target.perm)
|
||||
.values(name=target_get_perm)
|
||||
)
|
||||
connection.execute(
|
||||
view_menu_table.update()
|
||||
.where(view_menu_table.c.name == target.perm)
|
||||
.values(name=target_get_perm)
|
||||
)
|
||||
target.perm = target_get_perm
|
||||
|
||||
# check schema perm for datasets
|
||||
if (
|
||||
hasattr(target, "schema_perm")
|
||||
and target.schema_perm != target.get_schema_perm()
|
||||
):
|
||||
connection.execute(
|
||||
link_table.update()
|
||||
.where(link_table.c.id == target.id)
|
||||
.values(schema_perm=target.get_schema_perm())
|
||||
)
|
||||
target.schema_perm = target.get_schema_perm()
|
||||
|
||||
pvm_names = []
|
||||
if target.__tablename__ in {"dbs", "clusters"}:
|
||||
pvm_names.append(("database_access", target_get_perm))
|
||||
else:
|
||||
pvm_names.append(("datasource_access", target_get_perm))
|
||||
if target.schema:
|
||||
pvm_names.append(("schema_access", target.get_schema_perm()))
|
||||
|
||||
# TODO(bogdan): modify slice permissions as well.
|
||||
for permission_name, view_menu_name in pvm_names:
|
||||
permission = self.find_permission(permission_name)
|
||||
view_menu = self.find_view_menu(view_menu_name)
|
||||
pv = None
|
||||
|
||||
if not permission:
|
||||
connection.execute(
|
||||
permission_table.insert().values(name=permission_name)
|
||||
)
|
||||
permission = self.find_permission(permission_name)
|
||||
self.on_permission_after_insert(mapper, connection, permission)
|
||||
if not view_menu:
|
||||
connection.execute(view_menu_table.insert().values(name=view_menu_name))
|
||||
view_menu = self.find_view_menu(view_menu_name)
|
||||
self.on_view_menu_after_insert(mapper, connection, view_menu)
|
||||
|
||||
if permission and view_menu:
|
||||
pv = (
|
||||
self.get_session.query(self.permissionview_model)
|
||||
.filter_by(permission=permission, view_menu=view_menu)
|
||||
.first()
|
||||
)
|
||||
if not pv and permission and view_menu:
|
||||
permission_view_table = (
|
||||
self.permissionview_model.__table__ # pylint: disable=no-member
|
||||
)
|
||||
connection.execute(
|
||||
permission_view_table.insert().values(
|
||||
permission_id=permission.id, view_menu_id=view_menu.id
|
||||
)
|
||||
)
|
||||
permission = self.find_permission_view_menu(
|
||||
permission_name, view_menu_name
|
||||
)
|
||||
self.on_permission_view_after_insert(mapper, connection, permission)
|
||||
|
||||
def raise_for_access(
|
||||
# pylint: disable=too-many-arguments,too-many-locals
|
||||
|
|
|
@ -267,6 +267,15 @@ class TestDatasetApi(SupersetTestCase):
|
|||
if backend() == "sqlite":
|
||||
return
|
||||
|
||||
# Add main database access to gamma role
|
||||
main_db = get_main_database()
|
||||
main_db_pvm = security_manager.find_permission_view_menu(
|
||||
"database_access", main_db.perm
|
||||
)
|
||||
gamma_role = security_manager.find_role("Gamma")
|
||||
gamma_role.permissions.append(main_db_pvm)
|
||||
db.session.commit()
|
||||
|
||||
self.login(username="gamma")
|
||||
uri = "api/v1/dataset/related/database"
|
||||
rv = self.client.get(uri)
|
||||
|
@ -277,6 +286,10 @@ class TestDatasetApi(SupersetTestCase):
|
|||
main_db = get_main_database()
|
||||
assert filter(lambda x: x.text == main_db, response["result"]) != []
|
||||
|
||||
# revert gamma permission
|
||||
gamma_role.permissions.remove(main_db_pvm)
|
||||
db.session.commit()
|
||||
|
||||
@pytest.mark.usefixtures("load_energy_table_with_slice")
|
||||
def test_get_dataset_item(self):
|
||||
"""
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -53,6 +53,9 @@ def get_session(mocker: MockFixture) -> Callable[[], Session]:
|
|||
get_session.return_value = in_memory_session
|
||||
# FAB calls get_session.get_bind() to get a handler to the engine
|
||||
get_session.get_bind.return_value = engine
|
||||
# Allow for queries on security manager
|
||||
get_session.query = in_memory_session.query
|
||||
|
||||
mocker.patch("superset.db.session", in_memory_session)
|
||||
return in_memory_session
|
||||
|
||||
|
|
Loading…
Reference in New Issue