diff --git a/keystone/application_credential/core.py b/keystone/application_credential/core.py index 37b7fb6b65..1b794ec5cd 100644 --- a/keystone/application_credential/core.py +++ b/keystone/application_credential/core.py @@ -91,6 +91,14 @@ class Manager(manager.Manager): actor_id=user_id, target_id=project_id) + def _assert_limit_not_exceeded(self, user_id): + user_limit = CONF.application_credential.user_limit + if user_limit >= 0: + app_cred_count = len(self.list_application_credentials(user_id)) + if app_cred_count >= user_limit: + raise exception.ApplicationCredentialLimitExceeded( + limit=user_limit) + def _get_role_list(self, app_cred_roles): roles = [] for role in app_cred_roles: @@ -126,6 +134,8 @@ class Manager(manager.Manager): user_id = application_credential['user_id'] project_id = application_credential['project_id'] roles = application_credential.pop('roles', []) + + self._assert_limit_not_exceeded(user_id) self._require_user_has_role_in_project(roles, user_id, project_id) unhashed_secret = application_credential['secret'] ref = self.driver.create_application_credential( diff --git a/keystone/conf/application_credential.py b/keystone/conf/application_credential.py index e78d11812d..9881e7b073 100644 --- a/keystone/conf/application_credential.py +++ b/keystone/conf/application_credential.py @@ -40,12 +40,23 @@ Time to cache application credential data in seconds. This has no effect unless global caching is enabled. """)) +user_limit = cfg.IntOpt( + 'user_limit', + default=-1, + help=utils.fmt(""" +Maximum number of application credentials a user is permitted to create. A +value of -1 means unlimited. If a limit is not set, users are permitted to +create application credentials at will, which could lead to bloat in the +keystone database or open keystone to a DoS attack. +""")) + GROUP_NAME = __name__.split('.')[-1] ALL_OPTS = [ driver, caching, cache_time, + user_limit, ] diff --git a/keystone/exception.py b/keystone/exception.py index 57c3596610..0353f3f04a 100644 --- a/keystone/exception.py +++ b/keystone/exception.py @@ -205,6 +205,11 @@ class RegionDeletionError(ForbiddenNotSecurity): "its child regions have associated endpoints.") +class ApplicationCredentialLimitExceeded(ForbiddenNotSecurity): + message_format = _("Unable to create additional application credentials, " + "maximum of %(limit)d already exceeded for user.") + + class SecurityError(Error): """Security error exception. diff --git a/keystone/tests/unit/application_credential/test_backends.py b/keystone/tests/unit/application_credential/test_backends.py index b9a5e4d6b8..5b61dba4e1 100644 --- a/keystone/tests/unit/application_credential/test_backends.py +++ b/keystone/tests/unit/application_credential/test_backends.py @@ -13,6 +13,8 @@ import datetime import uuid +from oslo_config import fixture as config_fixture + from keystone.common import driver_hints from keystone.common import provider_api import keystone.conf @@ -92,6 +94,19 @@ class ApplicationCredentialTests(object): app_cred.pop('roles') self.assertDictEqual(app_cred, resp) + def test_application_credential_limits(self): + config_fixture_ = self.user = self.useFixture(config_fixture.Config()) + config_fixture_.config(group='application_credential', user_limit=2) + app_cred = self._new_app_cred_data(self.user_foo['id'], + self.tenant_bar['id']) + self.app_cred_api.create_application_credential(app_cred) + app_cred['name'] = 'two' + self.app_cred_api.create_application_credential(app_cred) + app_cred['name'] = 'three' + self.assertRaises(exception.ApplicationCredentialLimitExceeded, + self.app_cred_api.create_application_credential, + app_cred) + def test_get_application_credential(self): app_cred = self._new_app_cred_data(self.user_foo['id'], project_id=self.tenant_bar['id'])