Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
26 changes: 24 additions & 2 deletions src/appengine/libs/access.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,33 @@ def _is_privileged_user(email):
if local_config.AuthConfig().get('all_users_privileged'):
return True

# Check privileged access from direct user emails.
privileged_user_emails = (db_config.get_value('privileged_users') or
'').splitlines()
return any(
if any(
utils.emails_equal(email, privileged_user_email)
for privileged_user_email in privileged_user_emails)
for privileged_user_email in privileged_user_emails):
return True

# Check privileged access from google groups.
privileged_groups = (db_config.get_value('privileged_groups') or
'').splitlines()
identity_service = auth.get_identity_api()
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
50 changes: 48 additions & 2 deletions src/appengine/libs/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,21 @@
"""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

from clusterfuzz._internal.base import memoize
from clusterfuzz._internal.base import utils
from clusterfuzz._internal.config import local_config
from clusterfuzz._internal.datastore import data_types
from clusterfuzz._internal.google_cloud_utils import credentials
from clusterfuzz._internal.metrics import logs
from clusterfuzz._internal.system import environment
from libs import helpers
Expand Down Expand Up @@ -74,7 +76,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 +251,47 @@ 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_identity_api() -> discovery.Resource:
"""Return cloud identity api client."""
creds, _ = credentials.get_default()
return discovery.build('cloudidentity', 'v1', credentials=creds)


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 = get_identity_api()

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 = get_identity_api()

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
11 changes: 11 additions & 0 deletions src/appengine/private/components/configuration/configuration.html
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,17 @@
</td>
</tr>

<tr>
<td class="label">
<div class="help" title="List of privileged google groups (one per line). These groups should contain the appengine service account for permission to check membership. Members of these groups can access security vulnerabilities.">
Privileged groups
</div>
</td>
<td>
<paper-textarea name="privileged_groups" max-rows="5" value="[[config.privileged_groups]]" no-label-float></paper-textarea>
</td>
</tr>

<tr>
<td class="label">
<div class="help" title="List of blacklisted users (one email per line). These users are blocked from accessing pages. Has precedence over other rules.">
Expand Down
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 @@ def emails_equal(first, second):
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