summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.zuul.yaml21
-rw-r--r--api-ref/source/v3/project-tags.inc10
-rw-r--r--doc/source/admin/federation/introduction.rst2
-rw-r--r--doc/source/admin/identity-concepts.rst2
-rw-r--r--doc/source/install/keystone-install-obs.rst7
-rw-r--r--doc/source/install/keystone-install-rdo.rst7
-rw-r--r--doc/source/install/keystone-install-ubuntu.rst7
-rw-r--r--etc/policy.v3cloudsample.json8
-rw-r--r--keystone/api/auth.py1
-rw-r--r--keystone/api/limits.py65
-rw-r--r--keystone/api/projects.py30
-rw-r--r--keystone/api/trusts.py10
-rw-r--r--keystone/api/users.py15
-rw-r--r--keystone/application_credential/core.py5
-rw-r--r--keystone/cmd/cli.py27
-rw-r--r--keystone/common/policies/application_credential.py11
-rw-r--r--keystone/common/policies/base.py10
-rw-r--r--keystone/common/policies/consumer.py10
-rw-r--r--keystone/common/policies/credential.py26
-rw-r--r--keystone/common/policies/domain.py10
-rw-r--r--keystone/common/policies/domain_config.py10
-rw-r--r--keystone/common/policies/ec2_credential.py23
-rw-r--r--keystone/common/policies/endpoint.py10
-rw-r--r--keystone/common/policies/endpoint_group.py10
-rw-r--r--keystone/common/policies/grant.py10
-rw-r--r--keystone/common/policies/group.py9
-rw-r--r--keystone/common/policies/identity_provider.py10
-rw-r--r--keystone/common/policies/implied_role.py10
-rw-r--r--keystone/common/policies/limit.py22
-rw-r--r--keystone/common/policies/mapping.py10
-rw-r--r--keystone/common/policies/policy.py10
-rw-r--r--keystone/common/policies/policy_association.py10
-rw-r--r--keystone/common/policies/project.py110
-rw-r--r--keystone/common/policies/project_endpoint.py70
-rw-r--r--keystone/common/policies/protocol.py11
-rw-r--r--keystone/common/policies/region.py7
-rw-r--r--keystone/common/policies/role.py10
-rw-r--r--keystone/common/policies/role_assignment.py36
-rw-r--r--keystone/common/policies/service.py10
-rw-r--r--keystone/common/policies/service_provider.py10
-rw-r--r--keystone/common/policies/token.py9
-rw-r--r--keystone/common/policies/trust.py10
-rw-r--r--keystone/common/policies/user.py9
-rw-r--r--keystone/common/rbac_enforcer/enforcer.py51
-rw-r--r--keystone/common/sql/upgrades.py3
-rw-r--r--keystone/credential/backends/sql.py17
-rw-r--r--keystone/federation/utils.py4
-rw-r--r--keystone/identity/backends/ldap/common.py25
-rw-r--r--keystone/notifications.py1
-rw-r--r--keystone/server/flask/common.py2
-rw-r--r--keystone/tests/protection/v3/test_access_rules.py (renamed from keystone/tests/unit/protection/v3/test_access_rules.py)0
-rw-r--r--keystone/tests/protection/v3/test_assignment.py214
-rw-r--r--keystone/tests/protection/v3/test_consumer.py (renamed from keystone/tests/unit/protection/v3/test_consumer.py)0
-rw-r--r--keystone/tests/protection/v3/test_credentials.py12
-rw-r--r--keystone/tests/protection/v3/test_domain_config.py (renamed from keystone/tests/unit/protection/v3/test_domain_config.py)0
-rw-r--r--keystone/tests/protection/v3/test_domain_roles.py (renamed from keystone/tests/unit/protection/v3/test_domain_roles.py)0
-rw-r--r--keystone/tests/protection/v3/test_ec2_credential.py (renamed from keystone/tests/unit/protection/v3/test_ec2_credential.py)9
-rw-r--r--keystone/tests/protection/v3/test_implied_roles.py (renamed from keystone/tests/unit/protection/v3/test_implied_roles.py)0
-rw-r--r--keystone/tests/protection/v3/test_limits.py578
-rw-r--r--keystone/tests/protection/v3/test_policy_association.py7
-rw-r--r--keystone/tests/protection/v3/test_project_endpoint.py465
-rw-r--r--keystone/tests/protection/v3/test_project_tags.py974
-rw-r--r--keystone/tests/unit/base_classes.py1
-rw-r--r--keystone/tests/unit/credential/test_backend_sql.py14
-rw-r--r--keystone/tests/unit/test_backend_ldap.py46
-rw-r--r--keystone/tests/unit/test_cli.py26
-rw-r--r--keystone/tests/unit/test_limits.py29
-rw-r--r--keystone/tests/unit/test_policy.py6
-rw-r--r--keystone/tests/unit/test_v3_auth.py16
-rw-r--r--keystone/tests/unit/test_v3_federation.py10
-rw-r--r--keystone/tests/unit/token/test_fernet_provider.py68
-rw-r--r--keystone/token/token_formatters.py122
-rw-r--r--releasenotes/notes/bug-1805880-0032024ea6b83563.yaml14
-rw-r--r--releasenotes/notes/bug-1818736-98ea186a074056f4.yaml17
-rw-r--r--releasenotes/notes/bug-1832265-cb76ccf505c2d9d1.yaml7
-rw-r--r--releasenotes/notes/bug-1833739-f962e8caf3e22068.yaml9
-rw-r--r--releasenotes/notes/bug-1836568-66d853a1f22c5530.yaml10
-rw-r--r--releasenotes/notes/bug-1839133-24570c9fbacb530d.yaml5
-rw-r--r--releasenotes/notes/bug-1841486-425f367925f5e03f.yaml7
-rw-r--r--releasenotes/notes/bug-1843609-8498b132222596b7.yaml9
-rw-r--r--releasenotes/notes/bug-1844157-7808af9bcea0429d.yaml13
-rw-r--r--releasenotes/notes/bug-1844194-48ae60db49f91bd4.yaml43
-rw-r--r--releasenotes/notes/bug-1844207-x27a31f3403xfd7y.yaml7
-rw-r--r--releasenotes/notes/bug-1844461-08a8bdc5f613b88d.yaml31
-rw-r--r--releasenotes/notes/bug-1844664-905cf6cad2e032a7.yaml36
85 files changed, 3142 insertions, 486 deletions
diff --git a/.zuul.yaml b/.zuul.yaml
index dcfbfb847..7ea95164b 100644
--- a/.zuul.yaml
+++ b/.zuul.yaml
@@ -71,6 +71,7 @@
- job:
name: keystone-tox-protection
parent: openstack-tox-py37
+ timeout: 3600
vars:
tox_envlist: protection
bindep_profile: test py37
@@ -168,17 +169,6 @@
- release-notes-jobs-python3
check:
jobs:
- # Override tox timeouts
- - openstack-tox-py37:
- timeout: 5400
- - openstack-tox-py36:
- timeout: 5400
- - openstack-tox-py27:
- timeout: 5400
- - openstack-tox-cover:
- timeout: 5400
- - openstack-tox-lower-constraints:
- timeout: 5400
- keystone-dsvm-functional:
irrelevant-files: &irrelevant-files
- ^.*\.rst$
@@ -222,15 +212,6 @@
- keystone-tox-protection
gate:
jobs:
- # Override tox timeouts
- - openstack-tox-py37:
- timeout: 5400
- - openstack-tox-py36:
- timeout: 5400
- - openstack-tox-py27:
- timeout: 5400
- - openstack-tox-lower-constraints:
- timeout: 5400
- keystone-dsvm-functional:
irrelevant-files: *irrelevant-files
- keystone-dsvm-py3-functional:
diff --git a/api-ref/source/v3/project-tags.inc b/api-ref/source/v3/project-tags.inc
index 2021ec111..a789a8091 100644
--- a/api-ref/source/v3/project-tags.inc
+++ b/api-ref/source/v3/project-tags.inc
@@ -19,6 +19,16 @@ Tags for projects have the following restrictions:
- Each project can have a maximum of 80 tags
- Each tag can be a maximum of 255 characters in length
+.. warning::
+
+ We discourage the use of project tags for sensitive information like billing or
+ account codes. By default, access to project tags isn't exclusive to system
+ administrators or users. Domain and project administrators are allowed to
+ tag projects they have authorization to access. Domain and project users
+ (e.g., users with the ``member`` or ``reader`` roles) can view all project
+ tags on all projects within their domain or on projects they are
+ authorized to access.
+
List tags for a project
=======================
diff --git a/doc/source/admin/federation/introduction.rst b/doc/source/admin/federation/introduction.rst
index 0681621c2..52d32dd8b 100644
--- a/doc/source/admin/federation/introduction.rst
+++ b/doc/source/admin/federation/introduction.rst
@@ -37,7 +37,7 @@ but it requires keystone to handle passwords directly rather than offloading
authentication to the external source.
Keystone supports two configuration models for federated identity. The most
-common configuration is with `keystone as a Service Provider (SP)
+common configuration is with :ref:`keystone as a Service Provider (SP)
<keystone-as-sp>`, using an
external Identity Provider, such as a Keycloak or Google, as the identity source
and authentication method. The second type of configuration is
diff --git a/doc/source/admin/identity-concepts.rst b/doc/source/admin/identity-concepts.rst
index a9b1fb61e..3d615c0da 100644
--- a/doc/source/admin/identity-concepts.rst
+++ b/doc/source/admin/identity-concepts.rst
@@ -297,7 +297,7 @@ service, such as, a user named ``nova`` for the Compute service, and a
special service project called ``service``.
For information about how to create services and endpoints, see the
-`Administrator Guide <manage_services>`__.
+:ref:`Administrator Guide <manage_services>`.
Groups
~~~~~~
diff --git a/doc/source/install/keystone-install-obs.rst b/doc/source/install/keystone-install-obs.rst
index c62b7b37d..dfd23f5ec 100644
--- a/doc/source/install/keystone-install-obs.rst
+++ b/doc/source/install/keystone-install-obs.rst
@@ -123,6 +123,13 @@ Install and configure components
4. Initialize Fernet key repositories:
+ .. note::
+
+ The ``--keystone-user`` and ``--keystone-group`` flags are used to specify the
+ operating system's user/group that will be used to run keystone. These are provided
+ to allow running keystone under another operating system user/group. In the example
+ below, we call the user & group ``keystone``.
+
.. code-block:: console
# keystone-manage fernet_setup --keystone-user keystone --keystone-group keystone
diff --git a/doc/source/install/keystone-install-rdo.rst b/doc/source/install/keystone-install-rdo.rst
index 176f01ac5..a5c5384b7 100644
--- a/doc/source/install/keystone-install-rdo.rst
+++ b/doc/source/install/keystone-install-rdo.rst
@@ -112,6 +112,13 @@ Install and configure components
4. Initialize Fernet key repositories:
+ .. note::
+
+ The ``--keystone-user`` and ``--keystone-group`` flags are used to specify the
+ operating system's user/group that will be used to run keystone. These are provided
+ to allow running keystone under another operating system user/group. In the example
+ below, we call the user & group ``keystone``.
+
.. code-block:: console
# keystone-manage fernet_setup --keystone-user keystone --keystone-group keystone
diff --git a/doc/source/install/keystone-install-ubuntu.rst b/doc/source/install/keystone-install-ubuntu.rst
index 3d0ff9e89..7edc26730 100644
--- a/doc/source/install/keystone-install-ubuntu.rst
+++ b/doc/source/install/keystone-install-ubuntu.rst
@@ -116,6 +116,13 @@ Install and configure components
4. Initialize Fernet key repositories:
+ .. note::
+
+ The ``--keystone-user`` and ``--keystone-group`` flags are used to specify the
+ operating system's user/group that will be used to run keystone. These are provided
+ to allow running keystone under another operating system user/group. In the example
+ below, we call the user & group ``keystone``.
+
.. code-block:: console
# keystone-manage fernet_setup --keystone-user keystone --keystone-group keystone
diff --git a/etc/policy.v3cloudsample.json b/etc/policy.v3cloudsample.json
index 7b21c4bea..8e1273227 100644
--- a/etc/policy.v3cloudsample.json
+++ b/etc/policy.v3cloudsample.json
@@ -8,14 +8,6 @@
"default": "rule:admin_required",
- "identity:get_limit": "",
- "identity:create_limits": "rule:admin_required",
- "identity:update_limit": "rule:admin_required",
- "identity:delete_limit": "rule:admin_required",
-
- "identity:get_project_tag": "rule:admin_required",
- "identity:list_project_tags": "rule:admin_required",
-
"domain_admin_matches_domain_role": "rule:admin_required and domain_id:%(role.domain_id)s",
"get_domain_roles": "rule:domain_admin_matches_target_domain_role or rule:project_admin_matches_target_domain_role",
"domain_admin_matches_target_domain_role": "rule:admin_required and domain_id:%(target.role.domain_id)s",
diff --git a/keystone/api/auth.py b/keystone/api/auth.py
index d399df433..91dfa43ca 100644
--- a/keystone/api/auth.py
+++ b/keystone/api/auth.py
@@ -338,6 +338,7 @@ class AuthFederationWebSSOResource(_AuthFederationWebSSOBase):
@classmethod
def _perform_auth(cls, protocol_id):
idps = PROVIDERS.federation_api.list_idps()
+ remote_id = None
for idp in idps:
try:
remote_id_name = federation_utils.get_remote_id_parameter(
diff --git a/keystone/api/limits.py b/keystone/api/limits.py
index c265a3cc7..83eed9bee 100644
--- a/keystone/api/limits.py
+++ b/keystone/api/limits.py
@@ -20,6 +20,7 @@ from keystone.common import json_home
from keystone.common import provider_api
from keystone.common import rbac_enforcer
from keystone.common import validation
+from keystone import exception
from keystone.limit import schema
from keystone.server import flask as ks_flask
@@ -28,6 +29,27 @@ PROVIDERS = provider_api.ProviderAPIs
ENFORCER = rbac_enforcer.RBACEnforcer
+def _build_limit_enforcement_target():
+ target = {}
+ try:
+ limit = PROVIDERS.unified_limit_api.get_limit(
+ flask.request.view_args.get('limit_id')
+ )
+ target['limit'] = limit
+ if limit.get('project_id'):
+ project = PROVIDERS.resource_api.get_project(limit['project_id'])
+ target['limit']['project'] = project
+ elif limit.get('domain_id'):
+ domain = PROVIDERS.resource_api.get_domain(limit['domain_id'])
+ target['limit']['domain'] = domain
+ except exception.NotFound: # nosec
+ # Defer the existence check in the event the limit doesn't exist, this
+ # is checked later anyway.
+ pass
+
+ return target
+
+
class LimitsResource(ks_flask.ResourceBase):
collection_key = 'limits'
member_key = 'limit'
@@ -38,27 +60,38 @@ class LimitsResource(ks_flask.ResourceBase):
def _list_limits(self):
filters = ['service_id', 'region_id', 'resource_name', 'project_id',
'domain_id']
+
ENFORCER.enforce_call(action='identity:list_limits', filters=filters)
+
hints = self.build_driver_hints(filters)
- project_id_filter = hints.get_exact_filter_by_name('project_id')
- domain_id_filter = hints.get_exact_filter_by_name('domain_id')
- if project_id_filter or domain_id_filter:
- if self.oslo_context.system_scope:
- refs = PROVIDERS.unified_limit_api.list_limits(hints)
- else:
- refs = []
- else:
- project_id = self.oslo_context.project_id
- domain_id = self.oslo_context.domain_id
- if project_id:
- hints.add_filter('project_id', project_id)
- elif domain_id:
- hints.add_filter('domain_id', domain_id)
+
+ filtered_refs = []
+ if self.oslo_context.system_scope:
+ refs = PROVIDERS.unified_limit_api.list_limits(hints)
+ filtered_refs = refs
+ elif self.oslo_context.domain_id:
refs = PROVIDERS.unified_limit_api.list_limits(hints)
- return self.wrap_collection(refs, hints=hints)
+ projects = PROVIDERS.resource_api.list_projects_in_domain(
+ self.oslo_context.domain_id
+ )
+ project_ids = [project['id'] for project in projects]
+ for limit in refs:
+ if limit.get('project_id'):
+ if limit['project_id'] in project_ids:
+ filtered_refs.append(limit)
+ elif limit.get('domain_id'):
+ if limit['domain_id'] == self.oslo_context.domain_id:
+ filtered_refs.append(limit)
+ elif self.oslo_context.project_id:
+ hints.add_filter('project_id', self.oslo_context.project_id)
+ refs = PROVIDERS.unified_limit_api.list_limits(hints)
+ filtered_refs = refs
+
+ return self.wrap_collection(filtered_refs, hints=hints)
def _get_limit(self, limit_id):
- ENFORCER.enforce_call(action='identity:get_limit')
+ ENFORCER.enforce_call(action='identity:get_limit',
+ build_target=_build_limit_enforcement_target)
ref = PROVIDERS.unified_limit_api.get_limit(limit_id)
return self.wrap_member(ref)
diff --git a/keystone/api/projects.py b/keystone/api/projects.py
index 4eb76b48f..108971c21 100644
--- a/keystone/api/projects.py
+++ b/keystone/api/projects.py
@@ -236,7 +236,10 @@ class ProjectTagsResource(_ProjectTagResourceBase):
GET /v3/projects/{project_id}/tags
"""
- ENFORCER.enforce_call(action='identity:list_project_tags')
+ ENFORCER.enforce_call(
+ action='identity:list_project_tags',
+ build_target=_build_project_target_enforcement
+ )
ref = PROVIDERS.resource_api.list_project_tags(project_id)
return self.wrap_member(ref)
@@ -245,7 +248,10 @@ class ProjectTagsResource(_ProjectTagResourceBase):
PUT /v3/projects/{project_id}/tags
"""
- ENFORCER.enforce_call(action='identity:update_project_tags')
+ ENFORCER.enforce_call(
+ action='identity:update_project_tags',
+ build_target=_build_project_target_enforcement
+ )
tags = self.request_body_json.get('tags', {})
validation.lazy_validate(schema.project_tags_update, tags)
ref = PROVIDERS.resource_api.update_project_tags(
@@ -257,7 +263,10 @@ class ProjectTagsResource(_ProjectTagResourceBase):
DELETE /v3/projects/{project_id}/tags
"""
- ENFORCER.enforce_call(action='identity:delete_project_tags')
+ ENFORCER.enforce_call(
+ action='identity:delete_project_tags',
+ build_target=_build_project_target_enforcement
+ )
PROVIDERS.resource_api.update_project_tags(project_id, [])
return None, http_client.NO_CONTENT
@@ -268,7 +277,10 @@ class ProjectTagResource(_ProjectTagResourceBase):
GET /v3/projects/{project_id}/tags/{value}
"""
- ENFORCER.enforce_call(action='identity:get_project_tag')
+ ENFORCER.enforce_call(
+ action='identity:get_project_tag',
+ build_target=_build_project_target_enforcement,
+ )
PROVIDERS.resource_api.get_project_tag(project_id, value)
return None, http_client.NO_CONTENT
@@ -277,7 +289,10 @@ class ProjectTagResource(_ProjectTagResourceBase):
PUT /v3/projects/{project_id}/tags/{value}
"""
- ENFORCER.enforce_call(action='identity:create_project_tag')
+ ENFORCER.enforce_call(
+ action='identity:create_project_tag',
+ build_target=_build_project_target_enforcement
+ )
validation.lazy_validate(schema.project_tag_create, value)
# Check if we will exceed the max number of tags on this project
tags = PROVIDERS.resource_api.list_project_tags(project_id)
@@ -298,7 +313,10 @@ class ProjectTagResource(_ProjectTagResourceBase):
/v3/projects/{project_id}/tags/{value}
"""
- ENFORCER.enforce_call(action='identity:delete_project_tag')
+ ENFORCER.enforce_call(
+ action='identity:delete_project_tag',
+ build_target=_build_project_target_enforcement
+ )
PROVIDERS.resource_api.delete_project_tag(project_id, value)
return None, http_client.NO_CONTENT
diff --git a/keystone/api/trusts.py b/keystone/api/trusts.py
index 3c40d8c67..6c56fe1b0 100644
--- a/keystone/api/trusts.py
+++ b/keystone/api/trusts.py
@@ -228,11 +228,11 @@ class TrustResource(ks_flask.ResourceBase):
# rule check_str is ""
if isinstance(rules, op_checks.TrueCheck):
LOG.warning(
- "The policy check string for rule \"identity:list_trusts\" has been overridden"
- "to \"always true\". In the next release, this will cause the"
- "\"identity:list_trusts\" action to be fully permissive as hardcoded"
- "enforcement will be removed. To correct this issue, either stop overriding the"
- "\"identity:list_trusts\" rule in config to accept the defaults, or explicitly"
+ "The policy check string for rule \"identity:list_trusts\" has been overridden "
+ "to \"always true\". In the next release, this will cause the "
+ "\"identity:list_trusts\" action to be fully permissive as hardcoded "
+ "enforcement will be removed. To correct this issue, either stop overriding the "
+ "\"identity:list_trusts\" rule in config to accept the defaults, or explicitly "
"set a rule that is not empty."
)
if not flask.request.args:
diff --git a/keystone/api/users.py b/keystone/api/users.py
index 2e09f4b9a..b5938b17a 100644
--- a/keystone/api/users.py
+++ b/keystone/api/users.py
@@ -287,19 +287,6 @@ class UserGroupsResource(ks_flask.ResourceBase):
get_member_from_driver = PROVIDERS.deferred_provider_lookup(
api='identity_api', method='get_group')
- @staticmethod
- def _built_target_attr_enforcement():
- ref = None
- if flask.request.view_args:
- try:
- ref = {'user': PROVIDERS.identity_api.get_user(
- flask.request.view_args.get('user_id'))}
- except ks_exception.NotFound: # nosec
- # Defer existence in the event the user doesn't exist, we'll
- # check this later anyway.
- pass
- return ref
-
def get(self, user_id):
"""Get groups for a user.
@@ -308,7 +295,7 @@ class UserGroupsResource(ks_flask.ResourceBase):
filters = ('name',)
hints = self.build_driver_hints(filters)
ENFORCER.enforce_call(action='identity:list_groups_for_user',
- build_target=self._built_target_attr_enforcement,
+ build_target=_build_user_target_enforcement,
filters=filters)
refs = PROVIDERS.identity_api.list_groups_for_user(user_id=user_id,
hints=hints)
diff --git a/keystone/application_credential/core.py b/keystone/application_credential/core.py
index 00abb873e..8230b6544 100644
--- a/keystone/application_credential/core.py
+++ b/keystone/application_credential/core.py
@@ -205,7 +205,8 @@ class Manager(manager.Manager):
notifications.Audit.deleted(
self._APP_CRED, application_credential_id, initiator)
- def _delete_application_credentials_for_user(self, user_id):
+ def _delete_application_credentials_for_user(self, user_id,
+ initiator=None):
"""Delete all application credentials for a user.
:param str user_id: User ID
@@ -217,6 +218,8 @@ class Manager(manager.Manager):
self.driver.delete_application_credentials_for_user(user_id)
for app_cred in app_creds:
self.get_application_credential.invalidate(self, app_cred['id'])
+ notifications.Audit.deleted(self._APP_CRED, app_cred['id'],
+ initiator)
def _delete_application_credentials_for_user_on_project(self, user_id,
project_id):
diff --git a/keystone/cmd/cli.py b/keystone/cmd/cli.py
index 272bd5cb9..daf876e24 100644
--- a/keystone/cmd/cli.py
+++ b/keystone/cmd/cli.py
@@ -15,6 +15,7 @@
from __future__ import absolute_import
from __future__ import print_function
+import argparse
import datetime
import os
import sys
@@ -1216,26 +1217,28 @@ class MappingEngineTester(BaseApp):
parser = super(MappingEngineTester,
cls).add_argument_parser(subparsers)
+ parser.formatter_class = argparse.RawTextHelpFormatter
parser.add_argument('--rules', default=None, required=True,
help=("Path to the file with "
"rules to be executed. "
- "Content must be a proper JSON structure, "
- "with a top-level key 'rules' and "
+ "Content must be\na proper JSON structure, "
+ "with a top-level key 'rules' and\n"
"corresponding value being a list."))
parser.add_argument('--input', default=None, required=True,
help=("Path to the file with input attributes. "
- "The content consists of ':' separated "
- "parameter names and their values. "
- "There is only one key-value pair per line. "
- "A ';' in the value is a separator and then "
- "a value is treated as a list. Example:\n "
- "EMAIL: me@example.com\n"
- "LOGIN: me\n"
- "GROUPS: group1;group2;group3"))
+ "The content\nconsists of ':' separated "
+ "parameter names and their values.\nThere "
+ "is only one key-value pair per line. "
+ "A ';' in the\nvalue is a separator and "
+ "then a value is treated as a list.\n"
+ "Example:\n"
+ "\tEMAIL: me@example.com\n"
+ "\tLOGIN: me\n"
+ "\tGROUPS: group1;group2;group3"))
parser.add_argument('--prefix', default=None,
help=("A prefix used for each environment "
- "variable in the assertion. For example, "
- "all environment variables may have the "
+ "variable in the\nassertion. For example, "
+ "all environment variables may have\nthe "
"prefix ASDF_."))
parser.add_argument('--engine-debug',
default=False, action="store_true",
diff --git a/keystone/common/policies/application_credential.py b/keystone/common/policies/application_credential.py
index f09b63845..cebb85b02 100644
--- a/keystone/common/policies/application_credential.py
+++ b/keystone/common/policies/application_credential.py
@@ -31,13 +31,10 @@ deprecated_delete_application_credentials_for_user = policy.DeprecatedRule(
check_str=base.RULE_ADMIN_OR_OWNER
)
-DEPRECATED_REASON = """
-As of the Train release, the application credential API understands how to
-handle system-scoped tokens in addition to project tokens, making the API
-more accessible to users without compromising security or manageability for
-administrators. The new default policies for this API account for these changes
-automatically.
-"""
+DEPRECATED_REASON = (
+ "The application credential API is now aware of system scope and default "
+ "roles."
+)
application_credential_policies = [
policy.DocumentedRuleDefault(
diff --git a/keystone/common/policies/base.py b/keystone/common/policies/base.py
index 32ad5f087..bef063042 100644
--- a/keystone/common/policies/base.py
+++ b/keystone/common/policies/base.py
@@ -50,6 +50,16 @@ 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'
+# Credential and EC2 Credential policies
+SYSTEM_READER_OR_CRED_OWNER = (
+ '(' + SYSTEM_READER + ') '
+ 'or user_id:%(target.credential.user_id)s'
+)
+SYSTEM_ADMIN_OR_CRED_OWNER = (
+ '(' + SYSTEM_ADMIN + ') '
+ 'or user_id:%(target.credential.user_id)s'
+)
+
rules = [
policy.RuleDefault(
name='admin_required',
diff --git a/keystone/common/policies/consumer.py b/keystone/common/policies/consumer.py
index 94d6ec054..bf9a6bdd7 100644
--- a/keystone/common/policies/consumer.py
+++ b/keystone/common/policies/consumer.py
@@ -36,13 +36,9 @@ deprecated_delete_consumer = policy.DeprecatedRule(
check_str=base.RULE_ADMIN_REQUIRED
)
-DEPRECATED_REASON = """
-As of the Train release, the OAUTH1 consumer API understands how to
-handle system-scoped tokens in addition to project tokens, making the API
-more accessible to users without compromising security or manageability for
-administrators. The new default policies for this API account for these changes
-automatically.
-"""
+DEPRECATED_REASON = (
+ "The OAUTH1 consumer API is now aware of system scope and default roles."
+)
consumer_policies = [
policy.DocumentedRuleDefault(
diff --git a/keystone/common/policies/credential.py b/keystone/common/policies/credential.py
index 340f308b3..52a9fa808 100644
--- a/keystone/common/policies/credential.py
+++ b/keystone/common/policies/credential.py
@@ -15,22 +15,10 @@ from oslo_policy import policy
from keystone.common.policies import base
-SYSTEM_READER_OR_CRED_OWNER = (
- '(role:reader and system_scope:all) '
- 'or user_id:%(target.credential.user_id)s'
-)
-SYSTEM_ADMIN_OR_CRED_OWNER = (
- '(role:admin and system_scope:all) '
- 'or user_id:%(target.credential.user_id)s'
-)
-
DEPRECATED_REASON = (
- 'As of the Stein release, the credential API now understands how to '
- 'handle system-scoped tokens in addition to project-scoped tokens, making '
- 'the API more accessible to users without compromising security or '
- 'manageability for administrators. The new default policies for this API '
- 'account for these changes automatically.'
+ "The credential API is now aware of system scope and default roles."
)
+
deprecated_get_credential = policy.DeprecatedRule(
name=base.IDENTITY % 'get_credential',
check_str=base.RULE_ADMIN_REQUIRED
@@ -56,7 +44,7 @@ deprecated_delete_credential = policy.DeprecatedRule(
credential_policies = [
policy.DocumentedRuleDefault(
name=base.IDENTITY % 'get_credential',
- check_str=SYSTEM_READER_OR_CRED_OWNER,
+ check_str=base.SYSTEM_READER_OR_CRED_OWNER,
scope_types=['system', 'project'],
description='Show credentials details.',
operations=[{'path': '/v3/credentials/{credential_id}',
@@ -67,7 +55,7 @@ credential_policies = [
),
policy.DocumentedRuleDefault(
name=base.IDENTITY % 'list_credentials',
- check_str=SYSTEM_READER_OR_CRED_OWNER,
+ check_str=base.SYSTEM_READER_OR_CRED_OWNER,
scope_types=['system', 'project'],
description='List credentials.',
operations=[{'path': '/v3/credentials',
@@ -78,7 +66,7 @@ credential_policies = [
),
policy.DocumentedRuleDefault(
name=base.IDENTITY % 'create_credential',
- check_str=SYSTEM_ADMIN_OR_CRED_OWNER,
+ check_str=base.SYSTEM_ADMIN_OR_CRED_OWNER,
scope_types=['system', 'project'],
description='Create credential.',
operations=[{'path': '/v3/credentials',
@@ -89,7 +77,7 @@ credential_policies = [
),
policy.DocumentedRuleDefault(
name=base.IDENTITY % 'update_credential',
- check_str=SYSTEM_ADMIN_OR_CRED_OWNER,
+ check_str=base.SYSTEM_ADMIN_OR_CRED_OWNER,
scope_types=['system', 'project'],
description='Update credential.',
operations=[{'path': '/v3/credentials/{credential_id}',
@@ -100,7 +88,7 @@ credential_policies = [
),
policy.DocumentedRuleDefault(
name=base.IDENTITY % 'delete_credential',
- check_str=SYSTEM_ADMIN_OR_CRED_OWNER,
+ check_str=base.SYSTEM_ADMIN_OR_CRED_OWNER,
scope_types=['system', 'project'],
description='Delete credential.',
operations=[{'path': '/v3/credentials/{credential_id}',
diff --git a/keystone/common/policies/domain.py b/keystone/common/policies/domain.py
index 342bcdf61..7d3e3d788 100644
--- a/keystone/common/policies/domain.py
+++ b/keystone/common/policies/domain.py
@@ -15,13 +15,9 @@ from oslo_policy import policy
from keystone.common.policies import base
-DEPRECATED_REASON = """
-As of the Stein release, the domain API now understands how to handle
-system-scoped tokens in addition to project-scoped tokens, making the API more
-accessible to users without compromising security or manageability for
-administrators. The new default policies for this API account for these changes
-automatically
-"""
+DEPRECATED_REASON = (
+ "The domain API is now aware of system scope and default roles."
+)
deprecated_list_domains = policy.DeprecatedRule(
name=base.IDENTITY % 'list_domains',
diff --git a/keystone/common/policies/domain_config.py b/keystone/common/policies/domain_config.py
index 51b7a4006..a157f0d5c 100644
--- a/keystone/common/policies/domain_config.py
+++ b/keystone/common/policies/domain_config.py
@@ -41,13 +41,9 @@ deprecated_delete_domain_config = policy.DeprecatedRule(
)
-DEPRECATED_REASON = """
-As of the Train release, the domain config API now understands default roles and
-system-scoped tokens, making the API more granular by default without
-compromising security. The new policy defaults account for these changes
-automatically. Be sure to take these new defaults into consideration if you are
-relying on overrides in your deployment for the domain config API.
-"""
+DEPRECATED_REASON = (
+ "The domain config API is now aware of system scope and default roles."
+)
domain_config_policies = [
policy.DocumentedRuleDefault(
diff --git a/keystone/common/policies/ec2_credential.py b/keystone/common/policies/ec2_credential.py
index ea8603c7e..25e65b532 100644
--- a/keystone/common/policies/ec2_credential.py
+++ b/keystone/common/policies/ec2_credential.py
@@ -15,15 +15,6 @@ from oslo_policy import policy
from keystone.common.policies import base
-SYSTEM_READER_OR_CRED_OWNER = (
- '(role:reader and system_scope:all) '
- 'or user_id:%(target.credential.user_id)s'
-)
-SYSTEM_ADMIN_OR_CRED_OWNER = (
- '(role:admin and system_scope:all) '
- 'or user_id:%(target.credential.user_id)s'
-)
-
deprecated_ec2_get_credential = policy.DeprecatedRule(
name=base.IDENTITY % 'ec2_get_credential',
check_str=base.RULE_ADMIN_OR_CREDENTIAL_OWNER
@@ -41,18 +32,14 @@ deprecated_ec2_delete_credentials = policy.DeprecatedRule(
check_str=base.RULE_ADMIN_OR_CREDENTIAL_OWNER
)
-DEPRECATED_REASON = """
-As of the Train release, the EC2 credential API understands how to handle
-system-scoped tokens in addition to project tokens, making the API more
-accessible to users without compromising security or manageability for
-administrators. The new default policies for this API account for these changes
-automatically.
-"""
+DEPRECATED_REASON = (
+ "The EC2 credential API is now aware of system scope and default roles."
+)
ec2_credential_policies = [
policy.DocumentedRuleDefault(
name=base.IDENTITY % 'ec2_get_credential',
- check_str=SYSTEM_READER_OR_CRED_OWNER,
+ check_str=base.SYSTEM_READER_OR_CRED_OWNER,
scope_types=['system', 'project'],
description='Show ec2 credential details.',
operations=[{'path': ('/v3/users/{user_id}/credentials/OS-EC2/'
@@ -86,7 +73,7 @@ ec2_credential_policies = [
),
policy.DocumentedRuleDefault(
name=base.IDENTITY % 'ec2_delete_credential',
- check_str=SYSTEM_ADMIN_OR_CRED_OWNER,
+ check_str=base.SYSTEM_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/endpoint.py b/keystone/common/policies/endpoint.py
index 440280838..b99a40e24 100644
--- a/keystone/common/policies/endpoint.py
+++ b/keystone/common/policies/endpoint.py
@@ -31,13 +31,9 @@ deprecated_delete_endpoint = policy.DeprecatedRule(
name=base.IDENTITY % 'delete_endpoint', check_str=base.RULE_ADMIN_REQUIRED,
)
-DEPRECATED_REASON = """
-As of the Stein release, the endpoint API now understands default roles and
-system-scoped tokens, making the API more granular by default without
-compromising security. The new policy defaults account for these changes
-automatically. Be sure to take these new defaults into consideration if you are
-relying on overrides in your deployment for the endpoint API.
-"""
+DEPRECATED_REASON = (
+ "The endpoint API is now aware of system scope and default roles."
+)
endpoint_policies = [
diff --git a/keystone/common/policies/endpoint_group.py b/keystone/common/policies/endpoint_group.py
index 641d256bd..c9e34dff1 100644
--- a/keystone/common/policies/endpoint_group.py
+++ b/keystone/common/policies/endpoint_group.py
@@ -71,13 +71,9 @@ deprecated_remove_endpoint_group_from_project = policy.DeprecatedRule(
)
-DEPRECATED_REASON = """
-As of the Train release, the endpoint groups API now understands default roles
-and system-scoped tokens, making the API more granular by default without
-compromising security. The new policy defaults account for these changes
-automatically. Be sure to take these new defaults into consideration if you are
-relying on overrides in your deployment for the endpoint groups API.
-"""
+DEPRECATED_REASON = (
+ "The endpoint groups API is now aware of system scope and default roles."
+)
group_endpoint_policies = [
diff --git a/keystone/common/policies/grant.py b/keystone/common/policies/grant.py
index e0c974055..ab46fb0e8 100644
--- a/keystone/common/policies/grant.py
+++ b/keystone/common/policies/grant.py
@@ -101,13 +101,9 @@ deprecated_revoke_grant = policy.DeprecatedRule(
name=base.IDENTITY % 'revoke_grant', check_str=base.RULE_ADMIN_REQUIRED
)
-DEPRECATED_REASON = """
-As of the Stein release, the assignment API now understands default roles and
-system-scoped tokens, making the API more granular by default without
-compromising security. The new policy defaults account for these changes
-automatically. Be sure to take these new defaults into consideration if you are
-relying on overrides in your deployment for the system assignment API.
-"""
+DEPRECATED_REASON = (
+ "The assignment API is now aware of system scope and default roles."
+)
resource_paths = [
'/projects/{project_id}/users/{user_id}/roles/{role_id}',
diff --git a/keystone/common/policies/group.py b/keystone/common/policies/group.py
index 63324d41e..d33da9289 100644
--- a/keystone/common/policies/group.py
+++ b/keystone/common/policies/group.py
@@ -45,12 +45,9 @@ SYSTEM_ADMIN_OR_DOMAIN_ADMIN = (
'(role:admin and domain_id:%(target.group.domain_id)s)'
)
-DEPRECATED_REASON = """
-As of the Stein release, the group API understands how to handle system-scoped
-tokens in addition to project and domain tokens, making the API more accessible
-to users without compromising security or manageability for administrators. The
-new default policies for this API account for these changes automatically.
-"""
+DEPRECATED_REASON = (
+ "The group API is now aware of system scope and default roles."
+)
deprecated_get_group = policy.DeprecatedRule(
name=base.IDENTITY % 'get_group',
diff --git a/keystone/common/policies/identity_provider.py b/keystone/common/policies/identity_provider.py
index fb9fe75d0..2236d2aea 100644
--- a/keystone/common/policies/identity_provider.py
+++ b/keystone/common/policies/identity_provider.py
@@ -36,13 +36,9 @@ deprecated_delete_idp = policy.DeprecatedRule(
check_str=base.RULE_ADMIN_REQUIRED
)
-DEPRECATED_REASON = """
-As of the Stein release, the identity provider API now understands default
-roles and system-scoped tokens, making the API more granular by default without
-compromising security. The new policy defaults account for these changes
-automatically. Be sure to take these new defaults into consideration if you are
-relying on overrides in your deployment for the identity provider API.
-"""
+DEPRECATED_REASON = (
+ "The identity provider API is now aware of system scope and default roles."
+)
identity_provider_policies = [
policy.DocumentedRuleDefault(
diff --git a/keystone/common/policies/implied_role.py b/keystone/common/policies/implied_role.py
index c2c4c8aaf..6d164b035 100644
--- a/keystone/common/policies/implied_role.py
+++ b/keystone/common/policies/implied_role.py
@@ -40,13 +40,9 @@ deprecated_delete_implied_role = policy.DeprecatedRule(
check_str=base.RULE_ADMIN_REQUIRED,
)
-DEPRECATED_REASON = """
-As of the Train release, the implied role API understands how to
-handle system-scoped tokens in addition to project tokens, making the API
-more accessible to users without compromising security or manageability for
-administrators. The new default policies for this API account for these changes
-automatically.
-"""
+DEPRECATED_REASON = (
+ "The implied role API is now aware of system scope and default roles."
+)
implied_role_policies = [
diff --git a/keystone/common/policies/limit.py b/keystone/common/policies/limit.py
index c334359d2..7fd3262f2 100644
--- a/keystone/common/policies/limit.py
+++ b/keystone/common/policies/limit.py
@@ -14,11 +14,23 @@ from oslo_policy import policy
from keystone.common.policies import base
+SYSTEM_OR_DOMAIN_OR_PROJECT_USER = (
+ '(' + base.SYSTEM_READER + ') or '
+ '('
+ 'domain_id:%(target.limit.domain.id)s or '
+ 'domain_id:%(target.limit.project.domain_id)s'
+ ') or '
+ '('
+ 'project_id:%(target.limit.project_id)s and not '
+ 'None:%(target.limit.project_id)s'
+ ')'
+)
+
limit_policies = [
policy.DocumentedRuleDefault(
name=base.IDENTITY % 'get_limit_model',
check_str='',
- scope_types=['system', 'project'],
+ scope_types=['system', 'domain', 'project'],
description='Get limit enforcement model.',
operations=[{'path': '/v3/limits/model',
'method': 'GET'},
@@ -26,10 +38,8 @@ limit_policies = [
'method': 'HEAD'}]),
policy.DocumentedRuleDefault(
name=base.IDENTITY % 'get_limit',
- check_str='(role:reader and system_scope:all) or '
- 'project_id:%(target.limit.project_id)s or '
- 'domain_id:%(target.limit.domain_id)s',
- scope_types=['system', 'project', 'domain'],
+ check_str=SYSTEM_OR_DOMAIN_OR_PROJECT_USER,
+ scope_types=['system', 'domain', 'project'],
description='Show limit details.',
operations=[{'path': '/v3/limits/{limit_id}',
'method': 'GET'},
@@ -38,7 +48,7 @@ limit_policies = [
policy.DocumentedRuleDefault(
name=base.IDENTITY % 'list_limits',
check_str='',
- scope_types=['system', 'project'],
+ scope_types=['system', 'domain', 'project'],
description='List limits.',
operations=[{'path': '/v3/limits',
'method': 'GET'},
diff --git a/keystone/common/policies/mapping.py b/keystone/common/policies/mapping.py
index d651a3b42..498bc7c84 100644
--- a/keystone/common/policies/mapping.py
+++ b/keystone/common/policies/mapping.py
@@ -36,13 +36,9 @@ deprecated_delete_mapping = policy.DeprecatedRule(
check_str=base.RULE_ADMIN_REQUIRED
)
-DEPRECATED_REASON = """
-As of the Stein release, the federated mapping API now understands default
-roles and system-scoped tokens, making the API more granular by default without
-compromising security. The new policy defaults account for these changes
-automatically. Be sure to take these new defaults into consideration if you are
-relying on overrides in your deployment for the federated mapping API.
-"""
+DEPRECATED_REASON = (
+ "The federated mapping API is now aware of system scope and default roles."
+)
mapping_policies = [
policy.DocumentedRuleDefault(
diff --git a/keystone/common/policies/policy.py b/keystone/common/policies/policy.py
index b5163f975..4c912f33c 100644
--- a/keystone/common/policies/policy.py
+++ b/keystone/common/policies/policy.py
@@ -40,13 +40,9 @@ deprecated_delete_policy = policy.DeprecatedRule(
check_str=base.RULE_ADMIN_REQUIRED,
)
-DEPRECATED_REASON = """
-As of the Train release, the policy API now understands default roles and
-system-scoped tokens, making the API more granular by default without
-compromising security. The new policy defaults account for these changes
-automatically. Be sure to take these new defaults into consideration if you are
-relying on overrides in your deployment for the policy API.
-"""
+DEPRECATED_REASON = (
+ "The policy API is now aware of system scope and default roles."
+)
policy_policies = [
diff --git a/keystone/common/policies/policy_association.py b/keystone/common/policies/policy_association.py
index 9217d8647..e195d85c1 100644
--- a/keystone/common/policies/policy_association.py
+++ b/keystone/common/policies/policy_association.py
@@ -74,13 +74,9 @@ deprecated_delete_policy_association_for_region_and_service = policy.DeprecatedR
check_str=base.RULE_ADMIN_REQUIRED,
)
-DEPRECATED_REASON = """
-As of the Train release, the policy association API now understands default
-roles and system-scoped tokens, making the API more granular by default without
-compromising security. The new policy defaults account for these changes
-automatically. Be sure to take these new defaults into consideration if you are
-relying on overrides in your deployment for the policy association API.
-"""
+DEPRECATED_REASON = (
+ "The policy association API is now aware of system scope and default roles."
+)
policy_association_policies = [
policy.DocumentedRuleDefault(
diff --git a/keystone/common/policies/project.py b/keystone/common/policies/project.py
index cde64a93b..c7b7c0a9d 100644
--- a/keystone/common/policies/project.py
+++ b/keystone/common/policies/project.py
@@ -21,6 +21,12 @@ SYSTEM_READER_OR_DOMAIN_READER_OR_PROJECT_USER = (
'project_id:%(target.project.id)s'
)
+SYSTEM_ADMIN_OR_DOMAIN_ADMIN_OR_PROJECT_ADMIN = (
+ '(' + base.SYSTEM_ADMIN + ') or '
+ '(role:admin and domain_id:%(target.project.domain_id)s) or '
+ '(role:admin and project_id:%(target.project.id)s)'
+)
+
# This policy is only written to be used to protect the
# /v3/users/{user_id}/projects API. It should not be used to protect
# /v3/project APIs because the target information contained in the last check
@@ -70,9 +76,38 @@ deprecated_delete_project = policy.DeprecatedRule(
name=base.IDENTITY % 'delete_project',
check_str=base.RULE_ADMIN_REQUIRED
)
+deprecated_list_project_tags = policy.DeprecatedRule(
+ name=base.IDENTITY % 'list_project_tags',
+ check_str=base.RULE_ADMIN_OR_TARGET_PROJECT
+)
+deprecated_get_project_tag = policy.DeprecatedRule(
+ name=base.IDENTITY % 'get_project_tag',
+ check_str=base.RULE_ADMIN_OR_TARGET_PROJECT
+)
+deprecated_update_project_tag = policy.DeprecatedRule(
+ name=base.IDENTITY % 'update_project_tags',
+ check_str=base.RULE_ADMIN_REQUIRED
+)
+deprecated_create_project_tag = policy.DeprecatedRule(
+ name=base.IDENTITY % 'create_project_tag',
+ check_str=base.RULE_ADMIN_REQUIRED
+)
+deprecated_delete_project_tag = policy.DeprecatedRule(
+ name=base.IDENTITY % 'delete_project_tag',
+ check_str=base.RULE_ADMIN_REQUIRED
+)
+deprecated_delete_project_tags = policy.DeprecatedRule(
+ name=base.IDENTITY % 'delete_project_tags',
+ check_str=base.RULE_ADMIN_REQUIRED
+)
-DEPRECATED_REASON = """
-As of the Stein release, the project API understands how to handle
+
+DEPRECATED_REASON = (
+ "The project API is now aware of system scope and default roles."
+)
+
+TAGS_DEPRECATED_REASON = """
+As of the Train release, the project tags API understands how to handle
system-scoped tokens in addition to project and domain tokens, making the API
more accessible to users without compromising security or manageability for
administrators. The new default policies for this API account for these changes
@@ -146,67 +181,68 @@ project_policies = [
deprecated_since=versionutils.deprecated.STEIN),
policy.DocumentedRuleDefault(
name=base.IDENTITY % 'list_project_tags',
- check_str=base.RULE_ADMIN_OR_TARGET_PROJECT,
- # FIXME(lbragstad): We need to make sure we check the project in the
- # token scope when authorizing APIs for project tags. System
- # administrators should be able to tag any project with anything.
- # Domain administrators should only be able to tag projects within
- # their domain. Project administrators should only be able to tag their
- # project. Until we have support for these cases in code and tested, we
- # should keep scope_types commented out.
- # scope_types=['system', 'project'],
+ check_str=SYSTEM_READER_OR_DOMAIN_READER_OR_PROJECT_USER,
+ scope_types=['system', 'domain', 'project'],
description='List tags for a project.',
operations=[{'path': '/v3/projects/{project_id}/tags',
'method': 'GET'},
{'path': '/v3/projects/{project_id}/tags',
- 'method': 'HEAD'}]),
+ 'method': 'HEAD'}],
+ deprecated_rule=deprecated_list_project_tags,
+ deprecated_reason=TAGS_DEPRECATED_REASON,
+ deprecated_since=versionutils.deprecated.TRAIN),
policy.DocumentedRuleDefault(
name=base.IDENTITY % 'get_project_tag',
- check_str=base.RULE_ADMIN_OR_TARGET_PROJECT,
- # FIXME(lbragstad): See the above comments as to why this is commented
- # out.
- # scope_types=['system', 'project'],
+ check_str=SYSTEM_READER_OR_DOMAIN_READER_OR_PROJECT_USER,
+ scope_types=['system', 'domain', 'project'],
description='Check if project contains a tag.',
operations=[{'path': '/v3/projects/{project_id}/tags/{value}',
'method': 'GET'},
{'path': '/v3/projects/{project_id}/tags/{value}',
- 'method': 'HEAD'}]),
+ 'method': 'HEAD'}],
+ deprecated_rule=deprecated_get_project_tag,
+ deprecated_reason=TAGS_DEPRECATED_REASON,
+ deprecated_since=versionutils.deprecated.TRAIN),
policy.DocumentedRuleDefault(
name=base.IDENTITY % 'update_project_tags',
- check_str=base.RULE_ADMIN_REQUIRED,
- # FIXME(lbragstad): See the above comment for create_project as to why
- # this is limited to only system-scope.
- scope_types=['system'],
+ check_str=SYSTEM_ADMIN_OR_DOMAIN_ADMIN_OR_PROJECT_ADMIN,
+ scope_types=['system', 'domain', 'project'],
description='Replace all tags on a project with the new set of tags.',
operations=[{'path': '/v3/projects/{project_id}/tags',
- 'method': 'PUT'}]),
+ 'method': 'PUT'}],
+ deprecated_rule=deprecated_update_project_tag,
+ deprecated_reason=TAGS_DEPRECATED_REASON,
+ deprecated_since=versionutils.deprecated.TRAIN),
policy.DocumentedRuleDefault(
name=base.IDENTITY % 'create_project_tag',
- check_str=base.RULE_ADMIN_REQUIRED,
- # FIXME(lbragstad): See the above comment for create_project as to why
- # this is limited to only system-scope.
- scope_types=['system'],
+ check_str=SYSTEM_ADMIN_OR_DOMAIN_ADMIN_OR_PROJECT_ADMIN,
+ scope_types=['system', 'domain', 'project'],
description='Add a single tag to a project.',
operations=[{'path': '/v3/projects/{project_id}/tags/{value}',
- 'method': 'PUT'}]),
+ 'method': 'PUT'}],
+ deprecated_rule=deprecated_create_project_tag,
+ deprecated_reason=TAGS_DEPRECATED_REASON,
+ deprecated_since=versionutils.deprecated.TRAIN),
policy.DocumentedRuleDefault(
name=base.IDENTITY % 'delete_project_tags',
- check_str=base.RULE_ADMIN_REQUIRED,
- # FIXME(lbragstad): See the above comment for create_project as to why
- # this is limited to only system-scope.
- scope_types=['system'],
+ check_str=SYSTEM_ADMIN_OR_DOMAIN_ADMIN_OR_PROJECT_ADMIN,
+ scope_types=['system', 'domain', 'project'],
description='Remove all tags from a project.',
operations=[{'path': '/v3/projects/{project_id}/tags',
- 'method': 'DELETE'}]),
+ 'method': 'DELETE'}],
+ deprecated_rule=deprecated_delete_project_tags,
+ deprecated_reason=TAGS_DEPRECATED_REASON,
+ deprecated_since=versionutils.deprecated.TRAIN),
policy.DocumentedRuleDefault(
name=base.IDENTITY % 'delete_project_tag',
- check_str=base.RULE_ADMIN_REQUIRED,
- # FIXME(lbragstad): See the above comment for create_project as to why
- # this is limited to only system-scope.
- scope_types=['system'],
+ check_str=SYSTEM_ADMIN_OR_DOMAIN_ADMIN_OR_PROJECT_ADMIN,
+ scope_types=['system', 'domain', 'project'],
description='Delete a specified tag from project.',
operations=[{'path': '/v3/projects/{project_id}/tags/{value}',
- 'method': 'DELETE'}])
+ 'method': 'DELETE'}],
+ deprecated_rule=deprecated_delete_project_tag,
+ deprecated_reason=TAGS_DEPRECATED_REASON,
+ deprecated_since=versionutils.deprecated.TRAIN)
]
diff --git a/keystone/common/policies/project_endpoint.py b/keystone/common/policies/project_endpoint.py
index bc7bb77bb..c04cddd4d 100644
--- a/keystone/common/policies/project_endpoint.py
+++ b/keystone/common/policies/project_endpoint.py
@@ -10,15 +10,50 @@
# License for the specific language governing permissions and limitations
# under the License.
+from oslo_log import versionutils
from oslo_policy import policy
from keystone.common.policies import base
+deprecated_list_projects_for_endpoint = policy.DeprecatedRule(
+ name=base.IDENTITY % 'list_projects_for_endpoint',
+ check_str=base.RULE_ADMIN_REQUIRED,
+)
+
+deprecated_add_endpoint_to_project = policy.DeprecatedRule(
+ name=base.IDENTITY % 'add_endpoint_to_project',
+ check_str=base.RULE_ADMIN_REQUIRED,
+)
+
+deprecated_check_endpoint_in_project = policy.DeprecatedRule(
+ name=base.IDENTITY % 'check_endpoint_in_project',
+ check_str=base.RULE_ADMIN_REQUIRED,
+)
+
+deprecated_list_endpoints_for_project = policy.DeprecatedRule(
+ name=base.IDENTITY % 'list_endpoints_for_project',
+ check_str=base.RULE_ADMIN_REQUIRED,
+)
+
+deprecated_remove_endpoint_from_project = policy.DeprecatedRule(
+ name=base.IDENTITY % 'remove_endpoint_from_project',
+ check_str=base.RULE_ADMIN_REQUIRED,
+)
+
+DEPRECATED_REASON = """
+As of the Train release, the project endpoint API now understands default
+roles and system-scoped tokens, making the API more granular by default without
+compromising security. The new policy defaults account for these changes
+automatically. Be sure to take these new defaults into consideration if you are
+relying on overrides in your deployment for the project endpoint API.
+"""
+
+
project_endpoint_policies = [
policy.DocumentedRuleDefault(
name=base.IDENTITY % 'list_projects_for_endpoint',
- check_str=base.RULE_ADMIN_REQUIRED,
+ 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
@@ -27,18 +62,24 @@ project_endpoint_policies = [
description='List projects allowed to access an endpoint.',
operations=[{'path': ('/v3/OS-EP-FILTER/endpoints/{endpoint_id}/'
'projects'),
- 'method': 'GET'}]),
+ 'method': 'GET'}],
+ deprecated_rule=deprecated_list_projects_for_endpoint,
+ deprecated_reason=DEPRECATED_REASON,
+ deprecated_since=versionutils.deprecated.TRAIN),
policy.DocumentedRuleDefault(
name=base.IDENTITY % 'add_endpoint_to_project',
- check_str=base.RULE_ADMIN_REQUIRED,
+ check_str=base.SYSTEM_ADMIN,
scope_types=['system'],
description='Allow project to access an endpoint.',
operations=[{'path': ('/v3/OS-EP-FILTER/projects/{project_id}/'
'endpoints/{endpoint_id}'),
- 'method': 'PUT'}]),
+ 'method': 'PUT'}],
+ deprecated_rule=deprecated_add_endpoint_to_project,
+ deprecated_reason=DEPRECATED_REASON,
+ deprecated_since=versionutils.deprecated.TRAIN),
policy.DocumentedRuleDefault(
name=base.IDENTITY % 'check_endpoint_in_project',
- check_str=base.RULE_ADMIN_REQUIRED,
+ check_str=base.SYSTEM_READER,
scope_types=['system'],
description='Check if a project is allowed to access an endpoint.',
operations=[{'path': ('/v3/OS-EP-FILTER/projects/{project_id}/'
@@ -46,24 +87,33 @@ project_endpoint_policies = [
'method': 'GET'},
{'path': ('/v3/OS-EP-FILTER/projects/{project_id}/'
'endpoints/{endpoint_id}'),
- 'method': 'HEAD'}]),
+ 'method': 'HEAD'}],
+ deprecated_rule=deprecated_check_endpoint_in_project,
+ deprecated_reason=DEPRECATED_REASON,
+ deprecated_since=versionutils.deprecated.TRAIN),
policy.DocumentedRuleDefault(
name=base.IDENTITY % 'list_endpoints_for_project',
- check_str=base.RULE_ADMIN_REQUIRED,
+ check_str=base.SYSTEM_READER,
scope_types=['system'],
description='List the endpoints a project is allowed to access.',
operations=[{'path': ('/v3/OS-EP-FILTER/projects/{project_id}/'
'endpoints'),
- 'method': 'GET'}]),
+ 'method': 'GET'}],
+ deprecated_rule=deprecated_list_endpoints_for_project,
+ deprecated_reason=DEPRECATED_REASON,
+ deprecated_since=versionutils.deprecated.TRAIN),
policy.DocumentedRuleDefault(
name=base.IDENTITY % 'remove_endpoint_from_project',
- check_str=base.RULE_ADMIN_REQUIRED,
+ check_str=base.SYSTEM_ADMIN,
scope_types=['system'],
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}/'
'endpoints/{endpoint_id}'),
- 'method': 'DELETE'}])
+ 'method': 'DELETE'}],
+ deprecated_rule=deprecated_remove_endpoint_from_project,
+ deprecated_reason=DEPRECATED_REASON,
+ deprecated_since=versionutils.deprecated.TRAIN),
]
diff --git a/keystone/common/policies/protocol.py b/keystone/common/policies/protocol.py
index f57103e18..de2a7299e 100644
--- a/keystone/common/policies/protocol.py
+++ b/keystone/common/policies/protocol.py
@@ -36,13 +36,10 @@ deprecated_delete_protocol = policy.DeprecatedRule(
check_str=base.RULE_ADMIN_REQUIRED
)
-DEPRECATED_REASON = """
-As of the Stein release, the federated protocol API now understands default
-roles and system-scoped tokens, making the API more granular by default without
-compromising security. The new policy defaults account for these changes
-automatically. Be sure to take these new defaults into consideration if you are
-relying on overrides in your deployment for the protocol API.
-"""
+DEPRECATED_REASON = (
+ "The federated protocol API is now aware of system scope and default "
+ "roles."
+)
protocol_policies = [
policy.DocumentedRuleDefault(
diff --git a/keystone/common/policies/region.py b/keystone/common/policies/region.py
index 5b816e87a..bf60f8ff9 100644
--- a/keystone/common/policies/region.py
+++ b/keystone/common/policies/region.py
@@ -29,12 +29,7 @@ deprecated_delete_region = policy.DeprecatedRule(
)
DEPRECATED_REASON = (
- 'As of the Stein release, the region API now understands default roles '
- 'and system-scoped tokens, making the API more granular without '
- 'compromising security. The new policies for this API account for these '
- 'changes automatically. Be sure to take these new defaults into '
- 'consideration if you are relying on overrides in your deployment for the '
- 'region API.'
+ "The region API is now aware of system scope and default roles."
)
region_policies = [
diff --git a/keystone/common/policies/role.py b/keystone/common/policies/role.py
index 571ebdabf..7d6a38e46 100644
--- a/keystone/common/policies/role.py
+++ b/keystone/common/policies/role.py
@@ -56,13 +56,9 @@ deprecated_delete_domain_role = policy.DeprecatedRule(
check_str=base.RULE_ADMIN_REQUIRED
)
-DEPRECATED_REASON = """
-As of the Stein release, the role API now understands default roles and
-system-scoped tokens, making the API more granular by default without
-compromising security. The new policy defaults account for these changes
-automatically. Be sure to take these new defaults into consideration if you are
-relying on overrides in your deployment for the role API.
-"""
+DEPRECATED_REASON = (
+ "The role API is now aware of system scope and default roles."
+)
role_policies = [
policy.DocumentedRuleDefault(
diff --git a/keystone/common/policies/role_assignment.py b/keystone/common/policies/role_assignment.py
index e0dc9d777..c70f292f3 100644
--- a/keystone/common/policies/role_assignment.py
+++ b/keystone/common/policies/role_assignment.py
@@ -19,19 +19,24 @@ SYSTEM_READER_OR_DOMAIN_READER = (
'(' + base.SYSTEM_READER + ') or '
'(role:reader and domain_id:%(target.domain_id)s)'
)
+SYSTEM_READER_OR_PROJECT_DOMAIN_READER_OR_PROJECT_ADMIN = (
+ '(' + base.SYSTEM_READER + ') or '
+ '(role:reader and domain_id:%(target.project.domain_id)s) or '
+ '(role:admin and project_id:%(target.project.id)s)'
+)
deprecated_list_role_assignments = policy.DeprecatedRule(
name=base.IDENTITY % 'list_role_assignments',
check_str=base.RULE_ADMIN_REQUIRED
)
+deprecated_list_role_assignments_for_tree = policy.DeprecatedRule(
+ name=base.IDENTITY % 'list_role_assignments_for_tree',
+ check_str=base.RULE_ADMIN_REQUIRED
+)
-DEPRECATED_REASON = """
-As of the Stein release, the role assignment API now understands how to
-handle system-scoped tokens in addition to project-scoped tokens, making
-the API more accessible to users without compromising security or
-manageability for administrators. The new default policies for this API
-account for these changes automatically.
-"""
+DEPRECATED_REASON = (
+ "The assignment API is now aware of system scope and default roles."
+)
role_assignment_policies = [
policy.DocumentedRuleDefault(
@@ -48,21 +53,18 @@ role_assignment_policies = [
deprecated_since=versionutils.deprecated.STEIN),
policy.DocumentedRuleDefault(
name=base.IDENTITY % 'list_role_assignments_for_tree',
- check_str=base.RULE_ADMIN_REQUIRED,
- # NOTE(lbragstad): This is purely a project-scoped operation. The
- # project tree is calculated based on the project scope of the token
- # used to make the request. System administrators would have to find a
- # way to supply a project scope with a system-scoped token, which
- # defeats the purpose. System administrators can list all role
- # assignments anyway, so the usefulness of an API that returns a subset
- # is negligible when they have access to the entire set.
- scope_types=['project'],
+ check_str=SYSTEM_READER_OR_PROJECT_DOMAIN_READER_OR_PROJECT_ADMIN,
+ scope_types=['system', 'domain', 'project'],
description=('List all role assignments for a given tree of '
'hierarchical projects.'),
operations=[{'path': '/v3/role_assignments?include_subtree',
'method': 'GET'},
{'path': '/v3/role_assignments?include_subtree',
- 'method': 'HEAD'}])
+ 'method': 'HEAD'}],
+ deprecated_rule=deprecated_list_role_assignments_for_tree,
+ deprecated_reason=DEPRECATED_REASON,
+ deprecated_since=versionutils.deprecated.TRAIN),
+
]
diff --git a/keystone/common/policies/service.py b/keystone/common/policies/service.py
index ef433e604..66d3aaa72 100644
--- a/keystone/common/policies/service.py
+++ b/keystone/common/policies/service.py
@@ -36,13 +36,9 @@ deprecated_delete_service = policy.DeprecatedRule(
check_str=base.RULE_ADMIN_REQUIRED
)
-DEPRECATED_REASON = """
-As of the Stein release, the service API now understands default roles and
-system-scoped tokens, making the API more granular by default without
-compromising security. The new policy defaults account for these changes
-automatically. Be sure to take these new defaults into consideration if you are
-relying on overrides in your deployment for the service API.
-"""
+DEPRECATED_REASON = (
+ "The service API is now aware of system scope and default roles."
+)
service_policies = [
policy.DocumentedRuleDefault(
diff --git a/keystone/common/policies/service_provider.py b/keystone/common/policies/service_provider.py
index 04bc032de..4d0e3cb90 100644
--- a/keystone/common/policies/service_provider.py
+++ b/keystone/common/policies/service_provider.py
@@ -36,13 +36,9 @@ deprecated_delete_sp = policy.DeprecatedRule(
check_str=base.RULE_ADMIN_REQUIRED
)
-DEPRECATED_REASON = """
-As of the Stein release, the service provider API now understands default
-roles and system-scoped tokens, making the API more granular by default without
-compromising security. The new policy defaults account for these changes
-automatically. Be sure to take these new defaults into consideration if you are
-relying on overrides in your deployment for the service provider API.
-"""
+DEPRECATED_REASON = (
+ "The service provider API is now aware of system scope and default roles."
+)
service_provider_policies = [
policy.DocumentedRuleDefault(
diff --git a/keystone/common/policies/token.py b/keystone/common/policies/token.py
index 6db9913ec..9fa3c52f1 100644
--- a/keystone/common/policies/token.py
+++ b/keystone/common/policies/token.py
@@ -15,12 +15,9 @@ from oslo_policy import policy
from keystone.common.policies import base
-DEPRECATED_REASON = """
-As of the Train release, the token API now understands how to handle
-system-scoped tokens, making the API more accessible to users without
-compromising security or manageability for administrators. This support
-includes a read-only role by default.
-"""
+DEPRECATED_REASON = (
+ "The token API is now aware of system scope and default roles."
+)
deprecated_check_token = policy.DeprecatedRule(
name=base.IDENTITY % 'check_token',
diff --git a/keystone/common/policies/trust.py b/keystone/common/policies/trust.py
index 96fb698e5..4e9c7f4e1 100644
--- a/keystone/common/policies/trust.py
+++ b/keystone/common/policies/trust.py
@@ -45,13 +45,9 @@ deprecated_get_trust = policy.DeprecatedRule(
check_str=RULE_TRUSTOR + ' or ' + RULE_TRUSTEE
)
-DEPRECATED_REASON = """
-As of the Train release, the trust API now understands default roles and
-system-scoped tokens, making the API more granular by default without
-compromising security. The new policy defaults account for these changes
-automatically. Be sure to take these new defaults into consideration if you are
-relying on overrides in your deployment for the service API.
-"""
+DEPRECATED_REASON = (
+ "The trust API is now aware of system scope and default roles."
+)
trust_policies = [
policy.DocumentedRuleDefault(
diff --git a/keystone/common/policies/user.py b/keystone/common/policies/user.py
index 7d1743f77..75a0062cf 100644
--- a/keystone/common/policies/user.py
+++ b/keystone/common/policies/user.py
@@ -30,12 +30,9 @@ SYSTEM_ADMIN_OR_DOMAIN_ADMIN = (
'(role:admin and token.domain.id:%(target.user.domain_id)s)'
)
-DEPRECATED_REASON = """
-As of the Stein release, the user API understands how to handle system-scoped
-tokens in addition to project and domain tokens, making the API more accessible
-to users without compromising security or manageability for administrators. The
-new default policies for this API account for these changes automatically.
-"""
+DEPRECATED_REASON = (
+ "The user API is now aware of system scope and default roles."
+)
deprecated_get_user = policy.DeprecatedRule(
name=base.IDENTITY % 'get_user',
diff --git a/keystone/common/rbac_enforcer/enforcer.py b/keystone/common/rbac_enforcer/enforcer.py
index cb5034b06..b8a50fbd0 100644
--- a/keystone/common/rbac_enforcer/enforcer.py
+++ b/keystone/common/rbac_enforcer/enforcer.py
@@ -53,6 +53,40 @@ class RBACEnforcer(object):
# BORG pattern.
self.__dict__ = self.__shared_state__
+ def _check_deprecated_rule(self, action):
+ def _name_is_changing(rule):
+ deprecated_rule = rule.deprecated_rule
+ return (deprecated_rule and
+ deprecated_rule.name != rule.name and
+ deprecated_rule.name in self._enforcer.file_rules)
+
+ def _check_str_is_changing(rule):
+ deprecated_rule = rule.deprecated_rule
+ return (deprecated_rule and
+ deprecated_rule.check_str != rule.check_str and
+ rule.name not in self._enforcer.file_rules)
+
+ def _is_deprecated_for_removal(rule):
+ return (rule.deprecated_for_removal and
+ rule.name in self._enforcer.file_rules)
+
+ def _emit_warning():
+ if not self._enforcer._warning_emitted:
+ LOG.warning("Deprecated policy rules found. Use "
+ "oslopolicy-policy-generator and "
+ "oslopolicy-policy-upgrade to detect and resolve "
+ "deprecated policies in your configuration.")
+ self._enforcer._warning_emitted = True
+
+ registered_rule = self._enforcer.registered_rules.get(action)
+
+ if not registered_rule:
+ return
+ if (_name_is_changing(registered_rule) or
+ _check_str_is_changing(registered_rule) or
+ _is_deprecated_for_removal(registered_rule)):
+ _emit_warning()
+
def _enforce(self, credentials, action, target, do_raise=True):
"""Verify that the action is valid on the target in this context.
@@ -80,8 +114,10 @@ class RBACEnforcer(object):
do_raise=do_raise)
try:
- return self._enforcer.enforce(
+ result = self._enforcer.enforce(
rule=action, target=target, creds=credentials, **extra)
+ self._check_deprecated_rule(action)
+ return result
except common_policy.InvalidScope:
raise exception.ForbiddenAction(action=action)
@@ -94,9 +130,22 @@ class RBACEnforcer(object):
# The raw oslo-policy enforcer object
if self.__ENFORCER is None:
self.__ENFORCER = common_policy.Enforcer(CONF)
+ # NOTE(cmurphy) when running in the keystone server, suppress
+ # deprecation warnings for individual policy rules. Instead, we log
+ # a single notification at enforcement time indicating the
+ # oslo.policy tools the operator can use to detect and resolve
+ # deprecated policies. If there is no request context here, that
+ # means external tooling such as the oslo.policy tools are running
+ # this code, in which case we do want the full deprecation warnings
+ # emitted for individual polcy rules.
+ if flask.has_request_context():
+ self.__ENFORCER.suppress_deprecation_warnings = True
+ # NOTE(cmurphy) Tests may explicitly disable these warnings to
+ # prevent an explosion of test logs
if self.suppress_deprecation_warnings:
self.__ENFORCER.suppress_deprecation_warnings = True
self.register_rules(self.__ENFORCER)
+ self.__ENFORCER._warning_emitted = False
return self.__ENFORCER
@staticmethod
diff --git a/keystone/common/sql/upgrades.py b/keystone/common/sql/upgrades.py
index c45993bfc..91a11995b 100644
--- a/keystone/common/sql/upgrades.py
+++ b/keystone/common/sql/upgrades.py
@@ -231,8 +231,9 @@ def offline_sync_database_to_version(version=None):
def get_db_version(repo=LEGACY_REPO):
with sql.session_for_read() as session:
+ repo = find_repo(repo)
return migration.db_version(
- session.get_bind(), find_repo(repo), get_init_version())
+ session.get_bind(), repo, get_init_version(repo))
def validate_upgrade_order(repo_name, target_repo_version=None):
diff --git a/keystone/credential/backends/sql.py b/keystone/credential/backends/sql.py
index e499e2a86..561874907 100644
--- a/keystone/credential/backends/sql.py
+++ b/keystone/credential/backends/sql.py
@@ -13,6 +13,8 @@
# under the License.
from oslo_db import api as oslo_db_api
+import six
+from sqlalchemy.ext.hybrid import hybrid_property
from keystone.common import driver_hints
from keystone.common import sql
@@ -29,11 +31,24 @@ class CredentialModel(sql.ModelBase, sql.ModelDictMixinWithExtras):
user_id = sql.Column(sql.String(64),
nullable=False)
project_id = sql.Column(sql.String(64))
- encrypted_blob = sql.Column(sql.Text(), nullable=True)
+ _encrypted_blob = sql.Column('encrypted_blob', sql.Text(), nullable=True)
type = sql.Column(sql.String(255), nullable=False)
key_hash = sql.Column(sql.String(64), nullable=True)
extra = sql.Column(sql.JsonBlob())
+ @hybrid_property
+ def encrypted_blob(self):
+ return self._encrypted_blob
+
+ @encrypted_blob.setter
+ def encrypted_blob(self, encrypted_blob):
+ # Make sure to hand over the encrypted credential as a string value
+ # to the backend driver to avoid the sql drivers (esp. psycopg2)
+ # treating this as binary data and e.g. hex-escape it.
+ if six.PY3 and isinstance(encrypted_blob, six.binary_type):
+ encrypted_blob = encrypted_blob.decode('utf-8')
+ self._encrypted_blob = encrypted_blob
+
class Credential(base.CredentialDriverBase):
diff --git a/keystone/federation/utils.py b/keystone/federation/utils.py
index 92028fa7d..86bc6569d 100644
--- a/keystone/federation/utils.py
+++ b/keystone/federation/utils.py
@@ -243,6 +243,10 @@ class DirectMaps(object):
def __init__(self):
self._matches = []
+ def __str__(self):
+ """return the direct map array as a string."""
+ return '%s' % self._matches
+
def add(self, values):
"""Add a matched value to the list of matches.
diff --git a/keystone/identity/backends/ldap/common.py b/keystone/identity/backends/ldap/common.py
index 3036bc4c5..77cc24fd3 100644
--- a/keystone/identity/backends/ldap/common.py
+++ b/keystone/identity/backends/ldap/common.py
@@ -1781,6 +1781,7 @@ class EnabledEmuMixIn(BaseLdap):
DEFAULT_GROUP_OBJECTCLASS = 'groupOfNames'
DEFAULT_MEMBER_ATTRIBUTE = 'member'
+ DEFAULT_GROUP_MEMBERS_ARE_IDS = False
def __init__(self, conf):
super(EnabledEmuMixIn, self).__init__(conf)
@@ -1797,9 +1798,11 @@ class EnabledEmuMixIn(BaseLdap):
if not self.use_group_config:
self.member_attribute = self.DEFAULT_MEMBER_ATTRIBUTE
self.group_objectclass = self.DEFAULT_GROUP_OBJECTCLASS
+ self.group_members_are_ids = self.DEFAULT_GROUP_MEMBERS_ARE_IDS
else:
self.member_attribute = conf.ldap.group_member_attribute
self.group_objectclass = conf.ldap.group_objectclass
+ self.group_members_are_ids = conf.ldap.group_members_are_ids
if not self.enabled_emulation_dn:
naming_attr_name = 'cn'
@@ -1815,8 +1818,13 @@ class EnabledEmuMixIn(BaseLdap):
naming_rdn[1])
self.enabled_emulation_naming_attr = naming_attr
+ # TODO(yoctozepto): methods below use _id_to_dn which requests another LDAP connection - optimize it
+
def _get_enabled(self, object_id, conn):
- dn = self._id_to_dn(object_id)
+ if self.group_members_are_ids:
+ dn = object_id
+ else:
+ dn = self._id_to_dn(object_id)
query = '(%s=%s)' % (self.member_attribute,
ldap.filter.escape_filter_chars(dn))
try:
@@ -1829,24 +1837,33 @@ class EnabledEmuMixIn(BaseLdap):
return bool(enabled_value)
def _add_enabled(self, object_id):
+ if self.group_members_are_ids:
+ dn = object_id
+ else:
+ dn = self._id_to_dn(object_id)
with self.get_connection() as conn:
+ # TODO(yoctozepto): _get_enabled potentially calls _id_to_dn 2nd time - optimize it
if not self._get_enabled(object_id, conn):
modlist = [(ldap.MOD_ADD,
self.member_attribute,
- [self._id_to_dn(object_id)])]
+ [dn])]
try:
conn.modify_s(self.enabled_emulation_dn, modlist)
except ldap.NO_SUCH_OBJECT:
attr_list = [('objectClass', [self.group_objectclass]),
(self.member_attribute,
- [self._id_to_dn(object_id)]),
+ [dn]),
self.enabled_emulation_naming_attr]
conn.add_s(self.enabled_emulation_dn, attr_list)
def _remove_enabled(self, object_id):
+ if self.group_members_are_ids:
+ dn = object_id
+ else:
+ dn = self._id_to_dn(object_id)
modlist = [(ldap.MOD_DELETE,
self.member_attribute,
- [self._id_to_dn(object_id)])]
+ [dn])]
with self.get_connection() as conn:
try:
conn.modify_s(self.enabled_emulation_dn, modlist)
diff --git a/keystone/notifications.py b/keystone/notifications.py
index 3f59d151e..11e4a5875 100644
--- a/keystone/notifications.py
+++ b/keystone/notifications.py
@@ -66,6 +66,7 @@ CADF_TYPE_MAP = {
'OS-OAUTH1:access_token': taxonomy.SECURITY_CREDENTIAL,
'OS-OAUTH1:request_token': taxonomy.SECURITY_CREDENTIAL,
'OS-OAUTH1:consumer': taxonomy.SECURITY_ACCOUNT,
+ 'application_credential': taxonomy.SECURITY_CREDENTIAL,
}
SAML_AUDIT_TYPE = 'http://docs.oasis-open.org/security/saml/v2.0'
diff --git a/keystone/server/flask/common.py b/keystone/server/flask/common.py
index 416b7106c..cee393e37 100644
--- a/keystone/server/flask/common.py
+++ b/keystone/server/flask/common.py
@@ -935,6 +935,8 @@ class ResourceBase(flask_restful.Resource):
return token_ref.domain_id
elif token_ref.project_scoped:
return token_ref.project_domain['id']
+ elif token_ref.system_scoped:
+ return
else:
msg = 'No domain information specified as part of list request'
tr_msg = _('No domain information specified as part of list '
diff --git a/keystone/tests/unit/protection/v3/test_access_rules.py b/keystone/tests/protection/v3/test_access_rules.py
index be0706a13..be0706a13 100644
--- a/keystone/tests/unit/protection/v3/test_access_rules.py
+++ b/keystone/tests/protection/v3/test_access_rules.py
diff --git a/keystone/tests/protection/v3/test_assignment.py b/keystone/tests/protection/v3/test_assignment.py
index a5bc13d44..d275ba45a 100644
--- a/keystone/tests/protection/v3/test_assignment.py
+++ b/keystone/tests/protection/v3/test_assignment.py
@@ -10,6 +10,7 @@
# License for the specific language governing permissions and limitations
# under the License.
+import copy
from six.moves import http_client
import uuid
@@ -653,6 +654,49 @@ class _SystemUserTests(object):
for assignment in actual:
self.assertIn(assignment, expected)
+ def test_user_can_list_assignments_for_subtree(self):
+ assignments = self._setup_test_role_assignments()
+ user = PROVIDERS.identity_api.create_user(
+ unit.new_user_ref(domain_id=CONF.identity.default_domain_id)
+ )
+ project = PROVIDERS.resource_api.create_project(
+ uuid.uuid4().hex,
+ unit.new_project_ref(domain_id=CONF.identity.default_domain_id,
+ parent_id=assignments['project_id'])
+ )
+ PROVIDERS.assignment_api.create_grant(
+ assignments['role_id'],
+ user_id=user['id'],
+ project_id=project['id']
+ )
+ expected = [
+ {
+ 'user_id': assignments['user_id'],
+ 'project_id': assignments['project_id'],
+ 'role_id': assignments['role_id']
+ },
+ {
+ 'group_id': assignments['group_id'],
+ 'project_id': assignments['project_id'],
+ 'role_id': assignments['role_id']
+ },
+ {
+ 'user_id': user['id'],
+ 'project_id': project['id'],
+ 'role_id': assignments['role_id']
+ }
+ ]
+ with self.test_client() as c:
+ r = c.get(
+ ('/v3/role_assignments?scope.project.id=%s&include_subtree' %
+ assignments['project_id']),
+ headers=self.headers
+ )
+ self.assertEqual(len(expected), len(r.json['role_assignments']))
+ actual = self._extract_role_assignments_from_response_body(r)
+ for assignment in actual:
+ self.assertIn(assignment, expected)
+
class _DomainUserTests(object):
"""Common functionality for domain users."""
@@ -922,6 +966,60 @@ class _DomainUserTests(object):
)
self.assertEqual(0, len(r.json['role_assignments']))
+ def test_user_can_list_assignments_for_subtree_in_their_domain(self):
+ assignments = self._setup_test_role_assignments()
+ domain_assignments = self._setup_test_role_assignments_for_domain()
+ user = PROVIDERS.identity_api.create_user(
+ unit.new_user_ref(domain_id=self.domain_id)
+ )
+ project = PROVIDERS.resource_api.create_project(
+ uuid.uuid4().hex,
+ unit.new_project_ref(domain_id=self.domain_id,
+ parent_id=domain_assignments['project_id'])
+ )
+ PROVIDERS.assignment_api.create_grant(
+ assignments['role_id'],
+ user_id=user['id'],
+ project_id=project['id']
+ )
+ expected = [
+ {
+ 'user_id': domain_assignments['user_id'],
+ 'project_id': domain_assignments['project_id'],
+ 'role_id': assignments['role_id']
+ },
+ {
+ 'group_id': domain_assignments['group_id'],
+ 'project_id': domain_assignments['project_id'],
+ 'role_id': assignments['role_id']
+ },
+ {
+ 'user_id': user['id'],
+ 'project_id': project['id'],
+ 'role_id': assignments['role_id']
+ }
+ ]
+ with self.test_client() as c:
+ r = c.get(
+ ('/v3/role_assignments?scope.project.id=%s&include_subtree' %
+ domain_assignments['project_id']),
+ headers=self.headers
+ )
+ self.assertEqual(len(expected), len(r.json['role_assignments']))
+ actual = self._extract_role_assignments_from_response_body(r)
+ for assignment in actual:
+ self.assertIn(assignment, expected)
+
+ def test_user_cannot_list_assignments_for_subtree_in_other_domain(self):
+ assignments = self._setup_test_role_assignments()
+ with self.test_client() as c:
+ c.get(
+ ('/v3/role_assignments?scope.project.id=%s&include_subtree' %
+ assignments['project_id']),
+ headers=self.headers,
+ expected_status_code=http_client.FORBIDDEN
+ )
+
class _ProjectUserTests(object):
@@ -1016,6 +1114,30 @@ class _ProjectUserTests(object):
)
+class _ProjectReaderMemberTests(object):
+ def test_user_cannot_list_assignments_for_subtree(self):
+ user = PROVIDERS.identity_api.create_user(
+ unit.new_user_ref(domain_id=self.domain_id)
+ )
+ project = PROVIDERS.resource_api.create_project(
+ uuid.uuid4().hex,
+ unit.new_project_ref(domain_id=self.domain_id,
+ parent_id=self.project_id)
+ )
+ PROVIDERS.assignment_api.create_grant(
+ self.bootstrapper.reader_role_id,
+ user_id=user['id'],
+ project_id=project['id']
+ )
+ with self.test_client() as c:
+ c.get(
+ ('/v3/role_assignments?scope.project.id=%s&include_subtree' %
+ self.project_id),
+ headers=self.headers,
+ expected_status_code=http_client.FORBIDDEN
+ )
+
+
class SystemReaderTests(base_classes.TestCaseWithBootstrap,
common_auth.AuthTestMixin,
_AssignmentTestUtilities,
@@ -1216,10 +1338,37 @@ class DomainAdminTests(base_classes.TestCaseWithBootstrap,
_AssignmentTestUtilities,
_DomainUserTests):
+ def _override_policy(self):
+ # TODO(lbragstad): Remove this once the deprecated policies in
+ # keystone.common.policies.role_assignment have been removed. This is
+ # only here to make sure we test the new policies instead of the
+ # deprecated ones. Oslo.policy will OR deprecated policies with new
+ # policies to maintain compatibility and give operators a chance to
+ # update permissions or update policies without breaking users. This
+ # will cause these specific tests to fail since we're trying to correct
+ # this broken behavior with better scope checking.
+ with open(self.policy_file_name, 'w') as f:
+ overridden_policies = {
+ 'identity:list_role_assignments': (
+ rp.SYSTEM_READER_OR_DOMAIN_READER
+ ),
+ 'identity:list_role_assignments_for_tree': (
+ rp.SYSTEM_READER_OR_PROJECT_DOMAIN_READER_OR_PROJECT_ADMIN
+ )
+ }
+ f.write(jsonutils.dumps(overridden_policies))
+
def setUp(self):
super(DomainAdminTests, self).setUp()
self.loadapp()
- self.useFixture(ksfixtures.Policy(self.config_fixture))
+ self.policy_file = self.useFixture(temporaryfile.SecureTempFile())
+ self.policy_file_name = self.policy_file.file_name
+ self.useFixture(
+ ksfixtures.Policy(
+ self.config_fixture, policy_file=self.policy_file_name
+ )
+ )
+ self._override_policy()
self.config_fixture.config(group='oslo_policy', enforce_scope=True)
domain = PROVIDERS.resource_api.create_domain(
@@ -1256,7 +1405,8 @@ class DomainAdminTests(base_classes.TestCaseWithBootstrap,
class ProjectReaderTests(base_classes.TestCaseWithBootstrap,
common_auth.AuthTestMixin,
_AssignmentTestUtilities,
- _ProjectUserTests):
+ _ProjectUserTests,
+ _ProjectReaderMemberTests):
def setUp(self):
super(ProjectReaderTests, self).setUp()
@@ -1304,7 +1454,8 @@ class ProjectReaderTests(base_classes.TestCaseWithBootstrap,
class ProjectMemberTests(base_classes.TestCaseWithBootstrap,
common_auth.AuthTestMixin,
_AssignmentTestUtilities,
- _ProjectUserTests):
+ _ProjectUserTests,
+ _ProjectReaderMemberTests):
def setUp(self):
super(ProjectMemberTests, self).setUp()
@@ -1394,7 +1545,7 @@ class ProjectAdminTests(base_classes.TestCaseWithBootstrap,
auth = self.build_authentication_request(
user_id=self.user_id,
password=self.bootstrapper.admin_password,
- project_id=self.bootstrapper.project_id
+ project_id=self.project_id
)
# Grab a token using the persona we're testing and prepare headers
@@ -1417,6 +1568,61 @@ class ProjectAdminTests(base_classes.TestCaseWithBootstrap,
overridden_policies = {
'identity:list_role_assignments': (
rp.SYSTEM_READER_OR_DOMAIN_READER
+ ),
+ 'identity:list_role_assignments_for_tree': (
+ rp.SYSTEM_READER_OR_PROJECT_DOMAIN_READER_OR_PROJECT_ADMIN
)
}
f.write(jsonutils.dumps(overridden_policies))
+
+ def test_user_can_list_assignments_for_subtree_on_own_project(self):
+ user = PROVIDERS.identity_api.create_user(
+ unit.new_user_ref(domain_id=self.domain_id)
+ )
+ project = PROVIDERS.resource_api.create_project(
+ uuid.uuid4().hex,
+ unit.new_project_ref(domain_id=self.domain_id,
+ parent_id=self.project_id)
+ )
+ PROVIDERS.assignment_api.create_grant(
+ self.bootstrapper.reader_role_id,
+ user_id=user['id'],
+ project_id=project['id']
+ )
+ expected = copy.copy(self.expected)
+ expected.append({
+ 'project_id': project['id'],
+ 'user_id': user['id'],
+ 'role_id': self.bootstrapper.reader_role_id
+ })
+ with self.test_client() as c:
+ r = c.get(
+ ('/v3/role_assignments?scope.project.id=%s&include_subtree' %
+ self.project_id),
+ headers=self.headers
+ )
+ self.assertEqual(len(expected), len(r.json['role_assignments']))
+ actual = self._extract_role_assignments_from_response_body(r)
+ for assignment in actual:
+ self.assertIn(assignment, expected)
+
+ def test_user_cannot_list_assignments_for_subtree_on_other_project(self):
+ user = PROVIDERS.identity_api.create_user(
+ unit.new_user_ref(domain_id=self.domain_id)
+ )
+ project = PROVIDERS.resource_api.create_project(
+ uuid.uuid4().hex,
+ unit.new_project_ref(domain_id=self.domain_id)
+ )
+ PROVIDERS.assignment_api.create_grant(
+ self.bootstrapper.reader_role_id,
+ user_id=user['id'],
+ project_id=project['id']
+ )
+ with self.test_client() as c:
+ c.get(
+ ('/v3/role_assignments?scope.project.id=%s&include_subtree' %
+ project['id']),
+ headers=self.headers,
+ expected_status_code=http_client.FORBIDDEN
+ )
diff --git a/keystone/tests/unit/protection/v3/test_consumer.py b/keystone/tests/protection/v3/test_consumer.py
index e7ec0bf1a..e7ec0bf1a 100644
--- a/keystone/tests/unit/protection/v3/test_consumer.py
+++ b/keystone/tests/protection/v3/test_consumer.py
diff --git a/keystone/tests/protection/v3/test_credentials.py b/keystone/tests/protection/v3/test_credentials.py
index b681e7ee5..b08022995 100644
--- a/keystone/tests/protection/v3/test_credentials.py
+++ b/keystone/tests/protection/v3/test_credentials.py
@@ -15,7 +15,7 @@ import uuid
from oslo_serialization import jsonutils
from six.moves import http_client
-from keystone.common.policies import credential as cp
+from keystone.common.policies import base as bp
from keystone.common import provider_api
import keystone.conf
from keystone.tests.common import auth as common_auth
@@ -1131,10 +1131,10 @@ class ProjectAdminTests(base_classes.TestCaseWithBootstrap,
# broken behavior with better scope checking.
with open(self.policy_file_name, 'w') as f:
overridden_policies = {
- 'identity:get_credential': cp.SYSTEM_READER_OR_CRED_OWNER,
- 'identity:list_credentials': cp.SYSTEM_READER_OR_CRED_OWNER,
- 'identity:create_credential': cp.SYSTEM_ADMIN_OR_CRED_OWNER,
- 'identity:update_credential': cp.SYSTEM_ADMIN_OR_CRED_OWNER,
- 'identity:delete_credential': cp.SYSTEM_ADMIN_OR_CRED_OWNER
+ '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
}
f.write(jsonutils.dumps(overridden_policies))
diff --git a/keystone/tests/unit/protection/v3/test_domain_config.py b/keystone/tests/protection/v3/test_domain_config.py
index 1c1a891e4..1c1a891e4 100644
--- a/keystone/tests/unit/protection/v3/test_domain_config.py
+++ b/keystone/tests/protection/v3/test_domain_config.py
diff --git a/keystone/tests/unit/protection/v3/test_domain_roles.py b/keystone/tests/protection/v3/test_domain_roles.py
index 86a19cf85..86a19cf85 100644
--- a/keystone/tests/unit/protection/v3/test_domain_roles.py
+++ b/keystone/tests/protection/v3/test_domain_roles.py
diff --git a/keystone/tests/unit/protection/v3/test_ec2_credential.py b/keystone/tests/protection/v3/test_ec2_credential.py
index cb6bf135f..f17279fc9 100644
--- a/keystone/tests/unit/protection/v3/test_ec2_credential.py
+++ b/keystone/tests/protection/v3/test_ec2_credential.py
@@ -14,7 +14,6 @@ from oslo_serialization import jsonutils
from six.moves import http_client
from keystone.common.policies import base as bp
-from keystone.common.policies import ec2_credential as ec
from keystone.common import provider_api
import keystone.conf
from keystone.tests.common import auth as common_auth
@@ -398,11 +397,11 @@ class ProjectAdminTests(base_classes.TestCaseWithBootstrap,
# this broken behavior with better scope checking.
with open(self.policy_file_name, 'w') as f:
overridden_policies = {
- 'identity:ec2_get_credential': ec.SYSTEM_READER_OR_CRED_OWNER,
+ 'identity:ec2_get_credential': bp.SYSTEM_READER_OR_CRED_OWNER,
'identity:ec2_list_credentials': bp.RULE_SYSTEM_READER_OR_OWNER,
- 'identity:ec2_create_credential': ec.SYSTEM_ADMIN_OR_CRED_OWNER,
- 'identity:ec2_update_credential': ec.SYSTEM_ADMIN_OR_CRED_OWNER,
- 'identity:ec2_delete_credential': ec.SYSTEM_ADMIN_OR_CRED_OWNER
+ 'identity:ec2_create_credential': bp.SYSTEM_ADMIN_OR_CRED_OWNER,
+ 'identity:ec2_update_credential': bp.SYSTEM_ADMIN_OR_CRED_OWNER,
+ 'identity:ec2_delete_credential': bp.SYSTEM_ADMIN_OR_CRED_OWNER
}
f.write(jsonutils.dumps(overridden_policies))
diff --git a/keystone/tests/unit/protection/v3/test_implied_roles.py b/keystone/tests/protection/v3/test_implied_roles.py
index a974852c0..a974852c0 100644
--- a/keystone/tests/unit/protection/v3/test_implied_roles.py
+++ b/keystone/tests/protection/v3/test_implied_roles.py
diff --git a/keystone/tests/protection/v3/test_limits.py b/keystone/tests/protection/v3/test_limits.py
index 210c0cfa2..e16906c3a 100644
--- a/keystone/tests/protection/v3/test_limits.py
+++ b/keystone/tests/protection/v3/test_limits.py
@@ -25,8 +25,11 @@ CONF = keystone.conf.CONF
PROVIDERS = provider_api.ProviderAPIs
-def _create_limit_and_dependencies():
- """Create a limit and its dependencies to test with."""
+def _create_limits_and_dependencies(domain_id=None):
+ """Create limits and its dependencies for testing."""
+ if not domain_id:
+ domain_id = CONF.identity.default_domain_id
+
service = PROVIDERS.catalog_api.create_service(
uuid.uuid4().hex, unit.new_service_ref()
)
@@ -41,18 +44,34 @@ def _create_limit_and_dependencies():
)
registered_limit = registered_limits[0]
+ domain_limit = unit.new_limit_ref(
+ domain_id=domain_id, service_id=service['id'],
+ resource_name=registered_limit['resource_name'],
+ resource_limit=10, id=uuid.uuid4().hex
+ )
+
project = PROVIDERS.resource_api.create_project(
- uuid.uuid4().hex,
- unit.new_project_ref(domain_id=CONF.identity.default_domain_id)
+ uuid.uuid4().hex, unit.new_project_ref(domain_id=domain_id)
)
- limit = unit.new_limit_ref(
+ project_limit = unit.new_limit_ref(
project_id=project['id'], service_id=service['id'],
resource_name=registered_limit['resource_name'],
resource_limit=5, id=uuid.uuid4().hex
)
- limits = PROVIDERS.unified_limit_api.create_limits([limit])
- return limits
+ limits = PROVIDERS.unified_limit_api.create_limits(
+ [domain_limit, project_limit]
+ )
+
+ project_limit_id = None
+ domain_limit_id = None
+ for limit in limits:
+ if limit.get('domain_id'):
+ domain_limit_id = limit['id']
+ else:
+ project_limit_id = limit['id']
+
+ return (project_limit_id, domain_limit_id)
class _UserLimitTests(object):
@@ -63,21 +82,24 @@ class _UserLimitTests(object):
c.get('/v3/limits/model', headers=self.headers)
def test_user_can_get_a_limit(self):
- limits = _create_limit_and_dependencies()
- limit = limits[0]
+ limit_id, _ = _create_limits_and_dependencies()
with self.test_client() as c:
- r = c.get('/v3/limits/%s' % limit['id'], headers=self.headers)
- self.assertEqual(limit['id'], r.json['limit']['id'])
+ r = c.get('/v3/limits/%s' % limit_id, headers=self.headers)
+ self.assertEqual(limit_id, r.json['limit']['id'])
def test_user_can_list_limits(self):
- limits = _create_limit_and_dependencies()
- limit = limits[0]
+ project_limit_id, domain_limit_id = _create_limits_and_dependencies()
with self.test_client() as c:
r = c.get('/v3/limits', headers=self.headers)
- self.assertTrue(len(r.json['limits']) == 1)
- self.assertEqual(limit['id'], r.json['limits'][0]['id'])
+ self.assertTrue(len(r.json['limits']) == 2)
+ result = []
+ for limit in r.json['limits']:
+ result.append(limit['id'])
+
+ self.assertIn(project_limit_id, result)
+ self.assertIn(domain_limit_id, result)
def test_user_cannot_create_limits(self):
service = PROVIDERS.catalog_api.create_service(
@@ -116,25 +138,23 @@ class _UserLimitTests(object):
)
def test_user_cannot_update_limits(self):
- limits = _create_limit_and_dependencies()
- limit = limits[0]
+ limit_id, _ = _create_limits_and_dependencies()
update = {'limits': {'description': uuid.uuid4().hex}}
with self.test_client() as c:
c.patch(
- '/v3/limits/%s' % limit['id'], json=update,
+ '/v3/limits/%s' % limit_id, json=update,
headers=self.headers,
expected_status_code=http_client.FORBIDDEN
)
def test_user_cannot_delete_limits(self):
- limits = _create_limit_and_dependencies()
- limit = limits[0]
+ limit_id, _ = _create_limits_and_dependencies()
with self.test_client() as c:
c.delete(
- '/v3/limits/%s' % limit['id'],
+ '/v3/limits/%s' % limit_id,
headers=self.headers,
expected_status_code=http_client.FORBIDDEN
)
@@ -231,21 +251,24 @@ class SystemAdminTests(base_classes.TestCaseWithBootstrap,
self.headers = {'X-Auth-Token': self.token_id}
def test_user_can_get_a_limit(self):
- limits = _create_limit_and_dependencies()
- limit = limits[0]
+ limit_id, _ = _create_limits_and_dependencies()
with self.test_client() as c:
- r = c.get('/v3/limits/%s' % limit['id'], headers=self.headers)
- self.assertEqual(limit['id'], r.json['limit']['id'])
+ r = c.get('/v3/limits/%s' % limit_id, headers=self.headers)
+ self.assertEqual(limit_id, r.json['limit']['id'])
def test_user_can_list_limits(self):
- limits = _create_limit_and_dependencies()
- limit = limits[0]
+ project_limit_id, domain_limit_id = _create_limits_and_dependencies()
with self.test_client() as c:
r = c.get('/v3/limits', headers=self.headers)
- self.assertTrue(len(r.json['limits']) == 1)
- self.assertEqual(limit['id'], r.json['limits'][0]['id'])
+ self.assertTrue(len(r.json['limits']) == 2)
+ result = []
+ for limit in r.json['limits']:
+ result.append(limit['id'])
+
+ self.assertIn(project_limit_id, result)
+ self.assertIn(domain_limit_id, result)
def test_user_can_create_limits(self):
service = PROVIDERS.catalog_api.create_service(
@@ -281,20 +304,505 @@ class SystemAdminTests(base_classes.TestCaseWithBootstrap,
c.post('/v3/limits', json=create, headers=self.headers)
def test_user_can_update_limits(self):
- limits = _create_limit_and_dependencies()
- limit = limits[0]
+ limit_id, _ = _create_limits_and_dependencies()
update = {'limits': {'description': uuid.uuid4().hex}}
with self.test_client() as c:
c.patch(
- '/v3/limits/%s' % limit['id'], json=update,
+ '/v3/limits/%s' % limit_id, json=update,
headers=self.headers
)
def test_user_can_delete_limits(self):
- limits = _create_limit_and_dependencies()
- limit = limits[0]
+ limit_id, _ = _create_limits_and_dependencies()
+
+ with self.test_client() as c:
+ c.delete('/v3/limits/%s' % limit_id, headers=self.headers)
+
+
+class DomainUserTests(base_classes.TestCaseWithBootstrap,
+ common_auth.AuthTestMixin):
+
+ def setUp(self):
+ super(DomainUserTests, self).setUp()
+ self.loadapp()
+ self.useFixture(ksfixtures.Policy(self.config_fixture))
+ self.config_fixture.config(group='oslo_policy', enforce_scope=True)
+
+ domain = PROVIDERS.resource_api.create_domain(
+ uuid.uuid4().hex, unit.new_domain_ref()
+ )
+ self.domain_id = domain['id']
+ domain_admin = unit.new_user_ref(domain_id=self.domain_id)
+ self.user_id = PROVIDERS.identity_api.create_user(domain_admin)['id']
+ PROVIDERS.assignment_api.create_grant(
+ self.bootstrapper.admin_role_id, user_id=self.user_id,
+ domain_id=self.domain_id
+ )
+
+ auth = self.build_authentication_request(
+ user_id=self.user_id,
+ password=domain_admin['password'],
+ domain_id=self.domain_id
+ )
+
+ # Grab a token using the persona we're testing and prepare headers
+ # for requests we'll be making in the tests.
+ with self.test_client() as c:
+ r = c.post('/v3/auth/tokens', json=auth)
+ self.token_id = r.headers['X-Subject-Token']
+ self.headers = {'X-Auth-Token': self.token_id}
+
+ def test_user_can_get_project_limits_within_domain(self):
+ project_limit_id, _ = _create_limits_and_dependencies(
+ domain_id=self.domain_id
+ )
+
+ with self.test_client() as c:
+ c.get('/v3/limits/%s' % project_limit_id, headers=self.headers)
+
+ def test_user_can_get_domain_limits(self):
+ _, domain_limit_id = _create_limits_and_dependencies(
+ domain_id=self.domain_id
+ )
+
+ with self.test_client() as c:
+ r = c.get('/v3/limits/%s' % domain_limit_id, headers=self.headers)
+ self.assertEqual(self.domain_id, r.json['limit']['domain_id'])
+
+ def test_user_cannot_get_project_limit_outside_domain(self):
+ project_limit_id, _ = _create_limits_and_dependencies()
+
+ with self.test_client() as c:
+ c.get(
+ '/v3/limits/%s' % project_limit_id, headers=self.headers,
+ expected_status_code=http_client.FORBIDDEN
+ )
+
+ def test_user_cannot_get_domain_limits_for_other_domain(self):
+ _, domain_limit_id = _create_limits_and_dependencies()
+
+ with self.test_client() as c:
+ c.get(
+ '/v3/limits/%s' % domain_limit_id, headers=self.headers,
+ expected_status_code=http_client.FORBIDDEN
+ )
+
+ def test_user_can_list_limits_within_domain(self):
+ project_limit_id, domain_limit_id = _create_limits_and_dependencies(
+ domain_id=self.domain_id
+ )
+
+ with self.test_client() as c:
+ r = c.get('/v3/limits', headers=self.headers)
+ result = []
+ for limit in r.json['limits']:
+ result.append(limit['id'])
+
+ self.assertEqual(2, len(r.json['limits']))
+ self.assertIn(project_limit_id, result)
+ self.assertIn(domain_limit_id, result)
+
+ def test_user_cannot_list_limits_outside_domain(self):
+ _create_limits_and_dependencies()
+
+ with self.test_client() as c:
+ r = c.get('/v3/limits', headers=self.headers)
+ self.assertEqual(0, len(r.json['limits']))
+
+ def test_user_cannot_create_limits_for_domain(self):
+ service = PROVIDERS.catalog_api.create_service(
+ uuid.uuid4().hex, unit.new_service_ref()
+ )
+
+ registered_limit = unit.new_registered_limit_ref(
+ service_id=service['id'], id=uuid.uuid4().hex
+ )
+ registered_limits = (
+ PROVIDERS.unified_limit_api.create_registered_limits(
+ [registered_limit]
+ )
+ )
+ registered_limit = registered_limits[0]
+
+ create = {
+ 'limits': [
+ unit.new_limit_ref(
+ domain_id=self.domain_id, service_id=service['id'],
+ resource_name=registered_limit['resource_name'],
+ resource_limit=5
+ )
+ ]
+ }
+
+ with self.test_client() as c:
+ c.post(
+ '/v3/limits', json=create, headers=self.headers,
+ expected_status_code=http_client.FORBIDDEN
+ )
+
+ def test_user_cannot_create_limits_for_other_domain(self):
+ service = PROVIDERS.catalog_api.create_service(
+ uuid.uuid4().hex, unit.new_service_ref()
+ )
+
+ registered_limit = unit.new_registered_limit_ref(
+ service_id=service['id'], id=uuid.uuid4().hex
+ )
+ registered_limits = (
+ PROVIDERS.unified_limit_api.create_registered_limits(
+ [registered_limit]
+ )
+ )
+ registered_limit = registered_limits[0]
+
+ create = {
+ 'limits': [
+ unit.new_limit_ref(
+ domain_id=CONF.identity.default_domain_id,
+ service_id=service['id'],
+ resource_name=registered_limit['resource_name'],
+ resource_limit=5
+ )
+ ]
+ }
+
+ with self.test_client() as c:
+ c.post(
+ '/v3/limits', json=create, headers=self.headers,
+ expected_status_code=http_client.FORBIDDEN
+ )
+
+ def test_user_cannot_create_limits_for_projects_in_domain(self):
+ service = PROVIDERS.catalog_api.create_service(
+ uuid.uuid4().hex, unit.new_service_ref()
+ )
+
+ registered_limit = unit.new_registered_limit_ref(
+ service_id=service['id'], id=uuid.uuid4().hex
+ )
+ registered_limits = (
+ PROVIDERS.unified_limit_api.create_registered_limits(
+ [registered_limit]
+ )
+ )
+ registered_limit = registered_limits[0]
+
+ project = PROVIDERS.resource_api.create_project(
+ uuid.uuid4().hex, unit.new_project_ref(domain_id=self.domain_id)
+ )
+
+ create = {
+ 'limits': [
+ unit.new_limit_ref(
+ project_id=project['id'],
+ service_id=service['id'],
+ resource_name=registered_limit['resource_name'],
+ resource_limit=5
+ )
+ ]
+ }
+
+ with self.test_client() as c:
+ c.post(
+ '/v3/limits', json=create, headers=self.headers,
+ expected_status_code=http_client.FORBIDDEN
+ )
+
+ def test_user_cannot_create_limits_for_projects_outside_domain(self):
+ service = PROVIDERS.catalog_api.create_service(
+ uuid.uuid4().hex, unit.new_service_ref()
+ )
+
+ registered_limit = unit.new_registered_limit_ref(
+ service_id=service['id'], id=uuid.uuid4().hex
+ )
+ registered_limits = (
+ PROVIDERS.unified_limit_api.create_registered_limits(
+ [registered_limit]
+ )
+ )
+ registered_limit = registered_limits[0]
+
+ project = PROVIDERS.resource_api.create_project(
+ uuid.uuid4().hex,
+ unit.new_project_ref(domain_id=CONF.identity.default_domain_id)
+ )
+
+ create = {
+ 'limits': [
+ unit.new_limit_ref(
+ project_id=project['id'],
+ service_id=service['id'],
+ resource_name=registered_limit['resource_name'],
+ resource_limit=5
+ )
+ ]
+ }
+
+ with self.test_client() as c:
+ c.post(
+ '/v3/limits', json=create, headers=self.headers,
+ expected_status_code=http_client.FORBIDDEN
+ )
+
+ def test_user_cannot_update_limits_for_domain(self):
+ _, domain_limit_id = _create_limits_and_dependencies(
+ domain_id=self.domain_id
+ )
+
+ update = {'limit': {'resource_limit': 1}}
+
+ with self.test_client() as c:
+ c.patch(
+ '/v3/limits/%s' % domain_limit_id, json=update,
+ headers=self.headers,
+ expected_status_code=http_client.FORBIDDEN
+ )
+
+ def test_user_cannot_update_limits_for_other_domain(self):
+ _, domain_limit_id = _create_limits_and_dependencies()
+
+ update = {'limit': {'resource_limit': 1}}
+
+ with self.test_client() as c:
+ c.patch(
+ '/v3/limits/%s' % domain_limit_id, json=update,
+ headers=self.headers,
+ expected_status_code=http_client.FORBIDDEN
+ )
+
+ def test_user_cannot_update_limits_for_projects_in_domain(self):
+ project_limit_id, _ = _create_limits_and_dependencies(
+ domain_id=self.domain_id
+ )
+
+ update = {'limit': {'resource_limit': 1}}
+
+ with self.test_client() as c:
+ c.patch(
+ '/v3/limits/%s' % project_limit_id, headers=self.headers,
+ json=update, expected_status_code=http_client.FORBIDDEN
+ )
+
+ def test_user_cannot_update_limits_for_projects_outside_domain(self):
+ project_limit_id, _ = _create_limits_and_dependencies()
+
+ update = {'limit': {'resource_limit': 1}}
with self.test_client() as c:
- c.delete('/v3/limits/%s' % limit['id'], headers=self.headers)
+ c.patch(
+ '/v3/limits/%s' % project_limit_id, headers=self.headers,
+ json=update, expected_status_code=http_client.FORBIDDEN
+ )
+
+ def test_user_cannot_delete_limits_for_domain(self):
+ _, domain_limit_id = _create_limits_and_dependencies(
+ domain_id=self.domain_id
+ )
+
+ with self.test_client() as c:
+ c.delete(
+ '/v3/limits/%s' % domain_limit_id, headers=self.headers,
+ expected_status_code=http_client.FORBIDDEN
+ )
+
+ def test_user_cannot_delete_limits_for_other_domain(self):
+ _, domain_limit_id = _create_limits_and_dependencies()
+
+ with self.test_client() as c:
+ c.delete(
+ '/v3/limits/%s' % domain_limit_id, headers=self.headers,
+ expected_status_code=http_client.FORBIDDEN
+ )
+
+ def test_user_cannot_delete_limits_for_projects_in_domain(self):
+ project_limit_id, _ = _create_limits_and_dependencies(
+ domain_id=self.domain_id
+ )
+
+ with self.test_client() as c:
+ c.delete(
+ '/v3/limits/%s' % project_limit_id, headers=self.headers,
+ expected_status_code=http_client.FORBIDDEN
+ )
+
+ def test_user_cannot_delete_limits_for_projects_outside_domain(self):
+ project_limit_id, _ = _create_limits_and_dependencies()
+
+ with self.test_client() as c:
+ c.delete(
+ '/v3/limits/%s' % project_limit_id, headers=self.headers,
+ expected_status_code=http_client.FORBIDDEN
+ )
+
+
+class ProjectUserTests(base_classes.TestCaseWithBootstrap,
+ common_auth.AuthTestMixin):
+
+ def setUp(self):
+ super(ProjectUserTests, self).setUp()
+ self.loadapp()
+ self.useFixture(ksfixtures.Policy(self.config_fixture))
+ self.config_fixture.config(group='oslo_policy', enforce_scope=True)
+
+ # Reuse the system administrator account created during
+ # ``keystone-manage bootstrap``
+ self.user_id = self.bootstrapper.admin_user_id
+ auth = self.build_authentication_request(
+ user_id=self.user_id,
+ password=self.bootstrapper.admin_password,
+ project_id=self.bootstrapper.project_id
+ )
+
+ # Grab a token using the persona we're testing and prepare headers
+ # for requests we'll be making in the tests.
+ with self.test_client() as c:
+ r = c.post('/v3/auth/tokens', json=auth)
+ self.token_id = r.headers['X-Subject-Token']
+ self.headers = {'X-Auth-Token': self.token_id}
+
+ def test_user_can_get_project_limit(self):
+ project_limit_id, _ = _create_limits_and_dependencies()
+
+ limit = PROVIDERS.unified_limit_api.get_limit(project_limit_id)
+ # NOTE(lbragstad): Project users are only allowed to list limits for a
+ # project if they actually have a role assignment on the project and
+ # call the API with a project-scoped token.
+ PROVIDERS.assignment_api.create_grant(
+ self.bootstrapper.reader_role_id, user_id=self.user_id,
+ project_id=limit['project_id']
+ )
+ auth = self.build_authentication_request(
+ user_id=self.user_id,
+ password=self.bootstrapper.admin_password,
+ project_id=limit['project_id']
+ )
+ with self.test_client() as c:
+ r = c.post('/v3/auth/tokens', json=auth)
+ token_id = r.headers['X-Subject-Token']
+ headers = {'X-Auth-Token': token_id}
+
+ with self.test_client() as c:
+ r = c.get('/v3/limits/%s' % project_limit_id, headers=headers)
+
+ def test_user_cannot_get_project_limit_without_role_assignment(self):
+ project_limit_id, _ = _create_limits_and_dependencies()
+
+ with self.test_client() as c:
+ c.get(
+ '/v3/limits/%s' % project_limit_id, headers=self.headers,
+ expected_status_code=http_client.FORBIDDEN
+ )
+
+ def test_user_cannot_get_domain_limit(self):
+ _, domain_limit_id = _create_limits_and_dependencies()
+
+ with self.test_client() as c:
+ c.get(
+ '/v3/limits/%s' % domain_limit_id, headers=self.headers,
+ expected_status_code=http_client.FORBIDDEN
+ )
+
+ def test_user_can_list_limits(self):
+ project_limit_id, _ = _create_limits_and_dependencies()
+
+ limit = PROVIDERS.unified_limit_api.get_limit(project_limit_id)
+ # NOTE(lbragstad): Project users are only allowed to list limits for a
+ # project if they actually have a role assignment on the project and
+ # call the API with a project-scoped token.
+ PROVIDERS.assignment_api.create_grant(
+ self.bootstrapper.reader_role_id, user_id=self.user_id,
+ project_id=limit['project_id']
+ )
+ auth = self.build_authentication_request(
+ user_id=self.user_id,
+ password=self.bootstrapper.admin_password,
+ project_id=limit['project_id']
+ )
+ with self.test_client() as c:
+ r = c.post('/v3/auth/tokens', json=auth)
+ token_id = r.headers['X-Subject-Token']
+ headers = {'X-Auth-Token': token_id}
+
+ with self.test_client() as c:
+ r = c.get('/v3/limits', headers=headers)
+ self.assertTrue(len(r.json['limits']) == 1)
+ self.assertEqual(project_limit_id, r.json['limits'][0]['id'])
+
+ def test_user_cannot_list_limits_without_project_role_assignment(self):
+ _create_limits_and_dependencies()
+
+ with self.test_client() as c:
+ r = c.get('/v3/limits', headers=self.headers)
+ self.assertEqual(0, len(r.json['limits']))
+
+ def test_user_can_get_limit_model(self):
+ with self.test_client() as c:
+ c.get('/v3/limits/model', headers=self.headers)
+
+ def test_user_cannot_create_limits(self):
+ service = PROVIDERS.catalog_api.create_service(
+ uuid.uuid4().hex, unit.new_service_ref()
+ )
+
+ registered_limit = unit.new_registered_limit_ref(
+ service_id=service['id'], id=uuid.uuid4().hex
+ )
+ registered_limits = (
+ PROVIDERS.unified_limit_api.create_registered_limits(
+ [registered_limit]
+ )
+ )
+ registered_limit = registered_limits[0]
+
+ project = PROVIDERS.resource_api.create_project(
+ uuid.uuid4().hex,
+ unit.new_project_ref(domain_id=CONF.identity.default_domain_id)
+ )
+
+ create = {
+ 'limits': [
+ unit.new_limit_ref(
+ project_id=project['id'], service_id=service['id'],
+ resource_name=registered_limit['resource_name'],
+ resource_limit=5
+ )
+ ]
+ }
+
+ with self.test_client() as c:
+ c.post(
+ '/v3/limits', json=create, headers=self.headers,
+ expected_status_code=http_client.FORBIDDEN
+ )
+
+ def test_user_cannot_update_limits(self):
+ limit_id, _ = _create_limits_and_dependencies()
+
+ update = {'limits': {'description': uuid.uuid4().hex}}
+
+ with self.test_client() as c:
+ c.patch(
+ '/v3/limits/%s' % limit_id, json=update,
+ headers=self.headers,
+ expected_status_code=http_client.FORBIDDEN
+ )
+
+ def test_user_cannot_delete_limits(self):
+ limit_id, _ = _create_limits_and_dependencies()
+
+ with self.test_client() as c:
+ c.delete(
+ '/v3/limits/%s' % limit_id,
+ headers=self.headers,
+ expected_status_code=http_client.FORBIDDEN
+ )
+
+
+class ProjectUserTestsWithoutEnforceScope(ProjectUserTests):
+
+ def setUp(self):
+ super(ProjectUserTestsWithoutEnforceScope, self).setUp()
+ self.config_fixture.config(group='oslo_policy', enforce_scope=False)
diff --git a/keystone/tests/protection/v3/test_policy_association.py b/keystone/tests/protection/v3/test_policy_association.py
index bd1727339..c279745ee 100644
--- a/keystone/tests/protection/v3/test_policy_association.py
+++ b/keystone/tests/protection/v3/test_policy_association.py
@@ -118,10 +118,11 @@ class _SystemUserPoliciesAssociationTests(object):
policy['id'], endpoint['id']
)
with self.test_client() as c:
- r = c.get('/v3/endpoints/%s/OS-ENDPOINT-POLICY/policy'
- % (endpoint['id']),
+ r = c.get('/v3/policies/%s/OS-ENDPOINT-POLICY/endpoints'
+ % (policy['id']),
headers=self.headers)
- self.assertIn(policy['id'], r.json['policy']['id'])
+ for endpoint_itr in r.json['endpoints']:
+ self.assertIn(endpoint['id'], endpoint_itr['id'])
class _SystemReaderAndMemberPoliciesAssociationTests(object):
diff --git a/keystone/tests/protection/v3/test_project_endpoint.py b/keystone/tests/protection/v3/test_project_endpoint.py
new file mode 100644
index 000000000..4e2d5a14f
--- /dev/null
+++ b/keystone/tests/protection/v3/test_project_endpoint.py
@@ -0,0 +1,465 @@
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import uuid
+
+from oslo_serialization import jsonutils
+from six.moves import http_client
+
+from keystone.common.policies import base as bp
+from keystone.common import provider_api
+import keystone.conf
+from keystone.tests.common import auth as common_auth
+from keystone.tests import unit
+from keystone.tests.unit import base_classes
+from keystone.tests.unit import ksfixtures
+from keystone.tests.unit.ksfixtures import temporaryfile
+
+CONF = keystone.conf.CONF
+PROVIDERS = provider_api.ProviderAPIs
+
+
+class _SystemUserProjectEndpointTests(object):
+ """Common default functionality for all system users."""
+
+ def test_user_can_list_projects_for_endpoint(self):
+ project = PROVIDERS.resource_api.create_project(
+ uuid.uuid4().hex,
+ unit.new_project_ref(domain_id=CONF.identity.default_domain_id)
+ )
+
+ service = PROVIDERS.catalog_api.create_service(
+ uuid.uuid4().hex, unit.new_service_ref()
+ )
+ endpoint = unit.new_endpoint_ref(service['id'], region_id=None)
+ endpoint = PROVIDERS.catalog_api.create_endpoint(
+ endpoint['id'], endpoint
+ )
+
+ PROVIDERS.catalog_api.add_endpoint_to_project(endpoint['id'], project['id'])
+ with self.test_client() as c:
+ r = c.get('/v3/OS-EP-FILTER/endpoints/%s/projects' % endpoint['id'],
+ headers=self.headers)
+ for project_itr in r.json['projects']:
+ self.assertIn(project['id'], project_itr['id'])
+
+ def test_user_can_check_endpoint_in_project(self):
+ project = PROVIDERS.resource_api.create_project(
+ uuid.uuid4().hex,
+ unit.new_project_ref(domain_id=CONF.identity.default_domain_id)
+ )
+
+ service = PROVIDERS.catalog_api.create_service(
+ uuid.uuid4().hex, unit.new_service_ref()
+ )
+ endpoint = unit.new_endpoint_ref(service['id'], region_id=None)
+ endpoint = PROVIDERS.catalog_api.create_endpoint(
+ endpoint['id'], endpoint
+ )
+
+ PROVIDERS.catalog_api.add_endpoint_to_project(endpoint['id'], project['id'])
+ with self.test_client() as c:
+ c.get('/v3/OS-EP-FILTER/projects/%s/endpoints/%s'
+ % (project['id'], endpoint['id']),
+ headers=self.headers,
+ expected_status_code=http_client.NO_CONTENT)
+
+ def test_user_can_list_endpoints_for_project(self):
+ project = PROVIDERS.resource_api.create_project(
+ uuid.uuid4().hex,
+ unit.new_project_ref(domain_id=CONF.identity.default_domain_id)
+ )
+
+ service = PROVIDERS.catalog_api.create_service(
+ uuid.uuid4().hex, unit.new_service_ref()
+ )
+ endpoint = unit.new_endpoint_ref(service['id'], region_id=None)
+ endpoint = PROVIDERS.catalog_api.create_endpoint(
+ endpoint['id'], endpoint
+ )
+
+ PROVIDERS.catalog_api.add_endpoint_to_project(endpoint['id'], project['id'])
+ with self.test_client() as c:
+ r = c.get('/v3/OS-EP-FILTER/projects/%s/endpoints' % project['id'],
+ headers=self.headers)
+ for endpoint_itr in r.json['endpoints']:
+ self.assertIn(endpoint['id'], endpoint_itr['id'])
+
+
+class _SystemReaderAndMemberProjectEndpointTests(object):
+
+ def test_user_cannot_add_endpoint_to_project(self):
+ project = PROVIDERS.resource_api.create_project(
+ uuid.uuid4().hex,
+ unit.new_project_ref(domain_id=CONF.identity.default_domain_id)
+ )
+
+ service = PROVIDERS.catalog_api.create_service(
+ uuid.uuid4().hex, unit.new_service_ref()
+ )
+ endpoint = unit.new_endpoint_ref(service['id'], region_id=None)
+ endpoint = PROVIDERS.catalog_api.create_endpoint(
+ endpoint['id'], endpoint
+ )
+ with self.test_client() as c:
+ c.put('/v3/OS-EP-FILTER/projects/%s/endpoints/%s'
+ % (project['id'], endpoint['id']),
+ headers=self.headers,
+ expected_status_code=http_client.FORBIDDEN)
+
+ def test_user_cannot_remove_endpoint_from_project(self):
+ project = PROVIDERS.resource_api.create_project(
+ uuid.uuid4().hex,
+ unit.new_project_ref(domain_id=CONF.identity.default_domain_id)
+ )
+
+ service = PROVIDERS.catalog_api.create_service(
+ uuid.uuid4().hex, unit.new_service_ref()
+ )
+ endpoint = unit.new_endpoint_ref(service['id'], region_id=None)
+ endpoint = PROVIDERS.catalog_api.create_endpoint(
+ endpoint['id'], endpoint
+ )
+ with self.test_client() as c:
+ c.delete('/v3/OS-EP-FILTER/projects/%s/endpoints/%s'
+ % (project['id'], endpoint['id']),
+ headers=self.headers,
+ expected_status_code=http_client.FORBIDDEN)
+
+
+class _DomainAndProjectUserProjectEndpointTests(object):
+
+ def test_user_cannot_list_projects_for_endpoint(self):
+ project = PROVIDERS.resource_api.create_project(
+ uuid.uuid4().hex,
+ unit.new_project_ref(domain_id=CONF.identity.default_domain_id)
+ )
+
+ service = PROVIDERS.catalog_api.create_service(
+ uuid.uuid4().hex, unit.new_service_ref()
+ )
+ endpoint = unit.new_endpoint_ref(service['id'], region_id=None)
+ endpoint = PROVIDERS.catalog_api.create_endpoint(
+ endpoint['id'], endpoint
+ )
+
+ PROVIDERS.catalog_api.add_endpoint_to_project(endpoint['id'], project['id'])
+ with self.test_client() as c:
+ c.get('/v3/OS-EP-FILTER/endpoints/%s/projects' % endpoint['id'],
+ headers=self.headers,
+ expected_status_code=http_client.FORBIDDEN)
+
+ def test_user_cannot_check_endpoint_in_project(self):
+ project = PROVIDERS.resource_api.create_project(
+ uuid.uuid4().hex,
+ unit.new_project_ref(domain_id=CONF.identity.default_domain_id)
+ )
+
+ service = PROVIDERS.catalog_api.create_service(
+ uuid.uuid4().hex, unit.new_service_ref()
+ )
+ endpoint = unit.new_endpoint_ref(service['id'], region_id=None)
+ endpoint = PROVIDERS.catalog_api.create_endpoint(
+ endpoint['id'], endpoint
+ )
+
+ PROVIDERS.catalog_api.add_endpoint_to_project(endpoint['id'], project['id'])
+ with self.test_client() as c:
+ c.get('/v3/OS-EP-FILTER/projects/%s/endpoints/%s'
+ % (project['id'], endpoint['id']),
+ headers=self.headers,
+ expected_status_code=http_client.FORBIDDEN)
+
+ def test_user_cannot_list_endpoints_for_project(self):
+ project = PROVIDERS.resource_api.create_project(
+ uuid.uuid4().hex,
+ unit.new_project_ref(domain_id=CONF.identity.default_domain_id)
+ )
+
+ service = PROVIDERS.catalog_api.create_service(
+ uuid.uuid4().hex, unit.new_service_ref()
+ )
+ endpoint = unit.new_endpoint_ref(service['id'], region_id=None)
+ endpoint = PROVIDERS.catalog_api.create_endpoint(
+ endpoint['id'], endpoint
+ )
+
+ PROVIDERS.catalog_api.add_endpoint_to_project(endpoint['id'], project['id'])
+ with self.test_client() as c:
+ c.get('/v3/OS-EP-FILTER/projects/%s/endpoints' % project['id'],
+ headers=self.headers,
+ expected_status_code=http_client.FORBIDDEN)
+
+
+class SystemReaderTests(base_classes.TestCaseWithBootstrap,
+ common_auth.AuthTestMixin,
+ _SystemUserProjectEndpointTests,
+ _SystemReaderAndMemberProjectEndpointTests):
+
+ def setUp(self):
+ super(SystemReaderTests, self).setUp()
+ self.loadapp()
+ self.useFixture(ksfixtures.Policy(self.config_fixture))
+ self.config_fixture.config(group='oslo_policy', enforce_scope=True)
+
+ system_reader = unit.new_user_ref(
+ domain_id=CONF.identity.default_domain_id
+ )
+ self.user_id = PROVIDERS.identity_api.create_user(
+ system_reader
+ )['id']
+ PROVIDERS.assignment_api.create_system_grant_for_user(
+ self.user_id, self.bootstrapper.reader_role_id
+ )
+
+ auth = self.build_authentication_request(
+ user_id=self.user_id, password=system_reader['password'],
+ system=True
+ )
+
+ # Grab a token using the persona we're testing and prepare headers
+ # for requests we'll be making in the tests.
+ with self.test_client() as c:
+ r = c.post('/v3/auth/tokens', json=auth)
+ self.token_id = r.headers['X-Subject-Token']
+ self.headers = {'X-Auth-Token': self.token_id}
+
+
+class SystemMemberTests(base_classes.TestCaseWithBootstrap,
+ common_auth.AuthTestMixin,
+ _SystemUserProjectEndpointTests,
+ _SystemReaderAndMemberProjectEndpointTests):
+
+ def setUp(self):
+ super(SystemMemberTests, self).setUp()
+ self.loadapp()
+ self.useFixture(ksfixtures.Policy(self.config_fixture))
+ self.config_fixture.config(group='oslo_policy', enforce_scope=True)
+
+ system_member = unit.new_user_ref(
+ domain_id=CONF.identity.default_domain_id
+ )
+ self.user_id = PROVIDERS.identity_api.create_user(
+ system_member
+ )['id']
+ PROVIDERS.assignment_api.create_system_grant_for_user(
+ self.user_id, self.bootstrapper.member_role_id
+ )
+
+ auth = self.build_authentication_request(
+ user_id=self.user_id, password=system_member['password'],
+ system=True
+ )
+
+ # Grab a token using the persona we're testing and prepare headers
+ # for requests we'll be making in the tests.
+ with self.test_client() as c:
+ r = c.post('/v3/auth/tokens', json=auth)
+ self.token_id = r.headers['X-Subject-Token']
+ self.headers = {'X-Auth-Token': self.token_id}
+
+
+class SystemAdminTests(base_classes.TestCaseWithBootstrap,
+ common_auth.AuthTestMixin,
+ _SystemUserProjectEndpointTests):
+
+ def setUp(self):
+ super(SystemAdminTests, self).setUp()
+ self.loadapp()
+ self.useFixture(ksfixtures.Policy(self.config_fixture))
+ self.config_fixture.config(group='oslo_policy', enforce_scope=True)
+
+ # Reuse the system administrator account created during
+ # ``keystone-manage bootstrap``
+ self.user_id = self.bootstrapper.admin_user_id
+ auth = self.build_authentication_request(
+ user_id=self.user_id,
+ password=self.bootstrapper.admin_password,
+ system=True
+ )
+
+ # Grab a token using the persona we're testing and prepare headers
+ # for requests we'll be making in the tests.
+ with self.test_client() as c:
+ r = c.post('/v3/auth/tokens', json=auth)
+ self.token_id = r.headers['X-Subject-Token']
+ self.headers = {'X-Auth-Token': self.token_id}
+
+ def test_user_can_add_endpoint_to_project(self):
+ project = PROVIDERS.resource_api.create_project(
+ uuid.uuid4().hex,
+ unit.new_project_ref(domain_id=CONF.identity.default_domain_id)
+ )
+
+ service = PROVIDERS.catalog_api.create_service(
+ uuid.uuid4().hex, unit.new_service_ref()
+ )
+ endpoint = unit.new_endpoint_ref(service['id'], region_id=None)
+ endpoint = PROVIDERS.catalog_api.create_endpoint(
+ endpoint['id'], endpoint
+ )
+ with self.test_client() as c:
+ c.put('/v3/OS-EP-FILTER/projects/%s/endpoints/%s'
+ % (project['id'], endpoint['id']),
+ headers=self.headers,
+ expected_status_code=http_client.NO_CONTENT)
+
+ def test_user_can_remove_endpoint_from_project(self):
+ project = PROVIDERS.resource_api.create_project(
+ uuid.uuid4().hex,
+ unit.new_project_ref(domain_id=CONF.identity.default_domain_id)
+ )
+
+ service = PROVIDERS.catalog_api.create_service(
+ uuid.uuid4().hex, unit.new_service_ref()
+ )
+ endpoint = unit.new_endpoint_ref(service['id'], region_id=None)
+ endpoint = PROVIDERS.catalog_api.create_endpoint(
+ endpoint['id'], endpoint
+ )
+ PROVIDERS.catalog_api.add_endpoint_to_project(endpoint['id'], project['id'])
+ with self.test_client() as c:
+ c.delete('/v3/OS-EP-FILTER/projects/%s/endpoints/%s'
+ % (project['id'], endpoint['id']),
+ headers=self.headers,
+ expected_status_code=http_client.NO_CONTENT)
+
+
+class DomainUserTests(base_classes.TestCaseWithBootstrap,
+ common_auth.AuthTestMixin,
+ _DomainAndProjectUserProjectEndpointTests,
+ _SystemReaderAndMemberProjectEndpointTests):
+
+ def setUp(self):
+ super(DomainUserTests, self).setUp()
+ self.loadapp()
+ self.useFixture(ksfixtures.Policy(self.config_fixture))
+ self.config_fixture.config(group='oslo_policy', enforce_scope=True)
+
+ domain = PROVIDERS.resource_api.create_domain(
+ uuid.uuid4().hex, unit.new_domain_ref()
+ )
+ self.domain_id = domain['id']
+ domain_admin = unit.new_user_ref(domain_id=self.domain_id)
+ self.user_id = PROVIDERS.identity_api.create_user(domain_admin)['id']
+ PROVIDERS.assignment_api.create_grant(
+ self.bootstrapper.admin_role_id, user_id=self.user_id,
+ domain_id=self.domain_id
+ )
+
+ auth = self.build_authentication_request(
+ user_id=self.user_id,
+ password=domain_admin['password'],
+ domain_id=self.domain_id
+ )
+
+ # Grab a token using the persona we're testing and prepare headers
+ # for requests we'll be making in the tests.
+ with self.test_client() as c:
+ r = c.post('/v3/auth/tokens', json=auth)
+ self.token_id = r.headers['X-Subject-Token']
+ self.headers = {'X-Auth-Token': self.token_id}
+
+
+class ProjectUserTests(base_classes.TestCaseWithBootstrap,
+ common_auth.AuthTestMixin,
+ _DomainAndProjectUserProjectEndpointTests,
+ _SystemReaderAndMemberProjectEndpointTests):
+
+ def setUp(self):
+ super(ProjectUserTests, self).setUp()
+ self.loadapp()
+ self.useFixture(ksfixtures.Policy(self.config_fixture))
+ self.config_fixture.config(group='oslo_policy', enforce_scope=True)
+
+ self.user_id = self.bootstrapper.admin_user_id
+ auth = self.build_authentication_request(
+ user_id=self.user_id,
+ password=self.bootstrapper.admin_password,
+ project_id=self.bootstrapper.project_id
+ )
+
+ # Grab a token using the persona we're testing and prepare headers
+ # for requests we'll be making in the tests.
+ with self.test_client() as c:
+ r = c.post('/v3/auth/tokens', json=auth)
+ self.token_id = r.headers['X-Subject-Token']
+ self.headers = {'X-Auth-Token': self.token_id}
+
+
+class ProjectUserTestsWithoutEnforceScope(
+ base_classes.TestCaseWithBootstrap,
+ common_auth.AuthTestMixin,
+ _DomainAndProjectUserProjectEndpointTests,
+ _SystemReaderAndMemberProjectEndpointTests):
+
+ def _override_policy(self):
+ # TODO(cmurphy): Remove this once the deprecated policies in
+ # keystone.common.policies.project_endpoint have been removed. This is
+ # only here to make sure we test the new policies instead of the
+ # deprecated ones. Oslo.policy will OR deprecated policies with new
+ # policies to maintain compatibility and give operators a chance to
+ # update permissions or update policies without breaking users. This
+ # will cause these specific tests to fail since we're trying to correct
+ # this broken behavior with better scope checking.
+ with open(self.policy_file_name, 'w') as f:
+ overridden_policies = {
+ 'identity:list_projects_for_endpoint': bp.SYSTEM_READER,
+ 'identity:add_endpoint_to_project': bp.SYSTEM_ADMIN,
+ 'identity:check_endpoint_in_project': bp.SYSTEM_READER,
+ 'identity:list_endpoints_for_project': bp.SYSTEM_READER,
+ 'identity:remove_endpoint_from_project': bp.SYSTEM_ADMIN
+ }
+ f.write(jsonutils.dumps(overridden_policies))
+
+ def setUp(self):
+ super(ProjectUserTestsWithoutEnforceScope, self).setUp()
+ self.loadapp()
+ self.policy_file = self.useFixture(temporaryfile.SecureTempFile())
+ self.policy_file_name = self.policy_file.file_name
+ self.useFixture(
+ ksfixtures.Policy(
+ self.config_fixture, policy_file=self.policy_file_name
+ )
+ )
+ self._override_policy()
+ # Explicity set enforce_scope to False to make sure we maintain
+ # backwards compatibility with project users.
+ self.config_fixture.config(group='oslo_policy', enforce_scope=False)
+
+ domain = PROVIDERS.resource_api.create_domain(
+ uuid.uuid4().hex, unit.new_domain_ref()
+ )
+ user = unit.new_user_ref(domain_id=domain['id'])
+ self.user_id = PROVIDERS.identity_api.create_user(user)['id']
+
+ self.project_id = PROVIDERS.resource_api.create_project(
+ uuid.uuid4().hex, unit.new_project_ref(domain_id=domain['id'])
+ )['id']
+
+ PROVIDERS.assignment_api.create_grant(
+ self.bootstrapper.admin_role_id, user_id=self.user_id,
+ project_id=self.project_id
+ )
+
+ auth = self.build_authentication_request(
+ user_id=self.user_id,
+ password=user['password'],
+ project_id=self.project_id
+ )
+
+ # Grab a token using the persona we're testing and prepare headers
+ # for requests we'll be making in the tests.
+ with self.test_client() as c:
+ r = c.post('/v3/auth/tokens', json=auth)
+ self.token_id = r.headers['X-Subject-Token']
+ self.headers = {'X-Auth-Token': self.token_id}
diff --git a/keystone/tests/protection/v3/test_project_tags.py b/keystone/tests/protection/v3/test_project_tags.py
new file mode 100644
index 000000000..0807bedb6
--- /dev/null
+++ b/keystone/tests/protection/v3/test_project_tags.py
@@ -0,0 +1,974 @@
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import uuid
+
+from oslo_serialization import jsonutils
+from six.moves import http_client
+
+from keystone.common.policies import project as pp
+from keystone.common import provider_api
+import keystone.conf
+from keystone.tests.common import auth as common_auth
+from keystone.tests import unit
+from keystone.tests.unit import base_classes
+from keystone.tests.unit import ksfixtures
+from keystone.tests.unit.ksfixtures import temporaryfile
+
+CONF = keystone.conf.CONF
+PROVIDERS = provider_api.ProviderAPIs
+
+
+def _override_policy(policy_file):
+ # TODO(lbragstad): Remove this once the deprecated policies in
+ # keystone.common.policies.project have been removed. This is only
+ # here to make sure we test the new policies instead of the deprecated
+ # ones. Oslo.policy will OR deprecated policies with new policies to
+ # maintain compatibility and give operators a chance to update
+ # permissions or update policies without breaking users. This will
+ # cause these specific tests to fail since we're trying to correct this
+ # broken behavior with better scope checking.
+ with open(policy_file, 'w') as f:
+ overridden_policies = {
+ 'identity:get_project_tag': (
+ pp.SYSTEM_READER_OR_DOMAIN_READER_OR_PROJECT_USER
+ ),
+ 'identity:list_project_tags': (
+ pp.SYSTEM_READER_OR_DOMAIN_READER_OR_PROJECT_USER
+ ),
+ 'identity:create_project_tag': (
+ pp.SYSTEM_ADMIN_OR_DOMAIN_ADMIN_OR_PROJECT_ADMIN
+ ),
+ 'identity:update_project_tags': (
+ pp.SYSTEM_ADMIN_OR_DOMAIN_ADMIN_OR_PROJECT_ADMIN
+ ),
+ 'identity:delete_project_tag': (
+ pp.SYSTEM_ADMIN_OR_DOMAIN_ADMIN_OR_PROJECT_ADMIN
+ ),
+ 'identity:delete_project_tags': (
+ pp.SYSTEM_ADMIN_OR_DOMAIN_ADMIN_OR_PROJECT_ADMIN
+ )
+ }
+ f.write(jsonutils.dumps(overridden_policies))
+
+
+class _SystemUserTests(object):
+ def test_user_can_get_project_tag(self):
+ project = PROVIDERS.resource_api.create_project(
+ uuid.uuid4().hex,
+ unit.new_project_ref(domain_id=CONF.identity.default_domain_id)
+ )
+
+ tag = uuid.uuid4().hex
+ PROVIDERS.resource_api.create_project_tag(project['id'], tag)
+
+ with self.test_client() as c:
+ c.get(
+ '/v3/projects/%s/tags/%s' % (project['id'], tag),
+ headers=self.headers,
+ expected_status_code=http_client.NO_CONTENT
+ )
+
+ def test_user_can_list_project_tags(self):
+ project = PROVIDERS.resource_api.create_project(
+ uuid.uuid4().hex,
+ unit.new_project_ref(domain_id=CONF.identity.default_domain_id)
+ )
+
+ tag = uuid.uuid4().hex
+ PROVIDERS.resource_api.create_project_tag(project['id'], tag)
+
+ with self.test_client() as c:
+ r = c.get(
+ '/v3/projects/%s/tags' % project['id'], headers=self.headers
+ )
+ self.assertTrue(len(r.json['tags']) == 1)
+ self.assertEqual(tag, r.json['tags'][0])
+
+
+class _SystemMemberAndReaderTagTests(object):
+
+ def test_user_cannot_create_project_tag(self):
+ project = PROVIDERS.resource_api.create_project(
+ uuid.uuid4().hex,
+ unit.new_project_ref(domain_id=CONF.identity.default_domain_id)
+ )
+
+ tag = uuid.uuid4().hex
+
+ with self.test_client() as c:
+ c.put(
+ '/v3/projects/%s/tags/%s' % (project['id'], tag),
+ headers=self.headers,
+ expected_status_code=http_client.FORBIDDEN
+ )
+
+ def test_user_cannot_update_project_tag(self):
+ project = PROVIDERS.resource_api.create_project(
+ uuid.uuid4().hex,
+ unit.new_project_ref(domain_id=CONF.identity.default_domain_id)
+ )
+
+ tag = uuid.uuid4().hex
+ PROVIDERS.resource_api.create_project_tag(project['id'], tag)
+
+ update = {"tags": [uuid.uuid4().hex]}
+
+ with self.test_client() as c:
+ c.put(
+ '/v3/projects/%s/tags' % project['id'], headers=self.headers,
+ json=update, expected_status_code=http_client.FORBIDDEN
+ )
+
+ def test_user_cannot_delete_project_tag(self):
+ project = PROVIDERS.resource_api.create_project(
+ uuid.uuid4().hex,
+ unit.new_project_ref(domain_id=CONF.identity.default_domain_id)
+ )
+
+ tag = uuid.uuid4().hex
+ PROVIDERS.resource_api.create_project_tag(project['id'], tag)
+
+ with self.test_client() as c:
+ c.delete(
+ '/v3/projects/%s/tags/%s' % (project['id'], tag),
+ headers=self.headers,
+ expected_status_code=http_client.FORBIDDEN
+ )
+
+
+class _DomainAndProjectUserTagTests(object):
+
+ def test_user_cannot_create_project_tag(self):
+ project = PROVIDERS.resource_api.create_project(
+ uuid.uuid4().hex,
+ unit.new_project_ref(domain_id=CONF.identity.default_domain_id)
+ )
+
+ tag = uuid.uuid4().hex
+
+ with self.test_client() as c:
+ c.put(
+ '/v3/projects/%s/tags/%s' % (project['id'], tag),
+ headers=self.headers,
+ expected_status_code=http_client.FORBIDDEN
+ )
+
+ def test_user_cannot_update_project_tag(self):
+ project = PROVIDERS.resource_api.create_project(
+ uuid.uuid4().hex,
+ unit.new_project_ref(domain_id=CONF.identity.default_domain_id)
+ )
+
+ tag = uuid.uuid4().hex
+ PROVIDERS.resource_api.create_project_tag(project['id'], tag)
+
+ update = {"tags": [uuid.uuid4().hex]}
+
+ with self.test_client() as c:
+ c.put(
+ '/v3/projects/%s/tags' % project['id'], headers=self.headers,
+ json=update, expected_status_code=http_client.FORBIDDEN
+ )
+
+ def test_user_cannot_delete_project_tag(self):
+ project = PROVIDERS.resource_api.create_project(
+ uuid.uuid4().hex,
+ unit.new_project_ref(domain_id=CONF.identity.default_domain_id)
+ )
+
+ tag = uuid.uuid4().hex
+ PROVIDERS.resource_api.create_project_tag(project['id'], tag)
+
+ with self.test_client() as c:
+ c.delete(
+ '/v3/projects/%s/tags/%s' % (project['id'], tag),
+ headers=self.headers,
+ expected_status_code=http_client.FORBIDDEN
+ )
+
+
+class SystemReaderTests(base_classes.TestCaseWithBootstrap,
+ common_auth.AuthTestMixin,
+ _SystemUserTests,
+ _SystemMemberAndReaderTagTests):
+
+ def setUp(self):
+ super(SystemReaderTests, self).setUp()
+ self.loadapp()
+ self.useFixture(ksfixtures.Policy(self.config_fixture))
+ self.config_fixture.config(group='oslo_policy', enforce_scope=True)
+
+ system_reader = unit.new_user_ref(
+ domain_id=CONF.identity.default_domain_id
+ )
+ self.user_id = PROVIDERS.identity_api.create_user(
+ system_reader
+ )['id']
+ PROVIDERS.assignment_api.create_system_grant_for_user(
+ self.user_id, self.bootstrapper.reader_role_id
+ )
+
+ auth = self.build_authentication_request(
+ user_id=self.user_id, password=system_reader['password'],
+ system=True
+ )
+
+ # Grab a token using the persona we're testing and prepare headers
+ # for requests we'll be making in the tests.
+ with self.test_client() as c:
+ r = c.post('/v3/auth/tokens', json=auth)
+ self.token_id = r.headers['X-Subject-Token']
+ self.headers = {'X-Auth-Token': self.token_id}
+
+
+class SystemMemberTests(base_classes.TestCaseWithBootstrap,
+ common_auth.AuthTestMixin,
+ _SystemUserTests,
+ _SystemMemberAndReaderTagTests):
+
+ def setUp(self):
+ super(SystemMemberTests, self).setUp()
+ self.loadapp()
+ self.useFixture(ksfixtures.Policy(self.config_fixture))
+ self.config_fixture.config(group='oslo_policy', enforce_scope=True)
+
+ system_member = unit.new_user_ref(
+ domain_id=CONF.identity.default_domain_id
+ )
+ self.user_id = PROVIDERS.identity_api.create_user(
+ system_member
+ )['id']
+ PROVIDERS.assignment_api.create_system_grant_for_user(
+ self.user_id, self.bootstrapper.member_role_id
+ )
+
+ auth = self.build_authentication_request(
+ user_id=self.user_id, password=system_member['password'],
+ system=True
+ )
+
+ # Grab a token using the persona we're testing and prepare headers
+ # for requests we'll be making in the tests.
+ with self.test_client() as c:
+ r = c.post('/v3/auth/tokens', json=auth)
+ self.token_id = r.headers['X-Subject-Token']
+ self.headers = {'X-Auth-Token': self.token_id}
+
+
+class SystemAdminTests(base_classes.TestCaseWithBootstrap,
+ common_auth.AuthTestMixin,
+ _SystemUserTests):
+
+ def setUp(self):
+ super(SystemAdminTests, self).setUp()
+ self.loadapp()
+ self.useFixture(ksfixtures.Policy(self.config_fixture))
+ self.config_fixture.config(group='oslo_policy', enforce_scope=True)
+
+ self.user_id = self.bootstrapper.admin_user_id
+ auth = self.build_authentication_request(
+ user_id=self.user_id,
+ password=self.bootstrapper.admin_password,
+ system=True
+ )
+
+ # Grab a token using the persona we're testing and prepare headers
+ # for requests we'll be making in the tests.
+ with self.test_client() as c:
+ r = c.post('/v3/auth/tokens', json=auth)
+ self.token_id = r.headers['X-Subject-Token']
+ self.headers = {'X-Auth-Token': self.token_id}
+
+ def test_user_can_create_project_tag(self):
+ project = PROVIDERS.resource_api.create_project(
+ uuid.uuid4().hex,
+ unit.new_project_ref(domain_id=CONF.identity.default_domain_id)
+ )
+
+ tag = uuid.uuid4().hex
+
+ with self.test_client() as c:
+ c.put(
+ '/v3/projects/%s/tags/%s' % (project['id'], tag),
+ headers=self.headers,
+ expected_status_code=http_client.CREATED
+ )
+
+ def test_user_can_update_project_tag(self):
+ project = PROVIDERS.resource_api.create_project(
+ uuid.uuid4().hex,
+ unit.new_project_ref(domain_id=CONF.identity.default_domain_id)
+ )
+
+ tag = uuid.uuid4().hex
+ PROVIDERS.resource_api.create_project_tag(project['id'], tag)
+
+ update = {"tags": [uuid.uuid4().hex]}
+
+ with self.test_client() as c:
+ c.put(
+ '/v3/projects/%s/tags' % project['id'], headers=self.headers,
+ json=update,
+ expected_status_code=http_client.OK
+ )
+
+ def test_user_can_delete_project_tag(self):
+ project = PROVIDERS.resource_api.create_project(
+ uuid.uuid4().hex,
+ unit.new_project_ref(domain_id=CONF.identity.default_domain_id)
+ )
+
+ tag = uuid.uuid4().hex
+ PROVIDERS.resource_api.create_project_tag(project['id'], tag)
+
+ with self.test_client() as c:
+ c.delete(
+ '/v3/projects/%s/tags/%s' % (project['id'], tag),
+ headers=self.headers
+ )
+
+
+class _DomainUserTagTests(object):
+
+ def test_user_can_get_tag_for_project_in_domain(self):
+ project = PROVIDERS.resource_api.create_project(
+ uuid.uuid4().hex, unit.new_project_ref(domain_id=self.domain_id)
+ )
+
+ tag = uuid.uuid4().hex
+ PROVIDERS.resource_api.create_project_tag(project['id'], tag)
+
+ with self.test_client() as c:
+ c.get(
+ '/v3/projects/%s/tags/%s' % (project['id'], tag),
+ headers=self.headers,
+ expected_status_code=http_client.NO_CONTENT
+ )
+
+ def test_user_can_list_tags_for_project_in_domain(self):
+ project = PROVIDERS.resource_api.create_project(
+ uuid.uuid4().hex, unit.new_project_ref(domain_id=self.domain_id)
+ )
+
+ tag = uuid.uuid4().hex
+ PROVIDERS.resource_api.create_project_tag(project['id'], tag)
+
+ with self.test_client() as c:
+ r = c.get(
+ '/v3/projects/%s/tags' % project['id'], headers=self.headers
+ )
+ self.assertTrue(len(r.json['tags']) == 1)
+ self.assertEqual(tag, r.json['tags'][0])
+
+ def test_user_cannot_create_project_tag_outside_domain(self):
+ project = PROVIDERS.resource_api.create_project(
+ uuid.uuid4().hex,
+ unit.new_project_ref(domain_id=CONF.identity.default_domain_id)
+ )
+
+ tag = uuid.uuid4().hex
+
+ with self.test_client() as c:
+ c.put(
+ '/v3/projects/%s/tags/%s' % (project['id'], tag),
+ headers=self.headers,
+ expected_status_code=http_client.FORBIDDEN
+ )
+
+ def test_user_cannot_update_project_tag_outside_domain(self):
+ project = PROVIDERS.resource_api.create_project(
+ uuid.uuid4().hex,
+ unit.new_project_ref(domain_id=CONF.identity.default_domain_id)
+ )
+
+ tag = uuid.uuid4().hex
+ PROVIDERS.resource_api.create_project_tag(project['id'], tag)
+
+ new_tag = uuid.uuid4().hex
+ update = {"tags": [new_tag]}
+
+ with self.test_client() as c:
+ c.put(
+ '/v3/projects/%s/tags' % project['id'], headers=self.headers,
+ json=update, expected_status_code=http_client.FORBIDDEN
+ )
+
+ def test_user_cannot_delete_project_tag_outside_domain(self):
+ project = PROVIDERS.resource_api.create_project(
+ uuid.uuid4().hex,
+ unit.new_project_ref(domain_id=CONF.identity.default_domain_id)
+ )
+
+ tag = uuid.uuid4().hex
+ PROVIDERS.resource_api.create_project_tag(project['id'], tag)
+
+ with self.test_client() as c:
+ c.delete(
+ '/v3/projects/%s/tags/%s' % (project['id'], tag),
+ headers=self.headers,
+ expected_status_code=http_client.FORBIDDEN
+ )
+
+ def test_user_cannot_get_tag_for_project_outside_domain(self):
+ project = PROVIDERS.resource_api.create_project(
+ uuid.uuid4().hex,
+ unit.new_project_ref(domain_id=CONF.identity.default_domain_id)
+ )
+
+ tag = uuid.uuid4().hex
+ PROVIDERS.resource_api.create_project_tag(project['id'], tag)
+
+ with self.test_client() as c:
+ c.get(
+ '/v3/projects/%s/tags/%s' % (project['id'], tag),
+ headers=self.headers,
+ expected_status_code=http_client.FORBIDDEN
+ )
+
+ def test_user_cannot_list_tags_for_project_outside_domain(self):
+ project = PROVIDERS.resource_api.create_project(
+ uuid.uuid4().hex,
+ unit.new_project_ref(domain_id=CONF.identity.default_domain_id)
+ )
+
+ tag = uuid.uuid4().hex
+ PROVIDERS.resource_api.create_project_tag(project['id'], tag)
+
+ with self.test_client() as c:
+ c.get(
+ '/v3/projects/%s/tags' % project['id'],
+ headers=self.headers,
+ expected_status_code=http_client.FORBIDDEN
+ )
+
+
+class _DomainMemberAndReaderTagTests(object):
+
+ def test_user_cannot_create_project_tag_in_domain(self):
+ project = PROVIDERS.resource_api.create_project(
+ uuid.uuid4().hex, unit.new_project_ref(domain_id=self.domain_id)
+ )
+
+ tag = uuid.uuid4().hex
+
+ with self.test_client() as c:
+ c.put(
+ '/v3/projects/%s/tags/%s' % (project['id'], tag),
+ headers=self.headers,
+ expected_status_code=http_client.FORBIDDEN
+ )
+
+ def test_user_cannot_update_project_tag_in_domain(self):
+ project = PROVIDERS.resource_api.create_project(
+ uuid.uuid4().hex, unit.new_project_ref(domain_id=self.domain_id)
+ )
+
+ tag = uuid.uuid4().hex
+ PROVIDERS.resource_api.create_project_tag(project['id'], tag)
+
+ new_tag = uuid.uuid4().hex
+ update = {"tags": [new_tag]}
+
+ with self.test_client() as c:
+ c.put(
+ '/v3/projects/%s/tags' % project['id'], headers=self.headers,
+ json=update, expected_status_code=http_client.FORBIDDEN
+ )
+
+ def test_user_cannot_delete_project_tag_in_domain(self):
+ project = PROVIDERS.resource_api.create_project(
+ uuid.uuid4().hex, unit.new_project_ref(domain_id=self.domain_id)
+ )
+
+ tag = uuid.uuid4().hex
+ PROVIDERS.resource_api.create_project_tag(project['id'], tag)
+
+ with self.test_client() as c:
+ c.delete(
+ '/v3/projects/%s/tags/%s' % (project['id'], tag),
+ headers=self.headers,
+ expected_status_code=http_client.FORBIDDEN
+ )
+
+
+class DomainAdminUserTests(base_classes.TestCaseWithBootstrap,
+ common_auth.AuthTestMixin,
+ _DomainUserTagTests):
+
+ def setUp(self):
+ super(DomainAdminUserTests, self).setUp()
+ self.loadapp()
+
+ self.policy_file = self.useFixture(temporaryfile.SecureTempFile())
+ self.policy_file_name = self.policy_file.file_name
+ self.useFixture(
+ ksfixtures.Policy(
+ self.config_fixture, policy_file=self.policy_file_name
+ )
+ )
+
+ _override_policy(self.policy_file_name)
+ self.config_fixture.config(group='oslo_policy', enforce_scope=True)
+
+ domain = PROVIDERS.resource_api.create_domain(
+ uuid.uuid4().hex, unit.new_domain_ref()
+ )
+ self.domain_id = domain['id']
+ domain_admin = unit.new_user_ref(domain_id=self.domain_id)
+ self.user_id = PROVIDERS.identity_api.create_user(domain_admin)['id']
+ PROVIDERS.assignment_api.create_grant(
+ self.bootstrapper.admin_role_id, user_id=self.user_id,
+ domain_id=self.domain_id
+ )
+
+ auth = self.build_authentication_request(
+ user_id=self.user_id, password=domain_admin['password'],
+ domain_id=self.domain_id,
+ )
+
+ # Grab a token using the persona we're testing and prepare headers
+ # for requests we'll be making in the tests.
+ with self.test_client() as c:
+ r = c.post('/v3/auth/tokens', json=auth)
+ self.token_id = r.headers['X-Subject-Token']
+ self.headers = {'X-Auth-Token': self.token_id}
+
+ def test_user_can_create_project_tag_in_domain(self):
+ project = PROVIDERS.resource_api.create_project(
+ uuid.uuid4().hex, unit.new_project_ref(domain_id=self.domain_id)
+ )
+
+ tag = uuid.uuid4().hex
+
+ with self.test_client() as c:
+ c.put(
+ '/v3/projects/%s/tags/%s' % (project['id'], tag),
+ headers=self.headers, expected_status_code=http_client.CREATED
+ )
+
+ def test_user_can_update_project_tag_in_domain(self):
+ project = PROVIDERS.resource_api.create_project(
+ uuid.uuid4().hex, unit.new_project_ref(domain_id=self.domain_id)
+ )
+
+ tag = uuid.uuid4().hex
+ PROVIDERS.resource_api.create_project_tag(project['id'], tag)
+
+ new_tag = uuid.uuid4().hex
+ update = {"tags": [new_tag]}
+
+ with self.test_client() as c:
+ r = c.put(
+ '/v3/projects/%s/tags' % project['id'], headers=self.headers,
+ json=update, expected_status_code=http_client.OK
+ )
+ self.assertTrue(len(r.json['tags']) == 1)
+ self.assertEqual(new_tag, r.json['tags'][0])
+
+ def test_user_can_delete_project_tag_in_domain(self):
+ project = PROVIDERS.resource_api.create_project(
+ uuid.uuid4().hex, unit.new_project_ref(domain_id=self.domain_id)
+ )
+
+ tag = uuid.uuid4().hex
+ PROVIDERS.resource_api.create_project_tag(project['id'], tag)
+
+ with self.test_client() as c:
+ c.delete(
+ '/v3/projects/%s/tags/%s' % (project['id'], tag),
+ headers=self.headers
+ )
+
+
+class DomainMemberUserTests(base_classes.TestCaseWithBootstrap,
+ common_auth.AuthTestMixin,
+ _DomainUserTagTests,
+ _DomainMemberAndReaderTagTests):
+
+ def setUp(self):
+ super(DomainMemberUserTests, self).setUp()
+ self.loadapp()
+
+ self.policy_file = self.useFixture(temporaryfile.SecureTempFile())
+ self.policy_file_name = self.policy_file.file_name
+ self.useFixture(
+ ksfixtures.Policy(
+ self.config_fixture, policy_file=self.policy_file_name
+ )
+ )
+
+ _override_policy(self.policy_file_name)
+ self.config_fixture.config(group='oslo_policy', enforce_scope=True)
+
+ domain = PROVIDERS.resource_api.create_domain(
+ uuid.uuid4().hex, unit.new_domain_ref()
+ )
+ self.domain_id = domain['id']
+ domain_admin = unit.new_user_ref(domain_id=self.domain_id)
+ self.user_id = PROVIDERS.identity_api.create_user(domain_admin)['id']
+ PROVIDERS.assignment_api.create_grant(
+ self.bootstrapper.member_role_id, user_id=self.user_id,
+ domain_id=self.domain_id
+ )
+
+ auth = self.build_authentication_request(
+ user_id=self.user_id, password=domain_admin['password'],
+ domain_id=self.domain_id,
+ )
+
+ # Grab a token using the persona we're testing and prepare headers
+ # for requests we'll be making in the tests.
+ with self.test_client() as c:
+ r = c.post('/v3/auth/tokens', json=auth)
+ self.token_id = r.headers['X-Subject-Token']
+ self.headers = {'X-Auth-Token': self.token_id}
+
+
+class DomainReaderUserTests(base_classes.TestCaseWithBootstrap,
+ common_auth.AuthTestMixin,
+ _DomainUserTagTests,
+ _DomainMemberAndReaderTagTests):
+
+ def setUp(self):
+ super(DomainReaderUserTests, self).setUp()
+ self.loadapp()
+
+ self.policy_file = self.useFixture(temporaryfile.SecureTempFile())
+ self.policy_file_name = self.policy_file.file_name
+ self.useFixture(
+ ksfixtures.Policy(
+ self.config_fixture, policy_file=self.policy_file_name
+ )
+ )
+
+ _override_policy(self.policy_file_name)
+ self.config_fixture.config(group='oslo_policy', enforce_scope=True)
+
+ domain = PROVIDERS.resource_api.create_domain(
+ uuid.uuid4().hex, unit.new_domain_ref()
+ )
+ self.domain_id = domain['id']
+ domain_admin = unit.new_user_ref(domain_id=self.domain_id)
+ self.user_id = PROVIDERS.identity_api.create_user(domain_admin)['id']
+ PROVIDERS.assignment_api.create_grant(
+ self.bootstrapper.reader_role_id, user_id=self.user_id,
+ domain_id=self.domain_id
+ )
+
+ auth = self.build_authentication_request(
+ user_id=self.user_id, password=domain_admin['password'],
+ domain_id=self.domain_id,
+ )
+
+ # Grab a token using the persona we're testing and prepare headers
+ # for requests we'll be making in the tests.
+ with self.test_client() as c:
+ r = c.post('/v3/auth/tokens', json=auth)
+ self.token_id = r.headers['X-Subject-Token']
+ self.headers = {'X-Auth-Token': self.token_id}
+
+
+class _ProjectUserTagTests(object):
+
+ def test_user_can_get_tag_for_project(self):
+ tag = uuid.uuid4().hex
+ PROVIDERS.resource_api.create_project_tag(self.project_id, tag)
+
+ with self.test_client() as c:
+ c.get(
+ '/v3/projects/%s/tags/%s' % (self.project_id, tag),
+ headers=self.headers,
+ expected_status_code=http_client.NO_CONTENT
+ )
+
+ def test_user_can_list_tags_for_project(self):
+ tag = uuid.uuid4().hex
+ PROVIDERS.resource_api.create_project_tag(self.project_id, tag)
+
+ with self.test_client() as c:
+ r = c.get(
+ '/v3/projects/%s/tags' % self.project_id, headers=self.headers
+ )
+ self.assertTrue(len(r.json['tags']) == 1)
+ self.assertEqual(tag, r.json['tags'][0])
+
+ def test_user_cannot_create_tag_for_other_project(self):
+ project = PROVIDERS.resource_api.create_project(
+ uuid.uuid4().hex,
+ unit.new_project_ref(domain_id=CONF.identity.default_domain_id)
+ )
+
+ tag = uuid.uuid4().hex
+
+ with self.test_client() as c:
+ c.put(
+ '/v3/projects/%s/tags/%s' % (project['id'], tag),
+ headers=self.headers,
+ expected_status_code=http_client.FORBIDDEN
+ )
+
+ def test_user_cannot_update_tag_for_other_project(self):
+ project = PROVIDERS.resource_api.create_project(
+ uuid.uuid4().hex,
+ unit.new_project_ref(domain_id=CONF.identity.default_domain_id)
+ )
+
+ tag = uuid.uuid4().hex
+ PROVIDERS.resource_api.create_project_tag(project['id'], tag)
+
+ update = {"tags": [uuid.uuid4().hex]}
+
+ with self.test_client() as c:
+ c.put(
+ '/v3/projects/%s/tags' % project['id'], headers=self.headers,
+ json=update, expected_status_code=http_client.FORBIDDEN
+ )
+
+ def test_user_cannot_delete_tag_for_other_project(self):
+ project = PROVIDERS.resource_api.create_project(
+ uuid.uuid4().hex,
+ unit.new_project_ref(domain_id=CONF.identity.default_domain_id)
+ )
+
+ tag = uuid.uuid4().hex
+ PROVIDERS.resource_api.create_project_tag(project['id'], tag)
+
+ with self.test_client() as c:
+ c.delete(
+ '/v3/projects/%s/tags/%s' % (project['id'], tag),
+ headers=self.headers,
+ expected_status_code=http_client.FORBIDDEN
+ )
+
+ def test_user_cannot_get_tag_for_other_project(self):
+ project = PROVIDERS.resource_api.create_project(
+ uuid.uuid4().hex,
+ unit.new_project_ref(domain_id=CONF.identity.default_domain_id)
+ )
+
+ tag = uuid.uuid4().hex
+ PROVIDERS.resource_api.create_project_tag(project['id'], tag)
+
+ with self.test_client() as c:
+ c.get(
+ '/v3/projects/%s/tags/%s' % (project['id'], tag),
+ headers=self.headers,
+ expected_status_code=http_client.FORBIDDEN
+ )
+
+ def test_user_cannot_list_tags_for_other_project(self):
+ project = PROVIDERS.resource_api.create_project(
+ uuid.uuid4().hex,
+ unit.new_project_ref(domain_id=CONF.identity.default_domain_id)
+ )
+
+ tag = uuid.uuid4().hex
+ PROVIDERS.resource_api.create_project_tag(project['id'], tag)
+
+ with self.test_client() as c:
+ c.get(
+ '/v3/projects/%s/tags' % project['id'],
+ headers=self.headers,
+ expected_status_code=http_client.FORBIDDEN
+ )
+
+
+class _ProjectMemberAndReaderTagTests(object):
+
+ def test_user_cannot_create_project_tag(self):
+ tag = uuid.uuid4().hex
+ with self.test_client() as c:
+ c.put(
+ '/v3/projects/%s/tags/%s' % (self.project_id, tag),
+ headers=self.headers,
+ expected_status_code=http_client.FORBIDDEN
+ )
+
+ def test_user_cannot_update_project_tag(self):
+ tag = uuid.uuid4().hex
+ PROVIDERS.resource_api.create_project_tag(self.project_id, tag)
+
+ update = {"tags": [uuid.uuid4().hex]}
+
+ with self.test_client() as c:
+ c.put(
+ '/v3/projects/%s/tags' % self.project_id, headers=self.headers,
+ json=update, expected_status_code=http_client.FORBIDDEN
+ )
+
+ def test_user_cannot_delete_project_tag(self):
+ tag = uuid.uuid4().hex
+ PROVIDERS.resource_api.create_project_tag(self.project_id, tag)
+
+ with self.test_client() as c:
+ c.delete(
+ '/v3/projects/%s/tags/%s' % (self.project_id, tag),
+ headers=self.headers,
+ expected_status_code=http_client.FORBIDDEN
+ )
+
+
+class ProjectAdminTests(base_classes.TestCaseWithBootstrap,
+ common_auth.AuthTestMixin,
+ _ProjectUserTagTests):
+
+ def setUp(self):
+ super(ProjectAdminTests, self).setUp()
+ self.loadapp()
+
+ self.policy_file = self.useFixture(temporaryfile.SecureTempFile())
+ self.policy_file_name = self.policy_file.file_name
+ self.useFixture(
+ ksfixtures.Policy(
+ self.config_fixture, policy_file=self.policy_file_name
+ )
+ )
+ _override_policy(self.policy_file_name)
+ self.config_fixture.config(group='oslo_policy', enforce_scope=True)
+
+ self.user_id = self.bootstrapper.admin_user_id
+ PROVIDERS.assignment_api.create_grant(
+ self.bootstrapper.admin_role_id, user_id=self.user_id,
+ project_id=self.bootstrapper.project_id
+ )
+ self.project_id = self.bootstrapper.project_id
+
+ auth = self.build_authentication_request(
+ user_id=self.user_id, password=self.bootstrapper.admin_password,
+ project_id=self.bootstrapper.project_id
+ )
+
+ # Grab a token using the persona we're testing and prepare headers
+ # for requests we'll be making in the tests.
+ with self.test_client() as c:
+ r = c.post('/v3/auth/tokens', json=auth)
+ self.token_id = r.headers['X-Subject-Token']
+ self.headers = {'X-Auth-Token': self.token_id}
+
+ def test_user_can_create_project_tag(self):
+ tag = uuid.uuid4().hex
+ with self.test_client() as c:
+ c.put(
+ '/v3/projects/%s/tags/%s' % (self.project_id, tag),
+ headers=self.headers, expected_status_code=http_client.CREATED
+ )
+
+ def test_user_can_update_project_tag(self):
+ tag = uuid.uuid4().hex
+ PROVIDERS.resource_api.create_project_tag(self.project_id, tag)
+
+ update = {"tags": [uuid.uuid4().hex]}
+
+ with self.test_client() as c:
+ c.put(
+ '/v3/projects/%s/tags' % self.project_id, headers=self.headers,
+ json=update, expected_status_code=http_client.OK
+ )
+
+ def test_user_can_delete_project_tag(self):
+ tag = uuid.uuid4().hex
+ PROVIDERS.resource_api.create_project_tag(self.project_id, tag)
+
+ with self.test_client() as c:
+ c.delete(
+ '/v3/projects/%s/tags/%s' % (self.project_id, tag),
+ headers=self.headers
+ )
+
+
+class ProjectMemberTests(base_classes.TestCaseWithBootstrap,
+ common_auth.AuthTestMixin,
+ _ProjectUserTagTests,
+ _ProjectMemberAndReaderTagTests):
+
+ def setUp(self):
+ super(ProjectMemberTests, self).setUp()
+ self.loadapp()
+
+ self.policy_file = self.useFixture(temporaryfile.SecureTempFile())
+ self.policy_file_name = self.policy_file.file_name
+ self.useFixture(
+ ksfixtures.Policy(
+ self.config_fixture, policy_file=self.policy_file_name
+ )
+ )
+ _override_policy(self.policy_file_name)
+ self.config_fixture.config(group='oslo_policy', enforce_scope=True)
+
+ project = PROVIDERS.resource_api.create_project(
+ uuid.uuid4().hex,
+ unit.new_project_ref(domain_id=CONF.identity.default_domain_id)
+ )
+ self.project_id = project['id']
+ self.user_id = self.bootstrapper.admin_user_id
+
+ PROVIDERS.assignment_api.create_grant(
+ self.bootstrapper.member_role_id, user_id=self.user_id,
+ project_id=self.project_id
+ )
+
+ auth = self.build_authentication_request(
+ user_id=self.user_id, password=self.bootstrapper.admin_password,
+ project_id=self.project_id
+ )
+
+ # Grab a token using the persona we're testing and prepare headers
+ # for requests we'll be making in the tests.
+ with self.test_client() as c:
+ r = c.post('/v3/auth/tokens', json=auth)
+ self.token_id = r.headers['X-Subject-Token']
+ self.headers = {'X-Auth-Token': self.token_id}
+
+
+class ProjectReaderTests(base_classes.TestCaseWithBootstrap,
+ common_auth.AuthTestMixin,
+ _ProjectUserTagTests,
+ _ProjectMemberAndReaderTagTests):
+
+ def setUp(self):
+ super(ProjectReaderTests, self).setUp()
+ self.loadapp()
+
+ self.policy_file = self.useFixture(temporaryfile.SecureTempFile())
+ self.policy_file_name = self.policy_file.file_name
+ self.useFixture(
+ ksfixtures.Policy(
+ self.config_fixture, policy_file=self.policy_file_name
+ )
+ )
+ _override_policy(self.policy_file_name)
+ self.config_fixture.config(group='oslo_policy', enforce_scope=True)
+
+ project = PROVIDERS.resource_api.create_project(
+ uuid.uuid4().hex,
+ unit.new_project_ref(domain_id=CONF.identity.default_domain_id)
+ )
+ self.project_id = project['id']
+ self.user_id = self.bootstrapper.admin_user_id
+
+ PROVIDERS.assignment_api.create_grant(
+ self.bootstrapper.reader_role_id, user_id=self.user_id,
+ project_id=self.project_id
+ )
+
+ auth = self.build_authentication_request(
+ user_id=self.user_id, password=self.bootstrapper.admin_password,
+ project_id=self.project_id
+ )
+
+ # Grab a token using the persona we're testing and prepare headers
+ # for requests we'll be making in the tests.
+ with self.test_client() as c:
+ r = c.post('/v3/auth/tokens', json=auth)
+ self.token_id = r.headers['X-Subject-Token']
+ self.headers = {'X-Auth-Token': self.token_id}
diff --git a/keystone/tests/unit/base_classes.py b/keystone/tests/unit/base_classes.py
index bfc590b8c..95bf7fa02 100644
--- a/keystone/tests/unit/base_classes.py
+++ b/keystone/tests/unit/base_classes.py
@@ -67,6 +67,7 @@ class TestCaseWithBootstrap(core.BaseTestCase):
self.bootstrapper.admin_role_name = 'admin'
self.bootstrapper.service_name = 'keystone'
self.bootstrapper.public_url = 'http://localhost/identity/'
+ self.bootstrapper.immutable_roles = True
try:
PROVIDERS.resource_api.create_domain(
diff --git a/keystone/tests/unit/credential/test_backend_sql.py b/keystone/tests/unit/credential/test_backend_sql.py
index c21c87176..45836e429 100644
--- a/keystone/tests/unit/credential/test_backend_sql.py
+++ b/keystone/tests/unit/credential/test_backend_sql.py
@@ -21,6 +21,8 @@ from keystone.tests.unit import default_fixtures
from keystone.tests.unit import ksfixtures
from keystone.tests.unit.ksfixtures import database
+from keystone.credential.backends import sql as credential_sql
+
PROVIDERS = provider_api.ProviderAPIs
@@ -90,3 +92,15 @@ class SqlCredential(SqlTests):
def test_backend_credential_sql_no_hints(self):
credentials = PROVIDERS.credential_api.list_credentials()
self._validate_credential_list(credentials, self.user_credentials)
+
+ def test_backend_credential_sql_encrypted_string(self):
+ cred_dict = {
+ 'id': uuid.uuid4().hex,
+ 'type': uuid.uuid4().hex,
+ 'hash': uuid.uuid4().hex,
+ 'encrypted_blob': b'randomdata'
+ }
+ ref = credential_sql.CredentialModel.from_dict(cred_dict)
+ # Make sure CredentialModel is handing over a text string
+ # to the database. To avoid encoding issues
+ self.assertIsInstance(ref.encrypted_blob, str)
diff --git a/keystone/tests/unit/test_backend_ldap.py b/keystone/tests/unit/test_backend_ldap.py
index 8328beec5..cfded416a 100644
--- a/keystone/tests/unit/test_backend_ldap.py
+++ b/keystone/tests/unit/test_backend_ldap.py
@@ -2064,9 +2064,17 @@ class LDAPIdentityEnabledEmulation(LDAPIdentity, unit.TestCase):
"Enabled emulation conflicts with enabled mask")
def test_user_enabled_use_group_config(self):
+ # Establish enabled-emulation group name to later query its members
+ group_name = 'enabled_users'
+ driver = PROVIDERS.identity_api._select_identity_driver(
+ CONF.identity.default_domain_id)
+ group_dn = 'cn=%s,%s' % (group_name, driver.group.tree_dn)
+
self.config_fixture.config(
group='ldap',
user_enabled_emulation_use_group_config=True,
+ user_enabled_emulation_dn=group_dn,
+ group_name_attribute='cn',
group_member_attribute='uniqueMember',
group_objectclass='groupOfUniqueNames')
self.ldapdb.clear()
@@ -2082,6 +2090,44 @@ class LDAPIdentityEnabledEmulation(LDAPIdentity, unit.TestCase):
user_ref = PROVIDERS.identity_api.get_user(user_ref['id'])
self.assertIs(True, user_ref['enabled'])
+ # Ensure state matches the group config
+ group_ref = PROVIDERS.identity_api.get_group_by_name(group_name,
+ CONF.identity.default_domain_id)
+ PROVIDERS.identity_api.check_user_in_group(user_ref['id'], group_ref['id'])
+
+ def test_user_enabled_use_group_config_with_ids(self):
+ # Establish enabled-emulation group name to later query its members
+ group_name = 'enabled_users'
+ driver = PROVIDERS.identity_api._select_identity_driver(
+ CONF.identity.default_domain_id)
+ group_dn = 'cn=%s,%s' % (group_name, driver.group.tree_dn)
+
+ self.config_fixture.config(
+ group='ldap',
+ user_enabled_emulation_use_group_config=True,
+ user_enabled_emulation_dn=group_dn,
+ group_name_attribute='cn',
+ group_member_attribute='memberUid',
+ group_members_are_ids=True,
+ group_objectclass='posixGroup')
+ self.ldapdb.clear()
+ self.load_backends()
+
+ # Create a user and ensure they are enabled.
+ user1 = unit.new_user_ref(enabled=True,
+ domain_id=CONF.identity.default_domain_id)
+ user_ref = PROVIDERS.identity_api.create_user(user1)
+ self.assertIs(True, user_ref['enabled'])
+
+ # Get a user and ensure they are enabled.
+ user_ref = PROVIDERS.identity_api.get_user(user_ref['id'])
+ self.assertIs(True, user_ref['enabled'])
+
+ # Ensure state matches the group config
+ group_ref = PROVIDERS.identity_api.get_group_by_name(group_name,
+ CONF.identity.default_domain_id)
+ PROVIDERS.identity_api.check_user_in_group(user_ref['id'], group_ref['id'])
+
def test_user_enabled_invert(self):
self.config_fixture.config(group='ldap', user_enabled_invert=True,
user_enabled_default='False')
diff --git a/keystone/tests/unit/test_cli.py b/keystone/tests/unit/test_cli.py
index 42c771a9f..cdc0e1327 100644
--- a/keystone/tests/unit/test_cli.py
+++ b/keystone/tests/unit/test_cli.py
@@ -1866,6 +1866,32 @@ class TestMappingEngineTester(unit.BaseTestCase):
self.assertRaises(exception.ValidationError,
mapping_engine.main)
+ def test_mapping_engine_tester_logs_direct_maps(self):
+ tempfilejson = self.useFixture(temporaryfile.SecureTempFile())
+ tmpfilejsonname = tempfilejson.file_name
+ updated_mapping = copy.deepcopy(mapping_fixtures.MAPPING_SMALL)
+ with open(tmpfilejsonname, 'w') as f:
+ f.write(jsonutils.dumps(updated_mapping))
+ self.command_rules = tmpfilejsonname
+ tempfile = self.useFixture(temporaryfile.SecureTempFile())
+ tmpfilename = tempfile.file_name
+ with open(tmpfilename, 'w') as f:
+ f.write("\n")
+ f.write("UserName:me\n")
+ f.write("orgPersonType:NoContractor\n")
+ f.write("LastName:Bo\n")
+ f.write("FirstName:Jill\n")
+ self.command_input = tmpfilename
+ self.command_prefix = None
+ self.command_engine_debug = True
+ self.useFixture(fixtures.MockPatchObject(
+ CONF, 'command', self.FakeConfCommand(self)))
+ mapping_engine = cli.MappingEngineTester()
+ logging = self.useFixture(fixtures.FakeLogger(level=log.DEBUG))
+ mapping_engine.main()
+ expected_msg = "direct_maps: [['me']]"
+ self.assertThat(logging.output, matchers.Contains(expected_msg))
+
class CliStatusTestCase(unit.SQLDriverOverrides, unit.TestCase):
diff --git a/keystone/tests/unit/test_limits.py b/keystone/tests/unit/test_limits.py
index bd35b2ac3..947a461c7 100644
--- a/keystone/tests/unit/test_limits.py
+++ b/keystone/tests/unit/test_limits.py
@@ -560,6 +560,22 @@ class LimitsTestCase(test_v3.RestfulTestCase):
def setUp(self):
super(LimitsTestCase, self).setUp()
+ # FIXME(lbragstad): Remove all this duplicated logic once we get all
+ # keystone tests using bootstrap consistently. This is something the
+ # bootstrap utility already does for us.
+ reader_role = {'id': uuid.uuid4().hex, 'name': 'reader'}
+ reader_role = PROVIDERS.role_api.create_role(
+ reader_role['id'], reader_role
+ )
+
+ member_role = {'id': uuid.uuid4().hex, 'name': 'member'}
+ member_role = PROVIDERS.role_api.create_role(
+ member_role['id'], member_role
+ )
+ PROVIDERS.role_api.create_implied_role(self.role_id, member_role['id'])
+ PROVIDERS.role_api.create_implied_role(
+ member_role['id'], reader_role['id']
+ )
# Most of these tests require system-scoped tokens. Let's have one on
# hand so that we can use it in tests when we need it.
@@ -927,6 +943,8 @@ class LimitsTestCase(test_v3.RestfulTestCase):
def test_list_limit_with_project_id_filter(self):
# create two limit in different projects for test.
+ self.config_fixture.config(group='oslo_policy',
+ enforce_scope=True)
ref1 = unit.new_limit_ref(project_id=self.project_id,
service_id=self.service_id,
region_id=self.region_id,
@@ -955,21 +973,19 @@ class LimitsTestCase(test_v3.RestfulTestCase):
self.assertEqual(1, len(limits))
self.assertEqual(self.project_2_id, limits[0]['project_id'])
- # if non system scoped request contain project_id filter, keystone
- # will return an empty list.
+ # any project user can filter by their own project
r = self.get(
'/limits?project_id=%s' % self.project_id,
expected_status=http_client.OK)
limits = r.result['limits']
- self.assertEqual(0, len(limits))
+ self.assertEqual(1, len(limits))
+ self.assertEqual(self.project_id, limits[0]['project_id'])
# a system scoped request can specify the project_id filter
r = self.get(
'/limits?project_id=%s' % self.project_id,
expected_status=http_client.OK,
- auth=self.build_authentication_request(
- user_id=self.user['id'], password=self.user['password'],
- system=True)
+ token=self.system_admin_token
)
limits = r.result['limits']
self.assertEqual(1, len(limits))
@@ -1046,6 +1062,7 @@ class LimitsTestCase(test_v3.RestfulTestCase):
else:
id1 = r.result['limits'][1]['id']
self.get('/limits/fake_id',
+ token=self.system_admin_token,
expected_status=http_client.NOT_FOUND)
r = self.get('/limits/%s' % id1,
expected_status=http_client.OK)
diff --git a/keystone/tests/unit/test_policy.py b/keystone/tests/unit/test_policy.py
index 53f1e7726..fe0b8bba2 100644
--- a/keystone/tests/unit/test_policy.py
+++ b/keystone/tests/unit/test_policy.py
@@ -206,6 +206,7 @@ class PolicyJsonTestCase(unit.TestCase):
'identity:create_group',
'identity:create_identity_provider',
'identity:create_implied_role',
+ 'identity:create_limits',
'identity:create_mapping',
'identity:create_policy',
'identity:create_policy_association_for_endpoint',
@@ -237,6 +238,7 @@ class PolicyJsonTestCase(unit.TestCase):
'identity:delete_identity_provider',
'identity:delete_implied_role',
'identity:delete_mapping',
+ 'identity:delete_limit',
'identity:delete_policy',
'identity:delete_policy_association_for_endpoint',
'identity:delete_policy_association_for_region_and_service',
@@ -276,10 +278,12 @@ class PolicyJsonTestCase(unit.TestCase):
'identity:get_group',
'identity:get_identity_provider',
'identity:get_implied_role',
+ 'identity:get_limit',
'identity:get_limit_model',
'identity:get_mapping',
'identity:get_policy',
'identity:get_policy_for_endpoint',
+ 'identity:get_project_tag',
'identity:get_project',
'identity:get_protocol',
'identity:get_region',
@@ -318,6 +322,7 @@ class PolicyJsonTestCase(unit.TestCase):
'identity:list_projects_associated_with_endpoint_group',
'identity:list_projects_for_endpoint',
'identity:list_projects_for_user',
+ 'identity:list_project_tags',
'identity:list_protocols',
'identity:list_regions',
'identity:list_registered_limits',
@@ -352,6 +357,7 @@ class PolicyJsonTestCase(unit.TestCase):
'identity:update_endpoint_group',
'identity:update_group',
'identity:update_identity_provider',
+ 'identity:update_limit',
'identity:update_mapping',
'identity:update_policy',
'identity:update_project',
diff --git a/keystone/tests/unit/test_v3_auth.py b/keystone/tests/unit/test_v3_auth.py
index 926c10286..e8da2e946 100644
--- a/keystone/tests/unit/test_v3_auth.py
+++ b/keystone/tests/unit/test_v3_auth.py
@@ -2614,6 +2614,22 @@ class TokenAPITests(object):
allow_expired=True,
expected_status=http_client.NOT_FOUND)
+ def test_system_scoped_token_works_with_domain_specific_drivers(self):
+ self.config_fixture.config(
+ group='identity', domain_specific_drivers_enabled=True
+ )
+
+ PROVIDERS.assignment_api.create_system_grant_for_user(
+ self.user['id'], self.role['id']
+ )
+
+ token_id = self.get_system_scoped_token()
+ headers = {'X-Auth-Token': token_id}
+
+ app = self.loadapp()
+ with app.test_client() as c:
+ c.get('/v3/users', headers=headers)
+
class TokenDataTests(object):
"""Test the data in specific token types."""
diff --git a/keystone/tests/unit/test_v3_federation.py b/keystone/tests/unit/test_v3_federation.py
index 9b27f1bad..a24ddfc46 100644
--- a/keystone/tests/unit/test_v3_federation.py
+++ b/keystone/tests/unit/test_v3_federation.py
@@ -4886,6 +4886,16 @@ class WebSSOTests(FederatedTokenTests):
auth_api.AuthFederationWebSSOResource._perform_auth,
self.PROTOCOL)
+ def test_federated_sso_auth_protocol_not_found(self):
+ environment = {self.REMOTE_ID_ATTR: self.REMOTE_IDS[0],
+ 'QUERY_STRING': 'origin=%s' % self.ORIGIN}
+ environment.update(mapping_fixtures.EMPLOYEE_ASSERTION)
+ with self.make_request(environ=environment):
+ self.assertRaises(
+ exception.Unauthorized,
+ auth_api.AuthFederationWebSSOResource._perform_auth,
+ 'no_this_protocol')
+
def test_federated_sso_untrusted_dashboard(self):
environment = {self.REMOTE_ID_ATTR: self.REMOTE_IDS[0],
'QUERY_STRING': 'origin=%s' % uuid.uuid4().hex}
diff --git a/keystone/tests/unit/token/test_fernet_provider.py b/keystone/tests/unit/token/test_fernet_provider.py
index b2661b680..a3e6d870c 100644
--- a/keystone/tests/unit/token/test_fernet_provider.py
+++ b/keystone/tests/unit/token/test_fernet_provider.py
@@ -352,21 +352,73 @@ class TestPayloads(unit.TestCase):
actual_time_float)
self.assertEqual(expected_time_str, actual_time_str)
+ def test_convert_or_decode_uuid_bytes(self):
+ payload_cls = token_formatters.BasePayload
+
+ expected_hex_uuid = uuid.uuid4().hex
+ uuid_obj = uuid.UUID(expected_hex_uuid)
+ expected_uuid_in_bytes = uuid_obj.bytes
+
+ actual_hex_uuid = payload_cls._convert_or_decode(
+ is_stored_as_bytes=True,
+ value=expected_uuid_in_bytes
+ )
+
+ self.assertEqual(expected_hex_uuid, actual_hex_uuid)
+
+ def test_convert_or_decode_binary_type(self):
+ payload_cls = token_formatters.BasePayload
+
+ expected_hex_uuid = uuid.uuid4().hex
+
+ actual_hex_uuid = payload_cls._convert_or_decode(
+ is_stored_as_bytes=False,
+ value=expected_hex_uuid.encode('utf-8')
+ )
+
+ self.assertEqual(expected_hex_uuid, actual_hex_uuid)
+
+ def test_convert_or_decode_text_type(self):
+ payload_cls = token_formatters.BasePayload
+
+ expected_hex_uuid = uuid.uuid4().hex
+
+ actual_hex_uuid = payload_cls._convert_or_decode(
+ is_stored_as_bytes=False,
+ value=expected_hex_uuid
+ )
+
+ self.assertEqual(expected_hex_uuid, actual_hex_uuid)
+
def _test_payload(self, payload_class, exp_user_id=None, exp_methods=None,
exp_system=None, exp_project_id=None, exp_domain_id=None,
exp_trust_id=None, exp_federated_group_ids=None,
exp_identity_provider_id=None, exp_protocol_id=None,
- exp_access_token_id=None, exp_app_cred_id=None):
+ exp_access_token_id=None, exp_app_cred_id=None,
+ encode_ids=False):
+ def _encode_id(value):
+ if value is not None and six.text_type(value) and encode_ids:
+ return value.encode('utf-8')
+ return value
exp_user_id = exp_user_id or uuid.uuid4().hex
exp_methods = exp_methods or ['password']
exp_expires_at = utils.isotime(timeutils.utcnow(), subsecond=True)
exp_audit_ids = [provider.random_urlsafe_str()]
payload = payload_class.assemble(
- exp_user_id, exp_methods, exp_system, exp_project_id,
- exp_domain_id, exp_expires_at, exp_audit_ids, exp_trust_id,
- exp_federated_group_ids, exp_identity_provider_id, exp_protocol_id,
- exp_access_token_id, exp_app_cred_id)
+ _encode_id(exp_user_id),
+ exp_methods,
+ _encode_id(exp_system),
+ _encode_id(exp_project_id),
+ exp_domain_id,
+ exp_expires_at,
+ exp_audit_ids,
+ exp_trust_id,
+ _encode_id(exp_federated_group_ids),
+ _encode_id(exp_identity_provider_id),
+ exp_protocol_id,
+ _encode_id(exp_access_token_id),
+ _encode_id(exp_app_cred_id))
(user_id, methods, system, project_id,
domain_id, expires_at, audit_ids,
@@ -429,6 +481,12 @@ class TestPayloads(unit.TestCase):
exp_user_id='0123456789abcdef',
exp_project_id='0123456789abcdef')
+ def test_project_scoped_payload_with_binary_encoded_ids(self):
+ self._test_payload(token_formatters.ProjectScopedPayload,
+ exp_user_id='someNonUuidUserId',
+ exp_project_id='someNonUuidProjectId',
+ encode_ids=True)
+
def test_domain_scoped_payload_with_non_uuid_user_id(self):
self._test_payload(token_formatters.DomainScopedPayload,
exp_user_id='nonUuidUserId',
diff --git a/keystone/token/token_formatters.py b/keystone/token/token_formatters.py
index cfd22010a..182755cc6 100644
--- a/keystone/token/token_formatters.py
+++ b/keystone/token/token_formatters.py
@@ -313,9 +313,11 @@ class BasePayload(object):
"""
try:
return (True, cls.convert_uuid_hex_to_bytes(value))
- except ValueError:
- # this might not be a UUID, depending on the situation (i.e.
- # federation)
+ except (ValueError, TypeError):
+ # ValueError: this might not be a UUID, depending on the
+ # situation (i.e. federation)
+ # TypeError: the provided value may be binary encoded
+ # in which case just return the value (i.e. Python 3)
return (False, value)
@classmethod
@@ -344,6 +346,22 @@ class BasePayload(object):
# restore the padding (==) at the end of the string
return base64.urlsafe_b64decode(s + '==')
+ @classmethod
+ def _convert_or_decode(cls, is_stored_as_bytes, value):
+ """Convert a value to text type, translating uuid -> hex if required.
+
+ :param is_stored_as_bytes: whether value is already bytes
+ :type is_stored_as_bytes: six.boolean
+ :param value: value to attempt to convert to bytes
+ :type value: six.text_type or six.binary_type
+ :rtype: six.text_type
+ """
+ if is_stored_as_bytes:
+ return cls.convert_uuid_bytes_to_hex(value)
+ elif isinstance(value, six.binary_type):
+ return value.decode('utf-8')
+ return value
+
class UnscopedPayload(BasePayload):
version = 0
@@ -363,8 +381,7 @@ class UnscopedPayload(BasePayload):
@classmethod
def disassemble(cls, payload):
(is_stored_as_bytes, user_id) = payload[0]
- if is_stored_as_bytes:
- user_id = cls.convert_uuid_bytes_to_hex(user_id)
+ user_id = cls._convert_or_decode(is_stored_as_bytes, user_id)
methods = auth_plugins.convert_integer_to_method_list(payload[1])
expires_at_str = cls._convert_float_to_time_string(payload[2])
audit_ids = list(map(cls.base64_encode, payload[3]))
@@ -409,8 +426,7 @@ class DomainScopedPayload(BasePayload):
@classmethod
def disassemble(cls, payload):
(is_stored_as_bytes, user_id) = payload[0]
- if is_stored_as_bytes:
- user_id = cls.convert_uuid_bytes_to_hex(user_id)
+ user_id = cls._convert_or_decode(is_stored_as_bytes, user_id)
methods = auth_plugins.convert_integer_to_method_list(payload[1])
try:
domain_id = cls.convert_uuid_bytes_to_hex(payload[2])
@@ -457,12 +473,10 @@ class ProjectScopedPayload(BasePayload):
@classmethod
def disassemble(cls, payload):
(is_stored_as_bytes, user_id) = payload[0]
- if is_stored_as_bytes:
- user_id = cls.convert_uuid_bytes_to_hex(user_id)
+ user_id = cls._convert_or_decode(is_stored_as_bytes, user_id)
methods = auth_plugins.convert_integer_to_method_list(payload[1])
(is_stored_as_bytes, project_id) = payload[2]
- if is_stored_as_bytes:
- project_id = cls.convert_uuid_bytes_to_hex(project_id)
+ project_id = cls._convert_or_decode(is_stored_as_bytes, project_id)
expires_at_str = cls._convert_float_to_time_string(payload[3])
audit_ids = list(map(cls.base64_encode, payload[4]))
system = None
@@ -501,12 +515,10 @@ class TrustScopedPayload(BasePayload):
@classmethod
def disassemble(cls, payload):
(is_stored_as_bytes, user_id) = payload[0]
- if is_stored_as_bytes:
- user_id = cls.convert_uuid_bytes_to_hex(user_id)
+ user_id = cls._convert_or_decode(is_stored_as_bytes, user_id)
methods = auth_plugins.convert_integer_to_method_list(payload[1])
(is_stored_as_bytes, project_id) = payload[2]
- if is_stored_as_bytes:
- project_id = cls.convert_uuid_bytes_to_hex(project_id)
+ project_id = cls._convert_or_decode(is_stored_as_bytes, project_id)
expires_at_str = cls._convert_float_to_time_string(payload[3])
audit_ids = list(map(cls.base64_encode, payload[4]))
trust_id = cls.convert_uuid_bytes_to_hex(payload[5])
@@ -533,8 +545,7 @@ class FederatedUnscopedPayload(BasePayload):
@classmethod
def unpack_group_id(cls, group_id_in_bytes):
(is_stored_as_bytes, group_id) = group_id_in_bytes
- if is_stored_as_bytes:
- group_id = cls.convert_uuid_bytes_to_hex(group_id)
+ group_id = cls._convert_or_decode(is_stored_as_bytes, group_id)
return {'id': group_id}
@classmethod
@@ -556,24 +567,11 @@ class FederatedUnscopedPayload(BasePayload):
@classmethod
def disassemble(cls, payload):
(is_stored_as_bytes, user_id) = payload[0]
- if is_stored_as_bytes:
- user_id = cls.convert_uuid_bytes_to_hex(user_id)
- else:
- # NOTE(cmurphy): The user ID of shadowed federated users is no
- # longer a UUID but a sha256 hash string, and so it should not be
- # converted to a byte string since it is not a UUID format.
- # However. on python3 msgpack returns the serialized input as a
- # byte string anyway. Similar to other msgpack'd values in the
- # payload, we need to explicitly decode it to a string value.
- if six.PY3 and isinstance(user_id, six.binary_type):
- user_id = user_id.decode('utf-8')
+ user_id = cls._convert_or_decode(is_stored_as_bytes, user_id)
methods = auth_plugins.convert_integer_to_method_list(payload[1])
group_ids = list(map(cls.unpack_group_id, payload[2]))
(is_stored_as_bytes, idp_id) = payload[3]
- if is_stored_as_bytes:
- idp_id = cls.convert_uuid_bytes_to_hex(idp_id)
- else:
- idp_id = idp_id.decode('utf-8')
+ idp_id = cls._convert_or_decode(is_stored_as_bytes, idp_id)
protocol_id = payload[4]
if isinstance(protocol_id, six.binary_type):
protocol_id = protocol_id.decode('utf-8')
@@ -614,38 +612,10 @@ class FederatedScopedPayload(FederatedUnscopedPayload):
@classmethod
def disassemble(cls, payload):
(is_stored_as_bytes, user_id) = payload[0]
- if is_stored_as_bytes:
- user_id = cls.convert_uuid_bytes_to_hex(user_id)
- else:
- # NOTE(cmurphy): The user ID of shadowed federated users is no
- # longer a UUID but a sha256 hash string, and so it should not be
- # converted to a byte string since it is not a UUID format.
- # However. on python3 msgpack returns the serialized input as a
- # byte string anyway. Similar to other msgpack'd values in the
- # payload, we need to explicitly decode it to a string value.
- if six.PY3 and isinstance(user_id, six.binary_type):
- user_id = user_id.decode('utf-8')
+ user_id = cls._convert_or_decode(is_stored_as_bytes, user_id)
methods = auth_plugins.convert_integer_to_method_list(payload[1])
(is_stored_as_bytes, scope_id) = payload[2]
- if is_stored_as_bytes:
- scope_id = cls.convert_uuid_bytes_to_hex(scope_id)
- else:
- # NOTE(lbragstad): We assembled the token payload scope as a tuple
- # (False, domain_id) for cases like (False, 'default'), since the
- # default domain ID isn't converted to a byte string when it's not
- # in UUID format. Despite the boolean indicator in the tuple that
- # denotes if the value is stored as a byte string or not, msgpack
- # apparently returns the serialized input as byte strings anyway.
- # For example, this means what we though we were passing in as
- # (False, 'default') during token creation actually comes out as
- # (False, b'default') in token validation through msgpack, which
- # clearly isn't correct according to our boolean indicator. This
- # causes comparison issues due to different string types (e.g.,
- # b'default' != 'default') with python 3. See bug 1813085 for
- # details. We use this pattern for other strings in the payload
- # like idp_id and protocol_id for the same reason.
- if six.PY3 and isinstance(scope_id, six.binary_type):
- scope_id = scope_id.decode('utf-8')
+ scope_id = cls._convert_or_decode(is_stored_as_bytes, scope_id)
project_id = (
scope_id
if cls.version == FederatedProjectScopedPayload.version else None)
@@ -654,11 +624,7 @@ class FederatedScopedPayload(FederatedUnscopedPayload):
if cls.version == FederatedDomainScopedPayload.version else None)
group_ids = list(map(cls.unpack_group_id, payload[3]))
(is_stored_as_bytes, idp_id) = payload[4]
- if is_stored_as_bytes:
- idp_id = cls.convert_uuid_bytes_to_hex(idp_id)
- else:
- if six.PY3 and isinstance(idp_id, six.binary_type):
- idp_id = idp_id.decode('utf-8')
+ idp_id = cls._convert_or_decode(is_stored_as_bytes, idp_id)
protocol_id = payload[5]
if six.PY3 and isinstance(protocol_id, six.binary_type):
protocol_id = protocol_id.decode('utf-8')
@@ -703,15 +669,13 @@ class OauthScopedPayload(BasePayload):
@classmethod
def disassemble(cls, payload):
(is_stored_as_bytes, user_id) = payload[0]
- if is_stored_as_bytes:
- user_id = cls.convert_uuid_bytes_to_hex(user_id)
+ user_id = cls._convert_or_decode(is_stored_as_bytes, user_id)
methods = auth_plugins.convert_integer_to_method_list(payload[1])
(is_stored_as_bytes, project_id) = payload[2]
- if is_stored_as_bytes:
- project_id = cls.convert_uuid_bytes_to_hex(project_id)
+ project_id = cls._convert_or_decode(is_stored_as_bytes, project_id)
(is_stored_as_bytes, access_token_id) = payload[3]
- if is_stored_as_bytes:
- access_token_id = cls.convert_uuid_bytes_to_hex(access_token_id)
+ access_token_id = cls._convert_or_decode(is_stored_as_bytes,
+ access_token_id)
expires_at_str = cls._convert_float_to_time_string(payload[4])
audit_ids = list(map(cls.base64_encode, payload[5]))
system = None
@@ -746,8 +710,7 @@ class SystemScopedPayload(BasePayload):
@classmethod
def disassemble(cls, payload):
(is_stored_as_bytes, user_id) = payload[0]
- if is_stored_as_bytes:
- user_id = cls.convert_uuid_bytes_to_hex(user_id)
+ user_id = cls._convert_or_decode(is_stored_as_bytes, user_id)
methods = auth_plugins.convert_integer_to_method_list(payload[1])
system = payload[2]
expires_at_str = cls._convert_float_to_time_string(payload[3])
@@ -787,12 +750,10 @@ class ApplicationCredentialScopedPayload(BasePayload):
@classmethod
def disassemble(cls, payload):
(is_stored_as_bytes, user_id) = payload[0]
- if is_stored_as_bytes:
- user_id = cls.convert_uuid_bytes_to_hex(user_id)
+ user_id = cls._convert_or_decode(is_stored_as_bytes, user_id)
methods = auth_plugins.convert_integer_to_method_list(payload[1])
(is_stored_as_bytes, project_id) = payload[2]
- if is_stored_as_bytes:
- project_id = cls.convert_uuid_bytes_to_hex(project_id)
+ project_id = cls._convert_or_decode(is_stored_as_bytes, project_id)
expires_at_str = cls._convert_float_to_time_string(payload[3])
audit_ids = list(map(cls.base64_encode, payload[4]))
system = None
@@ -803,8 +764,7 @@ class ApplicationCredentialScopedPayload(BasePayload):
protocol_id = None
access_token_id = None
(is_stored_as_bytes, app_cred_id) = payload[5]
- if is_stored_as_bytes:
- app_cred_id = cls.convert_uuid_bytes_to_hex(app_cred_id)
+ app_cred_id = cls._convert_or_decode(is_stored_as_bytes, app_cred_id)
return (user_id, methods, system, project_id, domain_id,
expires_at_str, audit_ids, trust_id, federated_group_ids,
identity_provider_id, protocol_id, access_token_id,
diff --git a/releasenotes/notes/bug-1805880-0032024ea6b83563.yaml b/releasenotes/notes/bug-1805880-0032024ea6b83563.yaml
new file mode 100644
index 000000000..b00e4f680
--- /dev/null
+++ b/releasenotes/notes/bug-1805880-0032024ea6b83563.yaml
@@ -0,0 +1,14 @@
+---
+upgrade:
+ - |
+ [`bug 1805880 <https://bugs.launchpad.net/keystone/+bug/1805880>`_]
+ The limit policies defined in ``policy.v3cloudsample.json``
+ have been removed. These policies are now obsolete after incorporating
+ system-scope into the limit API and implementing default roles.
+fixes:
+ - |
+ [`bug 1805880 <https://bugs.launchpad.net/keystone/+bug/1805880>`_]
+ The limit policies in ``policy.v3cloudsample.json`` policy
+ file have been removed in favor of better defaults in code. These
+ policies weren't tested exhaustively and were misleading to users
+ and operators.
diff --git a/releasenotes/notes/bug-1818736-98ea186a074056f4.yaml b/releasenotes/notes/bug-1818736-98ea186a074056f4.yaml
new file mode 100644
index 000000000..007dc7ad2
--- /dev/null
+++ b/releasenotes/notes/bug-1818736-98ea186a074056f4.yaml
@@ -0,0 +1,17 @@
+---
+features:
+ - |
+ [`bug 1818736 <https://bugs.launchpad.net/keystone/+bug/1818736>`_]
+ The ``identity:get_limit``, ``identity:list_limits`` and
+ ``identity:get_limit_model`` policies now support domain scope, so domain
+ users are now able to get limit information on their own domains as well as
+ see the limit model in effect.
+upgrade:
+ - |
+ [`bug 1818736 <https://bugs.launchpad.net/keystone/+bug/1818736>`_]
+ The ``identity:get_limit`` policy default check string has been changed to
+ support domain scope. This policy are not being formally deprecated because
+ the unified limits API is still considered experimental. These
+ new default automatically account for domain scope in addition to system
+ scope. Please consider these new defaults if your deployment overrides the
+ limit policies.
diff --git a/releasenotes/notes/bug-1832265-cb76ccf505c2d9d1.yaml b/releasenotes/notes/bug-1832265-cb76ccf505c2d9d1.yaml
new file mode 100644
index 000000000..9dcf16aa7
--- /dev/null
+++ b/releasenotes/notes/bug-1832265-cb76ccf505c2d9d1.yaml
@@ -0,0 +1,7 @@
+---
+fixes:
+ - |
+ [`bug 1832265 <https://bugs.launchpad.net/keystone/+bug/1832265>`_]
+ Binary msgpack payload types are now consistently and correctly decoded
+ when running Keystone under Python 3, avoiding any TypeErrors when
+ attempting to convert binary encoded strings into UUID's.
diff --git a/releasenotes/notes/bug-1833739-f962e8caf3e22068.yaml b/releasenotes/notes/bug-1833739-f962e8caf3e22068.yaml
new file mode 100644
index 000000000..706015024
--- /dev/null
+++ b/releasenotes/notes/bug-1833739-f962e8caf3e22068.yaml
@@ -0,0 +1,9 @@
+---
+fixes:
+ - |
+ [`bug 1833739 <https://bugs.launchpad.net/keystone/+bug/1833739>`_]
+ Fix PostgreSQL specifc issue with storing encrypted credentials. In
+ Python 3 the psycopg2 module treats bytes strings as binary data. This
+ causes issues when storing encrypted credentials in the Database.
+ To fix this isseu the credentials sql backend is updated to encode the
+ credential into a text string before handing it over to the database.
diff --git a/releasenotes/notes/bug-1836568-66d853a1f22c5530.yaml b/releasenotes/notes/bug-1836568-66d853a1f22c5530.yaml
new file mode 100644
index 000000000..4426b53f0
--- /dev/null
+++ b/releasenotes/notes/bug-1836568-66d853a1f22c5530.yaml
@@ -0,0 +1,10 @@
+---
+fixes:
+ - |
+ [`bug 1836568 <https://bugs.launchpad.net/keystone/+bug/1836568>`_
+ Addresses a side effect of the large series of policy migrations in which
+ the volume of deprecation warnings that were emitted had become too massive
+ to be helpful. Instead of emitting warnings for individual policy rules,
+ the keystone server now emits a single warning indicating problematic rules
+ were found. Operators can use oslopolicy-policy-generator and
+ oslopolicy-policy-upgrade to find and resolve deprecated policies.
diff --git a/releasenotes/notes/bug-1839133-24570c9fbacb530d.yaml b/releasenotes/notes/bug-1839133-24570c9fbacb530d.yaml
new file mode 100644
index 000000000..b6ed1556d
--- /dev/null
+++ b/releasenotes/notes/bug-1839133-24570c9fbacb530d.yaml
@@ -0,0 +1,5 @@
+---
+fixes:
+ - |
+ [`bug 1839133 <https://bugs.launchpad.net/keystone/+bug/1839133>`_]
+ Makes user_enabled_emulation_use_group_config honor group_members_are_ids.
diff --git a/releasenotes/notes/bug-1841486-425f367925f5e03f.yaml b/releasenotes/notes/bug-1841486-425f367925f5e03f.yaml
new file mode 100644
index 000000000..da7d74450
--- /dev/null
+++ b/releasenotes/notes/bug-1841486-425f367925f5e03f.yaml
@@ -0,0 +1,7 @@
+---
+fixes:
+ - |
+ [`bug 1841486 <https://bugs.launchpad.net/keystone/+bug/1841486>`_]
+ The ``keystone-manage mapping_engine --engine-debug`` CLI tool now outputs
+ useful information about the direct mappings from an assertion after
+ processing mapping rules.
diff --git a/releasenotes/notes/bug-1843609-8498b132222596b7.yaml b/releasenotes/notes/bug-1843609-8498b132222596b7.yaml
new file mode 100644
index 000000000..19a140f9d
--- /dev/null
+++ b/releasenotes/notes/bug-1843609-8498b132222596b7.yaml
@@ -0,0 +1,9 @@
+---
+fixes:
+ - |
+ [`bug 1843609 <https://bugs.launchpad.net/keystone/+bug/1843609>`]
+ Fixed an issue where system-scoped tokens couldn't be used to list users
+ and groups (e.g., GET /v3/users or GET /v3/groups) if ``keystone.conf
+ [identity] domain_specific_drivers_enabled=True`` and the API would
+ return an ``HTTP 401 Unauthorized``. These APIs now recognize
+ system-scoped tokens when using domain-specific drivers.
diff --git a/releasenotes/notes/bug-1844157-7808af9bcea0429d.yaml b/releasenotes/notes/bug-1844157-7808af9bcea0429d.yaml
new file mode 100644
index 000000000..29e19cc48
--- /dev/null
+++ b/releasenotes/notes/bug-1844157-7808af9bcea0429d.yaml
@@ -0,0 +1,13 @@
+---
+fixes:
+ - >
+ [`bug 1844157 <https://bugs.launchpad.net/keystone/+bug/1844157>`_]
+ When performing `keystone-manage db_sync --check` if the legacy repo
+ started at the same version number as the expand/contract/migrate
+ repos the check to see if the db was under version control failed
+ indicating that the db was up-to-date. This was due to the function
+ `get_init_version` never receiving the path for the repo queried for
+ version information. The fix is to ensure the repo path is always
+ passed to get_init_version from the
+ `keystone.common.sql.upgrade.get_db_version` function.
+
diff --git a/releasenotes/notes/bug-1844194-48ae60db49f91bd4.yaml b/releasenotes/notes/bug-1844194-48ae60db49f91bd4.yaml
new file mode 100644
index 000000000..abdc17332
--- /dev/null
+++ b/releasenotes/notes/bug-1844194-48ae60db49f91bd4.yaml
@@ -0,0 +1,43 @@
+---
+features:
+ - |
+ [`bug 1844194 <https://bugs.launchpad.net/keystone/+bug/1844194>`_]
+ [`bug 1844193 <https://bugs.launchpad.net/keystone/+bug/1844193>`_]
+ The project tags API now supports the ``admin``, ``member``, and ``reader``
+ default roles.
+upgrade:
+ - |
+ [`bug 1844194 <https://bugs.launchpad.net/keystone/+bug/1844194>`_]
+ [`bug 1844193 <https://bugs.launchpad.net/keystone/+bug/1844193>`_]
+ The project tags API now uses new default policies that make it more
+ accessible to end users and administrators in a secure way. Please
+ consider these new defaults if your deployment overrides the project
+ tags policies.
+deprecations:
+ - |
+ [`bug 1844194 <https://bugs.launchpad.net/keystone/+bug/1844194>`_]
+ [`bug 1844193 <https://bugs.launchpad.net/keystone/+bug/1844193>`_]
+ The project tags API policies have been deprecated. The
+ ``identity:get_project_tag`` and ``identity:list_project_tags``
+ policies now use ``(role:reader and system_scope:all) or
+ (role:reader and domain_id:%(target.project.domain_id)s) or
+ project_id:%(target.project.id)s`` instead of
+ ``rule:admin_required or project_id:%(target.project.id)s``. The
+ ``identity:update_project_tags``, ``identity:delete_project_tags``,
+ ``identity:delete_project_tag``, and ``identity:create_project_tag``
+ policies now use ``(role:admin and system_scope:all) or (role:admin
+ and domain_id:%(target.project.domain_id)s) or (role:admin and
+ project_id:%(target.project.id)s)`` instead of
+ ``rule:admin_required``.
+
+ These new defaults automatically account for system-scope and support
+ a read-only role, making it easier for system administrators to
+ delegate subsets of responsibility with compromising security. Please
+ consider these new defaults if your deployment overrides the project
+ tag policies.
+security:
+ - |
+ [`bug 1844194 <https://bugs.launchpad.net/keystone/+bug/1844194>`_]
+ [`bug 1844193 <https://bugs.launchpad.net/keystone/+bug/1844193>`_]
+ The project tags API now uses system-scope and default roles to
+ provide better accessibility to users in a secure way.
diff --git a/releasenotes/notes/bug-1844207-x27a31f3403xfd7y.yaml b/releasenotes/notes/bug-1844207-x27a31f3403xfd7y.yaml
new file mode 100644
index 000000000..29ccaac42
--- /dev/null
+++ b/releasenotes/notes/bug-1844207-x27a31f3403xfd7y.yaml
@@ -0,0 +1,7 @@
+---
+fixes:
+ - |
+ [`bug 1844207 <https://bugs.launchpad.net/keystone/+bug/1844207>`_]
+ Fixes an issue with WebSSO auth where a server error was raised if a remote
+ ID can't be found for the requested federation protocol, now correctly
+ raises an Unauthorized client error.
diff --git a/releasenotes/notes/bug-1844461-08a8bdc5f613b88d.yaml b/releasenotes/notes/bug-1844461-08a8bdc5f613b88d.yaml
new file mode 100644
index 000000000..4ea290442
--- /dev/null
+++ b/releasenotes/notes/bug-1844461-08a8bdc5f613b88d.yaml
@@ -0,0 +1,31 @@
+---
+features:
+ - |
+ [`bug 1844461 <https://bugs.launchpad.net/keystone/+bug/1844461>`_]
+ Listing role assignments for a project subtree is now allowed by system
+ readers and domain readers in addition to project admins.
+upgrade:
+ - |
+ [`bug 1844461 <https://bugs.launchpad.net/keystone/+bug/1844461>`_]
+ The ``identity:list_role_assignments_for_subtree`` policy now allows system
+ and domain readers to list role assignments for a project subtree and
+ deprecates the old ``rule:admin_required`` policy check string. Please
+ consider the new policies if your deployment overrides role
+ assignment policies.
+deprecations:
+ - |
+ [`bug 1844461 <https://bugs.launchpad.net/keystone/+bug/1844461>`_]
+ The role assignment ``identity:list_role_assignments_for_subtree`` policy
+ now uses ``(role:reader and system_scope:all) or (role:reader and
+ domain_id:%(target.project.domain_id)s) or (role:admin and
+ project_id:%(target.project.id)s)`` instead of ``rule:admin_required``.
+ This new default automatically includes support for a read-only role
+ and allows for more granular access to the role assignment API. Please
+ consider this new default if your deployment overrides the role
+ assignment policies.
+security:
+ - |
+ [`bug 1844461 <https://bugs.launchpad.net/keystone/+bug/1844461>`_]
+ Listing role assignments for a project subtree now uses system-scope,
+ domain-scope, project-scope, and default roles to provide better
+ accessbility to users in a secure way.
diff --git a/releasenotes/notes/bug-1844664-905cf6cad2e032a7.yaml b/releasenotes/notes/bug-1844664-905cf6cad2e032a7.yaml
new file mode 100644
index 000000000..ed28fba43
--- /dev/null
+++ b/releasenotes/notes/bug-1844664-905cf6cad2e032a7.yaml
@@ -0,0 +1,36 @@
+---
+features:
+ - |
+ [`bug 1844664 <https://bugs.launchpad.net/keystone/+bug/1844664>`_]
+ The Project Endpoints API now supports the ``admin``,
+ ``member``, and ``reader`` default roles.
+
+upgrade:
+ - |
+ [`bug 1844664 <https://bugs.launchpad.net/keystone/+bug/1844664>`_]
+ The Project Endpoints API uses new default policies to
+ make it more accessible to end users and administrators in a secure way.
+ Please consider these new defaults if your deployment overrides Project
+ Endpoints policies.
+deprecations:
+ - |
+ [`bug 1844664 <https://bugs.launchpad.net/keystone/+bug/1844664>`_]
+ The Project Endpoints policies have been deprecated. The
+ ``identity:list_projects_for_endpoint`` now use ``(role:reader and system_scope:all)``
+ ``identity:check_endpoint_in_project`` policies now use
+ ``role:reader and system_scope:all`` and ``identity:list_endpoints_for_project``
+ now use ``(role:reader and system_scope:all)`` instead of
+ ``rule:admin_required``. The ``identity:add_endpoint_to_project`` now use
+ ``(role:admin and system_scope:all)``
+ instead of ``rule:admin_required``and ``identity:remove_endpoint_from_project``
+ policies now use ``role:admin and system_scope:all`` instead of
+ ``rule:admin_required``.
+ These new defaults automatically account for system-scope and support
+ a read-only role, making it easier for system administrators to delegate
+ subsets of responsibility without compromising security. Please consider
+ these new defaults if your deployment overrides the Project Endpoints policies.
+security:
+ - |
+ [`bug 1844664 <https://bugs.launchpad.net/keystone/+bug/1844664>`_]
+ The Project Endpoints API now uses system-scope and default
+ roles to provide better accessibility to users in a secure manner.