2017-01-20 20:21:08 +00:00
|
|
|
import logging
|
|
|
|
|
2018-05-15 17:28:43 +00:00
|
|
|
from oauth.base import OAuthEndpoint
|
2017-01-20 20:21:08 +00:00
|
|
|
from oauth.login import OAuthLoginService, OAuthLoginException
|
2016-03-18 19:09:25 +00:00
|
|
|
from util import slash_join
|
2015-09-04 20:14:46 +00:00
|
|
|
|
2017-01-20 20:21:08 +00:00
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
class GithubOAuthService(OAuthLoginService):
|
2015-01-07 21:20:51 +00:00
|
|
|
def __init__(self, config, key_name):
|
2017-01-19 20:23:15 +00:00
|
|
|
super(GithubOAuthService, self).__init__(config, key_name)
|
2014-11-05 21:43:37 +00:00
|
|
|
|
2017-01-23 22:53:34 +00:00
|
|
|
def login_enabled(self, config):
|
|
|
|
return config.get('FEATURE_GITHUB_LOGIN', False)
|
|
|
|
|
2017-01-20 20:21:08 +00:00
|
|
|
def service_id(self):
|
|
|
|
return 'github'
|
|
|
|
|
2014-11-05 21:43:37 +00:00
|
|
|
def service_name(self):
|
2017-01-20 20:21:08 +00:00
|
|
|
if self.is_enterprise():
|
|
|
|
return 'GitHub Enterprise'
|
|
|
|
|
2014-11-05 21:43:37 +00:00
|
|
|
return 'GitHub'
|
|
|
|
|
2017-01-20 20:21:08 +00:00
|
|
|
def get_icon(self):
|
|
|
|
return 'fa-github'
|
|
|
|
|
|
|
|
def get_login_scopes(self):
|
|
|
|
if self.config.get('ORG_RESTRICT'):
|
|
|
|
return ['user:email', 'read:org']
|
|
|
|
|
|
|
|
return ['user:email']
|
|
|
|
|
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):
|
2015-12-22 19:55:38 +00:00
|
|
|
return slash_join(self._endpoint(), suffix)
|
2015-05-03 17:38:11 +00:00
|
|
|
|
2014-11-07 01:35:52 +00:00
|
|
|
def _endpoint(self):
|
2015-12-22 19:55:38 +00:00
|
|
|
return self.config.get('GITHUB_ENDPOINT', 'https://github.com')
|
2014-11-07 01:35:52 +00:00
|
|
|
|
2015-04-16 16:17:39 +00:00
|
|
|
def is_enterprise(self):
|
2017-01-20 20:21:08 +00:00
|
|
|
return self._api_endpoint().find('.github.com') < 0
|
2015-04-16 16:17:39 +00:00
|
|
|
|
2014-11-07 01:35:52 +00:00
|
|
|
def authorize_endpoint(self):
|
2018-05-15 17:28:43 +00:00
|
|
|
return OAuthEndpoint(slash_join(self._endpoint(), '/login/oauth/authorize'))
|
2014-11-05 21:43:37 +00:00
|
|
|
|
|
|
|
def token_endpoint(self):
|
2018-05-15 17:28:43 +00:00
|
|
|
return OAuthEndpoint(slash_join(self._endpoint(), '/login/oauth/access_token'))
|
|
|
|
|
|
|
|
def user_endpoint(self):
|
|
|
|
return OAuthEndpoint(slash_join(self._api_endpoint(), 'user'))
|
2014-11-05 21:43:37 +00:00
|
|
|
|
|
|
|
def _api_endpoint(self):
|
2015-12-22 19:55:38 +00:00
|
|
|
return self.config.get('API_ENDPOINT', slash_join(self._endpoint(), '/api/v3/'))
|
2014-11-05 21:43:37 +00:00
|
|
|
|
2014-11-26 17:37:20 +00:00
|
|
|
def api_endpoint(self):
|
2016-06-30 18:10:22 +00:00
|
|
|
endpoint = self._api_endpoint()
|
|
|
|
if endpoint.endswith('/'):
|
|
|
|
return endpoint[0:-1]
|
|
|
|
|
|
|
|
return endpoint
|
2014-11-26 17:37:20 +00:00
|
|
|
|
2014-11-05 21:43:37 +00:00
|
|
|
def email_endpoint(self):
|
2015-12-22 19:55:38 +00:00
|
|
|
return slash_join(self._api_endpoint(), 'user/emails')
|
2014-11-05 21:43:37 +00:00
|
|
|
|
2015-03-04 00:58:42 +00:00
|
|
|
def orgs_endpoint(self):
|
2015-12-22 19:55:38 +00:00
|
|
|
return slash_join(self._api_endpoint(), 'user/orgs')
|
2015-03-04 00:58:42 +00:00
|
|
|
|
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.
|
2015-12-22 19:55:38 +00:00
|
|
|
validate_endpoint = slash_join(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()),
|
2015-12-22 19:54:47 +00:00
|
|
|
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):
|
2015-12-22 19:55:38 +00:00
|
|
|
org_endpoint = slash_join(self._api_endpoint(), 'orgs/%s' % organization_id.lower())
|
2015-03-04 00:58:42 +00:00
|
|
|
|
|
|
|
result = http_client.get(org_endpoint,
|
2015-12-22 19:54:47 +00:00
|
|
|
headers={'Accept': 'application/vnd.github.moondragon+json'},
|
|
|
|
timeout=5)
|
2015-03-04 00:58:42 +00:00
|
|
|
|
|
|
|
return result.status_code == 200
|
|
|
|
|
|
|
|
|
2014-11-07 01:35:52 +00:00
|
|
|
def get_public_config(self):
|
|
|
|
return {
|
|
|
|
'CLIENT_ID': self.client_id(),
|
2018-05-15 17:28:43 +00:00
|
|
|
'AUTHORIZE_ENDPOINT': self.authorize_endpoint().to_url_prefix(),
|
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
|
|
|
}
|
|
|
|
|
2017-01-20 20:21:08 +00:00
|
|
|
def get_login_service_id(self, user_info):
|
|
|
|
return user_info['id']
|
2014-11-07 01:35:52 +00:00
|
|
|
|
2017-01-20 20:21:08 +00:00
|
|
|
def get_login_service_username(self, user_info):
|
|
|
|
return user_info['login']
|
2014-11-05 21:43:37 +00:00
|
|
|
|
2017-01-20 20:21:08 +00:00
|
|
|
def get_verified_user_email(self, app_config, http_client, token, user_info):
|
|
|
|
v3_media_type = {
|
|
|
|
'Accept': 'application/vnd.github.v3'
|
2015-01-08 18:56:17 +00:00
|
|
|
}
|
|
|
|
|
2017-01-20 20:21:08 +00:00
|
|
|
token_param = {
|
|
|
|
'access_token': token,
|
2014-11-07 01:35:52 +00:00
|
|
|
}
|
|
|
|
|
2017-01-20 20:21:08 +00:00
|
|
|
# Find the e-mail address for the user: we will accept any email, but we prefer the primary
|
|
|
|
get_email = http_client.get(self.email_endpoint(), params=token_param, headers=v3_media_type)
|
|
|
|
if get_email.status_code // 100 != 2:
|
|
|
|
raise OAuthLoginException('Got non-2XX status code for emails endpoint: %s' %
|
|
|
|
get_email.status_code)
|
2014-11-05 21:43:37 +00:00
|
|
|
|
2017-01-20 20:21:08 +00:00
|
|
|
verified_emails = [email for email in get_email.json() if email['verified']]
|
|
|
|
primary_emails = [email for email in get_email.json() if email['primary']]
|
2015-12-21 19:20:37 +00:00
|
|
|
|
2017-01-20 20:21:08 +00:00
|
|
|
# Special case: We don't care about whether an e-mail address is "verified" under GHE.
|
|
|
|
if self.is_enterprise() and not verified_emails:
|
|
|
|
verified_emails = primary_emails
|
2015-05-02 21:54:48 +00:00
|
|
|
|
2017-01-20 20:21:08 +00:00
|
|
|
allowed_emails = (primary_emails or verified_emails or [])
|
|
|
|
return allowed_emails[0]['email'] if len(allowed_emails) > 0 else None
|
2015-05-02 21:54:48 +00:00
|
|
|
|
2017-01-20 20:21:08 +00:00
|
|
|
def service_verify_user_info_for_login(self, app_config, http_client, token, user_info):
|
|
|
|
# Retrieve the user's orgnizations (if organization filtering is turned on)
|
|
|
|
if self.allowed_organizations() is None:
|
|
|
|
return
|
2015-05-02 21:54:48 +00:00
|
|
|
|
2017-01-20 20:21:08 +00:00
|
|
|
moondragon_media_type = {
|
|
|
|
'Accept': 'application/vnd.github.moondragon+json'
|
2015-05-03 18:50:26 +00:00
|
|
|
}
|
|
|
|
|
2017-01-20 20:21:08 +00:00
|
|
|
token_param = {
|
|
|
|
'access_token': token,
|
2015-05-02 21:54:48 +00:00
|
|
|
}
|
2017-01-20 20:21:08 +00:00
|
|
|
|
|
|
|
get_orgs = http_client.get(self.orgs_endpoint(), params=token_param,
|
|
|
|
headers=moondragon_media_type)
|
|
|
|
|
|
|
|
if get_orgs.status_code // 100 != 2:
|
|
|
|
logger.debug('get_orgs response: %s', get_orgs.json())
|
|
|
|
raise OAuthLoginException('Got non-2XX response for org lookup: %s' %
|
|
|
|
get_orgs.status_code)
|
|
|
|
|
|
|
|
organizations = set([org.get('login').lower() for org in get_orgs.json()])
|
|
|
|
matching_organizations = organizations & set(self.allowed_organizations())
|
|
|
|
if not matching_organizations:
|
|
|
|
logger.debug('Found organizations %s, but expected one of %s', organizations,
|
|
|
|
self.allowed_organizations())
|
|
|
|
err = """You are not a member of an allowed GitHub organization.
|
|
|
|
Please contact your system administrator if you believe this is in error."""
|
|
|
|
raise OAuthLoginException(err)
|