This is an automated email from the ASF dual-hosted git repository.

imbajin pushed a commit to branch feat/oink-community-content
in repository https://gitbox.apache.org/repos/asf/hugegraph-doc.git

commit bb270838626165ad2c98c11150dde39f4eb113c9
Author: dark <[email protected]>
AuthorDate: Sat Sep 5 08:23:01 2026 +0800

    fix(community): reject duplicate roster IDs
    
    - reject duplicate LDAP owner IDs before set conversion
    - reject duplicate LDAP member IDs before set conversion
    - preserve the last-good roster when refresh validation fails
    - cover both build and refresh fail-closed paths
---
 scripts/community_roster.py      |  3 +++
 scripts/test_community_roster.py | 48 ++++++++++++++++++++++++++++++++++++++++
 2 files changed, 51 insertions(+)

diff --git a/scripts/community_roster.py b/scripts/community_roster.py
index 72c30aae2..c72f885fe 100644
--- a/scripts/community_roster.py
+++ b/scripts/community_roster.py
@@ -331,6 +331,9 @@ def build_roster(committee_data: dict, projects_data: dict, 
people_data: dict, m
         raise RosterError("LDAP project owners/members must be arrays")
     if any(not isinstance(item, str) or not ASF_ID_PATTERN.fullmatch(item) for 
item in owners + members):
         raise RosterError("LDAP project owners/members contain an invalid ASF 
ID")
+    for field, asf_ids in (("owners", owners), ("members", members)):
+        if len(asf_ids) != len(set(asf_ids)):
+            raise RosterError(f"LDAP project {field} contains duplicate ASF 
IDs")
     if not isinstance(chair_map, dict) or len(chair_map) != 1:
         raise RosterError("committee source must name exactly one Chair")
     if not isinstance(committee_roster, dict):
diff --git a/scripts/test_community_roster.py b/scripts/test_community_roster.py
index dfe60dde3..4842a0629 100644
--- a/scripts/test_community_roster.py
+++ b/scripts/test_community_roster.py
@@ -78,6 +78,19 @@ print(json.dumps([person["asf_id"] for person in 
result["roles"]["pmc"]]))
         with self.assertRaisesRegex(roster.RosterError, "disagree"):
             roster.build_roster(committee, projects, people, mapping)
 
+    def test_build_roster_rejects_duplicate_ldap_ids(self):
+        for field in ("owners", "members"):
+            with self.subTest(field=field):
+                committee, projects, people, mapping = self.fixture()
+                projects["projects"]["hugegraph"][field].append(
+                    projects["projects"]["hugegraph"][field][0]
+                )
+                with self.assertRaisesRegex(
+                    roster.RosterError,
+                    rf"LDAP project {field} contains duplicate ASF IDs",
+                ):
+                    roster.build_roster(committee, projects, people, mapping)
+
     def test_mapping_requires_unique_numeric_ids(self):
         mapping = {"schema_version": 1, "mappings": {"one": {"login": "same", 
"user_id": 1}, "two": {"login": "other", "user_id": 1}}}
         with self.assertRaisesRegex(roster.RosterError, "duplicate GitHub 
user_id"):
@@ -384,6 +397,41 @@ print(json.dumps([person["asf_id"] for person in 
result["roles"]["pmc"]]))
             roster._fetch_json = old_fetch
         self.assertEqual(original, roster.ROSTER_PATH.read_bytes())
 
+    def test_refresh_duplicate_ldap_ids_preserves_last_good(self):
+        for field in ("owners", "members"):
+            with self.subTest(field=field), tempfile.TemporaryDirectory(
+                prefix="community-duplicate-test-"
+            ) as directory:
+                root = pathlib.Path(directory)
+                data_dir = root / "data"
+                roster_path, map_path = data_dir / "roster.json", data_dir / 
"github-map.json"
+                data_dir.mkdir()
+                roster_path.write_bytes(b"last-good\n")
+                committee, projects, people, mapping = self.fixture()
+                projects["projects"]["hugegraph"][field].append(
+                    projects["projects"]["hugegraph"][field][0]
+                )
+                sources = {
+                    roster.SOURCES["committee"]: committee,
+                    roster.SOURCES["projects"]: projects,
+                    roster.SOURCES["people"]: people,
+                }
+                map_path.write_text(json.dumps(mapping))
+                with mock.patch.object(roster, "ROOT", root), \
+                     mock.patch.object(roster, "DATA_DIR", data_dir), \
+                     mock.patch.object(roster, "ROSTER_PATH", roster_path), \
+                     mock.patch.object(roster, "MAP_PATH", map_path), \
+                     mock.patch.object(roster, "AVATAR_DIR", root / 
"avatars"), \
+                     mock.patch.object(roster, "_fetch_json", 
side_effect=sources.__getitem__), \
+                     mock.patch.object(roster, "_commit_bundle") as commit:
+                    with self.assertRaisesRegex(
+                        roster.RosterError,
+                        rf"LDAP project {field} contains duplicate ASF IDs",
+                    ):
+                        roster.refresh()
+                commit.assert_not_called()
+                self.assertEqual(b"last-good\n", roster_path.read_bytes())
+
     def test_copy_failure_preserves_last_good_bundle(self):
         with tempfile.TemporaryDirectory(prefix="community-copy-test-") as 
directory:
             root = pathlib.Path(directory)

Reply via email to