sadpandajoe commented on code in PR #45591:
URL: https://github.com/apache/superset/pull/45591#discussion_r4352219851


##########
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:
   These cases don't pin the behavior they describe. The fixture has no 
username/email containing `%` or `\`, so `("%", [])` and `("\\", [])` also pass 
if escaping is wrong in a way that suppresses real matches (e.g. dropping the 
backslash doubling in `_escape_like`), and nothing asserts that a user whose 
username literally contains `%` or `\` is found. Separately, everything runs on 
SQLite where `LIKE` is already case-insensitive, so swapping `.ilike()` for 
`.like()` keeps `("JANE", ["jsmith"])` green while case-insensitive search 
would break on PostgreSQL. Could you add users with literal `%`/`\` in the 
username asserting they are returned, and a Postgres-dialect compile assertion 
that the filter emits `ILIKE`?



##########
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:
   Because `label` is now a React element, `AsyncSelect` paths that compare 
against a string `label` stop working for these pickers: `getPastedTextValue` 
matches pasted text with `item.label === text` (cached and fetched options), so 
pasting `Jane Smith,Bob Jones` into the Roles/Groups Users field no longer 
selects anyone, whereas the old string labels did. The default sort comparator 
also only ranks string labels, so with JSX labels it falls back to `a.value - 
b.value` and options are ordered by user id instead of by name. 
`title`/`optionFilterProps` aren't consulted by either path. Should paste 
matching and the comparator for these pickers use the plain-text name (e.g. a 
`sortComparator` plus paste lookup on `title`)?



-- 
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]

Reply via email to