Merge "Use app_id with github" into feature/zuulv3
This commit is contained in:
commit
0bfcfb5b2e
|
@ -24,3 +24,5 @@ sqlalchemy
|
|||
alembic
|
||||
cryptography>=1.6
|
||||
cachecontrol
|
||||
pyjwt
|
||||
iso8601
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
# under the License.
|
||||
|
||||
import collections
|
||||
import datetime
|
||||
import logging
|
||||
import hmac
|
||||
import hashlib
|
||||
|
@ -20,6 +21,9 @@ import time
|
|||
|
||||
import cachecontrol
|
||||
from cachecontrol.cache import DictCache
|
||||
import iso8601
|
||||
import jwt
|
||||
import requests
|
||||
import webob
|
||||
import webob.dec
|
||||
import voluptuous as v
|
||||
|
@ -31,6 +35,25 @@ from zuul.model import Ref
|
|||
from zuul.exceptions import MergeFailure
|
||||
from zuul.driver.github.githubmodel import PullRequest, GithubTriggerEvent
|
||||
|
||||
ACCESS_TOKEN_URL = 'https://api.github.com/installations/%s/access_tokens'
|
||||
PREVIEW_JSON_ACCEPT = 'application/vnd.github.machine-man-preview+json'
|
||||
|
||||
|
||||
class UTC(datetime.tzinfo):
|
||||
"""UTC"""
|
||||
|
||||
def utcoffset(self, dt):
|
||||
return datetime.timedelta(0)
|
||||
|
||||
def tzname(self, dt):
|
||||
return "UTC"
|
||||
|
||||
def dst(self, dt):
|
||||
return datetime.timedelta(0)
|
||||
|
||||
|
||||
utc = UTC()
|
||||
|
||||
|
||||
class GithubWebhookListener():
|
||||
|
||||
|
@ -279,20 +302,25 @@ class GithubConnection(BaseConnection):
|
|||
driver_name = 'github'
|
||||
log = logging.getLogger("connection.github")
|
||||
payload_path = 'payload'
|
||||
git_user = 'git'
|
||||
|
||||
def __init__(self, driver, connection_name, connection_config):
|
||||
super(GithubConnection, self).__init__(
|
||||
driver, connection_name, connection_config)
|
||||
self.github = None
|
||||
self._change_cache = {}
|
||||
self.projects = {}
|
||||
self._git_ssh = bool(self.connection_config.get('sshkey', None))
|
||||
self.git_ssh_key = self.connection_config.get('sshkey')
|
||||
self.git_host = self.connection_config.get('git_host', 'github.com')
|
||||
self.canonical_hostname = self.connection_config.get(
|
||||
'canonical_hostname', self.git_host)
|
||||
self.source = driver.getSource(self)
|
||||
|
||||
self._github = None
|
||||
self.app_id = None
|
||||
self.app_key = None
|
||||
self.installation_id = None
|
||||
self.installation_token = None
|
||||
self.installation_expiry = None
|
||||
|
||||
# NOTE(jamielennox): Better here would be to cache to memcache or file
|
||||
# or something external - but zuul already sucks at restarting so in
|
||||
# memory probably doesn't make this much worse.
|
||||
|
@ -310,23 +338,86 @@ class GithubConnection(BaseConnection):
|
|||
self.unregisterHttpHandler(self.payload_path)
|
||||
|
||||
def _authenticateGithubAPI(self):
|
||||
token = self.connection_config.get('api_token', None)
|
||||
if token is not None:
|
||||
if self.git_host != 'github.com':
|
||||
url = 'https://%s/' % self.git_host
|
||||
self.github = github3.enterprise_login(token=token, url=url)
|
||||
else:
|
||||
self.github = github3.login(token=token)
|
||||
self.log.info("Github API Authentication successful.")
|
||||
config = self.connection_config
|
||||
|
||||
# anything going through requests to http/s goes through cache
|
||||
self.github.session.mount('http://', self.cache_adapter)
|
||||
self.github.session.mount('https://', self.cache_adapter)
|
||||
if self.git_host != 'github.com':
|
||||
url = 'https://%s/' % self.git_host
|
||||
github = github3.GitHubEnterprise(url)
|
||||
else:
|
||||
self.github = None
|
||||
self.log.info(
|
||||
"No Github credentials found in zuul configuration, cannot "
|
||||
"authenticate.")
|
||||
github = github3.GitHub()
|
||||
|
||||
# anything going through requests to http/s goes through cache
|
||||
github.session.mount('http://', self.cache_adapter)
|
||||
github.session.mount('https://', self.cache_adapter)
|
||||
|
||||
api_token = config.get('api_token')
|
||||
|
||||
if api_token:
|
||||
github.login(token=api_token)
|
||||
else:
|
||||
app_id = config.get('app_id')
|
||||
installation_id = config.get('installation_id')
|
||||
app_key_file = config.get('app_key')
|
||||
|
||||
if app_key_file:
|
||||
with open(app_key_file, 'r') as f:
|
||||
app_key = f.read()
|
||||
|
||||
if not (app_id and app_key and installation_id):
|
||||
self.log.warning("You must provide an app_id, "
|
||||
"app_key and installation_id to use "
|
||||
"installation based authentication")
|
||||
|
||||
return
|
||||
|
||||
self.app_id = int(app_id)
|
||||
self.installation_id = int(installation_id)
|
||||
self.app_key = app_key
|
||||
|
||||
self._github = github
|
||||
|
||||
def _get_installation_key(self, user_id=None):
|
||||
if not (self.installation_id and self.app_id):
|
||||
return None
|
||||
|
||||
now = datetime.datetime.now(utc)
|
||||
|
||||
if ((not self.installation_expiry) or
|
||||
(not self.installation_token) or
|
||||
(now < self.installation_expiry)):
|
||||
expiry = now + datetime.timedelta(minutes=5)
|
||||
|
||||
data = {'iat': now, 'exp': expiry, 'iss': self.app_id}
|
||||
app_token = jwt.encode(data,
|
||||
self.app_key,
|
||||
algorithm='RS256')
|
||||
|
||||
url = ACCESS_TOKEN_URL % self.installation_id
|
||||
headers = {'Accept': PREVIEW_JSON_ACCEPT,
|
||||
'Authorization': 'Bearer %s' % app_token}
|
||||
json_data = {'user_id': user_id} if user_id else None
|
||||
|
||||
response = requests.post(url, headers=headers, json=json_data)
|
||||
response.raise_for_status()
|
||||
|
||||
data = response.json()
|
||||
|
||||
self.installation_expiry = iso8601.parse_date(data['expires_at'])
|
||||
self.installation_expiry -= datetime.timedelta(minutes=5)
|
||||
self.installation_token = data['token']
|
||||
|
||||
return self.installation_token
|
||||
|
||||
@property
|
||||
def github(self):
|
||||
# if we're using api_key authentication then we don't need to fetch
|
||||
# new installation tokens so return the existing one.
|
||||
installation_key = self._get_installation_key()
|
||||
|
||||
if installation_key:
|
||||
self._github.login(token=installation_key)
|
||||
|
||||
return self._github
|
||||
|
||||
def maintainCache(self, relevant):
|
||||
for key, change in self._change_cache.items():
|
||||
|
@ -363,12 +454,16 @@ class GithubConnection(BaseConnection):
|
|||
return change
|
||||
|
||||
def getGitUrl(self, project):
|
||||
if self._git_ssh:
|
||||
url = 'ssh://%s@%s/%s.git' % \
|
||||
(self.git_user, self.git_host, project)
|
||||
else:
|
||||
url = 'https://%s/%s' % (self.git_host, project)
|
||||
return url
|
||||
if self.git_ssh_key:
|
||||
return 'ssh://git@%s/%s.git' % (self.git_host, project)
|
||||
|
||||
installation_key = self._get_installation_key()
|
||||
if installation_key:
|
||||
return 'https://x-access-token:%s@%s/%s' % (installation_key,
|
||||
self.git_host,
|
||||
project)
|
||||
|
||||
return 'https://%s/%s' % (self.git_host, project)
|
||||
|
||||
def getGitwebUrl(self, project, sha=None):
|
||||
url = 'https://%s/%s' % (self.git_host, project)
|
||||
|
|
Loading…
Reference in New Issue