From 4ca5d9b04bea4a4403abd4ade25b37825e1bc417 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Tue, 3 Mar 2015 19:58:42 -0500 Subject: [PATCH] Add support for filtering github login by org --- endpoints/callbacks.py | 26 ++++++++++++++++--- static/css/core-ui.css | 9 +++++++ .../directives/config/config-setup-tool.html | 22 ++++++++++++++++ static/js/controllers/setup.js | 2 +- static/js/services/key-service.js | 2 +- util/config/validator.py | 8 ++++++ util/oauth.py | 22 ++++++++++++++++ 7 files changed, 86 insertions(+), 5 deletions(-) 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(),