Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
187 changes: 187 additions & 0 deletions tempest/api/compute/admin/test_flavor_permission_rules.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
# Copyright (c) 2026 SAP SE
# All Rights Reserved.
#
# 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_utils import uuidutils

from tempest.api.compute import base
from tempest import config
from tempest.lib.common.utils import data_utils
from tempest.lib import decorators
from tempest.lib import exceptions as lib_exc

CONF = config.CONF


class FlavorPermissionRulesAdminTest(base.BaseV2ComputeAdminTest):
"""Tests the flavor permission rules API with admin privileges.

All policies default to admin, whose check_str carries no caller
domain or project match, so the admin may manage rules for any domain_id
and project_id. The endpoint does not validate them against keystone, so
the management tests use synthetic random ids for isolating flavor
permission rule testing.
"""

@classmethod
def resource_setup(cls):
super(FlavorPermissionRulesAdminTest, cls).resource_setup()
cls.client = cls.admin_flavor_permission_rules_client
# Shared fixtures with synthetic domain and project ids for list tests
cls.flavor_ref = CONF.compute.flavor_ref
cls.flavor_ref_alt = CONF.compute.flavor_ref_alt
cls.domain_id = data_utils.rand_uuid()
cls.domain_id_alt = data_utils.rand_uuid()
cls.project_id = data_utils.rand_uuid()
cls.project_id_alt = data_utils.rand_uuid()
cls.domain_deny = cls.create_flavor_permission_rule(
domain_id=cls.domain_id, effect='deny')
cls.domain_allow_flavor = cls.create_flavor_permission_rule(
domain_id=cls.domain_id, effect='allow',
flavor_id=cls.flavor_ref)
cls.project_allow = cls.create_flavor_permission_rule(
domain_id=cls.domain_id, project_id=cls.project_id, effect='allow')
cls.other_project_deny_flavor = cls.create_flavor_permission_rule(
domain_id=cls.domain_id, project_id=cls.project_id_alt,
effect='deny', flavor_id=cls.flavor_ref_alt)
cls.other_domain_allow = cls.create_flavor_permission_rule(
domain_id=cls.domain_id_alt, effect='allow')

def _list_ids(self, **filters):
return {r['id'] for r in self.client.list_flavor_permission_rules(
**filters)['flavor_permission_rules']}

def _assert_rule(self, expected, actual):
"""Assert a returned rule matches the expected values."""
self.assertTrue(uuidutils.is_uuid_like(actual['id']),
f"rule id {actual['id']} is not a uuid")
self.assertEqual(expected['domain_id'], actual['domain_id'])
self.assertEqual(expected.get('project_id'), actual['project_id'])
self.assertEqual(expected.get('flavor_id'), actual['flavor_id'])
self.assertEqual(expected['effect'], actual['effect'])
expected_scope = 'project' if expected.get('project_id') else 'domain'
self.assertEqual(expected_scope, actual['scope'])

@decorators.idempotent_id('2d6b935a-ec9e-437e-8a8b-0f9cefefccff')
def test_create_show_delete_domain_rule(self):
"""Create, show and delete a domain-scoped flavor permission rule."""
params = dict(domain_id=data_utils.rand_uuid(), effect='deny')
rule = self.create_flavor_permission_rule(**params)
self._assert_rule(params, rule)

shown = self.client.show_flavor_permission_rule(
rule['id'])['flavor_permission_rule']
self._assert_rule(params, shown)

self.client.delete_flavor_permission_rule(rule['id'])
self.assertRaises(lib_exc.NotFound,
self.client.show_flavor_permission_rule, rule['id'])

@decorators.idempotent_id('5abdefbd-d953-4aa8-a6c5-304b286547f2')
def test_create_project_rule(self):
"""A rule with a project_id is project-scoped."""
params = dict(domain_id=data_utils.rand_uuid(),
project_id=data_utils.rand_uuid(), effect='deny')
rule = self.create_flavor_permission_rule(**params)
self._assert_rule(params, rule)

@decorators.idempotent_id('501f9c21-137e-46de-b28b-a83e5f6372c5')
def test_create_flavor_scoped_rule(self):
"""A flavor-scoped rule echoes the public flavor ref."""
params = dict(domain_id=data_utils.rand_uuid(), effect='allow',
flavor_id=self.flavor_ref)
rule = self.create_flavor_permission_rule(**params)
self._assert_rule(params, rule)

@decorators.idempotent_id('e138b518-0da7-45b0-b5c0-57de59bb41a0')
def test_update_show_rule_effect(self):
"""The effect of a rule can be updated."""
rule = self.create_flavor_permission_rule(
domain_id=data_utils.rand_uuid(), effect='deny')

updated = self.client.update_flavor_permission_rule(
rule['id'], effect='allow')['flavor_permission_rule']
self.assertEqual('allow', updated['effect'])

shown = self.client.show_flavor_permission_rule(
rule['id'])['flavor_permission_rule']
self.assertEqual('allow', shown['effect'])

@decorators.idempotent_id('b409a77f-1d21-4fa9-9913-b623803483ab')
def test_list_filter_by_domain_id(self):
"""Filter by domain_id"""
self.assertEqual(
{self.domain_deny['id'], self.domain_allow_flavor['id'],
self.project_allow['id'], self.other_project_deny_flavor['id']},
self._list_ids(domain_id=self.domain_id))
self.assertEqual({self.other_domain_allow['id']},
self._list_ids(domain_id=self.domain_id_alt))

@decorators.idempotent_id('90a8a227-4084-4e02-893c-b3c1b0ea768e')
def test_list_filter_by_project_id(self):
"""Filter by project_id"""
self.assertEqual({self.project_allow['id']},
self._list_ids(project_id=self.project_id))

@decorators.idempotent_id('95668109-c57f-4565-8e4a-c4eba8465f7d')
def test_list_filter_by_effect(self):
"""Filter by effect (allow/deny)"""
self.assertEqual(
{self.domain_allow_flavor['id'], self.project_allow['id']},
self._list_ids(domain_id=self.domain_id, effect='allow'))
self.assertEqual(
{self.domain_deny['id'], self.other_project_deny_flavor['id']},
self._list_ids(domain_id=self.domain_id, effect='deny'))

@decorators.idempotent_id('3ca050ad-52d7-4d28-acaa-9f555d0db6fb')
def test_list_filter_by_scope(self):
"""Filter by scope (domain/project)"""
self.assertEqual(
{self.project_allow['id'], self.other_project_deny_flavor['id']},
self._list_ids(domain_id=self.domain_id, scope='project'))
self.assertEqual(
{self.domain_deny['id'], self.domain_allow_flavor['id']},
self._list_ids(domain_id=self.domain_id, scope='domain'))

@decorators.idempotent_id('50e0b13f-c340-4171-b96d-c917db1d106a')
def test_list_filter_by_flavor_id(self):
"""Filter by flavor_id"""
self.assertEqual(
{self.domain_allow_flavor['id']},
self._list_ids(
domain_id=self.domain_id, flavor_id=self.flavor_ref))

@decorators.idempotent_id('3291f21c-66e5-4b1a-8342-a8422b97c76f')
def test_list_filter_by_has_flavor(self):
"""Filter by has_flavor (True/False)"""
self.assertEqual(
{self.domain_deny['id'], self.project_allow['id']},
self._list_ids(domain_id=self.domain_id, has_flavor=False))
self.assertEqual(
{self.domain_allow_flavor['id'],
self.other_project_deny_flavor['id']},
self._list_ids(domain_id=self.domain_id, has_flavor=True))

@decorators.idempotent_id('788b53aa-9013-487b-95e8-6ea68a8e9b69')
def test_list_pagination(self):
"""Pagination with limit and marker"""
resp = self.client.list_flavor_permission_rules(
domain_id=self.domain_id, limit=2)
page = resp['flavor_permission_rules']
self.assertEqual(2, len(page))
self.assertIn('flavor_permission_rules_links', resp)
page_ids = {r['id'] for r in page}
self.assertEqual(
self._list_ids(domain_id=self.domain_id) - page_ids,
self._list_ids(domain_id=self.domain_id, marker=page[-1]['id']))
168 changes: 168 additions & 0 deletions tempest/api/compute/admin/test_flavor_permission_rules_negative.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
# Copyright (c) 2026 SAP SE
# All Rights Reserved.
#
# 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 tempest.api.compute import base
from tempest import config
from tempest.lib.common.utils import data_utils
from tempest.lib import decorators
from tempest.lib import exceptions as lib_exc

CONF = config.CONF


class FlavorPermissionRulesNegativeTest(base.BaseV2ComputeAdminTest):
"""
Negative tests for the flavor permission rules API with admin privileges
"""

@classmethod
def resource_setup(cls):
super(FlavorPermissionRulesNegativeTest, cls).resource_setup()
cls.client = cls.admin_flavor_permission_rules_client

@decorators.attr(type=['negative'])
@decorators.idempotent_id('51973116-514d-4e0a-a96c-9ce1212bd5c0')
def test_create_duplicate_rule(self):
"""Creating duplicate rules raises conflict"""
domain_id = data_utils.rand_uuid()
self.create_flavor_permission_rule(domain_id=domain_id, effect='deny')
self.assertRaises(
lib_exc.Conflict, self.client.create_flavor_permission_rule,
domain_id=domain_id, effect='deny')

@decorators.attr(type=['negative'])
@decorators.idempotent_id('3f7ad1e1-77c5-4b55-a349-aeaad40cffa2')
def test_create_with_nonexistent_flavor(self):
"""Creating rules for non-existent flavors raises not found"""
self.assertRaises(
lib_exc.NotFound, self.client.create_flavor_permission_rule,
domain_id=data_utils.rand_uuid(), effect='allow',
flavor_id=data_utils.rand_uuid())

@decorators.attr(type=['negative'])
@decorators.idempotent_id('95d87ee7-4370-4aa7-a727-ea9779ed28e6')
def test_create_rule_for_non_public_flavor(self):
"""Creating rules for private flavors raises bad request"""
flavor = self.create_flavor(ram=512, vcpus=1, disk=1,
is_public='False')
self.assertRaises(
lib_exc.BadRequest, self.client.create_flavor_permission_rule,
domain_id=data_utils.rand_uuid(), effect='allow',
flavor_id=flavor['id'])

@decorators.attr(type=['negative'])
@decorators.idempotent_id('124deaa1-8178-47b2-9e29-940b6ececd6e')
def test_create_missing_domain_id(self):
"""Creating rules without domain_id raises bad request"""
self.assertRaises(
lib_exc.BadRequest, self.client.create_flavor_permission_rule,
effect='deny')

@decorators.attr(type=['negative'])
@decorators.idempotent_id('fd263ad5-c671-4783-b00e-c748e22e89cf')
def test_create_missing_effect(self):
"""Creating rules without effect raises bad request."""
self.assertRaises(
lib_exc.BadRequest, self.client.create_flavor_permission_rule,
domain_id=data_utils.rand_uuid())

@decorators.attr(type=['negative'])
@decorators.idempotent_id('49adf92b-28f9-4af1-8b38-239ca2096993')
def test_create_invalid_effect(self):
"""Creating rules with invalid effect raises bad request"""
self.assertRaises(
lib_exc.BadRequest, self.client.create_flavor_permission_rule,
domain_id=data_utils.rand_uuid(), effect='unsupported')

@decorators.attr(type=['negative'])
@decorators.idempotent_id('918c52e9-86d7-4a15-8765-06c741c35d37')
def test_create_additional_property(self):
"""Creating rules with unsupported properties raises bad request"""
self.assertRaises(
lib_exc.BadRequest, self.client.create_flavor_permission_rule,
domain_id=data_utils.rand_uuid(), effect='deny', unsupported='x')

@decorators.attr(type=['negative'])
@decorators.idempotent_id('ed625fb4-642e-43d4-9dcd-47b0f780db45')
def test_show_update_delete_nonexistent_rule(self):
"""Show/update/delete of non-existing rules raises not found"""
rule_id = data_utils.rand_uuid()
self.assertRaises(lib_exc.NotFound,
self.client.show_flavor_permission_rule, rule_id)
self.assertRaises(lib_exc.NotFound,
self.client.update_flavor_permission_rule,
rule_id, effect='allow')
self.assertRaises(lib_exc.NotFound,
self.client.delete_flavor_permission_rule, rule_id)

@decorators.attr(type=['negative'])
@decorators.idempotent_id('e6a295b5-3942-41e5-af14-65bb6071e035')
def test_update_invalid_effect(self):
"""Updating a rule with an invalid effect raises bad request"""
rule = self.create_flavor_permission_rule(
domain_id=data_utils.rand_uuid(), effect='deny')
self.assertRaises(
lib_exc.BadRequest, self.client.update_flavor_permission_rule,
rule['id'], effect='unsupported')

@decorators.attr(type=['negative'])
@decorators.idempotent_id('e0856298-bc49-4cc8-b36c-69377613d1b1')
def test_update_missing_effect(self):
"""Updating a rule without an effect raises bad request"""
rule = self.create_flavor_permission_rule(
domain_id=data_utils.rand_uuid(), effect='deny')
self.assertRaises(
lib_exc.BadRequest, self.client.update_flavor_permission_rule,
rule['id'])

@decorators.attr(type=['negative'])
@decorators.idempotent_id('930edc2d-c834-464a-828d-bf07bedbdfc5')
def test_list_flavor_id_and_has_flavor_mutually_exclusive(self):
"""Listing with both flavor_id and has_flavor raises bad request"""
self.assertRaises(
lib_exc.BadRequest, self.client.list_flavor_permission_rules,
flavor_id=CONF.compute.flavor_ref, has_flavor=True)

@decorators.attr(type=['negative'])
@decorators.idempotent_id('529a41b5-0417-49d7-b1e3-01318e0444f3')
def test_list_with_unknown_marker(self):
"""Listing with unknown marker raises bad request"""
self.assertRaises(
lib_exc.BadRequest, self.client.list_flavor_permission_rules,
marker=data_utils.rand_uuid())

@decorators.attr(type=['negative'])
@decorators.idempotent_id('a9924860-e5d0-4db4-a61e-759c363428a0')
def test_list_with_nonexistent_flavor(self):
"""Listing with a non-existent flavor raises not found"""
self.assertRaises(
lib_exc.NotFound, self.client.list_flavor_permission_rules,
flavor_id=data_utils.rand_uuid())

@decorators.attr(type=['negative'])
@decorators.idempotent_id('9fb1910c-83cf-47e1-9b77-613dcfe6a05f')
def test_list_with_invalid_scope(self):
"""Listing with an invalid scope filter raises bad request"""
self.assertRaises(
lib_exc.BadRequest, self.client.list_flavor_permission_rules,
scope='unsupported')

@decorators.attr(type=['negative'])
@decorators.idempotent_id('eb7bb63e-7e6d-4087-8bf8-e50ee4938030')
def test_list_with_invalid_has_flavor(self):
"""Listing with a non-boolean has_flavor value raises bad request"""
self.assertRaises(
lib_exc.BadRequest, self.client.list_flavor_permission_rules,
has_flavor='maybe')
Loading