Merge "Add ConsoleAuthToken object"
This commit is contained in:
@@ -1896,6 +1896,10 @@ class InvalidToken(Invalid):
|
|||||||
msg_fmt = _("The token '%(token)s' is invalid or has expired")
|
msg_fmt = _("The token '%(token)s' is invalid or has expired")
|
||||||
|
|
||||||
|
|
||||||
|
class TokenInUse(Invalid):
|
||||||
|
msg_fmt = _("The generated token is invalid")
|
||||||
|
|
||||||
|
|
||||||
class InvalidConnectionInfo(Invalid):
|
class InvalidConnectionInfo(Invalid):
|
||||||
msg_fmt = _("Invalid Connection Info")
|
msg_fmt = _("Invalid Connection Info")
|
||||||
|
|
||||||
|
@@ -32,6 +32,7 @@ def register_all():
|
|||||||
__import__('nova.objects.cell_mapping')
|
__import__('nova.objects.cell_mapping')
|
||||||
__import__('nova.objects.compute_node')
|
__import__('nova.objects.compute_node')
|
||||||
__import__('nova.objects.diagnostics')
|
__import__('nova.objects.diagnostics')
|
||||||
|
__import__('nova.objects.console_auth_token')
|
||||||
__import__('nova.objects.dns_domain')
|
__import__('nova.objects.dns_domain')
|
||||||
__import__('nova.objects.ec2')
|
__import__('nova.objects.ec2')
|
||||||
__import__('nova.objects.external_event')
|
__import__('nova.objects.external_event')
|
||||||
|
170
nova/objects/console_auth_token.py
Normal file
170
nova/objects/console_auth_token.py
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
# Copyright 2015 Intel Corp
|
||||||
|
# Copyright 2016 Hewlett Packard Enterprise Development Company LP
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
from oslo_db.exception import DBDuplicateEntry
|
||||||
|
from oslo_log import log as logging
|
||||||
|
from oslo_utils import strutils
|
||||||
|
from oslo_utils import timeutils
|
||||||
|
from oslo_utils import uuidutils
|
||||||
|
|
||||||
|
import nova.conf
|
||||||
|
from nova import db
|
||||||
|
from nova import exception
|
||||||
|
from nova.i18n import _
|
||||||
|
from nova.objects import base
|
||||||
|
from nova.objects import fields
|
||||||
|
from nova import utils
|
||||||
|
|
||||||
|
CONF = nova.conf.CONF
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@base.NovaObjectRegistry.register
|
||||||
|
class ConsoleAuthToken(base.NovaTimestampObject, base.NovaObject):
|
||||||
|
# Version 1.0: Initial version
|
||||||
|
VERSION = '1.0'
|
||||||
|
|
||||||
|
fields = {
|
||||||
|
'id': fields.IntegerField(),
|
||||||
|
'console_type': fields.StringField(nullable=False),
|
||||||
|
'host': fields.StringField(nullable=False),
|
||||||
|
'port': fields.IntegerField(nullable=False),
|
||||||
|
'internal_access_path': fields.StringField(nullable=True),
|
||||||
|
'instance_uuid': fields.UUIDField(nullable=False),
|
||||||
|
'access_url_base': fields.StringField(nullable=True),
|
||||||
|
# NOTE(PaulMurray): The unhashed token field is not stored in the
|
||||||
|
# database. A hash of the token is stored instead and is not a
|
||||||
|
# field on the object.
|
||||||
|
'token': fields.StringField(nullable=False),
|
||||||
|
}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def access_url(self):
|
||||||
|
"""The access url with token parameter.
|
||||||
|
|
||||||
|
:returns: the access url with credential parameters
|
||||||
|
|
||||||
|
access_url_base is the base url used to access a console.
|
||||||
|
Adding the unhashed token as a parameter in a query string makes it
|
||||||
|
specific to this authorization.
|
||||||
|
"""
|
||||||
|
if self.obj_attr_is_set('id'):
|
||||||
|
return '%s?token=%s' % (self.access_url_base, self.token)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _from_db_object(context, obj, db_obj):
|
||||||
|
# NOTE(PaulMurray): token is not stored in the database but
|
||||||
|
# this function assumes it is in db_obj. The unhashed token
|
||||||
|
# field is populated in the authorize method after the token
|
||||||
|
# authorization is created in the database.
|
||||||
|
for field in obj.fields:
|
||||||
|
setattr(obj, field, db_obj[field])
|
||||||
|
obj._context = context
|
||||||
|
obj.obj_reset_changes()
|
||||||
|
return obj
|
||||||
|
|
||||||
|
@base.remotable
|
||||||
|
def authorize(self, ttl):
|
||||||
|
"""Authorise the console token and store in the database.
|
||||||
|
|
||||||
|
:param ttl: time to live in seconds
|
||||||
|
:returns: an authorized token
|
||||||
|
|
||||||
|
The expires value is set for ttl seconds in the future and the token
|
||||||
|
hash is stored in the database. This function can only succeed if the
|
||||||
|
token is unique and the object has not already been stored.
|
||||||
|
"""
|
||||||
|
if self.obj_attr_is_set('id'):
|
||||||
|
raise exception.ObjectActionError(
|
||||||
|
action='authorize',
|
||||||
|
reason=_('must be a new object to authorize'))
|
||||||
|
|
||||||
|
token = uuidutils.generate_uuid()
|
||||||
|
token_hash = utils.get_sha256_str(token)
|
||||||
|
expires = timeutils.utcnow_ts() + ttl
|
||||||
|
|
||||||
|
updates = self.obj_get_changes()
|
||||||
|
# NOTE(melwitt): token could be in the updates if authorize() has been
|
||||||
|
# called twice on the same object. 'token' is not a database column and
|
||||||
|
# should not be included in the call to create the database record.
|
||||||
|
if 'token' in updates:
|
||||||
|
del updates['token']
|
||||||
|
updates['token_hash'] = token_hash
|
||||||
|
updates['expires'] = expires
|
||||||
|
|
||||||
|
try:
|
||||||
|
db_obj = db.console_auth_token_create(self._context, updates)
|
||||||
|
db_obj['token'] = token
|
||||||
|
self._from_db_object(self._context, self, db_obj)
|
||||||
|
except DBDuplicateEntry:
|
||||||
|
# NOTE(PaulMurray) we are generating the token above so this
|
||||||
|
# should almost never happen - but technically its possible
|
||||||
|
raise exception.TokenInUse()
|
||||||
|
|
||||||
|
LOG.debug("Authorized token with expiry %(expires)s for console "
|
||||||
|
"connection %(console)s",
|
||||||
|
{'expires': expires,
|
||||||
|
'console': strutils.mask_password(self)})
|
||||||
|
return token
|
||||||
|
|
||||||
|
@base.remotable_classmethod
|
||||||
|
def validate(cls, context, token):
|
||||||
|
"""Validate the token.
|
||||||
|
|
||||||
|
:param context: the context
|
||||||
|
:param token: the token for the authorization
|
||||||
|
:returns: The ConsoleAuthToken object if valid
|
||||||
|
|
||||||
|
The token is valid if the token is in the database and the expires
|
||||||
|
time has not passed.
|
||||||
|
"""
|
||||||
|
token_hash = utils.get_sha256_str(token)
|
||||||
|
db_obj = db.console_auth_token_get_valid(context, token_hash)
|
||||||
|
|
||||||
|
if db_obj is not None:
|
||||||
|
db_obj['token'] = token
|
||||||
|
obj = cls._from_db_object(context, cls(), db_obj)
|
||||||
|
LOG.debug("Validated token - console connection is "
|
||||||
|
"%(console)s",
|
||||||
|
{'console': strutils.mask_password(obj)})
|
||||||
|
return obj
|
||||||
|
else:
|
||||||
|
LOG.debug("Token validation failed")
|
||||||
|
raise exception.InvalidToken(token='***')
|
||||||
|
|
||||||
|
@base.remotable_classmethod
|
||||||
|
def clean_console_auths_for_instance(cls, context, instance_uuid):
|
||||||
|
"""Remove all console authorizations for the instance.
|
||||||
|
|
||||||
|
:param context: the context
|
||||||
|
:param instance_uuid: the instance to be cleaned
|
||||||
|
|
||||||
|
All authorizations related to the specified instance will be
|
||||||
|
removed from the database.
|
||||||
|
"""
|
||||||
|
db.console_auth_token_destroy_all_by_instance(context, instance_uuid)
|
||||||
|
|
||||||
|
@base.remotable_classmethod
|
||||||
|
def clean_expired_console_auths_for_host(cls, context, host):
|
||||||
|
"""Remove all expired console authorizations for the host.
|
||||||
|
|
||||||
|
:param context: the context
|
||||||
|
:param host: the host name
|
||||||
|
|
||||||
|
All expired authorizations related to the specified host
|
||||||
|
will be removed. Tokens that have not expired will
|
||||||
|
remain.
|
||||||
|
"""
|
||||||
|
db.console_auth_token_destroy_expired_by_host(context, host)
|
57
nova/tests/functional/db/test_console_auth_token.py
Normal file
57
nova/tests/functional/db/test_console_auth_token.py
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
# Copyright 2016 Hewlett Packard Enterprise Development Company LP
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
from oslo_versionedobjects import fixture as ovo_fixture
|
||||||
|
|
||||||
|
from nova import context
|
||||||
|
from nova import exception
|
||||||
|
from nova import objects
|
||||||
|
from nova import test
|
||||||
|
from nova.tests import uuidsentinel
|
||||||
|
|
||||||
|
|
||||||
|
class ConsoleAuthTokenTestCase(test.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
super(ConsoleAuthTokenTestCase, self).setUp()
|
||||||
|
self.context = context.RequestContext('fake-user', 'fake-project')
|
||||||
|
instance = objects.Instance(
|
||||||
|
context=self.context,
|
||||||
|
project_id=self.context.project_id,
|
||||||
|
uuid=uuidsentinel.fake_instance)
|
||||||
|
instance.create()
|
||||||
|
self.console = objects.ConsoleAuthToken(
|
||||||
|
context=self.context,
|
||||||
|
instance_uuid=uuidsentinel.fake_instance,
|
||||||
|
console_type='fake-type',
|
||||||
|
host='fake-host',
|
||||||
|
port=1000,
|
||||||
|
internal_access_path='fake-internal_access_path',
|
||||||
|
access_url_base='fake-external_access_path'
|
||||||
|
)
|
||||||
|
self.token = self.console.authorize(100)
|
||||||
|
|
||||||
|
def test_validate(self):
|
||||||
|
connection_info = objects.ConsoleAuthToken.validate(
|
||||||
|
self.context, self.token)
|
||||||
|
expected = self.console.obj_to_primitive()['nova_object.data']
|
||||||
|
del expected['created_at']
|
||||||
|
ovo_fixture.compare_obj(self, connection_info, expected,
|
||||||
|
allow_missing=['created_at'])
|
||||||
|
|
||||||
|
def test_validate_invalid(self):
|
||||||
|
unauthorized_token = uuidsentinel.token
|
||||||
|
self.assertRaises(
|
||||||
|
exception.InvalidToken,
|
||||||
|
objects.ConsoleAuthToken.validate,
|
||||||
|
self.context, unauthorized_token)
|
165
nova/tests/unit/objects/test_console_auth_token.py
Normal file
165
nova/tests/unit/objects/test_console_auth_token.py
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
# Copyright 2016 Intel Corp.
|
||||||
|
# Copyright 2016 Hewlett Packard Enterprise Development Company LP
|
||||||
|
#
|
||||||
|
# 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 copy
|
||||||
|
import mock
|
||||||
|
|
||||||
|
from oslo_db.exception import DBDuplicateEntry
|
||||||
|
from oslo_utils import timeutils
|
||||||
|
|
||||||
|
from nova import exception
|
||||||
|
from nova.objects import console_auth_token as token_obj
|
||||||
|
from nova.tests.unit import fake_console_auth_token as fakes
|
||||||
|
from nova.tests.unit.objects import test_objects
|
||||||
|
from nova.tests import uuidsentinel
|
||||||
|
|
||||||
|
|
||||||
|
class _TestConsoleAuthToken(object):
|
||||||
|
|
||||||
|
@mock.patch('nova.db.console_auth_token_create')
|
||||||
|
def test_authorize(self, mock_create):
|
||||||
|
# the expires time is calculated from the current time and
|
||||||
|
# a ttl value in the object. Fix the current time so we can
|
||||||
|
# test expires is calculated correctly as expected
|
||||||
|
self.addCleanup(timeutils.clear_time_override)
|
||||||
|
timeutils.set_time_override()
|
||||||
|
ttl = 10
|
||||||
|
expires = timeutils.utcnow_ts() + ttl
|
||||||
|
|
||||||
|
db_dict = copy.deepcopy(fakes.fake_token_dict)
|
||||||
|
db_dict['expires'] = expires
|
||||||
|
mock_create.return_value = db_dict
|
||||||
|
|
||||||
|
create_dict = copy.deepcopy(fakes.fake_token_dict)
|
||||||
|
create_dict['expires'] = expires
|
||||||
|
del create_dict['id']
|
||||||
|
del create_dict['created_at']
|
||||||
|
del create_dict['updated_at']
|
||||||
|
|
||||||
|
expected = copy.deepcopy(fakes.fake_token_dict)
|
||||||
|
del expected['token_hash']
|
||||||
|
del expected['expires']
|
||||||
|
expected['token'] = fakes.fake_token
|
||||||
|
|
||||||
|
obj = token_obj.ConsoleAuthToken(
|
||||||
|
context=self.context,
|
||||||
|
console_type=fakes.fake_token_dict['console_type'],
|
||||||
|
host=fakes.fake_token_dict['host'],
|
||||||
|
port=fakes.fake_token_dict['port'],
|
||||||
|
internal_access_path=fakes.fake_token_dict['internal_access_path'],
|
||||||
|
instance_uuid=fakes.fake_token_dict['instance_uuid'],
|
||||||
|
access_url_base=fakes.fake_token_dict['access_url_base'],
|
||||||
|
)
|
||||||
|
with mock.patch('uuid.uuid4', return_value=fakes.fake_token):
|
||||||
|
token = obj.authorize(ttl)
|
||||||
|
|
||||||
|
mock_create.assert_called_once_with(self.context, create_dict)
|
||||||
|
self.assertEqual(token, fakes.fake_token)
|
||||||
|
self.compare_obj(obj, expected)
|
||||||
|
|
||||||
|
url = obj.access_url
|
||||||
|
expected_url = '%s?token=%s' % (
|
||||||
|
fakes.fake_token_dict['access_url_base'],
|
||||||
|
fakes.fake_token)
|
||||||
|
self.assertEqual(expected_url, url)
|
||||||
|
|
||||||
|
@mock.patch('nova.db.console_auth_token_create')
|
||||||
|
def test_authorize_duplicate_token(self, mock_create):
|
||||||
|
mock_create.side_effect = DBDuplicateEntry()
|
||||||
|
|
||||||
|
obj = token_obj.ConsoleAuthToken(
|
||||||
|
context=self.context,
|
||||||
|
console_type=fakes.fake_token_dict['console_type'],
|
||||||
|
host=fakes.fake_token_dict['host'],
|
||||||
|
port=fakes.fake_token_dict['port'],
|
||||||
|
internal_access_path=fakes.fake_token_dict['internal_access_path'],
|
||||||
|
instance_uuid=fakes.fake_token_dict['instance_uuid'],
|
||||||
|
access_url_base=fakes.fake_token_dict['access_url_base'],
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertRaises(exception.TokenInUse,
|
||||||
|
obj.authorize,
|
||||||
|
100)
|
||||||
|
|
||||||
|
@mock.patch('nova.db.console_auth_token_create')
|
||||||
|
def test_authorize_instance_not_found(self, mock_create):
|
||||||
|
mock_create.side_effect = exception.InstanceNotFound(
|
||||||
|
instance_id=fakes.fake_token_dict['instance_uuid'])
|
||||||
|
|
||||||
|
obj = token_obj.ConsoleAuthToken(
|
||||||
|
context=self.context,
|
||||||
|
console_type=fakes.fake_token_dict['console_type'],
|
||||||
|
host=fakes.fake_token_dict['host'],
|
||||||
|
port=fakes.fake_token_dict['port'],
|
||||||
|
internal_access_path=fakes.fake_token_dict['internal_access_path'],
|
||||||
|
instance_uuid=fakes.fake_token_dict['instance_uuid'],
|
||||||
|
access_url_base=fakes.fake_token_dict['access_url_base'],
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertRaises(exception.InstanceNotFound,
|
||||||
|
obj.authorize,
|
||||||
|
100)
|
||||||
|
|
||||||
|
@mock.patch('nova.db.console_auth_token_create')
|
||||||
|
def test_authorize_object_already_created(self, mock_create):
|
||||||
|
# the expires time is calculated from the current time and
|
||||||
|
# a ttl value in the object. Fix the current time so we can
|
||||||
|
# test expires is calculated correctly as expected
|
||||||
|
self.addCleanup(timeutils.clear_time_override)
|
||||||
|
timeutils.set_time_override()
|
||||||
|
ttl = 10
|
||||||
|
expires = timeutils.utcnow_ts() + ttl
|
||||||
|
|
||||||
|
db_dict = copy.deepcopy(fakes.fake_token_dict)
|
||||||
|
db_dict['expires'] = expires
|
||||||
|
mock_create.return_value = db_dict
|
||||||
|
|
||||||
|
obj = token_obj.ConsoleAuthToken(
|
||||||
|
context=self.context,
|
||||||
|
console_type=fakes.fake_token_dict['console_type'],
|
||||||
|
host=fakes.fake_token_dict['host'],
|
||||||
|
port=fakes.fake_token_dict['port'],
|
||||||
|
internal_access_path=fakes.fake_token_dict['internal_access_path'],
|
||||||
|
instance_uuid=fakes.fake_token_dict['instance_uuid'],
|
||||||
|
access_url_base=fakes.fake_token_dict['access_url_base'],
|
||||||
|
)
|
||||||
|
obj.authorize(100)
|
||||||
|
self.assertRaises(exception.ObjectActionError,
|
||||||
|
obj.authorize,
|
||||||
|
100)
|
||||||
|
|
||||||
|
@mock.patch('nova.db.console_auth_token_destroy_all_by_instance')
|
||||||
|
def test_clean_console_auths_for_instance(self, mock_destroy):
|
||||||
|
token_obj.ConsoleAuthToken.clean_console_auths_for_instance(
|
||||||
|
self.context, uuidsentinel.instance)
|
||||||
|
mock_destroy.assert_called_once_with(
|
||||||
|
self.context, uuidsentinel.instance)
|
||||||
|
|
||||||
|
@mock.patch('nova.db.console_auth_token_destroy_expired_by_host')
|
||||||
|
def test_clean_expired_console_auths_for_host(self, mock_destroy):
|
||||||
|
token_obj.ConsoleAuthToken.clean_expired_console_auths_for_host(
|
||||||
|
self.context, 'fake-host')
|
||||||
|
mock_destroy.assert_called_once_with(
|
||||||
|
self.context, 'fake-host')
|
||||||
|
|
||||||
|
|
||||||
|
class TestConsoleAuthToken(test_objects._LocalTest,
|
||||||
|
_TestConsoleAuthToken):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class TestRemoteConsoleAuthToken(test_objects._RemoteTest,
|
||||||
|
_TestConsoleAuthToken):
|
||||||
|
pass
|
@@ -1073,6 +1073,7 @@ object_data = {
|
|||||||
'ComputeNode': '1.18-431fafd8ac4a5f3559bd9b1f1332cc22',
|
'ComputeNode': '1.18-431fafd8ac4a5f3559bd9b1f1332cc22',
|
||||||
'ComputeNodeList': '1.17-52f3b0962b1c86b98590144463ebb192',
|
'ComputeNodeList': '1.17-52f3b0962b1c86b98590144463ebb192',
|
||||||
'CpuDiagnostics': '1.0-d256f2e442d1b837735fd17dfe8e3d47',
|
'CpuDiagnostics': '1.0-d256f2e442d1b837735fd17dfe8e3d47',
|
||||||
|
'ConsoleAuthToken': '1.0-a61bf7b54517c4013a12289c5a5268ea',
|
||||||
'DNSDomain': '1.0-7b0b2dab778454b6a7b6c66afe163a1a',
|
'DNSDomain': '1.0-7b0b2dab778454b6a7b6c66afe163a1a',
|
||||||
'DNSDomainList': '1.0-4ee0d9efdfd681fed822da88376e04d2',
|
'DNSDomainList': '1.0-4ee0d9efdfd681fed822da88376e04d2',
|
||||||
'Destination': '1.1-fff0853f3acec6b04ddc03158ded11ba',
|
'Destination': '1.1-fff0853f3acec6b04ddc03158ded11ba',
|
||||||
|
Reference in New Issue
Block a user