Skip to content
2 changes: 2 additions & 0 deletions src/appengine/handlers/configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ def post(self):
'oss_fuzz_robot_github_personal_access_token')
platform_group_mappings = request.get('platform_group_mappings')
privileged_users = request.get('privileged_users')
privileged_groups = request.get('privileged_groups')
blacklisted_users = request.get('blacklisted_users')
relax_security_bug_restrictions = request.get(
'relax_security_bug_restrictions')
Expand All @@ -146,6 +147,7 @@ def post(self):
config.jira_url = jira_url
config.platform_group_mappings = platform_group_mappings
config.privileged_users = privileged_users
config.privileged_groups = privileged_groups
config.blacklisted_users = blacklisted_users
config.relax_security_bug_restrictions = bool(
relax_security_bug_restrictions)
Expand Down
27 changes: 24 additions & 3 deletions src/appengine/libs/access.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
# limitations under the License.
"""access.py contains static methods around access permissions."""

from googleapiclient import discovery

from clusterfuzz._internal.base import errors
from clusterfuzz._internal.base import external_users
from clusterfuzz._internal.base import utils
Expand All @@ -31,9 +33,28 @@ def _is_privileged_user(email):

privileged_user_emails = (db_config.get_value('privileged_users') or
'').splitlines()
return any(
utils.emails_equal(email, privileged_user_email)
for privileged_user_email in privileged_user_emails)
for privileged_user_email in privileged_user_emails:
if utils.emails_equal(email, privileged_user_email):
return True

privileged_groups = (db_config.get_value('privileged_groups') or
'').splitlines()
identity_service = discovery.build('cloudidentity', 'v1')
for privileged_group in privileged_groups:
# Filter for non-group patterns.
if ('@' not in privileged_group or
utils.is_service_account(privileged_group)):
continue

group_id = auth.get_google_group_id(privileged_group, identity_service)
if not group_id:
continue

if auth.check_transitive_group_membership(group_id, email,
identity_service):
return True

return False


def _is_blacklisted_user(email):
Expand Down
43 changes: 41 additions & 2 deletions src/appengine/libs/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,13 @@
"""Authentication helpers."""

import collections
from urllib import parse

from firebase_admin import auth
from google.auth.transport import requests as google_requests
from google.cloud import ndb
from google.oauth2 import id_token
from googleapiclient.discovery import build
from googleapiclient import discovery
import jwt
import requests

Expand Down Expand Up @@ -74,7 +75,7 @@ def is_current_user_admin():
@memoize.wrap(memoize.FifoInMemory(1))
def _project_number_from_id(project_id):
"""Get the project number from project ID."""
resource_manager = build('cloudresourcemanager', 'v1')
resource_manager = discovery.build('cloudresourcemanager', 'v1')
# pylint: disable=no-member
result = resource_manager.projects().get(projectId=project_id).execute()
if 'projectNumber' not in result:
Expand Down Expand Up @@ -249,3 +250,41 @@ def decode_claims(session_cookie):
return auth.verify_session_cookie(session_cookie, check_revoked=True)
except (ValueError, auth.AuthError):
raise AuthError('Invalid session cookie.')


def get_google_group_id(group_email: str,
identity_service: discovery.Resource | None = None
) -> str | None:
"""Retrive a google group ID."""
if not identity_service:
identity_service = discovery.build('cloudidentity', 'v1')

try:
request = identity_service.groups().lookup(groupKey_id=group_email)
response = request.execute()
return response.get('name')
except Exception:
logs.info(f"Unable to look up group {group_email}.")
return None


def check_transitive_group_membership(
group_id: str,
member: str,
identity_service: discovery.Resource | None = None) -> bool:
"""Check if an user is a member of a google group."""
if not identity_service:
identity_service = discovery.build('cloudidentity', 'v1')

try:
query_params = parse.urlencode({
"query": "member_key_id == '{}'".format(member)
})
request = identity_service.groups().memberships().checkTransitiveMembership(
parent=group_id)
request.uri += "&" + query_params
response = request.execute()
return response.get('hasMembership', False)
except Exception:
logs.info(f'Unable to check group membership from {member} to {group_id}.')
return False
8 changes: 8 additions & 0 deletions src/clusterfuzz/_internal/base/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -1005,6 +1005,14 @@
return normalize_email(first) == normalize_email(second)


def is_service_account(email: str) -> bool:
"""Check if an email is a SA based on email address pattern."""
sa_email = normalize_email(email)
if '@' not in sa_email:
return False
return sa_email.endswith('gserviceaccount.com')


def parse_delimited(value_or_handle, delimiter, strip=False,
remove_empty=False):
"""Parse a delimter separated value."""
Expand Down
3 changes: 3 additions & 0 deletions src/clusterfuzz/_internal/datastore/data_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -818,6 +818,9 @@ class Config(Model):
# Privileged users.
privileged_users = ndb.TextProperty(default='')

# Privileged groups.
privileged_groups = ndb.TextProperty(default='')

# Blacklisted users.
blacklisted_users = ndb.TextProperty(default='')

Expand Down
2 changes: 1 addition & 1 deletion src/clusterfuzz/_internal/system/environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -1004,7 +1004,7 @@ def set_bot_environment():
def set_local_log_only():
"""Force logs to be local-only."""

# We set this to an empty string because currently the logs
# We set this to an empty string because currently the logs
# module does not correctly evaluate env vars
# (i.e., 'false' is evaluated to true).
set_value('LOG_TO_GCP', '')
Expand Down
Loading