Use app_id with github

The basics of authenticating to github as an app when posting
comments and cloning. This is still a WIP.

Change-Id: I11fab75d635a8bcea7210945df4071bf51d7d3f2
This commit is contained in:
Jamie Lennox 2016-12-09 14:04:42 +11:00 committed by Jesse Keating
parent 3a00ae807f
commit 62847153d8
2 changed files with 121 additions and 24 deletions

View File

@ -24,3 +24,5 @@ sqlalchemy
alembic alembic
cryptography>=1.6 cryptography>=1.6
cachecontrol cachecontrol
pyjwt
iso8601

View File

@ -13,6 +13,7 @@
# under the License. # under the License.
import collections import collections
import datetime
import logging import logging
import hmac import hmac
import hashlib import hashlib
@ -20,6 +21,9 @@ import time
import cachecontrol import cachecontrol
from cachecontrol.cache import DictCache from cachecontrol.cache import DictCache
import iso8601
import jwt
import requests
import webob import webob
import webob.dec import webob.dec
import voluptuous as v import voluptuous as v
@ -31,6 +35,25 @@ from zuul.model import Ref
from zuul.exceptions import MergeFailure from zuul.exceptions import MergeFailure
from zuul.driver.github.githubmodel import PullRequest, GithubTriggerEvent 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(): class GithubWebhookListener():
@ -279,20 +302,25 @@ class GithubConnection(BaseConnection):
driver_name = 'github' driver_name = 'github'
log = logging.getLogger("connection.github") log = logging.getLogger("connection.github")
payload_path = 'payload' payload_path = 'payload'
git_user = 'git'
def __init__(self, driver, connection_name, connection_config): def __init__(self, driver, connection_name, connection_config):
super(GithubConnection, self).__init__( super(GithubConnection, self).__init__(
driver, connection_name, connection_config) driver, connection_name, connection_config)
self.github = None
self._change_cache = {} self._change_cache = {}
self.projects = {} 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.git_host = self.connection_config.get('git_host', 'github.com')
self.canonical_hostname = self.connection_config.get( self.canonical_hostname = self.connection_config.get(
'canonical_hostname', self.git_host) 'canonical_hostname', self.git_host)
self.source = driver.getSource(self) 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 # NOTE(jamielennox): Better here would be to cache to memcache or file
# or something external - but zuul already sucks at restarting so in # or something external - but zuul already sucks at restarting so in
# memory probably doesn't make this much worse. # memory probably doesn't make this much worse.
@ -310,23 +338,86 @@ class GithubConnection(BaseConnection):
self.unregisterHttpHandler(self.payload_path) self.unregisterHttpHandler(self.payload_path)
def _authenticateGithubAPI(self): def _authenticateGithubAPI(self):
token = self.connection_config.get('api_token', None) config = self.connection_config
if token is not None:
if self.git_host != 'github.com': if self.git_host != 'github.com':
url = 'https://%s/' % self.git_host url = 'https://%s/' % self.git_host
self.github = github3.enterprise_login(token=token, url=url) github = github3.GitHubEnterprise(url)
else: else:
self.github = github3.login(token=token) github = github3.GitHub()
self.log.info("Github API Authentication successful.")
# anything going through requests to http/s goes through cache # anything going through requests to http/s goes through cache
self.github.session.mount('http://', self.cache_adapter) github.session.mount('http://', self.cache_adapter)
self.github.session.mount('https://', 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: else:
self.github = None app_id = config.get('app_id')
self.log.info( installation_id = config.get('installation_id')
"No Github credentials found in zuul configuration, cannot " app_key_file = config.get('app_key')
"authenticate.")
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): def maintainCache(self, relevant):
for key, change in self._change_cache.items(): for key, change in self._change_cache.items():
@ -363,12 +454,16 @@ class GithubConnection(BaseConnection):
return change return change
def getGitUrl(self, project): def getGitUrl(self, project):
if self._git_ssh: if self.git_ssh_key:
url = 'ssh://%s@%s/%s.git' % \ return 'ssh://git@%s/%s.git' % (self.git_host, project)
(self.git_user, self.git_host, project)
else: installation_key = self._get_installation_key()
url = 'https://%s/%s' % (self.git_host, project) if installation_key:
return url 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): def getGitwebUrl(self, project, sha=None):
url = 'https://%s/%s' % (self.git_host, project) url = 'https://%s/%s' % (self.git_host, project)