Implement project personas for quotas and quota_classes

This commit updates the quota policies to use default roles provided by
keystone, including read-only support. This is inline with broader
changes across the OpenStack community to provide a more consistent
authorization experience.

Future changes will incorporate system scope when cinder fully supports
it.

This commit also updates the roles of a quotas test to not use the
'admin' role for checking the policy is not authorized. Instead, use
'member' and 'reader', which seem to be more realistic values for the
test since anyone with 'admin' can do just about anything in the
deployment.

Co-Authored-by: Alan Bishop <abishop@redhat.com>

Change-Id: I1b72a78180da46ff4a6257903d76069389f2a1f4
This commit is contained in:
Lance Bragstad 2021-09-08 16:35:54 -07:00 committed by Alan Bishop
parent 81e0da35dc
commit e0f4ab2286
6 changed files with 286 additions and 10 deletions

View File

@ -40,7 +40,7 @@ class QuotaClassSetsController(wsgi.Controller):
def show(self, req, id):
context = req.environ['cinder.context']
context.authorize(policy.MANAGE_POLICY)
context.authorize(policy.GET_POLICY)
try:
db.sqlalchemy.api.authorize_quota_class_context(context, id)
except exception.NotAuthorized:
@ -54,7 +54,7 @@ class QuotaClassSetsController(wsgi.Controller):
@validation.schema(quota_class.update_quota_class)
def update(self, req, id, body):
context = req.environ['cinder.context']
context.authorize(policy.MANAGE_POLICY)
context.authorize(policy.UPDATE_POLICY)
self.validate_string_length(id, 'quota_class_name',
min_length=1, max_length=255)

View File

@ -18,24 +18,45 @@ from oslo_policy import policy
from cinder.policies import base
# MANAGE_POLICY is deprecated
MANAGE_POLICY = 'volume_extension:quota_classes'
GET_POLICY = 'volume_extension:quota_classes:get'
UPDATE_POLICY = 'volume_extension:quota_classes:update'
deprecated_manage_policy = base.CinderDeprecatedRule(
name=MANAGE_POLICY,
check_str=base.RULE_ADMIN_API,
deprecated_reason=(f'{MANAGE_POLICY} has been replaced by more granular '
'policies that separately govern GET and PUT '
'operations.'),
)
quota_class_policies = [
policy.DocumentedRuleDefault(
name=MANAGE_POLICY,
name=GET_POLICY,
check_str=base.RULE_ADMIN_API,
description="Show or update project quota class.",
description="Show project quota class.",
operations=[
{
'method': 'GET',
'path': '/os-quota-class-sets/{project_id}'
},
}
],
deprecated_rule=deprecated_manage_policy,
),
policy.DocumentedRuleDefault(
name=UPDATE_POLICY,
check_str=base.RULE_ADMIN_API,
description="Update project quota class.",
operations=[
{
'method': 'PUT',
'path': '/os-quota-class-sets/{project_id}'
}
]),
],
deprecated_rule=deprecated_manage_policy,
),
]

View File

@ -22,11 +22,15 @@ SHOW_POLICY = 'volume_extension:quotas:show'
UPDATE_POLICY = 'volume_extension:quotas:update'
DELETE_POLICY = 'volume_extension:quotas:delete'
deprecated_show_policy = policy.DeprecatedRule(
name=SHOW_POLICY,
check_str=base.RULE_ADMIN_OR_OWNER
)
quota_policies = [
policy.DocumentedRuleDefault(
name=SHOW_POLICY,
check_str=base.RULE_ADMIN_OR_OWNER,
check_str=base.SYSTEM_READER_OR_PROJECT_READER,
description="Show project quota (including usage and default).",
operations=[
{
@ -41,7 +45,9 @@ quota_policies = [
'method': 'GET',
'path': '/os-quota-sets/{project_id}?usage=True'
}
]),
],
deprecated_rule=deprecated_show_policy,
),
policy.DocumentedRuleDefault(
name=UPDATE_POLICY,
check_str=base.RULE_ADMIN_API,
@ -51,7 +57,8 @@ quota_policies = [
'method': 'PUT',
'path': '/os-quota-sets/{project_id}'
}
]),
]
),
policy.DocumentedRuleDefault(
name=DELETE_POLICY,
check_str=base.RULE_ADMIN_API,
@ -61,7 +68,8 @@ quota_policies = [
'method': 'DELETE',
'path': '/os-quota-sets/{project_id}'
}
]),
]
),
]

View File

@ -134,6 +134,7 @@ class QuotaSetsControllerTest(QuotaSetsControllerTestBase):
self.req.environ['cinder.context'].is_admin = False
self.req.environ['cinder.context'].user_id = fake.USER_ID
self.req.environ['cinder.context'].project_id = fake.PROJECT_ID
self.req.environ['cinder.context'].roles = ['member', 'reader']
self.assertRaises(exception.PolicyNotAuthorized, self.controller.show,
self.req, fake.PROJECT2_ID)

View File

@ -0,0 +1,99 @@
# Copyright 2021 Red Hat, Inc.
# 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.
import ddt
from cinder.api.contrib import quota_classes
from cinder.api import microversions as mv
from cinder.policies import quota_class as policy
from cinder.tests.unit.api import fakes as fake_api
from cinder.tests.unit.policies import base
@ddt.ddt
class QuotaClassPolicyTest(base.BasePolicyTest):
authorized_admins = [
'legacy_admin',
'system_admin',
'project_admin',
]
unauthorized_admins = [
'legacy_owner',
'system_member',
'system_reader',
'system_foo',
'project_member',
'project_reader',
'project_foo',
'other_project_member',
'other_project_reader',
]
unauthorized_exceptions = []
# Basic policy test is without enforcing scope (which cinder doesn't
# yet support) and deprecated rules enabled.
def setUp(self, enforce_scope=False, enforce_new_defaults=False,
*args, **kwargs):
super().setUp(enforce_scope, enforce_new_defaults, *args, **kwargs)
self.controller = quota_classes.QuotaClassSetsController()
self.api_path = '/v3/os-quota-class-sets'
self.api_version = mv.BASE_VERSION
@ddt.data(*base.all_users)
def test_get_policy(self, user_id):
rule_name = policy.GET_POLICY
req = fake_api.HTTPRequest.blank(self.api_path,
version=self.api_version)
self.common_policy_check(user_id, self.authorized_admins,
self.unauthorized_admins,
self.unauthorized_exceptions,
rule_name, self.controller.show,
req, id='my_class')
@ddt.data(*base.all_users)
def test_update_policy(self, user_id):
rule_name = policy.UPDATE_POLICY
req = fake_api.HTTPRequest.blank(self.api_path,
version=self.api_version)
req.method = 'PUT'
body = {
"quota_class_set": {
"groups": 11,
"volumes": 5,
"backups": 4
}
}
self.common_policy_check(user_id, self.authorized_admins,
self.unauthorized_admins,
self.unauthorized_exceptions,
rule_name, self.controller.update,
req, id='my_class', body=body)
class QuotaClassPolicySecureRbacTest(QuotaClassPolicyTest):
# NOTE(Xena): The authorized_admins and unauthorized_admins are the same
# as the QuotasPolicyTest's. This is because in Xena the "admin only"
# rules are the legacy RULE_ADMIN_API. This will change in Yoga, when
# RULE_ADMIN_API will be deprecated in favor of the SYSTEM_ADMIN rule that
# is scope based.
def setUp(self, *args, **kwargs):
# Test secure RBAC by disabling deprecated policy rules (scope
# is still not enabled).
super().setUp(enforce_scope=False, enforce_new_defaults=True,
*args, **kwargs)

View File

@ -0,0 +1,147 @@
# Copyright 2021 Red Hat, Inc.
# 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.
import ddt
from cinder.api.contrib import quotas
from cinder.api import microversions as mv
from cinder.policies import quotas as policy
from cinder.tests.unit.api import fakes as fake_api
from cinder.tests.unit.policies import base
@ddt.ddt
class QuotasPolicyTest(base.BasePolicyTest):
authorized_users = [
'legacy_admin',
'legacy_owner',
'system_admin',
'project_admin',
'project_member',
'project_reader',
'project_foo',
]
unauthorized_users = [
'system_member',
'system_reader',
'system_foo',
'other_project_member',
'other_project_reader',
]
authorized_admins = [
'legacy_admin',
'system_admin',
'project_admin',
]
unauthorized_admins = [
'legacy_owner',
'system_member',
'system_reader',
'system_foo',
'project_member',
'project_reader',
'project_foo',
'other_project_member',
'other_project_reader',
]
unauthorized_exceptions = []
# Basic policy test is without enforcing scope (which cinder doesn't
# yet support) and deprecated rules enabled.
def setUp(self, enforce_scope=False, enforce_new_defaults=False,
*args, **kwargs):
super().setUp(enforce_scope, enforce_new_defaults, *args, **kwargs)
self.controller = quotas.QuotaSetsController()
self.api_path = '/v3/os-quota-sets'
self.api_version = mv.BASE_VERSION
@ddt.data(*base.all_users)
def test_show_policy(self, user_id):
rule_name = policy.SHOW_POLICY
req = fake_api.HTTPRequest.blank(self.api_path,
version=self.api_version)
self.common_policy_check(user_id, self.authorized_users,
self.unauthorized_users,
self.unauthorized_exceptions,
rule_name, self.controller.show,
req, id=self.project_id)
@ddt.data(*base.all_users)
def test_update_policy(self, user_id):
rule_name = policy.UPDATE_POLICY
req = fake_api.HTTPRequest.blank(self.api_path,
version=self.api_version)
req.method = 'PUT'
body = {
"quota_set": {
"groups": 11,
"volumes": 5,
"backups": 4
}
}
self.common_policy_check(user_id, self.authorized_admins,
self.unauthorized_admins,
self.unauthorized_exceptions,
rule_name, self.controller.update,
req, id=self.project_id, body=body)
@ddt.data(*base.all_users)
def test_delete_policy(self, user_id):
rule_name = policy.DELETE_POLICY
req = fake_api.HTTPRequest.blank(self.api_path,
version=self.api_version)
req.method = 'DELETE'
self.common_policy_check(user_id, self.authorized_admins,
self.unauthorized_admins,
self.unauthorized_exceptions,
rule_name, self.controller.delete,
req, id=self.project_id)
class QuotasPolicySecureRbacTest(QuotasPolicyTest):
authorized_users = [
'legacy_admin',
'system_admin',
'project_admin',
'project_member',
'project_reader',
]
unauthorized_users = [
'legacy_owner',
'system_member',
'system_foo',
'project_foo',
'other_project_member',
'other_project_reader',
]
# NOTE(Xena): The authorized_admins and unauthorized_admins are the same
# as the QuotasPolicyTest's. This is because in Xena the "admin only"
# rules are the legacy RULE_ADMIN_API. This will change in Yoga, when
# RULE_ADMIN_API will be deprecated in favor of the SYSTEM_ADMIN rule that
# is scope based.
def setUp(self, *args, **kwargs):
# Test secure RBAC by disabling deprecated policy rules (scope
# is still not enabled).
super().setUp(enforce_scope=False, enforce_new_defaults=True,
*args, **kwargs)