You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by ru...@apache.org on 2026/08/03 19:21:00 UTC
(superset) branch master updated: fix(dashboard): count extra_editors when deciding who may edit a dashboard (#42708)
This is an automated email from the ASF dual-hosted git repository.
rusackas pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git
The following commit(s) were added to refs/heads/master by this push:
new da9f4216e19 fix(dashboard): count extra_editors when deciding who may edit a dashboard (#42708)
da9f4216e19 is described below
commit da9f4216e19834ff1e247eba72c8b73ea4826481
Author: Mike Bridge <mi...@bridgecanada.com>
AuthorDate: Mon Aug 3 20:20:52 2026 +0100
fix(dashboard): count extra_editors when deciding who may edit a dashboard (#42708)
Co-authored-by: Claude Opus 5 (1M context) <no...@anthropic.com>
---
.../src/dashboard/util/permissionUtils.test.ts | 38 ++++++++++++++++++++++
.../src/dashboard/util/permissionUtils.ts | 34 +++++++++++++++++--
superset-frontend/src/types/Dashboard.ts | 4 +++
3 files changed, 73 insertions(+), 3 deletions(-)
diff --git a/superset-frontend/src/dashboard/util/permissionUtils.test.ts b/superset-frontend/src/dashboard/util/permissionUtils.test.ts
index fa548403188..2798472604a 100644
--- a/superset-frontend/src/dashboard/util/permissionUtils.test.ts
+++ b/superset-frontend/src/dashboard/util/permissionUtils.test.ts
@@ -132,6 +132,44 @@ test('isUserDashboardEditor returns false when editors is empty', () => {
expect(isUserDashboardEditor(dashNoEditors)).toEqual(false);
});
+test('isUserDashboardEditor counts editorship granted through extra_editors', () => {
+ // The server's is_editor unions EXTRA_EDITORS_RESOLVER output into the
+ // editor set, and the API attaches it after the response schema is dumped
+ // (so it survives the columns projection). extra_editors arrives as bare
+ // ids, not Subjects.
+ const dashExtraEditor = { ...dashboard, editors: [], extra_editors: [10] };
+ expect(isUserDashboardEditor(dashExtraEditor)).toEqual(true);
+ expect(canUserEditDashboard(dashExtraEditor, editorUser)).toEqual(true);
+});
+
+test('isUserDashboardEditor unions the two lists rather than preferring one', () => {
+ // Pins the union semantics: a refactor that consulted extra_editors only
+ // when editors is empty would pass the empty-editors cases above but
+ // fail here — non-matching editors must not mask a matching extra grant.
+ const dashUnion = {
+ ...dashboard,
+ editors: [{ id: 999, label: 'Other', type: 1 }],
+ extra_editors: [10],
+ };
+ expect(isUserDashboardEditor(dashUnion)).toEqual(true);
+});
+
+test('an empty extra_editors grants nothing on its own', () => {
+ // The resolver-configured-but-returns-nothing shape.
+ const dashEmptyExtra = { ...dashboard, editors: [], extra_editors: [] };
+ expect(isUserDashboardEditor(dashEmptyExtra)).toEqual(false);
+});
+
+test('isUserDashboardEditor ignores extra_editors for other subjects', () => {
+ const dashOtherExtraEditor = {
+ ...dashboard,
+ editors: [],
+ extra_editors: [999],
+ };
+ expect(isUserDashboardEditor(dashOtherExtraEditor)).toEqual(false);
+ expect(canUserEditDashboard(dashOtherExtraEditor, editorUser)).toEqual(false);
+});
+
test('canUserEditDashboard allows editors', () => {
expect(canUserEditDashboard(dashboard, editorUser)).toEqual(true);
});
diff --git a/superset-frontend/src/dashboard/util/permissionUtils.ts b/superset-frontend/src/dashboard/util/permissionUtils.ts
index 3c74b901f40..be9b8ea4999 100644
--- a/superset-frontend/src/dashboard/util/permissionUtils.ts
+++ b/superset-frontend/src/dashboard/util/permissionUtils.ts
@@ -33,11 +33,32 @@ const ADMIN_ROLE_NAME = 'admin';
const getUserSubjects = (): number[] =>
getBootstrapData()?.common?.user_subjects ?? [];
-const isUserInEditors = (editors: Subject[] = []): boolean => {
+/**
+ * Editor lists arrive either as full Subjects (`editors`) or as bare subject
+ * ids (`extra_editors`, which the API dumps straight from
+ * `get_extra_editor_subject_ids`).
+ */
+export type SubjectRef = number | { id: number };
+
+const subjectId = (subject: SubjectRef): number =>
+ typeof subject === 'number' ? subject : subject.id;
+
+/**
+ * Whether the viewer is any of the given subjects — their user, roles or
+ * groups — across every supplied list.
+ */
+export const isUserInSubjects = (
+ ...subjectLists: (SubjectRef[] | null | undefined)[]
+): boolean => {
const userSubjects = getUserSubjects();
- return editors.some(editor => userSubjects.includes(editor.id));
+ return subjectLists.some(subjects =>
+ (subjects ?? []).some(subject => userSubjects.includes(subjectId(subject))),
+ );
};
+const isUserInEditors = (editors: Subject[] = []): boolean =>
+ isUserInSubjects(editors);
+
export const isUserAdmin = (
user?: UserWithPermissionsAndRoles | UndefinedUser,
) =>
@@ -51,8 +72,15 @@ export const isUserEditorOrAdmin = (
editors: Subject[] = [],
): boolean => isUserInEditors(editors) || isUserAdmin(user);
+/**
+ * Editorship of *dashboard*, matching the server's `is_editor`: the explicit
+ * editor list unioned with any editorship granted indirectly by a
+ * deployment's EXTRA_EDITORS_RESOLVER. Must stay in step with the server's
+ * `is_editor` (security/manager.py): a predicate narrower than the server's
+ * hides actions the API permits.
+ */
export const isUserDashboardEditor = (dashboard: Dashboard): boolean =>
- isUserInEditors(dashboard.editors);
+ isUserInSubjects(dashboard.editors, dashboard.extra_editors);
export const canUserEditDashboard = (
dashboard: Dashboard,
diff --git a/superset-frontend/src/types/Dashboard.ts b/superset-frontend/src/types/Dashboard.ts
index 5af71ef63ef..d4d9cea1121 100644
--- a/superset-frontend/src/types/Dashboard.ts
+++ b/superset-frontend/src/types/Dashboard.ts
@@ -34,6 +34,10 @@ export interface Dashboard {
changed_on: string;
charts: string[]; // just chart names, unfortunately...
editors?: Subject[];
+ // Subject ids resolved by a deployment's EXTRA_EDITORS_RESOLVER; the API
+ // attaches them after the response schema is dumped, so they survive the
+ // `columns` projection. Bare ids, not Subjects.
+ extra_editors?: number[];
viewers?: Subject[];
theme?: {
id: number;