- Make the OAuth config system centralized

- Add support for Github Enterprise login
This commit is contained in:
Joseph Schorr 2014-11-05 16:43:37 -05:00
parent 6deafe8c86
commit 3e79379942
11 changed files with 196 additions and 83 deletions

6
app.py
View file

@ -19,6 +19,7 @@ from util.analytics import Analytics
from util.exceptionlog import Sentry
from util.queuemetrics import QueueMetrics
from util.names import urn_generator
from util.oauth import GoogleOAuthConfig, GithubOAuthConfig
from data.billing import Billing
from data.buildlogs import BuildLogs
from data.archivedlogs import LogArchive
@ -131,6 +132,11 @@ queue_metrics = QueueMetrics(app)
authentication = UserAuthentication(app)
userevents = UserEventsBuilderModule(app)
github_login = GithubOAuthConfig(app, 'GITHUB_LOGIN_CONFIG')
github_trigger = GithubOAuthConfig(app, 'GITHUB_TRIGGER_CONFIG')
google_login = GoogleOAuthConfig(app, 'GOOGLE_LOGIN_CONFIG')
oauth_apps = [github_login, github_trigger, google_login]
tf = app.config['DB_TRANSACTION_FACTORY']
image_diff_queue = WorkQueue(app.config['DIFFS_QUEUE_NAME'], tf)
dockerfile_build_queue = WorkQueue(app.config['DOCKERFILE_BUILD_QUEUE_NAME'], tf,

View file

@ -15,11 +15,10 @@ def build_requests_session():
# The set of configuration key names that will be accessible in the client. Since these
# values are set to the frontend, DO NOT PLACE ANY SECRETS OR KEYS in this list.
CLIENT_WHITELIST = ['SERVER_HOSTNAME', 'PREFERRED_URL_SCHEME', 'GITHUB_CLIENT_ID',
'GITHUB_LOGIN_CLIENT_ID', 'MIXPANEL_KEY', 'STRIPE_PUBLISHABLE_KEY',
'ENTERPRISE_LOGO_URL', 'SENTRY_PUBLIC_DSN', 'AUTHENTICATION_TYPE',
'REGISTRY_TITLE', 'REGISTRY_TITLE_SHORT', 'GOOGLE_LOGIN_CLIENT_ID',
# values are sent to the frontend, DO NOT PLACE ANY SECRETS OR KEYS in this list.
CLIENT_WHITELIST = ['SERVER_HOSTNAME', 'PREFERRED_URL_SCHEME', 'MIXPANEL_KEY',
'STRIPE_PUBLISHABLE_KEY', 'ENTERPRISE_LOGO_URL', 'SENTRY_PUBLIC_DSN',
'AUTHENTICATION_TYPE', 'REGISTRY_TITLE', 'REGISTRY_TITLE_SHORT',
'CONTACT_INFO']
@ -108,22 +107,11 @@ class DefaultConfig(object):
SENTRY_PUBLIC_DSN = None
# Github Config
GITHUB_TOKEN_URL = 'https://github.com/login/oauth/access_token'
GITHUB_USER_URL = 'https://api.github.com/user'
GITHUB_USER_EMAILS = GITHUB_USER_URL + '/emails'
GITHUB_CLIENT_ID = ''
GITHUB_CLIENT_SECRET = ''
GITHUB_LOGIN_CLIENT_ID = ''
GITHUB_LOGIN_CLIENT_SECRET = ''
GITHUB_LOGIN_CONFIG = None
GITHUB_TRIGGER_CONFIG = None
# Google Config.
GOOGLE_TOKEN_URL = 'https://accounts.google.com/o/oauth2/token'
GOOGLE_USER_URL = 'https://www.googleapis.com/oauth2/v1/userinfo'
GOOGLE_LOGIN_CLIENT_ID = ''
GOOGLE_LOGIN_CLIENT_SECRET = ''
GOOGLE_LOGIN_CONFIG = None
# Requests based HTTP client with a large request pool
HTTPCLIENT = build_requests_session()

View file

@ -1,10 +1,11 @@
import logging
import requests
from flask import request, redirect, url_for, Blueprint
from flask.ext.login import current_user
from endpoints.common import render_page_template, common_login, route_show_if
from app import app, analytics, get_app_url
from app import app, analytics, get_app_url, github_login, google_login, github_trigger
from data import model
from util.names import parse_repository_name
from util.validation import generate_valid_usernames
@ -29,20 +30,16 @@ def render_ologin_error(service_name,
service_url=get_app_url(),
user_creation=features.USER_CREATION)
def exchange_code_for_token(code, service_name='GITHUB', for_login=True, form_encode=False,
redirect_suffix=''):
def exchange_code_for_token(code, service, form_encode=False, redirect_suffix=''):
code = request.args.get('code')
id_config = service_name + '_LOGIN_CLIENT_ID' if for_login else service_name + '_CLIENT_ID'
secret_config = service_name + '_LOGIN_CLIENT_SECRET' if for_login else service_name + '_CLIENT_SECRET'
payload = {
'client_id': app.config[id_config],
'client_secret': app.config[secret_config],
'client_id': service.client_id(),
'client_secret': service.client_secret(),
'code': code,
'grant_type': 'authorization_code',
'redirect_uri': '%s://%s/oauth2/%s/callback%s' % (app.config['PREFERRED_URL_SCHEME'],
app.config['SERVER_HOSTNAME'],
service_name.lower(),
service.service_name().lower(),
redirect_suffix)
}
@ -50,12 +47,11 @@ def exchange_code_for_token(code, service_name='GITHUB', for_login=True, form_en
'Accept': 'application/json'
}
token_url = service.token_endpoint()
if form_encode:
get_access_token = client.post(app.config[service_name + '_TOKEN_URL'],
data=payload, headers=headers)
get_access_token = client.post(token_url, data=payload, headers=headers)
else:
get_access_token = client.post(app.config[service_name + '_TOKEN_URL'],
params=payload, headers=headers)
get_access_token = client.post(token_url, params=payload, headers=headers)
json_data = get_access_token.json()
if not json_data:
@ -65,25 +61,20 @@ def exchange_code_for_token(code, service_name='GITHUB', for_login=True, form_en
return token
def get_github_user(token):
token_param = {
'access_token': token,
}
get_user = client.get(app.config['GITHUB_USER_URL'], params=token_param)
return get_user.json()
def get_google_user(token):
def get_user(service, token):
token_param = {
'access_token': token,
'alt': 'json',
}
get_user = client.get(service.user_endpoint(), params=token_param)
if get_user.status_code != requests.codes.ok:
return {}
get_user = client.get(app.config['GOOGLE_USER_URL'], params=token_param)
return get_user.json()
def conduct_oauth_login(service_name, user_id, username, email, metadata={}):
def conduct_oauth_login(service, user_id, username, email, metadata={}):
service_name = service.service_name()
to_login = model.verify_federated_login(service_name.lower(), user_id)
if not to_login:
# See if we can create a new user.
@ -138,8 +129,8 @@ def google_oauth_callback():
if error:
return render_ologin_error('Google', error)
token = exchange_code_for_token(request.args.get('code'), service_name='GOOGLE', form_encode=True)
user_data = get_google_user(token)
token = exchange_code_for_token(request.args.get('code'), google_login, form_encode=True)
user_data = get_user(google_login, token)
if not user_data or not user_data.get('id', None) or not user_data.get('email', None):
return render_ologin_error('Google')
@ -148,7 +139,7 @@ def google_oauth_callback():
'service_username': user_data['email']
}
return conduct_oauth_login('Google', user_data['id'], username, user_data['email'],
return conduct_oauth_login(google_login, user_data['id'], username, user_data['email'],
metadata=metadata)
@ -159,8 +150,8 @@ def github_oauth_callback():
if error:
return render_ologin_error('GitHub', error)
token = exchange_code_for_token(request.args.get('code'), service_name='GITHUB')
user_data = get_github_user(token)
token = exchange_code_for_token(request.args.get('code'), github_login)
user_data = get_user(github_login, token)
if not user_data or not 'login' in user_data:
return render_ologin_error('GitHub')
@ -174,7 +165,7 @@ def github_oauth_callback():
token_param = {
'access_token': token,
}
get_email = client.get(app.config['GITHUB_USER_EMAILS'], params=token_param,
get_email = client.get(github_login.email_endpoint(), params=token_param,
headers=v3_media_type)
# We will accept any email, but we prefer the primary
@ -188,17 +179,17 @@ def github_oauth_callback():
'service_username': username
}
return conduct_oauth_login('github', github_id, username, found_email, metadata=metadata)
return conduct_oauth_login(github_login, github_id, username, found_email, metadata=metadata)
@callback.route('/google/callback/attach', methods=['GET'])
@route_show_if(features.GOOGLE_LOGIN)
@require_session_login
def google_oauth_attach():
token = exchange_code_for_token(request.args.get('code'), service_name='GOOGLE',
token = exchange_code_for_token(request.args.get('code'), google_login,
redirect_suffix='/attach', form_encode=True)
user_data = get_google_user(token)
user_data = get_user(google_login, token)
if not user_data or not user_data.get('id', None):
return render_ologin_error('Google')
@ -224,8 +215,8 @@ def google_oauth_attach():
@route_show_if(features.GITHUB_LOGIN)
@require_session_login
def github_oauth_attach():
token = exchange_code_for_token(request.args.get('code'), service_name='GITHUB')
user_data = get_github_user(token)
token = exchange_code_for_token(request.args.get('code'), github_login)
user_data = get_user(github_login, token)
if not user_data:
return render_ologin_error('GitHub')
@ -255,8 +246,7 @@ def github_oauth_attach():
def attach_github_build_trigger(namespace, repository):
permission = AdministerRepositoryPermission(namespace, repository)
if permission.can():
token = exchange_code_for_token(request.args.get('code'), service_name='GITHUB',
for_login=False)
token = exchange_code_for_token(request.args.get('code'), github_trigger)
repo = model.get_repository(namespace, repository)
if not repo:
msg = 'Invalid repository: %s/%s' % (namespace, repository)

View file

@ -11,7 +11,8 @@ from random import SystemRandom
from data import model
from data.database import db
from app import app, login_manager, dockerfile_build_queue, notification_queue
from app import app, login_manager, dockerfile_build_queue, notification_queue, oauth_apps
from auth.permissions import QuayDeferredPermissionUser
from auth import scopes
from endpoints.api.discovery import swagger_route_data
@ -176,6 +177,16 @@ def render_page_template(name, **kwargs):
external_styles = get_external_css(local=not app.config.get('USE_CDN', True))
external_scripts = get_external_javascript(local=not app.config.get('USE_CDN', True))
def get_oauth_config():
oauth_config = {}
for oauth_app in oauth_apps:
oauth_config[oauth_app.key_name] = {
'CLIENT_ID': oauth_app.client_id(),
'AUTHORIZE_ENDPOINT': oauth_app.authorize_endpoint()
}
return oauth_config
contact_href = None
if len(app.config.get('CONTACT_INFO', [])) == 1:
contact_href = app.config['CONTACT_INFO'][0]
@ -189,6 +200,7 @@ def render_page_template(name, **kwargs):
library_scripts=library_scripts,
feature_set=json.dumps(features.get_features()),
config_set=json.dumps(getFrontendVisibleConfig(app.config)),
oauth_set=json.dumps(get_oauth_config()),
mixpanel_key=app.config.get('MIXPANEL_KEY', ''),
google_analytics_key=app.config.get('GOOGLE_ANALYTICS_KEY', ''),
sentry_public_dsn=app.config.get('SENTRY_PUBLIC_DSN', ''),

View file

@ -8,7 +8,7 @@ import re
from github import Github, UnknownObjectException, GithubException
from tempfile import SpooledTemporaryFile
from app import app, userfiles as user_files
from app import app, userfiles as user_files, github_trigger
from util.tarfileappender import TarfileAppender
@ -150,8 +150,8 @@ def raise_unsupported():
class GithubBuildTrigger(BuildTrigger):
@staticmethod
def _get_client(auth_token):
return Github(auth_token, client_id=app.config['GITHUB_CLIENT_ID'],
client_secret=app.config['GITHUB_CLIENT_SECRET'])
return Github(auth_token, client_id=github_trigger.client_id(),
client_secret=github_trigger.client_secret())
@classmethod
def service_name(cls):

View file

@ -2,8 +2,15 @@
<span ng-if="provider == 'github'">
<a href="javascript:void(0)" class="btn btn-primary btn-block" quay-require="['GITHUB_LOGIN']" ng-click="startSignin('github')" style="margin-bottom: 10px" ng-disabled="signingIn">
<i class="fa fa-github fa-lg"></i>
<span ng-if="action != 'attach'">Sign In with GitHub</span>
<span ng-if="action == 'attach'">Attach to GitHub Account</span>
<span ng-if="action != 'attach'">
Sign In with GitHub
<span ng-if="isEnterprise('github')">Enterprise</span>
</span>
<span ng-if="action == 'attach'">
Attach to GitHub
<span ng-if="isEnterprise('github')">Enterprise</span>
Account
</span>
</a>
</span>

View file

@ -620,7 +620,8 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
}]);
$provide.factory('TriggerService', ['UtilService', '$sanitize', function(UtilService, $sanitize) {
$provide.factory('TriggerService', ['UtilService', '$sanitize', 'KeyService',
function(UtilService, $sanitize, KeyService) {
var triggerService = {};
var triggerTypes = {
@ -639,10 +640,29 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
'type': 'option',
'name': 'branch_name'
}
]
],
'get_redirect_url': function(namespace, repository) {
var redirect_uri = KeyService['githubRedirectUri'] + '/trigger/' +
namespace + '/' + repository;
var authorize_url = KeyService['githubTriggerAuthorizeUrl'];
var client_id = KeyService['githubTriggerClientId'];
return authorize_url + 'client_id=' + client_id +
'&scope=repo,user:email&redirect_uri=' + redirect_uri;
}
}
}
triggerService.getRedirectUrl = function(name, namespace, repository) {
var type = triggerTypes[name];
if (!type) {
return '';
}
return type['get_redirect_url'](namespace, repository);
};
triggerService.getDescription = function(name, config) {
var type = triggerTypes[name];
if (!type) {
@ -1693,21 +1713,29 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
$provide.factory('KeyService', ['$location', 'Config', function($location, Config) {
var keyService = {}
var oauth = window.__oauth;
keyService['stripePublishableKey'] = Config['STRIPE_PUBLISHABLE_KEY'];
keyService['githubClientId'] = Config['GITHUB_CLIENT_ID'];
keyService['githubLoginClientId'] = Config['GITHUB_LOGIN_CLIENT_ID'];
keyService['githubRedirectUri'] = Config.getUrl('/oauth2/github/callback');
keyService['githubTriggerClientId'] = oauth['GITHUB_TRIGGER_CONFIG']['CLIENT_ID'];
keyService['githubLoginClientId'] = oauth['GITHUB_LOGIN_CONFIG']['CLIENT_ID'];
keyService['googleLoginClientId'] = oauth['GOOGLE_LOGIN_CONFIG']['CLIENT_ID'];
keyService['googleLoginClientId'] = Config['GOOGLE_LOGIN_CLIENT_ID'];
keyService['githubRedirectUri'] = Config.getUrl('/oauth2/github/callback');
keyService['googleRedirectUri'] = Config.getUrl('/oauth2/google/callback');
keyService['googleLoginUrl'] = 'https://accounts.google.com/o/oauth2/auth?response_type=code&';
keyService['githubLoginUrl'] = Config['GITHUB_LOGIN_URL'] + '?';
keyService['githubLoginUrl'] = oauth['GITHUB_LOGIN_CONFIG']['AUTHORIZE_ENDPOINT'];
keyService['googleLoginUrl'] = oauth['GOOGLE_LOGIN_CONFIG']['AUTHORIZE_ENDPOINT'];
keyService['githubTriggerAuthorizeUrl'] = oauth['GITHUB_LOGIN_CONFIG']['AUTHORIZE_ENDPOINT'];
keyService['googleLoginScope'] = 'openid email';
keyService['githubLoginScope'] = 'user:email';
keyService['googleLoginScope'] = 'openid email';
keyService.isEnterprise = function(service) {
var isGithubEnterprise = keyService['githubLoginUrl'].indexOf('https://github.com/') < 0;
return service == 'github' && isGithubEnterprise;
};
keyService.getExternalLoginUrl = function(service, action) {
var state_clause = '';
@ -2688,6 +2716,8 @@ quayApp.directive('externalLoginButton', function () {
},
controller: function($scope, $timeout, $interval, ApiService, KeyService, CookieService, Features, Config) {
$scope.signingIn = false;
$scope.isEnterprise = KeyService.isEnterprise;
$scope.startSignin = function(service) {
$scope.signInStarted({'service': service});

View file

@ -1330,15 +1330,13 @@ function RepoAdminCtrl($scope, Restangular, ApiService, KeyService, TriggerServi
var name = $routeParams.name;
$scope.Features = Features;
$scope.TriggerService = TriggerService;
$scope.permissions = {'team': [], 'user': [], 'loading': 2};
$scope.logsShown = 0;
$scope.deleting = false;
$scope.permissionCache = {};
$scope.githubRedirectUri = KeyService.githubRedirectUri;
$scope.githubClientId = KeyService.githubClientId;
$scope.showTriggerSetupCounter = 0;
$scope.getBadgeFormat = function(format, repo) {
@ -2030,12 +2028,10 @@ function V1Ctrl($scope, $location, UserService) {
UserService.updateUserIn($scope);
}
function NewRepoCtrl($scope, $location, $http, $timeout, UserService, ApiService, PlanService, KeyService, Features) {
function NewRepoCtrl($scope, $location, $http, $timeout, UserService, ApiService, PlanService, TriggerService, Features) {
UserService.updateUserIn($scope);
$scope.Features = Features;
$scope.githubRedirectUri = KeyService.githubRedirectUri;
$scope.githubClientId = KeyService.githubClientId;
$scope.repo = {
'is_public': 0,
@ -2114,9 +2110,7 @@ function NewRepoCtrl($scope, $location, $http, $timeout, UserService, ApiService
// Conduct the Github redirect if applicable.
if ($scope.repo.initialize == 'github') {
window.location = 'https://github.com/login/oauth/authorize?client_id=' + $scope.githubClientId +
'&scope=repo,user:email&redirect_uri=' + $scope.githubRedirectUri + '/trigger/' +
repo.namespace + '/' + repo.name;
window.location = TriggerService.getRedirectUrl('github', repo.namespace, repo.name);
return;
}

View file

@ -306,7 +306,11 @@
<b class="caret"></b>
</button>
<ul class="dropdown-menu dropdown-menu-right pull-right">
<li><a href="https://github.com/login/oauth/authorize?client_id={{ githubClientId }}&scope=repo,user:email&redirect_uri={{ githubRedirectUri }}/trigger/{{ repo.namespace }}/{{ repo.name }}"><i class="fa fa-github fa-lg"></i>GitHub - Repository Push</a></li>
<li>
<a href="{{ TriggerService.getRedirectUrl('github', repo.namespace, repo.name) }}">
<i class="fa fa-github fa-lg"></i>GitHub - Repository Push
</a>
</li>
</ul>
</div>
</div>

View file

@ -44,6 +44,7 @@
window.__endpoints = {{ route_data|safe }}.apis;
window.__features = {{ feature_set|safe }};
window.__config = {{ config_set|safe }};
window.__oauth = {{ oauth_set|safe }};
window.__token = '{{ csrf_token() }}';
</script>

81
util/oauth.py Normal file
View file

@ -0,0 +1,81 @@
import urlparse
class OAuthConfig(object):
def __init__(self, app, key_name):
self.key_name = key_name
self.config = app.config.get(key_name, {})
def service_name(self):
raise NotImplementedError
def token_endpoint(self):
raise NotImplementedError
def user_endpoint(self):
raise NotImplementedError
def login_endpoint(self):
raise NotImplementedError
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):
if not endpoint:
raise Exception('Missing endpoint configuration for OAuth config %s', self.key_name)
for arg in args:
endpoint = urlparse.urljoin(endpoint, arg)
return endpoint
class GithubOAuthConfig(OAuthConfig):
def __init__(self, app, key_name):
super(GithubOAuthConfig, self).__init__(app, key_name)
def service_name(self):
return 'GitHub'
def authorize_endpoint(self):
endpoint = self.config.get('GITHUB_ENDPOINT')
return self._get_url(endpoint, '/login/oauth/authorize') + '?'
def token_endpoint(self):
endpoint = self.config.get('GITHUB_ENDPOINT')
return self._get_url(endpoint, '/login/oauth/access_token')
def _api_endpoint(self):
endpoint = self.config.get('GITHUB_ENDPOINT')
return self.config.get('API_ENDPOINT', self._get_url(endpoint, '/api/v3/'))
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')
class GoogleOAuthConfig(OAuthConfig):
def __init__(self, app, key_name):
super(GoogleOAuthConfig, self).__init__(app, key_name)
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'