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
diff --git a/requirements.txt b/requirements.txt
index 3bb7f91..48ac38e 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -24,3 +24,5 @@
alembic
cryptography>=1.6
cachecontrol
+pyjwt
+iso8601
diff --git a/zuul/driver/github/githubconnection.py b/zuul/driver/github/githubconnection.py
index a17ec40..a09e46f 100644
--- a/zuul/driver/github/githubconnection.py
+++ b/zuul/driver/github/githubconnection.py
@@ -13,6 +13,7 @@
# under the License.
import collections
+import datetime
import logging
import hmac
import hashlib
@@ -20,6 +21,9 @@
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.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 @@
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 @@
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 @@
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)