Skip to content

Commit fb1c36c

Browse files
Add cron for creating OSS-Fuzz CC groups (#5151)
Cronjob to create and sync the OSS-Fuzz projects' CCs with their respective google groups, which will be used in the issue tracker. After running this for a couple of times (which will create all the groups for the existing projects and sync them), my plan is to add this step in the project setup and remove this cronjob. Context: b/477964128, b/388390041, google/oss-fuzz#12945
1 parent f00b539 commit fb1c36c

File tree

2 files changed

+179
-0
lines changed

2 files changed

+179
-0
lines changed
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# Copyright 2026 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
"""Cron to sync OSS-Fuzz projects groups used as CC in the issue tracker."""
15+
16+
from clusterfuzz._internal.base import utils
17+
from clusterfuzz._internal.cron import project_setup
18+
from clusterfuzz._internal.google_cloud_utils import google_groups
19+
from clusterfuzz._internal.metrics import logs
20+
21+
_CC_GROUP_SUFFIX = '-ccs@oss-fuzz.com'
22+
_CC_GROUP_DESC = 'External CCs in OSS-Fuzz issue tracker for project'
23+
24+
25+
def sync_project_cc_group(project_name, info):
26+
"""Sync the project's google group used for CCing in the issue tracker."""
27+
group_name = f'{project_name}{_CC_GROUP_SUFFIX}'
28+
29+
group_id = google_groups.get_group_id(group_name)
30+
# Create the group and bail out since the CIG API might delay to create a
31+
# new group. Add members will be done in the next project-setup run.
32+
if not group_id:
33+
group_description = f'{_CC_GROUP_DESC}: {project_name}'
34+
created = google_groups.create_google_group(
35+
group_name, group_description=group_description)
36+
if not created:
37+
logs.info('Failed to create or retrieve the issue tracker CC group '
38+
f'for {project_name}')
39+
return
40+
logs.info(f'Created issue tracker CC group for {project_name}. '
41+
'Skipping adding members as group may still not exist.')
42+
return
43+
44+
group_memberships = google_groups.get_google_group_memberships(group_id)
45+
if group_memberships is None:
46+
logs.info(
47+
f'Failed to get list of group members for {project_name}. Skipping.')
48+
return
49+
50+
ccs = set(project_setup.ccs_from_info(info))
51+
52+
to_add = ccs - group_memberships.keys()
53+
for member in to_add:
54+
google_groups.add_member_to_group(group_id, member)
55+
56+
to_delete = group_memberships.keys() - ccs
57+
for member in to_delete:
58+
# Ignore the SA that created the group from members to delete.
59+
if utils.is_service_account(member):
60+
continue
61+
memebership_name = group_memberships[member]
62+
google_groups.delete_google_group_membership(group_id, member,
63+
memebership_name)
64+
65+
66+
def main():
67+
"""Sync OSS-Fuzz projects groups used to CC owners in the issue tracker."""
68+
projects = project_setup.get_oss_fuzz_projects()
69+
for project, info in projects:
70+
sync_project_cc_group(project, info)
71+
72+
logs.info('OSS-Fuzz CC groups sync succeeded.')
73+
return True
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
# Copyright 2026 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
"""Tests for oss_fuzz_cc_groups cron."""
15+
16+
import unittest
17+
18+
from clusterfuzz._internal.cron import oss_fuzz_cc_groups
19+
from clusterfuzz._internal.tests.test_libs import helpers as test_helpers
20+
21+
22+
class OssFuzzCcGroupsTest(unittest.TestCase):
23+
"""Tests for oss_fuzz_cc_groups."""
24+
25+
def setUp(self):
26+
test_helpers.patch(self, [
27+
'clusterfuzz._internal.cron.project_setup.get_oss_fuzz_projects',
28+
'clusterfuzz._internal.cron.project_setup.ccs_from_info',
29+
'clusterfuzz._internal.google_cloud_utils.google_groups.get_group_id',
30+
'clusterfuzz._internal.google_cloud_utils.google_groups.create_google_group',
31+
'clusterfuzz._internal.google_cloud_utils.google_groups.get_google_group_memberships',
32+
'clusterfuzz._internal.google_cloud_utils.google_groups.add_member_to_group',
33+
'clusterfuzz._internal.google_cloud_utils.google_groups.delete_google_group_membership',
34+
'clusterfuzz._internal.base.utils.is_service_account',
35+
])
36+
37+
def test_main(self):
38+
"""Test main execution for creating groups and syncing project ccs."""
39+
self.mock.get_oss_fuzz_projects.return_value = [
40+
('project1', {
41+
'info': 1
42+
}),
43+
('project2', {
44+
'info': 2
45+
}),
46+
]
47+
48+
# project1 group does not exist, so create it.
49+
# project2 group exists, only sync members.
50+
self.mock.get_group_id.side_effect = [None, 'group2_id']
51+
self.mock.create_google_group.return_value = True
52+
53+
self.mock.get_google_group_memberships.return_value = {
54+
'member1@example.com': 'membership1',
55+
'member2@example.com': 'membership2',
56+
}
57+
self.mock.ccs_from_info.return_value = [
58+
'member2@example.com',
59+
'member3@example.com',
60+
]
61+
self.mock.is_service_account.return_value = False
62+
63+
self.assertTrue(oss_fuzz_cc_groups.main())
64+
65+
# project1 check
66+
self.mock.create_google_group.assert_called_with(
67+
'project1-ccs@oss-fuzz.com',
68+
group_description=(
69+
'External CCs in OSS-Fuzz issue tracker for project: project1'))
70+
71+
# project2 check
72+
self.mock.add_member_to_group.assert_called_with('group2_id',
73+
'member3@example.com')
74+
self.mock.delete_google_group_membership.assert_called_with(
75+
'group2_id', 'member1@example.com', 'membership1')
76+
77+
def test_create_fail(self):
78+
"""Test group creation failure."""
79+
self.mock.get_oss_fuzz_projects.return_value = [('project1', {})]
80+
self.mock.get_group_id.return_value = None
81+
self.mock.create_google_group.return_value = False
82+
83+
self.assertTrue(oss_fuzz_cc_groups.main())
84+
self.mock.get_google_group_memberships.assert_not_called()
85+
86+
def test_get_memberships_fail(self):
87+
"""Test get memberships failure."""
88+
self.mock.get_oss_fuzz_projects.return_value = [('project1', {})]
89+
self.mock.get_group_id.return_value = 'group1_id'
90+
self.mock.get_google_group_memberships.return_value = None
91+
92+
self.assertTrue(oss_fuzz_cc_groups.main())
93+
self.mock.ccs_from_info.assert_not_called()
94+
95+
def test_skip_sa_deletion(self):
96+
"""Test that service accounts are not deleted from group."""
97+
self.mock.get_oss_fuzz_projects.return_value = [('project1', {})]
98+
self.mock.get_group_id.return_value = 'group1_id'
99+
self.mock.get_google_group_memberships.return_value = {
100+
'sa@serviceaccount.com': 'membership_sa',
101+
}
102+
self.mock.ccs_from_info.return_value = []
103+
self.mock.is_service_account.return_value = True
104+
105+
self.assertTrue(oss_fuzz_cc_groups.main())
106+
self.mock.delete_google_group_membership.assert_not_called()

0 commit comments

Comments
 (0)