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:
parent
3a00ae807f
commit
62847153d8
@ -24,3 +24,5 @@ sqlalchemy
|
|||||||
alembic
|
alembic
|
||||||
cryptography>=1.6
|
cryptography>=1.6
|
||||||
cachecontrol
|
cachecontrol
|
||||||
|
pyjwt
|
||||||
|
iso8601
|
||||||
|
@ -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':
|
|
||||||
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.")
|
|
||||||
|
|
||||||
# anything going through requests to http/s goes through cache
|
if self.git_host != 'github.com':
|
||||||
self.github.session.mount('http://', self.cache_adapter)
|
url = 'https://%s/' % self.git_host
|
||||||
self.github.session.mount('https://', self.cache_adapter)
|
github = github3.GitHubEnterprise(url)
|
||||||
else:
|
else:
|
||||||
self.github = None
|
github = github3.GitHub()
|
||||||
self.log.info(
|
|
||||||
"No Github credentials found in zuul configuration, cannot "
|
# anything going through requests to http/s goes through cache
|
||||||
"authenticate.")
|
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):
|
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)
|
||||||
|
Loading…
Reference in New Issue
Block a user