From e312782c227336c0012cfe7201150399e3992a95 Mon Sep 17 00:00:00 2001 From: legal-s Date: Fri, 31 Oct 2025 19:42:38 +0100 Subject: [PATCH 1/4] feat(owners): owners of charts and datasets can now see the resources they own --- superset/utils/charts.py | 49 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 superset/utils/charts.py diff --git a/superset/utils/charts.py b/superset/utils/charts.py new file mode 100644 index 000000000000..3d32492494eb --- /dev/null +++ b/superset/utils/charts.py @@ -0,0 +1,49 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +from superset import db, security_manager +from superset.connectors.sqla.models import SqlaTable, sqlatable_roles, sqlatable_user +from superset.utils.core import QueryObjectFilterClause, get_user_id + +def get_datasets_authorized_for_user_roles() -> list[QueryObjectFilterClause]: + """ + Function that returns the list of datasets authorized by the user's roles + """ + return ( + db.session.query(sqlatable_roles.c.table_id) + .join( + SqlaTable, + SqlaTable.id == sqlatable_roles.c.table_id, + ) + .filter( + sqlatable_roles.c.role_id.in_( + [x.id for x in security_manager.get_user_roles()] + ), + ) + ) + +def get_datasets_authorized_for_owners() -> list[QueryObjectFilterClause]: + """ + Function that returns the list of datasets authorized for the owners + """ + return ( + db.session.query(sqlatable_user.c.table_id) + .join( + SqlaTable, + SqlaTable.id == sqlatable_user.c.table_id, + ) + .filter(sqlatable_user.c.user_id == get_user_id()) + ) From 500f0cc51ff9d37ea8437700f55d57228fe0cfc7 Mon Sep 17 00:00:00 2001 From: legal-s Date: Fri, 31 Oct 2025 19:45:09 +0100 Subject: [PATCH 2/4] feat(owners): owners of charts and datasets can now see the resources they own --- superset/charts/filters.py | 13 +++++++++++-- superset/utils/charts.py | 34 +++++++++------------------------- superset/utils/datasets.py | 18 ++++++++++++++++-- superset/views/base.py | 13 +++++++++++-- 4 files changed, 47 insertions(+), 31 deletions(-) diff --git a/superset/charts/filters.py b/superset/charts/filters.py index 1d13ec177f3d..88d433b1edd5 100644 --- a/superset/charts/filters.py +++ b/superset/charts/filters.py @@ -27,8 +27,11 @@ from superset.models.core import FavStar from superset.models.slice import Slice from superset.tags.filters import BaseTagIdFilter, BaseTagNameFilter +from superset.utils.charts import get_charts_authorized_for_owners from superset.utils.core import get_user_id -from superset.utils.datasets import get_datasets_authorized_for_user_roles +from superset.utils.datasets import ( + get_datasets_authorized_for_user_roles, +) from superset.utils.filters import get_dataset_access_filters from superset.views.base import BaseFilter from superset.views.base_api import BaseFavoriteFilter @@ -111,12 +114,18 @@ def apply(self, query: Query, value: Any) -> Query: models.Database, table_alias.database_id == models.Database.id ) - # Select datasets authorized for this user's roles + # Display the charts for which the current user is the owner + # or authorized by dataset rights for one of the user's groups. feature_flagged_filters = [] if is_feature_enabled("DATASET_RBAC"): + # Roles access roles_based_query = get_datasets_authorized_for_user_roles() feature_flagged_filters.append(SqlaTable.id.in_(roles_based_query)) + # Owners access + owners_based_query = get_charts_authorized_for_owners() + feature_flagged_filters.append(Slice.id.in_(owners_based_query)) + return query.filter( or_(get_dataset_access_filters(self.model), *feature_flagged_filters) ) diff --git a/superset/utils/charts.py b/superset/utils/charts.py index 3d32492494eb..f5bd5b7d2ca3 100644 --- a/superset/utils/charts.py +++ b/superset/utils/charts.py @@ -14,36 +14,20 @@ # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. -from superset import db, security_manager -from superset.connectors.sqla.models import SqlaTable, sqlatable_roles, sqlatable_user -from superset.utils.core import QueryObjectFilterClause, get_user_id +from superset import db +from superset.models.slice import Slice, slice_user +from superset.utils.core import get_user_id, QueryObjectFilterClause -def get_datasets_authorized_for_user_roles() -> list[QueryObjectFilterClause]: - """ - Function that returns the list of datasets authorized by the user's roles - """ - return ( - db.session.query(sqlatable_roles.c.table_id) - .join( - SqlaTable, - SqlaTable.id == sqlatable_roles.c.table_id, - ) - .filter( - sqlatable_roles.c.role_id.in_( - [x.id for x in security_manager.get_user_roles()] - ), - ) - ) -def get_datasets_authorized_for_owners() -> list[QueryObjectFilterClause]: +def get_charts_authorized_for_owners() -> list[QueryObjectFilterClause]: """ - Function that returns the list of datasets authorized for the owners + Function that returns the list of charts where current user is owner """ return ( - db.session.query(sqlatable_user.c.table_id) + db.session.query(slice_user.c.slice_id) .join( - SqlaTable, - SqlaTable.id == sqlatable_user.c.table_id, + Slice, + Slice.id == slice_user.c.slice_id, ) - .filter(sqlatable_user.c.user_id == get_user_id()) + .filter(slice_user.c.user_id == get_user_id()) ) diff --git a/superset/utils/datasets.py b/superset/utils/datasets.py index 5d8f00c682c8..fe82847dd921 100644 --- a/superset/utils/datasets.py +++ b/superset/utils/datasets.py @@ -15,8 +15,8 @@ # specific language governing permissions and limitations # under the License. from superset import db, security_manager -from superset.connectors.sqla.models import SqlaTable, sqlatable_roles -from superset.utils.core import QueryObjectFilterClause +from superset.connectors.sqla.models import SqlaTable, sqlatable_roles, sqlatable_user +from superset.utils.core import get_user_id, QueryObjectFilterClause def get_datasets_authorized_for_user_roles() -> list[QueryObjectFilterClause]: @@ -35,3 +35,17 @@ def get_datasets_authorized_for_user_roles() -> list[QueryObjectFilterClause]: ), ) ) + + +def get_datasets_authorized_for_owners() -> list[QueryObjectFilterClause]: + """ + Function that returns the list of datasets where user is owner + """ + return ( + db.session.query(sqlatable_user.c.table_id) + .join( + SqlaTable, + SqlaTable.id == sqlatable_user.c.table_id, + ) + .filter(sqlatable_user.c.user_id == get_user_id()) + ) diff --git a/superset/views/base.py b/superset/views/base.py index 95aeb4411a34..aca69fa8eba5 100644 --- a/superset/views/base.py +++ b/superset/views/base.py @@ -67,7 +67,10 @@ from superset.utils.filters import get_dataset_access_filters from superset.views.error_handling import json_error_response -from ..utils.datasets import get_datasets_authorized_for_user_roles +from ..utils.datasets import ( + get_datasets_authorized_for_owners, + get_datasets_authorized_for_user_roles, +) from .utils import bootstrap_user_data FRONTEND_CONF_KEYS = ( @@ -452,12 +455,18 @@ def apply(self, query: Query, value: Any) -> Query: models.Database.id == self.model.database_id, ) - # Select datasets authorized for this user's roles + # Display the datasets for which the current user is the owner + # or authorized for one of the user's groups. feature_flagged_filters = [] if is_feature_enabled("DATASET_RBAC"): + # Role access roles_based_query = get_datasets_authorized_for_user_roles() feature_flagged_filters.append(SqlaTable.id.in_(roles_based_query)) + # Owner + owners_based_query = get_datasets_authorized_for_owners() + feature_flagged_filters.append(SqlaTable.id.in_(owners_based_query)) + return query.filter( or_(get_dataset_access_filters(self.model), *feature_flagged_filters) ) From dde19d5d7536756bb37af1ddf4eb4842e2e77fa3 Mon Sep 17 00:00:00 2001 From: legal-s Date: Fri, 31 Oct 2025 20:05:55 +0100 Subject: [PATCH 3/4] chore: update chart after edit --- .../PropertiesModal/PropertiesModal.test.tsx | 11 +++++----- .../components/PropertiesModal/index.tsx | 20 +++++++++---------- 2 files changed, 15 insertions(+), 16 deletions(-) diff --git a/superset-frontend/src/explore/components/PropertiesModal/PropertiesModal.test.tsx b/superset-frontend/src/explore/components/PropertiesModal/PropertiesModal.test.tsx index 49c5d3b0480a..40cbcc4bc8a2 100644 --- a/superset-frontend/src/explore/components/PropertiesModal/PropertiesModal.test.tsx +++ b/superset-frontend/src/explore/components/PropertiesModal/PropertiesModal.test.tsx @@ -58,16 +58,16 @@ fetchMock.get('glob:*/api/v1/chart/318', { viz_type: 'Viz Type', }, result: { - cache_timeout: null, - certified_by: 'John Doe', - certification_details: 'Sample certification', + cache_timeout: '1000', + certified_by: 'Test certified by', + certification_details: 'Test certification details', dashboards: [ { dashboard_title: 'FCC New Coder Survey 2018', id: 23, }, ], - description: null, + description: 'Test description', owners: [ { first_name: 'Superset', @@ -78,8 +78,9 @@ fetchMock.get('glob:*/api/v1/chart/318', { ], params: '{"adhoc_filters": [], "all_columns_x": ["age"], "color_scheme": "supersetColors", "datasource": "42__table", "granularity_sqla": "time_start", "groupby": null, "label_colors": {}, "link_length": "25", "queryFields": {"groupby": "groupby"}, "row_limit": 10000, "slice_id": 1380, "time_range": "No filter", "url_params": {}, "viz_type": "histogram", "x_axis_label": "age", "y_axis_label": "count"}', - slice_name: 'Age distribution of respondents', + slice_name: 'Test chart new name', viz_type: VizType.Histogram, + show_title: 'Show Slice', }, show_columns: [ 'cache_timeout', diff --git a/superset-frontend/src/explore/components/PropertiesModal/index.tsx b/superset-frontend/src/explore/components/PropertiesModal/index.tsx index 9a6831fc595c..bbf2e52038c6 100644 --- a/superset-frontend/src/explore/components/PropertiesModal/index.tsx +++ b/superset-frontend/src/explore/components/PropertiesModal/index.tsx @@ -174,20 +174,18 @@ function PropertiesModal({ } try { - const res = await SupersetClient.put({ - endpoint: `/api/v1/chart/${slice.slice_id}`, + // Retrieving the code from the following issue: + // https://github.com/apache/superset/pull/33392 + const chartEndpoint = `/api/v1/chart/${slice.slice_id}`; + let res = await SupersetClient.put({ + endpoint: chartEndpoint, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), }); - // update the redux state - const updatedChart = { - ...payload, - ...res.json.result, - tags, - id: slice.slice_id, - owners: selectedOwners, - }; - onSave(updatedChart); + res = await SupersetClient.get({ + endpoint: chartEndpoint, + }); + onSave(res.json.result); addSuccessToast(t('Chart properties updated')); onHide(); } catch (res) { From 13e82dfcd1e1c3f4b4983b0ce6a6570f95bf810a Mon Sep 17 00:00:00 2001 From: legal-s Date: Mon, 3 Nov 2025 11:22:14 +0100 Subject: [PATCH 4/4] chore: use const insteaf of let --- .../src/explore/components/PropertiesModal/index.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/superset-frontend/src/explore/components/PropertiesModal/index.tsx b/superset-frontend/src/explore/components/PropertiesModal/index.tsx index bbf2e52038c6..57dbe91edd34 100644 --- a/superset-frontend/src/explore/components/PropertiesModal/index.tsx +++ b/superset-frontend/src/explore/components/PropertiesModal/index.tsx @@ -177,12 +177,13 @@ function PropertiesModal({ // Retrieving the code from the following issue: // https://github.com/apache/superset/pull/33392 const chartEndpoint = `/api/v1/chart/${slice.slice_id}`; - let res = await SupersetClient.put({ + await SupersetClient.put({ endpoint: chartEndpoint, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), }); - res = await SupersetClient.get({ + + const res = await SupersetClient.get({ endpoint: chartEndpoint, }); onSave(res.json.result);