You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by as...@apache.org on 2026/09/11 13:51:17 UTC
(superset) branch alexandrusoare/feat/folders-testing-phase2-v2 updated: new changes
This is an automated email from the ASF dual-hosted git repository.
alexandrusoare pushed a commit to branch alexandrusoare/feat/folders-testing-phase2-v2
in repository https://gitbox.apache.org/repos/asf/superset.git
The following commit(s) were added to refs/heads/alexandrusoare/feat/folders-testing-phase2-v2 by this push:
new 2d41804991d new changes
2d41804991d is described below
commit 2d41804991d20c8a7a2719358a27db115c169aff
Author: alexandrusoare <al...@gmail.com>
AuthorDate: Fri Sep 11 16:51:02 2026 +0300
new changes
---
superset/daos/folder.py | 89 ++++++++++++++++------
superset/daos/folder_permissions.py | 76 +++++++++++++-----
superset/folders/api.py | 17 ++++-
superset/folders/models.py | 38 ++++-----
superset/folders/utils.py | 20 ++++-
...6-05-27_16-01_4567cf3d03cc_add_folder_tables.py | 52 +++++++++----
6 files changed, 213 insertions(+), 79 deletions(-)
diff --git a/superset/daos/folder.py b/superset/daos/folder.py
index a1080c7205e..7971b3c11e7 100644
--- a/superset/daos/folder.py
+++ b/superset/daos/folder.py
@@ -33,6 +33,8 @@ from sqlalchemy.orm import joinedload
from superset.daos.base import BaseDAO
from superset.extensions import db
from superset.folders.constants import ASSET_TYPE_CONFIGS, asset_types_for_folder_type
+from superset.subjects.models import Subject
+from superset.subjects.utils import get_user_subject, get_user_subject_ids_subquery
from superset.folders.models import (
Folder,
folder_editors,
@@ -202,9 +204,10 @@ class FolderDAO(BaseDAO[Folder]):
fq = fq.where(Folder.changed_on <= modified_end)
# Hide other users' private folders from everyone (including admins)
if user_id:
+ subject_ids_sq = get_user_subject_ids_subquery(user_id)
own_private_ids = (
select(folder_editors.c.folder_id).where(
- folder_editors.c.user_id == user_id
+ folder_editors.c.subject_id.in_(subject_ids_sq)
)
)
fq = fq.where(
@@ -229,13 +232,19 @@ class FolderDAO(BaseDAO[Folder]):
else:
fq = fq.where(Folder.id.in_(member_folder_ids))
if editors:
+ editor_subject_ids = select(Subject.id).where(
+ Subject.user_id.in_(editors)
+ )
editor_folder_ids = select(folder_editors.c.folder_id).where(
- folder_editors.c.user_id.in_(editors)
+ folder_editors.c.subject_id.in_(editor_subject_ids)
)
fq = fq.where(Folder.id.in_(editor_folder_ids))
if viewers:
+ viewer_subject_ids = select(Subject.id).where(
+ Subject.user_id.in_(viewers)
+ )
viewer_folder_ids = select(folder_viewers.c.folder_id).where(
- folder_viewers.c.user_id.in_(viewers)
+ folder_viewers.c.subject_id.in_(viewer_subject_ids)
)
fq = fq.where(Folder.id.in_(viewer_folder_ids))
selects.append(fq)
@@ -336,10 +345,13 @@ class FolderDAO(BaseDAO[Folder]):
fk_col_filter = getattr(
FolderObject, ASSET_TYPE_CONFIGS[name].fk_column
)
+ editor_subject_ids_aq = select(Subject.id).where(
+ Subject.user_id.in_(editors)
+ )
editor_asset_ids = select(fk_col_filter).where(
FolderObject.folder_id.in_(
select(folder_editors.c.folder_id).where(
- folder_editors.c.user_id.in_(editors)
+ folder_editors.c.subject_id.in_(editor_subject_ids_aq)
)
),
fk_col_filter.isnot(None),
@@ -349,10 +361,13 @@ class FolderDAO(BaseDAO[Folder]):
fk_col_filter = getattr(
FolderObject, ASSET_TYPE_CONFIGS[name].fk_column
)
+ viewer_subject_ids_aq = select(Subject.id).where(
+ Subject.user_id.in_(viewers)
+ )
viewer_asset_ids = select(fk_col_filter).where(
FolderObject.folder_id.in_(
select(folder_viewers.c.folder_id).where(
- folder_viewers.c.user_id.in_(viewers)
+ folder_viewers.c.subject_id.in_(viewer_subject_ids_aq)
)
),
fk_col_filter.isnot(None),
@@ -742,11 +757,14 @@ class FolderDAO(BaseDAO[Folder]):
"""Return the user's 'Only Me' folder, creating it if it doesn't exist."""
from superset.utils import json as json_utils
+ subject = get_user_subject(user_id)
+ subject_id = subject.id if subject else None
+
folder = (
db.session.query(Folder)
.join(folder_editors, folder_editors.c.folder_id == Folder.id)
.filter(
- folder_editors.c.user_id == user_id,
+ folder_editors.c.subject_id == subject_id,
Folder.is_private.is_(True),
Folder.parent_id.is_(None),
)
@@ -768,7 +786,7 @@ class FolderDAO(BaseDAO[Folder]):
)
db.session.flush()
db.session.execute(
- folder_editors.insert().values(folder_id=new_folder.id, user_id=user_id)
+ folder_editors.insert().values(folder_id=new_folder.id, subject_id=subject_id)
)
db.session.flush()
return new_folder
@@ -824,6 +842,7 @@ class FolderDAO(BaseDAO[Folder]):
@classmethod
def get_subjects(cls, folder_id: int) -> list[dict[str, Any]]:
from superset import security_manager
+ from superset.subjects.models import Subject
subjects: list[dict[str, Any]] = []
admin_role = security_manager.find_role("Admin")
@@ -835,7 +854,22 @@ class FolderDAO(BaseDAO[Folder]):
folder_viewers.select().where(folder_viewers.c.folder_id == folder_id)
).fetchall()
- all_user_ids = [r.user_id for r in editors] + [r.user_id for r in viewers]
+ all_subject_ids = [r.subject_id for r in editors] + [
+ r.subject_id for r in viewers
+ ]
+ subjects_by_id: dict[int, Subject] = (
+ {
+ s.id: s
+ for s in db.session.query(Subject)
+ .filter(Subject.id.in_(all_subject_ids))
+ .all()
+ }
+ if all_subject_ids
+ else {}
+ )
+ all_user_ids = [
+ s.user_id for s in subjects_by_id.values() if s.user_id is not None
+ ]
users_by_id = (
{
u.id: u
@@ -851,11 +885,12 @@ class FolderDAO(BaseDAO[Folder]):
return admin_role in user.roles
for row in editors:
- user = users_by_id.get(row.user_id)
+ subj = subjects_by_id.get(row.subject_id)
+ user = users_by_id.get(subj.user_id) if subj and subj.user_id else None
is_admin = _is_admin(user)
subjects.append(
{
- "user_id": row.user_id,
+ "user_id": subj.user_id if subj else None,
"permission": "admin" if is_admin else "editor",
"email": user.email if user else None,
"is_admin": is_admin,
@@ -863,11 +898,12 @@ class FolderDAO(BaseDAO[Folder]):
)
for row in viewers:
- user = users_by_id.get(row.user_id)
+ subj = subjects_by_id.get(row.subject_id)
+ user = users_by_id.get(subj.user_id) if subj and subj.user_id else None
is_admin = _is_admin(user)
subjects.append(
{
- "user_id": row.user_id,
+ "user_id": subj.user_id if subj else None,
"permission": "admin" if is_admin else "viewer",
"email": user.email if user else None,
"is_admin": is_admin,
@@ -883,12 +919,15 @@ class FolderDAO(BaseDAO[Folder]):
if not db.session.get(User, user_id):
raise ValueError(f"User {user_id} does not exist")
+ subject = get_user_subject(user_id)
+ subject_id = subject.id if subject else None
+
existing = (
db.session.execute(
folder_editors.select().where(
and_(
folder_editors.c.folder_id == folder_id,
- folder_editors.c.user_id == user_id,
+ folder_editors.c.subject_id == subject_id,
)
)
).first()
@@ -896,7 +935,7 @@ class FolderDAO(BaseDAO[Folder]):
folder_viewers.select().where(
and_(
folder_viewers.c.folder_id == folder_id,
- folder_viewers.c.user_id == user_id,
+ folder_viewers.c.subject_id == subject_id,
)
)
).first()
@@ -906,22 +945,25 @@ class FolderDAO(BaseDAO[Folder]):
if permission == "editor":
db.session.execute(
- folder_editors.insert().values(folder_id=folder_id, user_id=user_id)
+ folder_editors.insert().values(folder_id=folder_id, subject_id=subject_id)
)
else:
db.session.execute(
- folder_viewers.insert().values(folder_id=folder_id, user_id=user_id)
+ folder_viewers.insert().values(folder_id=folder_id, subject_id=subject_id)
)
db.session.flush()
@classmethod
def update_subject(cls, folder_id: int, user_id: int, permission: str) -> None:
+ subject = get_user_subject(user_id)
+ subject_id = subject.id if subject else None
+
existing = (
db.session.execute(
folder_editors.select().where(
and_(
folder_editors.c.folder_id == folder_id,
- folder_editors.c.user_id == user_id,
+ folder_editors.c.subject_id == subject_id,
)
)
).first()
@@ -929,7 +971,7 @@ class FolderDAO(BaseDAO[Folder]):
folder_viewers.select().where(
and_(
folder_viewers.c.folder_id == folder_id,
- folder_viewers.c.user_id == user_id,
+ folder_viewers.c.subject_id == subject_id,
)
)
).first()
@@ -940,7 +982,7 @@ class FolderDAO(BaseDAO[Folder]):
folder_editors.delete().where(
and_(
folder_editors.c.folder_id == folder_id,
- folder_editors.c.user_id == user_id,
+ folder_editors.c.subject_id == subject_id,
)
)
)
@@ -948,7 +990,7 @@ class FolderDAO(BaseDAO[Folder]):
folder_viewers.delete().where(
and_(
folder_viewers.c.folder_id == folder_id,
- folder_viewers.c.user_id == user_id,
+ folder_viewers.c.subject_id == subject_id,
)
)
)
@@ -1011,11 +1053,14 @@ class FolderDAO(BaseDAO[Folder]):
@classmethod
def remove_subject(cls, folder_id: int, user_id: int) -> None:
+ subject = get_user_subject(user_id)
+ subject_id = subject.id if subject else None
+
db.session.execute(
folder_editors.delete().where(
and_(
folder_editors.c.folder_id == folder_id,
- folder_editors.c.user_id == user_id,
+ folder_editors.c.subject_id == subject_id,
)
)
)
@@ -1023,7 +1068,7 @@ class FolderDAO(BaseDAO[Folder]):
folder_viewers.delete().where(
and_(
folder_viewers.c.folder_id == folder_id,
- folder_viewers.c.user_id == user_id,
+ folder_viewers.c.subject_id == subject_id,
)
)
)
diff --git a/superset/daos/folder_permissions.py b/superset/daos/folder_permissions.py
index 1342c6fdbcc..5cac016a5b7 100644
--- a/superset/daos/folder_permissions.py
+++ b/superset/daos/folder_permissions.py
@@ -22,6 +22,8 @@ from typing import Any
from sqlalchemy import and_
from superset import db
+from superset.subjects.models import Subject
+from superset.subjects.utils import get_user_subject
from superset.utils import json as json_utils
from superset.folders.models import (
@@ -47,13 +49,17 @@ class FolderPermissionDAO:
folder_editors.select().where(folder_editors.c.folder_id == folder_id)
).fetchall()
for row in editors:
- subjects.append({"user_id": row.user_id, "permission": "editor"})
+ subj = db.session.query(Subject).get(row.subject_id)
+ if subj and subj.user_id is not None:
+ subjects.append({"user_id": subj.user_id, "permission": "editor"})
viewers = db.session.execute(
folder_viewers.select().where(folder_viewers.c.folder_id == folder_id)
).fetchall()
for row in viewers:
- subjects.append({"user_id": row.user_id, "permission": "viewer"})
+ subj = db.session.query(Subject).get(row.subject_id)
+ if subj and subj.user_id is not None:
+ subjects.append({"user_id": subj.user_id, "permission": "viewer"})
return subjects
@@ -62,25 +68,37 @@ class FolderPermissionDAO:
# ------------------------------------------------------------------ #
@staticmethod
def add_editor(folder_id: int, user_id: int) -> None:
+ subject = get_user_subject(user_id)
+ subject_id = subject.id if subject else None
+ if subject_id is None:
+ return
db.session.execute(
- folder_editors.insert().values(folder_id=folder_id, user_id=user_id)
+ folder_editors.insert().values(folder_id=folder_id, subject_id=subject_id)
)
db.session.flush()
@staticmethod
def add_viewer(folder_id: int, user_id: int) -> None:
+ subject = get_user_subject(user_id)
+ subject_id = subject.id if subject else None
+ if subject_id is None:
+ return
db.session.execute(
- folder_viewers.insert().values(folder_id=folder_id, user_id=user_id)
+ folder_viewers.insert().values(folder_id=folder_id, subject_id=subject_id)
)
db.session.flush()
@staticmethod
def remove_editor(folder_id: int, user_id: int) -> None:
+ subject = get_user_subject(user_id)
+ subject_id = subject.id if subject else None
+ if subject_id is None:
+ return
db.session.execute(
folder_editors.delete().where(
and_(
folder_editors.c.folder_id == folder_id,
- folder_editors.c.user_id == user_id,
+ folder_editors.c.subject_id == subject_id,
)
)
)
@@ -88,11 +106,15 @@ class FolderPermissionDAO:
@staticmethod
def remove_viewer(folder_id: int, user_id: int) -> None:
+ subject = get_user_subject(user_id)
+ subject_id = subject.id if subject else None
+ if subject_id is None:
+ return
db.session.execute(
folder_viewers.delete().where(
and_(
folder_viewers.c.folder_id == folder_id,
- folder_viewers.c.user_id == user_id,
+ folder_viewers.c.subject_id == subject_id,
)
)
)
@@ -158,7 +180,7 @@ class FolderPermissionDAO:
for row in parent_editors:
db.session.execute(
folder_editors.insert().values(
- folder_id=child_folder_id, user_id=row.user_id
+ folder_id=child_folder_id, subject_id=row.subject_id
)
)
@@ -170,7 +192,7 @@ class FolderPermissionDAO:
for row in parent_viewers:
db.session.execute(
folder_viewers.insert().values(
- folder_id=child_folder_id, user_id=row.user_id
+ folder_id=child_folder_id, subject_id=row.subject_id
)
)
@@ -223,17 +245,20 @@ class FolderPermissionDAO:
folder_viewers.delete().where(folder_viewers.c.folder_id == desc_id)
)
# Copy from parent
- for subject in current_subjects:
- if subject["permission"] == "editor":
+ for subj_dict in current_subjects:
+ subject = get_user_subject(subj_dict["user_id"])
+ if not subject:
+ continue
+ if subj_dict["permission"] == "editor":
db.session.execute(
folder_editors.insert().values(
- folder_id=desc_id, user_id=subject["user_id"]
+ folder_id=desc_id, subject_id=subject.id
)
)
else:
db.session.execute(
folder_viewers.insert().values(
- folder_id=desc_id, user_id=subject["user_id"]
+ folder_id=desc_id, subject_id=subject.id
)
)
@@ -255,11 +280,16 @@ class FolderPermissionDAO:
@staticmethod
def user_has_folder_access(user_id: int, folder_id: int) -> bool:
"""Check if a user has viewer or editor access to a folder."""
+ subject = get_user_subject(user_id)
+ if not subject:
+ return False
+ subject_id = subject.id
+
is_editor = db.session.execute(
folder_editors.select().where(
and_(
folder_editors.c.folder_id == folder_id,
- folder_editors.c.user_id == user_id,
+ folder_editors.c.subject_id == subject_id,
)
)
).first()
@@ -270,7 +300,7 @@ class FolderPermissionDAO:
folder_viewers.select().where(
and_(
folder_viewers.c.folder_id == folder_id,
- folder_viewers.c.user_id == user_id,
+ folder_viewers.c.subject_id == subject_id,
)
)
).first()
@@ -279,12 +309,15 @@ class FolderPermissionDAO:
@staticmethod
def user_is_folder_editor(user_id: int, folder_id: int) -> bool:
"""Check if a user is an editor of a specific folder."""
+ subject = get_user_subject(user_id)
+ if not subject:
+ return False
return (
db.session.execute(
folder_editors.select().where(
and_(
folder_editors.c.folder_id == folder_id,
- folder_editors.c.user_id == user_id,
+ folder_editors.c.subject_id == subject.id,
)
)
).first()
@@ -294,13 +327,17 @@ class FolderPermissionDAO:
@staticmethod
def user_has_any_folder_access(user_id: int) -> bool:
"""Check if user has any folder-level access (editor or viewer)."""
+ subject = get_user_subject(user_id)
+ if not subject:
+ return False
+ subject_id = subject.id
has_access = (
db.session.query(folder_editors.c.id)
- .filter(folder_editors.c.user_id == user_id)
+ .filter(folder_editors.c.subject_id == subject_id)
.first()
) or (
db.session.query(folder_viewers.c.id)
- .filter(folder_viewers.c.user_id == user_id)
+ .filter(folder_viewers.c.subject_id == subject_id)
.first()
)
return has_access is not None
@@ -308,9 +345,12 @@ class FolderPermissionDAO:
@staticmethod
def user_has_any_folder_editor_access(user_id: int) -> bool:
"""Check if user is an editor on any folder."""
+ subject = get_user_subject(user_id)
+ if not subject:
+ return False
return (
db.session.query(folder_editors.c.id)
- .filter(folder_editors.c.user_id == user_id)
+ .filter(folder_editors.c.subject_id == subject.id)
.first()
) is not None
diff --git a/superset/folders/api.py b/superset/folders/api.py
index 803b867c1aa..bd25e3e8fdf 100644
--- a/superset/folders/api.py
+++ b/superset/folders/api.py
@@ -71,6 +71,7 @@ from superset.folders.schemas import (
)
from superset.folders.utils import can_manage_folders
from superset.utils import json as json_utils
+from superset.subjects.utils import get_user_subject_ids_subquery
from superset.utils.core import get_user_id
from superset.utils.decorators import transaction
from superset.views.base_api import BaseSupersetApi, requires_json, statsd_metrics
@@ -161,6 +162,14 @@ def serialize_folder(
"changed_on_humanized": folder.changed_on_humanized,
"created_by": _serialize_user(folder.created_by),
"changed_by": _serialize_user(folder.changed_by),
+ "editors": [
+ {"id": s.id, "label": s.label, "type": s.type, "user_id": s.user_id}
+ for s in (folder.editors or [])
+ ],
+ "viewers": [
+ {"id": s.id, "label": s.label, "type": s.type, "user_id": s.user_id}
+ for s in (folder.viewers or [])
+ ],
"user_permission": _get_user_permission(folder, implicit_folder_ids),
"inherits_permissions": _get_inherits_permissions(folder),
"is_only_me": _is_only_me(folder),
@@ -446,12 +455,13 @@ class FolderRestApi(BaseSupersetApi):
if user_id:
# Batch query instead of N+1 per-folder checks
all_folder_ids = [f.id for f in folders]
+ subject_ids_sq = get_user_subject_ids_subquery(user_id)
editor_ids = (
{
r[0]
for r in db.session.query(folder_editors.c.folder_id)
.filter(
- folder_editors.c.user_id == user_id,
+ folder_editors.c.subject_id.in_(subject_ids_sq),
folder_editors.c.folder_id.in_(all_folder_ids),
)
.all()
@@ -464,7 +474,7 @@ class FolderRestApi(BaseSupersetApi):
r[0]
for r in db.session.query(folder_viewers.c.folder_id)
.filter(
- folder_viewers.c.user_id == user_id,
+ folder_viewers.c.subject_id.in_(subject_ids_sq),
folder_viewers.c.folder_id.in_(all_folder_ids),
)
.all()
@@ -485,10 +495,11 @@ class FolderRestApi(BaseSupersetApi):
folders = []
# Hide other users' private folders (even for admins)
if user_id:
+ own_subject_ids_sq = get_user_subject_ids_subquery(user_id)
own_editor_ids = {
r[0]
for r in db.session.query(folder_editors.c.folder_id)
- .filter(folder_editors.c.user_id == user_id)
+ .filter(folder_editors.c.subject_id.in_(own_subject_ids_sq))
.all()
}
folders = [
diff --git a/superset/folders/models.py b/superset/folders/models.py
index 0c886c929c4..a8182479a51 100644
--- a/superset/folders/models.py
+++ b/superset/folders/models.py
@@ -38,12 +38,13 @@ from sqlalchemy.orm import relationship
from sqlalchemy_utils import UUIDType
from flask_appbuilder import Model
-from superset.models.helpers import AuditMixinNullable
+from superset.models.helpers import AuditMixinNullable, SoftDeleteMixin
+from superset.subjects.models import Subject
metadata = Model.metadata # pylint: disable=no-member
-# Junction table: folder editors (users who can manage folder contents).
+# Junction table: folder editors (subjects who can manage folder contents).
folder_editors = Table(
"folder_editors",
metadata,
@@ -55,16 +56,16 @@ folder_editors = Table(
nullable=False,
),
Column(
- "user_id",
+ "subject_id",
Integer,
- ForeignKey("ab_user.id", ondelete="CASCADE"),
+ ForeignKey("subjects.id", ondelete="CASCADE"),
nullable=False,
),
- UniqueConstraint("folder_id", "user_id"),
- Index("ix_folder_editors_user_id", "user_id"),
+ UniqueConstraint("folder_id", "subject_id"),
+ Index("ix_folder_editors_subject_id", "subject_id"),
)
-# Junction table: folder viewers (users who can see folder and its assets).
+# Junction table: folder viewers (subjects who can see folder and its assets).
folder_viewers = Table(
"folder_viewers",
metadata,
@@ -76,21 +77,23 @@ folder_viewers = Table(
nullable=False,
),
Column(
- "user_id",
+ "subject_id",
Integer,
- ForeignKey("ab_user.id", ondelete="CASCADE"),
+ ForeignKey("subjects.id", ondelete="CASCADE"),
nullable=False,
),
- UniqueConstraint("folder_id", "user_id"),
- Index("ix_folder_viewers_user_id", "user_id"),
+ UniqueConstraint("folder_id", "subject_id"),
+ Index("ix_folder_viewers_subject_id", "subject_id"),
)
-class Folder(AuditMixinNullable, Model):
+class Folder(AuditMixinNullable, SoftDeleteMixin, Model):
"""A folder for organizing dashboards, charts, and datasets."""
__tablename__ = "folders"
- __table_args__ = (UniqueConstraint("parent_id", "name", "folder_type"),)
+ # Name uniqueness enforced by partial index ix_folders_active_parent_name_type
+ # (WHERE deleted_at IS NULL) created in the migration.
+ __table_args__: tuple = ()
id = Column(Integer, primary_key=True)
uuid = Column(
@@ -101,6 +104,7 @@ class Folder(AuditMixinNullable, Model):
parent_id = Column(Integer, ForeignKey("folders.id"), nullable=True)
folder_type = Column(String(50), nullable=False, default="analytics")
is_private = Column(Boolean, nullable=False, default=False)
+ is_only_me = Column(Boolean, nullable=False, default=False)
extra = Column(Text, nullable=True)
# Relationships
@@ -119,17 +123,17 @@ class Folder(AuditMixinNullable, Model):
cascade="all, delete-orphan",
)
editors = relationship(
- User,
+ Subject,
secondary=folder_editors,
primaryjoin=lambda: Folder.id == folder_editors.c.folder_id,
- secondaryjoin=lambda: folder_editors.c.user_id == User.id,
+ secondaryjoin=lambda: folder_editors.c.subject_id == Subject.id,
viewonly=True,
)
viewers = relationship(
- User,
+ Subject,
secondary=folder_viewers,
primaryjoin=lambda: Folder.id == folder_viewers.c.folder_id,
- secondaryjoin=lambda: folder_viewers.c.user_id == User.id,
+ secondaryjoin=lambda: folder_viewers.c.subject_id == Subject.id,
viewonly=True,
)
diff --git a/superset/folders/utils.py b/superset/folders/utils.py
index ea92f50d499..b1a21522d65 100644
--- a/superset/folders/utils.py
+++ b/superset/folders/utils.py
@@ -6,6 +6,8 @@ from typing import Any
from sqlalchemy import select
+from superset.subjects.utils import get_user_subject_ids_subquery
+
FOLDER_MANAGEMENT_ROLES = {"Admin", "Alpha", "Gamma"}
@@ -25,13 +27,22 @@ def get_folder_editor_users(folder_id: int) -> list[Any]:
from superset import db
from superset.folders.models import folder_editors
+ from superset.subjects.models import Subject
- editor_user_ids = [
- row.user_id
+ editor_subject_ids = [
+ row.subject_id
for row in db.session.execute(
folder_editors.select().where(folder_editors.c.folder_id == folder_id)
).fetchall()
]
+ if not editor_subject_ids:
+ return []
+ editor_user_ids = [
+ s.user_id
+ for s in db.session.query(Subject)
+ .filter(Subject.id.in_(editor_subject_ids), Subject.user_id.isnot(None))
+ .all()
+ ]
if not editor_user_ids:
return []
return db.session.query(User).filter(User.id.in_(editor_user_ids)).all()
@@ -41,12 +52,13 @@ def user_accessible_folder_ids(user_id: int) -> Any:
"""Subquery of folder IDs the user has access to (editor or viewer)."""
from superset.folders.models import folder_editors, folder_viewers
+ subject_ids = get_user_subject_ids_subquery(user_id)
return (
select(folder_viewers.c.folder_id)
- .where(folder_viewers.c.user_id == user_id)
+ .where(folder_viewers.c.subject_id.in_(subject_ids))
.union(
select(folder_editors.c.folder_id).where(
- folder_editors.c.user_id == user_id
+ folder_editors.c.subject_id.in_(subject_ids)
)
)
)
diff --git a/superset/migrations/versions/2026-05-27_16-01_4567cf3d03cc_add_folder_tables.py b/superset/migrations/versions/2026-05-27_16-01_4567cf3d03cc_add_folder_tables.py
index 7d4ac9da41c..70a633f629d 100644
--- a/superset/migrations/versions/2026-05-27_16-01_4567cf3d03cc_add_folder_tables.py
+++ b/superset/migrations/versions/2026-05-27_16-01_4567cf3d03cc_add_folder_tables.py
@@ -22,6 +22,7 @@ Create Date: 2026-05-27 16:01:44.222914
"""
+from alembic import op
from sqlalchemy import (
Boolean,
CheckConstraint,
@@ -65,18 +66,23 @@ def upgrade():
Column("parent_id", Integer, nullable=True),
Column("folder_type", String(50), nullable=False, server_default="analytics"),
Column("is_private", Boolean, nullable=False, server_default="0"),
+ Column("is_only_me", Boolean, nullable=False, server_default="0"),
Column("extra", Text, nullable=True),
+ Column("deleted_at", DateTime, nullable=True),
Column("created_on", DateTime, nullable=True),
Column("changed_on", DateTime, nullable=True),
Column("created_by_fk", Integer, nullable=True),
Column("changed_by_fk", Integer, nullable=True),
- UniqueConstraint(
- "parent_id", "name", "folder_type", name="uq_folder_parent_name_type"
- ),
)
create_index(FOLDERS_TABLE, "idx_folders_uuid", ["uuid"], unique=True)
create_index(FOLDERS_TABLE, "idx_folders_parent_id", ["parent_id"])
create_index(FOLDERS_TABLE, "idx_folders_folder_type", ["folder_type"])
+ create_index(FOLDERS_TABLE, "ix_folders_deleted_at", ["deleted_at"])
+ op.execute(
+ "CREATE UNIQUE INDEX ix_folders_active_parent_name_type "
+ f"ON {FOLDERS_TABLE} (parent_id, name, folder_type) "
+ "WHERE deleted_at IS NULL"
+ )
create_fks_for_table(
foreign_key_name="fk_folders_parent_id_folders",
table_name=FOLDERS_TABLE,
@@ -167,9 +173,9 @@ def upgrade():
FOLDER_EDITORS_TABLE,
Column("id", Integer, primary_key=True),
Column("folder_id", Integer, nullable=False),
- Column("user_id", Integer, nullable=False),
+ Column("subject_id", Integer, nullable=False),
UniqueConstraint(
- "folder_id", "user_id", name="uq_folder_editors_folder_user"
+ "folder_id", "subject_id", name="uq_folder_editors_folder_subject"
),
)
create_fks_for_table(
@@ -181,22 +187,25 @@ def upgrade():
ondelete="CASCADE",
)
create_fks_for_table(
- foreign_key_name="fk_folder_editors_user_id_ab_user",
+ foreign_key_name="fk_folder_editors_subject_id_subjects",
table_name=FOLDER_EDITORS_TABLE,
- referenced_table="ab_user",
- local_cols=["user_id"],
+ referenced_table="subjects",
+ local_cols=["subject_id"],
remote_cols=["id"],
ondelete="CASCADE",
)
+ create_index(
+ FOLDER_EDITORS_TABLE, "ix_folder_editors_subject_id", ["subject_id"]
+ )
# --- folder_viewers ---
create_table(
FOLDER_VIEWERS_TABLE,
Column("id", Integer, primary_key=True),
Column("folder_id", Integer, nullable=False),
- Column("user_id", Integer, nullable=False),
+ Column("subject_id", Integer, nullable=False),
UniqueConstraint(
- "folder_id", "user_id", name="uq_folder_viewers_folder_user"
+ "folder_id", "subject_id", name="uq_folder_viewers_folder_subject"
),
)
create_fks_for_table(
@@ -208,13 +217,16 @@ def upgrade():
ondelete="CASCADE",
)
create_fks_for_table(
- foreign_key_name="fk_folder_viewers_user_id_ab_user",
+ foreign_key_name="fk_folder_viewers_subject_id_subjects",
table_name=FOLDER_VIEWERS_TABLE,
- referenced_table="ab_user",
- local_cols=["user_id"],
+ referenced_table="subjects",
+ local_cols=["subject_id"],
remote_cols=["id"],
ondelete="CASCADE",
)
+ create_index(
+ FOLDER_VIEWERS_TABLE, "ix_folder_viewers_subject_id", ["subject_id"]
+ )
# --- folder_pins ---
create_table(
@@ -292,15 +304,23 @@ def downgrade():
)
drop_table(FOLDER_PINS_TABLE)
+ drop_index(FOLDER_VIEWERS_TABLE, "ix_folder_viewers_subject_id")
drop_fks_for_table(
FOLDER_VIEWERS_TABLE,
- ["fk_folder_viewers_folder_id_folders", "fk_folder_viewers_user_id_ab_user"],
+ [
+ "fk_folder_viewers_folder_id_folders",
+ "fk_folder_viewers_subject_id_subjects",
+ ],
)
drop_table(FOLDER_VIEWERS_TABLE)
+ drop_index(FOLDER_EDITORS_TABLE, "ix_folder_editors_subject_id")
drop_fks_for_table(
FOLDER_EDITORS_TABLE,
- ["fk_folder_editors_folder_id_folders", "fk_folder_editors_user_id_ab_user"],
+ [
+ "fk_folder_editors_folder_id_folders",
+ "fk_folder_editors_subject_id_subjects",
+ ],
)
drop_table(FOLDER_EDITORS_TABLE)
@@ -318,6 +338,8 @@ def downgrade():
)
drop_table(FOLDER_OBJECTS_TABLE)
+ op.execute("DROP INDEX IF EXISTS ix_folders_active_parent_name_type")
+ drop_index(FOLDERS_TABLE, "ix_folders_deleted_at")
drop_index(FOLDERS_TABLE, "idx_folders_uuid")
drop_index(FOLDERS_TABLE, "idx_folders_parent_id")
drop_index(FOLDERS_TABLE, "idx_folders_folder_type")