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)
