diff options
-rw-r--r-- | keystone/common/policies/token.py | 82 | ||||
-rw-r--r-- | keystone/tests/unit/protection/v3/test_tokens.py | 526 | ||||
-rw-r--r-- | lower-constraints.txt | 2 | ||||
-rw-r--r-- | releasenotes/notes/bug-1750676-cf70c1a27b2c8de3.yaml | 35 | ||||
-rw-r--r-- | requirements.txt | 2 |
5 files changed, 614 insertions, 33 deletions
diff --git a/keystone/common/policies/token.py b/keystone/common/policies/token.py index 49165f42e..6db9913ec 100644 --- a/keystone/common/policies/token.py +++ b/keystone/common/policies/token.py @@ -10,54 +10,74 @@ # License for the specific language governing permissions and limitations # under the License. +from oslo_log import versionutils from oslo_policy import policy from keystone.common.policies import base +DEPRECATED_REASON = """ +As of the Train release, the token API now understands how to handle +system-scoped tokens, making the API more accessible to users without +compromising security or manageability for administrators. This support +includes a read-only role by default. +""" + +deprecated_check_token = policy.DeprecatedRule( + name=base.IDENTITY % 'check_token', + check_str=base.RULE_ADMIN_OR_TOKEN_SUBJECT +) +deprecated_validate_token = policy.DeprecatedRule( + name=base.IDENTITY % 'validate_token', + check_str=base.RULE_SERVICE_ADMIN_OR_TOKEN_SUBJECT +) +deprecated_revoke_token = policy.DeprecatedRule( + name=base.IDENTITY % 'revoke_token', + check_str=base.RULE_ADMIN_OR_TOKEN_SUBJECT +) + +SYSTEM_ADMIN_OR_TOKEN_SUBJECT = ( + '(role:admin and system_scope:all) or rule:token_subject' # nosec +) +SYSTEM_USER_OR_TOKEN_SUBJECT = ( + '(role:reader and system_scope:all) or rule:token_subject' # nosec +) +SYSTEM_USER_OR_SERVICE_OR_TOKEN_SUBJECT = ( + '(role:reader and system_scope:all) ' # nosec + 'or rule:service_role or rule:token_subject' # nosec +) + + token_policies = [ policy.DocumentedRuleDefault( name=base.IDENTITY % 'check_token', - check_str=base.RULE_ADMIN_OR_TOKEN_SUBJECT, - # FIXME(lbragstad): Token validation should be handled within keystone, - # but it makes sense to have this be a system-level operation and a - # project-level operation. If this API is called by a system-level - # administrator, they should be able to check any token. If this API - # is called by a project administrator, then the token should be - # checked with respect to the project the administrator has a role on. - # Otherwise it would be possible for administrators in one project to - # validate tokens scoped to another project, which is a security - # concern. Note the following line should be uncommented once keystone - # supports the ability for project administrators to validate tokens - # only within their project. - # scope_types=['system', 'project'], + check_str=SYSTEM_USER_OR_TOKEN_SUBJECT, + scope_types=['system', 'domain', 'project'], description='Check a token.', operations=[{'path': '/v3/auth/tokens', - 'method': 'HEAD'}]), + 'method': 'HEAD'}], + deprecated_rule=deprecated_check_token, + deprecated_reason=DEPRECATED_REASON, + deprecated_since=versionutils.deprecated.TRAIN), policy.DocumentedRuleDefault( name=base.IDENTITY % 'validate_token', - check_str=base.RULE_SERVICE_ADMIN_OR_TOKEN_SUBJECT, - # FIXME(lbragstad): See the comment above about why this is commented - # out. If this weren't commented out and the `enforce_scope` were set - # to True, then users with project-scoped tokens would no longer be - # able to validate them by setting the same token as the X-Auth-Header - # and X-Subject-Token. - # scope_types=['system', 'project'], + check_str=SYSTEM_USER_OR_SERVICE_OR_TOKEN_SUBJECT, + scope_types=['system', 'domain', 'project'], description='Validate a token.', operations=[{'path': '/v3/auth/tokens', - 'method': 'GET'}]), + 'method': 'GET'}], + deprecated_rule=deprecated_validate_token, + deprecated_reason=DEPRECATED_REASON, + deprecated_since=versionutils.deprecated.TRAIN), policy.DocumentedRuleDefault( name=base.IDENTITY % 'revoke_token', - check_str=base.RULE_ADMIN_OR_TOKEN_SUBJECT, - # FIXME(lbragstad): System administrators should be able to revoke any - # valid token. Project administrators should only be able to invalidate - # tokens scoped to the project they administer. Users should be able to - # invalidate their own tokens. If we uncommented this line without - # adding support for each of these cases in code, we'd be breaking the - # ability for users to invalidate their own tokens. - # scope_types=['system', 'project'], + check_str=SYSTEM_ADMIN_OR_TOKEN_SUBJECT, + scope_types=['system', 'domain', 'project'], description='Revoke a token.', operations=[{'path': '/v3/auth/tokens', - 'method': 'DELETE'}]) + 'method': 'DELETE'}], + deprecated_rule=deprecated_revoke_token, + deprecated_reason=DEPRECATED_REASON, + deprecated_since=versionutils.deprecated.TRAIN) ] diff --git a/keystone/tests/unit/protection/v3/test_tokens.py b/keystone/tests/unit/protection/v3/test_tokens.py new file mode 100644 index 000000000..a4379838e --- /dev/null +++ b/keystone/tests/unit/protection/v3/test_tokens.py @@ -0,0 +1,526 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import uuid + +from six.moves import http_client + +from keystone.common import provider_api +import keystone.conf +from keystone.tests.common import auth as common_auth +from keystone.tests import unit +from keystone.tests.unit import base_classes +from keystone.tests.unit import ksfixtures + +CONF = keystone.conf.CONF +PROVIDERS = provider_api.ProviderAPIs + + +class _SystemUserTokenTests(object): + + def test_user_can_validate_system_scoped_token(self): + user = unit.new_user_ref(domain_id=CONF.identity.default_domain_id) + user['id'] = PROVIDERS.identity_api.create_user(user)['id'] + + PROVIDERS.assignment_api.create_system_grant_for_user( + user['id'], self.bootstrapper.reader_role_id + ) + + system_auth = self.build_authentication_request( + user_id=user['id'], password=user['password'], + system=True + ) + + with self.test_client() as c: + r = c.post('/v3/auth/tokens', json=system_auth) + system_token = r.headers['X-Subject-Token'] + + with self.test_client() as c: + self.headers['X-Subject-Token'] = system_token + c.get('/v3/auth/tokens', headers=self.headers) + + def test_user_can_validate_domain_scoped_token(self): + domain = PROVIDERS.resource_api.create_domain( + uuid.uuid4().hex, unit.new_domain_ref() + ) + + user = unit.new_user_ref(domain_id=domain['id']) + user['id'] = PROVIDERS.identity_api.create_user(user)['id'] + + PROVIDERS.assignment_api.create_grant( + self.bootstrapper.reader_role_id, user_id=user['id'], + domain_id=domain['id'] + ) + + domain_auth = self.build_authentication_request( + user_id=user['id'], password=user['password'], + domain_id=domain['id'] + ) + + with self.test_client() as c: + r = c.post('/v3/auth/tokens', json=domain_auth) + domain_token = r.headers['X-Subject-Token'] + + with self.test_client() as c: + self.headers['X-Subject-Token'] = domain_token + c.get('/v3/auth/tokens', headers=self.headers) + + def test_user_can_validate_project_scoped_token(self): + project = PROVIDERS.resource_api.create_project( + uuid.uuid4().hex, + unit.new_project_ref(domain_id=CONF.identity.default_domain_id) + ) + + user = unit.new_user_ref(domain_id=CONF.identity.default_domain_id) + user['id'] = PROVIDERS.identity_api.create_user(user)['id'] + + PROVIDERS.assignment_api.create_grant( + self.bootstrapper.reader_role_id, user_id=user['id'], + project_id=project['id'] + ) + + project_auth = self.build_authentication_request( + user_id=user['id'], password=user['password'], + project_id=project['id'] + ) + + with self.test_client() as c: + r = c.post('/v3/auth/tokens', json=project_auth) + project_token = r.headers['X-Subject-Token'] + + with self.test_client() as c: + self.headers['X-Subject-Token'] = project_token + c.get('/v3/auth/tokens', headers=self.headers) + + +class _SystemMemberAndReaderTokenTests(object): + + def test_user_cannot_revoke_a_system_scoped_token(self): + user = unit.new_user_ref(domain_id=CONF.identity.default_domain_id) + user['id'] = PROVIDERS.identity_api.create_user(user)['id'] + + PROVIDERS.assignment_api.create_system_grant_for_user( + user['id'], self.bootstrapper.reader_role_id + ) + + system_auth = self.build_authentication_request( + user_id=user['id'], password=user['password'], + system=True + ) + + with self.test_client() as c: + r = c.post('/v3/auth/tokens', json=system_auth) + system_token = r.headers['X-Subject-Token'] + + with self.test_client() as c: + self.headers['X-Subject-Token'] = system_token + c.delete( + '/v3/auth/tokens', headers=self.headers, + expected_status_code=http_client.FORBIDDEN + ) + + def test_user_cannot_revoke_a_domain_scoped_token(self): + domain = PROVIDERS.resource_api.create_domain( + uuid.uuid4().hex, unit.new_domain_ref() + ) + + user = unit.new_user_ref(domain_id=domain['id']) + user['id'] = PROVIDERS.identity_api.create_user(user)['id'] + + PROVIDERS.assignment_api.create_grant( + self.bootstrapper.reader_role_id, user_id=user['id'], + domain_id=domain['id'] + ) + + domain_auth = self.build_authentication_request( + user_id=user['id'], password=user['password'], + domain_id=domain['id'] + ) + + with self.test_client() as c: + r = c.post('/v3/auth/tokens', json=domain_auth) + domain_token = r.headers['X-Subject-Token'] + + with self.test_client() as c: + self.headers['X-Subject-Token'] = domain_token + c.delete( + '/v3/auth/tokens', headers=self.headers, + expected_status_code=http_client.FORBIDDEN + ) + + def test_user_cannot_revoke_a_project_scoped_token(self): + project = PROVIDERS.resource_api.create_project( + uuid.uuid4().hex, + unit.new_project_ref(domain_id=CONF.identity.default_domain_id) + ) + + user = unit.new_user_ref(domain_id=CONF.identity.default_domain_id) + user['id'] = PROVIDERS.identity_api.create_user(user)['id'] + + PROVIDERS.assignment_api.create_grant( + self.bootstrapper.reader_role_id, user_id=user['id'], + project_id=project['id'] + ) + + project_auth = self.build_authentication_request( + user_id=user['id'], password=user['password'], + project_id=project['id'] + ) + + with self.test_client() as c: + r = c.post('/v3/auth/tokens', json=project_auth) + project_token = r.headers['X-Subject-Token'] + + with self.test_client() as c: + self.headers['X-Subject-Token'] = project_token + c.delete( + '/v3/auth/tokens', headers=self.headers, + expected_status_code=http_client.FORBIDDEN + ) + + +class SystemReaderTests(base_classes.TestCaseWithBootstrap, + common_auth.AuthTestMixin, + _SystemUserTokenTests, + _SystemMemberAndReaderTokenTests): + + def setUp(self): + super(SystemReaderTests, self).setUp() + self.loadapp() + self.useFixture(ksfixtures.Policy(self.config_fixture)) + self.config_fixture.config(group='oslo_policy', enforce_scope=True) + + system_reader = unit.new_user_ref( + domain_id=CONF.identity.default_domain_id + ) + self.user_id = PROVIDERS.identity_api.create_user( + system_reader + )['id'] + PROVIDERS.assignment_api.create_system_grant_for_user( + self.user_id, self.bootstrapper.reader_role_id + ) + + auth = self.build_authentication_request( + user_id=self.user_id, password=system_reader['password'], + system=True + ) + + # Grab a token using the persona we're testing and prepare headers + # for requests we'll be making in the tests. + with self.test_client() as c: + r = c.post('/v3/auth/tokens', json=auth) + self.token_id = r.headers['X-Subject-Token'] + self.headers = {'X-Auth-Token': self.token_id} + + +class SystemMemberTests(base_classes.TestCaseWithBootstrap, + common_auth.AuthTestMixin, + _SystemUserTokenTests, + _SystemMemberAndReaderTokenTests): + + def setUp(self): + super(SystemMemberTests, self).setUp() + self.loadapp() + self.useFixture(ksfixtures.Policy(self.config_fixture)) + self.config_fixture.config(group='oslo_policy', enforce_scope=True) + + system_reader = unit.new_user_ref( + domain_id=CONF.identity.default_domain_id + ) + self.user_id = PROVIDERS.identity_api.create_user( + system_reader + )['id'] + PROVIDERS.assignment_api.create_system_grant_for_user( + self.user_id, self.bootstrapper.reader_role_id + ) + + auth = self.build_authentication_request( + user_id=self.user_id, password=system_reader['password'], + system=True + ) + + # Grab a token using the persona we're testing and prepare headers + # for requests we'll be making in the tests. + with self.test_client() as c: + r = c.post('/v3/auth/tokens', json=auth) + self.token_id = r.headers['X-Subject-Token'] + self.headers = {'X-Auth-Token': self.token_id} + + +class SystemAdminTests(base_classes.TestCaseWithBootstrap, + common_auth.AuthTestMixin, + _SystemUserTokenTests): + + def setUp(self): + super(SystemAdminTests, self).setUp() + self.loadapp() + self.useFixture(ksfixtures.Policy(self.config_fixture)) + self.config_fixture.config(group='oslo_policy', enforce_scope=True) + + self.user_id = self.bootstrapper.admin_user_id + auth = self.build_authentication_request( + user_id=self.user_id, + password=self.bootstrapper.admin_password, + system=True + ) + + # Grab a token using the persona we're testing and prepare headers + # for requests we'll be making in the tests. + with self.test_client() as c: + r = c.post('/v3/auth/tokens', json=auth) + self.token_id = r.headers['X-Subject-Token'] + self.headers = {'X-Auth-Token': self.token_id} + + def test_user_can_revoke_a_system_scoped_token(self): + user = unit.new_user_ref(domain_id=CONF.identity.default_domain_id) + user['id'] = PROVIDERS.identity_api.create_user(user)['id'] + + PROVIDERS.assignment_api.create_system_grant_for_user( + user['id'], self.bootstrapper.reader_role_id + ) + + system_auth = self.build_authentication_request( + user_id=user['id'], password=user['password'], + system=True + ) + + with self.test_client() as c: + r = c.post('/v3/auth/tokens', json=system_auth) + system_token = r.headers['X-Subject-Token'] + + with self.test_client() as c: + self.headers['X-Subject-Token'] = system_token + c.delete('/v3/auth/tokens', headers=self.headers) + + def test_user_can_revoke_a_domain_scoped_token(self): + domain = PROVIDERS.resource_api.create_domain( + uuid.uuid4().hex, unit.new_domain_ref() + ) + + user = unit.new_user_ref(domain_id=domain['id']) + user['id'] = PROVIDERS.identity_api.create_user(user)['id'] + + PROVIDERS.assignment_api.create_grant( + self.bootstrapper.reader_role_id, user_id=user['id'], + domain_id=domain['id'] + ) + + domain_auth = self.build_authentication_request( + user_id=user['id'], password=user['password'], + domain_id=domain['id'] + ) + + with self.test_client() as c: + r = c.post('/v3/auth/tokens', json=domain_auth) + domain_token = r.headers['X-Subject-Token'] + + with self.test_client() as c: + self.headers['X-Subject-Token'] = domain_token + c.delete('/v3/auth/tokens', headers=self.headers) + + def test_user_can_revoke_a_project_scoped_token(self): + project = PROVIDERS.resource_api.create_project( + uuid.uuid4().hex, + unit.new_project_ref(domain_id=CONF.identity.default_domain_id) + ) + + user = unit.new_user_ref(domain_id=CONF.identity.default_domain_id) + user['id'] = PROVIDERS.identity_api.create_user(user)['id'] + + PROVIDERS.assignment_api.create_grant( + self.bootstrapper.reader_role_id, user_id=user['id'], + project_id=project['id'] + ) + + project_auth = self.build_authentication_request( + user_id=user['id'], password=user['password'], + project_id=project['id'] + ) + + with self.test_client() as c: + r = c.post('/v3/auth/tokens', json=project_auth) + project_token = r.headers['X-Subject-Token'] + + with self.test_client() as c: + self.headers['X-Subject-Token'] = project_token + c.delete('/v3/auth/tokens', headers=self.headers) + + +class _DomainAndProjectUserTests(object): + + def test_user_can_validate_their_own_tokens(self): + with self.test_client() as c: + self.headers['X-Subject-Token'] = self.token_id + c.get('/v3/auth/tokens', headers=self.headers) + + def test_user_cannot_validate_system_scoped_token(self): + user = unit.new_user_ref(domain_id=CONF.identity.default_domain_id) + user['id'] = PROVIDERS.identity_api.create_user(user)['id'] + + PROVIDERS.assignment_api.create_system_grant_for_user( + user['id'], self.bootstrapper.reader_role_id + ) + + system_auth = self.build_authentication_request( + user_id=user['id'], password=user['password'], + system=True + ) + + with self.test_client() as c: + r = c.post('/v3/auth/tokens', json=system_auth) + system_token = r.headers['X-Subject-Token'] + + with self.test_client() as c: + self.headers['X-Subject-Token'] = system_token + c.get( + '/v3/auth/tokens', headers=self.headers, + expected_status_code=http_client.FORBIDDEN + ) + + def test_user_cannot_validate_domain_scoped_token(self): + domain = PROVIDERS.resource_api.create_domain( + uuid.uuid4().hex, unit.new_domain_ref() + ) + + user = unit.new_user_ref(domain_id=domain['id']) + user['id'] = PROVIDERS.identity_api.create_user(user)['id'] + + PROVIDERS.assignment_api.create_grant( + self.bootstrapper.reader_role_id, user_id=user['id'], + domain_id=domain['id'] + ) + + domain_auth = self.build_authentication_request( + user_id=user['id'], password=user['password'], + domain_id=domain['id'] + ) + + with self.test_client() as c: + r = c.post('/v3/auth/tokens', json=domain_auth) + domain_token = r.headers['X-Subject-Token'] + + with self.test_client() as c: + self.headers['X-Subject-Token'] = domain_token + c.get( + '/v3/auth/tokens', headers=self.headers, + expected_status_code=http_client.FORBIDDEN + ) + pass + + def test_user_cannot_validate_project_scoped_token(self): + project = PROVIDERS.resource_api.create_project( + uuid.uuid4().hex, + unit.new_project_ref(domain_id=CONF.identity.default_domain_id) + ) + + user = unit.new_user_ref(domain_id=CONF.identity.default_domain_id) + user['id'] = PROVIDERS.identity_api.create_user(user)['id'] + + PROVIDERS.assignment_api.create_grant( + self.bootstrapper.reader_role_id, user_id=user['id'], + project_id=project['id'] + ) + + project_auth = self.build_authentication_request( + user_id=user['id'], password=user['password'], + project_id=project['id'] + ) + + with self.test_client() as c: + r = c.post('/v3/auth/tokens', json=project_auth) + project_token = r.headers['X-Subject-Token'] + + with self.test_client() as c: + self.headers['X-Subject-Token'] = project_token + c.get( + '/v3/auth/tokens', headers=self.headers, + expected_status_code=http_client.FORBIDDEN + ) + + +class DomainUserTests(base_classes.TestCaseWithBootstrap, + common_auth.AuthTestMixin, + _DomainAndProjectUserTests): + + def setUp(self): + super(DomainUserTests, self).setUp() + self.loadapp() + self.useFixture(ksfixtures.Policy(self.config_fixture)) + self.config_fixture.config(group='oslo_policy', enforce_scope=True) + + domain = PROVIDERS.resource_api.create_domain( + uuid.uuid4().hex, unit.new_domain_ref() + ) + self.domain_id = domain['id'] + domain_user = unit.new_user_ref(domain_id=self.domain_id) + self.domain_user_id = PROVIDERS.identity_api.create_user( + domain_user + )['id'] + PROVIDERS.assignment_api.create_grant( + self.bootstrapper.member_role_id, user_id=self.domain_user_id, + domain_id=self.domain_id + ) + + auth = self.build_authentication_request( + user_id=self.domain_user_id, password=domain_user['password'], + domain_id=self.domain_id + ) + + # Grab a token using the persona we're testing and prepare headers + # for requests we'll be making in the tests. + with self.test_client() as c: + r = c.post('/v3/auth/tokens', json=auth) + self.token_id = r.headers['X-Subject-Token'] + self.headers = {'X-Auth-Token': self.token_id} + + +class ProjectUserTests(base_classes.TestCaseWithBootstrap, + common_auth.AuthTestMixin, + _DomainAndProjectUserTests): + + def setUp(self): + super(ProjectUserTests, self).setUp() + self.loadapp() + self.useFixture(ksfixtures.Policy(self.config_fixture)) + self.config_fixture.config(group='oslo_policy', enforce_scope=True) + + domain = PROVIDERS.resource_api.create_domain( + uuid.uuid4().hex, unit.new_domain_ref() + ) + self.domain_id = domain['id'] + + project_reader = unit.new_user_ref(domain_id=self.domain_id) + project_reader_id = PROVIDERS.identity_api.create_user( + project_reader + )['id'] + project = unit.new_project_ref(domain_id=self.domain_id) + project_id = PROVIDERS.resource_api.create_project( + project['id'], project + )['id'] + + PROVIDERS.assignment_api.create_grant( + self.bootstrapper.reader_role_id, user_id=project_reader_id, + project_id=project_id + ) + + auth = self.build_authentication_request( + user_id=project_reader_id, + password=project_reader['password'], + project_id=project_id + ) + + # Grab a token using the persona we're testing and prepare headers + # for requests we'll be making in the tests. + with self.test_client() as c: + r = c.post('/v3/auth/tokens', json=auth) + self.token_id = r.headers['X-Subject-Token'] + self.headers = {'X-Auth-Token': self.token_id} diff --git a/lower-constraints.txt b/lower-constraints.txt index dab11af99..307e9246d 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -31,7 +31,7 @@ oslo.config==5.2.0 oslo.context==2.22.0 oslo.db==4.27.0 oslo.i18n==3.15.3 -oslo.log==3.38.0 +oslo.log==3.44.0 oslo.messaging==5.29.0 oslo.middleware==3.31.0 oslo.policy==1.43.1 diff --git a/releasenotes/notes/bug-1750676-cf70c1a27b2c8de3.yaml b/releasenotes/notes/bug-1750676-cf70c1a27b2c8de3.yaml new file mode 100644 index 000000000..293e14949 --- /dev/null +++ b/releasenotes/notes/bug-1750676-cf70c1a27b2c8de3.yaml @@ -0,0 +1,35 @@ +--- +features: + - | + [`bug 1750676 <https://bugs.launchpad.net/keystone/+bug/1750676>`_] + [`bug 1818844 <https://bugs.launchpad.net/keystone/+bug/1818844>`_] + The token API now supports the ``admin``, ``member``, and ``reader`` + default roles. +upgrade: + - | + [`bug 1750676 <https://bugs.launchpad.net/keystone/+bug/1750676>`_] + [`bug 1818844 <https://bugs.launchpad.net/keystone/+bug/1818844>`_] + The token API uses new default policies that make it easier for system + users to delegate functionality in a secure way. Please consider the new + policies if your deployment overrides the token policies. +deprecations: + - | + [`bug 1750676 <https://bugs.launchpad.net/keystone/+bug/1750676>`_] + [`bug 1818844 <https://bugs.launchpad.net/keystone/+bug/1818844>`_] + The ``identity:check_token`` policy now uses ``(role:reader and + system_scope:all) or rule:token_subject`` instead of ``rule:admin_required + or rule:token_subject``. The ``identity:validate_token`` policy now uses + ``(role:reader and system_scope:all) or rule:service_role or + rule:token_subject`` instead or ``rule:service_or_admin or + rule:token_subject``. The ``identity:revoke_token`` policy now uses + ``(role:admin and system_scope:all) or rule:token_subject`` instead of + ``rule:admin_or_token_subject``. These new defaults automatically account + for a read-only role by default and allow more granular access to the API. + Please consider these new defaults if your deployment overrides the token + policies. +security: + - | + [`bug 1750676 <https://bugs.launchpad.net/keystone/+bug/1750676>`_] + [`bug 1818844 <https://bugs.launchpad.net/keystone/+bug/1818844>`_] + The token API now uses system-scope and default roles properly to provide + more granular access to the token API. diff --git a/requirements.txt b/requirements.txt index 90e0375b6..b46f3cfe4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -27,7 +27,7 @@ oslo.context>=2.22.0 # Apache-2.0 oslo.messaging>=5.29.0 # Apache-2.0 oslo.db>=4.27.0 # Apache-2.0 oslo.i18n>=3.15.3 # Apache-2.0 -oslo.log>=3.38.0 # Apache-2.0 +oslo.log>=3.44.0 # Apache-2.0 oslo.middleware>=3.31.0 # Apache-2.0 oslo.policy>=1.43.1 # Apache-2.0 oslo.serialization!=2.19.1,>=2.18.0 # Apache-2.0 |