Version in base suite: 22.0.0-2 Base version: keystone_22.0.0-2 Target version: keystone_22.0.2-0+deb12u1 Base file: /srv/ftp-master.debian.org/ftp/pool/main/k/keystone/keystone_22.0.0-2.dsc Target file: /srv/ftp-master.debian.org/policy/pool/main/k/keystone/keystone_22.0.2-0+deb12u1.dsc .gitreview | 1 debian/changelog | 22 debian/control | 1 debian/patches/Allow_admin_to_access_tokens_and_credentials.patch | 254 + debian/patches/Consistent_and_Secure_RBAC_Phase_1.patch | 2000 ++++++++++ debian/patches/Dont_enforce_when_HTTP_GET_on_s3tokens_and_ec2tokens.patch | 73 debian/patches/Fix_policies_for_groups.patch | 30 debian/patches/keystone-bug-2119646-stable-2024.1.patch | 383 + debian/patches/series | 5 debian/rules | 2 doc/source/admin/integrate-with-ldap.inc | 26 keystone/common/password_hashing.py | 35 keystone/conf/identity.py | 6 keystone/conf/ldap.py | 13 keystone/federation/utils.py | 38 keystone/identity/backends/ldap/common.py | 29 keystone/tests/unit/common/test_utils.py | 15 keystone/tests/unit/contrib/federation/test_utils.py | 18 keystone/tests/unit/fakeldap.py | 9 keystone/tests/unit/identity/backends/test_ldap_common.py | 27 keystone/tests/unit/mapping_fixtures.py | 6 keystone/tests/unit/test_backend_ldap_pool.py | 118 keystone/tests/unit/test_v3_auth.py | 15 keystone/token/provider.py | 17 releasenotes/notes/bcrypt_truncation_fix-674dc5d7f1e776f2.yaml | 7 releasenotes/notes/max-password-length-truncation-and-warning-bd69090315ec18a7.yaml | 9 releasenotes/notes/randomize_urls-c0c19f48b2bfa299.yaml | 6 releasenotes/notes/token_expiration_to_match_application_credential-56d058355a9f240d.yaml | 10 tox.ini | 11 29 files changed, 3140 insertions(+), 46 deletions(-) diff -Nru keystone-22.0.0/.gitreview keystone-22.0.2/.gitreview --- keystone-22.0.0/.gitreview 2022-09-08 03:35:35.000000000 +0000 +++ keystone-22.0.2/.gitreview 2023-08-28 07:26:05.000000000 +0000 @@ -2,3 +2,4 @@ host=review.opendev.org port=29418 project=openstack/keystone.git +defaultbranch=stable/zed diff -Nru keystone-22.0.0/debian/changelog keystone-22.0.2/debian/changelog --- keystone-22.0.0/debian/changelog 2023-04-14 08:07:08.000000000 +0000 +++ keystone-22.0.2/debian/changelog 2025-11-11 08:19:08.000000000 +0000 @@ -1,3 +1,25 @@ +keystone (2:22.0.2-0+deb12u1) bookworm-security; urgency=medium + + * New upstream release. + * Blacklist failing SAMLGenerationTests test: + - test_sign_assertion_logs_message_if_xmlsec1_is_not_installed + * Add xmlsec1 as build-depends. + * kay reported a vulnerability in Keystone’s ec2tokens and s3tokens APIs. By + sending those endpoints a valid AWS Signature (e.g., from a presigned S3 + URL), an unauthenticated attacker may obtain Keystone authorization + (ec2tokens can yield a fully scoped token; s3tokens can reveal scope + accepted by some services), resulting in unauthorized access and privilege + escalation. Deployments where /v3/ec2tokens or /v3/s3tokens are reachable + by unauthenticated clients (e.g., exposed on a public API) are affected. + Applied upstream patch (Closes: #XXXXXXX): + - Consistent_and_Secure_RBAC_Phase_1.patch + - Fix_policies_for_groups.patch + - Allow_admin_to_access_tokens_and_credentials.patch + - Dont_enforce_when_HTTP_GET_on_s3tokens_and_ec2tokens.patch + - keystone-bug-2119646-stable-2024.1.patch (backported by me) + + -- Thomas Goirand Tue, 11 Nov 2025 09:19:08 +0100 + keystone (2:22.0.0-2) unstable; urgency=medium * Build-depends on openstack-pkg-tools (>= 123~). diff -Nru keystone-22.0.0/debian/control keystone-22.0.2/debian/control --- keystone-22.0.0/debian/control 2023-04-14 08:07:08.000000000 +0000 +++ keystone-22.0.2/debian/control 2025-11-11 08:19:08.000000000 +0000 @@ -78,6 +78,7 @@ python3-webtest, subunit, tempest, + xmlsec1, Standards-Version: 4.4.1 Homepage: http://keystone.openstack.org/ Vcs-Browser: https://salsa.debian.org/openstack-team/services/keystone diff -Nru keystone-22.0.0/debian/patches/Allow_admin_to_access_tokens_and_credentials.patch keystone-22.0.2/debian/patches/Allow_admin_to_access_tokens_and_credentials.patch --- keystone-22.0.0/debian/patches/Allow_admin_to_access_tokens_and_credentials.patch 1970-01-01 00:00:00.000000000 +0000 +++ keystone-22.0.2/debian/patches/Allow_admin_to_access_tokens_and_credentials.patch 2025-11-11 08:19:08.000000000 +0000 @@ -0,0 +1,254 @@ +From b449d86ee56960adacfcf2086f07b57324f51d4e Mon Sep 17 00:00:00 2001 +From: Douglas Mendizábal +Date: Wed, 27 Mar 2024 08:55:54 -0500 +Subject: [PATCH] Allow admin to access tokens and credentials + +This patch modifies a few policies to allow users with the "admin" role +to access /v3/auth/tokens and /v3/credentials. These policies were +missed when we implemented Phase 1 of Secure RBAC. + +Change-Id: Id789c09121f1405f7ba5e4926498dab4ad98e057 +(cherry picked from commit b31007e1b2ecbea5e1268d3e28d6230d0f5d09b2) +(cherry picked from commit 0dcc423a2621943ab9188cff3edb9bc488339fe0) +(cherry picked from commit 570c19e91bc3212f748221bdab5f2976f479fa13) +--- + +diff --git a/keystone/common/policies/application_credential.py b/keystone/common/policies/application_credential.py +index bae998a..12958d8 100644 +--- a/keystone/common/policies/application_credential.py ++++ b/keystone/common/policies/application_credential.py +@@ -46,7 +46,7 @@ + application_credential_policies = [ + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'get_application_credential', +- check_str=base.RULE_SYSTEM_READER_OR_OWNER, ++ check_str=base.ADMIN_OR_SYSTEM_READER_OR_OWNER, + scope_types=['system', 'project'], + description='Show application credential details.', + operations=[{'path': resource_path, +@@ -56,7 +56,7 @@ + deprecated_rule=deprecated_get_application_credentials_for_user), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'list_application_credentials', +- check_str=base.RULE_SYSTEM_READER_OR_OWNER, ++ check_str=base.ADMIN_OR_SYSTEM_READER_OR_OWNER, + scope_types=['system', 'project'], + description='List application credentials for a user.', + operations=[{'path': collection_path, +@@ -73,7 +73,7 @@ + 'method': 'POST'}]), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'delete_application_credential', +- check_str=base.RULE_SYSTEM_ADMIN_OR_OWNER, ++ check_str=base.RULE_ADMIN_OR_OWNER, + scope_types=['system', 'project'], + description='Delete an application credential.', + operations=[{'path': resource_path, +diff --git a/keystone/common/policies/base.py b/keystone/common/policies/base.py +index 5b323f5..39df9ea 100644 +--- a/keystone/common/policies/base.py ++++ b/keystone/common/policies/base.py +@@ -48,16 +48,20 @@ + SYSTEM_ADMIN = 'role:admin and system_scope:all' + DOMAIN_READER = 'role:reader and domain_id:%(target.domain_id)s' + RULE_SYSTEM_ADMIN_OR_OWNER = '(' + SYSTEM_ADMIN + ') or rule:owner' +-RULE_SYSTEM_READER_OR_OWNER = '(' + SYSTEM_READER + ') or rule:owner' ++ADMIN_OR_SYSTEM_READER_OR_OWNER = ( ++ '(' + RULE_ADMIN_REQUIRED + ') or ' ++ '(' + SYSTEM_READER + ') or rule:owner' ++) + RULE_ADMIN_OR_SYSTEM_READER = 'rule:admin_required or (' + SYSTEM_READER + ')' + + # Credential and EC2 Credential policies +-SYSTEM_READER_OR_CRED_OWNER = ( ++ADMIN_OR_SYSTEM_READER_OR_CRED_OWNER = ( ++ '(' + RULE_ADMIN_REQUIRED + ') or ' + '(' + SYSTEM_READER + ') ' + 'or user_id:%(target.credential.user_id)s' + ) +-SYSTEM_ADMIN_OR_CRED_OWNER = ( +- '(' + SYSTEM_ADMIN + ') ' ++ADMIN_OR_CRED_OWNER = ( ++ '(' + RULE_ADMIN_REQUIRED + ') ' + 'or user_id:%(target.credential.user_id)s' + ) + +diff --git a/keystone/common/policies/credential.py b/keystone/common/policies/credential.py +index 675e318..41d49f6 100644 +--- a/keystone/common/policies/credential.py ++++ b/keystone/common/policies/credential.py +@@ -54,7 +54,7 @@ + credential_policies = [ + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'get_credential', +- check_str=base.SYSTEM_READER_OR_CRED_OWNER, ++ check_str=base.ADMIN_OR_SYSTEM_READER_OR_CRED_OWNER, + scope_types=['system', 'project'], + description='Show credentials details.', + operations=[{'path': '/v3/credentials/{credential_id}', +@@ -63,7 +63,7 @@ + ), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'list_credentials', +- check_str=base.SYSTEM_READER_OR_CRED_OWNER, ++ check_str=base.ADMIN_OR_SYSTEM_READER_OR_CRED_OWNER, + scope_types=['system', 'project'], + description='List credentials.', + operations=[{'path': '/v3/credentials', +@@ -72,7 +72,7 @@ + ), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'create_credential', +- check_str=base.SYSTEM_ADMIN_OR_CRED_OWNER, ++ check_str=base.ADMIN_OR_CRED_OWNER, + scope_types=['system', 'project'], + description='Create credential.', + operations=[{'path': '/v3/credentials', +@@ -81,7 +81,7 @@ + ), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'update_credential', +- check_str=base.SYSTEM_ADMIN_OR_CRED_OWNER, ++ check_str=base.ADMIN_OR_CRED_OWNER, + scope_types=['system', 'project'], + description='Update credential.', + operations=[{'path': '/v3/credentials/{credential_id}', +@@ -90,7 +90,7 @@ + ), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'delete_credential', +- check_str=base.SYSTEM_ADMIN_OR_CRED_OWNER, ++ check_str=base.ADMIN_OR_CRED_OWNER, + scope_types=['system', 'project'], + description='Delete credential.', + operations=[{'path': '/v3/credentials/{credential_id}', +diff --git a/keystone/common/policies/ec2_credential.py b/keystone/common/policies/ec2_credential.py +index 9e52709..0a87658 100644 +--- a/keystone/common/policies/ec2_credential.py ++++ b/keystone/common/policies/ec2_credential.py +@@ -48,7 +48,7 @@ + ec2_credential_policies = [ + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'ec2_get_credential', +- check_str=base.SYSTEM_READER_OR_CRED_OWNER, ++ check_str=base.ADMIN_OR_SYSTEM_READER_OR_CRED_OWNER, + scope_types=['system', 'project'], + description='Show ec2 credential details.', + operations=[{'path': ('/v3/users/{user_id}/credentials/OS-EC2/' +@@ -58,7 +58,7 @@ + ), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'ec2_list_credentials', +- check_str=base.RULE_SYSTEM_READER_OR_OWNER, ++ check_str=base.ADMIN_OR_SYSTEM_READER_OR_OWNER, + scope_types=['system', 'project'], + description='List ec2 credentials.', + operations=[{'path': '/v3/users/{user_id}/credentials/OS-EC2', +@@ -67,7 +67,7 @@ + ), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'ec2_create_credential', +- check_str=base.RULE_SYSTEM_ADMIN_OR_OWNER, ++ check_str=base.RULE_ADMIN_OR_OWNER, + scope_types=['system', 'project'], + description='Create ec2 credential.', + operations=[{'path': '/v3/users/{user_id}/credentials/OS-EC2', +@@ -76,7 +76,7 @@ + ), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'ec2_delete_credential', +- check_str=base.SYSTEM_ADMIN_OR_CRED_OWNER, ++ check_str=base.ADMIN_OR_CRED_OWNER, + scope_types=['system', 'project'], + description='Delete ec2 credential.', + operations=[{'path': ('/v3/users/{user_id}/credentials/OS-EC2/' +diff --git a/keystone/common/policies/token.py b/keystone/common/policies/token.py +index cb321b0..974c8d9 100644 +--- a/keystone/common/policies/token.py ++++ b/keystone/common/policies/token.py +@@ -38,13 +38,15 @@ + deprecated_since=versionutils.deprecated.TRAIN + ) + +-SYSTEM_ADMIN_OR_TOKEN_SUBJECT = ( +- '(role:admin and system_scope:all) or rule:token_subject' # nosec ++ADMIN_OR_TOKEN_SUBJECT = ( ++ base.RULE_ADMIN_REQUIRED + ' or rule:token_subject' # nosec + ) +-SYSTEM_USER_OR_TOKEN_SUBJECT = ( ++ADMIN_OR_SYSTEM_USER_OR_TOKEN_SUBJECT = ( ++ base.RULE_ADMIN_REQUIRED + ' or ' + '(role:reader and system_scope:all) or rule:token_subject' # nosec + ) +-SYSTEM_USER_OR_SERVICE_OR_TOKEN_SUBJECT = ( ++ADMIN_OR_SYSTEM_USER_OR_SERVICE_OR_TOKEN_SUBJECT = ( ++ base.RULE_ADMIN_REQUIRED + ' or ' + '(role:reader and system_scope:all) ' # nosec + 'or rule:service_role or rule:token_subject' # nosec + ) +@@ -53,7 +55,7 @@ + token_policies = [ + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'check_token', +- check_str=SYSTEM_USER_OR_TOKEN_SUBJECT, ++ check_str=ADMIN_OR_SYSTEM_USER_OR_TOKEN_SUBJECT, + scope_types=['system', 'domain', 'project'], + description='Check a token.', + operations=[{'path': '/v3/auth/tokens', +@@ -61,7 +63,7 @@ + deprecated_rule=deprecated_check_token), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'validate_token', +- check_str=SYSTEM_USER_OR_SERVICE_OR_TOKEN_SUBJECT, ++ check_str=ADMIN_OR_SYSTEM_USER_OR_SERVICE_OR_TOKEN_SUBJECT, + scope_types=['system', 'domain', 'project'], + description='Validate a token.', + operations=[{'path': '/v3/auth/tokens', +@@ -69,7 +71,7 @@ + deprecated_rule=deprecated_validate_token), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'revoke_token', +- check_str=SYSTEM_ADMIN_OR_TOKEN_SUBJECT, ++ check_str=ADMIN_OR_TOKEN_SUBJECT, + scope_types=['system', 'domain', 'project'], + description='Revoke a token.', + operations=[{'path': '/v3/auth/tokens', +diff --git a/keystone/tests/protection/v3/test_credentials.py b/keystone/tests/protection/v3/test_credentials.py +index 800452f..5a1960e 100644 +--- a/keystone/tests/protection/v3/test_credentials.py ++++ b/keystone/tests/protection/v3/test_credentials.py +@@ -1131,11 +1131,13 @@ + # broken behavior with better scope checking. + with open(self.policy_file_name, 'w') as f: + overridden_policies = { +- 'identity:get_credential': bp.SYSTEM_READER_OR_CRED_OWNER, +- 'identity:list_credentials': bp.SYSTEM_READER_OR_CRED_OWNER, +- 'identity:create_credential': bp.SYSTEM_ADMIN_OR_CRED_OWNER, +- 'identity:update_credential': bp.SYSTEM_ADMIN_OR_CRED_OWNER, +- 'identity:delete_credential': bp.SYSTEM_ADMIN_OR_CRED_OWNER ++ 'identity:get_credential': ++ bp.ADMIN_OR_SYSTEM_READER_OR_CRED_OWNER, ++ 'identity:list_credentials': ++ bp.ADMIN_OR_SYSTEM_READER_OR_CRED_OWNER, ++ 'identity:create_credential': bp.ADMIN_OR_CRED_OWNER, ++ 'identity:update_credential': bp.ADMIN_OR_CRED_OWNER, ++ 'identity:delete_credential': bp.ADMIN_OR_CRED_OWNER + } + f.write(jsonutils.dumps(overridden_policies)) + +diff --git a/keystone/tests/protection/v3/test_ec2_credential.py b/keystone/tests/protection/v3/test_ec2_credential.py +index 7614173..1f995d5 100644 +--- a/keystone/tests/protection/v3/test_ec2_credential.py ++++ b/keystone/tests/protection/v3/test_ec2_credential.py +@@ -402,9 +402,9 @@ + # 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. +- reader_or_cred_owner = bp.SYSTEM_READER_OR_CRED_OWNER ++ reader_or_cred_owner = bp.ADMIN_OR_SYSTEM_READER_OR_CRED_OWNER + reader_or_owner = bp.RULE_SYSTEM_READER_OR_OWNER +- admin_or_cred_owner = bp.SYSTEM_ADMIN_OR_CRED_OWNER ++ admin_or_cred_owner = bp.ADMIN_OR_CRED_OWNER + with open(self.policy_file_name, 'w') as f: + overridden_policies = { + 'identity:ec2_get_credential': reader_or_cred_owner, diff -Nru keystone-22.0.0/debian/patches/Consistent_and_Secure_RBAC_Phase_1.patch keystone-22.0.2/debian/patches/Consistent_and_Secure_RBAC_Phase_1.patch --- keystone-22.0.0/debian/patches/Consistent_and_Secure_RBAC_Phase_1.patch 1970-01-01 00:00:00.000000000 +0000 +++ keystone-22.0.2/debian/patches/Consistent_and_Secure_RBAC_Phase_1.patch 2025-11-11 08:19:08.000000000 +0000 @@ -0,0 +1,2000 @@ +From 658fd7d584ee6ce5fa4365e137fdf4d92861b22d Mon Sep 17 00:00:00 2001 +From: Douglas Mendizábal +Date: Tue, 05 Dec 2023 16:41:28 -0500 +Subject: [PATCH] Consistent and Secure RBAC (Phase 1) + +This patch updates system-scoped policies to also accept project-admin +tokens so that operators can continue to use the "admin" role to access +system level APIs. + +The protection test job is marked non-voting since tempest does not yet +expect these policy changes. A follow-up patch will make it voting +again after the test changes have merged into tempest. + +[1] https://governance.openstack.org/tc/goals/selected/consistent-and-secure-rbac.html#phase-1 + +Change-Id: I31b5a1f85d994a90578657bc77fa46ace0748582 +(cherry picked from commit f2f1a5c38847ddc5aa28eec9722885d9c64c6e7b) +(cherry picked from commit 991662c666b6dcb410a622c9ec32e18a094757b2) +--- + +Index: keystone/.zuul.yaml +=================================================================== +--- keystone.orig/.zuul.yaml ++++ keystone/.zuul.yaml +@@ -250,7 +250,8 @@ + irrelevant-files: *tempest-irrelevant-files + - tempest-ipv6-only: + irrelevant-files: *tempest-irrelevant-files +- - keystone-protection-functional ++ # FIXME(dmendiza): temporarily disabling protection job ++ # - keystone-protection-functional: + gate: + jobs: + - keystone-dsvm-py3-functional: +@@ -263,7 +264,8 @@ + irrelevant-files: *tempest-irrelevant-files + - tempest-ipv6-only: + irrelevant-files: *tempest-irrelevant-files +- - keystone-protection-functional ++ # FIXME(dmendiza): temporarily disabling protection job ++ # - keystone-protection-functional + experimental: + jobs: + - keystone-tox-patch_cover +Index: keystone/keystone/common/policies/base.py +=================================================================== +--- keystone.orig/keystone/common/policies/base.py ++++ keystone/keystone/common/policies/base.py +@@ -49,6 +49,7 @@ SYSTEM_ADMIN = 'role:admin and system_sc + DOMAIN_READER = 'role:reader and domain_id:%(target.domain_id)s' + RULE_SYSTEM_ADMIN_OR_OWNER = '(' + SYSTEM_ADMIN + ') or rule:owner' + RULE_SYSTEM_READER_OR_OWNER = '(' + SYSTEM_READER + ') or rule:owner' ++RULE_ADMIN_OR_SYSTEM_READER = 'rule:admin_required or (' + SYSTEM_READER + ')' + + # Credential and EC2 Credential policies + SYSTEM_READER_OR_CRED_OWNER = ( +Index: keystone/keystone/common/policies/consumer.py +=================================================================== +--- keystone.orig/keystone/common/policies/consumer.py ++++ keystone/keystone/common/policies/consumer.py +@@ -54,40 +54,40 @@ deprecated_delete_consumer = policy.Depr + consumer_policies = [ + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'get_consumer', +- check_str=base.SYSTEM_READER, +- scope_types=['system'], ++ check_str=base.RULE_ADMIN_OR_SYSTEM_READER, ++ scope_types=['system', 'project'], + description='Show OAUTH1 consumer details.', + operations=[{'path': '/v3/OS-OAUTH1/consumers/{consumer_id}', + 'method': 'GET'}], + deprecated_rule=deprecated_get_consumer), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'list_consumers', +- check_str=base.SYSTEM_READER, +- scope_types=['system'], ++ check_str=base.RULE_ADMIN_OR_SYSTEM_READER, ++ scope_types=['system', 'project'], + description='List OAUTH1 consumers.', + operations=[{'path': '/v3/OS-OAUTH1/consumers', + 'method': 'GET'}], + deprecated_rule=deprecated_list_consumers), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'create_consumer', +- check_str=base.SYSTEM_ADMIN, +- scope_types=['system'], ++ check_str=base.RULE_ADMIN_REQUIRED, ++ scope_types=['system', 'project'], + description='Create OAUTH1 consumer.', + operations=[{'path': '/v3/OS-OAUTH1/consumers', + 'method': 'POST'}], + deprecated_rule=deprecated_create_consumer), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'update_consumer', +- check_str=base.SYSTEM_ADMIN, +- scope_types=['system'], ++ check_str=base.RULE_ADMIN_REQUIRED, ++ scope_types=['system', 'project'], + description='Update OAUTH1 consumer.', + operations=[{'path': '/v3/OS-OAUTH1/consumers/{consumer_id}', + 'method': 'PATCH'}], + deprecated_rule=deprecated_update_consumer), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'delete_consumer', +- check_str=base.SYSTEM_ADMIN, +- scope_types=['system'], ++ check_str=base.RULE_ADMIN_REQUIRED, ++ scope_types=['system', 'project'], + description='Delete OAUTH1 consumer.', + operations=[{'path': '/v3/OS-OAUTH1/consumers/{consumer_id}', + 'method': 'DELETE'}], +Index: keystone/keystone/common/policies/domain.py +=================================================================== +--- keystone.orig/keystone/common/policies/domain.py ++++ keystone/keystone/common/policies/domain.py +@@ -49,7 +49,8 @@ deprecated_delete_domain = policy.Deprec + deprecated_reason=DEPRECATED_REASON, + deprecated_since=versionutils.deprecated.STEIN + ) +-SYSTEM_USER_OR_DOMAIN_USER_OR_PROJECT_USER = ( ++ADMIN_OR_SYSTEM_USER_OR_DOMAIN_USER_OR_PROJECT_USER = ( ++ base.RULE_ADMIN_REQUIRED + ' or ' + '(role:reader and system_scope:all) or ' + 'token.domain.id:%(target.domain.id)s or ' + 'token.project.domain.id:%(target.domain.id)s' +@@ -61,7 +62,7 @@ domain_policies = [ + name=base.IDENTITY % 'get_domain', + # NOTE(lbragstad): This policy allows system, domain, and + # project-scoped tokens. +- check_str=SYSTEM_USER_OR_DOMAIN_USER_OR_PROJECT_USER, ++ check_str=ADMIN_OR_SYSTEM_USER_OR_DOMAIN_USER_OR_PROJECT_USER, + scope_types=['system', 'domain', 'project'], + description='Show domain details.', + operations=[{'path': '/v3/domains/{domain_id}', +@@ -69,32 +70,32 @@ domain_policies = [ + deprecated_rule=deprecated_get_domain), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'list_domains', +- check_str=base.SYSTEM_READER, +- scope_types=['system'], ++ check_str=base.RULE_ADMIN_OR_SYSTEM_READER, ++ scope_types=['system', 'project'], + description='List domains.', + operations=[{'path': '/v3/domains', + 'method': 'GET'}], + deprecated_rule=deprecated_list_domains), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'create_domain', +- check_str=base.SYSTEM_ADMIN, +- scope_types=['system'], ++ check_str=base.RULE_ADMIN_REQUIRED, ++ scope_types=['system', 'project'], + description='Create domain.', + operations=[{'path': '/v3/domains', + 'method': 'POST'}], + deprecated_rule=deprecated_create_domain), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'update_domain', +- check_str=base.SYSTEM_ADMIN, +- scope_types=['system'], ++ check_str=base.RULE_ADMIN_REQUIRED, ++ scope_types=['system', 'project'], + description='Update domain.', + operations=[{'path': '/v3/domains/{domain_id}', + 'method': 'PATCH'}], + deprecated_rule=deprecated_update_domain), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'delete_domain', +- check_str=base.SYSTEM_ADMIN, +- scope_types=['system'], ++ check_str=base.RULE_ADMIN_REQUIRED, ++ scope_types=['system', 'project'], + description='Delete domain.', + operations=[{'path': '/v3/domains/{domain_id}', + 'method': 'DELETE'}], +Index: keystone/keystone/common/policies/domain_config.py +=================================================================== +--- keystone.orig/keystone/common/policies/domain_config.py ++++ keystone/keystone/common/policies/domain_config.py +@@ -58,16 +58,8 @@ deprecated_delete_domain_config = policy + domain_config_policies = [ + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'create_domain_config', +- check_str=base.SYSTEM_ADMIN, +- # FIXME(lbragstad): The domain configuration API has traditionally +- # required system or cloud administrators. If, or when, keystone +- # implements the ability for project administrator to use these APIs, +- # then 'project' should be added to scope_types. Adding support for +- # project or domain administrator to manage their own domain +- # configuration would be useful and alleviate work for system +- # administrators, but until we have checks in code that enforce those +- # checks, let's keep this as a system-level operation. +- scope_types=['system'], ++ check_str=base.RULE_ADMIN_REQUIRED, ++ scope_types=['system', 'project'], + description='Create domain configuration.', + operations=[ + { +@@ -79,8 +71,8 @@ domain_config_policies = [ + ), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'get_domain_config', +- check_str=base.SYSTEM_READER, +- scope_types=['system'], ++ check_str=base.RULE_ADMIN_OR_SYSTEM_READER, ++ scope_types=['system', 'project'], + description=('Get the entire domain configuration for a domain, an ' + 'option group within a domain, or a specific ' + 'configuration option within a group for a domain.'), +@@ -143,8 +135,8 @@ domain_config_policies = [ + ), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'update_domain_config', +- check_str=base.SYSTEM_ADMIN, +- scope_types=['system'], ++ check_str=base.RULE_ADMIN_REQUIRED, ++ scope_types=['system', 'project'], + description=('Update domain configuration for either a domain, ' + 'specific group or a specific option in a group.'), + operations=[ +@@ -165,8 +157,8 @@ domain_config_policies = [ + ), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'delete_domain_config', +- check_str=base.SYSTEM_ADMIN, +- scope_types=['system'], ++ check_str=base.RULE_ADMIN_REQUIRED, ++ scope_types=['system', 'project'], + description=('Delete domain configuration for either a domain, ' + 'specific group or a specific option in a group.'), + operations=[ +@@ -187,8 +179,8 @@ domain_config_policies = [ + ), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'get_domain_config_default', +- check_str=base.SYSTEM_READER, +- scope_types=['system'], ++ check_str=base.RULE_ADMIN_OR_SYSTEM_READER, ++ scope_types=['system', 'project'], + description=('Get domain configuration default for either a domain, ' + 'specific group or a specific option in a group.'), + operations=[ +Index: keystone/keystone/common/policies/endpoint.py +=================================================================== +--- keystone.orig/keystone/common/policies/endpoint.py ++++ keystone/keystone/common/policies/endpoint.py +@@ -49,40 +49,40 @@ deprecated_delete_endpoint = policy.Depr + endpoint_policies = [ + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'get_endpoint', +- check_str=base.SYSTEM_READER, +- scope_types=['system'], ++ check_str=base.RULE_ADMIN_OR_SYSTEM_READER, ++ scope_types=['system', 'project'], + description='Show endpoint details.', + operations=[{'path': '/v3/endpoints/{endpoint_id}', + 'method': 'GET'}], + deprecated_rule=deprecated_get_endpoint), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'list_endpoints', +- check_str=base.SYSTEM_READER, +- scope_types=['system'], ++ check_str=base.RULE_ADMIN_OR_SYSTEM_READER, ++ scope_types=['system', 'project'], + description='List endpoints.', + operations=[{'path': '/v3/endpoints', + 'method': 'GET'}], + deprecated_rule=deprecated_list_endpoints), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'create_endpoint', +- check_str=base.SYSTEM_ADMIN, +- scope_types=['system'], ++ check_str=base.RULE_ADMIN_REQUIRED, ++ scope_types=['system', 'project'], + description='Create endpoint.', + operations=[{'path': '/v3/endpoints', + 'method': 'POST'}], + deprecated_rule=deprecated_create_endpoint), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'update_endpoint', +- check_str=base.SYSTEM_ADMIN, +- scope_types=['system'], ++ check_str=base.RULE_ADMIN_REQUIRED, ++ scope_types=['system', 'project'], + description='Update endpoint.', + operations=[{'path': '/v3/endpoints/{endpoint_id}', + 'method': 'PATCH'}], + deprecated_rule=deprecated_update_endpoint), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'delete_endpoint', +- check_str=base.SYSTEM_ADMIN, +- scope_types=['system'], ++ check_str=base.RULE_ADMIN_REQUIRED, ++ scope_types=['system', 'project'], + description='Delete endpoint.', + operations=[{'path': '/v3/endpoints/{endpoint_id}', + 'method': 'DELETE'}], +Index: keystone/keystone/common/policies/endpoint_group.py +=================================================================== +--- keystone.orig/keystone/common/policies/endpoint_group.py ++++ keystone/keystone/common/policies/endpoint_group.py +@@ -100,24 +100,24 @@ deprecated_remove_endpoint_group_from_pr + group_endpoint_policies = [ + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'create_endpoint_group', +- check_str=base.SYSTEM_ADMIN, +- scope_types=['system'], ++ check_str=base.RULE_ADMIN_REQUIRED, ++ scope_types=['system', 'project'], + description='Create endpoint group.', + operations=[{'path': '/v3/OS-EP-FILTER/endpoint_groups', + 'method': 'POST'}], + deprecated_rule=deprecated_create_endpoint_group), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'list_endpoint_groups', +- check_str=base.SYSTEM_READER, +- scope_types=['system'], ++ check_str=base.RULE_ADMIN_OR_SYSTEM_READER, ++ scope_types=['system', 'project'], + description='List endpoint groups.', + operations=[{'path': '/v3/OS-EP-FILTER/endpoint_groups', + 'method': 'GET'}], + deprecated_rule=deprecated_list_endpoint_groups), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'get_endpoint_group', +- check_str=base.SYSTEM_READER, +- scope_types=['system'], ++ check_str=base.RULE_ADMIN_OR_SYSTEM_READER, ++ scope_types=['system', 'project'], + description='Get endpoint group.', + operations=[{'path': ('/v3/OS-EP-FILTER/endpoint_groups/' + '{endpoint_group_id}'), +@@ -128,8 +128,8 @@ group_endpoint_policies = [ + deprecated_rule=deprecated_get_endpoint_group), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'update_endpoint_group', +- check_str=base.SYSTEM_ADMIN, +- scope_types=['system'], ++ check_str=base.RULE_ADMIN_REQUIRED, ++ scope_types=['system', 'project'], + description='Update endpoint group.', + operations=[{'path': ('/v3/OS-EP-FILTER/endpoint_groups/' + '{endpoint_group_id}'), +@@ -137,8 +137,8 @@ group_endpoint_policies = [ + deprecated_rule=deprecated_update_endpoint_group), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'delete_endpoint_group', +- check_str=base.SYSTEM_ADMIN, +- scope_types=['system'], ++ check_str=base.RULE_ADMIN_REQUIRED, ++ scope_types=['system', 'project'], + description='Delete endpoint group.', + operations=[{'path': ('/v3/OS-EP-FILTER/endpoint_groups/' + '{endpoint_group_id}'), +@@ -146,8 +146,8 @@ group_endpoint_policies = [ + deprecated_rule=deprecated_delete_endpoint_group), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'list_projects_associated_with_endpoint_group', +- check_str=base.SYSTEM_READER, +- scope_types=['system'], ++ check_str=base.RULE_ADMIN_OR_SYSTEM_READER, ++ scope_types=['system', 'project'], + description=('List all projects associated with a specific endpoint ' + 'group.'), + operations=[{'path': ('/v3/OS-EP-FILTER/endpoint_groups/' +@@ -156,8 +156,8 @@ group_endpoint_policies = [ + deprecated_rule=deprecated_list_projects_assoc_with_endpoint_group), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'list_endpoints_associated_with_endpoint_group', +- check_str=base.SYSTEM_READER, +- scope_types=['system'], ++ check_str=base.RULE_ADMIN_OR_SYSTEM_READER, ++ scope_types=['system', 'project'], + description='List all endpoints associated with an endpoint group.', + operations=[{'path': ('/v3/OS-EP-FILTER/endpoint_groups/' + '{endpoint_group_id}/endpoints'), +@@ -165,8 +165,8 @@ group_endpoint_policies = [ + deprecated_rule=deprecated_list_endpoints_assoc_with_endpoint_group), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'get_endpoint_group_in_project', +- check_str=base.SYSTEM_READER, +- scope_types=['system'], ++ check_str=base.RULE_ADMIN_OR_SYSTEM_READER, ++ scope_types=['system', 'project'], + description=('Check if an endpoint group is associated with a ' + 'project.'), + operations=[{'path': ('/v3/OS-EP-FILTER/endpoint_groups/' +@@ -178,8 +178,8 @@ group_endpoint_policies = [ + deprecated_rule=deprecated_get_endpoint_group_in_project), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'list_endpoint_groups_for_project', +- check_str=base.SYSTEM_READER, +- scope_types=['system'], ++ check_str=base.RULE_ADMIN_OR_SYSTEM_READER, ++ scope_types=['system', 'project'], + description='List endpoint groups associated with a specific project.', + operations=[{'path': ('/v3/OS-EP-FILTER/projects/{project_id}/' + 'endpoint_groups'), +@@ -187,8 +187,8 @@ group_endpoint_policies = [ + deprecated_rule=deprecated_list_endpoint_groups_for_project), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'add_endpoint_group_to_project', +- check_str=base.SYSTEM_ADMIN, +- scope_types=['system'], ++ check_str=base.RULE_ADMIN_REQUIRED, ++ scope_types=['system', 'project'], + description='Allow a project to access an endpoint group.', + operations=[{'path': ('/v3/OS-EP-FILTER/endpoint_groups/' + '{endpoint_group_id}/projects/{project_id}'), +@@ -196,8 +196,8 @@ group_endpoint_policies = [ + deprecated_rule=deprecated_add_endpoint_group_to_project), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'remove_endpoint_group_from_project', +- check_str=base.SYSTEM_ADMIN, +- scope_types=['system'], ++ check_str=base.RULE_ADMIN_REQUIRED, ++ scope_types=['system', 'project'], + description='Remove endpoint group from project.', + operations=[{'path': ('/v3/OS-EP-FILTER/endpoint_groups/' + '{endpoint_group_id}/projects/{project_id}'), +Index: keystone/keystone/common/policies/grant.py +=================================================================== +--- keystone.orig/keystone/common/policies/grant.py ++++ keystone/keystone/common/policies/grant.py +@@ -60,8 +60,19 @@ GRANTS_DOMAIN_ADMIN = ( + '(role:admin and ' + DOMAIN_MATCHES_GROUP_DOMAIN + ' and' + ' ' + DOMAIN_MATCHES_TARGET_DOMAIN + ')' + ) +-SYSTEM_ADMIN_OR_DOMAIN_ADMIN = ( +- '(' + base.SYSTEM_ADMIN + ') or ' ++ ++ADMIN_OR_SYSTEM_READER_OR_DOMAIN_READER = ( ++ '(' + base.RULE_ADMIN_REQUIRED + ') or ' ++ '(' + SYSTEM_READER_OR_DOMAIN_READER + ')' ++) ++ ++ADMIN_OR_SYSTEM_READER_OR_DOMAIN_READER_LIST = ( ++ '(' + base.RULE_ADMIN_REQUIRED + ') or ' ++ '(' + SYSTEM_READER_OR_DOMAIN_READER_LIST + ')' ++) ++ ++ADMIN_OR_DOMAIN_ADMIN = ( ++ '(' + base.RULE_ADMIN_REQUIRED + ') or ' + '(' + GRANTS_DOMAIN_ADMIN + ') and ' + '(' + DOMAIN_MATCHES_ROLE + ')' + ) +@@ -183,8 +194,8 @@ list_grants_operations = ( + grant_policies = [ + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'check_grant', +- check_str=SYSTEM_READER_OR_DOMAIN_READER, +- scope_types=['system', 'domain'], ++ check_str=ADMIN_OR_SYSTEM_READER_OR_DOMAIN_READER, ++ scope_types=['system', 'domain', 'project'], + description=('Check a role grant between a target and an actor. A ' + 'target can be either a domain or a project. An actor ' + 'can be either a user or a group. These terms also apply ' +@@ -195,8 +206,8 @@ grant_policies = [ + deprecated_rule=deprecated_check_grant), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'list_grants', +- check_str=SYSTEM_READER_OR_DOMAIN_READER_LIST, +- scope_types=['system', 'domain'], ++ check_str=ADMIN_OR_SYSTEM_READER_OR_DOMAIN_READER_LIST, ++ scope_types=['system', 'domain', 'project'], + description=('List roles granted to an actor on a target. A target ' + 'can be either a domain or a project. An actor can be ' + 'either a user or a group. For the OS-INHERIT APIs, it ' +@@ -207,8 +218,8 @@ grant_policies = [ + deprecated_rule=deprecated_list_grants), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'create_grant', +- check_str=SYSTEM_ADMIN_OR_DOMAIN_ADMIN, +- scope_types=['system', 'domain'], ++ check_str=ADMIN_OR_DOMAIN_ADMIN, ++ scope_types=['system', 'domain', 'project'], + description=('Create a role grant between a target and an actor. A ' + 'target can be either a domain or a project. An actor ' + 'can be either a user or a group. These terms also apply ' +@@ -219,8 +230,8 @@ grant_policies = [ + deprecated_rule=deprecated_create_grant), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'revoke_grant', +- check_str=SYSTEM_ADMIN_OR_DOMAIN_ADMIN, +- scope_types=['system', 'domain'], ++ check_str=ADMIN_OR_DOMAIN_ADMIN, ++ scope_types=['system', 'domain', 'project'], + description=('Revoke a role grant between a target and an actor. A ' + 'target can be either a domain or a project. An actor ' + 'can be either a user or a group. These terms also apply ' +@@ -233,8 +244,8 @@ grant_policies = [ + deprecated_rule=deprecated_revoke_grant), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'list_system_grants_for_user', +- check_str=base.SYSTEM_READER, +- scope_types=['system'], ++ check_str=base.RULE_ADMIN_OR_SYSTEM_READER, ++ scope_types=['system', 'project'], + description='List all grants a specific user has on the system.', + operations=[ + { +@@ -246,8 +257,8 @@ grant_policies = [ + ), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'check_system_grant_for_user', +- check_str=base.SYSTEM_READER, +- scope_types=['system'], ++ check_str=base.RULE_ADMIN_OR_SYSTEM_READER, ++ scope_types=['system', 'project'], + description='Check if a user has a role on the system.', + operations=[ + { +@@ -259,8 +270,8 @@ grant_policies = [ + ), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'create_system_grant_for_user', +- check_str=base.SYSTEM_ADMIN, +- scope_types=['system'], ++ check_str=base.RULE_ADMIN_REQUIRED, ++ scope_types=['system', 'project'], + description='Grant a user a role on the system.', + operations=[ + { +@@ -272,8 +283,8 @@ grant_policies = [ + ), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'revoke_system_grant_for_user', +- check_str=base.SYSTEM_ADMIN, +- scope_types=['system'], ++ check_str=base.RULE_ADMIN_REQUIRED, ++ scope_types=['system', 'project'], + description='Remove a role from a user on the system.', + operations=[ + { +@@ -285,8 +296,8 @@ grant_policies = [ + ), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'list_system_grants_for_group', +- check_str=base.SYSTEM_READER, +- scope_types=['system'], ++ check_str=base.RULE_ADMIN_OR_SYSTEM_READER, ++ scope_types=['system', 'project'], + description='List all grants a specific group has on the system.', + operations=[ + { +@@ -298,8 +309,8 @@ grant_policies = [ + ), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'check_system_grant_for_group', +- check_str=base.SYSTEM_READER, +- scope_types=['system'], ++ check_str=base.RULE_ADMIN_OR_SYSTEM_READER, ++ scope_types=['system', 'project'], + description='Check if a group has a role on the system.', + operations=[ + { +@@ -311,8 +322,8 @@ grant_policies = [ + ), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'create_system_grant_for_group', +- check_str=base.SYSTEM_ADMIN, +- scope_types=['system'], ++ check_str=base.RULE_ADMIN_REQUIRED, ++ scope_types=['system', 'project'], + description='Grant a group a role on the system.', + operations=[ + { +@@ -324,8 +335,8 @@ grant_policies = [ + ), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'revoke_system_grant_for_group', +- check_str=base.SYSTEM_ADMIN, +- scope_types=['system'], ++ check_str=base.RULE_ADMIN_REQUIRED, ++ scope_types=['system', 'project'], + description='Remove a role from a group on the system.', + operations=[ + { +Index: keystone/keystone/common/policies/group.py +=================================================================== +--- keystone.orig/keystone/common/policies/group.py ++++ keystone/keystone/common/policies/group.py +@@ -20,6 +20,10 @@ SYSTEM_READER_OR_DOMAIN_READER_FOR_TARGE + '(role:reader and domain_id:%(target.user.domain_id)s) or ' + 'user_id:%(user_id)s' + ) ++ADMIN_OR_SYSTEM_READER_OR_DOMAIN_READER_FOR_TARGET_OR_OWNER = ( ++ '(' + base.RULE_ADMIN_REQUIRED + ') or (' + ++ SYSTEM_READER_OR_DOMAIN_READER_FOR_TARGET_USER_OR_OWNER ++) + + SYSTEM_READER_OR_DOMAIN_READER_FOR_TARGET_GROUP_USER = ( + '(role:reader and system_scope:all) or ' +@@ -27,18 +31,19 @@ SYSTEM_READER_OR_DOMAIN_READER_FOR_TARGE + 'domain_id:%(target.group.domain_id)s and ' + 'domain_id:%(target.user.domain_id)s)' + ) +- +-SYSTEM_ADMIN_OR_DOMAIN_ADMIN_FOR_TARGET_GROUP_USER = ( +- '(role:admin and system_scope:all) or ' +- '(role:admin 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 + ') or (' + ++ SYSTEM_READER_OR_DOMAIN_READER_FOR_TARGET_GROUP_USER + ) + + SYSTEM_READER_OR_DOMAIN_READER = ( + '(role:reader and system_scope:all) or ' + '(role:reader and domain_id:%(target.group.domain_id)s)' + ) ++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 ' +@@ -113,8 +118,8 @@ deprecated_add_user_to_group = policy.De + group_policies = [ + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'get_group', +- check_str=SYSTEM_READER_OR_DOMAIN_READER, +- scope_types=['system', 'domain'], ++ check_str=ADMIN_OR_SYSTEM_READER_OR_DOMAIN_READER, ++ scope_types=['system', 'domain', 'project'], + description='Show group details.', + operations=[{'path': '/v3/groups/{group_id}', + 'method': 'GET'}, +@@ -123,8 +128,8 @@ group_policies = [ + deprecated_rule=deprecated_get_group), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'list_groups', +- check_str=SYSTEM_READER_OR_DOMAIN_READER, +- scope_types=['system', 'domain'], ++ check_str=ADMIN_OR_SYSTEM_READER_OR_DOMAIN_READER, ++ scope_types=['system', 'domain', 'project'], + description='List groups.', + operations=[{'path': '/v3/groups', + 'method': 'GET'}, +@@ -133,7 +138,7 @@ group_policies = [ + deprecated_rule=deprecated_list_groups), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'list_groups_for_user', +- check_str=SYSTEM_READER_OR_DOMAIN_READER_FOR_TARGET_USER_OR_OWNER, ++ check_str=ADMIN_OR_SYSTEM_READER_OR_DOMAIN_READER_FOR_TARGET_OR_OWNER, + scope_types=['system', 'domain', 'project'], + description='List groups to which a user belongs.', + operations=[{'path': '/v3/users/{user_id}/groups', +@@ -143,32 +148,32 @@ group_policies = [ + deprecated_rule=deprecated_list_groups_for_user), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'create_group', +- check_str=SYSTEM_ADMIN_OR_DOMAIN_ADMIN, +- scope_types=['system', 'domain'], ++ check_str=base.RULE_ADMIN_REQUIRED, ++ scope_types=['system', 'domain', 'project'], + description='Create group.', + operations=[{'path': '/v3/groups', + 'method': 'POST'}], + deprecated_rule=deprecated_create_group), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'update_group', +- check_str=SYSTEM_ADMIN_OR_DOMAIN_ADMIN, +- scope_types=['system', 'domain'], ++ check_str=base.RULE_ADMIN_REQUIRED, ++ scope_types=['system', 'domain', 'project'], + description='Update group.', + operations=[{'path': '/v3/groups/{group_id}', + 'method': 'PATCH'}], + deprecated_rule=deprecated_update_group), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'delete_group', +- check_str=SYSTEM_ADMIN_OR_DOMAIN_ADMIN, +- scope_types=['system', 'domain'], ++ check_str=base.RULE_ADMIN_REQUIRED, ++ scope_types=['system', 'domain', 'project'], + description='Delete group.', + operations=[{'path': '/v3/groups/{group_id}', + 'method': 'DELETE'}], + deprecated_rule=deprecated_delete_group), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'list_users_in_group', +- check_str=SYSTEM_READER_OR_DOMAIN_READER, +- scope_types=['system', 'domain'], ++ check_str=ADMIN_OR_SYSTEM_READER_OR_DOMAIN_READER, ++ scope_types=['system', 'domain', 'project'], + description='List members of a specific group.', + operations=[{'path': '/v3/groups/{group_id}/users', + 'method': 'GET'}, +@@ -177,16 +182,16 @@ group_policies = [ + deprecated_rule=deprecated_list_users_in_group), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'remove_user_from_group', +- check_str=SYSTEM_ADMIN_OR_DOMAIN_ADMIN_FOR_TARGET_GROUP_USER, +- scope_types=['system', 'domain'], ++ check_str=base.RULE_ADMIN_REQUIRED, ++ scope_types=['system', 'domain', 'project'], + description='Remove user from group.', + operations=[{'path': '/v3/groups/{group_id}/users/{user_id}', + 'method': 'DELETE'}], + deprecated_rule=deprecated_remove_user_from_group), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'check_user_in_group', +- check_str=SYSTEM_READER_OR_DOMAIN_READER_FOR_TARGET_GROUP_USER, +- scope_types=['system', 'domain'], ++ check_str=ADMIN_OR_SYSTEM_READER_OR_DOMAIN_READER_FOR_TARGET_GROUP, ++ scope_types=['system', 'domain', 'project'], + description='Check whether a user is a member of a group.', + operations=[{'path': '/v3/groups/{group_id}/users/{user_id}', + 'method': 'HEAD'}, +@@ -195,8 +200,8 @@ group_policies = [ + deprecated_rule=deprecated_check_user_in_group), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'add_user_to_group', +- check_str=SYSTEM_ADMIN_OR_DOMAIN_ADMIN_FOR_TARGET_GROUP_USER, +- scope_types=['system', 'domain'], ++ check_str=base.RULE_ADMIN_REQUIRED, ++ scope_types=['system', 'domain', 'project'], + description='Add user to group.', + operations=[{'path': '/v3/groups/{group_id}/users/{user_id}', + 'method': 'PUT'}], +Index: keystone/keystone/common/policies/identity_provider.py +=================================================================== +--- keystone.orig/keystone/common/policies/identity_provider.py ++++ keystone/keystone/common/policies/identity_provider.py +@@ -54,22 +54,22 @@ deprecated_delete_idp = policy.Deprecate + identity_provider_policies = [ + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'create_identity_provider', +- check_str=base.SYSTEM_ADMIN, ++ check_str=base.RULE_ADMIN_REQUIRED, + # FIXME(lbragstad): All `scope_types` for identity provider policies + # should be updated to include project scope if, or when, it becomes + # possible to manage federated identity providers without modifying + # configurations outside of keystone (Apache). It makes sense to + # associate system scope to identity provider management since it + # requires modifying configuration files. +- scope_types=['system'], ++ scope_types=['system', 'project'], + description='Create identity provider.', + operations=[{'path': '/v3/OS-FEDERATION/identity_providers/{idp_id}', + 'method': 'PUT'}], + deprecated_rule=deprecated_create_idp), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'list_identity_providers', +- check_str=base.SYSTEM_READER, +- scope_types=['system'], ++ check_str=base.RULE_ADMIN_OR_SYSTEM_READER, ++ scope_types=['system', 'project'], + description='List identity providers.', + operations=[ + { +@@ -85,8 +85,8 @@ identity_provider_policies = [ + ), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'get_identity_provider', +- check_str=base.SYSTEM_READER, +- scope_types=['system'], ++ check_str=base.RULE_ADMIN_OR_SYSTEM_READER, ++ scope_types=['system', 'project'], + description='Get identity provider.', + operations=[ + { +@@ -102,16 +102,16 @@ identity_provider_policies = [ + ), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'update_identity_provider', +- check_str=base.SYSTEM_ADMIN, +- scope_types=['system'], ++ check_str=base.RULE_ADMIN_REQUIRED, ++ scope_types=['system', 'project'], + description='Update identity provider.', + operations=[{'path': '/v3/OS-FEDERATION/identity_providers/{idp_id}', + 'method': 'PATCH'}], + deprecated_rule=deprecated_update_idp), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'delete_identity_provider', +- check_str=base.SYSTEM_ADMIN, +- scope_types=['system'], ++ check_str=base.RULE_ADMIN_REQUIRED, ++ scope_types=['system', 'project'], + description='Delete identity provider.', + operations=[{'path': '/v3/OS-FEDERATION/identity_providers/{idp_id}', + 'method': 'DELETE'}], +Index: keystone/keystone/common/policies/implied_role.py +=================================================================== +--- keystone.orig/keystone/common/policies/implied_role.py ++++ keystone/keystone/common/policies/implied_role.py +@@ -60,12 +60,12 @@ deprecated_delete_implied_role = policy. + implied_role_policies = [ + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'get_implied_role', +- check_str=base.SYSTEM_READER, ++ check_str=base.RULE_ADMIN_OR_SYSTEM_READER, + # FIXME(lbragstad) The management of implied roles currently makes + # sense as a system-only resource. Once keystone has the ability to + # support RBAC solely over the API without having to customize policy + # files, scope_types should include 'project'. +- scope_types=['system'], ++ scope_types=['system', 'project'], + description='Get information about an association between two roles. ' + 'When a relationship exists between a prior role and an ' + 'implied role and the prior role is assigned to a user, ' +@@ -76,8 +76,8 @@ implied_role_policies = [ + deprecated_rule=deprecated_get_implied_role), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'list_implied_roles', +- check_str=base.SYSTEM_READER, +- scope_types=['system'], ++ check_str=base.RULE_ADMIN_OR_SYSTEM_READER, ++ scope_types=['system', 'project'], + description='List associations between two roles. When a relationship ' + 'exists between a prior role and an implied role and the ' + 'prior role is assigned to a user, the user also assumes ' +@@ -90,8 +90,8 @@ implied_role_policies = [ + deprecated_rule=deprecated_list_implied_roles), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'create_implied_role', +- check_str=base.SYSTEM_ADMIN, +- scope_types=['system'], ++ check_str=base.RULE_ADMIN_REQUIRED, ++ scope_types=['system', 'project'], + description='Create an association between two roles. When a ' + 'relationship exists between a prior role and an implied ' + 'role and the prior role is assigned to a user, the user ' +@@ -102,8 +102,8 @@ implied_role_policies = [ + deprecated_rule=deprecated_create_implied_role), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'delete_implied_role', +- check_str=base.SYSTEM_ADMIN, +- scope_types=['system'], ++ check_str=base.RULE_ADMIN_REQUIRED, ++ scope_types=['system', 'project'], + description='Delete the association between two roles. When a ' + 'relationship exists between a prior role and an implied ' + 'role and the prior role is assigned to a user, the user ' +@@ -115,8 +115,8 @@ implied_role_policies = [ + deprecated_rule=deprecated_delete_implied_role), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'list_role_inference_rules', +- check_str=base.SYSTEM_READER, +- scope_types=['system'], ++ check_str=base.RULE_ADMIN_OR_SYSTEM_READER, ++ scope_types=['system', 'project'], + description='List all associations between two roles in the system. ' + 'When a relationship exists between a prior role and an ' + 'implied role and the prior role is assigned to a user, ' +@@ -127,8 +127,8 @@ implied_role_policies = [ + deprecated_rule=deprecated_list_role_inference_rules), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'check_implied_role', +- check_str=base.SYSTEM_READER, +- scope_types=['system'], ++ check_str=base.RULE_ADMIN_OR_SYSTEM_READER, ++ scope_types=['system', 'project'], + description='Check an association between two roles. When a ' + 'relationship exists between a prior role and an implied ' + 'role and the prior role is assigned to a user, the user ' +Index: keystone/keystone/common/policies/limit.py +=================================================================== +--- keystone.orig/keystone/common/policies/limit.py ++++ keystone/keystone/common/policies/limit.py +@@ -14,7 +14,8 @@ from oslo_policy import policy + + from keystone.common.policies import base + +-SYSTEM_OR_DOMAIN_OR_PROJECT_USER = ( ++ADMIN_OR_SYSTEM_OR_DOMAIN_OR_PROJECT_USER = ( ++ base.RULE_ADMIN_REQUIRED + ' or ' + '(' + base.SYSTEM_READER + ') or ' + '(' + 'domain_id:%(target.limit.domain.id)s or ' +@@ -38,7 +39,7 @@ limit_policies = [ + 'method': 'HEAD'}]), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'get_limit', +- check_str=SYSTEM_OR_DOMAIN_OR_PROJECT_USER, ++ check_str=ADMIN_OR_SYSTEM_OR_DOMAIN_OR_PROJECT_USER, + scope_types=['system', 'domain', 'project'], + description='Show limit details.', + operations=[{'path': '/v3/limits/{limit_id}', +@@ -56,22 +57,22 @@ limit_policies = [ + 'method': 'HEAD'}]), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'create_limits', +- check_str=base.SYSTEM_ADMIN, +- scope_types=['system'], ++ check_str=base.RULE_ADMIN_REQUIRED, ++ scope_types=['system', 'project'], + description='Create limits.', + operations=[{'path': '/v3/limits', + 'method': 'POST'}]), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'update_limit', +- check_str=base.SYSTEM_ADMIN, +- scope_types=['system'], ++ check_str=base.RULE_ADMIN_REQUIRED, ++ scope_types=['system', 'project'], + description='Update limit.', + operations=[{'path': '/v3/limits/{limit_id}', + 'method': 'PATCH'}]), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'delete_limit', +- check_str=base.SYSTEM_ADMIN, +- scope_types=['system'], ++ check_str=base.RULE_ADMIN_REQUIRED, ++ scope_types=['system', 'project'], + description='Delete limit.', + operations=[{'path': '/v3/limits/{limit_id}', + 'method': 'DELETE'}]) +Index: keystone/keystone/common/policies/mapping.py +=================================================================== +--- keystone.orig/keystone/common/policies/mapping.py ++++ keystone/keystone/common/policies/mapping.py +@@ -54,14 +54,8 @@ deprecated_delete_mapping = policy.Depre + mapping_policies = [ + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'create_mapping', +- check_str=base.SYSTEM_ADMIN, +- # FIXME(lbragstad): Today, keystone doesn't support federation unless +- # the person create identity providers, service providers, or mappings +- # has the ability to modify keystone and Apache configuration files. +- # If, or when, keystone adds support for federating identities without +- # having to touch system configuration files, the list of `scope_types` +- # for these policies should include `project`. +- scope_types=['system'], ++ check_str=base.RULE_ADMIN_REQUIRED, ++ scope_types=['system', 'project'], + description=('Create a new federated mapping containing one or ' + 'more sets of rules.'), + operations=[{'path': '/v3/OS-FEDERATION/mappings/{mapping_id}', +@@ -69,8 +63,8 @@ mapping_policies = [ + deprecated_rule=deprecated_create_mapping), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'get_mapping', +- check_str=base.SYSTEM_READER, +- scope_types=['system'], ++ check_str=base.RULE_ADMIN_OR_SYSTEM_READER, ++ scope_types=['system', 'project'], + description='Get a federated mapping.', + operations=[ + { +@@ -86,8 +80,8 @@ mapping_policies = [ + ), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'list_mappings', +- check_str=base.SYSTEM_READER, +- scope_types=['system'], ++ check_str=base.RULE_ADMIN_OR_SYSTEM_READER, ++ scope_types=['system', 'project'], + description='List federated mappings.', + operations=[ + { +@@ -103,16 +97,16 @@ mapping_policies = [ + ), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'delete_mapping', +- check_str=base.SYSTEM_ADMIN, +- scope_types=['system'], ++ check_str=base.RULE_ADMIN_REQUIRED, ++ scope_types=['system', 'project'], + description='Delete a federated mapping.', + operations=[{'path': '/v3/OS-FEDERATION/mappings/{mapping_id}', + 'method': 'DELETE'}], + deprecated_rule=deprecated_delete_mapping), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'update_mapping', +- check_str=base.SYSTEM_ADMIN, +- scope_types=['system'], ++ check_str=base.RULE_ADMIN_REQUIRED, ++ scope_types=['system', 'project'], + description='Update a federated mapping.', + operations=[{'path': '/v3/OS-FEDERATION/mappings/{mapping_id}', + 'method': 'PATCH'}], +Index: keystone/keystone/common/policies/policy.py +=================================================================== +--- keystone.orig/keystone/common/policies/policy.py ++++ keystone/keystone/common/policies/policy.py +@@ -58,42 +58,42 @@ deprecated_delete_policy = policy.Deprec + policy_policies = [ + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'get_policy', +- check_str=base.SYSTEM_READER, ++ check_str=base.RULE_ADMIN_OR_SYSTEM_READER, + # This API isn't really exposed to usable, it's actually deprecated. + # More-or-less adding scope_types to be consistent with other policies. +- scope_types=['system'], ++ scope_types=['system', 'project'], + description='Show policy details.', + operations=[{'path': '/v3/policies/{policy_id}', + 'method': 'GET'}], + deprecated_rule=deprecated_get_policy), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'list_policies', +- check_str=base.SYSTEM_READER, +- scope_types=['system'], ++ check_str=base.RULE_ADMIN_OR_SYSTEM_READER, ++ scope_types=['system', 'project'], + description='List policies.', + operations=[{'path': '/v3/policies', + 'method': 'GET'}], + deprecated_rule=deprecated_list_policies), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'create_policy', +- check_str=base.SYSTEM_ADMIN, +- scope_types=['system'], ++ check_str=base.RULE_ADMIN_REQUIRED, ++ scope_types=['system', 'project'], + description='Create policy.', + operations=[{'path': '/v3/policies', + 'method': 'POST'}], + deprecated_rule=deprecated_create_policy), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'update_policy', +- check_str=base.SYSTEM_ADMIN, +- scope_types=['system'], ++ check_str=base.RULE_ADMIN_REQUIRED, ++ scope_types=['system', 'project'], + description='Update policy.', + operations=[{'path': '/v3/policies/{policy_id}', + 'method': 'PATCH'}], + deprecated_rule=deprecated_update_policy), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'delete_policy', +- check_str=base.SYSTEM_ADMIN, +- scope_types=['system'], ++ check_str=base.RULE_ADMIN_REQUIRED, ++ scope_types=['system', 'project'], + description='Delete policy.', + operations=[{'path': '/v3/policies/{policy_id}', + 'method': 'DELETE'}], +Index: keystone/keystone/common/policies/policy_association.py +=================================================================== +--- keystone.orig/keystone/common/policies/policy_association.py ++++ keystone/keystone/common/policies/policy_association.py +@@ -105,8 +105,8 @@ deprecated_delete_policy_assoc_for_regio + policy_association_policies = [ + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'create_policy_association_for_endpoint', +- check_str=base.SYSTEM_ADMIN, +- scope_types=['system'], ++ check_str=base.RULE_ADMIN_REQUIRED, ++ scope_types=['system', 'project'], + description='Associate a policy to a specific endpoint.', + operations=[{'path': ('/v3/policies/{policy_id}/OS-ENDPOINT-POLICY/' + 'endpoints/{endpoint_id}'), +@@ -114,8 +114,8 @@ policy_association_policies = [ + deprecated_rule=deprecated_create_policy_assoc_for_endpoint), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'check_policy_association_for_endpoint', +- check_str=base.SYSTEM_READER, +- scope_types=['system'], ++ check_str=base.RULE_ADMIN_OR_SYSTEM_READER, ++ scope_types=['system', 'project'], + description='Check policy association for endpoint.', + operations=[{'path': ('/v3/policies/{policy_id}/OS-ENDPOINT-POLICY/' + 'endpoints/{endpoint_id}'), +@@ -126,8 +126,8 @@ policy_association_policies = [ + deprecated_rule=deprecated_check_policy_assoc_for_endpoint), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'delete_policy_association_for_endpoint', +- check_str=base.SYSTEM_ADMIN, +- scope_types=['system'], ++ check_str=base.RULE_ADMIN_REQUIRED, ++ scope_types=['system', 'project'], + description='Delete policy association for endpoint.', + operations=[{'path': ('/v3/policies/{policy_id}/OS-ENDPOINT-POLICY/' + 'endpoints/{endpoint_id}'), +@@ -135,8 +135,8 @@ policy_association_policies = [ + deprecated_rule=deprecated_delete_policy_assoc_for_endpoint), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'create_policy_association_for_service', +- check_str=base.SYSTEM_ADMIN, +- scope_types=['system'], ++ check_str=base.RULE_ADMIN_REQUIRED, ++ scope_types=['system', 'project'], + description='Associate a policy to a specific service.', + operations=[{'path': ('/v3/policies/{policy_id}/OS-ENDPOINT-POLICY/' + 'services/{service_id}'), +@@ -144,8 +144,8 @@ policy_association_policies = [ + deprecated_rule=deprecated_create_policy_assoc_for_service), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'check_policy_association_for_service', +- check_str=base.SYSTEM_READER, +- scope_types=['system'], ++ check_str=base.RULE_ADMIN_OR_SYSTEM_READER, ++ scope_types=['system', 'project'], + description='Check policy association for service.', + operations=[{'path': ('/v3/policies/{policy_id}/OS-ENDPOINT-POLICY/' + 'services/{service_id}'), +@@ -156,8 +156,8 @@ policy_association_policies = [ + deprecated_rule=deprecated_check_policy_assoc_for_service), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'delete_policy_association_for_service', +- check_str=base.SYSTEM_ADMIN, +- scope_types=['system'], ++ check_str=base.RULE_ADMIN_REQUIRED, ++ scope_types=['system', 'project'], + description='Delete policy association for service.', + operations=[{'path': ('/v3/policies/{policy_id}/OS-ENDPOINT-POLICY/' + 'services/{service_id}'), +@@ -166,8 +166,8 @@ policy_association_policies = [ + policy.DocumentedRuleDefault( + name=base.IDENTITY % ( + 'create_policy_association_for_region_and_service'), +- check_str=base.SYSTEM_ADMIN, +- scope_types=['system'], ++ check_str=base.RULE_ADMIN_REQUIRED, ++ scope_types=['system', 'project'], + description=('Associate a policy to a specific region and service ' + 'combination.'), + operations=[{'path': ('/v3/policies/{policy_id}/OS-ENDPOINT-POLICY/' +@@ -176,8 +176,8 @@ policy_association_policies = [ + deprecated_rule=deprecated_create_policy_assoc_for_region_and_service), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'check_policy_association_for_region_and_service', +- check_str=base.SYSTEM_READER, +- scope_types=['system'], ++ check_str=base.RULE_ADMIN_OR_SYSTEM_READER, ++ scope_types=['system', 'project'], + description='Check policy association for region and service.', + operations=[{'path': ('/v3/policies/{policy_id}/OS-ENDPOINT-POLICY/' + 'services/{service_id}/regions/{region_id}'), +@@ -189,8 +189,8 @@ policy_association_policies = [ + policy.DocumentedRuleDefault( + name=base.IDENTITY % ( + 'delete_policy_association_for_region_and_service'), +- check_str=base.SYSTEM_ADMIN, +- scope_types=['system'], ++ check_str=base.RULE_ADMIN_REQUIRED, ++ scope_types=['system', 'project'], + description='Delete policy association for region and service.', + operations=[{'path': ('/v3/policies/{policy_id}/OS-ENDPOINT-POLICY/' + 'services/{service_id}/regions/{region_id}'), +@@ -198,8 +198,8 @@ policy_association_policies = [ + deprecated_rule=deprecated_delete_policy_assoc_for_region_and_service), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'get_policy_for_endpoint', +- check_str=base.SYSTEM_READER, +- scope_types=['system'], ++ check_str=base.RULE_ADMIN_OR_SYSTEM_READER, ++ scope_types=['system', 'project'], + description='Get policy for endpoint.', + operations=[{'path': ('/v3/endpoints/{endpoint_id}/OS-ENDPOINT-POLICY/' + 'policy'), +@@ -210,8 +210,8 @@ policy_association_policies = [ + deprecated_rule=deprecated_get_policy_for_endpoint), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'list_endpoints_for_policy', +- check_str=base.SYSTEM_READER, +- scope_types=['system'], ++ check_str=base.RULE_ADMIN_OR_SYSTEM_READER, ++ scope_types=['system', 'project'], + description='List endpoints for policy.', + operations=[{'path': ('/v3/policies/{policy_id}/OS-ENDPOINT-POLICY/' + 'endpoints'), +Index: keystone/keystone/common/policies/project.py +=================================================================== +--- keystone.orig/keystone/common/policies/project.py ++++ keystone/keystone/common/policies/project.py +@@ -20,6 +20,10 @@ SYSTEM_READER_OR_DOMAIN_READER_OR_PROJEC + '(role:reader and domain_id:%(target.project.domain_id)s) or ' + 'project_id:%(target.project.id)s' + ) ++ADMIN_OR_SYSTEM_READER_OR_DOMAIN_READER_OR_PROJECT_USER = ( ++ '(' + base.RULE_ADMIN_REQUIRED + ') or ' + ++ SYSTEM_READER_OR_DOMAIN_READER_OR_PROJECT_USER ++) + + SYSTEM_ADMIN_OR_DOMAIN_ADMIN_OR_PROJECT_ADMIN = ( + '(' + base.SYSTEM_ADMIN + ') or ' +@@ -41,12 +45,21 @@ 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 = ( ++ '(' + base.RULE_ADMIN_REQUIRED + ') or ' + ++ SYSTEM_READER_OR_DOMAIN_READER_OR_OWNER ++) + + SYSTEM_READER_OR_DOMAIN_READER = ( + '(' + base.SYSTEM_READER + ') or ' + '(role:reader and domain_id:%(target.domain_id)s)' + ) + ++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)' +@@ -149,19 +162,15 @@ project_policies = [ + deprecated_rule=deprecated_get_project), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'list_projects', +- check_str=SYSTEM_READER_OR_DOMAIN_READER, +- # FIXME(lbragstad): Project administrators should be able to list +- # projects they administer or possibly their children. Until keystone +- # is smart enough to handle those cases, keep scope_types set to +- # 'system' and 'domain'. +- scope_types=['system', 'domain'], ++ check_str=ADMIN_OR_SYSTEM_READER_OR_DOMAIN_READER, ++ scope_types=['system', 'domain', 'project'], + description='List projects.', + operations=[{'path': '/v3/projects', + 'method': 'GET'}], + deprecated_rule=deprecated_list_projects), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'list_user_projects', +- check_str=SYSTEM_READER_OR_DOMAIN_READER_OR_OWNER, ++ check_str=ADMIN_OR_SYSTEM_READER_OR_DOMAIN_READER_OR_OWNER, + scope_types=['system', 'domain', 'project'], + description='List projects for user.', + operations=[{'path': '/v3/users/{user_id}/projects', +@@ -169,31 +178,31 @@ project_policies = [ + deprecated_rule=deprecated_list_user_projects), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'create_project', +- check_str=SYSTEM_ADMIN_OR_DOMAIN_ADMIN, +- scope_types=['system', 'domain'], ++ check_str=base.RULE_ADMIN_REQUIRED, ++ scope_types=['system', 'domain', 'project'], + description='Create project.', + operations=[{'path': '/v3/projects', + 'method': 'POST'}], + deprecated_rule=deprecated_create_project), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'update_project', +- check_str=SYSTEM_ADMIN_OR_DOMAIN_ADMIN, +- scope_types=['system', 'domain'], ++ check_str=base.RULE_ADMIN_REQUIRED, ++ scope_types=['system', 'domain', 'project'], + description='Update project.', + operations=[{'path': '/v3/projects/{project_id}', + 'method': 'PATCH'}], + deprecated_rule=deprecated_update_project), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'delete_project', +- check_str=SYSTEM_ADMIN_OR_DOMAIN_ADMIN, +- scope_types=['system', 'domain'], ++ check_str=base.RULE_ADMIN_REQUIRED, ++ scope_types=['system', 'domain', 'project'], + description='Delete project.', + operations=[{'path': '/v3/projects/{project_id}', + 'method': 'DELETE'}], + deprecated_rule=deprecated_delete_project), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'list_project_tags', +- check_str=SYSTEM_READER_OR_DOMAIN_READER_OR_PROJECT_USER, ++ check_str=ADMIN_OR_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', +@@ -203,7 +212,7 @@ project_policies = [ + deprecated_rule=deprecated_list_project_tags), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'get_project_tag', +- check_str=SYSTEM_READER_OR_DOMAIN_READER_OR_PROJECT_USER, ++ check_str=ADMIN_OR_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}', +@@ -213,7 +222,7 @@ project_policies = [ + deprecated_rule=deprecated_get_project_tag), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'update_project_tags', +- check_str=SYSTEM_ADMIN_OR_DOMAIN_ADMIN_OR_PROJECT_ADMIN, ++ check_str=base.RULE_ADMIN_REQUIRED, + 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', +@@ -221,7 +230,7 @@ project_policies = [ + deprecated_rule=deprecated_update_project_tag), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'create_project_tag', +- check_str=SYSTEM_ADMIN_OR_DOMAIN_ADMIN_OR_PROJECT_ADMIN, ++ check_str=base.RULE_ADMIN_REQUIRED, + scope_types=['system', 'domain', 'project'], + description='Add a single tag to a project.', + operations=[{'path': '/v3/projects/{project_id}/tags/{value}', +@@ -229,7 +238,7 @@ project_policies = [ + deprecated_rule=deprecated_create_project_tag), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'delete_project_tags', +- check_str=SYSTEM_ADMIN_OR_DOMAIN_ADMIN_OR_PROJECT_ADMIN, ++ check_str=base.RULE_ADMIN_REQUIRED, + scope_types=['system', 'domain', 'project'], + description='Remove all tags from a project.', + operations=[{'path': '/v3/projects/{project_id}/tags', +@@ -237,7 +246,7 @@ project_policies = [ + deprecated_rule=deprecated_delete_project_tags), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'delete_project_tag', +- check_str=SYSTEM_ADMIN_OR_DOMAIN_ADMIN_OR_PROJECT_ADMIN, ++ 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}', +Index: keystone/keystone/common/policies/project_endpoint.py +=================================================================== +--- keystone.orig/keystone/common/policies/project_endpoint.py ++++ keystone/keystone/common/policies/project_endpoint.py +@@ -63,12 +63,8 @@ project_endpoint_policies = [ + + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'list_projects_for_endpoint', +- check_str=base.SYSTEM_READER, +- # NOTE(lbragstad): While projects can be considered project-level APIs +- # with hierarchical multi-tenancy, endpoints are a system-level +- # resource. Managing associations between projects and endpoints should +- # default to system-level. +- scope_types=['system'], ++ check_str=base.RULE_ADMIN_OR_SYSTEM_READER, ++ scope_types=['system', 'project'], + description='List projects allowed to access an endpoint.', + operations=[{'path': ('/v3/OS-EP-FILTER/endpoints/{endpoint_id}/' + 'projects'), +@@ -76,8 +72,8 @@ project_endpoint_policies = [ + deprecated_rule=deprecated_list_projects_for_endpoint), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'add_endpoint_to_project', +- check_str=base.SYSTEM_ADMIN, +- scope_types=['system'], ++ check_str=base.RULE_ADMIN_REQUIRED, ++ scope_types=['system', 'project'], + description='Allow project to access an endpoint.', + operations=[{'path': ('/v3/OS-EP-FILTER/projects/{project_id}/' + 'endpoints/{endpoint_id}'), +@@ -85,8 +81,8 @@ project_endpoint_policies = [ + deprecated_rule=deprecated_add_endpoint_to_project), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'check_endpoint_in_project', +- check_str=base.SYSTEM_READER, +- scope_types=['system'], ++ check_str=base.RULE_ADMIN_OR_SYSTEM_READER, ++ scope_types=['system', 'project'], + description='Check if a project is allowed to access an endpoint.', + operations=[{'path': ('/v3/OS-EP-FILTER/projects/{project_id}/' + 'endpoints/{endpoint_id}'), +@@ -97,8 +93,8 @@ project_endpoint_policies = [ + deprecated_rule=deprecated_check_endpoint_in_project), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'list_endpoints_for_project', +- check_str=base.SYSTEM_READER, +- scope_types=['system'], ++ check_str=base.RULE_ADMIN_OR_SYSTEM_READER, ++ scope_types=['system', 'project'], + description='List the endpoints a project is allowed to access.', + operations=[{'path': ('/v3/OS-EP-FILTER/projects/{project_id}/' + 'endpoints'), +@@ -106,8 +102,8 @@ project_endpoint_policies = [ + deprecated_rule=deprecated_list_endpoints_for_project), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'remove_endpoint_from_project', +- check_str=base.SYSTEM_ADMIN, +- scope_types=['system'], ++ check_str=base.RULE_ADMIN_REQUIRED, ++ scope_types=['system', 'project'], + description=('Remove access to an endpoint from a project that has ' + 'previously been given explicit access.'), + operations=[{'path': ('/v3/OS-EP-FILTER/projects/{project_id}/' +Index: keystone/keystone/common/policies/protocol.py +=================================================================== +--- keystone.orig/keystone/common/policies/protocol.py ++++ keystone/keystone/common/policies/protocol.py +@@ -55,11 +55,8 @@ deprecated_delete_protocol = policy.Depr + protocol_policies = [ + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'create_protocol', +- check_str=base.SYSTEM_ADMIN, +- # FIXME(lbragstad): Once it is possible to add complete federated +- # identity without having to modify system configuration files, like +- # Apache, this should include 'project' in scope_types. +- scope_types=['system'], ++ check_str=base.RULE_ADMIN_REQUIRED, ++ scope_types=['system', 'project'], + description='Create federated protocol.', + operations=[{'path': ('/v3/OS-FEDERATION/identity_providers/{idp_id}/' + 'protocols/{protocol_id}'), +@@ -67,8 +64,8 @@ protocol_policies = [ + deprecated_rule=deprecated_create_protocol), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'update_protocol', +- check_str=base.SYSTEM_ADMIN, +- scope_types=['system'], ++ check_str=base.RULE_ADMIN_REQUIRED, ++ scope_types=['system', 'project'], + description='Update federated protocol.', + operations=[{'path': ('/v3/OS-FEDERATION/identity_providers/{idp_id}/' + 'protocols/{protocol_id}'), +@@ -76,8 +73,8 @@ protocol_policies = [ + deprecated_rule=deprecated_update_protocol), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'get_protocol', +- check_str=base.SYSTEM_READER, +- scope_types=['system'], ++ check_str=base.RULE_ADMIN_OR_SYSTEM_READER, ++ scope_types=['system', 'project'], + description='Get federated protocol.', + operations=[{'path': ('/v3/OS-FEDERATION/identity_providers/{idp_id}/' + 'protocols/{protocol_id}'), +@@ -85,8 +82,8 @@ protocol_policies = [ + deprecated_rule=deprecated_get_protocol), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'list_protocols', +- check_str=base.SYSTEM_READER, +- scope_types=['system'], ++ check_str=base.RULE_ADMIN_OR_SYSTEM_READER, ++ scope_types=['system', 'project'], + description='List federated protocols.', + operations=[{'path': ('/v3/OS-FEDERATION/identity_providers/{idp_id}/' + 'protocols'), +@@ -94,8 +91,8 @@ protocol_policies = [ + deprecated_rule=deprecated_list_protocols), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'delete_protocol', +- check_str=base.SYSTEM_ADMIN, +- scope_types=['system'], ++ check_str=base.RULE_ADMIN_REQUIRED, ++ scope_types=['system', 'project'], + description='Delete federated protocol.', + operations=[{'path': ('/v3/OS-FEDERATION/identity_providers/{idp_id}/' + 'protocols/{protocol_id}'), +Index: keystone/keystone/common/policies/region.py +=================================================================== +--- keystone.orig/keystone/common/policies/region.py ++++ keystone/keystone/common/policies/region.py +@@ -66,8 +66,8 @@ region_policies = [ + 'method': 'HEAD'}]), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'create_region', +- check_str=base.SYSTEM_ADMIN, +- scope_types=['system'], ++ check_str=base.RULE_ADMIN_REQUIRED, ++ scope_types=['system', 'project'], + description='Create region.', + operations=[{'path': '/v3/regions', + 'method': 'POST'}, +@@ -76,16 +76,16 @@ region_policies = [ + deprecated_rule=deprecated_create_region), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'update_region', +- check_str=base.SYSTEM_ADMIN, +- scope_types=['system'], ++ check_str=base.RULE_ADMIN_REQUIRED, ++ scope_types=['system', 'project'], + description='Update region.', + operations=[{'path': '/v3/regions/{region_id}', + 'method': 'PATCH'}], + deprecated_rule=deprecated_update_region), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'delete_region', +- check_str=base.SYSTEM_ADMIN, +- scope_types=['system'], ++ check_str=base.RULE_ADMIN_REQUIRED, ++ scope_types=['system', 'project'], + description='Delete region.', + operations=[{'path': '/v3/regions/{region_id}', + 'method': 'DELETE'}], +Index: keystone/keystone/common/policies/registered_limit.py +=================================================================== +--- keystone.orig/keystone/common/policies/registered_limit.py ++++ keystone/keystone/common/policies/registered_limit.py +@@ -35,22 +35,22 @@ registered_limit_policies = [ + 'method': 'HEAD'}]), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'create_registered_limits', +- check_str=base.SYSTEM_ADMIN, +- scope_types=['system'], ++ check_str=base.RULE_ADMIN_REQUIRED, ++ scope_types=['system', 'project'], + description='Create registered limits.', + operations=[{'path': '/v3/registered_limits', + 'method': 'POST'}]), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'update_registered_limit', +- check_str=base.SYSTEM_ADMIN, +- scope_types=['system'], ++ check_str=base.RULE_ADMIN_REQUIRED, ++ scope_types=['system', 'project'], + description='Update registered limit.', + operations=[{'path': '/v3/registered_limits/{registered_limit_id}', + 'method': 'PATCH'}]), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'delete_registered_limit', +- check_str=base.SYSTEM_ADMIN, +- scope_types=['system'], ++ check_str=base.RULE_ADMIN_REQUIRED, ++ scope_types=['system', 'project'], + description='Delete registered limit.', + operations=[{'path': '/v3/registered_limits/{registered_limit_id}', + 'method': 'DELETE'}]) +Index: keystone/keystone/common/policies/revoke_event.py +=================================================================== +--- keystone.orig/keystone/common/policies/revoke_event.py ++++ keystone/keystone/common/policies/revoke_event.py +@@ -18,11 +18,7 @@ revoke_event_policies = [ + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'list_revoke_events', + check_str=base.RULE_SERVICE_OR_ADMIN, +- # NOTE(lbragstad): This API was originally introduced so that services +- # could invalidate tokens based on revocation events. This is system +- # specific so it make sense to associate `system` as the scope type +- # required for this policy. +- scope_types=['system'], ++ scope_types=['system', 'project'], + description='List revocation events.', + operations=[{'path': '/v3/OS-REVOKE/events', + 'method': 'GET'}]) +Index: keystone/keystone/common/policies/role.py +=================================================================== +--- keystone.orig/keystone/common/policies/role.py ++++ keystone/keystone/common/policies/role.py +@@ -84,13 +84,8 @@ deprecated_delete_domain_role = policy.D + role_policies = [ + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'get_role', +- check_str=base.SYSTEM_READER, +- # FIXME(lbragstad): Roles should be considered a system-level resource. +- # The current RBAC design of OpenStack requires configuration +- # modification depending on the roles created in keystone. Once that is +- # no longer true we should consider adding `project` to the list of +- # scope_types. +- scope_types=['system'], ++ check_str=base.RULE_ADMIN_OR_SYSTEM_READER, ++ scope_types=['system', 'project'], + description='Show role details.', + operations=[{'path': '/v3/roles/{role_id}', + 'method': 'GET'}, +@@ -99,8 +94,8 @@ role_policies = [ + deprecated_rule=deprecated_get_role), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'list_roles', +- check_str=base.SYSTEM_READER, +- scope_types=['system'], ++ check_str=base.RULE_ADMIN_OR_SYSTEM_READER, ++ scope_types=['system', 'project'], + description='List roles.', + operations=[{'path': '/v3/roles', + 'method': 'GET'}, +@@ -109,37 +104,32 @@ role_policies = [ + deprecated_rule=deprecated_list_role), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'create_role', +- check_str=base.SYSTEM_ADMIN, +- scope_types=['system'], ++ check_str=base.RULE_ADMIN_REQUIRED, ++ scope_types=['system', 'project'], + description='Create role.', + operations=[{'path': '/v3/roles', + 'method': 'POST'}], + deprecated_rule=deprecated_create_role), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'update_role', +- check_str=base.SYSTEM_ADMIN, +- scope_types=['system'], ++ check_str=base.RULE_ADMIN_REQUIRED, ++ scope_types=['system', 'project'], + description='Update role.', + operations=[{'path': '/v3/roles/{role_id}', + 'method': 'PATCH'}], + deprecated_rule=deprecated_update_role), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'delete_role', +- check_str=base.SYSTEM_ADMIN, +- scope_types=['system'], ++ check_str=base.RULE_ADMIN_REQUIRED, ++ scope_types=['system', 'project'], + description='Delete role.', + operations=[{'path': '/v3/roles/{role_id}', + 'method': 'DELETE'}], + deprecated_rule=deprecated_delete_role), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'get_domain_role', +- check_str=base.SYSTEM_READER, +- # FIXME(lbragstad): Once OpenStack supports a way to make role changes +- # without having to modify policy files, scope_types for +- # domain-specific roles should include `project`. This will expose +- # these APIs to domain/project administrators, allowing them to create, +- # modify, and delete roles for their own projects and domains. +- scope_types=['system'], ++ check_str=base.RULE_ADMIN_OR_SYSTEM_READER, ++ scope_types=['system', 'project'], + description='Show domain role.', + operations=[{'path': '/v3/roles/{role_id}', + 'method': 'GET'}, +@@ -148,9 +138,9 @@ role_policies = [ + deprecated_rule=deprecated_get_domain_role), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'list_domain_roles', +- check_str=base.SYSTEM_READER, ++ check_str=base.RULE_ADMIN_OR_SYSTEM_READER, + description='List domain roles.', +- scope_types=['system'], ++ scope_types=['system', 'project'], + operations=[{'path': '/v3/roles?domain_id={domain_id}', + 'method': 'GET'}, + {'path': '/v3/roles?domain_id={domain_id}', +@@ -158,25 +148,25 @@ role_policies = [ + deprecated_rule=deprecated_list_domain_roles), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'create_domain_role', +- check_str=base.SYSTEM_ADMIN, ++ check_str=base.RULE_ADMIN_REQUIRED, + description='Create domain role.', +- scope_types=['system'], ++ scope_types=['system', 'project'], + operations=[{'path': '/v3/roles', + 'method': 'POST'}], + deprecated_rule=deprecated_create_domain_role), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'update_domain_role', +- check_str=base.SYSTEM_ADMIN, ++ check_str=base.RULE_ADMIN_REQUIRED, + description='Update domain role.', +- scope_types=['system'], ++ scope_types=['system', 'project'], + operations=[{'path': '/v3/roles/{role_id}', + 'method': 'PATCH'}], + deprecated_rule=deprecated_update_domain_role), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'delete_domain_role', +- check_str=base.SYSTEM_ADMIN, ++ check_str=base.RULE_ADMIN_REQUIRED, + description='Delete domain role.', +- scope_types=['system'], ++ scope_types=['system', 'project'], + operations=[{'path': '/v3/roles/{role_id}', + 'method': 'DELETE'}], + deprecated_rule=deprecated_delete_domain_role) +Index: keystone/keystone/common/policies/role_assignment.py +=================================================================== +--- keystone.orig/keystone/common/policies/role_assignment.py ++++ keystone/keystone/common/policies/role_assignment.py +@@ -19,6 +19,11 @@ SYSTEM_READER_OR_DOMAIN_READER = ( + '(' + base.SYSTEM_READER + ') or ' + '(role:reader and domain_id:%(target.domain_id)s)' + ) ++ADMIN_OR_SYSTEM_READER_OR_DOMAIN_READER = ( ++ '(' + base.RULE_ADMIN_REQUIRED + ') or ' + ++ SYSTEM_READER_OR_DOMAIN_READER ++) ++ + SYSTEM_READER_OR_PROJECT_DOMAIN_READER_OR_PROJECT_ADMIN = ( + '(' + base.SYSTEM_READER + ') or ' + '(role:reader and domain_id:%(target.project.domain_id)s) or ' +@@ -46,8 +51,8 @@ deprecated_list_role_assignments_for_tre + role_assignment_policies = [ + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'list_role_assignments', +- check_str=SYSTEM_READER_OR_DOMAIN_READER, +- scope_types=['system', 'domain'], ++ check_str=ADMIN_OR_SYSTEM_READER_OR_DOMAIN_READER, ++ scope_types=['system', 'domain', 'project'], + description='List role assignments.', + operations=[{'path': '/v3/role_assignments', + 'method': 'GET'}, +@@ -56,7 +61,7 @@ role_assignment_policies = [ + deprecated_rule=deprecated_list_role_assignments), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'list_role_assignments_for_tree', +- check_str=SYSTEM_READER_OR_PROJECT_DOMAIN_READER_OR_PROJECT_ADMIN, ++ check_str=ADMIN_OR_SYSTEM_READER_OR_DOMAIN_READER, + scope_types=['system', 'domain', 'project'], + description=('List all role assignments for a given tree of ' + 'hierarchical projects.'), +Index: keystone/keystone/common/policies/service.py +=================================================================== +--- keystone.orig/keystone/common/policies/service.py ++++ keystone/keystone/common/policies/service.py +@@ -54,40 +54,40 @@ deprecated_delete_service = policy.Depre + service_policies = [ + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'get_service', +- check_str=base.SYSTEM_READER, +- scope_types=['system'], ++ check_str=base.RULE_ADMIN_OR_SYSTEM_READER, ++ scope_types=['system', 'project'], + description='Show service details.', + operations=[{'path': '/v3/services/{service_id}', + 'method': 'GET'}], + deprecated_rule=deprecated_get_service), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'list_services', +- check_str=base.SYSTEM_READER, +- scope_types=['system'], ++ check_str=base.RULE_ADMIN_OR_SYSTEM_READER, ++ scope_types=['system', 'project'], + description='List services.', + operations=[{'path': '/v3/services', + 'method': 'GET'}], + deprecated_rule=deprecated_list_service), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'create_service', +- check_str=base.SYSTEM_ADMIN, +- scope_types=['system'], ++ check_str=base.RULE_ADMIN_REQUIRED, ++ scope_types=['system', 'project'], + description='Create service.', + operations=[{'path': '/v3/services', + 'method': 'POST'}], + deprecated_rule=deprecated_create_service), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'update_service', +- check_str=base.SYSTEM_ADMIN, +- scope_types=['system'], ++ check_str=base.RULE_ADMIN_REQUIRED, ++ scope_types=['system', 'project'], + description='Update service.', + operations=[{'path': '/v3/services/{service_id}', + 'method': 'PATCH'}], + deprecated_rule=deprecated_update_service), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'delete_service', +- check_str=base.SYSTEM_ADMIN, +- scope_types=['system'], ++ check_str=base.RULE_ADMIN_REQUIRED, ++ scope_types=['system', 'project'], + description='Delete service.', + operations=[{'path': '/v3/services/{service_id}', + 'method': 'DELETE'}], +Index: keystone/keystone/common/policies/service_provider.py +=================================================================== +--- keystone.orig/keystone/common/policies/service_provider.py ++++ keystone/keystone/common/policies/service_provider.py +@@ -54,14 +54,8 @@ deprecated_delete_sp = policy.Deprecated + service_provider_policies = [ + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'create_service_provider', +- check_str=base.SYSTEM_ADMIN, +- # FIXME(lbragstad): Today, keystone doesn't support federation without +- # modifying configuration files. It makes sense to require system scope +- # for these operations until keystone supports a way to add federated +- # identity and service providers strictly over the API. At that point, +- # it will make sense to include `project` in the list of `scope_types` +- # for service provider policies. +- scope_types=['system'], ++ check_str=base.RULE_ADMIN_REQUIRED, ++ scope_types=['system', 'project'], + description='Create federated service provider.', + operations=[{'path': ('/v3/OS-FEDERATION/service_providers/' + '{service_provider_id}'), +@@ -69,8 +63,8 @@ service_provider_policies = [ + deprecated_rule=deprecated_create_sp), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'list_service_providers', +- check_str=base.SYSTEM_READER, +- scope_types=['system'], ++ check_str=base.RULE_ADMIN_OR_SYSTEM_READER, ++ scope_types=['system', 'project'], + description='List federated service providers.', + operations=[ + { +@@ -86,8 +80,8 @@ service_provider_policies = [ + ), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'get_service_provider', +- check_str=base.SYSTEM_READER, +- scope_types=['system'], ++ check_str=base.RULE_ADMIN_OR_SYSTEM_READER, ++ scope_types=['system', 'project'], + description='Get federated service provider.', + operations=[ + { +@@ -105,8 +99,8 @@ service_provider_policies = [ + ), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'update_service_provider', +- check_str=base.SYSTEM_ADMIN, +- scope_types=['system'], ++ check_str=base.RULE_ADMIN_REQUIRED, ++ scope_types=['system', 'project'], + description='Update federated service provider.', + operations=[{'path': ('/v3/OS-FEDERATION/service_providers/' + '{service_provider_id}'), +@@ -114,8 +108,8 @@ service_provider_policies = [ + deprecated_rule=deprecated_update_sp), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'delete_service_provider', +- check_str=base.SYSTEM_ADMIN, +- scope_types=['system'], ++ check_str=base.RULE_ADMIN_REQUIRED, ++ scope_types=['system', 'project'], + description='Delete federated service provider.', + operations=[{'path': ('/v3/OS-FEDERATION/service_providers/' + '{service_provider_id}'), +Index: keystone/keystone/common/policies/trust.py +=================================================================== +--- keystone.orig/keystone/common/policies/trust.py ++++ keystone/keystone/common/policies/trust.py +@@ -24,6 +24,17 @@ SYSTEM_READER_OR_TRUSTOR = base.SYSTEM_R + SYSTEM_READER_OR_TRUSTEE = base.SYSTEM_READER + ' or ' + RULE_TRUSTEE + SYSTEM_ADMIN_OR_TRUSTOR = base.SYSTEM_ADMIN + ' or ' + RULE_TRUSTOR + ++ADMIN_OR_TRUSTOR = base.RULE_ADMIN_REQUIRED + ' or ' + RULE_TRUSTOR ++ADMIN_OR_SYSTEM_READER_OR_TRUSTOR = ( ++ '(' + base.RULE_ADMIN_REQUIRED + ') or ' + ++ '(' + SYSTEM_READER_OR_TRUSTOR + ')') ++ADMIN_OR_SYSTEM_READER_OR_TRUSTEE = ( ++ '(' + base.RULE_ADMIN_REQUIRED + ') or ' + ++ '(' + SYSTEM_READER_OR_TRUSTEE + ')') ++ADMIN_OR_SYSTEM_READER_OR_TRUSTOR_OR_TRUSTEE = ( ++ '(' + base.RULE_ADMIN_REQUIRED + ') or ' + ++ '(' + SYSTEM_READER_OR_TRUSTOR_OR_TRUSTEE + ')') ++ + DEPRECATED_REASON = ( + "The trust API is now aware of system scope and default roles." + ) +@@ -72,8 +83,8 @@ trust_policies = [ + 'method': 'POST'}]), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'list_trusts', +- check_str=base.SYSTEM_READER, +- scope_types=['system'], ++ check_str=base.RULE_ADMIN_OR_SYSTEM_READER, ++ scope_types=['system', 'project'], + description='List trusts.', + operations=[{'path': '/v3/OS-TRUST/trusts', + 'method': 'GET'}, +@@ -82,7 +93,7 @@ trust_policies = [ + deprecated_rule=deprecated_list_trusts), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'list_trusts_for_trustor', +- check_str=SYSTEM_READER_OR_TRUSTOR, ++ check_str=ADMIN_OR_SYSTEM_READER_OR_TRUSTOR, + scope_types=['system', 'project'], + description='List trusts for trustor.', + operations=[{'path': '/v3/OS-TRUST/trusts?' +@@ -93,7 +104,7 @@ trust_policies = [ + 'method': 'HEAD'}]), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'list_trusts_for_trustee', +- check_str=SYSTEM_READER_OR_TRUSTEE, ++ check_str=ADMIN_OR_SYSTEM_READER_OR_TRUSTEE, + scope_types=['system', 'project'], + description='List trusts for trustee.', + operations=[{'path': '/v3/OS-TRUST/trusts?' +@@ -104,7 +115,7 @@ trust_policies = [ + 'method': 'HEAD'}]), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'list_roles_for_trust', +- check_str=SYSTEM_READER_OR_TRUSTOR_OR_TRUSTEE, ++ check_str=ADMIN_OR_SYSTEM_READER_OR_TRUSTOR_OR_TRUSTEE, + scope_types=['system', 'project'], + description='List roles delegated by a trust.', + operations=[{'path': '/v3/OS-TRUST/trusts/{trust_id}/roles', +@@ -114,7 +125,7 @@ trust_policies = [ + deprecated_rule=deprecated_list_roles_for_trust), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'get_role_for_trust', +- check_str=SYSTEM_READER_OR_TRUSTOR_OR_TRUSTEE, ++ check_str=ADMIN_OR_SYSTEM_READER_OR_TRUSTOR_OR_TRUSTEE, + scope_types=['system', 'project'], + description='Check if trust delegates a particular role.', + operations=[{'path': '/v3/OS-TRUST/trusts/{trust_id}/roles/{role_id}', +@@ -124,7 +135,7 @@ trust_policies = [ + deprecated_rule=deprecated_get_role_for_trust), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'delete_trust', +- check_str=SYSTEM_ADMIN_OR_TRUSTOR, ++ check_str=ADMIN_OR_TRUSTOR, + scope_types=['system', 'project'], + description='Revoke trust.', + operations=[{'path': '/v3/OS-TRUST/trusts/{trust_id}', +@@ -132,7 +143,7 @@ trust_policies = [ + deprecated_rule=deprecated_delete_trust), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'get_trust', +- check_str=SYSTEM_READER_OR_TRUSTOR_OR_TRUSTEE, ++ check_str=ADMIN_OR_SYSTEM_READER_OR_TRUSTOR_OR_TRUSTEE, + scope_types=['system', 'project'], + description='Get trust.', + operations=[{'path': '/v3/OS-TRUST/trusts/{trust_id}', +Index: keystone/keystone/common/policies/user.py +=================================================================== +--- keystone.orig/keystone/common/policies/user.py ++++ keystone/keystone/common/policies/user.py +@@ -20,14 +20,18 @@ SYSTEM_READER_OR_DOMAIN_READER_OR_USER = + '(role:reader and token.domain.id:%(target.user.domain_id)s) or ' + 'user_id:%(target.user.id)s' + ) ++ADMIN_OR_SYSTEM_READER_OR_DOMAIN_READER_OR_USER = ( ++ '(' + base.RULE_ADMIN_REQUIRED + ') or ' + ++ SYSTEM_READER_OR_DOMAIN_READER_OR_USER ++ ++) + + SYSTEM_READER_OR_DOMAIN_READER = ( + '(' + base.SYSTEM_READER + ') or (' + base.DOMAIN_READER + ')' + ) +- +-SYSTEM_ADMIN_OR_DOMAIN_ADMIN = ( +- '(role:admin and system_scope:all) or ' +- '(role:admin and token.domain.id:%(target.user.domain_id)s)' ++ADMIN_OR_SYSTEM_READER_OR_DOMAIN_READER = ( ++ '(' + base.RULE_ADMIN_REQUIRED + ') or ' + ++ SYSTEM_READER_OR_DOMAIN_READER + ) + + DEPRECATED_REASON = ( +@@ -68,7 +72,7 @@ deprecated_delete_user = policy.Deprecat + user_policies = [ + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'get_user', +- check_str=SYSTEM_READER_OR_DOMAIN_READER_OR_USER, ++ check_str=ADMIN_OR_SYSTEM_READER_OR_DOMAIN_READER_OR_USER, + scope_types=['system', 'domain', 'project'], + description='Show user details.', + operations=[{'path': '/v3/users/{user_id}', +@@ -78,8 +82,8 @@ user_policies = [ + deprecated_rule=deprecated_get_user), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'list_users', +- check_str=SYSTEM_READER_OR_DOMAIN_READER, +- scope_types=['system', 'domain'], ++ check_str=ADMIN_OR_SYSTEM_READER_OR_DOMAIN_READER, ++ scope_types=['system', 'domain', 'project'], + description='List users.', + operations=[{'path': '/v3/users', + 'method': 'GET'}, +@@ -112,24 +116,24 @@ user_policies = [ + 'method': 'GET'}]), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'create_user', +- check_str=SYSTEM_ADMIN_OR_DOMAIN_ADMIN, +- scope_types=['system', 'domain'], ++ check_str=base.RULE_ADMIN_REQUIRED, ++ scope_types=['system', 'domain', 'project'], + description='Create a user.', + operations=[{'path': '/v3/users', + 'method': 'POST'}], + deprecated_rule=deprecated_create_user), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'update_user', +- check_str=SYSTEM_ADMIN_OR_DOMAIN_ADMIN, +- scope_types=['system', 'domain'], ++ check_str=base.RULE_ADMIN_REQUIRED, ++ scope_types=['system', 'domain', 'project'], + description='Update a user, including administrative password resets.', + operations=[{'path': '/v3/users/{user_id}', + 'method': 'PATCH'}], + deprecated_rule=deprecated_update_user), + policy.DocumentedRuleDefault( + name=base.IDENTITY % 'delete_user', +- check_str=SYSTEM_ADMIN_OR_DOMAIN_ADMIN, +- scope_types=['system', 'domain'], ++ check_str=base.RULE_ADMIN_REQUIRED, ++ scope_types=['system', 'domain', 'project'], + description='Delete a user.', + operations=[{'path': '/v3/users/{user_id}', + 'method': 'DELETE'}], +Index: keystone/keystone/tests/unit/test_v3_trust.py +=================================================================== +--- keystone.orig/keystone/tests/unit/test_v3_trust.py ++++ keystone/keystone/tests/unit/test_v3_trust.py +@@ -178,7 +178,7 @@ class TestTrustOperations(test_v3.Restfu + self.assertEqual(3, len(trusts)) + self.assertValidTrustListResponse(r) + +- # list all trusts as the trustor as the trustee. ++ # list all trusts for trustee as the trustor + list_as_trustor_url = ( + '/OS-TRUST/trusts?trustee_user_id=%s' % self.user_id + ) +@@ -188,17 +188,20 @@ class TestTrustOperations(test_v3.Restfu + self.assertEqual(0, len(trusts)) + + # list all trusts as the trustee is forbidden +- list_all_as_trustee_url = ( +- '/OS-TRUST/trusts?trustee_user_id=%s' % self.trustee_user_id +- ) +- r = self.get( +- list_all_as_trustee_url, +- expected_status=http.client.FORBIDDEN +- ) +- self.head( +- list_all_as_trustee_url, +- expected_status=http.client.FORBIDDEN +- ) ++ # FIXME(dmendiza): This test is not written to do what the above ++ # comment says it should be doing. The main issue is that it's ++ # still using the trustor credentiasl to make the request. ++ # list_all_as_trustee_url = ( ++ # '/OS-TRUST/trusts?trustee_user_id=%s' % self.trustee_user_id ++ # ) ++ # r = self.get( ++ # list_all_as_trustee_url, ++ # expected_status=http.client.FORBIDDEN ++ # ) ++ # self.head( ++ # list_all_as_trustee_url, ++ # expected_status=http.client.FORBIDDEN ++ # ) + + def test_create_trust_with_expiration_in_the_past_fails(self): + ref = unit.new_trust_ref( diff -Nru keystone-22.0.0/debian/patches/Dont_enforce_when_HTTP_GET_on_s3tokens_and_ec2tokens.patch keystone-22.0.2/debian/patches/Dont_enforce_when_HTTP_GET_on_s3tokens_and_ec2tokens.patch --- keystone-22.0.0/debian/patches/Dont_enforce_when_HTTP_GET_on_s3tokens_and_ec2tokens.patch 1970-01-01 00:00:00.000000000 +0000 +++ keystone-22.0.2/debian/patches/Dont_enforce_when_HTTP_GET_on_s3tokens_and_ec2tokens.patch 2025-11-11 08:19:08.000000000 +0000 @@ -0,0 +1,73 @@ +Description: Dont enforce when HTTP GET on s3tokens and ec2tokens + When calling the s3tokens or ec2tokens API with a + HTTP GET we should get a 405 Method Not Allowed but + we get a 500 Internal Server Error because we enforce + that method. +Author: Tobias Urdin +Date: Mon, 12 Feb 2024 08:36:53 +0000 +Bug: https://launchpad.net/bugs/2052916 +Change-Id: I5f60d10dc25551175cc73ca8f3f28b0b95ec9f99 +Signed-off-by: Tobias Urdin +Origin: upstream, https://review.opendev.org/c/openstack/keystone/+/908760 +Last-Update: 2025-10-30 + +diff --git a/keystone/api/_shared/EC2_S3_Resource.py b/keystone/api/_shared/EC2_S3_Resource.py +index ff94286..7b2fc21 100644 +--- a/keystone/api/_shared/EC2_S3_Resource.py ++++ b/keystone/api/_shared/EC2_S3_Resource.py +@@ -31,6 +31,7 @@ + + + class ResourceBase(ks_flask.ResourceBase): ++ @ks_flask.unenforced_api + def get(self): + # SPECIAL CASE: GET is not allowed, raise METHOD_NOT_ALLOWED + raise exceptions.MethodNotAllowed(valid_methods=['POST']) +diff --git a/keystone/tests/unit/test_contrib_ec2_core.py b/keystone/tests/unit/test_contrib_ec2_core.py +index 4b514f8..8da5bba 100644 +--- a/keystone/tests/unit/test_contrib_ec2_core.py ++++ b/keystone/tests/unit/test_contrib_ec2_core.py +@@ -37,6 +37,13 @@ + PROVIDERS.credential_api.create_credential( + self.credential['id'], self.credential) + ++ def test_http_get_method_not_allowed(self): ++ resp = self.get('/ec2tokens', ++ expected_status=http.client.METHOD_NOT_ALLOWED, ++ convert=False) ++ self.assertEqual(http.client.METHOD_NOT_ALLOWED, ++ resp.status_code) ++ + def test_valid_authentication_response_with_proper_secret(self): + signer = ec2_utils.Ec2Signer(self.cred_blob['secret']) + timestamp = utils.isotime(timeutils.utcnow()) +diff --git a/keystone/tests/unit/test_contrib_s3_core.py b/keystone/tests/unit/test_contrib_s3_core.py +index a9c8acd..b109e8c 100644 +--- a/keystone/tests/unit/test_contrib_s3_core.py ++++ b/keystone/tests/unit/test_contrib_s3_core.py +@@ -39,6 +39,13 @@ + PROVIDERS.credential_api.create_credential( + self.credential['id'], self.credential) + ++ def test_http_get_method_not_allowed(self): ++ resp = self.get('/s3tokens', ++ expected_status=http.client.METHOD_NOT_ALLOWED, ++ convert=False) ++ self.assertEqual(http.client.METHOD_NOT_ALLOWED, ++ resp.status_code) ++ + def test_good_response(self): + sts = 'string to sign' # opaque string from swift3 + sig = hmac.new(self.cred_blob['secret'].encode('ascii'), +diff --git a/releasenotes/notes/dont-enforce-get-s3tokens-ec2tokens-62b90b199e8075d8.yaml b/releasenotes/notes/dont-enforce-get-s3tokens-ec2tokens-62b90b199e8075d8.yaml +new file mode 100644 +index 0000000..fcdd030 +--- /dev/null ++++ b/releasenotes/notes/dont-enforce-get-s3tokens-ec2tokens-62b90b199e8075d8.yaml +@@ -0,0 +1,6 @@ ++--- ++fixes: ++ - | ++ [`bug 2052916 `_] ++ Fixed a bug where a HTTP GET request against ``/v3/s3tokens`` or ++ ``/v3/ec2tokens`` would return HTTP 500 instead of HTTP 405. diff -Nru keystone-22.0.0/debian/patches/Fix_policies_for_groups.patch keystone-22.0.2/debian/patches/Fix_policies_for_groups.patch --- keystone-22.0.0/debian/patches/Fix_policies_for_groups.patch 1970-01-01 00:00:00.000000000 +0000 +++ keystone-22.0.2/debian/patches/Fix_policies_for_groups.patch 2025-11-11 08:19:08.000000000 +0000 @@ -0,0 +1,30 @@ +Author: Douglas Mendizábal +Date: Thu, 25 Jan 2024 15:37:50 -0500 +Description: Fix policies for groups + This patch fixes a couple of broken policies in the groups resource. +Change-Id: Ia47ecc71c04bcb50c2e0d677a99b3754ffbc1c04 +Origin: upstream, https://review.opendev.org/c/openstack/keystone/+/906892 +Last-Update: 2025-10-30 + +diff --git a/keystone/common/policies/group.py b/keystone/common/policies/group.py +index 024ee65..8c8293c 100644 +--- a/keystone/common/policies/group.py ++++ b/keystone/common/policies/group.py +@@ -21,7 +21,7 @@ + 'user_id:%(user_id)s' + ) + ADMIN_OR_SYSTEM_READER_OR_DOMAIN_READER_FOR_TARGET_OR_OWNER = ( +- '(' + base.RULE_ADMIN_REQUIRED + ') or (' + ++ '(' + base.RULE_ADMIN_REQUIRED + ') or ' + + SYSTEM_READER_OR_DOMAIN_READER_FOR_TARGET_USER_OR_OWNER + ) + +@@ -32,7 +32,7 @@ + 'domain_id:%(target.user.domain_id)s)' + ) + ADMIN_OR_SYSTEM_READER_OR_DOMAIN_READER_FOR_TARGET_GROUP = ( +- '(' + base.RULE_ADMIN_REQUIRED + ') or (' + ++ '(' + base.RULE_ADMIN_REQUIRED + ') or ' + + SYSTEM_READER_OR_DOMAIN_READER_FOR_TARGET_GROUP_USER + ) + diff -Nru keystone-22.0.0/debian/patches/keystone-bug-2119646-stable-2024.1.patch keystone-22.0.2/debian/patches/keystone-bug-2119646-stable-2024.1.patch --- keystone-22.0.0/debian/patches/keystone-bug-2119646-stable-2024.1.patch 1970-01-01 00:00:00.000000000 +0000 +++ keystone-22.0.2/debian/patches/keystone-bug-2119646-stable-2024.1.patch 2025-11-11 08:19:08.000000000 +0000 @@ -0,0 +1,383 @@ +From 8c246cac15b2d770f26eeafee51e09f66cb0ad79 Mon Sep 17 00:00:00 2001 +From: Grzegorz Grasza +Date: Fri, 19 Sep 2025 14:02:18 +0200 +Subject: [PATCH] Add service user authentication to ec2 and s3 endpoints + +Add a policy to enforce authentication with a user in the service +group. This maintains AWS compatibility with the added security +layer. + +Closes-Bug: 2119646 +Change-Id: Ic84b84247e05f29874e2c5636a033aaedd4de83c +(cherry picked from commit 69d299eab04a1e1bab25eb89e0fdf7f0106b8ee5) +Signed-off-by: Jeremy Stanley +--- + keystone/api/ec2tokens.py | 8 ++- + keystone/api/s3tokens.py | 7 ++- + keystone/common/policies/__init__.py | 4 ++ + keystone/common/policies/ec2tokens.py | 37 ++++++++++++++ + keystone/common/policies/s3tokens.py | 35 ++++++++++++++ + keystone/tests/unit/test_contrib_ec2_core.py | 30 ++++++++++-- + keystone/tests/unit/test_contrib_s3_core.py | 51 ++++++++++++++------ + keystone/tests/unit/test_v3_credential.py | 22 ++++++--- + 8 files changed, 166 insertions(+), 28 deletions(-) + create mode 100644 keystone/common/policies/ec2tokens.py + create mode 100644 keystone/common/policies/s3tokens.py + +Index: keystone/keystone/api/ec2tokens.py +=================================================================== +--- keystone.orig/keystone/api/ec2tokens.py ++++ keystone/keystone/api/ec2tokens.py +@@ -21,6 +21,7 @@ from oslo_serialization import jsonutils + + from keystone.api._shared import EC2_S3_Resource + from keystone.api._shared import json_home_relations ++from keystone.common import rbac_enforcer + from keystone.common import render_token + from keystone.common import utils + from keystone import exception +@@ -31,6 +32,9 @@ from keystone.server import flask as ks_ + CRED_TYPE_EC2 = 'ec2' + + ++ENFORCER = rbac_enforcer.RBACEnforcer ++ ++ + class EC2TokensResource(EC2_S3_Resource.ResourceBase): + @staticmethod + def _check_signature(creds_ref, credentials): +@@ -60,12 +64,14 @@ class EC2TokensResource(EC2_S3_Resource. + raise exception.Unauthorized( + _('EC2 signature not supplied.')) + +- @ks_flask.unenforced_api + def post(self): + """Authenticate ec2 token. + + POST /v3/ec2tokens + """ ++ # Enforce RBAC in the same way as S3 tokens ++ ENFORCER.enforce_call(action='identity:ec2tokens_validate') ++ + token = self.handle_authenticate() + token_reference = render_token.render_token_response_from_model(token) + resp_body = jsonutils.dumps(token_reference) +Index: keystone/keystone/api/s3tokens.py +=================================================================== +--- keystone.orig/keystone/api/s3tokens.py ++++ keystone/keystone/api/s3tokens.py +@@ -22,12 +22,15 @@ from oslo_serialization import jsonutils + + from keystone.api._shared import EC2_S3_Resource + from keystone.api._shared import json_home_relations ++from keystone.common import rbac_enforcer + from keystone.common import render_token + from keystone.common import utils + from keystone import exception + from keystone.i18n import _ + from keystone.server import flask as ks_flask + ++ENFORCER = rbac_enforcer.RBACEnforcer ++ + + def _calculate_signature_v1(string_to_sign, secret_key): + """Calculate a v1 signature. +@@ -90,12 +93,14 @@ class S3Resource(EC2_S3_Resource.Resourc + raise exception.Unauthorized( + message=_('Credential signature mismatch')) + +- @ks_flask.unenforced_api + def post(self): + """Authenticate s3token. + + POST /v3/s3tokens + """ ++ # Use standard Keystone policy enforcement for s3tokens access ++ ENFORCER.enforce_call(action='identity:s3tokens_validate') ++ + token = self.handle_authenticate() + token_reference = render_token.render_token_response_from_model(token) + resp_body = jsonutils.dumps(token_reference) +Index: keystone/keystone/common/policies/__init__.py +=================================================================== +--- keystone.orig/keystone/common/policies/__init__.py ++++ keystone/keystone/common/policies/__init__.py +@@ -22,6 +22,7 @@ from keystone.common.policies import cre + from keystone.common.policies import domain + from keystone.common.policies import domain_config + from keystone.common.policies import ec2_credential ++from keystone.common.policies import ec2tokens + from keystone.common.policies import endpoint + from keystone.common.policies import endpoint_group + from keystone.common.policies import grant +@@ -40,6 +41,7 @@ from keystone.common.policies import reg + from keystone.common.policies import revoke_event + from keystone.common.policies import role + from keystone.common.policies import role_assignment ++from keystone.common.policies import s3tokens + from keystone.common.policies import service + from keystone.common.policies import service_provider + from keystone.common.policies import token +@@ -78,6 +80,8 @@ def list_rules(): + revoke_event.list_rules(), + role.list_rules(), + role_assignment.list_rules(), ++ s3tokens.list_rules(), ++ ec2tokens.list_rules(), + service.list_rules(), + service_provider.list_rules(), + token_revocation.list_rules(), +Index: keystone/keystone/common/policies/ec2tokens.py +=================================================================== +--- /dev/null ++++ keystone/keystone/common/policies/ec2tokens.py +@@ -0,0 +1,37 @@ ++# 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_policy import policy ++ ++from keystone.common.policies import base ++ ++# Align EC2 tokens API with S3 tokens: require admin or service users ++ADMIN_OR_SERVICE = 'rule:service_or_admin' ++ ++ ++ec2tokens_policies = [ ++ policy.DocumentedRuleDefault( ++ name=base.IDENTITY % 'ec2tokens_validate', ++ check_str=ADMIN_OR_SERVICE, ++ scope_types=['system', 'domain', 'project'], ++ description='Validate EC2 credentials and create a Keystone token. ' ++ 'Restricted to service users or administrators.', ++ operations=[{'path': '/v3/ec2tokens', 'method': 'POST'}], ++ ) ++] ++ ++ ++def list_rules(): ++ return ec2tokens_policies ++ ++ ++ +Index: keystone/keystone/common/policies/s3tokens.py +=================================================================== +--- /dev/null ++++ keystone/keystone/common/policies/s3tokens.py +@@ -0,0 +1,35 @@ ++# 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_policy import policy ++ ++from keystone.common.policies import base ++ ++# S3 tokens API requires service authentication to prevent presigned URL exploitation ++# This policy restricts access to service users or administrators only ++ADMIN_OR_SERVICE = 'rule:service_or_admin' ++ ++s3tokens_policies = [ ++ policy.DocumentedRuleDefault( ++ name=base.IDENTITY % 's3tokens_validate', ++ check_str=ADMIN_OR_SERVICE, ++ scope_types=['system', 'domain', 'project'], ++ description='Validate S3 credentials and create a Keystone token. ' ++ 'Restricted to service users or administrators to prevent ' ++ 'exploitation via presigned URLs.', ++ operations=[{'path': '/v3/s3tokens', 'method': 'POST'}], ++ ) ++] ++ ++ ++def list_rules(): ++ return s3tokens_policies +Index: keystone/keystone/tests/unit/test_contrib_ec2_core.py +=================================================================== +--- keystone.orig/keystone/tests/unit/test_contrib_ec2_core.py ++++ keystone/keystone/tests/unit/test_contrib_ec2_core.py +@@ -44,7 +44,7 @@ class EC2ContribCoreV3(test_v3.RestfulTe + self.assertEqual(http.client.METHOD_NOT_ALLOWED, + resp.status_code) + +- def test_valid_authentication_response_with_proper_secret(self): ++ def _test_valid_authentication_response_with_proper_secret(self, **kwargs): + signer = ec2_utils.Ec2Signer(self.cred_blob['secret']) + timestamp = utils.isotime(timeutils.utcnow()) + credentials = { +@@ -56,15 +56,39 @@ class EC2ContribCoreV3(test_v3.RestfulTe + 'params': { + 'SignatureVersion': '2', + 'Action': 'Test', +- 'Timestamp': timestamp ++ 'Timestamp': timestamp, + }, + } + credentials['signature'] = signer.generate(credentials) ++ # Authenticate as system admin by default unless overridden via kwargs ++ token = None ++ if 'noauth' in kwargs and kwargs['noauth']: ++ token = None ++ else: ++ PROVIDERS.assignment_api.create_system_grant_for_user( ++ self.user_id, self.role_id ++ ) ++ token = self.get_system_scoped_token() ++ ++ expected_status = kwargs.get('expected_status', http.client.OK) + resp = self.post( + '/ec2tokens', + body={'credentials': credentials}, +- expected_status=http.client.OK) +- self.assertValidProjectScopedTokenResponse(resp, self.user) ++ expected_status=expected_status, ++ token=token, ++ noauth=kwargs.get('noauth'), ++ ) ++ if expected_status == http.client.OK: ++ self.assertValidProjectScopedTokenResponse(resp, self.user) ++ ++ def test_valid_authentication_response_with_proper_secret(self): ++ self._test_valid_authentication_response_with_proper_secret() ++ ++ def test_valid_authentication_response_with_proper_secret_noauth(self): ++ # ec2 endpoint now enforces RBAC; unauthenticated should be denied ++ self._test_valid_authentication_response_with_proper_secret( ++ expected_status=http.client.UNAUTHORIZED, noauth=True ++ ) + + def test_valid_authentication_response_with_signature_v4(self): + signer = ec2_utils.Ec2Signer(self.cred_blob['secret']) +Index: keystone/keystone/tests/unit/test_contrib_s3_core.py +=================================================================== +--- keystone.orig/keystone/tests/unit/test_contrib_s3_core.py ++++ keystone/keystone/tests/unit/test_contrib_s3_core.py +@@ -40,26 +40,45 @@ class S3ContribCore(test_v3.RestfulTestC + self.credential['id'], self.credential) + + def test_http_get_method_not_allowed(self): +- resp = self.get('/s3tokens', +- expected_status=http.client.METHOD_NOT_ALLOWED, +- convert=False) +- self.assertEqual(http.client.METHOD_NOT_ALLOWED, +- resp.status_code) ++ resp = self.get( ++ '/s3tokens', ++ expected_status=http.client.METHOD_NOT_ALLOWED, ++ convert=False, ++ ) ++ self.assertEqual(http.client.METHOD_NOT_ALLOWED, resp.status_code) + +- def test_good_response(self): ++ def _test_good_response(self, expected_status=http.client.OK, **kwargs): + sts = 'string to sign' # opaque string from swift3 +- sig = hmac.new(self.cred_blob['secret'].encode('ascii'), +- sts.encode('ascii'), hashlib.sha1).digest() ++ sig = hmac.new( ++ self.cred_blob['secret'].encode('ascii'), ++ sts.encode('ascii'), ++ hashlib.sha1, ++ ).digest() + resp = self.post( + '/s3tokens', +- body={'credentials': { +- 'access': self.cred_blob['access'], +- 'signature': base64.b64encode(sig).strip(), +- 'token': base64.b64encode(sts.encode('ascii')).strip(), +- }}, +- expected_status=http.client.OK) +- self.assertValidProjectScopedTokenResponse(resp, self.user, +- forbid_token_id=True) ++ body={ ++ 'credentials': { ++ 'access': self.cred_blob['access'], ++ 'signature': base64.b64encode(sig).strip(), ++ 'token': base64.b64encode(sts.encode('ascii')).strip(), ++ } ++ }, ++ expected_status=expected_status, ++ **kwargs, ++ ) ++ if expected_status == http.client.OK: ++ self.assertValidProjectScopedTokenResponse( ++ resp, self.user, forbid_token_id=True ++ ) ++ else: ++ self.assertValidErrorResponse(resp) ++ ++ def test_good_response(self): ++ self._test_good_response() ++ ++ def test_good_response_noauth(self): ++ # s3tokens now requires service/admin auth; unauthenticated should be denied ++ self._test_good_response(http.client.UNAUTHORIZED, noauth=True) + + def test_bad_request(self): + self.post( +Index: keystone/keystone/tests/unit/test_v3_credential.py +=================================================================== +--- keystone.orig/keystone/tests/unit/test_v3_credential.py ++++ keystone/keystone/tests/unit/test_v3_credential.py +@@ -79,16 +79,24 @@ class CredentialBaseTestCase(test_v3.Res + + # Now make a request to validate the signed dummy request via the + # ec2tokens API. This proves the v3 ec2 credentials actually work. +- sig_ref = {'access': access, +- 'signature': signature, +- 'host': 'foo', +- 'verb': 'GET', +- 'path': '/bar', +- 'params': params} ++ sig_ref = { ++ 'access': access, ++ 'signature': signature, ++ 'host': 'foo', ++ 'verb': 'GET', ++ 'path': '/bar', ++ 'params': params, ++ } ++ PROVIDERS.assignment_api.create_system_grant_for_user( ++ self.user_id, self.role_id ++ ) ++ token = self.get_system_scoped_token() + r = self.post( + '/ec2tokens', + body={'ec2Credentials': sig_ref}, +- expected_status=http.client.OK) ++ expected_status=http.client.OK, ++ token=token, ++ ) + self.assertValidTokenResponse(r) + return r.result['token'] + +Index: keystone/doc/source/getting-started/policy_mapping.rst +=================================================================== +--- keystone.orig/doc/source/getting-started/policy_mapping.rst ++++ keystone/doc/source/getting-started/policy_mapping.rst +@@ -246,6 +246,9 @@ identity:get_access_rule + identity:list_access_rules GET /v3/users/{user_id}/access_rules + identity:delete_access_rule DELETE /v3/users/{user_id}/access_rules/{access_rule_id} + ++identity:ec2tokens_validate POST /v3/ec2tokens ++identity:s3tokens_validate POST /v3/s3tokens ++ + ========================================================= === + + .. _grant_resources: diff -Nru keystone-22.0.0/debian/patches/series keystone-22.0.2/debian/patches/series --- keystone-22.0.0/debian/patches/series 2023-04-14 08:07:08.000000000 +0000 +++ keystone-22.0.2/debian/patches/series 2025-11-11 08:19:08.000000000 +0000 @@ -1,3 +1,8 @@ fixes-keystone-default-catalog.patch #fixes-default-connection.patch install-missing-files.patch +Consistent_and_Secure_RBAC_Phase_1.patch +Fix_policies_for_groups.patch +Allow_admin_to_access_tokens_and_credentials.patch +Dont_enforce_when_HTTP_GET_on_s3tokens_and_ec2tokens.patch +keystone-bug-2119646-stable-2024.1.patch diff -Nru keystone-22.0.0/debian/rules keystone-22.0.2/debian/rules --- keystone-22.0.0/debian/rules 2023-04-14 08:07:08.000000000 +0000 +++ keystone-22.0.2/debian/rules 2025-11-11 08:19:08.000000000 +0000 @@ -56,7 +56,7 @@ ifeq (,$(findstring nocheck, $(DEB_BUILD_OPTIONS))) @echo "===> Running tests" mkdir -p $(CURDIR)/keystone/tests/tmp - pkgos-dh_auto_test --no-py2 'keystone\.tests\.unit.(?!(.*test_v3_federation\.SAMLGenerationTests\.test_sign_assertion_exc.*|.*test_sql_upgrade.FullMigration.*|.*test_hacking_checks\.TestCheckForMutableDefaultArgs\.test.*|.*common\.test_notifications\.NotificationsTestCase.*))' + pkgos-dh_auto_test --no-py2 'keystone\.tests\.unit\.(?!(.*test_v3_federation\.SAMLGenerationTests\.test_sign_assertion_exc.*|.*test_sql_upgrade.FullMigration.*|.*test_hacking_checks\.TestCheckForMutableDefaultArgs\.test.*|.*common\.test_notifications\.NotificationsTestCase.*|test_v3_federation\.SAMLGenerationTests\.test_sign_assertion_logs_message_if_xmlsec1_is_not_installed))' endif rm -rf $(CURDIR)/debian/tmp/usr/etc diff -Nru keystone-22.0.0/doc/source/admin/integrate-with-ldap.inc keystone-22.0.2/doc/source/admin/integrate-with-ldap.inc --- keystone-22.0.0/doc/source/admin/integrate-with-ldap.inc 2022-09-08 03:35:35.000000000 +0000 +++ keystone-22.0.2/doc/source/admin/integrate-with-ldap.inc 2023-08-28 07:26:05.000000000 +0000 @@ -68,14 +68,32 @@ suffix = dc=example,dc=org -Multiple LDAP servers can be supplied to ``url`` to provide high-availability -support for a single LDAP backend. To specify multiple LDAP servers, simply -change the ``url`` option in the ``[ldap]`` section to be a list, separated by -commas: +Although it's not recommended (see note below), multiple LDAP servers can be +supplied to ``url`` to provide high-availability support for a single LDAP +backend. By default, these will be tried in order of apperance, but an +additional option, ``randomize_urls`` can be set to true, to randomize the +list in each process (when it starts). To specify multiple LDAP servers, +simply change the ``url`` option in the ``[ldap]`` section to be a list, +separated by commas: .. code-block:: ini url = "ldap://localhost,ldap://backup.localhost" + randomize_urls = true + +.. NOTE:: + + Failover mechanisms in the LDAP backend can cause delays when switching + over to the next working LDAP server. Randomizing the order in which the + servers are tried only makes the failure behavior not dependent on which + of the ordered servers fail. Individual processes can still be delayed or + time out, so this doesn't fix the issue at hand, but only makes the + failure mode more gradual. This behavior cannot be easily fixed inside the + service, because keystone would have to monitor the status of each LDAP + server, which is in fact a task for a load balancer. Because of this, it + is recommended to use a load balancer in front of the LDAP servers, + which can monitor the state of the cluster and instantly redirect + connections to the working LDAP server. **Additional LDAP integration settings** diff -Nru keystone-22.0.0/keystone/common/password_hashing.py keystone-22.0.2/keystone/common/password_hashing.py --- keystone-22.0.0/keystone/common/password_hashing.py 2022-09-08 03:35:35.000000000 +0000 +++ keystone-22.0.2/keystone/common/password_hashing.py 2023-08-28 07:26:05.000000000 +0000 @@ -57,19 +57,38 @@ def verify_length_and_trunc_password(password): - """Verify and truncate the provided password to the max_password_length.""" - max_length = CONF.identity.max_password_length + """Verify and truncate the provided password to the max_password_length. + + We also need to check that the configured password hashing algorithm does + not silently truncate the password. For example, passlib.hash.bcrypt does + this: + https://passlib.readthedocs.io/en/stable/lib/passlib.hash.bcrypt.html#security-issues + + """ + # When using bcrypt, we limit the password length to 54 to ensure all + # bytes are fully mixed. See: + # https://passlib.readthedocs.io/en/stable/lib/passlib.hash.bcrypt.html#security-issues + BCRYPT_MAX_LENGTH = 72 + if (CONF.identity.password_hash_algorithm == 'bcrypt' and # nosec: B105 + CONF.identity.max_password_length > BCRYPT_MAX_LENGTH): + msg = "Truncating password to algorithm specific maximum length %d characters." + LOG.warning(msg, BCRYPT_MAX_LENGTH) + max_length = BCRYPT_MAX_LENGTH + else: + max_length = CONF.identity.max_password_length + try: - if len(password) > max_length: + password_utf8 = password.encode('utf-8') + if len(password_utf8) > max_length: if CONF.strict_password_check: raise exception.PasswordVerificationError(size=max_length) else: msg = "Truncating user password to %d characters." LOG.warning(msg, max_length) - return password[:max_length] + return password_utf8[:max_length] else: - return password - except TypeError: + return password_utf8 + except AttributeError: raise exception.ValidationError(attribute='string', target='password') @@ -82,7 +101,7 @@ """ if password is None or hashed is None: return False - password_utf8 = verify_length_and_trunc_password(password).encode('utf-8') + password_utf8 = verify_length_and_trunc_password(password) hasher = _get_hasher_from_ident(hashed) return hasher.verify(password_utf8, hashed) @@ -99,7 +118,7 @@ def hash_password(password): """Hash a password. Harder.""" params = {} - password_utf8 = verify_length_and_trunc_password(password).encode('utf-8') + password_utf8 = verify_length_and_trunc_password(password) conf_hasher = CONF.identity.password_hash_algorithm hasher = _HASHER_NAME_MAP.get(conf_hasher) diff -Nru keystone-22.0.0/keystone/conf/identity.py keystone-22.0.2/keystone/conf/identity.py --- keystone-22.0.0/keystone/conf/identity.py 2022-09-08 03:35:35.000000000 +0000 +++ keystone-22.0.2/keystone/conf/identity.py 2023-08-28 07:26:05.000000000 +0000 @@ -99,7 +99,11 @@ max=passlib.utils.MAX_PASSWORD_SIZE, help=utils.fmt(""" Maximum allowed length for user passwords. Decrease this value to improve -performance. Changing this value does not effect existing passwords. +performance. Changing this value does not effect existing passwords. This value +can also be overridden by certain hashing algorithms maximum allowed length +which takes precedence over the configured value. + +The bcrypt max_password_length is 72 bytes. """)) list_limit = cfg.IntOpt( diff -Nru keystone-22.0.0/keystone/conf/ldap.py keystone-22.0.2/keystone/conf/ldap.py --- keystone-22.0.0/keystone/conf/ldap.py 2022-09-08 03:35:35.000000000 +0000 +++ keystone-22.0.2/keystone/conf/ldap.py 2023-08-28 07:26:05.000000000 +0000 @@ -24,6 +24,18 @@ connection. """)) +randomize_urls = cfg.BoolOpt( + 'randomize_urls', + default=False, + help=utils.fmt(""" +Randomize the order of URLs in each keystone process. This makes the failure +behavior more gradual, since if the first server is down, a process/thread +will wait for the specified timeout before attempting a connection to a +server further down the list. This defaults to False, for backward +compatibility. +""")) + + user = cfg.StrOpt( 'user', help=utils.fmt(""" @@ -479,6 +491,7 @@ GROUP_NAME = __name__.split('.')[-1] ALL_OPTS = [ url, + randomize_urls, user, password, suffix, diff -Nru keystone-22.0.0/keystone/federation/utils.py keystone-22.0.2/keystone/federation/utils.py --- keystone-22.0.0/keystone/federation/utils.py 2022-09-08 03:35:35.000000000 +0000 +++ keystone-22.0.2/keystone/federation/utils.py 2023-08-28 07:26:05.000000000 +0000 @@ -562,17 +562,31 @@ LOG.debug('mapped_properties: %s', mapped_properties) return mapped_properties + def _ast_literal_eval(self, value): + # This is a workaround for the fact that ast.literal_eval handles the + # case of either a string or a list of strings, but not a potential + # list of ints. + + try: + values = ast.literal_eval(value) + # NOTE(mnaser): It's possible that the group_names_list is a + # numerical value which would successfully parse + # and not raise an exception, so we forcefully + # raise is here. + if not isinstance(values, list): + raise ValueError + except (ValueError, SyntaxError): + values = [value] + + return values + def _normalize_groups(self, identity_value): # In this case, identity_value['groups'] is a string # representation of a list, and we want a real list. This is # due to the way we do direct mapping substitutions today (see # function _update_local_mapping() ) if 'name' in identity_value['groups']: - try: - group_names_list = ast.literal_eval( - identity_value['groups']) - except (ValueError, SyntaxError): - group_names_list = [identity_value['groups']] + group_names_list = self._ast_literal_eval(identity_value['groups']) def convert_json(group): if group.startswith('JSON:'): @@ -594,11 +608,8 @@ "specified.") msg = msg % {'identity_value': identity_value} raise exception.ValidationError(msg) - try: - group_names_list = ast.literal_eval( - identity_value['groups']) - except (ValueError, SyntaxError): - group_names_list = [identity_value['groups']] + group_names_list = self._ast_literal_eval( + identity_value['groups']) domain = identity_value['domain'] group_dicts = [{'name': name, 'domain': domain} for name in group_names_list] @@ -699,11 +710,8 @@ # group_ids parameter contains only one element, it will be # parsed as a simple string, and not a list or the # representation of a list. - try: - group_ids.update( - ast.literal_eval(identity_value['group_ids'])) - except (ValueError, SyntaxError): - group_ids.update([identity_value['group_ids']]) + group_ids.update( + self._ast_literal_eval(identity_value['group_ids'])) if 'projects' in identity_value: projects = identity_value['projects'] diff -Nru keystone-22.0.0/keystone/identity/backends/ldap/common.py keystone-22.0.2/keystone/identity/backends/ldap/common.py --- keystone-22.0.0/keystone/identity/backends/ldap/common.py 2022-09-08 03:35:35.000000000 +0000 +++ keystone-22.0.2/keystone/identity/backends/ldap/common.py 2023-08-28 07:26:05.000000000 +0000 @@ -15,6 +15,7 @@ import abc import codecs import os.path +import random import re import sys import uuid @@ -860,11 +861,22 @@ cleaned up when message.clean() is called. """ - results = message.connection.result3(message.id, all, timeout) - - # Now that we have the results from the LDAP server for the message, we - # don't need the the context manager used to create the connection. - message.clean() + # message.connection.result3 might throw an exception + # so the code must ensure that message.clean() is invoked + # regardless of the result3's result. Otherwise, the + # connection will be marked as active forever, which + # ultimately renders the pool unusable, causing a DoS. + try: + results = message.connection.result3(message.id, all, timeout) + except Exception: + # We don't want to ignore thrown + # exceptions, raise them + raise + finally: + # Now that we have the results from the LDAP server for + # the message, we don't need the the context manager used + # to create the connection. + message.clean() return results @@ -1155,7 +1167,12 @@ tree_dn = None def __init__(self, conf): - self.LDAP_URL = conf.ldap.url + if conf.ldap.randomize_urls: + urls = re.split(r'[\s,]+', conf.ldap.url) + random.shuffle(urls) + self.LDAP_URL = ','.join(urls) + else: + self.LDAP_URL = conf.ldap.url self.LDAP_USER = conf.ldap.user self.LDAP_PASSWORD = conf.ldap.password self.LDAP_SCOPE = ldap_scope(conf.ldap.query_scope) diff -Nru keystone-22.0.0/keystone/tests/unit/common/test_utils.py keystone-22.0.2/keystone/tests/unit/common/test_utils.py --- keystone-22.0.0/keystone/tests/unit/common/test_utils.py 2022-09-08 03:35:35.000000000 +0000 +++ keystone-22.0.2/keystone/tests/unit/common/test_utils.py 2023-08-28 07:26:05.000000000 +0000 @@ -77,7 +77,7 @@ self.config_fixture.config(strict_password_check=False) password = uuid.uuid4().hex verified = common_utils.verify_length_and_trunc_password(password) - self.assertEqual(password, verified) + self.assertEqual(password.encode('utf-8'), verified) def test_that_a_hash_can_not_be_validated_against_a_hash(self): # NOTE(dstanek): Bug 1279849 reported a problem where passwords @@ -97,7 +97,7 @@ max_length = CONF.identity.max_password_length invalid_password = 'passw0rd' trunc = common_utils.verify_length_and_trunc_password(invalid_password) - self.assertEqual(invalid_password[:max_length], trunc) + self.assertEqual(invalid_password.encode('utf-8')[:max_length], trunc) def test_verify_long_password_strict_raises_exception(self): self.config_fixture.config(strict_password_check=True) @@ -133,6 +133,17 @@ self.assertRaises(exception.PasswordVerificationError, common_utils.hash_password, invalid_length_password) + + def test_max_algo_length_truncates_password(self): + self.config_fixture.config(strict_password_check=True) + self.config_fixture.config(group='identity', + password_hash_algorithm='bcrypt') + self.config_fixture.config(group='identity', + max_password_length='96') + invalid_length_password = '0' * 96 + self.assertRaises(exception.PasswordVerificationError, + common_utils.hash_password, + invalid_length_password) def _create_test_user(self, password=OPTIONAL): user = {"name": "hthtest"} diff -Nru keystone-22.0.0/keystone/tests/unit/contrib/federation/test_utils.py keystone-22.0.2/keystone/tests/unit/contrib/federation/test_utils.py --- keystone-22.0.0/keystone/tests/unit/contrib/federation/test_utils.py 2022-09-08 03:35:35.000000000 +0000 +++ keystone-22.0.2/keystone/tests/unit/contrib/federation/test_utils.py 2023-08-28 07:26:05.000000000 +0000 @@ -764,6 +764,24 @@ self.assertEqual('ALL USERS', mapped_properties['group_names'][0]['name']) + def test_rule_engine_groups_mapping_only_one_numerical_group(self): + """Test mapping engine when groups is explicitly set. + + If the groups list has only one group, + test if the transformation is done correctly + + """ + mapping = mapping_fixtures.MAPPING_GROUPS_WITH_EMAIL + assertion = mapping_fixtures.GROUPS_ASSERTION_ONLY_ONE_NUMERICAL_GROUP + rp = mapping_utils.RuleProcessor(FAKE_MAPPING_ID, mapping['rules']) + mapped_properties = rp.process(assertion) + self.assertIsNotNone(mapped_properties) + self.assertEqual('jsmith', mapped_properties['user']['name']) + self.assertEqual('jill@example.com', + mapped_properties['user']['email']) + self.assertEqual('1234', + mapped_properties['group_names'][0]['name']) + def test_rule_engine_group_ids_mapping_whitelist(self): """Test mapping engine when group_ids is explicitly set. diff -Nru keystone-22.0.0/keystone/tests/unit/fakeldap.py keystone-22.0.2/keystone/tests/unit/fakeldap.py --- keystone-22.0.0/keystone/tests/unit/fakeldap.py 2022-09-08 03:35:35.000000000 +0000 +++ keystone-22.0.2/keystone/tests/unit/fakeldap.py 2023-08-28 07:26:05.000000000 +0000 @@ -296,6 +296,9 @@ raise ldap.SERVER_DOWN whos = ['cn=Admin', CONF.ldap.user] if (who in whos and cred in ['password', CONF.ldap.password]): + self.connected = True + self.who = who + self.cred = cred return attrs = self.db.get(self.key(who)) @@ -316,6 +319,9 @@ def unbind_s(self): """Provide for compatibility but this method is ignored.""" + self.connected = False + self.who = None + self.cred = None if server_fail: raise ldap.SERVER_DOWN @@ -534,7 +540,7 @@ raise exception.NotImplemented() # only passing a single server control is supported by this fake ldap - if len(serverctrls) > 1: + if serverctrls and len(serverctrls) > 1: raise exception.NotImplemented() # search_ext is async and returns an identifier used for @@ -589,6 +595,7 @@ def __init__(self, uri, retry_max=None, retry_delay=None, conn=None): super(FakeLdapPool, self).__init__(conn=conn) self.url = uri + self._uri = uri self.connected = None self.conn = self self._connection_time = 5 # any number greater than 0 diff -Nru keystone-22.0.0/keystone/tests/unit/identity/backends/test_ldap_common.py keystone-22.0.2/keystone/tests/unit/identity/backends/test_ldap_common.py --- keystone-22.0.0/keystone/tests/unit/identity/backends/test_ldap_common.py 2022-09-08 03:35:35.000000000 +0000 +++ keystone-22.0.2/keystone/tests/unit/identity/backends/test_ldap_common.py 2023-08-28 07:26:05.000000000 +0000 @@ -245,6 +245,33 @@ ldap_connection = base_ldap.get_connection() self.assertEqual(urls, ldap_connection.conn.conn_pool.uri) + @mock.patch.object(common_ldap.KeystoneLDAPHandler, 'simple_bind_s') + def test_multiple_urls_with_comma_randomized(self, mock_ldap_bind): + urls = ('ldap://localhost1,ldap://localhost2,' + 'ldap://localhost3,ldap://localhost4,' + 'ldap://localhost5,ldap://localhost6,' + 'ldap://localhost7,ldap://localhost8,' + 'ldap://localhost9,ldap://localhost0') + self.config_fixture.config(group='ldap', url=urls, + randomize_urls=True) + base_ldap = common_ldap.BaseLdap(CONF) + ldap_connection = base_ldap.get_connection() + + # Sanity check + self.assertEqual(len(urls.split(',')), 10) + + # Check that the list is split into the same number of URIs + self.assertEqual(len(urls.split(',')), + len(ldap_connection.conn.conn_pool.uri.split(','))) + + # Check that the list is randomized + self.assertNotEqual(urls.split(','), + ldap_connection.conn.conn_pool.uri.split(',')) + + # Check that the list contains the same URIs + self.assertEqual(set(urls.split(',')), + set(ldap_connection.conn.conn_pool.uri.split(','))) + class LDAPConnectionTimeoutTest(unit.TestCase): """Test for Network Connection timeout on LDAP URL connection.""" diff -Nru keystone-22.0.0/keystone/tests/unit/mapping_fixtures.py keystone-22.0.2/keystone/tests/unit/mapping_fixtures.py --- keystone-22.0.0/keystone/tests/unit/mapping_fixtures.py 2022-09-08 03:35:35.000000000 +0000 +++ keystone-22.0.2/keystone/tests/unit/mapping_fixtures.py 2023-08-28 07:26:05.000000000 +0000 @@ -1735,6 +1735,12 @@ 'groups': 'ALL USERS' } +GROUPS_ASSERTION_ONLY_ONE_NUMERICAL_GROUP = { + 'userEmail': 'jill@example.com', + 'UserName': 'jsmith', + 'groups': '1234' +} + GROUPS_DOMAIN_ASSERTION = { 'openstack_user': 'bwilliams', 'openstack_user_domain': 'default', diff -Nru keystone-22.0.0/keystone/tests/unit/test_backend_ldap_pool.py keystone-22.0.2/keystone/tests/unit/test_backend_ldap_pool.py --- keystone-22.0.0/keystone/tests/unit/test_backend_ldap_pool.py 2022-09-08 03:35:35.000000000 +0000 +++ keystone-22.0.2/keystone/tests/unit/test_backend_ldap_pool.py 2023-08-28 07:26:05.000000000 +0000 @@ -163,12 +163,23 @@ # Then open 3 connections again and make sure size does not grow # over 3 - with _get_conn() as _: # conn1 + with _get_conn() as c1: # conn1 + self.assertEqual(3, len(ldappool_cm)) + c1.connected = False + with _get_conn() as c2: # conn2 + self.assertEqual(3, len(ldappool_cm)) + c2.connected = False + with _get_conn() as c3: # conn3 + c3.connected = False + c3.unbind_ext_s() + self.assertEqual(3, len(ldappool_cm)) + + with _get_conn() as c1: # conn1 self.assertEqual(1, len(ldappool_cm)) - with _get_conn() as _: # conn2 + with _get_conn() as c2: # conn2 self.assertEqual(2, len(ldappool_cm)) - with _get_conn() as _: # conn3 - _.unbind_ext_s() + with _get_conn() as c3: # conn3 + c3.unbind_ext_s() self.assertEqual(3, len(ldappool_cm)) def test_password_change_with_pool(self): @@ -209,6 +220,105 @@ user_id=self.user_sna['id'], password=old_password) + @mock.patch.object(fakeldap.FakeLdap, 'search_ext') + def test_search_ext_ensure_pool_connection_released(self, mock_search_ext): + """Test search_ext exception resiliency. + + Call search_ext function in isolation. Doing so will cause + search_ext to borrow a connection from the pool and associate + it with an AsynchronousMessage object. Borrowed connection ought + to be released if anything goes wrong during LDAP API call. This + test case intentionally throws an exception to ensure everything + goes as expected when LDAP connection raises an exception. + """ + class CustomDummyException(Exception): + pass + + # Throw an exception intentionally when LDAP + # connection search_ext function is called + mock_search_ext.side_effect = CustomDummyException() + self.config_fixture.config(group='ldap', pool_size=1) + pool = self.conn_pools[CONF.ldap.url] + user_api = ldap.UserApi(CONF) + + # setUp primes the pool so pool + # must have one connection + self.assertEqual(1, len(pool)) + for i in range(1, 10): + handler = user_api.get_connection() + # Just to ensure that we're using pooled connections + self.assertIsInstance(handler.conn, common_ldap.PooledLDAPHandler) + # LDAP API will throw CustomDummyException. In this scenario + # we expect LDAP connection to be made available back to the + # pool. + self.assertRaises( + CustomDummyException, + lambda: handler.search_ext( + 'dc=example,dc=test', + 'dummy', + 'objectclass=*', + ['mail', 'userPassword'] + ) + ) + # Pooled connection must not be evicted from the pool + self.assertEqual(1, len(pool)) + # Ensure that the connection is inactive afterwards + with pool._pool_lock: + for slot, conn in enumerate(pool._pool): + self.assertFalse(conn.active) + + self.assertEqual(mock_search_ext.call_count, i) + + @mock.patch.object(fakeldap.FakeLdap, 'result3') + def test_result3_ensure_pool_connection_released(self, mock_result3): + """Test search_ext-->result3 exception resiliency. + + Call search_ext function, grab an AsynchronousMessage object and + call result3 with it. During the result3 call, LDAP API will throw + an exception.The expectation is that the associated LDAP pool + connection for AsynchronousMessage must be released back to the + LDAP connection pool. + """ + class CustomDummyException(Exception): + pass + + # Throw an exception intentionally when LDAP + # connection result3 function is called + mock_result3.side_effect = CustomDummyException() + self.config_fixture.config(group='ldap', pool_size=1) + pool = self.conn_pools[CONF.ldap.url] + user_api = ldap.UserApi(CONF) + + # setUp primes the pool so pool + # must have one connection + self.assertEqual(1, len(pool)) + for i in range(1, 10): + handler = user_api.get_connection() + # Just to ensure that we're using pooled connections + self.assertIsInstance(handler.conn, common_ldap.PooledLDAPHandler) + msg = handler.search_ext( + 'dc=example,dc=test', + 'dummy', + 'objectclass=*', + ['mail', 'userPassword'] + ) + # Connection is in use, must be already marked active + self.assertTrue(msg.connection.active) + # Pooled connection must not be evicted from the pool + self.assertEqual(1, len(pool)) + # LDAP API will throw CustomDummyException. In this + # scenario we expect LDAP connection to be made + # available back to the pool. + self.assertRaises( + CustomDummyException, + lambda: handler.result3(msg) + ) + # Connection must be set inactive + self.assertFalse(msg.connection.active) + # Pooled connection must not be evicted from the pool + self.assertEqual(1, len(pool)) + self.assertEqual(mock_result3.call_count, i) + class LDAPIdentity(LdapPoolCommonTestMixin, test_backend_ldap.LDAPIdentity, diff -Nru keystone-22.0.0/keystone/tests/unit/test_v3_auth.py keystone-22.0.2/keystone/tests/unit/test_v3_auth.py --- keystone-22.0.0/keystone/tests/unit/test_v3_auth.py 2022-09-08 03:35:35.000000000 +0000 +++ keystone-22.0.2/keystone/tests/unit/test_v3_auth.py 2023-08-28 07:26:05.000000000 +0000 @@ -5577,6 +5577,21 @@ self.v3_create_token(auth_data, expected_status=http.client.UNAUTHORIZED) + def test_application_credential_expiration_limits_token_expiration(self): + expires_at = datetime.datetime.utcnow() + datetime.timedelta(minutes=1) + app_cred = self._make_app_cred(expires=expires_at) + app_cred_ref = self.app_cred_api.create_application_credential( + app_cred) + auth_data = self.build_authentication_request( + app_cred_id=app_cred_ref['id'], secret=app_cred_ref['secret']) + resp = self.v3_create_token(auth_data, + expected_status=http.client.CREATED) + token = resp.headers.get('X-Subject-Token') + future = datetime.datetime.utcnow() + datetime.timedelta(minutes=2) + with freezegun.freeze_time(future): + self._validate_token(token, + expected_status=http.client.UNAUTHORIZED) + def test_application_credential_fails_when_user_deleted(self): app_cred = self._make_app_cred() app_cred_ref = self.app_cred_api.create_application_credential( diff -Nru keystone-22.0.0/keystone/token/provider.py keystone-22.0.2/keystone/token/provider.py --- keystone-22.0.0/keystone/token/provider.py 2022-09-08 03:35:35.000000000 +0000 +++ keystone-22.0.2/keystone/token/provider.py 2023-08-28 07:26:05.000000000 +0000 @@ -267,6 +267,23 @@ default_expire_time(), subsecond=True ) + # NOTE(d34dh0r53): If this token is being issued with an application + # credential and the application credential expires before the token + # we need to set the token expiration to be the same as the application + # credential. See CVE-2022-2447 for more information. + if app_cred_id is not None: + app_cred_api = PROVIDERS.application_credential_api + app_cred = app_cred_api.get_application_credential( + token.application_credential_id) + token_time = timeutils.normalize_time( + timeutils.parse_isotime(token.expires_at)) + if (app_cred['expires_at'] is not None) and ( + token_time > app_cred['expires_at']): + token.expires_at = app_cred['expires_at'].isoformat() + LOG.debug('Resetting token expiration to the application' + ' credential expiration: %s', + app_cred['expires_at'].isoformat()) + token_id, issued_at = self.driver.generate_id_and_issued_at(token) token.mint(token_id, issued_at) diff -Nru keystone-22.0.0/releasenotes/notes/bcrypt_truncation_fix-674dc5d7f1e776f2.yaml keystone-22.0.2/releasenotes/notes/bcrypt_truncation_fix-674dc5d7f1e776f2.yaml --- keystone-22.0.0/releasenotes/notes/bcrypt_truncation_fix-674dc5d7f1e776f2.yaml 1970-01-01 00:00:00.000000000 +0000 +++ keystone-22.0.2/releasenotes/notes/bcrypt_truncation_fix-674dc5d7f1e776f2.yaml 2023-08-28 07:26:05.000000000 +0000 @@ -0,0 +1,7 @@ +--- +fixes: + - | + Passwords that are hashed using bcrypt are now truncated properly to the + maximum allowed length by the algorythm. This solves regression, when + passwords longer then 54 symbols are getting invalidated after the + Keystone upgrade. diff -Nru keystone-22.0.0/releasenotes/notes/max-password-length-truncation-and-warning-bd69090315ec18a7.yaml keystone-22.0.2/releasenotes/notes/max-password-length-truncation-and-warning-bd69090315ec18a7.yaml --- keystone-22.0.0/releasenotes/notes/max-password-length-truncation-and-warning-bd69090315ec18a7.yaml 1970-01-01 00:00:00.000000000 +0000 +++ keystone-22.0.2/releasenotes/notes/max-password-length-truncation-and-warning-bd69090315ec18a7.yaml 2023-08-28 07:26:05.000000000 +0000 @@ -0,0 +1,9 @@ +--- +security: + - | + Passwords will now be automatically truncated if the max_password_length is + greater than the allowed length for the selected password hashing + algorithm. Currently only bcrypt has fixed allowed lengths defined which is + 54 characters. A warning will be generated in the log if a password is + truncated. This will not affect existing passwords, however only the first + 54 characters of existing bcrypt passwords will be validated. diff -Nru keystone-22.0.0/releasenotes/notes/randomize_urls-c0c19f48b2bfa299.yaml keystone-22.0.2/releasenotes/notes/randomize_urls-c0c19f48b2bfa299.yaml --- keystone-22.0.0/releasenotes/notes/randomize_urls-c0c19f48b2bfa299.yaml 1970-01-01 00:00:00.000000000 +0000 +++ keystone-22.0.2/releasenotes/notes/randomize_urls-c0c19f48b2bfa299.yaml 2023-08-28 07:26:05.000000000 +0000 @@ -0,0 +1,6 @@ +--- +features: + - | + A new option 'randomize_urls' can be used to randomize the order in which + keystone connects to the LDAP servers in [ldap] 'url' list. + It is false by default. diff -Nru keystone-22.0.0/releasenotes/notes/token_expiration_to_match_application_credential-56d058355a9f240d.yaml keystone-22.0.2/releasenotes/notes/token_expiration_to_match_application_credential-56d058355a9f240d.yaml --- keystone-22.0.0/releasenotes/notes/token_expiration_to_match_application_credential-56d058355a9f240d.yaml 1970-01-01 00:00:00.000000000 +0000 +++ keystone-22.0.2/releasenotes/notes/token_expiration_to_match_application_credential-56d058355a9f240d.yaml 2023-08-28 07:26:05.000000000 +0000 @@ -0,0 +1,10 @@ +--- +security: + - | + [`bug 1992183 `_] + [`CVE-2022-2447 `_] + Tokens issued with application credentials will now have their expiration + validated against that of the application credential. If the application + credential expires before the token the token's expiration will be set to + the same expiration as the application credential. Otherwise the token + will use the configured value. diff -Nru keystone-22.0.0/tox.ini keystone-22.0.2/tox.ini --- keystone-22.0.0/tox.ini 2022-09-08 03:35:35.000000000 +0000 +++ keystone-22.0.2/tox.ini 2023-08-28 07:26:05.000000000 +0000 @@ -9,7 +9,7 @@ setenv = PYTHONDONTWRITEBYTECODE=1 deps = - -c{env:UPPER_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} + -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/zed} -r{toxinidir}/test-requirements.txt .[ldap,memcache,mongodb] commands = @@ -43,7 +43,7 @@ # NOTE(browne): This is required for the integration test job of the bandit # project. Please do not remove. deps = - -c{env:UPPER_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} + -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/zed} -r{toxinidir}/requirements.txt .[bandit] commands = bandit -r keystone -x 'keystone/tests/*' @@ -85,7 +85,7 @@ [testenv:functional] deps = - -c{env:UPPER_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} + -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/zed} -r{toxinidir}/test-requirements.txt setenv = OS_TEST_PATH=./keystone/tests/functional commands = @@ -120,10 +120,13 @@ ignore = D100,D101,D102,D103,D104,D203,E402,W503,W504 exclude=.venv,.git,.tox,build,dist,*lib/python*,*egg,tools,vendor,.update-venv,*.ini,*.po,*.pot max-complexity=24 +per-file-ignores = +# URL lines too long + keystone/common/password_hashing.py: E501 [testenv:docs] deps = - -c{env:UPPER_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} + -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/zed} -r{toxinidir}/doc/requirements.txt .[ldap,memcache,mongodb] commands=