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")
|
||||
|
||||
|
||||
class TokenInUse(Invalid):
|
||||
msg_fmt = _("The generated token is invalid")
|
||||
|
||||
|
||||
class InvalidConnectionInfo(Invalid):
|
||||
msg_fmt = _("Invalid Connection Info")
|
||||
|
||||
|
@@ -32,6 +32,7 @@ def register_all():
|
||||
__import__('nova.objects.cell_mapping')
|
||||
__import__('nova.objects.compute_node')
|
||||
__import__('nova.objects.diagnostics')
|
||||
__import__('nova.objects.console_auth_token')
|
||||
__import__('nova.objects.dns_domain')
|
||||
__import__('nova.objects.ec2')
|
||||
__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',
|
||||
'ComputeNodeList': '1.17-52f3b0962b1c86b98590144463ebb192',
|
||||
'CpuDiagnostics': '1.0-d256f2e442d1b837735fd17dfe8e3d47',
|
||||
'ConsoleAuthToken': '1.0-a61bf7b54517c4013a12289c5a5268ea',
|
||||
'DNSDomain': '1.0-7b0b2dab778454b6a7b6c66afe163a1a',
|
||||
'DNSDomainList': '1.0-4ee0d9efdfd681fed822da88376e04d2',
|
||||
'Destination': '1.1-fff0853f3acec6b04ddc03158ded11ba',
|
||||
|
Reference in New Issue
Block a user