You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by el...@apache.org on 2026/10/07 20:57:45 UTC
(superset) 01/01: fix(subjects): truncate synced values to Subject column widths
This is an automated email from the ASF dual-hosted git repository.
eschutho pushed a commit to branch subject-sync-secondary-label-truncate
in repository https://gitbox.apache.org/repos/asf/superset.git
commit 05722f9e8ce09b50647f15b2e180368c6caf344f
Author: Elizabeth Thompson <es...@gmail.com>
AuthorDate: Wed Oct 7 20:57:30 2026 +0000
fix(subjects): truncate synced values to Subject column widths
Flask-AppBuilder allows user emails up to 320 characters and group
descriptions up to 512, but Subject.secondary_label and
Subject.extra_search are String(255). Syncing a user or group with a
longer value failed with a "value too long" error on strict databases.
Clip synced values to the Subject column lengths before writing.
Co-Authored-By: Claude Opus 5.5 <no...@anthropic.com>
---
superset/subjects/sync.py | 45 ++++++++++--
tests/unit_tests/subjects/test_sync.py | 124 +++++++++++++++++++++++++++++++++
2 files changed, 162 insertions(+), 7 deletions(-)
diff --git a/superset/subjects/sync.py b/superset/subjects/sync.py
index 558a2ff3573..0cb9c11f52e 100644
--- a/superset/subjects/sync.py
+++ b/superset/subjects/sync.py
@@ -21,29 +21,56 @@ from __future__ import annotations
import logging
from typing import Any
+from sqlalchemy.orm import InstrumentedAttribute
+
from superset.daos.subject import SubjectDAO
+from superset.subjects.models import Subject
from superset.subjects.types import SubjectType
from superset.subjects.utils import get_user_label
logger = logging.getLogger(__name__)
+def _fit(value: str | None, column: InstrumentedAttribute[Any]) -> str | None:
+ """Truncate ``value`` to the length of a ``Subject`` string column.
+
+ Source fields can be wider than the Subject columns (e.g. FAB allows
+ 320-char emails and 512-char group descriptions), so values are clipped
+ to avoid "value too long" errors on strict databases.
+ """
+ max_length = getattr(column.type, "length", None)
+ if value is None or max_length is None or len(value) <= max_length:
+ return value
+ return value[:max_length]
+
+
+def _user_subject_fields(user: Any) -> tuple[str, str | None, str | None]:
+ """Compute label, secondary_label, and extra_search for a user Subject."""
+ email = getattr(user, "email", None)
+ return (
+ _fit(get_user_label(user), Subject.label) or "",
+ _fit(email, Subject.secondary_label),
+ _fit(email, Subject.extra_search),
+ )
+
+
def sync_user_subject(user: Any) -> None:
"""Create or update the Subject row for a User."""
if not user or not getattr(user, "id", None):
return
subject = SubjectDAO.find_one_or_none(user_id=user.id)
+ label, secondary_label, extra_search = _user_subject_fields(user)
if subject:
- subject.label = get_user_label(user)
- subject.secondary_label = getattr(user, "email", None)
- subject.extra_search = getattr(user, "email", None)
+ subject.label = label
+ subject.secondary_label = secondary_label
+ subject.extra_search = extra_search
subject.active = getattr(user, "active", True)
else:
SubjectDAO.create(
attributes={
- "label": get_user_label(user),
- "secondary_label": getattr(user, "email", None),
- "extra_search": getattr(user, "email", None),
+ "label": label,
+ "secondary_label": secondary_label,
+ "extra_search": extra_search,
"active": getattr(user, "active", True),
"type": SubjectType.USER,
"user_id": user.id,
@@ -95,7 +122,11 @@ def _group_subject_fields(group: Any) -> tuple[str, str | None, str | None]:
secondary_label = description if description else None
extra_search = name if group_label and group_label != name else None
- return label, secondary_label, extra_search
+ return (
+ _fit(label, Subject.label) or "",
+ _fit(secondary_label, Subject.secondary_label),
+ _fit(extra_search, Subject.extra_search),
+ )
def sync_group_subject(group: Any) -> None:
diff --git a/tests/unit_tests/subjects/test_sync.py b/tests/unit_tests/subjects/test_sync.py
new file mode 100644
index 00000000000..c388f6dc1f3
--- /dev/null
+++ b/tests/unit_tests/subjects/test_sync.py
@@ -0,0 +1,124 @@
+# 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.
+"""Tests for Subject sync helpers."""
+
+from sqlalchemy.orm.session import Session
+
+from superset.subjects.models import Subject
+from superset.subjects.types import SubjectType
+
+SECONDARY_LABEL_LENGTH = Subject.secondary_label.type.length
+EXTRA_SEARCH_LENGTH = Subject.extra_search.type.length
+
+
+def _setup(session: Session) -> None:
+ from flask_appbuilder.security.sqla.models import Group, User
+
+ engine = session.get_bind()
+ for model in (User, Group, Subject):
+ model.metadata.create_all(engine) # pylint: disable=no-member
+
+
+def test_sync_user_subject_truncates_long_email(session: Session) -> None:
+ """Emails longer than the Subject columns are truncated, not rejected."""
+ from flask_appbuilder.security.sqla.models import User
+
+ from superset.subjects.sync import sync_user_subject
+
+ _setup(session)
+ email = f"{'a' * 300}@example.com"
+ assert len(email) > SECONDARY_LABEL_LENGTH
+ user = User(
+ first_name="Long",
+ last_name="Email",
+ username="long_email",
+ email=email,
+ )
+ session.add(user)
+ session.flush()
+
+ sync_user_subject(user)
+ session.flush()
+
+ subject = session.query(Subject).filter_by(user_id=user.id).one()
+ assert subject.type == SubjectType.USER
+ assert subject.label == "Long Email"
+ assert subject.secondary_label == email[:SECONDARY_LABEL_LENGTH]
+ assert subject.extra_search == email[:EXTRA_SEARCH_LENGTH]
+
+ # Updating an existing Subject truncates as well.
+ user.email = f"{'b' * 300}@example.com"
+ sync_user_subject(user)
+ session.flush()
+
+ session.refresh(subject)
+ assert subject.secondary_label == user.email[:SECONDARY_LABEL_LENGTH]
+ assert subject.extra_search == user.email[:EXTRA_SEARCH_LENGTH]
+
+
+def test_sync_user_subject_keeps_short_email(session: Session) -> None:
+ """Values that fit the column are stored unchanged."""
+ from flask_appbuilder.security.sqla.models import User
+
+ from superset.subjects.sync import sync_user_subject
+
+ _setup(session)
+ user = User(
+ first_name="Short",
+ last_name="Email",
+ username="short_email",
+ email="short@example.com",
+ )
+ session.add(user)
+ session.flush()
+
+ sync_user_subject(user)
+ session.flush()
+
+ subject = session.query(Subject).filter_by(user_id=user.id).one()
+ assert subject.secondary_label == "short@example.com"
+ assert subject.extra_search == "short@example.com"
+
+
+def test_sync_group_subject_truncates_long_description(session: Session) -> None:
+ """Group descriptions longer than the Subject column are truncated."""
+ from flask_appbuilder.security.sqla.models import Group
+
+ from superset.subjects.sync import sync_group_subject
+
+ _setup(session)
+ description = "d" * 512
+ group = Group(name="long_desc", label="Long Description", description=description)
+ session.add(group)
+ session.flush()
+
+ sync_group_subject(group)
+ session.flush()
+
+ subject = session.query(Subject).filter_by(group_id=group.id).one()
+ assert subject.type == SubjectType.GROUP
+ assert subject.label == "Long Description"
+ assert subject.secondary_label == description[:SECONDARY_LABEL_LENGTH]
+ assert subject.extra_search == "long_desc"
+
+ # Updating an existing Subject truncates as well.
+ group.description = "e" * 400
+ sync_group_subject(group)
+ session.flush()
+
+ session.refresh(subject)
+ assert subject.secondary_label == "e" * SECONDARY_LABEL_LENGTH