summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.zuul.yaml3
-rw-r--r--doc/requirements.txt3
-rw-r--r--keystone/api/_shared/EC2_S3_Resource.py75
-rw-r--r--keystone/api/credentials.py78
-rw-r--r--keystone/api/users.py22
-rw-r--r--keystone/assignment/backends/sql.py5
-rw-r--r--keystone/common/policies/identity_provider.py8
-rw-r--r--keystone/conf/credential.py11
-rw-r--r--keystone/credential/core.py17
-rw-r--r--keystone/identity/backends/ldap/common.py44
-rw-r--r--keystone/identity/backends/sql.py8
-rw-r--r--keystone/models/token_model.py18
-rw-r--r--keystone/tests/unit/assignment/test_backends.py19
-rw-r--r--keystone/tests/unit/identity/backends/test_ldap_common.py14
-rw-r--r--keystone/tests/unit/test_backend_ldap.py52
-rw-r--r--keystone/tests/unit/test_backend_sql.py37
-rw-r--r--keystone/tests/unit/test_contrib_ec2_core.py125
-rw-r--r--keystone/tests/unit/test_v3_application_credential.py31
-rw-r--r--keystone/tests/unit/test_v3_credential.py430
-rw-r--r--keystone/tests/unit/test_v3_oauth1.py13
-rw-r--r--lower-constraints.txt8
-rw-r--r--releasenotes/notes/bug-1831918-c70cf87ef086d871.yaml6
-rw-r--r--releasenotes/notes/bug-1839133-24570c9fbacb530d.yaml5
-rw-r--r--releasenotes/notes/bug-1872733-2377f456a57ad32c.yaml16
-rw-r--r--releasenotes/notes/bug-1872735-0989e51d2248ce1e.yaml31
-rw-r--r--releasenotes/notes/bug-1872737-f8e1ad3b6705b766.yaml28
-rw-r--r--releasenotes/notes/bug-1872755-2c81d3267b89f124.yaml19
-rw-r--r--releasenotes/notes/bug-1873290-ff7f8e4cee15b75a.yaml19
-rw-r--r--releasenotes/notes/bug-1878938-70ee2af6fdf66004.yaml16
-rw-r--r--releasenotes/notes/bug-1885753-51df25f3ff1d9ae8.yaml6
-rw-r--r--releasenotes/notes/bug-1889936-78d6853b5212b8f1.yaml5
-rw-r--r--setup.cfg3
-rw-r--r--test-requirements.txt1
33 files changed, 1086 insertions, 90 deletions
diff --git a/.zuul.yaml b/.zuul.yaml
index 261ecc3eb..84d757ef5 100644
--- a/.zuul.yaml
+++ b/.zuul.yaml
@@ -196,6 +196,7 @@
voting: false
irrelevant-files: *irrelevant-files
- keystone-dsvm-py3-functional-federation-opensuse15-k2k:
+ voting: false
irrelevant-files: *irrelevant-files
- keystoneclient-devstack-functional:
voting: false
@@ -225,8 +226,6 @@
irrelevant-files: *irrelevant-files
- keystone-dsvm-py3-functional:
irrelevant-files: *irrelevant-files
- - keystone-dsvm-py3-functional-federation-opensuse15-k2k:
- irrelevant-files: *irrelevant-files
- tempest-full:
irrelevant-files: *tempest-irrelevant-files
- tempest-full-py3:
diff --git a/doc/requirements.txt b/doc/requirements.txt
index 7247e6aee..47cafa36c 100644
--- a/doc/requirements.txt
+++ b/doc/requirements.txt
@@ -2,7 +2,8 @@
# of appearance. Changing the order has an impact on the overall integration
# process, which may cause wedges in the gate later.
openstackdocstheme>=1.18.1 # Apache-2.0
-sphinx!=1.6.6,!=1.6.7,>=1.6.2 # BSD
+sphinx!=1.6.6,!=1.6.7,>=1.6.2,<2.0.0;python_version=='2.7' # BSD
+sphinx!=1.6.6,!=1.6.7,>=1.6.2;python_version>='3.4' # BSD
sphinxcontrib-apidoc>=0.2.0 # BSD
sphinxcontrib-seqdiag>=0.8.4 # BSD
reno>=2.5.0 # Apache-2.0
diff --git a/keystone/api/_shared/EC2_S3_Resource.py b/keystone/api/_shared/EC2_S3_Resource.py
index a1f139116..2e348bcec 100644
--- a/keystone/api/_shared/EC2_S3_Resource.py
+++ b/keystone/api/_shared/EC2_S3_Resource.py
@@ -12,18 +12,22 @@
# Common base resource for EC2 and S3 Authentication
+import datetime
import sys
from oslo_serialization import jsonutils
+from oslo_utils import timeutils
import six
from werkzeug import exceptions
from keystone.common import provider_api
from keystone.common import utils
+import keystone.conf
from keystone import exception as ks_exceptions
from keystone.i18n import _
from keystone.server import flask as ks_flask
+CONF = keystone.conf.CONF
PROVIDERS = provider_api.ProviderAPIs
CRED_TYPE_EC2 = 'ec2'
@@ -39,6 +43,31 @@ class ResourceBase(ks_flask.ResourceBase):
# the ABC module.
raise NotImplementedError()
+ @staticmethod
+ def _check_timestamp(credentials):
+ timestamp = (
+ # AWS Signature v1/v2
+ credentials.get('params', {}).get('Timestamp') or
+ # AWS Signature v4
+ credentials.get('headers', {}).get('X-Amz-Date') or
+ credentials.get('params', {}).get('X-Amz-Date')
+ )
+ if not timestamp:
+ # If the signed payload doesn't include a timestamp then the signer
+ # must have intentionally left it off
+ return
+ try:
+ timestamp = timeutils.parse_isotime(timestamp)
+ timestamp = timeutils.normalize_time(timestamp)
+ except Exception as e:
+ raise ks_exceptions.Unauthorized(
+ _('Credential timestamp is invalid: %s') % e)
+ auth_ttl = datetime.timedelta(minutes=CONF.credential.auth_ttl)
+ current_time = timeutils.normalize_time(timeutils.utcnow())
+ if current_time > timestamp + auth_ttl:
+ raise ks_exceptions.Unauthorized(
+ _('Credential is expired'))
+
def handle_authenticate(self):
# TODO(morgan): convert this dirty check to JSON Schema validation
# this mirrors the previous behavior of the webob system where an
@@ -86,7 +115,9 @@ class ResourceBase(ks_flask.ResourceBase):
project_id=cred.get('project_id'),
access=loaded.get('access'),
secret=loaded.get('secret'),
- trust_id=loaded.get('trust_id')
+ trust_id=loaded.get('trust_id'),
+ app_cred_id=loaded.get('app_cred_id'),
+ access_token_id=loaded.get('access_token_id')
)
# validate the signature
@@ -107,8 +138,35 @@ class ResourceBase(ks_flask.ResourceBase):
ks_exceptions.Unauthorized(e),
sys.exc_info()[2])
- roles = PROVIDERS.assignment_api.get_roles_for_user_and_project(
- user_ref['id'], project_ref['id'])
+ self._check_timestamp(credentials)
+
+ trustee_user_id = None
+ auth_context = None
+ if cred_data['trust_id']:
+ trust = PROVIDERS.trust_api.get_trust(cred_data['trust_id'])
+ roles = [r['id'] for r in trust['roles']]
+ # NOTE(cmurphy): if this credential was created using a
+ # trust-scoped token with impersonation, the user_id will be for
+ # the trustor, not the trustee. In this case, issuing a
+ # trust-scoped token to the trustor will fail. In order to get a
+ # trust-scoped token, use the user ID of the trustee. With
+ # impersonation, the resulting token will still be for the trustor.
+ # Without impersonation, the token will be for the trustee.
+ if trust['impersonation'] is True:
+ trustee_user_id = trust['trustee_user_id']
+ elif cred_data['app_cred_id']:
+ ac_client = PROVIDERS.application_credential_api
+ app_cred = ac_client.get_application_credential(
+ cred_data['app_cred_id'])
+ roles = [r['id'] for r in app_cred['roles']]
+ elif cred_data['access_token_id']:
+ access_token = PROVIDERS.oauth_api.get_access_token(
+ cred_data['access_token_id'])
+ roles = jsonutils.loads(access_token['role_ids'])
+ auth_context = {'access_token_id': cred_data['access_token_id']}
+ else:
+ roles = PROVIDERS.assignment_api.get_roles_for_user_and_project(
+ user_ref['id'], project_ref['id'])
if not roles:
raise ks_exceptions.Unauthorized(_('User not valid for project.'))
@@ -119,7 +177,14 @@ class ResourceBase(ks_flask.ResourceBase):
method_names = ['ec2credential']
+ if trustee_user_id:
+ user_id = trustee_user_id
+ else:
+ user_id = user_ref['id']
token = PROVIDERS.token_provider_api.issue_token(
- user_id=user_ref['id'], method_names=method_names,
- project_id=project_ref['id'])
+ user_id=user_id, method_names=method_names,
+ project_id=project_ref['id'],
+ trust_id=cred_data['trust_id'],
+ app_cred_id=cred_data['app_cred_id'],
+ auth_context=auth_context)
return token
diff --git a/keystone/api/credentials.py b/keystone/api/credentials.py
index 08a492c1d..4ff11ec7b 100644
--- a/keystone/api/credentials.py
+++ b/keystone/api/credentials.py
@@ -13,6 +13,7 @@
# This file handles all flask-restful resources for /v3/credentials
import hashlib
+import six
import flask
from oslo_serialization import jsonutils
@@ -60,30 +61,41 @@ class CredentialResource(ks_flask.ResourceBase):
ref['blob'] = jsonutils.dumps(blob)
return ref
- def _assign_unique_id(self, ref, trust_id=None):
+ def _validate_blob_json(self, ref):
+ try:
+ blob = jsonutils.loads(ref.get('blob'))
+ except (ValueError, TabError):
+ raise exception.ValidationError(
+ message=_('Invalid blob in credential'))
+ if not blob or not isinstance(blob, dict):
+ raise exception.ValidationError(attribute='blob',
+ target='credential')
+ if blob.get('access') is None:
+ raise exception.ValidationError(attribute='access',
+ target='credential')
+ return blob
+
+ def _assign_unique_id(
+ self, ref, trust_id=None, app_cred_id=None, access_token_id=None):
# Generates an assigns a unique identifier to a credential reference.
if ref.get('type', '').lower() == 'ec2':
- try:
- blob = jsonutils.loads(ref.get('blob'))
- except (ValueError, TabError):
- raise exception.ValidationError(
- message=_('Invalid blob in credential'))
- if not blob or not isinstance(blob, dict):
- raise exception.ValidationError(attribute='blob',
- target='credential')
- if blob.get('access') is None:
- raise exception.ValidationError(attribute='access',
- target='credential')
-
+ blob = self._validate_blob_json(ref)
ref = ref.copy()
ref['id'] = hashlib.sha256(
blob['access'].encode('utf8')).hexdigest()
- # update the blob with the trust_id, so credentials created with
- # a trust scoped token will result in trust scoped tokens when
- # authentication via ec2tokens happens
+ # update the blob with the trust_id or app_cred_id, so credentials
+ # created with a trust- or app cred-scoped token will result in
+ # trust- or app cred-scoped tokens when authentication via
+ # ec2tokens happens
if trust_id is not None:
blob['trust_id'] = trust_id
ref['blob'] = jsonutils.dumps(blob)
+ if app_cred_id is not None:
+ blob['app_cred_id'] = app_cred_id
+ ref['blob'] = jsonutils.dumps(blob)
+ if access_token_id is not None:
+ blob['access_token_id'] = access_token_id
+ ref['blob'] = jsonutils.dumps(blob)
return ref
else:
return super(CredentialResource, self)._assign_unique_id(ref)
@@ -146,22 +158,47 @@ class CredentialResource(ks_flask.ResourceBase):
)
validation.lazy_validate(schema.credential_create, credential)
trust_id = getattr(self.oslo_context, 'trust_id', None)
+ app_cred_id = getattr(
+ self.auth_context['token'], 'application_credential_id', None)
+ access_token_id = getattr(
+ self.auth_context['token'], 'access_token_id', None)
ref = self._assign_unique_id(
- self._normalize_dict(credential), trust_id=trust_id)
- ref = PROVIDERS.credential_api.create_credential(ref['id'], ref)
+ self._normalize_dict(credential),
+ trust_id=trust_id, app_cred_id=app_cred_id,
+ access_token_id=access_token_id)
+ ref = PROVIDERS.credential_api.create_credential(
+ ref['id'], ref, initiator=self.audit_initiator)
return self.wrap_member(ref), http_client.CREATED
+ def _validate_blob_update_keys(self, credential, ref):
+ if credential.get('type', '').lower() == 'ec2':
+ new_blob = self._validate_blob_json(ref)
+ old_blob = credential.get('blob')
+ if isinstance(old_blob, six.string_types):
+ old_blob = jsonutils.loads(old_blob)
+ # if there was a scope set, prevent changing it or unsetting it
+ for key in ['trust_id', 'app_cred_id', 'access_token_id']:
+ if old_blob.get(key) != new_blob.get(key):
+ message = _('%s can not be updated for credential') % key
+ raise exception.ValidationError(message=message)
+
def patch(self, credential_id):
# Update Credential
ENFORCER.enforce_call(
action='identity:update_credential',
build_target=_build_target_enforcement
)
- PROVIDERS.credential_api.get_credential(credential_id)
+ current = PROVIDERS.credential_api.get_credential(credential_id)
credential = self.request_body_json.get('credential', {})
validation.lazy_validate(schema.credential_update, credential)
+ self._validate_blob_update_keys(current.copy(), credential.copy())
self._require_matching_id(credential)
+ # Check that the user hasn't illegally modified the owner or scope
+ target = {'credential': dict(current, **credential)}
+ ENFORCER.enforce_call(
+ action='identity:update_credential', target_attr=target
+ )
ref = PROVIDERS.credential_api.update_credential(
credential_id, credential)
return self.wrap_member(ref)
@@ -173,7 +210,8 @@ class CredentialResource(ks_flask.ResourceBase):
build_target=_build_target_enforcement
)
- return (PROVIDERS.credential_api.delete_credential(credential_id),
+ return (PROVIDERS.credential_api.delete_credential(credential_id,
+ initiator=self.audit_initiator),
http_client.NO_CONTENT)
diff --git a/keystone/api/users.py b/keystone/api/users.py
index 97626da9b..117ace2d3 100644
--- a/keystone/api/users.py
+++ b/keystone/api/users.py
@@ -568,6 +568,25 @@ class UserAppCredListCreateResource(ks_flask.ResourceBase):
role['name']))
return roles
+ def _get_roles(self, app_cred_data, token):
+ if app_cred_data.get('roles'):
+ roles = self._normalize_role_list(app_cred_data['roles'])
+ # NOTE(cmurphy): The user is not allowed to add a role that is not
+ # in their token. This is to prevent trustees or application
+ # credential users from escallating their privileges to include
+ # additional roles that the trustor or application credential
+ # creator has assigned on the project.
+ token_roles = [r['id'] for r in token.roles]
+ for role in roles:
+ if role['id'] not in token_roles:
+ detail = _('Cannot create an application credential with '
+ 'unassigned role')
+ raise ks_exception.ApplicationCredentialValidationError(
+ detail=detail)
+ else:
+ roles = token.roles
+ return roles
+
def get(self, user_id):
"""List application credentials for user.
@@ -603,8 +622,7 @@ class UserAppCredListCreateResource(ks_flask.ResourceBase):
app_cred_data['secret'] = self._generate_secret()
app_cred_data['user_id'] = user_id
app_cred_data['project_id'] = project_id
- app_cred_data['roles'] = self._normalize_role_list(
- app_cred_data.get('roles', token.roles))
+ app_cred_data['roles'] = self._get_roles(app_cred_data, token)
if app_cred_data.get('expires_at'):
app_cred_data['expires_at'] = utils.parse_expiration_date(
app_cred_data['expires_at'])
diff --git a/keystone/assignment/backends/sql.py b/keystone/assignment/backends/sql.py
index 6822811ca..5eda2b724 100644
--- a/keystone/assignment/backends/sql.py
+++ b/keystone/assignment/backends/sql.py
@@ -262,6 +262,11 @@ class Assignment(base.AssignmentDriverBase):
q = q.filter_by(role_id=role_id)
q.delete(False)
+ with sql.session_for_write() as session:
+ q = session.query(SystemRoleAssignment)
+ q = q.filter_by(role_id=role_id)
+ q.delete(False)
+
def delete_domain_assignments(self, domain_id):
with sql.session_for_write() as session:
q = session.query(RoleAssignment)
diff --git a/keystone/common/policies/identity_provider.py b/keystone/common/policies/identity_provider.py
index fb9fe75d0..006a6cc70 100644
--- a/keystone/common/policies/identity_provider.py
+++ b/keystone/common/policies/identity_provider.py
@@ -16,7 +16,7 @@ from oslo_policy import policy
from keystone.common.policies import base
deprecated_get_idp = policy.DeprecatedRule(
- name=base.IDENTITY % 'get_identity_providers',
+ name=base.IDENTITY % 'get_identity_provider',
check_str=base.RULE_ADMIN_REQUIRED
)
deprecated_list_idp = policy.DeprecatedRule(
@@ -24,15 +24,15 @@ deprecated_list_idp = policy.DeprecatedRule(
check_str=base.RULE_ADMIN_REQUIRED
)
deprecated_update_idp = policy.DeprecatedRule(
- name=base.IDENTITY % 'update_identity_providers',
+ name=base.IDENTITY % 'update_identity_provider',
check_str=base.RULE_ADMIN_REQUIRED
)
deprecated_create_idp = policy.DeprecatedRule(
- name=base.IDENTITY % 'create_identity_providers',
+ name=base.IDENTITY % 'create_identity_provider',
check_str=base.RULE_ADMIN_REQUIRED
)
deprecated_delete_idp = policy.DeprecatedRule(
- name=base.IDENTITY % 'delete_identity_providers',
+ name=base.IDENTITY % 'delete_identity_provider',
check_str=base.RULE_ADMIN_REQUIRED
)
diff --git a/keystone/conf/credential.py b/keystone/conf/credential.py
index b7877a816..048d22283 100644
--- a/keystone/conf/credential.py
+++ b/keystone/conf/credential.py
@@ -46,12 +46,21 @@ share this repository with the repository used to manage keys for Fernet
tokens.
"""))
+auth_ttl = cfg.IntOpt(
+ 'auth_ttl',
+ default=15,
+ help=utils.fmt("""
+The length of time in minutes for which a signed EC2 or S3 token request is
+valid from the timestamp contained in the token request.
+"""))
+
GROUP_NAME = __name__.split('.')[-1]
ALL_OPTS = [
driver,
provider,
- key_repository
+ key_repository,
+ auth_ttl
]
diff --git a/keystone/credential/core.py b/keystone/credential/core.py
index cb28b314e..d6c48ff16 100644
--- a/keystone/credential/core.py
+++ b/keystone/credential/core.py
@@ -21,6 +21,7 @@ from keystone.common import manager
from keystone.common import provider_api
import keystone.conf
from keystone import exception
+from keystone import notifications
CONF = keystone.conf.CONF
@@ -38,6 +39,8 @@ class Manager(manager.Manager):
driver_namespace = 'keystone.credential'
_provides_api = 'credential_api'
+ _CRED = 'credential'
+
def __init__(self):
super(Manager, self).__init__(CONF.credential.driver)
@@ -102,13 +105,18 @@ class Manager(manager.Manager):
credential = self.driver.get_credential(credential_id)
return self._decrypt_credential(credential)
- def create_credential(self, credential_id, credential):
+ def create_credential(self, credential_id, credential,
+ initiator=None):
"""Create a credential."""
credential_copy = self._encrypt_credential(credential)
ref = self.driver.create_credential(credential_id, credential_copy)
ref.pop('key_hash', None)
ref.pop('encrypted_blob', None)
ref['blob'] = credential['blob']
+ notifications.Audit.created(
+ self._CRED,
+ credential_id,
+ initiator)
return ref
def _validate_credential_update(self, credential_id, credential):
@@ -143,3 +151,10 @@ class Manager(manager.Manager):
else:
ref['blob'] = existing_blob
return ref
+
+ def delete_credential(self, credential_id,
+ initiator=None):
+ """Delete a credential."""
+ self.driver.delete_credential(credential_id)
+ notifications.Audit.deleted(
+ self._CRED, credential_id, initiator)
diff --git a/keystone/identity/backends/ldap/common.py b/keystone/identity/backends/ldap/common.py
index b9becea74..f2f36718f 100644
--- a/keystone/identity/backends/ldap/common.py
+++ b/keystone/identity/backends/ldap/common.py
@@ -18,6 +18,7 @@ import functools
import os.path
import re
import sys
+import uuid
import weakref
import ldap.controls
@@ -95,7 +96,16 @@ def utf8_decode(value):
:raises UnicodeDecodeError: for invalid UTF-8 encoding
"""
if isinstance(value, six.binary_type):
- return _utf8_decoder(value)[0]
+ try:
+ return _utf8_decoder(value)[0]
+ except UnicodeDecodeError:
+ # NOTE(lbragstad): We could be dealing with a UUID in byte form,
+ # which some LDAP implementations use.
+ uuid_byte_string_length = 16
+ if len(value) == uuid_byte_string_length:
+ return six.text_type(uuid.UUID(bytes_le=value))
+ else:
+ raise
return six.text_type(value)
@@ -1781,6 +1791,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 +1808,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,10 +1828,19 @@ class EnabledEmuMixIn(BaseLdap):
naming_rdn[1])
self.enabled_emulation_naming_attr = naming_attr
- def _get_enabled(self, object_id, conn):
- dn = self._id_to_dn(object_id)
+ def _id_to_member_attribute_value(self, object_id):
+ """Convert id to value expected by member_attribute."""
+ if self.group_members_are_ids:
+ return object_id
+ return self._id_to_dn(object_id)
+
+ def _is_id_enabled(self, object_id, conn):
+ member_attr_val = self._id_to_member_attribute_value(object_id)
+ return self._is_member_enabled(member_attr_val, conn)
+
+ def _is_member_enabled(self, member_attr_val, conn):
query = '(%s=%s)' % (self.member_attribute,
- ldap.filter.escape_filter_chars(dn))
+ ldap.filter.escape_filter_chars(member_attr_val))
try:
enabled_value = conn.search_s(self.enabled_emulation_dn,
ldap.SCOPE_BASE,
@@ -1829,24 +1851,26 @@ class EnabledEmuMixIn(BaseLdap):
return bool(enabled_value)
def _add_enabled(self, object_id):
+ member_attr_val = self._id_to_member_attribute_value(object_id)
with self.get_connection() as conn:
- if not self._get_enabled(object_id, conn):
+ if not self._is_member_enabled(member_attr_val, conn):
modlist = [(ldap.MOD_ADD,
self.member_attribute,
- [self._id_to_dn(object_id)])]
+ [member_attr_val])]
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)]),
+ [member_attr_val]),
self.enabled_emulation_naming_attr]
conn.add_s(self.enabled_emulation_dn, attr_list)
def _remove_enabled(self, object_id):
+ member_attr_val = self._id_to_member_attribute_value(object_id)
modlist = [(ldap.MOD_DELETE,
self.member_attribute,
- [self._id_to_dn(object_id)])]
+ [member_attr_val])]
with self.get_connection() as conn:
try:
conn.modify_s(self.enabled_emulation_dn, modlist)
@@ -1871,7 +1895,7 @@ class EnabledEmuMixIn(BaseLdap):
ref = super(EnabledEmuMixIn, self).get(object_id, ldap_filter)
if ('enabled' not in self.attribute_ignore and
self.enabled_emulation):
- ref['enabled'] = self._get_enabled(object_id, conn)
+ ref['enabled'] = self._is_id_enabled(object_id, conn)
return ref
def get_all(self, ldap_filter=None, hints=None):
@@ -1883,7 +1907,7 @@ class EnabledEmuMixIn(BaseLdap):
if x[0] != self.enabled_emulation_dn]
with self.get_connection() as conn:
for obj_ref in obj_list:
- obj_ref['enabled'] = self._get_enabled(
+ obj_ref['enabled'] = self._is_id_enabled(
obj_ref['id'], conn)
return obj_list
else:
diff --git a/keystone/identity/backends/sql.py b/keystone/identity/backends/sql.py
index eed349630..5a97c7fd5 100644
--- a/keystone/identity/backends/sql.py
+++ b/keystone/identity/backends/sql.py
@@ -32,6 +32,10 @@ from keystone.identity.backends import sql_model as model
CONF = keystone.conf.CONF
+def _stale_data_exception_checker(exc):
+ return isinstance(exc, sqlalchemy.orm.exc.StaleDataError)
+
+
class Identity(base.IdentityDriverBase):
# NOTE(henry-nash): Override the __init__() method so as to take a
# config parameter to enable sql to be used as a domain-specific driver.
@@ -201,6 +205,10 @@ class Identity(base.IdentityDriverBase):
return base.filter_user(user_ref.to_dict())
@sql.handle_conflicts(conflict_type='user')
+ # Explicitly retry on StaleDataErrors, which can happen if two clients
+ # update the same user's password and the second client has stale password
+ # information.
+ @oslo_db_api.wrap_db_retry(exception_checker=_stale_data_exception_checker)
def update_user(self, user_id, user):
with sql.session_for_write() as session:
user_ref = self._get_user(session, user_id)
diff --git a/keystone/models/token_model.py b/keystone/models/token_model.py
index 7f190286a..413545b53 100644
--- a/keystone/models/token_model.py
+++ b/keystone/models/token_model.py
@@ -13,6 +13,7 @@
"""Unified in-memory token model."""
from oslo_log import log
+from oslo_serialization import jsonutils
from oslo_serialization import msgpackutils
from oslo_utils import reflection
import six
@@ -325,6 +326,21 @@ class TokenModel(object):
return roles
+ def _get_oauth_roles(self):
+ roles = []
+ access_token_roles = self.access_token['role_ids']
+ access_token_roles = [
+ {'role_id': r} for r in jsonutils.loads(access_token_roles)]
+ effective_access_token_roles = (
+ PROVIDERS.assignment_api.add_implied_roles(access_token_roles)
+ )
+ user_roles = [r['id'] for r in self._get_project_roles()]
+ for role in effective_access_token_roles:
+ if role['role_id'] in user_roles:
+ role = PROVIDERS.role_api.get_role(role['role_id'])
+ roles.append({'id': role['id'], 'name': role['name']})
+ return roles
+
def _get_federated_roles(self):
roles = []
group_ids = [group['id'] for group in self.federated_groups]
@@ -428,6 +444,8 @@ class TokenModel(object):
roles = self._get_system_roles()
elif self.trust_scoped:
roles = self._get_trust_roles()
+ elif self.oauth_scoped:
+ roles = self._get_oauth_roles()
elif self.is_federated and not self.unscoped:
roles = self._get_federated_roles()
elif self.domain_scoped:
diff --git a/keystone/tests/unit/assignment/test_backends.py b/keystone/tests/unit/assignment/test_backends.py
index 589256eff..181c42a54 100644
--- a/keystone/tests/unit/assignment/test_backends.py
+++ b/keystone/tests/unit/assignment/test_backends.py
@@ -4226,3 +4226,22 @@ class SystemAssignmentTests(AssignmentTestHelperMixin):
group_id,
role['id']
)
+
+ def test_delete_role_with_system_assignments(self):
+ role = unit.new_role_ref()
+ PROVIDERS.role_api.create_role(role['id'], role)
+ domain = unit.new_domain_ref()
+ PROVIDERS.resource_api.create_domain(domain['id'], domain)
+ user = unit.new_user_ref(domain_id=domain['id'])
+ user = PROVIDERS.identity_api.create_user(user)
+
+ # creating a system grant for user
+ PROVIDERS.assignment_api.create_system_grant_for_user(
+ user['id'], role['id']
+ )
+ # deleting the role user has on system
+ PROVIDERS.role_api.delete_role(role['id'])
+ system_roles = PROVIDERS.assignment_api.list_role_assignments(
+ role_id=role['id']
+ )
+ self.assertEqual(len(system_roles), 0)
diff --git a/keystone/tests/unit/identity/backends/test_ldap_common.py b/keystone/tests/unit/identity/backends/test_ldap_common.py
index e464a8a14..2a0d9ab28 100644
--- a/keystone/tests/unit/identity/backends/test_ldap_common.py
+++ b/keystone/tests/unit/identity/backends/test_ldap_common.py
@@ -520,6 +520,20 @@ class CommonLdapTestCase(unit.BaseTestCase):
# The user name should still be a string value.
self.assertEqual(user_name, py_result[0][1]['user_name'][0])
+ def test_user_id_attribute_is_uuid_in_byte_form(self):
+ results = [(
+ 'cn=alice,dc=example,dc=com',
+ {
+ 'cn': [b'cn=alice'],
+ 'objectGUID': [b'\xdd\xd8Rt\xee]bA\x8e(\xe39\x0b\xe1\xf8\xe8'],
+ 'email': [uuid.uuid4().hex],
+ 'sn': [uuid.uuid4().hex]
+ }
+ )]
+ py_result = common_ldap.convert_ldap_result(results)
+ exp_object_guid = '7452d8dd-5dee-4162-8e28-e3390be1f8e8'
+ self.assertEqual(exp_object_guid, py_result[0][1]['objectGUID'][0])
+
class LDAPFilterQueryCompositionTest(unit.BaseTestCase):
"""These test cases test LDAP filter generation."""
diff --git a/keystone/tests/unit/test_backend_ldap.py b/keystone/tests/unit/test_backend_ldap.py
index aa7a50747..48fea00ec 100644
--- a/keystone/tests/unit/test_backend_ldap.py
+++ b/keystone/tests/unit/test_backend_ldap.py
@@ -2046,9 +2046,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()
@@ -2064,6 +2072,46 @@ 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')
@@ -2157,7 +2205,7 @@ class LDAPIdentityEnabledEmulation(LDAPIdentity, unit.TestCase):
# Override the tree_dn, it's used to build the enabled member filter
mixin_impl.tree_dn = sample_dn
- # The filter that _get_enabled is going to build contains the
+ # The filter, which _is_id_enabled is going to build, contains the
# tree_dn, which better be escaped in this case.
exp_filter = '(%s=%s=%s,%s)' % (
mixin_impl.member_attribute, mixin_impl.id_attr, object_id,
@@ -2166,7 +2214,7 @@ class LDAPIdentityEnabledEmulation(LDAPIdentity, unit.TestCase):
with mixin_impl.get_connection() as conn:
m = self.useFixture(
fixtures.MockPatchObject(conn, 'search_s')).mock
- mixin_impl._get_enabled(object_id, conn)
+ mixin_impl._is_id_enabled(object_id, conn)
# The 3rd argument is the DN.
self.assertEqual(exp_filter, m.call_args[0][2])
diff --git a/keystone/tests/unit/test_backend_sql.py b/keystone/tests/unit/test_backend_sql.py
index d165eb8c5..0412c6432 100644
--- a/keystone/tests/unit/test_backend_sql.py
+++ b/keystone/tests/unit/test_backend_sql.py
@@ -14,9 +14,11 @@
import uuid
+import fixtures
import mock
from oslo_db import exception as db_exception
from oslo_db import options
+from oslo_log import log
from six.moves import range
import sqlalchemy
from sqlalchemy import exc
@@ -745,6 +747,41 @@ class SqlIdentity(SqlTests,
PROVIDERS.resource_api.check_project_depth,
2)
+ def test_update_user_with_stale_data_forces_retry(self):
+ # Capture log output so we know oslo.db attempted a retry
+ log_fixture = self.useFixture(fixtures.FakeLogger(level=log.DEBUG))
+
+ # Create a new user
+ user_dict = unit.new_user_ref(
+ domain_id=CONF.identity.default_domain_id)
+ new_user_dict = PROVIDERS.identity_api.create_user(user_dict)
+
+ side_effects = [
+ # Raise a StaleDataError simulating that another client has
+ # updated the user's password while this client's request was
+ # being processed
+ sqlalchemy.orm.exc.StaleDataError,
+ # The oslo.db library will retry the request, so the second
+ # time this method is called let's return a valid session
+ # object
+ sql.session_for_write()
+ ]
+ with mock.patch('keystone.common.sql.session_for_write') as m:
+ m.side_effect = side_effects
+
+ # Update a user's attribute, the first attempt will fail but
+ # oslo.db will handle the exception and retry, the second attempt
+ # will succeed
+ new_user_dict['email'] = uuid.uuid4().hex
+ PROVIDERS.identity_api.update_user(
+ new_user_dict['id'], new_user_dict)
+
+ # Make sure oslo.db retried the update by checking the log output
+ expected_log_message = (
+ 'Performing DB retry for function keystone.identity.backends.sql'
+ )
+ self.assertIn(expected_log_message, log_fixture.output)
+
class SqlTrust(SqlTests, trust_tests.TrustTests):
diff --git a/keystone/tests/unit/test_contrib_ec2_core.py b/keystone/tests/unit/test_contrib_ec2_core.py
index e6cd96beb..967a41c01 100644
--- a/keystone/tests/unit/test_contrib_ec2_core.py
+++ b/keystone/tests/unit/test_contrib_ec2_core.py
@@ -12,10 +12,15 @@
# License for the specific language governing permissions and limitations
# under the License.
+import datetime
+import hashlib
+
from keystoneclient.contrib.ec2 import utils as ec2_utils
+from oslo_utils import timeutils
from six.moves import http_client
from keystone.common import provider_api
+from keystone.common import utils
from keystone.tests import unit
from keystone.tests.unit import test_v3
@@ -34,6 +39,7 @@ class EC2ContribCoreV3(test_v3.RestfulTestCase):
def test_valid_authentication_response_with_proper_secret(self):
signer = ec2_utils.Ec2Signer(self.cred_blob['secret'])
+ timestamp = utils.isotime(timeutils.utcnow())
credentials = {
'access': self.cred_blob['access'],
'secret': self.cred_blob['secret'],
@@ -43,7 +49,7 @@ class EC2ContribCoreV3(test_v3.RestfulTestCase):
'params': {
'SignatureVersion': '2',
'Action': 'Test',
- 'Timestamp': '2007-01-31T23:59:59Z'
+ 'Timestamp': timestamp
},
}
credentials['signature'] = signer.generate(credentials)
@@ -53,6 +59,48 @@ class EC2ContribCoreV3(test_v3.RestfulTestCase):
expected_status=http_client.OK)
self.assertValidProjectScopedTokenResponse(resp, self.user)
+ def test_valid_authentication_response_with_signature_v4(self):
+ signer = ec2_utils.Ec2Signer(self.cred_blob['secret'])
+ timestamp = utils.isotime(timeutils.utcnow())
+ hashed_payload = (
+ 'GET\n'
+ '/\n'
+ 'Action=Test\n'
+ 'host:localhost\n'
+ 'x-amz-date:' + timestamp + '\n'
+ '\n'
+ 'host;x-amz-date\n'
+ 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'
+ )
+ body_hash = hashlib.sha256(hashed_payload.encode()).hexdigest()
+ amz_credential = (
+ 'AKIAIOSFODNN7EXAMPLE/%s/us-east-1/iam/aws4_request,' %
+ timestamp[:8])
+
+ credentials = {
+ 'access': self.cred_blob['access'],
+ 'secret': self.cred_blob['secret'],
+ 'host': 'localhost',
+ 'verb': 'GET',
+ 'path': '/',
+ 'params': {
+ 'Action': 'Test',
+ 'X-Amz-Algorithm': 'AWS4-HMAC-SHA256',
+ 'X-Amz-SignedHeaders': 'host,x-amz-date,',
+ 'X-Amz-Credential': amz_credential
+ },
+ 'headers': {
+ 'X-Amz-Date': timestamp
+ },
+ 'body_hash': body_hash
+ }
+ credentials['signature'] = signer.generate(credentials)
+ resp = self.post(
+ '/ec2tokens',
+ body={'credentials': credentials},
+ expected_status=http_client.OK)
+ self.assertValidProjectScopedTokenResponse(resp, self.user)
+
def test_authenticate_with_empty_body_returns_bad_request(self):
self.post(
'/ec2tokens',
@@ -72,6 +120,7 @@ class EC2ContribCoreV3(test_v3.RestfulTestCase):
def test_authenticate_without_proper_secret_returns_unauthorized(self):
signer = ec2_utils.Ec2Signer('totally not the secret')
+ timestamp = utils.isotime(timeutils.utcnow())
credentials = {
'access': self.cred_blob['access'],
'secret': 'totally not the secret',
@@ -81,8 +130,80 @@ class EC2ContribCoreV3(test_v3.RestfulTestCase):
'params': {
'SignatureVersion': '2',
'Action': 'Test',
- 'Timestamp': '2007-01-31T23:59:59Z'
+ 'Timestamp': timestamp
+ },
+ }
+ credentials['signature'] = signer.generate(credentials)
+ self.post(
+ '/ec2tokens',
+ body={'credentials': credentials},
+ expected_status=http_client.UNAUTHORIZED)
+
+ def test_authenticate_expired_request(self):
+ self.config_fixture.config(
+ group='credential',
+ auth_ttl=5
+ )
+ signer = ec2_utils.Ec2Signer(self.cred_blob['secret'])
+ past = timeutils.utcnow() - datetime.timedelta(minutes=10)
+ timestamp = utils.isotime(past)
+ credentials = {
+ 'access': self.cred_blob['access'],
+ 'secret': self.cred_blob['secret'],
+ 'host': 'localhost',
+ 'verb': 'GET',
+ 'path': '/',
+ 'params': {
+ 'SignatureVersion': '2',
+ 'Action': 'Test',
+ 'Timestamp': timestamp
+ },
+ }
+ credentials['signature'] = signer.generate(credentials)
+ self.post(
+ '/ec2tokens',
+ body={'credentials': credentials},
+ expected_status=http_client.UNAUTHORIZED)
+
+ def test_authenticate_expired_request_v4(self):
+ self.config_fixture.config(
+ group='credential',
+ auth_ttl=5
+ )
+ signer = ec2_utils.Ec2Signer(self.cred_blob['secret'])
+ past = timeutils.utcnow() - datetime.timedelta(minutes=10)
+ timestamp = utils.isotime(past)
+ hashed_payload = (
+ 'GET\n'
+ '/\n'
+ 'Action=Test\n'
+ 'host:localhost\n'
+ 'x-amz-date:' + timestamp + '\n'
+ '\n'
+ 'host;x-amz-date\n'
+ 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'
+ )
+ body_hash = hashlib.sha256(hashed_payload.encode()).hexdigest()
+ amz_credential = (
+ 'AKIAIOSFODNN7EXAMPLE/%s/us-east-1/iam/aws4_request,' %
+ timestamp[:8])
+
+ credentials = {
+ 'access': self.cred_blob['access'],
+ 'secret': self.cred_blob['secret'],
+ 'host': 'localhost',
+ 'verb': 'GET',
+ 'path': '/',
+ 'params': {
+ 'Action': 'Test',
+ 'X-Amz-Algorithm': 'AWS4-HMAC-SHA256',
+ 'X-Amz-SignedHeaders': 'host,x-amz-date,',
+ 'X-Amz-Credential': amz_credential
+ },
+ 'headers': {
+ 'X-Amz-Date': timestamp
},
+ 'body_hash': body_hash
}
credentials['signature'] = signer.generate(credentials)
self.post(
diff --git a/keystone/tests/unit/test_v3_application_credential.py b/keystone/tests/unit/test_v3_application_credential.py
index 4e8899e56..017031873 100644
--- a/keystone/tests/unit/test_v3_application_credential.py
+++ b/keystone/tests/unit/test_v3_application_credential.py
@@ -166,6 +166,37 @@ class ApplicationCredentialTestCase(test_v3.RestfulTestCase):
expected_status_code=http_client.FORBIDDEN,
headers={'X-Auth-Token': token})
+ def test_create_application_credential_with_trust(self):
+ second_role = unit.new_role_ref(name='reader')
+ PROVIDERS.role_api.create_role(second_role['id'], second_role)
+ PROVIDERS.assignment_api.add_role_to_user_and_project(
+ self.user_id, self.project_id, second_role['id'])
+ with self.test_client() as c:
+ pw_token = self.get_scoped_token()
+ # create a self-trust - only the roles are important for this test
+ trust_ref = unit.new_trust_ref(
+ trustor_user_id=self.user_id,
+ trustee_user_id=self.user_id,
+ project_id=self.project_id,
+ role_ids=[second_role['id']])
+ resp = c.post('/v3/OS-TRUST/trusts',
+ headers={'X-Auth-Token': pw_token},
+ json={'trust': trust_ref})
+ trust_id = resp.json['trust']['id']
+ trust_auth = self.build_authentication_request(
+ user_id=self.user_id,
+ password=self.user['password'],
+ trust_id=trust_id)
+ trust_token = self.v3_create_token(
+ trust_auth).headers['X-Subject-Token']
+ app_cred = self._app_cred_body(roles=[{'id': self.role_id}])
+ # only the roles from the trust token should be allowed, even if
+ # the user has the role assigned on the project
+ c.post('/v3/users/%s/application_credentials' % self.user_id,
+ headers={'X-Auth-Token': trust_token},
+ json=app_cred,
+ expected_status_code=http_client.BAD_REQUEST)
+
def test_create_application_credential_allow_recursion(self):
with self.test_client() as c:
roles = [{'id': self.role_id}]
diff --git a/keystone/tests/unit/test_v3_credential.py b/keystone/tests/unit/test_v3_credential.py
index 2538c3a10..b9a2f482d 100644
--- a/keystone/tests/unit/test_v3_credential.py
+++ b/keystone/tests/unit/test_v3_credential.py
@@ -19,7 +19,7 @@ import uuid
from keystoneclient.contrib.ec2 import utils as ec2_utils
import mock
from oslo_db import exception as oslo_db_exception
-from six.moves import http_client
+from six.moves import http_client, urllib
from testtools import matchers
from keystone.api import ec2tokens
@@ -27,6 +27,7 @@ from keystone.common import provider_api
from keystone.common import utils
from keystone.credential.providers import fernet as credential_fernet
from keystone import exception
+from keystone import oauth1
from keystone.tests import unit
from keystone.tests.unit import ksfixtures
from keystone.tests.unit import test_v3
@@ -63,6 +64,33 @@ class CredentialBaseTestCase(test_v3.RestfulTestCase):
return json.dumps(blob), credential_id
+ def _test_get_token(self, access, secret):
+ """Test signature validation with the access/secret provided."""
+ signer = ec2_utils.Ec2Signer(secret)
+ params = {'SignatureMethod': 'HmacSHA256',
+ 'SignatureVersion': '2',
+ 'AWSAccessKeyId': access}
+ request = {'host': 'foo',
+ 'verb': 'GET',
+ 'path': '/bar',
+ 'params': params}
+ signature = signer.generate(request)
+
+ # Now make a request to validate the signed dummy request via the
+ # ec2tokens API. This proves the v3 ec2 credentials actually work.
+ sig_ref = {'access': access,
+ 'signature': signature,
+ 'host': 'foo',
+ 'verb': 'GET',
+ 'path': '/bar',
+ 'params': params}
+ r = self.post(
+ '/ec2tokens',
+ body={'ec2Credentials': sig_ref},
+ expected_status=http_client.OK)
+ self.assertValidTokenResponse(r)
+ return r.result['token']
+
class CredentialTestCase(CredentialBaseTestCase):
"""Test credential CRUD."""
@@ -258,6 +286,126 @@ class CredentialTestCase(CredentialBaseTestCase):
'credential_id': credential_id},
body={'credential': update_ref})
+ def test_update_credential_non_owner(self):
+ """Call ``PATCH /credentials/{credential_id}``."""
+ alt_user = unit.create_user(
+ PROVIDERS.identity_api, domain_id=self.domain_id)
+ alt_user_id = alt_user['id']
+ alt_project = unit.new_project_ref(domain_id=self.domain_id)
+ alt_project_id = alt_project['id']
+ PROVIDERS.resource_api.create_project(
+ alt_project['id'], alt_project)
+ alt_role = unit.new_role_ref(name='reader')
+ alt_role_id = alt_role['id']
+ PROVIDERS.role_api.create_role(alt_role_id, alt_role)
+ PROVIDERS.assignment_api.add_role_to_user_and_project(
+ alt_user_id, alt_project_id, alt_role_id)
+ auth = self.build_authentication_request(
+ user_id=alt_user_id,
+ password=alt_user['password'],
+ project_id=alt_project_id)
+ ref = unit.new_credential_ref(user_id=alt_user_id,
+ project_id=alt_project_id)
+ r = self.post(
+ '/credentials',
+ auth=auth,
+ body={'credential': ref})
+ self.assertValidCredentialResponse(r, ref)
+ credential_id = r.result.get('credential')['id']
+
+ # Cannot change the credential to be owned by another user
+ update_ref = {'user_id': self.user_id, 'project_id': self.project_id}
+ self.patch(
+ '/credentials/%(credential_id)s' % {
+ 'credential_id': credential_id},
+ expected_status=403,
+ auth=auth,
+ body={'credential': update_ref})
+
+ def test_update_ec2_credential_change_trust_id(self):
+ """Call ``PATCH /credentials/{credential_id}``."""
+ blob, ref = unit.new_ec2_credential(user_id=self.user['id'],
+ project_id=self.project_id)
+ blob['trust_id'] = uuid.uuid4().hex
+ ref['blob'] = json.dumps(blob)
+ r = self.post(
+ '/credentials',
+ body={'credential': ref})
+ self.assertValidCredentialResponse(r, ref)
+ credential_id = r.result.get('credential')['id']
+ # Try changing to a different trust
+ blob['trust_id'] = uuid.uuid4().hex
+ update_ref = {'blob': json.dumps(blob)}
+ self.patch(
+ '/credentials/%(credential_id)s' % {
+ 'credential_id': credential_id},
+ body={'credential': update_ref},
+ expected_status=http_client.BAD_REQUEST)
+ # Try removing the trust
+ del blob['trust_id']
+ update_ref = {'blob': json.dumps(blob)}
+ self.patch(
+ '/credentials/%(credential_id)s' % {
+ 'credential_id': credential_id},
+ body={'credential': update_ref},
+ expected_status=http_client.BAD_REQUEST)
+
+ def test_update_ec2_credential_change_app_cred_id(self):
+ """Call ``PATCH /credentials/{credential_id}``."""
+ blob, ref = unit.new_ec2_credential(user_id=self.user['id'],
+ project_id=self.project_id)
+ blob['app_cred_id'] = uuid.uuid4().hex
+ ref['blob'] = json.dumps(blob)
+ r = self.post(
+ '/credentials',
+ body={'credential': ref})
+ self.assertValidCredentialResponse(r, ref)
+ credential_id = r.result.get('credential')['id']
+ # Try changing to a different app cred
+ blob['app_cred_id'] = uuid.uuid4().hex
+ update_ref = {'blob': json.dumps(blob)}
+ self.patch(
+ '/credentials/%(credential_id)s' % {
+ 'credential_id': credential_id},
+ body={'credential': update_ref},
+ expected_status=http_client.BAD_REQUEST)
+ # Try removing the app cred
+ del blob['app_cred_id']
+ update_ref = {'blob': json.dumps(blob)}
+ self.patch(
+ '/credentials/%(credential_id)s' % {
+ 'credential_id': credential_id},
+ body={'credential': update_ref},
+ expected_status=http_client.BAD_REQUEST)
+
+ def test_update_ec2_credential_change_access_token_id(self):
+ """Call ``PATCH /credentials/{credential_id}``."""
+ blob, ref = unit.new_ec2_credential(user_id=self.user['id'],
+ project_id=self.project_id)
+ blob['access_token_id'] = uuid.uuid4().hex
+ ref['blob'] = json.dumps(blob)
+ r = self.post(
+ '/credentials',
+ body={'credential': ref})
+ self.assertValidCredentialResponse(r, ref)
+ credential_id = r.result.get('credential')['id']
+ # Try changing to a different access token
+ blob['access_token_id'] = uuid.uuid4().hex
+ update_ref = {'blob': json.dumps(blob)}
+ self.patch(
+ '/credentials/%(credential_id)s' % {
+ 'credential_id': credential_id},
+ body={'credential': update_ref},
+ expected_status=http_client.BAD_REQUEST)
+ # Try removing the access token
+ del blob['access_token_id']
+ update_ref = {'blob': json.dumps(blob)}
+ self.patch(
+ '/credentials/%(credential_id)s' % {
+ 'credential_id': credential_id},
+ body={'credential': update_ref},
+ expected_status=http_client.BAD_REQUEST)
+
def test_delete_credential(self):
"""Call ``DELETE /credentials/{credential_id}``."""
self.delete(
@@ -393,7 +541,7 @@ class CredentialTestCase(CredentialBaseTestCase):
self.assertValidCredentialResponse(r, ref)
-class TestCredentialTrustScoped(test_v3.RestfulTestCase):
+class TestCredentialTrustScoped(CredentialBaseTestCase):
"""Test credential with trust scoped token."""
def setUp(self):
@@ -446,7 +594,7 @@ class TestCredentialTrustScoped(test_v3.RestfulTestCase):
token_id = r.headers.get('X-Subject-Token')
# Create the credential with the trust scoped token
- blob, ref = unit.new_ec2_credential(user_id=self.user['id'],
+ blob, ref = unit.new_ec2_credential(user_id=self.user_id,
project_id=self.project_id)
r = self.post('/credentials', body={'credential': ref}, token=token_id)
@@ -463,6 +611,97 @@ class TestCredentialTrustScoped(test_v3.RestfulTestCase):
self.assertEqual(hashlib.sha256(access).hexdigest(),
r.result['credential']['id'])
+ # Create a role assignment to ensure that it is ignored and only the
+ # trust-delegated roles are used
+ role = unit.new_role_ref(name='reader')
+ role_id = role['id']
+ PROVIDERS.role_api.create_role(role_id, role)
+ PROVIDERS.assignment_api.add_role_to_user_and_project(
+ self.user_id, self.project_id, role_id)
+
+ ret_blob = json.loads(r.result['credential']['blob'])
+ ec2token = self._test_get_token(
+ access=ret_blob['access'], secret=ret_blob['secret'])
+ ec2_roles = [role['id'] for role in ec2token['roles']]
+ self.assertIn(self.role_id, ec2_roles)
+ self.assertNotIn(role_id, ec2_roles)
+
+ # Create second ec2 credential with the same access key id and check
+ # for conflict.
+ self.post(
+ '/credentials',
+ body={'credential': ref},
+ token=token_id,
+ expected_status=http_client.CONFLICT)
+
+
+class TestCredentialAppCreds(CredentialBaseTestCase):
+ """Test credential with application credential token."""
+
+ def setUp(self):
+ super(TestCredentialAppCreds, self).setUp()
+ self.useFixture(
+ ksfixtures.KeyRepository(
+ self.config_fixture,
+ 'credential',
+ credential_fernet.MAX_ACTIVE_KEYS
+ )
+ )
+
+ def test_app_cred_ec2_credential(self):
+ """Test creating ec2 credential from an application credential.
+
+ Call ``POST /credentials``.
+ """
+ # Create the app cred
+ ref = {
+ 'name': uuid.uuid4().hex,
+ 'roles': [{'id': self.role_id}]
+ }
+ r = self.post('/users/%s/application_credentials' % self.user_id,
+ body={'application_credential': ref})
+ app_cred = r.result['application_credential']
+
+ # Get an application credential token
+ auth_data = self.build_authentication_request(
+ app_cred_id=app_cred['id'],
+ secret=app_cred['secret'])
+ r = self.v3_create_token(auth_data)
+ token_id = r.headers.get('X-Subject-Token')
+
+ # Create the credential with the app cred token
+ blob, ref = unit.new_ec2_credential(user_id=self.user_id,
+ project_id=self.project_id)
+ r = self.post('/credentials', body={'credential': ref}, token=token_id)
+
+ # We expect the response blob to contain the app_cred_id
+ ret_ref = ref.copy()
+ ret_blob = blob.copy()
+ ret_blob['app_cred_id'] = app_cred['id']
+ ret_ref['blob'] = json.dumps(ret_blob)
+ self.assertValidCredentialResponse(r, ref=ret_ref)
+
+ # Assert credential id is same as hash of access key id for
+ # ec2 credentials
+ access = blob['access'].encode('utf-8')
+ self.assertEqual(hashlib.sha256(access).hexdigest(),
+ r.result['credential']['id'])
+
+ # Create a role assignment to ensure that it is ignored and only the
+ # roles in the app cred are used
+ role = unit.new_role_ref(name='reader')
+ role_id = role['id']
+ PROVIDERS.role_api.create_role(role_id, role)
+ PROVIDERS.assignment_api.add_role_to_user_and_project(
+ self.user_id, self.project_id, role_id)
+
+ ret_blob = json.loads(r.result['credential']['blob'])
+ ec2token = self._test_get_token(
+ access=ret_blob['access'], secret=ret_blob['secret'])
+ ec2_roles = [role['id'] for role in ec2token['roles']]
+ self.assertIn(self.role_id, ec2_roles)
+ self.assertNotIn(role_id, ec2_roles)
+
# Create second ec2 credential with the same access key id and check
# for conflict.
self.post(
@@ -472,34 +711,155 @@ class TestCredentialTrustScoped(test_v3.RestfulTestCase):
expected_status=http_client.CONFLICT)
-class TestCredentialEc2(CredentialBaseTestCase):
- """Test v3 credential compatibility with ec2tokens."""
+class TestCredentialAccessToken(CredentialBaseTestCase):
+ """Test credential with access token."""
- def _validate_signature(self, access, secret):
- """Test signature validation with the access/secret provided."""
- signer = ec2_utils.Ec2Signer(secret)
- params = {'SignatureMethod': 'HmacSHA256',
- 'SignatureVersion': '2',
- 'AWSAccessKeyId': access}
- request = {'host': 'foo',
- 'verb': 'GET',
- 'path': '/bar',
- 'params': params}
- signature = signer.generate(request)
+ def setUp(self):
+ super(TestCredentialAccessToken, self).setUp()
+ self.useFixture(
+ ksfixtures.KeyRepository(
+ self.config_fixture,
+ 'credential',
+ credential_fernet.MAX_ACTIVE_KEYS
+ )
+ )
+ self.base_url = 'http://localhost/v3'
+
+ def _urllib_parse_qs_text_keys(self, content):
+ results = urllib.parse.parse_qs(content)
+ return {key.decode('utf-8'): value for key, value in results.items()}
+
+ def _create_single_consumer(self):
+ endpoint = '/OS-OAUTH1/consumers'
+
+ ref = {'description': uuid.uuid4().hex}
+ resp = self.post(endpoint, body={'consumer': ref})
+ return resp.result['consumer']
+
+ def _create_request_token(self, consumer, project_id, base_url=None):
+ endpoint = '/OS-OAUTH1/request_token'
+ client = oauth1.Client(consumer['key'],
+ client_secret=consumer['secret'],
+ signature_method=oauth1.SIG_HMAC,
+ callback_uri="oob")
+ headers = {'requested_project_id': project_id}
+ if not base_url:
+ base_url = self.base_url
+ url, headers, body = client.sign(base_url + endpoint,
+ http_method='POST',
+ headers=headers)
+ return endpoint, headers
+
+ def _create_access_token(self, consumer, token, base_url=None):
+ endpoint = '/OS-OAUTH1/access_token'
+ client = oauth1.Client(consumer['key'],
+ client_secret=consumer['secret'],
+ resource_owner_key=token.key,
+ resource_owner_secret=token.secret,
+ signature_method=oauth1.SIG_HMAC,
+ verifier=token.verifier)
+ if not base_url:
+ base_url = self.base_url
+ url, headers, body = client.sign(base_url + endpoint,
+ http_method='POST')
+ headers.update({'Content-Type': 'application/json'})
+ return endpoint, headers
+
+ def _get_oauth_token(self, consumer, token):
+ client = oauth1.Client(consumer['key'],
+ client_secret=consumer['secret'],
+ resource_owner_key=token.key,
+ resource_owner_secret=token.secret,
+ signature_method=oauth1.SIG_HMAC)
+ endpoint = '/auth/tokens'
+ url, headers, body = client.sign(self.base_url + endpoint,
+ http_method='POST')
+ headers.update({'Content-Type': 'application/json'})
+ ref = {'auth': {'identity': {'oauth1': {}, 'methods': ['oauth1']}}}
+ return endpoint, headers, ref
+
+ def _authorize_request_token(self, request_id):
+ if isinstance(request_id, bytes):
+ request_id = request_id.decode()
+ return '/OS-OAUTH1/authorize/%s' % (request_id)
+
+ def _get_access_token(self):
+ consumer = self._create_single_consumer()
+ consumer_id = consumer['id']
+ consumer_secret = consumer['secret']
+ consumer = {'key': consumer_id, 'secret': consumer_secret}
+
+ url, headers = self._create_request_token(consumer, self.project_id)
+ content = self.post(
+ url, headers=headers,
+ response_content_type='application/x-www-form-urlencoded')
+ credentials = self._urllib_parse_qs_text_keys(content.result)
+ request_key = credentials['oauth_token'][0]
+ request_secret = credentials['oauth_token_secret'][0]
+ request_token = oauth1.Token(request_key, request_secret)
+
+ url = self._authorize_request_token(request_key)
+ body = {'roles': [{'id': self.role_id}]}
+ resp = self.put(url, body=body, expected_status=http_client.OK)
+ verifier = resp.result['token']['oauth_verifier']
+
+ request_token.set_verifier(verifier)
+ url, headers = self._create_access_token(consumer, request_token)
+ content = self.post(
+ url, headers=headers,
+ response_content_type='application/x-www-form-urlencoded')
+ credentials = self._urllib_parse_qs_text_keys(content.result)
+ access_key = credentials['oauth_token'][0]
+ access_secret = credentials['oauth_token_secret'][0]
+ access_token = oauth1.Token(access_key, access_secret)
+
+ url, headers, body = self._get_oauth_token(consumer, access_token)
+ content = self.post(url, headers=headers, body=body)
+ return access_key, content.headers['X-Subject-Token']
+
+ def test_access_token_ec2_credential(self):
+ """Test creating ec2 credential from an oauth access token.
- # Now make a request to validate the signed dummy request via the
- # ec2tokens API. This proves the v3 ec2 credentials actually work.
- sig_ref = {'access': access,
- 'signature': signature,
- 'host': 'foo',
- 'verb': 'GET',
- 'path': '/bar',
- 'params': params}
- r = self.post(
- '/ec2tokens',
- body={'ec2Credentials': sig_ref},
- expected_status=http_client.OK)
- self.assertValidTokenResponse(r)
+ Call ``POST /credentials``.
+ """
+ access_key, token_id = self._get_access_token()
+
+ # Create the credential with the access token
+ blob, ref = unit.new_ec2_credential(user_id=self.user_id,
+ project_id=self.project_id)
+ r = self.post('/credentials', body={'credential': ref}, token=token_id)
+
+ # We expect the response blob to contain the access_token_id
+ ret_ref = ref.copy()
+ ret_blob = blob.copy()
+ ret_blob['access_token_id'] = access_key.decode('utf-8')
+ ret_ref['blob'] = json.dumps(ret_blob)
+ self.assertValidCredentialResponse(r, ref=ret_ref)
+
+ # Assert credential id is same as hash of access key id for
+ # ec2 credentials
+ access = blob['access'].encode('utf-8')
+ self.assertEqual(hashlib.sha256(access).hexdigest(),
+ r.result['credential']['id'])
+
+ # Create a role assignment to ensure that it is ignored and only the
+ # roles in the access token are used
+ role = unit.new_role_ref(name='reader')
+ role_id = role['id']
+ PROVIDERS.role_api.create_role(role_id, role)
+ PROVIDERS.assignment_api.add_role_to_user_and_project(
+ self.user_id, self.project_id, role_id)
+
+ ret_blob = json.loads(r.result['credential']['blob'])
+ ec2token = self._test_get_token(
+ access=ret_blob['access'], secret=ret_blob['secret'])
+ ec2_roles = [role['id'] for role in ec2token['roles']]
+ self.assertIn(self.role_id, ec2_roles)
+ self.assertNotIn(role_id, ec2_roles)
+
+
+class TestCredentialEc2(CredentialBaseTestCase):
+ """Test v3 credential compatibility with ec2tokens."""
def test_ec2_credential_signature_validate(self):
"""Test signature validation with a v3 ec2 credential."""
@@ -514,15 +874,15 @@ class TestCredentialEc2(CredentialBaseTestCase):
cred_blob = json.loads(r.result['credential']['blob'])
self.assertEqual(blob, cred_blob)
- self._validate_signature(access=cred_blob['access'],
- secret=cred_blob['secret'])
+ self._test_get_token(access=cred_blob['access'],
+ secret=cred_blob['secret'])
def test_ec2_credential_signature_validate_legacy(self):
"""Test signature validation with a legacy v3 ec2 credential."""
cred_json, _ = self._create_dict_blob_credential()
cred_blob = json.loads(cred_json)
- self._validate_signature(access=cred_blob['access'],
- secret=cred_blob['secret'])
+ self._test_get_token(access=cred_blob['access'],
+ secret=cred_blob['secret'])
def _get_ec2_cred_uri(self):
return '/users/%s/credentials/OS-EC2' % self.user_id
@@ -538,8 +898,8 @@ class TestCredentialEc2(CredentialBaseTestCase):
self.assertEqual(self.user_id, ec2_cred['user_id'])
self.assertEqual(self.project_id, ec2_cred['tenant_id'])
self.assertIsNone(ec2_cred['trust_id'])
- self._validate_signature(access=ec2_cred['access'],
- secret=ec2_cred['secret'])
+ self._test_get_token(access=ec2_cred['access'],
+ secret=ec2_cred['secret'])
uri = '/'.join([self._get_ec2_cred_uri(), ec2_cred['access']])
self.assertThat(ec2_cred['links']['self'],
matchers.EndsWith(uri))
diff --git a/keystone/tests/unit/test_v3_oauth1.py b/keystone/tests/unit/test_v3_oauth1.py
index 90378214e..4c648a23e 100644
--- a/keystone/tests/unit/test_v3_oauth1.py
+++ b/keystone/tests/unit/test_v3_oauth1.py
@@ -308,6 +308,19 @@ class OAuthFlowTests(OAuth1Tests):
self.keystone_token = content.result['token']
self.assertIsNotNone(self.keystone_token_id)
+ # add a new role assignment to ensure it is ignored in the access token
+ new_role = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex}
+ PROVIDERS.role_api.create_role(new_role['id'], new_role)
+ PROVIDERS.assignment_api.add_role_to_user_and_project(
+ user_id=self.user_id,
+ project_id=self.project_id,
+ role_id=new_role['id'])
+ content = self.post(url, headers=headers, body=body)
+ token = content.result['token']
+ token_roles = [r['id'] for r in token['roles']]
+ self.assertIn(self.role_id, token_roles)
+ self.assertNotIn(new_role['id'], token_roles)
+
class AccessTokenCRUDTests(OAuthFlowTests):
def test_delete_access_token_dne(self):
diff --git a/lower-constraints.txt b/lower-constraints.txt
index dab11af99..48a357922 100644
--- a/lower-constraints.txt
+++ b/lower-constraints.txt
@@ -1,4 +1,4 @@
-amqp==2.2.2
+amqp==5.0.0
Babel==2.3.4
bashate==0.5.1
bcrypt==3.1.3
@@ -8,7 +8,7 @@ docutils==0.14
dogpile.cache==0.6.2
fixtures==3.0.0
flake8-docstrings==0.2.1.post1
-flake8==2.5.5
+flake8==2.6.0
Flask===1.0.2
Flask-RESTful===0.3.5
freezegun==0.3.6
@@ -50,8 +50,10 @@ python-ldap===3.0.0
pymongo===3.0.2
pysaml2==4.5.0
PyJWT==1.6.1
+PyMySQL==0.7.6
python-keystoneclient==3.8.0
-python-memcached===1.56
+python-memcached==1.56;python_version=='2.7'
+python-memcached==1.58;python_version>='3.4'
pytz==2013.6
requests==2.14.2
scrypt==0.8.0
diff --git a/releasenotes/notes/bug-1831918-c70cf87ef086d871.yaml b/releasenotes/notes/bug-1831918-c70cf87ef086d871.yaml
new file mode 100644
index 000000000..33a355cc5
--- /dev/null
+++ b/releasenotes/notes/bug-1831918-c70cf87ef086d871.yaml
@@ -0,0 +1,6 @@
+---
+fixes:
+ - |
+ [`bug 1831918 <https://bugs.launchpad.net/keystone/+bug/1831918>`_]
+ Credentials now logs cadf audit messages.
+
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-1872733-2377f456a57ad32c.yaml b/releasenotes/notes/bug-1872733-2377f456a57ad32c.yaml
new file mode 100644
index 000000000..656822c2a
--- /dev/null
+++ b/releasenotes/notes/bug-1872733-2377f456a57ad32c.yaml
@@ -0,0 +1,16 @@
+---
+critical:
+ - |
+ [`bug 1872733 <https://bugs.launchpad.net/keystone/+bug/1872733>`_]
+ Fixed a critical security issue in which an authenticated user could
+ escalate their privileges by altering a valid EC2 credential.
+security:
+ - |
+ [`bug 1872733 <https://bugs.launchpad.net/keystone/+bug/1872733>`_]
+ Fixed a critical security issue in which an authenticated user could
+ escalate their privileges by altering a valid EC2 credential.
+fixes:
+ - |
+ [`bug 1872733 <https://bugs.launchpad.net/keystone/+bug/1872733>`_]
+ Fixed a critical security issue in which an authenticated user could
+ escalate their privileges by altering a valid EC2 credential.
diff --git a/releasenotes/notes/bug-1872735-0989e51d2248ce1e.yaml b/releasenotes/notes/bug-1872735-0989e51d2248ce1e.yaml
new file mode 100644
index 000000000..1aed86301
--- /dev/null
+++ b/releasenotes/notes/bug-1872735-0989e51d2248ce1e.yaml
@@ -0,0 +1,31 @@
+---
+critical:
+ - |
+ [`bug 1872735 <https://bugs.launchpad.net/keystone/+bug/1872735>`_]
+ Fixed a security issue in which a trustee or an application credential user
+ could create an EC2 credential or an application credential that would
+ permit them to get a token that elevated their role assignments beyond the
+ subset delegated to them in the trust or application credential. A new
+ attribute ``app_cred_id`` is now automatically added to the access blob of
+ an EC2 credential and the role list in the trust or application credential
+ is respected.
+security:
+ - |
+ [`bug 1872735 <https://bugs.launchpad.net/keystone/+bug/1872735>`_]
+ Fixed a security issue in which a trustee or an application credential user
+ could create an EC2 credential or an application credential that would
+ permit them to get a token that elevated their role assignments beyond the
+ subset delegated to them in the trust or application credential. A new
+ attribute ``app_cred_id`` is now automatically added to the access blob of
+ an EC2 credential and the role list in the trust or application credential
+ is respected.
+fixes:
+ - |
+ [`bug 1872735 <https://bugs.launchpad.net/keystone/+bug/1872735>`_]
+ Fixed a security issue in which a trustee or an application credential user
+ could create an EC2 credential or an application credential that would
+ permit them to get a token that elevated their role assignments beyond the
+ subset delegated to them in the trust or application credential. A new
+ attribute ``app_cred_id`` is now automatically added to the access blob of
+ an EC2 credential and the role list in the trust or application credential
+ is respected.
diff --git a/releasenotes/notes/bug-1872737-f8e1ad3b6705b766.yaml b/releasenotes/notes/bug-1872737-f8e1ad3b6705b766.yaml
new file mode 100644
index 000000000..d0732ab4c
--- /dev/null
+++ b/releasenotes/notes/bug-1872737-f8e1ad3b6705b766.yaml
@@ -0,0 +1,28 @@
+---
+feature:
+ - |
+ [`bug 1872737 <https://bugs.launchpad.net/keystone/+bug/1872737>`_]
+ Added a new config option ``auth_ttl`` in the ``[credential]`` config
+ section to allow configuring the period for which a signed token request
+ from AWS is valid. The default is 15 minutes in accordance with the AWS
+ Signature V4 API reference.
+upgrade:
+ - |
+ [`bug 1872737 <https://bugs.launchpad.net/keystone/+bug/1872737>`_]
+ Added a default TTL of 15 minutes for signed EC2 credential requests,
+ where previously an EC2 signed token request was valid indefinitely. This
+ change in behavior is needed to protect against replay attacks.
+security:
+ - |
+ [`bug 1872737 <https://bugs.launchpad.net/keystone/+bug/1872737>`_]
+ Fixed an incorrect EC2 token validation implementation in which the
+ timestamp of the signed request was ignored, which made EC2 and S3 token
+ requests vulnerable to replay attacks. The default TTL is 15 minutes but
+ is configurable.
+fixes:
+ - |
+ [`bug 1872737 <https://bugs.launchpad.net/keystone/+bug/1872737>`_]
+ Fixed an incorrect EC2 token validation implementation in which the
+ timestamp of the signed request was ignored, which made EC2 and S3 token
+ requests vulnerable to replay attacks. The default TTL is 15 minutes but
+ is configurable.
diff --git a/releasenotes/notes/bug-1872755-2c81d3267b89f124.yaml b/releasenotes/notes/bug-1872755-2c81d3267b89f124.yaml
new file mode 100644
index 000000000..a30259ffa
--- /dev/null
+++ b/releasenotes/notes/bug-1872755-2c81d3267b89f124.yaml
@@ -0,0 +1,19 @@
+---
+security:
+ - |
+ [`bug 1872755 <https://bugs.launchpad.net/keystone/+bug/1872755>`_]
+ Added validation to the EC2 credentials update API to ensure the metadata
+ labels 'trust_id' and 'app_cred_id' are not altered by the user. These
+ labels are used by keystone to determine the scope allowed by the
+ credential, and altering these automatic labels could enable an EC2
+ credential holder to elevate their access beyond what is permitted by the
+ application credential or trust that was used to create the EC2 credential.
+fixes:
+ - |
+ [`bug 1872755 <https://bugs.launchpad.net/keystone/+bug/1872755>`_]
+ Added validation to the EC2 credentials update API to ensure the metadata
+ labels 'trust_id' and 'app_cred_id' are not altered by the user. These
+ labels are used by keystone to determine the scope allowed by the
+ credential, and altering these automatic labels could enable an EC2
+ credential holder to elevate their access beyond what is permitted by the
+ application credential or trust that was used to create the EC2 credential.
diff --git a/releasenotes/notes/bug-1873290-ff7f8e4cee15b75a.yaml b/releasenotes/notes/bug-1873290-ff7f8e4cee15b75a.yaml
new file mode 100644
index 000000000..ad35a3047
--- /dev/null
+++ b/releasenotes/notes/bug-1873290-ff7f8e4cee15b75a.yaml
@@ -0,0 +1,19 @@
+---
+security:
+ - |
+ [`bug 1873290 <https://bugs.launchpad.net/keystone/+bug/1873290>`_]
+ [`bug 1872735 <https://bugs.launchpad.net/keystone/+bug/1872735>`_]
+ Fixed the token model to respect the roles authorized OAuth1 access tokens.
+ Previously, the list of roles authorized for an OAuth1 access token were
+ ignored, so when an access token was used to request a keystone token, the
+ keystone token would contain every role assignment the creator had for the
+ project. This also fixed EC2 credentials to respect those roles as well.
+fixes:
+ - |
+ [`bug 1873290 <https://bugs.launchpad.net/keystone/+bug/1873290>`_]
+ [`bug 1872735 <https://bugs.launchpad.net/keystone/+bug/1872735>`_]
+ Fixed the token model to respect the roles authorized OAuth1 access tokens.
+ Previously, the list of roles authorized for an OAuth1 access token were
+ ignored, so when an access token was used to request a keystone token, the
+ keystone token would contain every role assignment the creator had for the
+ project. This also fixed EC2 credentials to respect those roles as well.
diff --git a/releasenotes/notes/bug-1878938-70ee2af6fdf66004.yaml b/releasenotes/notes/bug-1878938-70ee2af6fdf66004.yaml
new file mode 100644
index 000000000..21a53b482
--- /dev/null
+++ b/releasenotes/notes/bug-1878938-70ee2af6fdf66004.yaml
@@ -0,0 +1,16 @@
+---
+fixes:
+ - |
+ [`bug 1878938 <https://bugs.launchpad.net/keystone/+bug/1878938>`_]
+ Previously when a user used to have system role assignment and tries to delete
+ the same role, the system role assignments still existed in system_assignment
+ table. This causes keystone to return `HTTP 404 Not Found` errors when listing
+ role assignments with names (e.g., `--names` or `?include_names`).
+
+ If you are affected by this bug, you must remove stale role assignments
+ manually. The following is an example SQL statement you can use to fix the
+ issue, but you should verify it's applicability to your deployment's SQL
+ implementation and version.
+
+ SQL:
+ - delete from system_assignment where role_id not in (select id from role);
diff --git a/releasenotes/notes/bug-1885753-51df25f3ff1d9ae8.yaml b/releasenotes/notes/bug-1885753-51df25f3ff1d9ae8.yaml
new file mode 100644
index 000000000..f8f2d7f9c
--- /dev/null
+++ b/releasenotes/notes/bug-1885753-51df25f3ff1d9ae8.yaml
@@ -0,0 +1,6 @@
+---
+fixes:
+ - |
+ [`bug 1885753 <https://bugs.launchpad.net/keystone/+bug/1885753>`_]
+ Keystone's SQL identity backend now retries update user requests to safely
+ handle stale data when two clients update a user at the same time.
diff --git a/releasenotes/notes/bug-1889936-78d6853b5212b8f1.yaml b/releasenotes/notes/bug-1889936-78d6853b5212b8f1.yaml
new file mode 100644
index 000000000..de96b27f7
--- /dev/null
+++ b/releasenotes/notes/bug-1889936-78d6853b5212b8f1.yaml
@@ -0,0 +1,5 @@
+---
+fixes:
+ - |
+ [`bug 1889936 <https://bugs.launchpad.net/keystone/+bug/1889936>`_]
+ Properly decode octet strings, or byte arrays, returned from LDAP.
diff --git a/setup.cfg b/setup.cfg
index 20f674d9a..80ad3b74c 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -31,7 +31,8 @@ ldap =
python-ldap>=3.0.0 # PSF
ldappool>=2.3.1 # MPL
memcache =
- python-memcached>=1.56 # PSF
+ python-memcached>=1.56:python_version=='2.7' # PSF
+ python-memcached>=1.58:python_version>='3.4' # PSF
mongodb =
pymongo!=3.1,>=3.0.2 # Apache-2.0
bandit =
diff --git a/test-requirements.txt b/test-requirements.txt
index a86a1fa44..6cdc34d31 100644
--- a/test-requirements.txt
+++ b/test-requirements.txt
@@ -4,7 +4,6 @@
hacking>=1.1.0,<1.2.0 # Apache-2.0
pep257==0.7.0 # MIT License
-pycodestyle>=2.0.0 # MIT License
flake8-docstrings==0.2.1.post1 # MIT
bashate>=0.5.1 # Apache-2.0
os-testr>=1.0.0 # Apache-2.0