diff --git a/config_app/config_util/config/TransientDirectoryProvider.py b/config_app/config_util/config/TransientDirectoryProvider.py index 3737ec732..9c96bedf6 100644 --- a/config_app/config_util/config/TransientDirectoryProvider.py +++ b/config_app/config_util/config/TransientDirectoryProvider.py @@ -24,6 +24,7 @@ class TransientDirectoryProvider(FileConfigProvider): """ Update the path with a new temporary directory, deleting the old one in the process """ + self.temp_dir.cleanup() temp_dir = TemporaryDirectory() self.config_volume = temp_dir.name diff --git a/config_app/js/core-config-setup/core-config-setup.js b/config_app/js/core-config-setup/core-config-setup.js index 73841712a..425c2a407 100644 --- a/config_app/js/core-config-setup/core-config-setup.js +++ b/config_app/js/core-config-setup/core-config-setup.js @@ -51,15 +51,15 @@ angular.module("quay-config") {'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': 'keystone', 'title': 'Keystone Authentication', 'condition': function(config) { return config.AUTHENTICATION_TYPE == 'Keystone'; - }, 'password': true}, + }}, {'id': 'apptoken-auth', 'title': 'App Token Authentication', 'condition': function(config) { return config.AUTHENTICATION_TYPE == 'AppToken'; @@ -345,50 +345,7 @@ angular.module("quay-config") $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": { - "success": { - "label": "Validate Config", - "className": "btn-success btn-continue", - "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'); - }); - }); + $scope.performValidateAndSave(); }; $scope.performValidateAndSave = function(opt_password) { diff --git a/config_app/static/css/config-setup-app-component.css b/config_app/static/css/config-setup-app-component.css index 453a08019..a70bc0ab1 100644 --- a/config_app/static/css/config-setup-app-component.css +++ b/config_app/static/css/config-setup-app-component.css @@ -29,8 +29,10 @@ /* Fixes the transition to font awesome 5 */ .quay-config-app .co-alert.co-alert-warning::before { font-family: Font Awesome\ 5 Free; + font-weight: 900; } .quay-config-app .co-alert.co-alert-info::before { font-family: Font Awesome\ 5 Free; + font-weight: 900; } diff --git a/data/users/externaljwt.py b/data/users/externaljwt.py index 64960a08b..da4d003db 100644 --- a/data/users/externaljwt.py +++ b/data/users/externaljwt.py @@ -39,6 +39,7 @@ class ExternalJWTAuthN(FederatedUsers): def ping(self): result = self.client.get(self.getuser_url, timeout=2) + # We expect a 401 or 403 of some kind, since we explicitly don't send an auth header if result.status_code // 100 != 4: return (False, result.text or 'Could not reach JWT authn endpoint') diff --git a/data/users/keystone.py b/data/users/keystone.py index d6fa3c628..21a7e6ec5 100644 --- a/data/users/keystone.py +++ b/data/users/keystone.py @@ -51,6 +51,24 @@ class KeystoneV2Users(FederatedUsers): return (True, None) + def at_least_one_user_exists(self): + logger.debug('Checking if any users exist in Keystone') + try: + keystone_client = kclient.Client(username=self.admin_username, password=self.admin_password, + tenant_name=self.admin_tenant, auth_url=self.auth_url, + timeout=self.timeout, debug=self.debug) + + user_list = keystone_client.users.list(tenant_id=self.admin_tenant, limit=1) + if len(user_list) < 1: + return (False, None) + + return (True, None) + except Exception as e: + # Catch exceptions to give the user our custom error message + logger.exception('Unable to list users in Keystone') + return (False, e.message) + + def verify_credentials(self, username_or_email, password): try: keystone_client = kclient.Client(username=username_or_email, password=password, @@ -116,6 +134,19 @@ class KeystoneV3Users(FederatedUsers): return (True, None) + def at_least_one_user_exists(self): + logger.debug('Checking if any users exist in admin tenant in Keystone') + try: + user_list = self._get_admin_client().users.list(self.admin_tenant, limit=1) + if len(user_list) < 1: + return (False, 'No users found in tenant: %s' % self.admin_tenant) + + return (True, None) + except Exception as e: + # Catch exceptions to give the user our custom error message + logger.exception('Unable to list users in Keystone') + return (False, e.message) + def verify_credentials(self, username_or_email, password): try: keystone_client = kv3client.Client(username=username_or_email, password=password, diff --git a/test/test_keystone_auth.py b/test/test_keystone_auth.py index 4d70a4c1b..099d0e64b 100644 --- a/test/test_keystone_auth.py +++ b/test/test_keystone_auth.py @@ -83,6 +83,11 @@ def _create_app(requires_email=True): abort(404) + # v2 referred to all groups as tenants, so replace occurrences of 'group' with 'tenant' + @ks_app.route('/v2.0/admin/tenants//users', methods=['GET']) + def getv2_tenant_members(tenant): + return getv3groupmembers(tenant) + @ks_app.route('/v3/identity/groups//users', methods=['GET']) def getv3groupmembers(groupid): for group in groups: diff --git a/util/config/validators/test/test_validate_keystone.py b/util/config/validators/test/test_validate_keystone.py index f6d8cf0c2..f3776a0b7 100644 --- a/util/config/validators/test/test_validate_keystone.py +++ b/util/config/validators/test/test_validate_keystone.py @@ -3,7 +3,6 @@ import pytest from util.config.validator import ValidatorContext from util.config.validators import ConfigValidationException from util.config.validators.validate_keystone import KeystoneValidator -from util.morecollections import AttrDict from test.test_keystone_auth import fake_keystone @@ -29,13 +28,13 @@ def test_invalid_config(unvalidated_config, app): KeystoneValidator.validate(ValidatorContext(unvalidated_config)) -@pytest.mark.parametrize('username, password, expected_exception', [ - ('invaliduser', 'invalidpass', ConfigValidationException), - ('cool.user', 'invalidpass', ConfigValidationException), - ('invaliduser', 'somepass', ConfigValidationException), - ('cool.user', 'password', None), +@pytest.mark.parametrize('admin_tenant_id, expected_exception', [ + ('somegroupid', None), + ('groupwithnousers', ConfigValidationException), + ('somegroupid', None), + ('groupwithnousers', ConfigValidationException), ]) -def test_validated_keystone(username, password, expected_exception, app): +def test_validated_keystone(admin_tenant_id, expected_exception, app): with fake_keystone(2) as keystone_auth: auth_url = keystone_auth.auth_url @@ -44,11 +43,9 @@ def test_validated_keystone(username, password, expected_exception, app): config['KEYSTONE_AUTH_URL'] = auth_url config['KEYSTONE_ADMIN_USERNAME'] = 'adminuser' config['KEYSTONE_ADMIN_PASSWORD'] = 'adminpass' - config['KEYSTONE_ADMIN_TENANT'] = 'admintenant' + config['KEYSTONE_ADMIN_TENANT'] = admin_tenant_id unvalidated_config = ValidatorContext(config) - unvalidated_config.user = AttrDict(dict(username=username)) - unvalidated_config.user_password = password if expected_exception is not None: with pytest.raises(ConfigValidationException): diff --git a/util/config/validators/validate_jwt.py b/util/config/validators/validate_jwt.py index c56f3630b..d7b5e36f5 100644 --- a/util/config/validators/validate_jwt.py +++ b/util/config/validators/validate_jwt.py @@ -9,8 +9,6 @@ class JWTAuthValidator(BaseValidator): def validate(cls, validator_context, public_key_path=None): """ Validates the JWT authentication system. """ config = validator_context.config - user = validator_context.user - user_password = validator_context.user_password http_client = validator_context.http_client jwt_auth_max = validator_context.jwt_auth_max config_provider = validator_context.config_provider @@ -31,10 +29,7 @@ class JWTAuthValidator(BaseValidator): raise ConfigValidationException('Missing JWT Issuer ID') - # TODO(jschorr): fix this - return - - override_config_directory = os.path.join(config_provider.get_config_root(), '../stack/') + override_config_directory = config_provider.get_config_dir_path() # Try to instatiate the JWT authentication mechanism. This will raise an exception if # the key cannot be found. @@ -45,31 +40,9 @@ class JWTAuthValidator(BaseValidator): public_key_path=public_key_path, requires_email=config.get('FEATURE_MAILING', True)) - # Verify that the superuser exists. If not, raise an exception. - username = user.username - (result, err_msg) = users.verify_credentials(username, user_password) + # Verify that we can reach the jwt server + (result, err_msg) = users.ping() if not result: - msg = ('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) + msg = ('Verification of JWT failed: %s. \n\nWe cannot reach the JWT server' + + 'OR JWT auth is misconfigured') % err_msg raise ConfigValidationException(msg) - - # If the query endpoint exists, ensure we can query to find the current user and that we can - # look up users directly. - if query_endpoint: - (results, _, err_msg) = users.query_users(username) - if not results: - err_msg = err_msg or ('Could not find users matching query: %s' % username) - raise ConfigValidationException('Query endpoint is misconfigured or not returning ' + - 'proper users: %s' % err_msg) - - # Make sure the get user endpoint is also configured. - if not getuser_endpoint: - raise ConfigValidationException('The lookup user endpoint must be configured if the ' + - 'query endpoint is set') - - (result, err_msg) = users.get_user(username) - if not result: - err_msg = err_msg or ('Could not find user %s' % username) - raise ConfigValidationException('Lookup endpoint is misconfigured or not returning ' + - 'properly: %s' % err_msg) diff --git a/util/config/validators/validate_keystone.py b/util/config/validators/validate_keystone.py index 2d4f7bbe0..c9dcc22c8 100644 --- a/util/config/validators/validate_keystone.py +++ b/util/config/validators/validate_keystone.py @@ -8,8 +8,6 @@ class KeystoneValidator(BaseValidator): def validate(cls, validator_context): """ Validates the Keystone authentication system. """ config = validator_context.config - user = validator_context.user - user_password = validator_context.user_password if config.get('AUTHENTICATION_TYPE', 'Database') != 'Keystone': return @@ -37,10 +35,10 @@ class KeystoneValidator(BaseValidator): requires_email) # Verify that the superuser exists. If not, raise an exception. - username = user.username - (result, err_msg) = users.verify_credentials(username, user_password) + (result, err_msg) = users.at_least_one_user_exists() if not result: - msg = ('Verification of superuser %s failed: %s \n\nThe user either does not ' + - 'exist in the remote authentication system ' + - 'OR Keystone auth is misconfigured.') % (username, err_msg) + msg = ('Verification that users exist failed: %s. \n\nNo users exist ' + + 'in the admin tenant/project ' + + 'in the remote authentication system ' + + 'OR Keystone auth is misconfigured.') % err_msg raise ConfigValidationException(msg)