diff --git a/data/users.py b/data/users.py index 91b72785c..b2cdfabd5 100644 --- a/data/users.py +++ b/data/users.py @@ -54,10 +54,8 @@ class JWTAuthUsers(object): """ Delegates authentication to a REST endpoint that returns JWTs. """ PUBLIC_KEY_FILENAME = 'jwt-authn.cert' - def __init__(self, exists_url, verify_url, issuer, override_config_dir, http_client, - public_key_path=None): + def __init__(self, verify_url, issuer, override_config_dir, http_client, public_key_path=None): self.verify_url = verify_url - self.exists_url = exists_url self.issuer = issuer self.client = http_client @@ -109,13 +107,6 @@ class JWTAuthUsers(object): # Parse out the username and email. return _get_federated_user(payload['sub'], payload['email'], 'jwtauthn', create_new_user) - def user_exists(self, username): - result = self.client.get(self.exists_url, auth=(username, ''), timeout=2) - if result.status_code / 500 >= 1: - raise Exception('Internal Error when trying to check if user exists: %s' % result.text) - - return result.status_code == 200 - def confirm_existing_user(self, username, password): db_user = model.get_user(username) if not db_user: @@ -140,9 +131,6 @@ class DatabaseUsers(object): def confirm_existing_user(self, username, password): return self.verify_user(username, password) - def user_exists(self, username): - return model.get_user(username) is not None - class LDAPConnection(object): def __init__(self, ldap_uri, user_dn, user_pw): @@ -299,10 +287,6 @@ class LDAPUsers(object): email = found_response[self._email_attr][0] return _get_federated_user(username, email, 'ldap', create_new_user) - def user_exists(self, username): - found_user = self._ldap_user_search(username) - return found_user is not None - class UserAuthentication(object): @@ -333,10 +317,8 @@ class UserAuthentication(object): users = LDAPUsers(ldap_uri, base_dn, admin_dn, admin_passwd, user_rdn, uid_attr, email_attr) elif authentication_type == 'JWT': verify_url = app.config.get('JWT_VERIFY_ENDPOINT') - exists_url = app.config.get('JWT_EXISTS_ENDPOINT') issuer = app.config.get('JWT_AUTH_ISSUER') - users = JWTAuthUsers(exists_url, verify_url, issuer, override_config_dir, - app.config['HTTPCLIENT']) + users = JWTAuthUsers(verify_url, issuer, override_config_dir, app.config['HTTPCLIENT']) else: raise RuntimeError('Unknown authentication type: %s' % authentication_type) diff --git a/endpoints/api/suconfig.py b/endpoints/api/suconfig.py index 4802dc99d..b2fa941cd 100644 --- a/endpoints/api/suconfig.py +++ b/endpoints/api/suconfig.py @@ -343,6 +343,10 @@ class SuperUserConfigValidate(ApiResource): 'properties': { 'config': { 'type': 'object' + }, + 'password': { + 'type': 'string', + 'description': 'The users password, used for auth validation' } }, }, @@ -358,6 +362,6 @@ class SuperUserConfigValidate(ApiResource): # this is also safe since this method does not access any information not given in the request. if not CONFIG_PROVIDER.yaml_exists() or SuperUserPermission().can(): config = request.get_json()['config'] - return validate_service_for_config(service, config) + return validate_service_for_config(service, config, request.get_json().get('password', '')) abort(403) \ No newline at end of file diff --git a/static/directives/config/config-setup-tool.html b/static/directives/config/config-setup-tool.html index 31a05706b..b1c0c7da4 100644 --- a/static/directives/config/config-setup-tool.html +++ b/static/directives/config/config-setup-tool.html @@ -383,20 +383,6 @@ - - User Exists Endpoint: - - -
- The URL (starting with http or https) on the JWT authentication server for checking whether a username exists. -
- -
- The username will be sent in the Authorization header as Basic Auth, and this endpoint should return 200 OK on success (or a 4** otherwise). -
- - Authentication Issuer: diff --git a/static/js/core-config-setup.js b/static/js/core-config-setup.js index 1d9cf852d..0679e3759 100644 --- a/static/js/core-config-setup.js +++ b/static/js/core-config-setup.js @@ -25,11 +25,11 @@ angular.module("core-config-setup", ['angularFileUpload']) {'id': 'ldap', 'title': 'LDAP Authentication', 'condition': function(config) { return config.AUTHENTICATION_TYPE == 'LDAP'; - }}, + }, 'password': true}, {'id': 'jwt', 'title': 'JWT Authentication', 'condition': function(config) { return config.AUTHENTICATION_TYPE == 'JWT'; - }}, + }, 'password': true}, {'id': 'mail', 'title': 'E-mail Support', 'condition': function(config) { return config.FEATURE_MAILING; @@ -153,12 +153,17 @@ angular.module("core-config-setup", ['angularFileUpload']) $scope.savingConfiguration = false; }; - $scope.validateService = function(serviceInfo) { + $scope.validateService = function(serviceInfo, opt_password) { var params = { 'service': serviceInfo.service.id }; - ApiService.scValidateConfig({'config': $scope.config}, params).then(function(resp) { + var data = { + 'config': $scope.config, + 'password': opt_password || '' + }; + + ApiService.scValidateConfig(data, params).then(function(resp) { serviceInfo.status = resp.status ? 'success' : 'error'; serviceInfo.errorMessage = $.trim(resp.reason || ''); }, ApiService.errorDisplay('Could not validate configuration. Please report this error.')); @@ -175,6 +180,57 @@ angular.module("core-config-setup", ['angularFileUpload']) }; $scope.validateAndSave = function() { + $scope.validating = $scope.getServices($scope.config); + + var requirePassword = false; + for (var i = 0; i < $scope.validating.length; ++i) { + var serviceInfo = $scope.validating[i]; + if (serviceInfo.service.password) { + requirePassword = true; + break; + } + } + + if (!requirePassword) { + $scope.performValidateAndSave(); + return; + } + + var box = bootbox.dialog({ + "message": 'Please enter your superuser password to validate your auth configuration:' + + '
' + + '' + + '
', + "title": 'Enter Password', + "buttons": { + "verify": { + "label": "Validate Config", + "className": "btn-success", + "callback": function() { + $scope.performValidateAndSave($('#validatePassword').val()); + } + }, + "close": { + "label": "Cancel", + "className": "btn-default", + "callback": function() { + } + } + } + }); + + box.bind('shown.bs.modal', function(){ + box.find("input").focus(); + box.find("form").submit(function() { + if (!$('#validatePassword').val()) { return; } + + box.modal('hide'); + verifyNow(); + }); + }); + }; + + $scope.performValidateAndSave = function(opt_password) { $scope.savingConfiguration = false; $scope.validating = $scope.getServices($scope.config); @@ -185,7 +241,7 @@ angular.module("core-config-setup", ['angularFileUpload']) for (var i = 0; i < $scope.validating.length; ++i) { var serviceInfo = $scope.validating[i]; - $scope.validateService(serviceInfo); + $scope.validateService(serviceInfo, opt_password); } }; diff --git a/test/test_jwt_auth.py b/test/test_jwt_auth.py index 1cd17a9cf..1cf6c845d 100644 --- a/test/test_jwt_auth.py +++ b/test/test_jwt_auth.py @@ -42,15 +42,6 @@ class JWTAuthTestCase(LiveServerTestCase): data = base64.b64decode(request.headers['Authorization'][len('Basic '):]) return data.split(':', 1) - @jwt_app.route('/user/exists', methods=['GET']) - def user_exists(): - username, _ = _get_basic_auth() - for user in users: - if user['name'] == username or user['email'] == username: - return 'OK' - - abort(404) - @jwt_app.route('/user/verify', methods=['GET']) def verify_user(): username, password = _get_basic_auth() @@ -92,7 +83,6 @@ class JWTAuthTestCase(LiveServerTestCase): self.session = requests.Session() self.jwt_auth = JWTAuthUsers( - self.get_server_url() + '/user/exists', self.get_server_url() + '/user/verify', 'authy', '', app.config['HTTPCLIENT'], JWTAuthTestCase.public_key.name) @@ -101,13 +91,6 @@ class JWTAuthTestCase(LiveServerTestCase): finished_database_for_testing(self) self.ctx.__exit__(True, None, None) - def test_user_exists(self): - self.assertFalse(self.jwt_auth.user_exists('testuser')) - self.assertFalse(self.jwt_auth.user_exists('anotheruser')) - - self.assertTrue(self.jwt_auth.user_exists('cooluser')) - self.assertTrue(self.jwt_auth.user_exists('user@domain.com')) - def test_verify_user(self): result, error_message = self.jwt_auth.verify_user('invaliduser', 'foobar') self.assertEquals('Invalid username or password', error_message) diff --git a/util/config/validator.py b/util/config/validator.py index 2829a5733..1fc5b2b62 100644 --- a/util/config/validator.py +++ b/util/config/validator.py @@ -7,7 +7,7 @@ import OpenSSL import logging from fnmatch import fnmatch -from data.users import LDAPConnection, JWTAuthUsers +from data.users import LDAPConnection, JWTAuthUsers, LDAPUsers from flask import Flask from flask.ext.mail import Mail, Message from data.database import validate_database_url, User @@ -31,7 +31,7 @@ def get_storage_provider(config): except TypeError: raise Exception('Missing required storage configuration parameter(s)') -def validate_service_for_config(service, config): +def validate_service_for_config(service, config, password=None): """ Attempts to validate the configuration for the given service. """ if not service in _VALIDATORS: return { @@ -39,7 +39,7 @@ def validate_service_for_config(service, config): } try: - _VALIDATORS[service](config) + _VALIDATORS[service](config, password) return { 'status': True } @@ -51,7 +51,7 @@ def validate_service_for_config(service, config): } -def _validate_database(config): +def _validate_database(config, _): """ Validates connecting to the database. """ try: validate_database_url(config['DB_URI']) @@ -62,7 +62,7 @@ def _validate_database(config): raise ex -def _validate_redis(config): +def _validate_redis(config, _): """ Validates connecting to redis. """ redis_config = config.get('BUILDLOGS_REDIS', {}) if not 'host' in redis_config: @@ -72,7 +72,7 @@ def _validate_redis(config): client.ping() -def _validate_registry_storage(config): +def _validate_registry_storage(config, _): """ Validates registry storage. """ driver = get_storage_provider(config) @@ -87,7 +87,7 @@ def _validate_registry_storage(config): raise Exception('Could not prepare storage: %s' % str(ex)) -def _validate_mailing(config): +def _validate_mailing(config, _): """ Validates sending email. """ test_app = Flask("mail-test-app") test_app.config.update(config) @@ -103,7 +103,7 @@ def _validate_mailing(config): test_mail.send(test_msg) -def _validate_gitlab(config): +def _validate_gitlab(config, _): """ Validates the OAuth credentials and API endpoint for a GitLab service. """ github_config = config.get('GITLAB_TRIGGER_CONFIG') if not github_config: @@ -130,7 +130,7 @@ def _validate_gitlab(config): def _validate_github(config_key): - return lambda config: _validate_github_with_key(config_key, config) + return lambda config, _: _validate_github_with_key(config_key, config) def _validate_github_with_key(config_key, config): @@ -167,7 +167,7 @@ def _validate_github_with_key(config_key, config): raise Exception('Invalid organization: %s' % org_id) -def _validate_bitbucket(config): +def _validate_bitbucket(config, _): """ Validates the config for BitBucket. """ trigger_config = config.get('BITBUCKET_TRIGGER_CONFIG') if not trigger_config: @@ -189,7 +189,7 @@ def _validate_bitbucket(config): raise Exception('Invaid consumer key or secret') -def _validate_google_login(config): +def _validate_google_login(config, _): """ Validates the Google Login client ID and secret. """ google_login_config = config.get('GOOGLE_LOGIN_CONFIG') if not google_login_config: @@ -208,7 +208,7 @@ def _validate_google_login(config): raise Exception('Invalid client id or client secret') -def _validate_ssl(config): +def _validate_ssl(config, _): """ Validates the SSL configuration (if enabled). """ if config.get('PREFERRED_URL_SCHEME', 'http') != 'https': return @@ -276,7 +276,7 @@ def _validate_ssl(config): -def _validate_ldap(config): +def _validate_ldap(config, password): """ Validates the LDAP connection. """ if config.get('AUTHENTICATION_TYPE', 'Database') != 'LDAP': return @@ -305,37 +305,47 @@ def _validate_ldap(config): raise Exception(values.get('desc', 'Unknown error')) + # Verify that the superuser exists. If not, raise an exception. + base_dn = config.get('LDAP_BASE_DN') + user_rdn = config.get('LDAP_USER_RDN', []) + uid_attr = config.get('LDAP_UID_ATTR', 'uid') + email_attr = config.get('LDAP_EMAIL_ATTR', 'mail') -def _validate_jwt(config): + users = LDAPUsers(ldap_uri, base_dn, admin_dn, admin_passwd, user_rdn, uid_attr, email_attr) + + username = get_authenticated_user().username + (result, err_msg) = users.verify_user(username, password) + if not result: + raise Exception(('Verification of superuser %s failed: %s. \n\nThe user either does not exist ' + + 'in the remote authentication system ' + + 'OR LDAP auth is misconfigured.') % (username, err_msg)) + + +def _validate_jwt(config, password): """ Validates the JWT authentication system. """ if config.get('AUTHENTICATION_TYPE', 'Database') != 'JWT': return verify_endpoint = config.get('JWT_VERIFY_ENDPOINT') - exists_endpoint = config.get('JWT_EXISTS_ENDPOINT') issuer = config.get('JWT_AUTH_ISSUER') if not verify_endpoint: raise Exception('Missing JWT Verification endpoint') - if not exists_endpoint: - raise Exception('Missing JWT Exists endpoint') - if not issuer: raise Exception('Missing JWT Issuer ID') # Try to instatiate the JWT authentication mechanism. This will raise an exception if # the key cannot be found. - users = JWTAuthUsers(exists_endpoint, verify_endpoint, issuer, - OVERRIDE_CONFIG_DIRECTORY, - app.config['HTTPCLIENT']) + users = JWTAuthUsers(verify_endpoint, issuer, OVERRIDE_CONFIG_DIRECTORY, app.config['HTTPCLIENT']) # Verify that the superuser exists. If not, raise an exception. username = get_authenticated_user().username - result = users.user_exists(username) + (result, err_msg) = users.verify_user(username, password) if not result: - raise Exception(('Verification of superuser %s failed. The user either does not exist ' + - 'in the remote authentication system OR JWT auth is misconfigured.') % username) + raise Exception(('Verification of superuser %s failed: %s. \n\nThe user either does not ' + + 'exist in the remote authentication system ' + + 'OR JWT auth is misconfigured.') % (username, err_msg)) _VALIDATORS = {