Browse Source

Add default roles and scope checking to project tags

This commit makes it so that project tags adhere to system-scope and
also incorporates default roles into the policy checks by default.

Change-Id: Ie36df5677a08d7d95f056f3ea00eda05e1315ea5
Closes-Bug: 1844194
Closes-Bug: 1844193
Related-Bug: 1806762
changes/03/682503/6
Lance Bragstad 2 years ago
parent
commit
8e67249d5b
  1. 10
      api-ref/source/v3/project-tags.inc
  2. 3
      etc/policy.v3cloudsample.json
  3. 30
      keystone/api/projects.py
  4. 110
      keystone/common/policies/project.py
  5. 974
      keystone/tests/protection/v3/test_project_tags.py
  6. 2
      keystone/tests/unit/test_policy.py
  7. 43
      releasenotes/notes/bug-1844194-48ae60db49f91bd4.yaml

10
api-ref/source/v3/project-tags.inc

@ -19,6 +19,16 @@ Tags for projects have the following restrictions:
- Each project can have a maximum of 80 tags
- Each tag can be a maximum of 255 characters in length
.. warning::
We discourage the use of project tags for sensitive information like billing or
account codes. By default, access to project tags isn't exclusive to system
administrators or users. Domain and project administrators are allowed to
tag projects they have authorization to access. Domain and project users
(e.g., users with the ``member`` or ``reader`` roles) can view all project
tags on all projects within their domain or on projects they are
authorized to access.
List tags for a project
=======================

3
etc/policy.v3cloudsample.json

@ -13,9 +13,6 @@
"identity:update_limit": "rule:admin_required",
"identity:delete_limit": "rule:admin_required",
"identity:get_project_tag": "rule:admin_required",
"identity:list_project_tags": "rule:admin_required",
"domain_admin_matches_domain_role": "rule:admin_required and domain_id:%(role.domain_id)s",
"get_domain_roles": "rule:domain_admin_matches_target_domain_role or rule:project_admin_matches_target_domain_role",
"domain_admin_matches_target_domain_role": "rule:admin_required and domain_id:%(target.role.domain_id)s",

30
keystone/api/projects.py

@ -236,7 +236,10 @@ class ProjectTagsResource(_ProjectTagResourceBase):
GET /v3/projects/{project_id}/tags
"""
ENFORCER.enforce_call(action='identity:list_project_tags')
ENFORCER.enforce_call(
action='identity:list_project_tags',
build_target=_build_project_target_enforcement
)
ref = PROVIDERS.resource_api.list_project_tags(project_id)
return self.wrap_member(ref)
@ -245,7 +248,10 @@ class ProjectTagsResource(_ProjectTagResourceBase):
PUT /v3/projects/{project_id}/tags
"""
ENFORCER.enforce_call(action='identity:update_project_tags')
ENFORCER.enforce_call(
action='identity:update_project_tags',
build_target=_build_project_target_enforcement
)
tags = self.request_body_json.get('tags', {})
validation.lazy_validate(schema.project_tags_update, tags)
ref = PROVIDERS.resource_api.update_project_tags(
@ -257,7 +263,10 @@ class ProjectTagsResource(_ProjectTagResourceBase):
DELETE /v3/projects/{project_id}/tags
"""
ENFORCER.enforce_call(action='identity:delete_project_tags')
ENFORCER.enforce_call(
action='identity:delete_project_tags',
build_target=_build_project_target_enforcement
)
PROVIDERS.resource_api.update_project_tags(project_id, [])
return None, http_client.NO_CONTENT
@ -268,7 +277,10 @@ class ProjectTagResource(_ProjectTagResourceBase):
GET /v3/projects/{project_id}/tags/{value}
"""
ENFORCER.enforce_call(action='identity:get_project_tag')
ENFORCER.enforce_call(
action='identity:get_project_tag',
build_target=_build_project_target_enforcement,
)
PROVIDERS.resource_api.get_project_tag(project_id, value)
return None, http_client.NO_CONTENT
@ -277,7 +289,10 @@ class ProjectTagResource(_ProjectTagResourceBase):
PUT /v3/projects/{project_id}/tags/{value}
"""
ENFORCER.enforce_call(action='identity:create_project_tag')
ENFORCER.enforce_call(
action='identity:create_project_tag',
build_target=_build_project_target_enforcement
)
validation.lazy_validate(schema.project_tag_create, value)
# Check if we will exceed the max number of tags on this project
tags = PROVIDERS.resource_api.list_project_tags(project_id)
@ -298,7 +313,10 @@ class ProjectTagResource(_ProjectTagResourceBase):
/v3/projects/{project_id}/tags/{value}
"""
ENFORCER.enforce_call(action='identity:delete_project_tag')
ENFORCER.enforce_call(
action='identity:delete_project_tag',
build_target=_build_project_target_enforcement
)
PROVIDERS.resource_api.delete_project_tag(project_id, value)
return None, http_client.NO_CONTENT

110
keystone/common/policies/project.py

@ -21,6 +21,12 @@ SYSTEM_READER_OR_DOMAIN_READER_OR_PROJECT_USER = (
'project_id:%(target.project.id)s'
)
SYSTEM_ADMIN_OR_DOMAIN_ADMIN_OR_PROJECT_ADMIN = (
'(' + base.SYSTEM_ADMIN + ') or '
'(role:admin and domain_id:%(target.project.domain_id)s) or '
'(role:admin and project_id:%(target.project.id)s)'
)
# This policy is only written to be used to protect the
# /v3/users/{user_id}/projects API. It should not be used to protect
# /v3/project APIs because the target information contained in the last check
@ -70,6 +76,31 @@ deprecated_delete_project = policy.DeprecatedRule(
name=base.IDENTITY % 'delete_project',
check_str=base.RULE_ADMIN_REQUIRED
)
deprecated_list_project_tags = policy.DeprecatedRule(
name=base.IDENTITY % 'list_project_tags',
check_str=base.RULE_ADMIN_OR_TARGET_PROJECT
)
deprecated_get_project_tag = policy.DeprecatedRule(
name=base.IDENTITY % 'get_project_tag',
check_str=base.RULE_ADMIN_OR_TARGET_PROJECT
)
deprecated_update_project_tag = policy.DeprecatedRule(
name=base.IDENTITY % 'update_project_tags',
check_str=base.RULE_ADMIN_REQUIRED
)
deprecated_create_project_tag = policy.DeprecatedRule(
name=base.IDENTITY % 'create_project_tag',
check_str=base.RULE_ADMIN_REQUIRED
)
deprecated_delete_project_tag = policy.DeprecatedRule(
name=base.IDENTITY % 'delete_project_tag',
check_str=base.RULE_ADMIN_REQUIRED
)
deprecated_delete_project_tags = policy.DeprecatedRule(
name=base.IDENTITY % 'delete_project_tags',
check_str=base.RULE_ADMIN_REQUIRED
)
DEPRECATED_REASON = """
As of the Stein release, the project API understands how to handle
@ -79,6 +110,14 @@ administrators. The new default policies for this API account for these changes
automatically.
"""
TAGS_DEPRECATED_REASON = """
As of the Train release, the project tags API understands how to handle
system-scoped tokens in addition to project and domain tokens, making the API
more accessible to users without compromising security or manageability for
administrators. The new default policies for this API account for these changes
automatically.
"""
project_policies = [
policy.DocumentedRuleDefault(
name=base.IDENTITY % 'get_project',
@ -146,67 +185,68 @@ project_policies = [
deprecated_since=versionutils.deprecated.STEIN),
policy.DocumentedRuleDefault(
name=base.IDENTITY % 'list_project_tags',
check_str=base.RULE_ADMIN_OR_TARGET_PROJECT,
# FIXME(lbragstad): We need to make sure we check the project in the
# token scope when authorizing APIs for project tags. System
# administrators should be able to tag any project with anything.
# Domain administrators should only be able to tag projects within
# their domain. Project administrators should only be able to tag their
# project. Until we have support for these cases in code and tested, we
# should keep scope_types commented out.
# scope_types=['system', 'project'],
check_str=SYSTEM_READER_OR_DOMAIN_READER_OR_PROJECT_USER,
scope_types=['system', 'domain', 'project'],
description='List tags for a project.',
operations=[{'path': '/v3/projects/{project_id}/tags',
'method': 'GET'},
{'path': '/v3/projects/{project_id}/tags',
'method': 'HEAD'}]),
'method': 'HEAD'}],
deprecated_rule=deprecated_list_project_tags,
deprecated_reason=TAGS_DEPRECATED_REASON,
deprecated_since=versionutils.deprecated.TRAIN),
policy.DocumentedRuleDefault(
name=base.IDENTITY % 'get_project_tag',
check_str=base.RULE_ADMIN_OR_TARGET_PROJECT,
# FIXME(lbragstad): See the above comments as to why this is commented
# out.
# scope_types=['system', 'project'],
check_str=SYSTEM_READER_OR_DOMAIN_READER_OR_PROJECT_USER,
scope_types=['system', 'domain', 'project'],
description='Check if project contains a tag.',
operations=[{'path': '/v3/projects/{project_id}/tags/{value}',
'method': 'GET'},
{'path': '/v3/projects/{project_id}/tags/{value}',
'method': 'HEAD'}]),
'method': 'HEAD'}],
deprecated_rule=deprecated_get_project_tag,
deprecated_reason=TAGS_DEPRECATED_REASON,
deprecated_since=versionutils.deprecated.TRAIN),
policy.DocumentedRuleDefault(
name=base.IDENTITY % 'update_project_tags',
check_str=base.RULE_ADMIN_REQUIRED,
# FIXME(lbragstad): See the above comment for create_project as to why
# this is limited to only system-scope.
scope_types=['system'],
check_str=SYSTEM_ADMIN_OR_DOMAIN_ADMIN_OR_PROJECT_ADMIN,
scope_types=['system', 'domain', 'project'],
description='Replace all tags on a project with the new set of tags.',
operations=[{'path': '/v3/projects/{project_id}/tags',
'method': 'PUT'}]),
'method': 'PUT'}],
deprecated_rule=deprecated_update_project_tag,
deprecated_reason=TAGS_DEPRECATED_REASON,
deprecated_since=versionutils.deprecated.TRAIN),
policy.DocumentedRuleDefault(
name=base.IDENTITY % 'create_project_tag',
check_str=base.RULE_ADMIN_REQUIRED,
# FIXME(lbragstad): See the above comment for create_project as to why
# this is limited to only system-scope.
scope_types=['system'],
check_str=SYSTEM_ADMIN_OR_DOMAIN_ADMIN_OR_PROJECT_ADMIN,
scope_types=['system', 'domain', 'project'],
description='Add a single tag to a project.',
operations=[{'path': '/v3/projects/{project_id}/tags/{value}',
'method': 'PUT'}]),
'method': 'PUT'}],
deprecated_rule=deprecated_create_project_tag,
deprecated_reason=TAGS_DEPRECATED_REASON,
deprecated_since=versionutils.deprecated.TRAIN),
policy.DocumentedRuleDefault(
name=base.IDENTITY % 'delete_project_tags',
check_str=base.RULE_ADMIN_REQUIRED,
# FIXME(lbragstad): See the above comment for create_project as to why
# this is limited to only system-scope.
scope_types=['system'],
check_str=SYSTEM_ADMIN_OR_DOMAIN_ADMIN_OR_PROJECT_ADMIN,
scope_types=['system', 'domain', 'project'],
description='Remove all tags from a project.',
operations=[{'path': '/v3/projects/{project_id}/tags',
'method': 'DELETE'}]),
'method': 'DELETE'}],
deprecated_rule=deprecated_delete_project_tags,
deprecated_reason=TAGS_DEPRECATED_REASON,
deprecated_since=versionutils.deprecated.TRAIN),
policy.DocumentedRuleDefault(
name=base.IDENTITY % 'delete_project_tag',
check_str=base.RULE_ADMIN_REQUIRED,
# FIXME(lbragstad): See the above comment for create_project as to why
# this is limited to only system-scope.
scope_types=['system'],
check_str=SYSTEM_ADMIN_OR_DOMAIN_ADMIN_OR_PROJECT_ADMIN,
scope_types=['system', 'domain', 'project'],
description='Delete a specified tag from project.',
operations=[{'path': '/v3/projects/{project_id}/tags/{value}',
'method': 'DELETE'}])
'method': 'DELETE'}],
deprecated_rule=deprecated_delete_project_tag,
deprecated_reason=TAGS_DEPRECATED_REASON,
deprecated_since=versionutils.deprecated.TRAIN)
]

974
keystone/tests/protection/v3/test_project_tags.py

@ -0,0 +1,974 @@
# 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 uuid
from oslo_serialization import jsonutils
from six.moves import http_client
from keystone.common.policies import project as pp
from keystone.common import provider_api
import keystone.conf
from keystone.tests.common import auth as common_auth
from keystone.tests import unit
from keystone.tests.unit import base_classes
from keystone.tests.unit import ksfixtures
from keystone.tests.unit.ksfixtures import temporaryfile
CONF = keystone.conf.CONF
PROVIDERS = provider_api.ProviderAPIs
def _override_policy(policy_file):
# TODO(lbragstad): Remove this once the deprecated policies in
# keystone.common.policies.project have been removed. This is only
# here to make sure we test the new policies instead of the deprecated
# ones. Oslo.policy will OR deprecated policies with new policies to
# maintain compatibility and give operators a chance to update
# permissions or update policies without breaking users. This will
# cause these specific tests to fail since we're trying to correct this
# broken behavior with better scope checking.
with open(policy_file, 'w') as f:
overridden_policies = {
'identity:get_project_tag': (
pp.SYSTEM_READER_OR_DOMAIN_READER_OR_PROJECT_USER
),
'identity:list_project_tags': (
pp.SYSTEM_READER_OR_DOMAIN_READER_OR_PROJECT_USER
),
'identity:create_project_tag': (
pp.SYSTEM_ADMIN_OR_DOMAIN_ADMIN_OR_PROJECT_ADMIN
),
'identity:update_project_tags': (
pp.SYSTEM_ADMIN_OR_DOMAIN_ADMIN_OR_PROJECT_ADMIN
),
'identity:delete_project_tag': (
pp.SYSTEM_ADMIN_OR_DOMAIN_ADMIN_OR_PROJECT_ADMIN
),
'identity:delete_project_tags': (
pp.SYSTEM_ADMIN_OR_DOMAIN_ADMIN_OR_PROJECT_ADMIN
)
}
f.write(jsonutils.dumps(overridden_policies))
class _SystemUserTests(object):
def test_user_can_get_project_tag(self):
project = PROVIDERS.resource_api.create_project(
uuid.uuid4().hex,
unit.new_project_ref(domain_id=CONF.identity.default_domain_id)
)
tag = uuid.uuid4().hex
PROVIDERS.resource_api.create_project_tag(project['id'], tag)
with self.test_client() as c:
c.get(
'/v3/projects/%s/tags/%s' % (project['id'], tag),
headers=self.headers,
expected_status_code=http_client.NO_CONTENT
)
def test_user_can_list_project_tags(self):
project = PROVIDERS.resource_api.create_project(
uuid.uuid4().hex,
unit.new_project_ref(domain_id=CONF.identity.default_domain_id)
)
tag = uuid.uuid4().hex
PROVIDERS.resource_api.create_project_tag(project['id'], tag)
with self.test_client() as c:
r = c.get(
'/v3/projects/%s/tags' % project['id'], headers=self.headers
)
self.assertTrue(len(r.json['tags']) == 1)
self.assertEqual(tag, r.json['tags'][0])
class _SystemMemberAndReaderTagTests(object):
def test_user_cannot_create_project_tag(self):
project = PROVIDERS.resource_api.create_project(
uuid.uuid4().hex,
unit.new_project_ref(domain_id=CONF.identity.default_domain_id)
)
tag = uuid.uuid4().hex
with self.test_client() as c:
c.put(
'/v3/projects/%s/tags/%s' % (project['id'], tag),
headers=self.headers,
expected_status_code=http_client.FORBIDDEN
)
def test_user_cannot_update_project_tag(self):
project = PROVIDERS.resource_api.create_project(
uuid.uuid4().hex,
unit.new_project_ref(domain_id=CONF.identity.default_domain_id)
)
tag = uuid.uuid4().hex
PROVIDERS.resource_api.create_project_tag(project['id'], tag)
update = {"tags": [uuid.uuid4().hex]}
with self.test_client() as c:
c.put(
'/v3/projects/%s/tags' % project['id'], headers=self.headers,
json=update, expected_status_code=http_client.FORBIDDEN
)
def test_user_cannot_delete_project_tag(self):
project = PROVIDERS.resource_api.create_project(
uuid.uuid4().hex,
unit.new_project_ref(domain_id=CONF.identity.default_domain_id)
)
tag = uuid.uuid4().hex
PROVIDERS.resource_api.create_project_tag(project['id'], tag)
with self.test_client() as c:
c.delete(
'/v3/projects/%s/tags/%s' % (project['id'], tag),
headers=self.headers,
expected_status_code=http_client.FORBIDDEN
)
class _DomainAndProjectUserTagTests(object):
def test_user_cannot_create_project_tag(self):
project = PROVIDERS.resource_api.create_project(
uuid.uuid4().hex,
unit.new_project_ref(domain_id=CONF.identity.default_domain_id)
)
tag = uuid.uuid4().hex
with self.test_client() as c:
c.put(
'/v3/projects/%s/tags/%s' % (project['id'], tag),
headers=self.headers,
expected_status_code=http_client.FORBIDDEN
)
def test_user_cannot_update_project_tag(self):
project = PROVIDERS.resource_api.create_project(
uuid.uuid4().hex,
unit.new_project_ref(domain_id=CONF.identity.default_domain_id)
)
tag = uuid.uuid4().hex
PROVIDERS.resource_api.create_project_tag(project['id'], tag)
update = {"tags": [uuid.uuid4().hex]}
with self.test_client() as c:
c.put(
'/v3/projects/%s/tags' % project['id'], headers=self.headers,
json=update, expected_status_code=http_client.FORBIDDEN
)
def test_user_cannot_delete_project_tag(self):
project = PROVIDERS.resource_api.create_project(
uuid.uuid4().hex,
unit.new_project_ref(domain_id=CONF.identity.default_domain_id)
)
tag = uuid.uuid4().hex
PROVIDERS.resource_api.create_project_tag(project['id'], tag)
with self.test_client() as c:
c.delete(
'/v3/projects/%s/tags/%s' % (project['id'], tag),
headers=self.headers,
expected_status_code=http_client.FORBIDDEN
)
class SystemReaderTests(base_classes.TestCaseWithBootstrap,
common_auth.AuthTestMixin,
_SystemUserTests,
_SystemMemberAndReaderTagTests):
def setUp(self):
super(SystemReaderTests, self).setUp()
self.loadapp()
self.useFixture(ksfixtures.Policy(self.config_fixture))
self.config_fixture.config(group='oslo_policy', enforce_scope=True)
system_reader = unit.new_user_ref(
domain_id=CONF.identity.default_domain_id
)
self.user_id = PROVIDERS.identity_api.create_user(
system_reader
)['id']
PROVIDERS.assignment_api.create_system_grant_for_user(
self.user_id, self.bootstrapper.reader_role_id
)
auth = self.build_authentication_request(
user_id=self.user_id, password=system_reader['password'],
system=True
)
# Grab a token using the persona we're testing and prepare headers
# for requests we'll be making in the tests.
with self.test_client() as c:
r = c.post('/v3/auth/tokens', json=auth)
self.token_id = r.headers['X-Subject-Token']
self.headers = {'X-Auth-Token': self.token_id}
class SystemMemberTests(base_classes.TestCaseWithBootstrap,
common_auth.AuthTestMixin,
_SystemUserTests,
_SystemMemberAndReaderTagTests):
def setUp(self):
super(SystemMemberTests, self).setUp()
self.loadapp()
self.useFixture(ksfixtures.Policy(self.config_fixture))
self.config_fixture.config(group='oslo_policy', enforce_scope=True)
system_member = unit.new_user_ref(
domain_id=CONF.identity.default_domain_id
)
self.user_id = PROVIDERS.identity_api.create_user(
system_member
)['id']
PROVIDERS.assignment_api.create_system_grant_for_user(
self.user_id, self.bootstrapper.member_role_id
)
auth = self.build_authentication_request(
user_id=self.user_id, password=system_member['password'],
system=True
)
# Grab a token using the persona we're testing and prepare headers
# for requests we'll be making in the tests.
with self.test_client() as c:
r = c.post('/v3/auth/tokens', json=auth)
self.token_id = r.headers['X-Subject-Token']
self.headers = {'X-Auth-Token': self.token_id}
class SystemAdminTests(base_classes.TestCaseWithBootstrap,
common_auth.AuthTestMixin,
_SystemUserTests):
def setUp(self):
super(SystemAdminTests, self).setUp()
self.loadapp()
self.useFixture(ksfixtures.Policy(self.config_fixture))
self.config_fixture.config(group='oslo_policy', enforce_scope=True)
self.user_id = self.bootstrapper.admin_user_id
auth = self.build_authentication_request(
user_id=self.user_id,
password=self.bootstrapper.admin_password,
system=True
)
# Grab a token using the persona we're testing and prepare headers
# for requests we'll be making in the tests.
with self.test_client() as c:
r = c.post('/v3/auth/tokens', json=auth)
self.token_id = r.headers['X-Subject-Token']
self.headers = {'X-Auth-Token': self.token_id}
def test_user_can_create_project_tag(self):
project = PROVIDERS.resource_api.create_project(
uuid.uuid4().hex,
unit.new_project_ref(domain_id=CONF.identity.default_domain_id)
)
tag = uuid.uuid4().hex
with self.test_client() as c:
c.put(
'/v3/projects/%s/tags/%s' % (project['id'], tag),
headers=self.headers,
expected_status_code=http_client.CREATED
)
def test_user_can_update_project_tag(self):
project = PROVIDERS.resource_api.create_project(
uuid.uuid4().hex,
unit.new_project_ref(domain_id=CONF.identity.default_domain_id)
)
tag = uuid.uuid4().hex
PROVIDERS.resource_api.create_project_tag(project['id'], tag)
update = {"tags": [uuid.uuid4().hex]}
with self.test_client() as c:
c.put(
'/v3/projects/%s/tags' % project['id'], headers=self.headers,
json=update,
expected_status_code=http_client.OK
)
def test_user_can_delete_project_tag(self):
project = PROVIDERS.resource_api.create_project(
uuid.uuid4().hex,
unit.new_project_ref(domain_id=CONF.identity.default_domain_id)
)
tag = uuid.uuid4().hex
PROVIDERS.resource_api.create_project_tag(project['id'], tag)
with self.test_client() as c:
c.delete(
'/v3/projects/%s/tags/%s' % (project['id'], tag),
headers=self.headers
)
class _DomainUserTagTests(object):
def test_user_can_get_tag_for_project_in_domain(self):
project = PROVIDERS.resource_api.create_project(
uuid.uuid4().hex, unit.new_project_ref(domain_id=self.domain_id)
)
tag = uuid.uuid4().hex
PROVIDERS.resource_api.create_project_tag(project['id'], tag)
with self.test_client() as c:
c.get(
'/v3/projects/%s/tags/%s' % (project['id'], tag),
headers=self.headers,
expected_status_code=http_client.NO_CONTENT
)
def test_user_can_list_tags_for_project_in_domain(self):
project = PROVIDERS.resource_api.create_project(
uuid.uuid4().hex, unit.new_project_ref(domain_id=self.domain_id)
)
tag = uuid.uuid4().hex
PROVIDERS.resource_api.create_project_tag(project['id'], tag)
with self.test_client() as c:
r = c.get(
'/v3/projects/%s/tags' % project['id'], headers=self.headers
)
self.assertTrue(len(r.json['tags']) == 1)
self.assertEqual(tag, r.json['tags'][0])
def test_user_cannot_create_project_tag_outside_domain(self):
project = PROVIDERS.resource_api.create_project(
uuid.uuid4().hex,
unit.new_project_ref(domain_id=CONF.identity.default_domain_id)
)
tag = uuid.uuid4().hex
with self.test_client() as c:
c.put(
'/v3/projects/%s/tags/%s' % (project['id'], tag),
headers=self.headers,
expected_status_code=http_client.FORBIDDEN
)
def test_user_cannot_update_project_tag_outside_domain(self):
project = PROVIDERS.resource_api.create_project(
uuid.uuid4().hex,
unit.new_project_ref(domain_id=CONF.identity.default_domain_id)
)
tag = uuid.uuid4().hex
PROVIDERS.resource_api.create_project_tag(project['id'], tag)
new_tag = uuid.uuid4().hex
update = {"tags": [new_tag]}
with self.test_client() as c:
c.put(
'/v3/projects/%s/tags' % project['id'], headers=self.headers,
json=update, expected_status_code=http_client.FORBIDDEN
)
def test_user_cannot_delete_project_tag_outside_domain(self):
project = PROVIDERS.resource_api.create_project(
uuid.uuid4().hex,
unit.new_project_ref(domain_id=CONF.identity.default_domain_id)
)
tag = uuid.uuid4().hex
PROVIDERS.resource_api.create_project_tag(project['id'], tag)
with self.test_client() as c:
c.delete(
'/v3/projects/%s/tags/%s' % (project['id'], tag),
headers=self.headers,
expected_status_code=http_client.FORBIDDEN
)
def test_user_cannot_get_tag_for_project_outside_domain(self):
project = PROVIDERS.resource_api.create_project(
uuid.uuid4().hex,
unit.new_project_ref(domain_id=CONF.identity.default_domain_id)
)
tag = uuid.uuid4().hex
PROVIDERS.resource_api.create_project_tag(project['id'], tag)
with self.test_client() as c:
c.get(
'/v3/projects/%s/tags/%s' % (project['id'], tag),
headers=self.headers,
expected_status_code=http_client.FORBIDDEN
)
def test_user_cannot_list_tags_for_project_outside_domain(self):
project = PROVIDERS.resource_api.create_project(
uuid.uuid4().hex,
unit.new_project_ref(domain_id=CONF.identity.default_domain_id)
)
tag = uuid.uuid4().hex
PROVIDERS.resource_api.create_project_tag(project['id'], tag)
with self.test_client() as c:
c.get(
'/v3/projects/%s/tags' % project['id'],
headers=self.headers,
expected_status_code=http_client.FORBIDDEN
)
class _DomainMemberAndReaderTagTests(object):
def test_user_cannot_create_project_tag_in_domain(self):
project = PROVIDERS.resource_api.create_project(
uuid.uuid4().hex, unit.new_project_ref(domain_id=self.domain_id)
)
tag = uuid.uuid4().hex
with self.test_client() as c:
c.put(
'/v3/projects/%s/tags/%s' % (project['id'], tag),
headers=self.headers,
expected_status_code=http_client.FORBIDDEN
)
def test_user_cannot_update_project_tag_in_domain(self):
project = PROVIDERS.resource_api.create_project(
uuid.uuid4().hex, unit.new_project_ref(domain_id=self.domain_id)
)
tag = uuid.uuid4().hex
PROVIDERS.resource_api.create_project_tag(project['id'], tag)
new_tag = uuid.uuid4().hex
update = {"tags": [new_tag]}
with self.test_client() as c:
c.put(
'/v3/projects/%s/tags' % project['id'], headers=self.headers,
json=update, expected_status_code=http_client.FORBIDDEN
)
def test_user_cannot_delete_project_tag_in_domain(self):
project = PROVIDERS.resource_api.create_project(
uuid.uuid4().hex, unit.new_project_ref(domain_id=self.domain_id)
)
tag = uuid.uuid4().hex
PROVIDERS.resource_api.create_project_tag(project['id'], tag)
with self.test_client() as c:
c.delete(
'/v3/projects/%s/tags/%s' % (project['id'], tag),
headers=self.headers,
expected_status_code=http_client.FORBIDDEN
)
class DomainAdminUserTests(base_classes.TestCaseWithBootstrap,
common_auth.AuthTestMixin,
_DomainUserTagTests):
def setUp(self):
super(DomainAdminUserTests, self).setUp()
self.loadapp()
self.policy_file = self.useFixture(temporaryfile.SecureTempFile())
self.policy_file_name = self.policy_file.file_name
self.useFixture(
ksfixtures.Policy(
self.config_fixture, policy_file=self.policy_file_name
)
)
_override_policy(self.policy_file_name)
self.config_fixture.config(group='oslo_policy', enforce_scope=True)
domain = PROVIDERS.resource_api.create_domain(
uuid.uuid4().hex, unit.new_domain_ref()
)
self.domain_id = domain['id']
domain_admin = unit.new_user_ref(domain_id=self.domain_id)
self.user_id = PROVIDERS.identity_api.create_user(domain_admin)['id']
PROVIDERS.assignment_api.create_grant(
self.bootstrapper.admin_role_id, user_id=self.user_id,
domain_id=self.domain_id
)
auth = self.build_authentication_request(
user_id=self.user_id, password=domain_admin['password'],
domain_id=self.domain_id,
)
# Grab a token using the persona we're testing and prepare headers
# for requests we'll be making in the tests.
with self.test_client() as c:
r = c.post('/v3/auth/tokens', json=auth)
self.token_id = r.headers['X-Subject-Token']
self.headers = {'X-Auth-Token': self.token_id}
def test_user_can_create_project_tag_in_domain(self):
project = PROVIDERS.resource_api.create_project(
uuid.uuid4().hex, unit.new_project_ref(domain_id=self.domain_id)
)
tag = uuid.uuid4().hex
with self.test_client() as c:
c.put(
'/v3/projects/%s/tags/%s' % (project['id'], tag),
headers=self.headers, expected_status_code=http_client.CREATED
)
def test_user_can_update_project_tag_in_domain(self):
project = PROVIDERS.resource_api.create_project(
uuid.uuid4().hex, unit.new_project_ref(domain_id=self.domain_id)
)
tag = uuid.uuid4().hex
PROVIDERS.resource_api.create_project_tag(project['id'], tag)
new_tag = uuid.uuid4().hex
update = {"tags": [new_tag]}
with self.test_client() as c:
r = c.put(
'/v3/projects/%s/tags' % project['id'], headers=self.headers,
json=update, expected_status_code=http_client.OK
)
self.assertTrue(len(r.json['tags']) == 1)
self.assertEqual(new_tag, r.json['tags'][0])
def test_user_can_delete_project_tag_in_domain(self):
project = PROVIDERS.resource_api.create_project(
uuid.uuid4().hex, unit.new_project_ref(domain_id=self.domain_id)
)
tag = uuid.uuid4().hex
PROVIDERS.resource_api.create_project_tag(project['id'], tag)
with self.test_client() as c:
c.delete(
'/v3/projects/%s/tags/%s' % (project['id'], tag),
headers=self.headers
)
class DomainMemberUserTests(base_classes.TestCaseWithBootstrap,
common_auth.AuthTestMixin,
_DomainUserTagTests,
_DomainMemberAndReaderTagTests):
def setUp(self):
super(DomainMemberUserTests, self).setUp()
self.loadapp()
self.policy_file = self.useFixture(temporaryfile.SecureTempFile())
self.policy_file_name = self.policy_file.file_name
self.useFixture(
ksfixtures.Policy(
self.config_fixture, policy_file=self.policy_file_name
)
)
_override_policy(self.policy_file_name)
self.config_fixture.config(group='oslo_policy', enforce_scope=True)
domain = PROVIDERS.resource_api.create_domain(
uuid.uuid4().hex, unit.new_domain_ref()
)
self.domain_id = domain['id']
domain_admin = unit.new_user_ref(domain_id=self.domain_id)
self.user_id = PROVIDERS.identity_api.create_user(domain_admin)['id']
PROVIDERS.assignment_api.create_grant(
self.bootstrapper.member_role_id, user_id=self.user_id,
domain_id=self.domain_id
)
auth = self.build_authentication_request(
user_id=self.user_id, password=domain_admin['password'],
domain_id=self.domain_id,
)
# Grab a token using the persona we're testing and prepare headers
# for requests we'll be making in the tests.
with self.test_client() as c:
r = c.post('/v3/auth/tokens', json=auth)
self.token_id = r.headers['X-Subject-Token']
self.headers = {'X-Auth-Token': self.token_id}
class DomainReaderUserTests(base_classes.TestCaseWithBootstrap,
common_auth.AuthTestMixin,
_DomainUserTagTests,
_DomainMemberAndReaderTagTests):
def setUp(self):
super(DomainReaderUserTests, self).setUp()
self.loadapp()
self.policy_file = self.useFixture(temporaryfile.SecureTempFile())
self.policy_file_name = self.policy_file.file_name
self.useFixture(
ksfixtures.Policy(
self.config_fixture, policy_file=self.policy_file_name
)
)
_override_policy(self.policy_file_name)
self.config_fixture.config(group='oslo_policy', enforce_scope=True)
domain = PROVIDERS.resource_api.create_domain(
uuid.uuid4().hex, unit.new_domain_ref()
)
self.domain_id = domain['id']
domain_admin = unit.new_user_ref(domain_id=self.domain_id)
self.user_id = PROVIDERS.identity_api.create_user(domain_admin)['id']
PROVIDERS.assignment_api.create_grant(
self.bootstrapper.reader_role_id, user_id=self.user_id,
domain_id=self.domain_id
)
auth = self.build_authentication_request(
user_id=self.user_id, password=domain_admin['password'],
domain_id=self.domain_id,
)
# Grab a token using the persona we're testing and prepare headers
# for requests we'll be making in the tests.
with self.test_client() as c:
r = c.post('/v3/auth/tokens', json=auth)
self.token_id = r.headers['X-Subject-Token']
self.headers = {'X-Auth-Token': self.token_id}
class _ProjectUserTagTests(object):
def test_user_can_get_tag_for_project(self):
tag = uuid.uuid4().hex
PROVIDERS.resource_api.create_project_tag(self.project_id, tag)
with self.test_client() as c:
c.get(
'/v3/projects/%s/tags/%s' % (self.project_id, tag),
headers=self.headers,
expected_status_code=http_client.NO_CONTENT
)
def test_user_can_list_tags_for_project(self):
tag = uuid.uuid4().hex
PROVIDERS.resource_api.create_project_tag(self.project_id, tag)
with self.test_client() as c:
r = c.get(
'/v3/projects/%s/tags' % self.project_id, headers=self.headers
)
self.assertTrue(len(r.json['tags']) == 1)
self.assertEqual(tag, r.json['tags'][0])
def test_user_cannot_create_tag_for_other_project(self):
project = PROVIDERS.resource_api.create_project(
uuid.uuid4().hex,
unit.new_project_ref(domain_id=CONF.identity.default_domain_id)
)
tag = uuid.uuid4().hex
with self.test_client() as c:
c.put(
'/v3/projects/%s/tags/%s' % (project['id'], tag),
headers=self.headers,
expected_status_code=http_client.FORBIDDEN
)
def test_user_cannot_update_tag_for_other_project(self):
project = PROVIDERS.resource_api.create_project(
uuid.uuid4().hex,
unit.new_project_ref(domain_id=CONF.identity.default_domain_id)
)
tag = uuid.uuid4().hex
PROVIDERS.resource_api.create_project_tag(project['id'], tag)
update = {"tags": [uuid.uuid4().hex]}
with self.test_client() as c:
c.put(
'/v3/projects/%s/tags' % project['id'], headers=self.headers,
json=update, expected_status_code=http_client.FORBIDDEN
)
def test_user_cannot_delete_tag_for_other_project(self):
project = PROVIDERS.resource_api.create_project(
uuid.uuid4().hex,
unit.new_project_ref(domain_id=CONF.identity.default_domain_id)
)
tag = uuid.uuid4().hex
PROVIDERS.resource_api.create_project_tag(project['id'], tag)
with self.test_client() as c:
c.delete(
'/v3/projects/%s/tags/%s' % (project['id'], tag),
headers=self.headers,
expected_status_code=http_client.FORBIDDEN
)
def test_user_cannot_get_tag_for_other_project(self):
project = PROVIDERS.resource_api.create_project(
uuid.uuid4().hex,
unit.new_project_ref(domain_id=CONF.identity.default_domain_id)
)
tag = uuid.uuid4().hex
PROVIDERS.resource_api.create_project_tag(project['id'], tag)
with self.test_client() as c:
c.get(
'/v3/projects/%s/tags/%s' % (project['id'], tag),
headers=self.headers,
expected_status_code=http_client.FORBIDDEN
)
def test_user_cannot_list_tags_for_other_project(self):
project = PROVIDERS.resource_api.create_project(
uuid.uuid4().hex,
unit.new_project_ref(domain_id=CONF.identity.default_domain_id)
)
tag = uuid.uuid4().hex
PROVIDERS.resource_api.create_project_tag(project['id'], tag)
with self.test_client() as c:
c.get(
'/v3/projects/%s/tags' % project['id'],
headers=self.headers,
expected_status_code=http_client.FORBIDDEN
)
class _ProjectMemberAndReaderTagTests(object):
def test_user_cannot_create_project_tag(self):
tag = uuid.uuid4().hex
with self.test_client() as c:
c.put(
'/v3/projects/%s/tags/%s' % (self.project_id, tag),
headers=self.headers,
expected_status_code=http_client.FORBIDDEN
)
def test_user_cannot_update_project_tag(self):
tag = uuid.uuid4().hex
PROVIDERS.resource_api.create_project_tag(self.project_id, tag)
update = {"tags": [uuid.uuid4().hex]}
with self.test_client() as c:
c.put(
'/v3/projects/%s/tags' % self.project_id, headers=self.headers,
json=update, expected_status_code=http_client.FORBIDDEN
)
def test_user_cannot_delete_project_tag(self):
tag = uuid.uuid4().hex
PROVIDERS.resource_api.create_project_tag(self.project_id, tag)
with self.test_client() as c:
c.delete(
'/v3/projects/%s/tags/%s' % (self.project_id, tag),
headers=self.headers,
expected_status_code=http_client.FORBIDDEN
)
class ProjectAdminTests(base_classes.TestCaseWithBootstrap,
common_auth.AuthTestMixin,
_ProjectUserTagTests):
def setUp(self):
super(ProjectAdminTests, self).setUp()
self.loadapp()
self.policy_file = self.useFixture(temporaryfile.SecureTempFile())
self.policy_file_name = self.policy_file.file_name
self.useFixture(
ksfixtures.Policy(
self.config_fixture, policy_file=self.policy_file_name
)
)
_override_policy(self.policy_file_name)
self.config_fixture.config(group='oslo_policy', enforce_scope=True)
self.user_id = self.bootstrapper.admin_user_id
PROVIDERS.assignment_api.create_grant(
self.bootstrapper.admin_role_id, user_id=self.user_id,
project_id=self.bootstrapper.project_id
)
self.project_id = self.bootstrapper.project_id
auth = self.build_authentication_request(
user_id=self.user_id, password=self.bootstrapper.admin_password,
project_id=self.bootstrapper.project_id
)
# Grab a token using the persona we're testing and prepare headers
# for requests we'll be making in the tests.
with self.test_client() as c:
r = c.post('/v3/auth/tokens', json=auth)
self.token_id = r.headers['X-Subject-Token']
self.headers = {'X-Auth-Token': self.token_id}
def test_user_can_create_project_tag(self):
tag = uuid.uuid4().hex
with self.test_client() as c:
c.put(
'/v3/projects/%s/tags/%s' % (self.project_id, tag),
headers=self.headers, expected_status_code=http_client.CREATED
)
def test_user_can_update_project_tag(self):
tag = uuid.uuid4().hex
PROVIDERS.resource_api.create_project_tag(self.project_id, tag)
update = {"tags": [uuid.uuid4().hex]}
with self.test_client() as c:
c.put(
'/v3/projects/%s/tags' % self.project_id, headers=self.headers,
json=update, expected_status_code=http_client.OK
)
def test_user_can_delete_project_tag(self):
tag = uuid.uuid4().hex
PROVIDERS.resource_api.create_project_tag(self.project_id, tag)
with self.test_client() as c:
c.delete(
'/v3/projects/%s/tags/%s' % (self.project_id, tag),
headers=self.headers
)
class ProjectMemberTests(base_classes.TestCaseWithBootstrap,
common_auth.AuthTestMixin,
_ProjectUserTagTests,
_ProjectMemberAndReaderTagTests):
def setUp(self):
super(ProjectMemberTests, self).setUp()
self.loadapp()
self.policy_file = self.useFixture(temporaryfile.SecureTempFile())
self.policy_file_name = self.policy_file.file_name
self.useFixture(
ksfixtures.Policy(
self.config_fixture, policy_file=self.policy_file_name
)
)
_override_policy(self.policy_file_name)
self.config_fixture.config(group='oslo_policy', enforce_scope=True)
project = PROVIDERS.resource_api.create_project(
uuid.uuid4().hex,
unit.new_project_ref(domain_id=CONF.identity.default_domain_id)
)
self.project_id = project['id']
self.user_id = self.bootstrapper.admin_user_id
PROVIDERS.assignment_api.create_grant(
self.bootstrapper.member_role_id, user_id=self.user_id,
project_id=self.project_id
)
auth = self.build_authentication_request(
user_id=self.user_id, password=self.bootstrapper.admin_password,
project_id=self.project_id
)
# Grab a token using the persona we're testing and prepare headers
# for requests we'll be making in the tests.
with self.test_client() as c:
r = c.post('/v3/auth/tokens', json=auth)
self.token_id = r.headers['X-Subject-Token']
self.headers = {'X-Auth-Token': self.token_id}
class ProjectReaderTests(base_classes.TestCaseWithBootstrap,
common_auth.AuthTestMixin,
_ProjectUserTagTests,
_ProjectMemberAndReaderTagTests):
def setUp(self):
super(ProjectReaderTests, self).setUp()
self.loadapp()
self.policy_file = self.useFixture(temporaryfile.SecureTempFile())
self.policy_file_name = self.policy_file.file_name
self.useFixture(
ksfixtures.Policy(
self.config_fixture, policy_file=self.policy_file_name
)
)
_override_policy(self.policy_file_name)
self.config_fixture.config(group='oslo_policy', enforce_scope=True)
project = PROVIDERS.resource_api.create_project(
uuid.uuid4().hex,
unit.new_project_ref(domain_id=CONF.identity.default_domain_id)
)
self.project_id = project['id']
self.user_id = self.bootstrapper.admin_user_id
PROVIDERS.assignment_api.create_grant(
self.bootstrapper.reader_role_id, user_id=self.user_id,
project_id=self.project_id
)
auth = self.build_authentication_request(
user_id=self.user_id, password=self.bootstrapper.admin_password,
project_id=self.project_id
)
# Grab a token using the persona we're testing and prepare headers
# for requests we'll be making in the tests.
with self.test_client() as c:
r = c.post('/v3/auth/tokens', json=auth)
self.token_id = r.headers['X-Subject-Token']
self.headers = {'X-Auth-Token': self.token_id}

2
keystone/tests/unit/test_policy.py

@ -280,6 +280,7 @@ class PolicyJsonTestCase(unit.TestCase):
'identity:get_mapping',
'identity:get_policy',
'identity:get_policy_for_endpoint',
'identity:get_project_tag',
'identity:get_project',
'identity:get_protocol',
'identity:get_region',
@ -318,6 +319,7 @@ class PolicyJsonTestCase(unit.TestCase):
'identity:list_projects_associated_with_endpoint_group',
'identity:list_projects_for_endpoint',
'identity:list_projects_for_user',
'identity:list_project_tags',
'identity:list_protocols',
'identity:list_regions',
'identity:list_registered_limits',

43
releasenotes/notes/bug-1844194-48ae60db49f91bd4.yaml

@ -0,0 +1,43 @@
---
features:
- |
[`bug 1844194 <https://bugs.launchpad.net/keystone/+bug/1844194>`_]
[`bug 1844193 <https://bugs.launchpad.net/keystone/+bug/1844193>`_]
The project tags API now supports the ``admin``, ``member``, and ``reader``
default roles.
upgrade:
- |
[`bug 1844194 <https://bugs.launchpad.net/keystone/+bug/1844194>`_]
[`bug 1844193 <https://bugs.launchpad.net/keystone/+bug/1844193>`_]
The project tags API now uses new default policies that make it more
accessible to end users and administrators in a secure way. Please
consider these new defaults if your deployment overrides the project
tags policies.
deprecations:
- |
[`bug 1844194 <https://bugs.launchpad.net/keystone/+bug/1844194>`_]
[`bug 1844193 <https://bugs.launchpad.net/keystone/+bug/1844193>`_]
The project tags API policies have been deprecated. The
``identity:get_project_tag`` and ``identity:list_project_tags``
policies now use ``(role:reader and system_scope:all) or
(role:reader and domain_id:%(target.project.domain_id)s) or
project_id:%(target.project.id)s`` instead of
``rule:admin_required or project_id:%(target.project.id)s``. The
``identity:update_project_tags``, ``identity:delete_project_tags``,
``identity:delete_project_tag``, and ``identity:create_project_tag``
policies now use ``(role:admin and system_scope:all) or (role:admin
and domain_id:%(target.project.domain_id)s) or (role:admin and
project_id:%(target.project.id)s)`` instead of
``rule:admin_required``.
These new defaults automatically account for system-scope and support
a read-only role, making it easier for system administrators to
delegate subsets of responsibility with compromising security. Please
consider these new defaults if your deployment overrides the project
tag policies.
security:
- |
[`bug 1844194 <https://bugs.launchpad.net/keystone/+bug/1844194>`_]
[`bug 1844193 <https://bugs.launchpad.net/keystone/+bug/1844193>`_]
The project tags API now uses system-scope and default roles to
provide better accessibility to users in a secure way.
Loading…
Cancel
Save