initial import for Open Source 🎉
This commit is contained in:
parent
1898c361f3
commit
9c0dd3b722
2048 changed files with 218743 additions and 0 deletions
0
oauth/services/__init__.py
Normal file
0
oauth/services/__init__.py
Normal file
180
oauth/services/github.py
Normal file
180
oauth/services/github.py
Normal file
|
@ -0,0 +1,180 @@
|
|||
import logging
|
||||
|
||||
from oauth.base import OAuthEndpoint
|
||||
from oauth.login import OAuthLoginService, OAuthLoginException
|
||||
from util import slash_join
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class GithubOAuthService(OAuthLoginService):
|
||||
def __init__(self, config, key_name):
|
||||
super(GithubOAuthService, self).__init__(config, key_name)
|
||||
|
||||
def login_enabled(self, config):
|
||||
return config.get('FEATURE_GITHUB_LOGIN', False)
|
||||
|
||||
def service_id(self):
|
||||
return 'github'
|
||||
|
||||
def service_name(self):
|
||||
if self.is_enterprise():
|
||||
return 'GitHub Enterprise'
|
||||
|
||||
return 'GitHub'
|
||||
|
||||
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']
|
||||
|
||||
def allowed_organizations(self):
|
||||
if not self.config.get('ORG_RESTRICT', False):
|
||||
return None
|
||||
|
||||
allowed = self.config.get('ALLOWED_ORGANIZATIONS', None)
|
||||
if allowed is None:
|
||||
return None
|
||||
|
||||
return [org.lower() for org in allowed]
|
||||
|
||||
def get_public_url(self, suffix):
|
||||
return slash_join(self._endpoint(), suffix)
|
||||
|
||||
def _endpoint(self):
|
||||
return self.config.get('GITHUB_ENDPOINT', 'https://github.com')
|
||||
|
||||
def is_enterprise(self):
|
||||
return self._api_endpoint().find('.github.com') < 0
|
||||
|
||||
def authorize_endpoint(self):
|
||||
return OAuthEndpoint(slash_join(self._endpoint(), '/login/oauth/authorize'))
|
||||
|
||||
def token_endpoint(self):
|
||||
return OAuthEndpoint(slash_join(self._endpoint(), '/login/oauth/access_token'))
|
||||
|
||||
def user_endpoint(self):
|
||||
return OAuthEndpoint(slash_join(self._api_endpoint(), 'user'))
|
||||
|
||||
def _api_endpoint(self):
|
||||
return self.config.get('API_ENDPOINT', slash_join(self._endpoint(), '/api/v3/'))
|
||||
|
||||
def api_endpoint(self):
|
||||
endpoint = self._api_endpoint()
|
||||
if endpoint.endswith('/'):
|
||||
return endpoint[0:-1]
|
||||
|
||||
return endpoint
|
||||
|
||||
def email_endpoint(self):
|
||||
return slash_join(self._api_endpoint(), 'user/emails')
|
||||
|
||||
def orgs_endpoint(self):
|
||||
return slash_join(self._api_endpoint(), 'user/orgs')
|
||||
|
||||
def validate_client_id_and_secret(self, http_client, url_scheme_and_hostname):
|
||||
# First: Verify that the github endpoint is actually Github by checking for the
|
||||
# X-GitHub-Request-Id here.
|
||||
api_endpoint = self._api_endpoint()
|
||||
result = http_client.get(api_endpoint, auth=(self.client_id(), self.client_secret()), timeout=5)
|
||||
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. This workaround was given to us
|
||||
# *by a Github Engineer* (Jan 8, 2015).
|
||||
#
|
||||
# TODO: 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 = slash_join(api_endpoint, 'applications/%s/tokens/foo' % self.client_id())
|
||||
result = http_client.get(validate_endpoint, auth=(self.client_id(), self.client_secret()),
|
||||
timeout=5)
|
||||
return result.status_code == 404
|
||||
|
||||
def validate_organization(self, organization_id, http_client):
|
||||
org_endpoint = slash_join(self._api_endpoint(), 'orgs/%s' % organization_id.lower())
|
||||
|
||||
result = http_client.get(org_endpoint,
|
||||
headers={'Accept': 'application/vnd.github.moondragon+json'},
|
||||
timeout=5)
|
||||
|
||||
return result.status_code == 200
|
||||
|
||||
|
||||
def get_public_config(self):
|
||||
return {
|
||||
'CLIENT_ID': self.client_id(),
|
||||
'AUTHORIZE_ENDPOINT': self.authorize_endpoint().to_url(),
|
||||
'GITHUB_ENDPOINT': self._endpoint(),
|
||||
'ORG_RESTRICT': self.config.get('ORG_RESTRICT', False)
|
||||
}
|
||||
|
||||
def get_login_service_id(self, user_info):
|
||||
return user_info['id']
|
||||
|
||||
def get_login_service_username(self, user_info):
|
||||
return user_info['login']
|
||||
|
||||
def get_verified_user_email(self, app_config, http_client, token, user_info):
|
||||
v3_media_type = {
|
||||
'Accept': 'application/vnd.github.v3'
|
||||
}
|
||||
|
||||
token_param = {
|
||||
'access_token': token,
|
||||
}
|
||||
|
||||
# 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)
|
||||
|
||||
verified_emails = [email for email in get_email.json() if email['verified']]
|
||||
primary_emails = [email for email in get_email.json() if email['primary']]
|
||||
|
||||
# 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
|
||||
|
||||
allowed_emails = (primary_emails or verified_emails or [])
|
||||
return allowed_emails[0]['email'] if len(allowed_emails) > 0 else None
|
||||
|
||||
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
|
||||
|
||||
moondragon_media_type = {
|
||||
'Accept': 'application/vnd.github.moondragon+json'
|
||||
}
|
||||
|
||||
token_param = {
|
||||
'access_token': token,
|
||||
}
|
||||
|
||||
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)
|
60
oauth/services/gitlab.py
Normal file
60
oauth/services/gitlab.py
Normal file
|
@ -0,0 +1,60 @@
|
|||
from oauth.base import OAuthService, OAuthEndpoint
|
||||
from util import slash_join
|
||||
|
||||
class GitLabOAuthService(OAuthService):
|
||||
def __init__(self, config, key_name):
|
||||
super(GitLabOAuthService, self).__init__(config, key_name)
|
||||
|
||||
def service_id(self):
|
||||
return 'gitlab'
|
||||
|
||||
def service_name(self):
|
||||
return 'GitLab'
|
||||
|
||||
def _endpoint(self):
|
||||
return self.config.get('GITLAB_ENDPOINT', 'https://gitlab.com')
|
||||
|
||||
def user_endpoint(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def api_endpoint(self):
|
||||
return self._endpoint()
|
||||
|
||||
def get_public_url(self, suffix):
|
||||
return slash_join(self._endpoint(), suffix)
|
||||
|
||||
def authorize_endpoint(self):
|
||||
return OAuthEndpoint(slash_join(self._endpoint(), '/oauth/authorize'))
|
||||
|
||||
def token_endpoint(self):
|
||||
return OAuthEndpoint(slash_join(self._endpoint(), '/oauth/token'))
|
||||
|
||||
def validate_client_id_and_secret(self, http_client, url_scheme_and_hostname):
|
||||
# We validate the client ID and secret by hitting the OAuth token exchange endpoint with
|
||||
# the real client ID and secret, but a fake auth code to exchange. Gitlab's implementation will
|
||||
# return `invalid_client` as the `error` if the client ID or secret is invalid; otherwise, it
|
||||
# will return another error.
|
||||
url = self.token_endpoint().to_url()
|
||||
redirect_uri = self.get_redirect_uri(url_scheme_and_hostname, 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'
|
||||
|
||||
def get_public_config(self):
|
||||
return {
|
||||
'CLIENT_ID': self.client_id(),
|
||||
'AUTHORIZE_ENDPOINT': self.authorize_endpoint().to_url(),
|
||||
'GITLAB_ENDPOINT': self._endpoint(),
|
||||
}
|
81
oauth/services/google.py
Normal file
81
oauth/services/google.py
Normal file
|
@ -0,0 +1,81 @@
|
|||
from oauth.base import OAuthEndpoint
|
||||
from oauth.login import OAuthLoginService
|
||||
|
||||
def _get_email_username(email_address):
|
||||
username = email_address
|
||||
at = username.find('@')
|
||||
if at > 0:
|
||||
username = username[0:at]
|
||||
|
||||
return username
|
||||
|
||||
class GoogleOAuthService(OAuthLoginService):
|
||||
def __init__(self, config, key_name):
|
||||
super(GoogleOAuthService, self).__init__(config, key_name)
|
||||
|
||||
def login_enabled(self, config):
|
||||
return config.get('FEATURE_GOOGLE_LOGIN', False)
|
||||
|
||||
def service_id(self):
|
||||
return 'google'
|
||||
|
||||
def service_name(self):
|
||||
return 'Google'
|
||||
|
||||
def get_icon(self):
|
||||
return 'fa-google'
|
||||
|
||||
def get_login_scopes(self):
|
||||
return ['openid', 'email']
|
||||
|
||||
def authorize_endpoint(self):
|
||||
return OAuthEndpoint('https://accounts.google.com/o/oauth2/auth',
|
||||
params=dict(response_type='code'))
|
||||
|
||||
def token_endpoint(self):
|
||||
return OAuthEndpoint('https://accounts.google.com/o/oauth2/token')
|
||||
|
||||
def user_endpoint(self):
|
||||
return OAuthEndpoint('https://www.googleapis.com/oauth2/v1/userinfo')
|
||||
|
||||
def requires_form_encoding(self):
|
||||
return True
|
||||
|
||||
def validate_client_id_and_secret(self, http_client, url_scheme_and_hostname):
|
||||
# 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
|
||||
|
||||
def get_public_config(self):
|
||||
return {
|
||||
'CLIENT_ID': self.client_id(),
|
||||
'AUTHORIZE_ENDPOINT': self.authorize_endpoint().to_url()
|
||||
}
|
||||
|
||||
def get_login_service_id(self, user_info):
|
||||
return user_info['id']
|
||||
|
||||
def get_login_service_username(self, user_info):
|
||||
return _get_email_username(user_info['email'])
|
||||
|
||||
def get_verified_user_email(self, app_config, http_client, token, user_info):
|
||||
if not user_info.get('verified_email', False):
|
||||
return None
|
||||
|
||||
return user_info['email']
|
||||
|
||||
def service_verify_user_info_for_login(self, app_config, http_client, token, user_info):
|
||||
# Nothing to do.
|
||||
pass
|
38
oauth/services/test/test_github.py
Normal file
38
oauth/services/test/test_github.py
Normal file
|
@ -0,0 +1,38 @@
|
|||
import pytest
|
||||
|
||||
from oauth.services.github import GithubOAuthService
|
||||
|
||||
@pytest.mark.parametrize('trigger_config, domain, api_endpoint, is_enterprise', [
|
||||
({
|
||||
'CLIENT_ID': 'someclientid',
|
||||
'CLIENT_SECRET': 'someclientsecret',
|
||||
'API_ENDPOINT': 'https://api.github.com/v3',
|
||||
}, 'https://github.com', 'https://api.github.com/v3', False),
|
||||
({
|
||||
'GITHUB_ENDPOINT': 'https://github.somedomain.com/',
|
||||
'CLIENT_ID': 'someclientid',
|
||||
'CLIENT_SECRET': 'someclientsecret',
|
||||
}, 'https://github.somedomain.com', 'https://github.somedomain.com/api/v3', True),
|
||||
({
|
||||
'GITHUB_ENDPOINT': 'https://github.somedomain.com/',
|
||||
'API_ENDPOINT': 'http://somedomain.com/api/',
|
||||
'CLIENT_ID': 'someclientid',
|
||||
'CLIENT_SECRET': 'someclientsecret',
|
||||
}, 'https://github.somedomain.com', 'http://somedomain.com/api', True),
|
||||
])
|
||||
def test_basic_enterprise_config(trigger_config, domain, api_endpoint, is_enterprise):
|
||||
config = {
|
||||
'GITHUB_TRIGGER_CONFIG': trigger_config
|
||||
}
|
||||
|
||||
github_trigger = GithubOAuthService(config, 'GITHUB_TRIGGER_CONFIG')
|
||||
assert github_trigger.is_enterprise() == is_enterprise
|
||||
|
||||
assert github_trigger.authorize_endpoint().to_url() == '%s/login/oauth/authorize' % domain
|
||||
|
||||
assert github_trigger.token_endpoint().to_url() == '%s/login/oauth/access_token' % domain
|
||||
|
||||
assert github_trigger.api_endpoint() == api_endpoint
|
||||
assert github_trigger.user_endpoint().to_url() == '%s/user' % api_endpoint
|
||||
assert github_trigger.email_endpoint() == '%s/user/emails' % api_endpoint
|
||||
assert github_trigger.orgs_endpoint() == '%s/user/orgs' % api_endpoint
|
Reference in a new issue