From b5a4d44645d36648836a244dd07d99c39a8f88c7 Mon Sep 17 00:00:00 2001 From: "James E. Blair" Date: Wed, 29 Jan 2020 14:23:27 -0800 Subject: [PATCH] Add gcloud_service auth option for Gerrit driver A user running Zuul in the Google cloud may want to use service accounts to authenticate to Gerrit. Notably, the Gerrit project itself operates in this manner. Therefore, add support for using the default service account to access Gerrit. It is also very likely that a user may not want to expose a service account with Gerrit credentials to the executors. Therefore, we can not assume that they will have access to the same service account. Therefore if the gloud_service auth type is specified for a Gerrit connection, we will only use anonymous HTTP access for the git repositories. This restriction is not ideal, and may be able to be removed later if we find an out-of-band method of communicating a service account token to the executor in a manner that would not make it available to jobs running on it. Change-Id: I02bc3219018278d517bc3ae6f1ac0be22ef7c0ed --- doc/source/reference/drivers/gerrit.rst | 9 ++++ zuul/driver/gerrit/gcloudauth.py | 66 +++++++++++++++++++++++++ zuul/driver/gerrit/gerritconnection.py | 19 +++++-- 3 files changed, 89 insertions(+), 5 deletions(-) create mode 100644 zuul/driver/gerrit/gcloudauth.py diff --git a/doc/source/reference/drivers/gerrit.rst b/doc/source/reference/drivers/gerrit.rst index cc924eb4d6..dc1ecaa280 100644 --- a/doc/source/reference/drivers/gerrit.rst +++ b/doc/source/reference/drivers/gerrit.rst @@ -122,6 +122,15 @@ The supported options in ``zuul.conf`` connections are: Zuul will submit a username and password to a form in order to authenticate. + .. value:: gcloud_service + + Only valid when running in Google Cloud. This will use the + default service account to authenticate to Gerrit. Note that + this will only be used for interacting with the Gerrit API; + anonymous HTTP access will be used to access the git + repositories, therefore private repos or draft changes will + not be available. + .. attr:: verify_ssl :default: true diff --git a/zuul/driver/gerrit/gcloudauth.py b/zuul/driver/gerrit/gcloudauth.py new file mode 100644 index 0000000000..addb471fac --- /dev/null +++ b/zuul/driver/gerrit/gcloudauth.py @@ -0,0 +1,66 @@ +# Copyright 2012 Google Inc. +# Copyright 2019 Red Hat, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import logging +import requests +import threading +import time + +TOKEN_URL = ('http://metadata.google.internal/computeMetadata/' + 'v1/instance/service-accounts/default/token') +REFRESH = 25 +RETRY_INTERVAL = 5 + + +class GCloudAuth(requests.auth.AuthBase): + log = logging.getLogger('zuul.GerritConnection') + + def __init__(self, user, password): + self.token = None + self.expires = 0 + try: + self.getToken() + except Exception: + self.log.exception("Error updating token:") + self.update_thread = threading.Thread(target=self.update) + self.update_thread.daemon = True + self.update_thread.start() + + def update(self): + while True: + try: + self._update() + except Exception: + self.log.exception("Error updating token:") + time.sleep(5) + + def _update(self): + now = time.time() + expires = self.expires - REFRESH + expires = max(expires, now + RETRY_INTERVAL) + while now < expires: + time.sleep(expires - now) + now = time.time() + self.getToken() + + def getToken(self): + r = requests.get(TOKEN_URL, headers={'Metadata-Flavor': 'Google'}) + data = r.json() + self.token = data['access_token'] + self.expires = time.time() + data['expires_in'] + + def __call__(self, request): + request.prepare_cookies({'o': self.token}) + return request diff --git a/zuul/driver/gerrit/gerritconnection.py b/zuul/driver/gerrit/gerritconnection.py index 7dea617ea8..5e58991d45 100644 --- a/zuul/driver/gerrit/gerritconnection.py +++ b/zuul/driver/gerrit/gerritconnection.py @@ -37,6 +37,7 @@ from uuid import uuid4 from zuul import version as zuul_version from zuul.connection import BaseConnection from zuul.driver.gerrit.auth import FormAuth +from zuul.driver.gerrit.gcloudauth import GCloudAuth from zuul.driver.gerrit.gerritmodel import GerritChange, GerritTriggerEvent from zuul.lib.logutil import get_annotated_logger from zuul.model import Ref, Tag, Branch, Project @@ -474,8 +475,9 @@ class GerritConnection(BaseConnection): self.session = None self.password = self.connection_config.get('password', None) - if self.password: - self.auth_type = self.connection_config.get('auth_type', None) + self.auth_type = self.connection_config.get('auth_type', None) + self.anonymous_git = False + if self.password or self.auth_type == 'gcloud_service': self.verify_ssl = self.connection_config.get('verify_ssl', True) if self.verify_ssl not in ['true', 'True', '1', 1, 'TRUE']: self.verify_ssl = False @@ -487,10 +489,15 @@ class GerritConnection(BaseConnection): authclass = requests.auth.HTTPDigestAuth elif self.auth_type == 'form': authclass = FormAuth + elif self.auth_type == 'gcloud_service': + authclass = GCloudAuth + # The executors in google cloud may not have access + # to the gerrit account credentials, so just use + # anonymous http access for git + self.anonymous_git = True else: authclass = requests.auth.HTTPBasicAuth - self.auth = authclass( - self.user, self.password) + self.auth = authclass(self.user, self.password) def setWatchedCheckers(self, checkers_to_watch): self.log.debug("Setting watched checkers to %s", checkers_to_watch) @@ -1291,7 +1298,9 @@ class GerritConnection(BaseConnection): return ret def getGitUrl(self, project: Project) -> str: - if self.session: + if self.anonymous_git: + url = ('%s/%s' % (self.baseurl, project.name)) + elif self.session: baseurl = list(urllib.parse.urlparse(self.baseurl)) # Make sure we escape '/' symbols, otherwise git's url # parser will think the username is a hostname.