Merge "Implement the Domain Manager Persona for Keystone"

This commit is contained in:
Zuul 2024-08-28 18:29:44 +00:00 committed by Gerrit Code Review
commit dd30a921a2
10 changed files with 194 additions and 50 deletions

View File

@ -26,10 +26,14 @@ functionality to their team, auditors, customers, and users without maintaining
custom policies.
In addition to ``admin``, ``member``, and ``reader`` role, from 2023.2 (Bobcat)
release keystone will provide ``service`` role by default as well. Operators
can use this role for service to service API calls instead of using ``admin``
role for the same. The service role will be separate from ``admin``,
``member``, ``reader`` and will not implicate any of these roles.
release keystone will provide the ``service`` and ``manager`` roles by default
as well. Operators can use the ``service`` role for service to service API
calls instead of using ``admin`` role for the same. The service role will be
separate from ``admin``, ``member``, ``reader`` and will not implicate any of
these roles.
Operators can give the ``manager`` role to users to within a domain to enable
self-service management of users, groups, projects and role assignments within
their domain.
.. _`token guide`: https://docs.openstack.org/keystone/latest/admin/tokens-overview.html#authorization-scopes
@ -39,14 +43,16 @@ Roles Definitions
The default roles provided by keystone via ``keystone-manage bootstrap``
(except for the ``service`` role) are related through role implications. The
``admin`` role implies the ``member`` role, and the ``member`` role implies
the ``reader`` role. These implications mean users with the ``admin`` role
automatically have the ``member`` and ``reader`` roles. Additionally,
users with the ``member`` role automatically have the ``reader`` role.
Implying roles reduces role assignments and forms a natural hierarchy between
the default roles. It also reduces the complexity of default policies by
making check strings short. For example, a policy that requires ``reader``
can be expressed as:
``admin`` role implies the ``manager`` role, the ``manager`` implies the
``member`` role, and the ``member`` role implies the ``reader`` role. These
implications mean users with the ``admin`` role automatically have the
``manager``, ``member`` and ``reader`` roles. Additionally, users with the
``manager`` role automatically have the ``member`` and ``reader`` roles. Users
with the ``member`` role automatically have the ``reader`` role. Implying roles
reduces role assignments and forms a natural hierarchy between the default
roles. It also reduces the complexity of default policies by making check
strings short. For example, a policy that requires ``reader`` can be expressed
as:
.. code-block:: yaml
@ -56,7 +62,7 @@ Instead of:
.. code-block:: yaml
"identity:list_foo": "role:admin or role:member or role:reader"
"identity:list_foo": "role:admin or role:manager or role:member or role:reader"
Reader
======
@ -122,6 +128,30 @@ users with ``admin`` on a project can list, create, and delete instances.
Service developers can use the ``member`` role to provide more flexibility
between ``admin`` and ``reader`` on different scopes.
Manager
=======
The ``manager`` role takes a special place in keystone. It sits between the
``admin`` and ``member`` role, allowing limited identity management while being
clearly differentiated from the ``admin`` role both in terms of purpose and
privileges. The ``manager`` role is meant to be assigned in a domain scope and
enables users to manage identity assets in a whole domain including users,
projects, groups and role assignments. This enables identity self-service
management capabilities for users within a domain without the need to assign
the most privileged ``admin`` role to them.
The keystone default policies include a special rule that specifies the list of
roles a user with the ``manager`` role is be able to assign and revoke within
the domain scope. This prevents such user from escalating their own privileges
or those of others beyond ``manager`` and for this purpose the list excludes
the ``admin`` role. The list can be adjusted by cloud administrators via policy
definitions in case the role model differs. For example, if a new role is
introduced for a specific cloud environment, the list can be adjusted to allow
users with the ``manager`` role to also assign it.
Other services might write default policies to enable the ``manager`` role to
have more privileged managing rights or cross-project privileges in a domain.
Admin
=====
@ -255,14 +285,15 @@ role assignments on a specific domain using the following query:
.. code-block:: console
$ openstack role assignment list --names --domain foobar
+--------+-----------------+----------------------+---------+--------+--------+-----------+
| Role | User | Group | Project | Domain | System | Inherited |
+--------+-----------------+----------------------+---------+--------+--------+-----------+
| reader | support@Default | | | foobar | | False |
| admin | jsmith@Default | | | foobar | | False |
| admin | | foobar-admins@foobar | | foobar | | False |
| member | jdoe@foobar | | | foobar | | False |
+--------+-----------------+----------------------+---------+--------+--------+-----------+
+---------+-----------------+----------------------+---------+--------+--------+-----------+
| Role | User | Group | Project | Domain | System | Inherited |
+---------+-----------------+----------------------+---------+--------+--------+-----------+
| reader | support@Default | | | foobar | | False |
| admin | jsmith@Default | | | foobar | | False |
| admin | | foobar-admins@foobar | | foobar | | False |
| manager | alice@foobar | | | foobar | | False |
| member | jdoe@foobar | | | foobar | | False |
+---------+-----------------+----------------------+---------+--------+--------+-----------+
Domain Administrators
=====================
@ -288,6 +319,32 @@ assignment:
| admin | | foobar-admins@foobar | | foobar | | False |
+-------+----------------+----------------------+---------+--------+--------+-----------+
Domain Managers
===============
*Domain managers* can only manage specific resources related to identity
management within their domain. This includes creating new users, projects and
groups as well as updating and deleting them. They can also assign and revoke
roles between those or in relation to the domain. Furthermore, they can inspect
role assignments within the domain.
*Domain managers* cannot change any aspects of the domain itself. The role
assignments they can apply within their domain is limited to a specific list of
applicable roles and in the default configuration, this excludes the ``admin``
role to prevent privilege escalation.
You can find *domain managers* in your deployment with the following role
assignment:
.. code-block:: console
$ openstack role assignment list --names --domain foobar --role manager
+---------+-----------------+----------------------+---------+--------+--------+-----------+
| Role | User | Group | Project | Domain | System | Inherited |
+---------+-----------------+----------------------+---------+--------+--------+-----------+
| manager | alice@foobar | | | foobar | | False |
+---------+-----------------+----------------------+---------+--------+--------+-----------+
Domain Members & Domain Readers
===============================

View File

@ -111,7 +111,10 @@ class RoleResource(ks_flask.ResourceBase):
"""
role = self.request_body_json.get('role', {})
if self._is_domain_role(role):
ENFORCER.enforce_call(action='identity:create_domain_role')
target = {'role': role}
ENFORCER.enforce_call(
action='identity:create_domain_role', target_attr=target
)
else:
ENFORCER.enforce_call(action='identity:create_role')
validation.lazy_validate(schema.role_create, role)

View File

@ -65,6 +65,18 @@ ADMIN_OR_CRED_OWNER = (
'(' + RULE_ADMIN_REQUIRED + ') ' 'or user_id:%(target.credential.user_id)s'
)
# This rule template is meant for restricting role assignments done by domain
# managers. It is intended to restrict the roles a domain manager can assign or
# revoke to a sensible default set while allowing overrides via policy file by
# adjusting the corresponding rule definition.
# For the default, any roles with higher-level privileges than "manager" (e.g.
# "admin") must be omitted to avoid privilege escalation.
DOMAIN_MANAGER_ALLOWED_ROLES = (
"'manager':%(target.role.name)s or "
"'member':%(target.role.name)s or "
"'reader':%(target.role.name)s"
)
rules = [
policy.RuleDefault(
name='admin_required', check_str='role:admin or is_admin:1'
@ -89,6 +101,10 @@ rules = [
name='service_admin_or_token_subject',
check_str='rule:service_or_admin or rule:token_subject',
),
policy.RuleDefault(
name="domain_managed_target_role",
check_str=DOMAIN_MANAGER_ALLOWED_ROLES,
),
]

View File

@ -61,6 +61,17 @@ GRANTS_DOMAIN_ADMIN = (
' ' + DOMAIN_MATCHES_TARGET_DOMAIN + ')'
)
GRANTS_DOMAIN_MANAGER = (
'(role:manager and ' + DOMAIN_MATCHES_USER_DOMAIN + ' and'
' ' + DOMAIN_MATCHES_PROJECT_DOMAIN + ') or '
'(role:manager and ' + DOMAIN_MATCHES_USER_DOMAIN + ' and'
' ' + DOMAIN_MATCHES_TARGET_DOMAIN + ') or '
'(role:manager and ' + DOMAIN_MATCHES_GROUP_DOMAIN + ' and'
' ' + DOMAIN_MATCHES_PROJECT_DOMAIN + ') or '
'(role:manager and ' + DOMAIN_MATCHES_GROUP_DOMAIN + ' and'
' ' + DOMAIN_MATCHES_TARGET_DOMAIN + ')'
)
ADMIN_OR_SYSTEM_READER_OR_DOMAIN_READER = (
'(' + base.RULE_ADMIN_REQUIRED + ') or '
'(' + SYSTEM_READER_OR_DOMAIN_READER + ')'
@ -71,10 +82,12 @@ ADMIN_OR_SYSTEM_READER_OR_DOMAIN_READER_LIST = (
'(' + SYSTEM_READER_OR_DOMAIN_READER_LIST + ')'
)
ADMIN_OR_DOMAIN_ADMIN = (
ADMIN_OR_DOMAIN_ADMIN_OR_DOMAIN_MANAGER = (
'(' + base.RULE_ADMIN_REQUIRED + ') or '
'(' + GRANTS_DOMAIN_ADMIN + ') and '
'(' + DOMAIN_MATCHES_ROLE + ')'
'(' + DOMAIN_MATCHES_ROLE + ') or '
'(' + GRANTS_DOMAIN_MANAGER + ') and '
'rule:domain_managed_target_role'
)
DEPRECATED_REASON = (
@ -236,7 +249,7 @@ grant_policies = [
),
policy.DocumentedRuleDefault(
name=base.IDENTITY % 'create_grant',
check_str=ADMIN_OR_DOMAIN_ADMIN,
check_str=ADMIN_OR_DOMAIN_ADMIN_OR_DOMAIN_MANAGER,
scope_types=['system', 'domain', 'project'],
description=(
'Create a role grant between a target and an actor. A '
@ -251,7 +264,7 @@ grant_policies = [
),
policy.DocumentedRuleDefault(
name=base.IDENTITY % 'revoke_grant',
check_str=ADMIN_OR_DOMAIN_ADMIN,
check_str=ADMIN_OR_DOMAIN_ADMIN_OR_DOMAIN_MANAGER,
scope_types=['system', 'domain', 'project'],
description=(
'Revoke a role grant between a target and an actor. A '

View File

@ -33,6 +33,14 @@ SYSTEM_READER_OR_DOMAIN_READER_FOR_TARGET_GROUP_USER = (
'domain_id:%(target.group.domain_id)s and '
'domain_id:%(target.user.domain_id)s)'
)
DOMAIN_MANAGER_FOR_TARGET_GROUP = (
'role:manager and domain_id:%(target.group.domain_id)s'
)
DOMAIN_MANAGER_FOR_TARGET_GROUP_USER = (
'role:manager and '
'domain_id:%(target.group.domain_id)s and '
'domain_id:%(target.user.domain_id)s'
)
ADMIN_OR_SYSTEM_READER_OR_DOMAIN_READER_FOR_TARGET_GROUP = (
'('
+ base.RULE_ADMIN_REQUIRED
@ -53,6 +61,21 @@ SYSTEM_ADMIN_OR_DOMAIN_ADMIN = (
'(role:admin and domain_id:%(target.group.domain_id)s)'
)
ADMIN_OR_DOMAIN_MANAGER_FOR_GROUPS = (
'('
+ base.RULE_ADMIN_REQUIRED
+ ') or ('
+ DOMAIN_MANAGER_FOR_TARGET_GROUP
+ ')'
)
ADMIN_OR_DOMAIN_MANAGER_FOR_GROUP_ASSIGNMENTS = (
'('
+ base.RULE_ADMIN_REQUIRED
+ ') or ('
+ DOMAIN_MANAGER_FOR_TARGET_GROUP_USER
+ ')'
)
DEPRECATED_REASON = (
"The group API is now aware of system scope and default roles."
)
@ -154,7 +177,7 @@ group_policies = [
),
policy.DocumentedRuleDefault(
name=base.IDENTITY % 'create_group',
check_str=base.RULE_ADMIN_REQUIRED,
check_str=ADMIN_OR_DOMAIN_MANAGER_FOR_GROUPS,
scope_types=['system', 'domain', 'project'],
description='Create group.',
operations=[{'path': '/v3/groups', 'method': 'POST'}],
@ -162,7 +185,7 @@ group_policies = [
),
policy.DocumentedRuleDefault(
name=base.IDENTITY % 'update_group',
check_str=base.RULE_ADMIN_REQUIRED,
check_str=ADMIN_OR_DOMAIN_MANAGER_FOR_GROUPS,
scope_types=['system', 'domain', 'project'],
description='Update group.',
operations=[{'path': '/v3/groups/{group_id}', 'method': 'PATCH'}],
@ -170,7 +193,7 @@ group_policies = [
),
policy.DocumentedRuleDefault(
name=base.IDENTITY % 'delete_group',
check_str=base.RULE_ADMIN_REQUIRED,
check_str=ADMIN_OR_DOMAIN_MANAGER_FOR_GROUPS,
scope_types=['system', 'domain', 'project'],
description='Delete group.',
operations=[{'path': '/v3/groups/{group_id}', 'method': 'DELETE'}],
@ -189,7 +212,7 @@ group_policies = [
),
policy.DocumentedRuleDefault(
name=base.IDENTITY % 'remove_user_from_group',
check_str=base.RULE_ADMIN_REQUIRED,
check_str=ADMIN_OR_DOMAIN_MANAGER_FOR_GROUP_ASSIGNMENTS,
scope_types=['system', 'domain', 'project'],
description='Remove user from group.',
operations=[
@ -216,7 +239,7 @@ group_policies = [
),
policy.DocumentedRuleDefault(
name=base.IDENTITY % 'add_user_to_group',
check_str=base.RULE_ADMIN_REQUIRED,
check_str=ADMIN_OR_DOMAIN_MANAGER_FOR_GROUP_ASSIGNMENTS,
scope_types=['system', 'domain', 'project'],
description='Add user to group.',
operations=[

View File

@ -49,7 +49,7 @@ SYSTEM_READER_OR_DOMAIN_READER_OR_OWNER = (
# the context user_id to the target user id.
'user_id:%(target.user.id)s'
)
ADMIN_OR_SYSTEM_READER_OR_DOMAIN_READER_OR_OWNER = (
ADMIN_SYSTEM_READER_OR_DOMAIN_READER_OR_OWNER = (
'('
+ base.RULE_ADMIN_REQUIRED
+ ') or '
@ -65,9 +65,9 @@ ADMIN_OR_SYSTEM_READER_OR_DOMAIN_READER = (
'(' + base.RULE_ADMIN_REQUIRED + ') or ' + SYSTEM_READER_OR_DOMAIN_READER
)
SYSTEM_ADMIN_OR_DOMAIN_ADMIN = (
'(role:admin and system_scope:all) or '
'(role:admin and domain_id:%(target.project.domain_id)s)'
ADMIN_OR_DOMAIN_MANAGER = (
'(' + base.RULE_ADMIN_REQUIRED + ') or '
'(role:manager and domain_id:%(target.project.domain_id)s)'
)
DEPRECATED_REASON = (
@ -175,7 +175,7 @@ project_policies = [
),
policy.DocumentedRuleDefault(
name=base.IDENTITY % 'list_user_projects',
check_str=ADMIN_OR_SYSTEM_READER_OR_DOMAIN_READER_OR_OWNER,
check_str=ADMIN_SYSTEM_READER_OR_DOMAIN_READER_OR_OWNER,
scope_types=['system', 'domain', 'project'],
description='List projects for user.',
operations=[{'path': '/v3/users/{user_id}/projects', 'method': 'GET'}],
@ -183,7 +183,7 @@ project_policies = [
),
policy.DocumentedRuleDefault(
name=base.IDENTITY % 'create_project',
check_str=base.RULE_ADMIN_REQUIRED,
check_str=ADMIN_OR_DOMAIN_MANAGER,
scope_types=['system', 'domain', 'project'],
description='Create project.',
operations=[{'path': '/v3/projects', 'method': 'POST'}],
@ -191,7 +191,7 @@ project_policies = [
),
policy.DocumentedRuleDefault(
name=base.IDENTITY % 'update_project',
check_str=base.RULE_ADMIN_REQUIRED,
check_str=ADMIN_OR_DOMAIN_MANAGER,
scope_types=['system', 'domain', 'project'],
description='Update project.',
operations=[{'path': '/v3/projects/{project_id}', 'method': 'PATCH'}],
@ -199,7 +199,7 @@ project_policies = [
),
policy.DocumentedRuleDefault(
name=base.IDENTITY % 'delete_project',
check_str=base.RULE_ADMIN_REQUIRED,
check_str=ADMIN_OR_DOMAIN_MANAGER,
scope_types=['system', 'domain', 'project'],
description='Delete project.',
operations=[{'path': '/v3/projects/{project_id}', 'method': 'DELETE'}],
@ -235,7 +235,7 @@ project_policies = [
),
policy.DocumentedRuleDefault(
name=base.IDENTITY % 'update_project_tags',
check_str=base.RULE_ADMIN_REQUIRED,
check_str=ADMIN_OR_DOMAIN_MANAGER,
scope_types=['system', 'domain', 'project'],
description='Replace all tags on a project with the new set of tags.',
operations=[
@ -245,7 +245,7 @@ project_policies = [
),
policy.DocumentedRuleDefault(
name=base.IDENTITY % 'create_project_tag',
check_str=base.RULE_ADMIN_REQUIRED,
check_str=ADMIN_OR_DOMAIN_MANAGER,
scope_types=['system', 'domain', 'project'],
description='Add a single tag to a project.',
operations=[
@ -255,7 +255,7 @@ project_policies = [
),
policy.DocumentedRuleDefault(
name=base.IDENTITY % 'delete_project_tags',
check_str=base.RULE_ADMIN_REQUIRED,
check_str=ADMIN_OR_DOMAIN_MANAGER,
scope_types=['system', 'domain', 'project'],
description='Remove all tags from a project.',
operations=[
@ -265,7 +265,7 @@ project_policies = [
),
policy.DocumentedRuleDefault(
name=base.IDENTITY % 'delete_project_tag',
check_str=base.RULE_ADMIN_REQUIRED,
check_str=ADMIN_OR_DOMAIN_MANAGER,
scope_types=['system', 'domain', 'project'],
description='Delete a specified tag from project.',
operations=[

View File

@ -15,6 +15,19 @@ from oslo_policy import policy
from keystone.common.policies import base
ADMIN_OR_SYSTEM_READER_OR_DOMAIN_MANAGER_ROLE = (
'(' + base.RULE_ADMIN_OR_SYSTEM_READER + ') or '
'(role:manager and rule:domain_managed_target_role)'
)
# For the domain manager persona we only check for a domain id in the token
# that is not None here to exclude scopes like a project manager. Since most
# roles are global we do not have a target domain attribute to match against.
ADMIN_OR_SYSTEM_READER_OR_DOMAIN_MANAGER = (
'(' + base.RULE_ADMIN_OR_SYSTEM_READER + ') or '
'(role:manager and not domain_id:None)'
)
DEPRECATED_REASON = (
"The role API is now aware of system scope and default roles."
)
@ -84,7 +97,7 @@ deprecated_delete_domain_role = policy.DeprecatedRule(
role_policies = [
policy.DocumentedRuleDefault(
name=base.IDENTITY % 'get_role',
check_str=base.RULE_ADMIN_OR_SYSTEM_READER,
check_str=ADMIN_OR_SYSTEM_READER_OR_DOMAIN_MANAGER_ROLE,
scope_types=['system', 'domain', 'project'],
description='Show role details.',
operations=[
@ -95,7 +108,7 @@ role_policies = [
),
policy.DocumentedRuleDefault(
name=base.IDENTITY % 'list_roles',
check_str=base.RULE_ADMIN_OR_SYSTEM_READER,
check_str=ADMIN_OR_SYSTEM_READER_OR_DOMAIN_MANAGER,
scope_types=['system', 'domain', 'project'],
description='List roles.',
operations=[

View File

@ -30,10 +30,16 @@ ADMIN_OR_SYSTEM_READER_OR_DOMAIN_READER_OR_USER = (
SYSTEM_READER_OR_DOMAIN_READER = (
'(' + base.SYSTEM_READER + ') or (' + base.DOMAIN_READER + ')'
)
ADMIN_OR_SYSTEM_READER_OR_DOMAIN_READER = (
ADMIN_SYSTEM_READER_OR_DOMAIN_READER = (
'(' + base.RULE_ADMIN_REQUIRED + ') or ' + SYSTEM_READER_OR_DOMAIN_READER
)
ADMIN_OR_DOMAIN_MANAGER = (
'(' + base.RULE_ADMIN_REQUIRED + ') or '
'(role:manager and token.domain.id:%(target.user.domain_id)s)'
)
DEPRECATED_REASON = (
"The user API is now aware of system scope and default roles."
)
@ -83,7 +89,7 @@ user_policies = [
),
policy.DocumentedRuleDefault(
name=base.IDENTITY % 'list_users',
check_str=ADMIN_OR_SYSTEM_READER_OR_DOMAIN_READER,
check_str=ADMIN_SYSTEM_READER_OR_DOMAIN_READER,
scope_types=['system', 'domain', 'project'],
description='List users.',
operations=[
@ -120,7 +126,7 @@ user_policies = [
),
policy.DocumentedRuleDefault(
name=base.IDENTITY % 'create_user',
check_str=base.RULE_ADMIN_REQUIRED,
check_str=ADMIN_OR_DOMAIN_MANAGER,
scope_types=['system', 'domain', 'project'],
description='Create a user.',
operations=[{'path': '/v3/users', 'method': 'POST'}],
@ -128,7 +134,7 @@ user_policies = [
),
policy.DocumentedRuleDefault(
name=base.IDENTITY % 'update_user',
check_str=base.RULE_ADMIN_REQUIRED,
check_str=ADMIN_OR_DOMAIN_MANAGER,
scope_types=['system', 'domain', 'project'],
description='Update a user, including administrative password resets.',
operations=[{'path': '/v3/users/{user_id}', 'method': 'PATCH'}],
@ -136,7 +142,7 @@ user_policies = [
),
policy.DocumentedRuleDefault(
name=base.IDENTITY % 'delete_user',
check_str=base.RULE_ADMIN_REQUIRED,
check_str=ADMIN_OR_DOMAIN_MANAGER,
scope_types=['system', 'domain', 'project'],
description='Delete a user.',
operations=[{'path': '/v3/users/{user_id}', 'method': 'DELETE'}],

View File

@ -243,6 +243,7 @@ class PolicyJsonTestCase(unit.TestCase):
'service_or_admin',
'service_role',
'token_subject',
'domain_managed_target_role',
]
def read_doc_targets():

View File

@ -0,0 +1,12 @@
---
features:
- >
[`bug 2045974 <https://bugs.launchpad.net/keystone/+bug/2045974>`_]
The Domain Manager Persona has been added. This makes identity-related
self-service capabilities for users within domains possible without
requiring the 'admin' role. Assigning the 'manager' role to users in domain
scope now allows them to manage projects, groups, users and role
assignments within the domain. This is subject to the following
restriction: the roles that domain managers can assign and revoke are
limited by a new ``domain_managed_target_role`` policy rule which defaults
to 'reader', 'member' and 'manager'.