diff --git a/endpoints/callbacks.py b/endpoints/callbacks.py
index a8bb05dbe..f381074ad 100644
--- a/endpoints/callbacks.py
+++ b/endpoints/callbacks.py
@@ -157,7 +157,10 @@ def github_oauth_callback():
if error:
return render_ologin_error('GitHub', error)
+ # Exchange the OAuth code.
token = exchange_code_for_token(request.args.get('code'), github_login)
+
+ # Retrieve the user's information.
user_data = get_user(github_login, token)
if not user_data or not 'login' in user_data:
return render_ologin_error('GitHub')
@@ -172,16 +175,33 @@ def github_oauth_callback():
token_param = {
'access_token': token,
}
+
+ # Retrieve the user's orgnizations (if organization filtering is turned on)
+ if github_login.allowed_organizations() is not None:
+ get_orgs = client.get(github_login.orgs_endpoint(), params=token_param,
+ headers={'Accept': 'application/vnd.github.moondragon+json'})
+
+ organizations = set([org.get('login') for org in get_orgs.json()])
+ if not (organizations & set(github_login.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."""
+ return render_ologin_error('GitHub', err)
+
+ # Find the e-mail address for the user: we will accept any email, but we prefer the primary
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
found_email = None
for user_email in get_email.json():
- found_email = user_email['email']
- if user_email['primary']:
+ if not user_email['primary'] or not user_email['verified']:
break
+ found_email = user_email['email']
+
+ if found_email is None:
+ err = 'There is no verified e-mail address attached to the GitHub account.'
+ return render_ologin_error('GitHub', err)
+
metadata = {
'service_username': username
}
diff --git a/static/css/core-ui.css b/static/css/core-ui.css
index 2012129c1..218b3b72d 100644
--- a/static/css/core-ui.css
+++ b/static/css/core-ui.css
@@ -262,6 +262,15 @@
display: block;
}
+.config-list-field-element input {
+ vertical-align: middle;
+}
+
+.config-list-field-element .item-delete {
+ display: inline-block;
+ margin-left: 20px;
+}
+
.config-list-field-element input {
width: 350px;
}
diff --git a/static/directives/config/config-setup-tool.html b/static/directives/config/config-setup-tool.html
index 2ed51dd5c..b769797fa 100644
--- a/static/directives/config/config-setup-tool.html
+++ b/static/directives/config/config-setup-tool.html
@@ -400,6 +400,28 @@
+
+
Organization Filtering:
+
+
+
+
+
+
+
+ If enabled, only members of specified GitHub
+ Enterprise organizations will be allowed to login via GitHub
+ Enterprise.
+
+
+
+
+
+
diff --git a/static/js/controllers/setup.js b/static/js/controllers/setup.js
index 9dc76a17f..8bebad19f 100644
--- a/static/js/controllers/setup.js
+++ b/static/js/controllers/setup.js
@@ -124,7 +124,7 @@ function SetupCtrl($scope, $timeout, ApiService, Features, UserService, Containe
$scope.showSuperuserPanel = function() {
$('#setupModal').modal('hide');
var prefix = $scope.hasSSL ? 'https' : 'http';
- var hostname = $scope.hostname;
+ var hostname = $scope.hostname || document.location.hostname;
window.location = prefix + '://' + hostname + '/superuser';
};
diff --git a/static/js/services/key-service.js b/static/js/services/key-service.js
index 1ab153635..1c419b25e 100644
--- a/static/js/services/key-service.js
+++ b/static/js/services/key-service.js
@@ -23,7 +23,7 @@ angular.module('quay').factory('KeyService', ['$location', 'Config', function($l
keyService['githubTriggerEndpoint'] = oauth['GITHUB_TRIGGER_CONFIG']['GITHUB_ENDPOINT'];
keyService['githubTriggerAuthorizeUrl'] = oauth['GITHUB_TRIGGER_CONFIG']['AUTHORIZE_ENDPOINT'];
- keyService['githubLoginScope'] = 'user:email';
+ keyService['githubLoginScope'] = 'user:email,read:org';
keyService['googleLoginScope'] = 'openid email';
keyService.isEnterprise = function(service) {
diff --git a/util/config/validator.py b/util/config/validator.py
index 271ce678e..d27bf2106 100644
--- a/util/config/validator.py
+++ b/util/config/validator.py
@@ -122,12 +122,20 @@ def _validate_github_with_key(config_key, config):
if not github_config.get('CLIENT_SECRET'):
raise Exception('Missing Client Secret')
+ if github_config.get('ORG_RESTRICT') and not github_config.get('ALLOWED_ORGANIZATIONS'):
+ raise Exception('Organization restriction must have at least one allowed organization')
+
client = app.config['HTTPCLIENT']
oauth = GithubOAuthConfig(config, config_key)
result = oauth.validate_client_id_and_secret(client)
if not result:
raise Exception('Invalid client id or client secret')
+ if github_config.get('ALLOWED_ORGANIZATIONS'):
+ for org_id in github_config.get('ALLOWED_ORGANIZATIONS'):
+ if not oauth.validate_organization(org_id, client):
+ raise Exception('Invalid organization: %s' % org_id)
+
def _validate_google_login(config):
""" Validates the Google Login client ID and secret. """
diff --git a/util/oauth.py b/util/oauth.py
index ede8823aa..731cec81a 100644
--- a/util/oauth.py
+++ b/util/oauth.py
@@ -1,4 +1,5 @@
import urlparse
+import github
class OAuthConfig(object):
def __init__(self, config, key_name):
@@ -40,6 +41,12 @@ class GithubOAuthConfig(OAuthConfig):
def service_name(self):
return 'GitHub'
+ def allowed_organizations(self):
+ if not self.config.get('ORG_RESTRICT', False):
+ return None
+
+ return self.config.get('ALLOWED_ORGANIZATIONS', None)
+
def _endpoint(self):
endpoint = self.config.get('GITHUB_ENDPOINT', 'https://github.com')
if not endpoint.endswith('/'):
@@ -66,6 +73,10 @@ class GithubOAuthConfig(OAuthConfig):
api_endpoint = self._api_endpoint()
return self._get_url(api_endpoint, 'user/emails')
+ def orgs_endpoint(self):
+ api_endpoint = self._api_endpoint()
+ return self._get_url(api_endpoint, 'user/orgs')
+
def validate_client_id_and_secret(self, http_client):
# First: Verify that the github endpoint is actually Github by checking for the
# X-GitHub-Request-Id here.
@@ -91,6 +102,17 @@ class GithubOAuthConfig(OAuthConfig):
timeout=5)
return result.status_code == 404
+ def validate_organization(self, organization_id, http_client):
+ api_endpoint = self._api_endpoint()
+ org_endpoint = self._get_url(api_endpoint, 'orgs/%s' % organization_id)
+
+ 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(),