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")