Skip to content

Commit cb5cb5b

Browse files
Fix group settings to allow external member into oss-fuzz cc groups (#5155)
Creating groups through the cloud identity API uses a default set of access settings. We need to change these in order for them to work as CC groups for OSS-Fuzz projects. Mainly, allowing external members to be added to the group. These changes cannot be done through the identity API, so we need to use the groups settings API (which I enabled for the clusterfuzz-external deployment). I added some other settings as safeguard to avoid issues with these groups (as they will be in the public tracker), such as who can view/post/contact/manage the group to only owners/managers. Also, fixed some single/double quote style. **Tests** * Added unit test. * Run locally impersonating the clusterfuzz-external service account. * Output: https://paste.googleplex.com/4898709642018816 (errors in the first run are expected as googleapiclient logs an error when the checking for the group and it does not exist). * Group settings after executing it: https://screenshot.googleplex.com/5ymqVPoFMwHYraN.png Context: b/477964128
1 parent f96a469 commit cb5cb5b

File tree

3 files changed

+89
-21
lines changed

3 files changed

+89
-21
lines changed

src/clusterfuzz/_internal/cron/oss_fuzz_cc_groups.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,25 +28,32 @@ def sync_project_cc_group(project_name, info):
2828

2929
group_id = google_groups.get_group_id(group_name)
3030
# 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.
31+
# new group. Add members will be done in the next cron run.
3232
if not group_id:
3333
group_description = f'{_CC_GROUP_DESC}: {project_name}'
3434
created = google_groups.create_google_group(
3535
group_name, group_description=group_description)
3636
if not created:
37-
logs.info('Failed to create or retrieve the issue tracker CC group '
38-
f'for {project_name}')
37+
logs.warning('Failed to create or retrieve the issue tracker CC group '
38+
f'for {project_name}')
3939
return
4040
logs.info(f'Created issue tracker CC group for {project_name}. '
4141
'Skipping adding members as group may still not exist.')
4242
return
4343

4444
group_memberships = google_groups.get_google_group_memberships(group_id)
4545
if group_memberships is None:
46-
logs.info(
46+
logs.warning(
4747
f'Failed to get list of group members for {project_name}. Skipping.')
4848
return
4949

50+
if len(group_memberships) <= 1:
51+
# If only the SA is a member, we know that the group has just been created
52+
# and we need to update settings to allow external members.
53+
if not google_groups.set_oss_fuzz_access_settings(group_name):
54+
logs.warning(f'Failed to allow external members for {group_name}')
55+
return
56+
5057
ccs = set(project_setup.ccs_from_info(info))
5158

5259
to_add = ccs - group_memberships.keys()
@@ -66,6 +73,7 @@ def sync_project_cc_group(project_name, info):
6673
def main():
6774
"""Sync OSS-Fuzz projects groups used to CC owners in the issue tracker."""
6875
projects = project_setup.get_oss_fuzz_projects()
76+
6977
for project, info in projects:
7078
sync_project_cc_group(project, info)
7179

src/clusterfuzz/_internal/google_cloud_utils/google_groups.py

Lines changed: 56 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,16 @@ def get_identity_api() -> discovery.Resource | None:
4040
return _local.identity_service
4141

4242

43+
def get_group_settings_api() -> discovery.Resource | None:
44+
"""Return the groups settings api client."""
45+
if not hasattr(_local, 'groups_settings_service'):
46+
creds, _ = credentials.get_default()
47+
_local.groups_settings_service = discovery.build(
48+
'groupssettings', 'v1', credentials=creds, cache_discovery=False)
49+
50+
return _local.groups_settings_service
51+
52+
4353
def get_group_id(group_name: str, exists_check: bool = False) -> str | None:
4454
"""Retrive a google group ID."""
4555
identity_service = get_identity_api()
@@ -49,7 +59,7 @@ def get_group_id(group_name: str, exists_check: bool = False) -> str | None:
4959
return response.get('name')
5060
except errors.HttpError:
5161
if not exists_check:
52-
logs.warning(f"Unable to look up group {group_name}.")
62+
logs.warning(f'Unable to look up group {group_name}.')
5363
return None
5464

5565

@@ -58,11 +68,11 @@ def check_transitive_group_membership(group_id: str, member: str) -> bool:
5868
identity_service = get_identity_api()
5969
try:
6070
query_params = parse.urlencode({
61-
"query": "member_key_id == '{}'".format(member)
71+
'query': "member_key_id == '{}'".format(member)
6272
})
6373
request = identity_service.groups().memberships().checkTransitiveMembership(
6474
parent=group_id)
65-
request.uri += "&" + query_params
75+
request.uri += '&' + query_params
6676
response = request.execute(num_retries=_FAIL_RETRIES)
6777
return response.get('hasMembership', False)
6878
except errors.HttpError:
@@ -71,6 +81,35 @@ def check_transitive_group_membership(group_id: str, member: str) -> bool:
7181
return False
7282

7383

84+
def set_oss_fuzz_access_settings(group_name: str) -> bool:
85+
"""Sets expected access settings for oss-fuzz projects cc groups.
86+
87+
The main setting is allowing external members, but as a safeguard, it sets
88+
who can view/post/contact/moderate the group to only owners/managers.
89+
"""
90+
groups_service = get_group_settings_api()
91+
92+
try:
93+
group_resources = {
94+
'allowExternalMembers': 'true',
95+
'whoCanViewMembership': 'ALL_MANAGERS_CAN_VIEW',
96+
'whoCanViewGroup': 'ALL_MANAGERS_CAN_VIEW',
97+
'whoCanPostMessage': 'ALL_OWNERS_CAN_POST',
98+
'whoCanContactOwner': 'ALL_MANAGERS_CAN_CONTACT',
99+
'whoCanModerateMembers': 'OWNERS_ONLY'
100+
}
101+
response = groups_service.groups().patch(
102+
groupUniqueId=group_name,
103+
body=group_resources).execute(num_retries=_FAIL_RETRIES)
104+
logs.info(
105+
f'Updated group access settings for {group_name}',
106+
request_response=response)
107+
return True
108+
except errors.HttpError:
109+
logs.error(f'Failed to set access settings for {group_name}')
110+
return False
111+
112+
74113
def create_google_group(group_name: str,
75114
group_display_name: str | None = None,
76115
group_description: str | None = None,
@@ -84,20 +123,20 @@ def create_google_group(group_name: str,
84123
logs.error('No customer ID set. Unable to create a new google group.')
85124
return None
86125

87-
group_key = {"id": group_name}
126+
group_key = {'id': group_name}
88127
group = {
89-
"parent": "customers/" + customer_id,
90-
"description": group_description,
91-
"displayName": group_display_name,
92-
"groupKey": group_key,
128+
'parent': 'customers/' + customer_id,
129+
'description': group_description,
130+
'displayName': group_display_name,
131+
'groupKey': group_key,
93132
# Set the label to specify creation of a Google Group.
94-
"labels": {
95-
"cloudidentity.googleapis.com/groups.discussion_forum": ""
133+
'labels': {
134+
'cloudidentity.googleapis.com/groups.discussion_forum': ''
96135
}
97136
}
98137
try:
99138
request = identity_service.groups().create(body=group)
100-
request.uri += "&initialGroupConfig=WITH_INITIAL_OWNER"
139+
request.uri += '&initialGroupConfig=WITH_INITIAL_OWNER'
101140
response = request.execute(num_retries=_FAIL_RETRIES)
102141
group_id = response.get('response').get('name')
103142
logs.info(f'Created google group {group_name}', request_response=response)
@@ -131,11 +170,11 @@ def add_member_to_group(group_id: str, member: str) -> bool:
131170
try:
132171
# Create a membership object with a role type MEMBER
133172
membership = {
134-
"preferredMemberKey": {
135-
"id": member
173+
'preferredMemberKey': {
174+
'id': member
136175
},
137-
"roles": {
138-
"name": "MEMBER",
176+
'roles': {
177+
'name': 'MEMBER',
139178
}
140179
}
141180
# Create a membership using the group ID and the membership object
@@ -159,9 +198,9 @@ def delete_google_group_membership(group_id: str,
159198
if not membership_name:
160199
membership_lookup_request = identity_service.groups().memberships(
161200
).lookup(parent=group_id)
162-
membership_lookup_request.uri += "&memberKey.id=" + member
201+
membership_lookup_request.uri += '&memberKey.id=' + member
163202
membership_lookup_response = membership_lookup_request.execute()
164-
membership_name = membership_lookup_response.get("name")
203+
membership_name = membership_lookup_response.get('name')
165204

166205
response = identity_service.groups().memberships().delete(
167206
name=membership_name).execute(num_retries=_FAIL_RETRIES)

src/clusterfuzz/_internal/tests/appengine/handlers/cron/oss_fuzz_cc_groups_test.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ def setUp(self):
3131
'clusterfuzz._internal.google_cloud_utils.google_groups.get_google_group_memberships',
3232
'clusterfuzz._internal.google_cloud_utils.google_groups.add_member_to_group',
3333
'clusterfuzz._internal.google_cloud_utils.google_groups.delete_google_group_membership',
34+
'clusterfuzz._internal.google_cloud_utils.google_groups.set_oss_fuzz_access_settings',
3435
'clusterfuzz._internal.base.utils.is_service_account',
3536
])
3637

@@ -74,6 +75,26 @@ def test_main(self):
7475
self.mock.delete_google_group_membership.assert_called_with(
7576
'group2_id', '[email protected]', 'membership1')
7677

78+
def test_main_exec_for_new_group(self):
79+
"""Test main execution for a newly created group."""
80+
self.mock.get_oss_fuzz_projects.return_value = [('project1', {'info': 1})]
81+
self.mock.get_group_id.return_value = '1'
82+
self.mock.get_google_group_memberships.return_value = {
83+
'[email protected]': 'membership1'
84+
}
85+
self.mock.set_oss_fuzz_access_settings.return_value = True
86+
self.mock.ccs_from_info.return_value = ['[email protected]']
87+
self.mock.is_service_account.return_value = True
88+
89+
self.assertTrue(oss_fuzz_cc_groups.main())
90+
self.mock.create_google_group.assert_not_called()
91+
self.mock.get_group_id.assert_called_once_with('[email protected]')
92+
self.mock.set_oss_fuzz_access_settings.assert_called_once_with(
93+
94+
self.mock.add_member_to_group.assert_called_once_with(
95+
96+
self.mock.delete_google_group_membership.assert_not_called()
97+
7798
def test_create_fail(self):
7899
"""Test group creation failure."""
79100
self.mock.get_oss_fuzz_projects.return_value = [('project1', {})]

0 commit comments

Comments
 (0)