Implement system scope and default roles for token API

This commit adds protection testing for the token API along with
changes to default policies to properly consume system-scope and
default roles.

Originally, this work was going to include the ability for project and
domain administrator to validate, check, or revoke tokens within the
context of their authorization (e.g., a domain administrator could
revoke tokens on projects within their domain). This seems like extra
work for not much benefit since we're using bearer tokens. The holder
of the token can do anything with that token, which means they can
validate it or revoke it without using their own token. Adding
project and domain administrator support seems unnecessary given the
existing functionality. If someone comes forward asking for this
functionality, we can re-evaluate the effort. For now, this patch is
limited to system user support, allowing them to validate, check, and
revoke any token in the system. Service users can still validate
tokens on behalf of users. Users can do anything they wish with their
own tokens.

This commit also bumps the minimum version of oslo.log so that we can
use the official TRAIN deprecated release marker.

Change-Id: Ia8b35258b43213bd117df4275c907aac223342b3
Closes-Bug: 1818844
Closes-Bug: 1750676
This commit is contained in:
Lance Bragstad 2019-06-13 20:12:56 +00:00
parent 4f16676797
commit 092570fc5e
5 changed files with 614 additions and 33 deletions

View File

@ -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)
]

View File

@ -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}

View File

@ -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

View File

@ -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.

View File

@ -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