sfirke commented on code in PR #45591:
URL: https://github.com/apache/superset/pull/45591#discussion_r4352702826
##########
superset-frontend/src/features/groups/utils.ts:
##########
@@ -19,9 +19,54 @@
import { t } from '@apache-superset/core/translation';
import { SupersetClient } from '@superset-ui/core';
import rison from 'rison';
+import {
+ SUBJECT_DETAIL_PROP,
+ SUBJECT_OPTION_FILTER_PROPS,
+ SUBJECT_TEXT_LABEL_PROP,
+ SubjectSelectLabel,
+} from 'src/features/subjects/SubjectSelectLabel';
import { getUserDisplayLabel } from 'src/features/users/utils';
import { FormValues } from './types';
+type UserRecord = { id: number; email?: string } & Parameters<
+ typeof getUserDisplayLabel
+>[0];
+
+/**
+ * Option props AsyncSelect's client-side re-filter should match against for
+ * user options: the display name, the email and the username. These mirror
+ * the columns the server-side `user_all_text` filter searches, so a user the
+ * server matched is never hidden by the picker.
+ */
+export const USER_OPTION_FILTER_PROPS = [
+ ...SUBJECT_OPTION_FILTER_PROPS,
+ 'username',
+];
+
+/**
+ * Build a select option for a user picker.
+ *
+ * The option shows the email under the display name, like the subject-based
+ * owner pickers do, so that a server-side match on email is visible to the
+ * person searching. Plain-text copies of both lines, plus the username, are
+ * attached under the `USER_OPTION_FILTER_PROPS` keys so AsyncSelect's
+ * client-side re-filter can match them (a React label is otherwise filtered
+ * as an empty string), and the display name under `title` so ListView can
+ * serialize the chosen option's label into the URL. The username is not
+ * rendered, so a username match surfaces the user without explaining why.
+ */
+export const toUserSelectOption = (user: UserRecord) => {
+ const textLabel = getUserDisplayLabel(user);
+ return {
+ value: user.id,
+ label: SubjectSelectLabel({ label: textLabel, secondaryLabel: user.email
}),
Review Comment:
Good catch on both, thanks. Fixed in 9e33a4d258:
- **Paste:** `AsyncSelect`'s paste lookup now falls back to the option's
`title` when its `label` isn't a string, for both cached and freshly fetched
options. User options already carry the plain-text name in `title`, so pasting
`Jane Smith,Bob Jones` selects both users again. Options with string labels
behave exactly as before. Added an `AsyncSelect` test that pastes against
React-labelled options.
- **Sort:** both user pickers now pass a `userOptionSortComparator` that
runs the default comparator on `title`, so options are ranked by display name
(and by search relevance while typing) rather than by user id.
As a side benefit, paste now works better than it did before this PR. The
fetch fallback used to ignore the search term and only see the first page of
users. It now searches the server by full name, so pasting the name of a user
who isn't loaded yet also works.
---
🤖 _Drafted by Claude Code, reviewed and approved by @sfirke._
##########
tests/unit_tests/security/user_search_filter_test.py:
##########
@@ -0,0 +1,154 @@
+# 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 collections.abc import Iterator
+
+import pytest
+from flask_appbuilder.models.sqla.interface import SQLAInterface
+from flask_appbuilder.security.sqla.models import User
+from sqlalchemy import create_engine
+from sqlalchemy.orm import Session, sessionmaker
+
+from superset.security.manager import SupersetUserApi, UserSearchFilter
+
+
[email protected]
+def session() -> Iterator[Session]:
+ engine = create_engine("sqlite://")
+ User.metadata.create_all(engine, tables=[User.__table__])
+ session = sessionmaker(bind=engine)()
+ session.add_all(
+ [
+ User(
+ username="jsmith",
+ first_name="Jane",
+ last_name="Smith",
+ email="[email protected]",
+ active=True,
+ ),
+ User(
+ username="00f09b2e-f4fb-48a3-80fc-53ccd9528021",
+ first_name="Bob",
+ last_name="Jones",
+ email="[email protected]",
+ active=True,
+ ),
+ User(
+ username="svc_reporting",
+ first_name="Service",
+ last_name="Account",
+ email="[email protected]",
+ active=True,
+ ),
+ ]
+ )
+ session.commit()
+ yield session
+ session.close()
+
+
+def _search(session: Session, value: str) -> list[str]:
+ filter_ = UserSearchFilter("username", SQLAInterface(User))
+ query = filter_.apply(session.query(User), value)
+ return sorted(user.username for user in query.all())
+
+
[email protected](
+ "value, expected",
+ [
+ ("jsmi", ["jsmith"]),
+ ("JANE", ["jsmith"]),
+ ("jones", ["00f09b2e-f4fb-48a3-80fc-53ccd9528021"]),
+ ("Bob Jo", ["00f09b2e-f4fb-48a3-80fc-53ccd9528021"]),
+ ("corp.example", ["00f09b2e-f4fb-48a3-80fc-53ccd9528021"]),
+ ("00f09b2e", ["00f09b2e-f4fb-48a3-80fc-53ccd9528021"]),
+ (" jane ", ["jsmith"]),
+ ("nobody", []),
+ ],
+)
+def test_user_search_filter_matches(
+ session: Session, value: str, expected: list[str]
+) -> None:
+ """
+ UserSearchFilter matches username, first/last name, full name and email.
+ """
+ assert _search(session, value) == expected
+
+
[email protected](
+ "value, expected",
+ [
+ ("svc_reporting", ["svc_reporting"]),
+ ("svc_", ["svc_reporting"]),
+ ("_", ["svc_reporting"]),
+ ("%", []),
+ ("\\", []),
Review Comment:
Agreed, those cases only proved "no false positives". Fixed in 9e33a4d258:
- The fixture now includes users with a literal `%` (`pct%user`) and `\`
(`back\slash`) in the username. `%`, `pct%`, `\` and `back\s` must each return
exactly that user, so dropping the backslash doubling in `_escape_like` or the
`%` escaping now fails the suite. I checked both by mutating the code locally.
- Added `test_user_search_filter_is_case_insensitive_on_postgres`, which
compiles the filter against the PostgreSQL dialect and asserts all five columns
use `ILIKE` with no plain `LIKE`. Swapping `.ilike()` for `.like()` now fails
it.
---
🤖 _Drafted by Claude Code, reviewed and approved by @sfirke._
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]