diff --git a/nova/db/api.py b/nova/db/api.py index a120b069d5ef..d3c64f9721ad 100644 --- a/nova/db/api.py +++ b/nova/db/api.py @@ -2050,3 +2050,39 @@ def instance_tag_delete_all(context, instance_uuid): def instance_tag_exists(context, instance_uuid, tag): """Check if specified tag exist on the instance.""" return IMPL.instance_tag_exists(context, instance_uuid, tag) + + +#################### + + +def console_auth_token_create(context, values): + """Create a console authorization.""" + return IMPL.console_auth_token_create(context, values) + + +def console_auth_token_get_valid(context, token_hash, instance_uuid): + """Get a valid console authorization by token_hash and instance_uuid. + + The console authorizations expire at the time specified by their + 'expires' column. An expired console auth token will not be returned + to the caller - it is treated as if it does not exist. + """ + return IMPL.console_auth_token_get_valid(context, + token_hash, + instance_uuid) + + +def console_auth_token_destroy_all_by_instance(context, instance_uuid): + """Delete all console authorizations belonging to the instance.""" + return IMPL.console_auth_token_destroy_all_by_instance(context, + instance_uuid) + + +def console_auth_token_destroy_expired_by_host(context, host): + """Delete expired console authorizations belonging to the host. + + The console authorizations expire at the time specified by their + 'expires' column. This function is used to garbage collect expired + tokens associated with the given host. + """ + return IMPL.console_auth_token_destroy_expired_by_host(context, host) diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index 08ff0e154857..8d832f8ee0cf 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -6812,3 +6812,40 @@ def instance_tag_exists(context, instance_uuid, tag): q = context.session.query(models.Tag).filter_by( resource_id=instance_uuid, tag=tag) return context.session.query(q.exists()).scalar() + + +#################### + + +@pick_context_manager_writer +def console_auth_token_create(context, values): + instance_uuid = values.get('instance_uuid') + _check_instance_exists_in_project(context, instance_uuid) + token_ref = models.ConsoleAuthToken() + token_ref.update(values) + context.session.add(token_ref) + return token_ref + + +@pick_context_manager_reader +def console_auth_token_get_valid(context, token_hash, instance_uuid): + _check_instance_exists_in_project(context, instance_uuid) + return context.session.query(models.ConsoleAuthToken).\ + filter_by(token_hash=token_hash).\ + filter_by(instance_uuid=instance_uuid).\ + filter(models.ConsoleAuthToken.expires > timeutils.utcnow_ts()).\ + first() + + +@pick_context_manager_writer +def console_auth_token_destroy_all_by_instance(context, instance_uuid): + context.session.query(models.ConsoleAuthToken).\ + filter_by(instance_uuid=instance_uuid).delete() + + +@pick_context_manager_writer +def console_auth_token_destroy_expired_by_host(context, host): + context.session.query(models.ConsoleAuthToken).\ + filter_by(host=host).\ + filter(models.ConsoleAuthToken.expires <= timeutils.utcnow_ts()).\ + delete() diff --git a/nova/tests/unit/db/test_db_api.py b/nova/tests/unit/db/test_db_api.py index 0c7c9d3f3f32..b6a276d0245e 100644 --- a/nova/tests/unit/db/test_db_api.py +++ b/nova/tests/unit/db/test_db_api.py @@ -65,6 +65,7 @@ from nova import objects from nova.objects import fields from nova import quota from nova import test +from nova.tests.unit import fake_console_auth_token from nova.tests.unit import matchers from nova.tests import uuidsentinel from nova import utils @@ -10003,3 +10004,135 @@ class TestInstanceTagsFiltering(test.TestCase): 'not-tags': [u't5', u't6'], 'not-tags-any': [u't7', u't8']}) self._assertEqualInstanceUUIDs([uuids[3], uuids[5], uuids[6]], result) + + +class ConsoleAuthTokenTestCase(test.TestCase): + + def _create_instances(self, uuids): + for uuid in uuids: + db.instance_create(self.context, + {'uuid': uuid, + 'project_id': self.context.project_id}) + + def _create(self, token_hash, instance_uuid, expire_offset, host=None): + t = copy.deepcopy(fake_console_auth_token.fake_token_dict) + del t['id'] + t['token_hash'] = token_hash + t['instance_uuid'] = instance_uuid + t['expires'] = timeutils.utcnow_ts() + expire_offset + if host: + t['host'] = host + db.console_auth_token_create(self.context, t) + + def setUp(self): + super(ConsoleAuthTokenTestCase, self).setUp() + self.context = context.RequestContext('fake', 'fake') + + def test_console_auth_token_create_no_instance(self): + t = copy.deepcopy(fake_console_auth_token.fake_token_dict) + del t['id'] + self.assertRaises(exception.InstanceNotFound, + db.console_auth_token_create, + self.context, t) + + def test_console_auth_token_get_valid_deleted_instance(self): + uuid1 = uuidsentinel.uuid1 + hash1 = utils.get_sha256_str(uuidsentinel.token1) + self._create_instances([uuid1]) + self._create(hash1, uuid1, 100) + + db_obj1 = db.console_auth_token_get_valid(self.context, hash1, uuid1) + self.assertIsNotNone(db_obj1, "a valid token should be in database") + + db.instance_destroy(self.context, uuid1) + self.assertRaises(exception.InstanceNotFound, + db.console_auth_token_get_valid, + self.context, hash1, uuid1) + + def test_console_auth_token_destroy_all_by_instance(self): + uuid1 = uuidsentinel.uuid1 + uuid2 = uuidsentinel.uuid2 + hash1 = utils.get_sha256_str(uuidsentinel.token1) + hash2 = utils.get_sha256_str(uuidsentinel.token2) + hash3 = utils.get_sha256_str(uuidsentinel.token3) + self._create_instances([uuid1, uuid2]) + self._create(hash1, uuid1, 100) + self._create(hash2, uuid1, 100) + self._create(hash3, uuid2, 100) + + db_obj1 = db.console_auth_token_get_valid(self.context, hash1, uuid1) + db_obj2 = db.console_auth_token_get_valid(self.context, hash2, uuid1) + db_obj3 = db.console_auth_token_get_valid(self.context, hash3, uuid2) + self.assertIsNotNone(db_obj1, "a valid token should be in database") + self.assertIsNotNone(db_obj2, "a valid token should be in database") + self.assertIsNotNone(db_obj3, "a valid token should be in database") + + db.console_auth_token_destroy_all_by_instance(self.context, uuid1) + + db_obj4 = db.console_auth_token_get_valid(self.context, hash1, uuid1) + db_obj5 = db.console_auth_token_get_valid(self.context, hash2, uuid1) + db_obj6 = db.console_auth_token_get_valid(self.context, hash3, uuid2) + self.assertIsNone(db_obj4, "no valid token should be in database") + self.assertIsNone(db_obj5, "no valid token should be in database") + self.assertIsNotNone(db_obj6, "a valid token should be in database") + + def test_console_auth_token_get_valid_by_expiry(self): + uuid1 = uuidsentinel.uuid1 + uuid2 = uuidsentinel.uuid2 + hash1 = utils.get_sha256_str(uuidsentinel.token1) + hash2 = utils.get_sha256_str(uuidsentinel.token2) + self.addCleanup(timeutils.clear_time_override) + timeutils.set_time_override(timeutils.utcnow()) + self._create_instances([uuid1, uuid2]) + + self._create(hash1, uuid1, 10) + timeutils.advance_time_seconds(100) + self._create(hash2, uuid2, 10) + + db_obj1 = db.console_auth_token_get_valid(self.context, hash1, uuid1) + db_obj2 = db.console_auth_token_get_valid(self.context, hash2, uuid2) + self.assertIsNone(db_obj1, "the token should have expired") + self.assertIsNotNone(db_obj2, "a valid token should be found here") + + def test_console_auth_token_get_valid_by_uuid(self): + uuid1 = uuidsentinel.uuid1 + uuid2 = uuidsentinel.uuid2 + hash1 = utils.get_sha256_str(uuidsentinel.token1) + self._create_instances([uuid1, uuid2]) + + self._create(hash1, uuid1, 10) + + db_obj1 = db.console_auth_token_get_valid(self.context, hash1, uuid1) + db_obj2 = db.console_auth_token_get_valid(self.context, hash1, uuid2) + self.assertIsNotNone(db_obj1, "a valid token should be found here") + self.assertIsNone(db_obj2, "the token uuid should not match") + + def test_console_auth_token_destroy_expired_by_host(self): + uuid1 = uuidsentinel.uuid1 + uuid2 = uuidsentinel.uuid2 + uuid3 = uuidsentinel.uuid3 + hash1 = utils.get_sha256_str(uuidsentinel.token1) + hash2 = utils.get_sha256_str(uuidsentinel.token2) + hash3 = utils.get_sha256_str(uuidsentinel.token3) + self.addCleanup(timeutils.clear_time_override) + timeutils.set_time_override(timeutils.utcnow()) + self._create_instances([uuid1, uuid2, uuid3]) + + self._create(hash1, uuid1, 10) + self._create(hash2, uuid2, 10, host='other-host') + timeutils.advance_time_seconds(100) + self._create(hash3, uuid3, 10) + + db.console_auth_token_destroy_expired_by_host( + self.context, 'fake-host') + + # the api only supports getting unexpired tokens + # but by rolling back time we can see if a token that + # should be deleted is still there + timeutils.advance_time_seconds(-100) + db_obj1 = db.console_auth_token_get_valid(self.context, hash1, uuid1) + db_obj2 = db.console_auth_token_get_valid(self.context, hash2, uuid2) + db_obj3 = db.console_auth_token_get_valid(self.context, hash3, uuid3) + self.assertIsNone(db_obj1, "the token should have been deleted") + self.assertIsNotNone(db_obj2, "a valid token should be found here") + self.assertIsNotNone(db_obj3, "a valid token should be found here") diff --git a/nova/tests/unit/fake_console_auth_token.py b/nova/tests/unit/fake_console_auth_token.py new file mode 100644 index 000000000000..a44ce631c4fa --- /dev/null +++ b/nova/tests/unit/fake_console_auth_token.py @@ -0,0 +1,33 @@ +# 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. + +from nova.tests import uuidsentinel +from nova import utils + +fake_token = uuidsentinel.token +fake_token_hash = utils.get_sha256_str(fake_token) +fake_instance_uuid = uuidsentinel.instance +fake_token_dict = { + 'created_at': None, + 'updated_at': None, + 'id': 123, + 'token_hash': fake_token_hash, + 'console_type': 'fake-type', + 'host': 'fake-host', + 'port': 1000, + 'internal_access_path': 'fake-path', + 'instance_uuid': fake_instance_uuid, + 'expires': 100, + } diff --git a/nova/utils.py b/nova/utils.py index 83e5f6e0eefb..eeeb8ea1c9d1 100644 --- a/nova/utils.py +++ b/nova/utils.py @@ -1211,6 +1211,19 @@ def get_hash_str(base_str): return hashlib.md5(base_str).hexdigest() +def get_sha256_str(base_str): + """Returns string that represents sha256 hash of base_str (in hex format). + + sha1 and md5 are known to be breakable, so sha256 is a better option + when the hash is being used for security purposes. If hashing passwords + or anything else that needs to be retained for a long period a salted + hash is better. + """ + if isinstance(base_str, six.text_type): + base_str = base_str.encode('utf-8') + return hashlib.sha256(base_str).hexdigest() + + def filter_and_format_resource_metadata(resource_type, resource_list, search_filts, metadata_type=None): """Get all metadata for a list of resources after filtering.