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
6 changes: 6 additions & 0 deletions doc/source/getting-started/policy_mapping.rst
Original file line number Diff line number Diff line change
Expand Up @@ -49,17 +49,23 @@ identity:get_project GET /v3/projects/{pro
identity:list_projects GET /v3/projects
identity:list_user_projects GET /v3/users/{user_id}/projects
identity:create_project POST /v3/projects
identity:create_project:provider_tags
identity:update_project PATCH /v3/projects/{project_id}
identity:update_project:provider_tags
identity:delete_project DELETE /v3/projects/{project_id}

identity:get_project_tag GET /v3/projects/{project_id}/tags/{tag_name}
HEAD /v3/projects/{project_id}/tags/{tag_name}
identity:list_project_tags GET /v3/projects/{project_id}/tags
HEAD /v3/projects/{project_id}/tags
identity:create_project_tag PUT /v3/projects/{project_id}/tags/{tag_name}
identity:create_project_tag:provider_tags
identity:update_project_tags PUT /v3/projects/{project_id}/tags
identity:update_project_tags:provider_tags
identity:delete_project_tag DELETE /v3/projects/{project_id}/tags/{tag_name}
identity:delete_project_tag:provider_tags
identity:delete_project_tags DELETE /v3/projects/{project_id}/tags
identity:delete_project_tags:provider_tags

identity:get_user GET /v3/users/{user_id}
identity:list_users GET /v3/users
Expand Down
66 changes: 65 additions & 1 deletion keystone/api/projects.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

# This file handles all flask-restful resources for /v3/projects

from collections.abc import Iterable
import functools

import flask
Expand Down Expand Up @@ -47,6 +48,49 @@ def _build_project_target_enforcement():
return target


def is_provider_tag(tag: str) -> bool:
"""Check if the provided tag is a provider tag."""
return tag.startswith(tuple(CONF.provider_tag_prefix))


def get_provider_tags(tags: Iterable[str]) -> set[str]:
"""Return the provider tags from the given tags."""
return set(t for t in tags if is_provider_tag(t))


def validate_tags(project_id: str, tags: Iterable[str], policy: str
) -> list[str]:
"""Return the tags the user is allowed to change.

We check whether the list of tags contains provider tags. If it does, we
check if the user is allowed to change provider tags. If they are not
allowed, we restore the original provider tags in the list.
"""
original_ptags = get_provider_tags(
PROVIDERS.resource_api.list_project_tags(project_id)
)
ptags = get_provider_tags(tags)
removed_ptags = original_ptags - ptags
new_ptags = ptags - original_ptags
if removed_ptags or new_ptags:
# NOTE(jkulik): Adding/removing provider tags needs special
# privileges but the user might just send us an update of their own
# tags instead of including the provider tags, so we try to handle
# this gracefully here by updating the non-provider tags
# nonetheless.
try:
ENFORCER.enforce_call(
action='identity:%s:provider_tags' % policy,
build_target=_build_project_target_enforcement
)
except exception.Forbidden:
# User is not allowed to change provider tags so we restore the
# original provider tags to allow them to update their tags.
tags = (set(tags) | removed_ptags) - new_ptags

return list(tags)


class ProjectResource(ks_flask.ResourceBase):
collection_key = 'projects'
member_key = 'project'
Expand Down Expand Up @@ -182,6 +226,11 @@ def post(self):
if not project.get('parent_id'):
project['parent_id'] = project.get('domain_id')
project = self._normalize_dict(project)
if get_provider_tags(project.get('tags', [])):
ENFORCER.enforce_call(
action='identity:create_project:provider_tags',
target_attr=target
)
try:
ref = PROVIDERS.resource_api.create_project(
project['id'],
Expand All @@ -203,6 +252,9 @@ def patch(self, project_id):
project = self.request_body_json.get('project', {})
validation.lazy_validate(schema.project_update, project)
self._require_matching_id(project)
if project.get('tags') is not None:
project['tags'] = validate_tags(
project_id, project['tags'], 'update_project')
ref = PROVIDERS.resource_api.update_project(
project_id,
project,
Expand Down Expand Up @@ -264,6 +316,7 @@ def put(self, project_id):
)
tags = self.request_body_json.get('tags', {})
validation.lazy_validate(schema.project_tags_update, tags)
tags = validate_tags(project_id, tags, 'update_project_tags')
ref = PROVIDERS.resource_api.update_project_tags(
project_id, tags, initiator=self.audit_initiator)
return self.wrap_member(ref)
Expand All @@ -277,7 +330,8 @@ def delete(self, project_id):
action='identity:delete_project_tags',
build_target=_build_project_target_enforcement
)
PROVIDERS.resource_api.update_project_tags(project_id, [])
tags = validate_tags(project_id, [], 'delete_project_tags')
PROVIDERS.resource_api.update_project_tags(project_id, tags)
return None, http.client.NO_CONTENT


Expand Down Expand Up @@ -308,6 +362,11 @@ def put(self, project_id, value):
tags = PROVIDERS.resource_api.list_project_tags(project_id)
tags.append(value)
validation.lazy_validate(schema.project_tags_update, tags)
if is_provider_tag(value):
ENFORCER.enforce_call(
action='identity:create_project_tag:provider_tags',
build_target=_build_project_target_enforcement
)
PROVIDERS.resource_api.create_project_tag(
project_id,
value,
Expand All @@ -327,6 +386,11 @@ def delete(self, project_id, value):
action='identity:delete_project_tag',
build_target=_build_project_target_enforcement
)
if is_provider_tag(value):
ENFORCER.enforce_call(
action='identity:delete_project_tag:provider_tags',
build_target=_build_project_target_enforcement
)
PROVIDERS.resource_api.delete_project_tag(project_id, value)
return None, http.client.NO_CONTENT

Expand Down
47 changes: 46 additions & 1 deletion keystone/common/policies/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@
'(role:admin and domain_id:%(target.project.domain_id)s)'
)

RULE_CLOUD_ADMIN_OR_SERVICE = 'rule:cloud_admin or rule:service_role'

DEPRECATED_REASON = (
"The project API is now aware of system scope and default roles."
)
Expand Down Expand Up @@ -184,6 +186,13 @@
operations=[{'path': '/v3/projects',
'method': 'POST'}],
deprecated_rule=deprecated_create_project),
policy.DocumentedRuleDefault(
name=base.IDENTITY % 'create_project:provider_tags',
check_str=RULE_CLOUD_ADMIN_OR_SERVICE,
scope_types=['system', 'domain', 'project'],
description='Create project with provider tags.',
operations=[{'path': '/v3/projects',
'method': 'POST'}]),
policy.DocumentedRuleDefault(
name=base.IDENTITY % 'update_project',
check_str=base.RULE_ADMIN_REQUIRED,
Expand All @@ -192,6 +201,13 @@
operations=[{'path': '/v3/projects/{project_id}',
'method': 'PATCH'}],
deprecated_rule=deprecated_update_project),
policy.DocumentedRuleDefault(
name=base.IDENTITY % 'update_project:provider_tags',
check_str=RULE_CLOUD_ADMIN_OR_SERVICE,
scope_types=['system', 'domain', 'project'],
description='Update project with provider tags.',
operations=[{'path': '/v3/projects/{project_id}',
'method': 'PATCH'}]),
policy.DocumentedRuleDefault(
name=base.IDENTITY % 'delete_project',
check_str=base.RULE_ADMIN_REQUIRED,
Expand Down Expand Up @@ -228,6 +244,14 @@
operations=[{'path': '/v3/projects/{project_id}/tags',
'method': 'PUT'}],
deprecated_rule=deprecated_update_project_tag),
policy.DocumentedRuleDefault(
name=base.IDENTITY % 'update_project_tags:provider_tags',
check_str=RULE_CLOUD_ADMIN_OR_SERVICE,
scope_types=['system', 'domain', 'project'],
description='Replace all tags on a project with the new set of tags '
'that includes provider tags.',
operations=[{'path': '/v3/projects/{project_id}/tags',
'method': 'PUT'}]),
policy.DocumentedRuleDefault(
name=base.IDENTITY % 'create_project_tag',
check_str=base.RULE_ADMIN_REQUIRED,
Expand All @@ -236,6 +260,13 @@
operations=[{'path': '/v3/projects/{project_id}/tags/{value}',
'method': 'PUT'}],
deprecated_rule=deprecated_create_project_tag),
policy.DocumentedRuleDefault(
name=base.IDENTITY % 'create_project_tag:provider_tags',
check_str=RULE_CLOUD_ADMIN_OR_SERVICE,
scope_types=['system', 'domain', 'project'],
description='Add a single provider tag to a project.',
operations=[{'path': '/v3/projects/{project_id}/tags/{value}',
'method': 'PUT'}]),
policy.DocumentedRuleDefault(
name=base.IDENTITY % 'delete_project_tags',
check_str=base.RULE_ADMIN_REQUIRED,
Expand All @@ -244,14 +275,28 @@
operations=[{'path': '/v3/projects/{project_id}/tags',
'method': 'DELETE'}],
deprecated_rule=deprecated_delete_project_tags),
policy.DocumentedRuleDefault(
name=base.IDENTITY % 'delete_project_tags:provider_tags',
check_str=RULE_CLOUD_ADMIN_OR_SERVICE,
scope_types=['system', 'domain', 'project'],
description='Remove all tags from a project including provider tags.',
operations=[{'path': '/v3/projects/{project_id}/tags',
'method': 'DELETE'}]),
policy.DocumentedRuleDefault(
name=base.IDENTITY % 'delete_project_tag',
check_str=base.RULE_ADMIN_REQUIRED,
scope_types=['system', 'domain', 'project'],
description='Delete a specified tag from project.',
operations=[{'path': '/v3/projects/{project_id}/tags/{value}',
'method': 'DELETE'}],
deprecated_rule=deprecated_delete_project_tag)
deprecated_rule=deprecated_delete_project_tag),
policy.DocumentedRuleDefault(
name=base.IDENTITY % 'delete_project_tag:provider_tags',
check_str=RULE_CLOUD_ADMIN_OR_SERVICE,
scope_types=['system', 'domain', 'project'],
description='Delete a specified provider tag from project.',
operations=[{'path': '/v3/projects/{project_id}/tags/{value}',
'method': 'DELETE'}]),
]


Expand Down
8 changes: 8 additions & 0 deletions keystone/conf/default.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,13 @@
default_tag=tag_1
"""))

provider_tag_prefix = cfg.MultiStrOpt(
'provider_tag_prefix',
default=[],
help=utils.fmt("""
Prefixes for `tag`(s) on projects that need special privileges.
"""))

notification_format = cfg.StrOpt(
'notification_format',
default='cadf',
Expand Down Expand Up @@ -158,6 +165,7 @@
insecure_debug,
default_publisher_id,
default_tag,
provider_tag_prefix,
notification_format,
notification_opt_out,
]
Expand Down
Loading