2014-11-05 21:43:37 +00:00
|
|
|
import urlparse
|
2015-03-04 00:58:42 +00:00
|
|
|
import github
|
2014-11-05 21:43:37 +00:00
|
|
|
|
|
|
|
class OAuthConfig(object):
|
2015-01-07 21:20:51 +00:00
|
|
|
def __init__(self, config, key_name):
|
2014-11-05 21:43:37 +00:00
|
|
|
self.key_name = key_name
|
2015-01-07 21:20:51 +00:00
|
|
|
self.config = config.get(key_name) or {}
|
2014-11-05 21:43:37 +00:00
|
|
|
|
|
|
|
def service_name(self):
|
|
|
|
raise NotImplementedError
|
|
|
|
|
|
|
|
def token_endpoint(self):
|
|
|
|
raise NotImplementedError
|
|
|
|
|
|
|
|
def user_endpoint(self):
|
|
|
|
raise NotImplementedError
|
|
|
|
|
2015-05-03 18:50:26 +00:00
|
|
|
def validate_client_id_and_secret(self, http_client, app_config):
|
2015-01-08 18:26:24 +00:00
|
|
|
raise NotImplementedError
|
|
|
|
|
2014-11-05 21:43:37 +00:00
|
|
|
def client_id(self):
|
|
|
|
return self.config.get('CLIENT_ID')
|
|
|
|
|
|
|
|
def client_secret(self):
|
|
|
|
return self.config.get('CLIENT_SECRET')
|
|
|
|
|
|
|
|
def _get_url(self, endpoint, *args):
|
|
|
|
for arg in args:
|
|
|
|
endpoint = urlparse.urljoin(endpoint, arg)
|
|
|
|
|
|
|
|
return endpoint
|
|
|
|
|
2015-05-03 18:50:26 +00:00
|
|
|
def get_redirect_uri(self, app_config, redirect_suffix=''):
|
|
|
|
return '%s://%s/oauth2/%s/callback%s' % (app_config['PREFERRED_URL_SCHEME'],
|
|
|
|
app_config['SERVER_HOSTNAME'],
|
|
|
|
self.service_name().lower(),
|
|
|
|
redirect_suffix)
|
|
|
|
|
|
|
|
|
2015-04-24 19:13:08 +00:00
|
|
|
def exchange_code_for_token(self, app_config, http_client, code, form_encode=False,
|
|
|
|
redirect_suffix=''):
|
|
|
|
payload = {
|
|
|
|
'client_id': self.client_id(),
|
|
|
|
'client_secret': self.client_secret(),
|
|
|
|
'code': code,
|
|
|
|
'grant_type': 'authorization_code',
|
2015-05-03 18:50:26 +00:00
|
|
|
'redirect_uri': self.get_redirect_uri(app_config, redirect_suffix)
|
2015-04-24 19:13:08 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
headers = {
|
|
|
|
'Accept': 'application/json'
|
|
|
|
}
|
|
|
|
|
|
|
|
token_url = self.token_endpoint()
|
|
|
|
if form_encode:
|
|
|
|
get_access_token = http_client.post(token_url, data=payload, headers=headers)
|
|
|
|
else:
|
|
|
|
get_access_token = http_client.post(token_url, params=payload, headers=headers)
|
|
|
|
|
|
|
|
json_data = get_access_token.json()
|
|
|
|
if not json_data:
|
|
|
|
return ''
|
|
|
|
|
|
|
|
token = json_data.get('access_token', '')
|
|
|
|
return token
|
|
|
|
|
2014-11-05 21:43:37 +00:00
|
|
|
|
|
|
|
class GithubOAuthConfig(OAuthConfig):
|
2015-01-07 21:20:51 +00:00
|
|
|
def __init__(self, config, key_name):
|
|
|
|
super(GithubOAuthConfig, self).__init__(config, key_name)
|
2014-11-05 21:43:37 +00:00
|
|
|
|
|
|
|
def service_name(self):
|
|
|
|
return 'GitHub'
|
|
|
|
|
2015-03-04 00:58:42 +00:00
|
|
|
def allowed_organizations(self):
|
|
|
|
if not self.config.get('ORG_RESTRICT', False):
|
|
|
|
return None
|
|
|
|
|
2015-04-16 16:17:39 +00:00
|
|
|
allowed = self.config.get('ALLOWED_ORGANIZATIONS', None)
|
|
|
|
if allowed is None:
|
|
|
|
return None
|
|
|
|
|
|
|
|
return [org.lower() for org in allowed]
|
2015-03-04 00:58:42 +00:00
|
|
|
|
2015-05-03 17:38:11 +00:00
|
|
|
def get_public_url(self, suffix):
|
|
|
|
return '%s%s' % (self._endpoint(), suffix)
|
|
|
|
|
2014-11-07 01:35:52 +00:00
|
|
|
def _endpoint(self):
|
2014-11-05 22:17:38 +00:00
|
|
|
endpoint = self.config.get('GITHUB_ENDPOINT', 'https://github.com')
|
2014-11-07 01:35:52 +00:00
|
|
|
if not endpoint.endswith('/'):
|
|
|
|
endpoint = endpoint + '/'
|
|
|
|
return endpoint
|
|
|
|
|
2015-04-16 16:17:39 +00:00
|
|
|
def is_enterprise(self):
|
|
|
|
return self._endpoint().find('.github.com') < 0
|
|
|
|
|
2014-11-07 01:35:52 +00:00
|
|
|
def authorize_endpoint(self):
|
|
|
|
return self._get_url(self._endpoint(), '/login/oauth/authorize') + '?'
|
2014-11-05 21:43:37 +00:00
|
|
|
|
|
|
|
def token_endpoint(self):
|
2014-11-07 01:35:52 +00:00
|
|
|
return self._get_url(self._endpoint(), '/login/oauth/access_token')
|
2014-11-05 21:43:37 +00:00
|
|
|
|
|
|
|
def _api_endpoint(self):
|
2014-11-07 01:35:52 +00:00
|
|
|
return self.config.get('API_ENDPOINT', self._get_url(self._endpoint(), '/api/v3/'))
|
2014-11-05 21:43:37 +00:00
|
|
|
|
2014-11-26 17:37:20 +00:00
|
|
|
def api_endpoint(self):
|
|
|
|
return self._api_endpoint()[0:-1]
|
|
|
|
|
2014-11-05 21:43:37 +00:00
|
|
|
def user_endpoint(self):
|
|
|
|
api_endpoint = self._api_endpoint()
|
|
|
|
return self._get_url(api_endpoint, 'user')
|
|
|
|
|
|
|
|
def email_endpoint(self):
|
|
|
|
api_endpoint = self._api_endpoint()
|
|
|
|
return self._get_url(api_endpoint, 'user/emails')
|
|
|
|
|
2015-03-04 00:58:42 +00:00
|
|
|
def orgs_endpoint(self):
|
|
|
|
api_endpoint = self._api_endpoint()
|
|
|
|
return self._get_url(api_endpoint, 'user/orgs')
|
|
|
|
|
2015-05-03 18:50:26 +00:00
|
|
|
def validate_client_id_and_secret(self, http_client, app_config):
|
2015-01-08 18:26:24 +00:00
|
|
|
# First: Verify that the github endpoint is actually Github by checking for the
|
|
|
|
# X-GitHub-Request-Id here.
|
|
|
|
api_endpoint = self._api_endpoint()
|
2015-01-08 18:56:17 +00:00
|
|
|
result = http_client.get(api_endpoint, auth=(self.client_id(), self.client_secret()), timeout=5)
|
2015-01-08 18:26:24 +00:00
|
|
|
if not 'X-GitHub-Request-Id' in result.headers:
|
|
|
|
raise Exception('Endpoint is not a Github (Enterprise) installation')
|
|
|
|
|
|
|
|
# Next: Verify the client ID and secret.
|
|
|
|
# Note: The following code is a hack until such time as Github officially adds an API endpoint
|
|
|
|
# for verifying a {client_id, client_secret} pair. That being said, this hack was given to us
|
|
|
|
# *by a Github Engineer*, so I think it is okay for the time being :)
|
|
|
|
#
|
|
|
|
# TODO(jschorr): Replace with the real API call once added.
|
|
|
|
#
|
|
|
|
# Hitting the endpoint applications/{client_id}/tokens/foo will result in the following
|
|
|
|
# behavior IF the client_id is given as the HTTP username and the client_secret as the HTTP
|
|
|
|
# password:
|
|
|
|
# - If the {client_id, client_secret} pair is invalid in some way, we get a 401 error.
|
|
|
|
# - If the pair is valid, then we get a 404 because the 'foo' token does not exists.
|
|
|
|
validate_endpoint = self._get_url(api_endpoint, 'applications/%s/tokens/foo' % self.client_id())
|
2015-01-08 18:56:17 +00:00
|
|
|
result = http_client.get(validate_endpoint, auth=(self.client_id(), self.client_secret()),
|
|
|
|
timeout=5)
|
2015-01-08 18:26:24 +00:00
|
|
|
return result.status_code == 404
|
|
|
|
|
2015-03-04 00:58:42 +00:00
|
|
|
def validate_organization(self, organization_id, http_client):
|
|
|
|
api_endpoint = self._api_endpoint()
|
2015-04-16 16:17:39 +00:00
|
|
|
org_endpoint = self._get_url(api_endpoint, 'orgs/%s' % organization_id.lower())
|
2015-03-04 00:58:42 +00:00
|
|
|
|
|
|
|
result = http_client.get(org_endpoint,
|
|
|
|
headers={'Accept': 'application/vnd.github.moondragon+json'},
|
|
|
|
timeout=5)
|
|
|
|
|
|
|
|
return result.status_code == 200
|
|
|
|
|
|
|
|
|
2014-11-07 01:35:52 +00:00
|
|
|
def get_public_config(self):
|
|
|
|
return {
|
|
|
|
'CLIENT_ID': self.client_id(),
|
|
|
|
'AUTHORIZE_ENDPOINT': self.authorize_endpoint(),
|
2015-03-05 17:07:39 +00:00
|
|
|
'GITHUB_ENDPOINT': self._endpoint(),
|
|
|
|
'ORG_RESTRICT': self.config.get('ORG_RESTRICT', False)
|
2014-11-07 01:35:52 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2014-11-05 21:43:37 +00:00
|
|
|
|
|
|
|
class GoogleOAuthConfig(OAuthConfig):
|
2015-01-07 21:20:51 +00:00
|
|
|
def __init__(self, config, key_name):
|
|
|
|
super(GoogleOAuthConfig, self).__init__(config, key_name)
|
2014-11-05 21:43:37 +00:00
|
|
|
|
|
|
|
def service_name(self):
|
|
|
|
return 'Google'
|
|
|
|
|
|
|
|
def authorize_endpoint(self):
|
|
|
|
return 'https://accounts.google.com/o/oauth2/auth?response_type=code&'
|
|
|
|
|
|
|
|
def token_endpoint(self):
|
|
|
|
return 'https://accounts.google.com/o/oauth2/token'
|
|
|
|
|
|
|
|
def user_endpoint(self):
|
|
|
|
return 'https://www.googleapis.com/oauth2/v1/userinfo'
|
|
|
|
|
2015-05-03 18:50:26 +00:00
|
|
|
def validate_client_id_and_secret(self, http_client, app_config):
|
2015-01-08 18:56:17 +00:00
|
|
|
# To verify the Google client ID and secret, we hit the
|
|
|
|
# https://www.googleapis.com/oauth2/v3/token endpoint with an invalid request. If the client
|
|
|
|
# ID or secret are invalid, we get returned a 403 Unauthorized. Otherwise, we get returned
|
|
|
|
# another response code.
|
|
|
|
url = 'https://www.googleapis.com/oauth2/v3/token'
|
|
|
|
data = {
|
|
|
|
'code': 'fakecode',
|
|
|
|
'client_id': self.client_id(),
|
|
|
|
'client_secret': self.client_secret(),
|
|
|
|
'grant_type': 'authorization_code',
|
|
|
|
'redirect_uri': 'http://example.com'
|
|
|
|
}
|
|
|
|
|
|
|
|
result = http_client.post(url, data=data, timeout=5)
|
|
|
|
return result.status_code != 401
|
2015-01-08 18:26:24 +00:00
|
|
|
|
2014-11-07 01:35:52 +00:00
|
|
|
def get_public_config(self):
|
|
|
|
return {
|
|
|
|
'CLIENT_ID': self.client_id(),
|
|
|
|
'AUTHORIZE_ENDPOINT': self.authorize_endpoint()
|
|
|
|
}
|
|
|
|
|
2014-11-05 21:43:37 +00:00
|
|
|
|
2015-05-02 21:54:48 +00:00
|
|
|
class GitLabOAuthConfig(OAuthConfig):
|
|
|
|
def __init__(self, config, key_name):
|
|
|
|
super(GitLabOAuthConfig, self).__init__(config, key_name)
|
|
|
|
|
|
|
|
def _endpoint(self):
|
|
|
|
endpoint = self.config.get('GITLAB_ENDPOINT', 'https://gitlab.com')
|
|
|
|
if not endpoint.endswith('/'):
|
|
|
|
endpoint = endpoint + '/'
|
|
|
|
return endpoint
|
|
|
|
|
|
|
|
def service_name(self):
|
|
|
|
return 'GitLab'
|
|
|
|
|
|
|
|
def authorize_endpoint(self):
|
|
|
|
return self._get_url(self._endpoint(), '/oauth/authorize')
|
|
|
|
|
|
|
|
def token_endpoint(self):
|
|
|
|
return self._get_url(self._endpoint(), '/oauth/token')
|
|
|
|
|
2015-05-03 18:50:26 +00:00
|
|
|
def validate_client_id_and_secret(self, http_client, app_config):
|
|
|
|
url = self.token_endpoint()
|
|
|
|
redirect_uri = self.get_redirect_uri(app_config, redirect_suffix='trigger')
|
|
|
|
data = {
|
|
|
|
'code': 'fakecode',
|
|
|
|
'client_id': self.client_id(),
|
|
|
|
'client_secret': self.client_secret(),
|
|
|
|
'grant_type': 'authorization_code',
|
|
|
|
'redirect_uri': redirect_uri
|
|
|
|
}
|
|
|
|
|
|
|
|
# We validate by checking the error code we receive from this call.
|
|
|
|
result = http_client.post(url, data=data, timeout=5)
|
|
|
|
value = result.json()
|
|
|
|
if not value:
|
|
|
|
return False
|
|
|
|
|
|
|
|
return value.get('error', '') != 'invalid_client'
|
2014-11-05 21:43:37 +00:00
|
|
|
|
2015-05-02 21:54:48 +00:00
|
|
|
def get_public_config(self):
|
|
|
|
return {
|
|
|
|
'CLIENT_ID': self.client_id(),
|
|
|
|
'AUTHORIZE_ENDPOINT': self.authorize_endpoint(),
|
|
|
|
'GITLAB_ENDPOINT': self._endpoint(),
|
|
|
|
}
|