Allow an explicit_domain_id parameter when creating a domain

This allows domain_ids to match across distinct Keystone
deployments The domain_id is used to create unique
identifiers with the mapping backend.  When this
option is used, mapped user identifiers can be
consistant across different Keystone servers.

closes-bug: 1794527

Change-Id: I100bca162e71a9d394ed5787b976b13b1e57987f
This commit is contained in:
Adam Young 2018-09-25 17:09:02 -04:00 committed by ayoung
parent af4ec20f83
commit 4cd99e7197
7 changed files with 96 additions and 4 deletions

View File

@ -113,7 +113,17 @@ class DomainResource(ks_flask.ResourceBase):
ENFORCER.enforce_call(action='identity:create_domain')
domain = self.request_body_json.get('domain', {})
validation.lazy_validate(schema.domain_create, domain)
domain = self._assign_unique_id(domain)
domain_id = domain.get('explicit_domain_id')
if domain_id is None:
domain = self._assign_unique_id(domain)
else:
# Domain ID validation provided by PyCADF
try:
self._validate_id_format(domain_id)
except ValueError:
raise exception.DomainIdInvalid
domain['id'] = domain_id
domain = self._normalize_dict(domain)
ref = PROVIDERS.resource_api.create_domain(
domain['id'], domain, initiator=self.audit_initiator)

View File

@ -429,6 +429,10 @@ class DomainSpecificRoleNotWithinIdPDomain(Forbidden):
"the identity provider: %(identity_provider)s.")
class DomainIdInvalid(ValidationError):
message_format = _("Domain ID does not conform to required UUID format.")
class RoleAssignmentNotFound(NotFound):
message_format = _("Could not find role assignment with role: "
"%(role_id)s, user or group: %(actor_id)s, "

View File

@ -186,8 +186,10 @@ class Manager(manager.Manager):
) % project['name']
else:
return _('it is not permitted to have two projects '
'with the same name in the same domain : %s'
) % project['name']
'with either the same name or same id in '
'the same domain: '
'name is %(name)s, project id %(id)s'
) % project
def create_project(self, project_id, project, initiator=None):
project = project.copy()

View File

@ -604,6 +604,12 @@ class ResourceBase(flask_restful.Resource):
ref['id'] = uuid.uuid4().hex
return ref
@staticmethod
def _validate_id_format(id):
uval = uuid.UUID(id).hex
if uval != id:
raise ValueError('badly formed hexadecimal UUID value')
@classmethod
def _require_matching_id(cls, ref):
"""Ensure the value matches the reference's ID, if any."""

View File

@ -59,7 +59,8 @@ class TestResourceManagerNoFixtures(unit.SQLDriverOverrides, unit.TestCase):
self.assertRaises(exception.Conflict,
PROVIDERS.resource_api.update_project,
project['id'], {'name': project1['name']})
project['id'], {'name': project1['name'],
'id': project['id']})
class DomainConfigDriverTests(object):

View File

@ -142,6 +142,54 @@ class ResourceTestCase(test_v3.RestfulTestCase,
self.assertValidDomainResponse(r)
self.assertIsNotNone(r.result['domain'])
def test_create_domain_valid_explicit_id(self):
"""Call ``POST /domains`` with a valid `explicit_domain_id` set."""
ref = unit.new_domain_ref()
explicit_domain_id = '9aea63518f0040c6b4518d8d2242911c'
ref['explicit_domain_id'] = explicit_domain_id
r = self.post(
'/domains',
body={'domain': ref})
self.assertValidDomainResponse(r, ref)
r = self.get('/domains/%(domain_id)s' % {
'domain_id': explicit_domain_id})
self.assertValidDomainResponse(r)
self.assertIsNotNone(r.result['domain'])
def test_create_second_domain_valid_explicit_id_fails(self):
"""Call ``POST /domains`` with a valid `explicit_domain_id` set."""
ref = unit.new_domain_ref()
explicit_domain_id = '9aea63518f0040c6b4518d8d2242911c'
ref['explicit_domain_id'] = explicit_domain_id
r = self.post(
'/domains',
body={'domain': ref})
self.assertValidDomainResponse(r, ref)
# second one should fail
r = self.post(
'/domains',
body={'domain': ref},
expected_status=http_client.CONFLICT)
def test_create_domain_invalid_explicit_ids(self):
"""Call ``POST /domains`` with various invalid explicit_domain_ids."""
ref = unit.new_domain_ref()
bad_ids = ['bad!',
'',
'9aea63518f0040c',
'1234567890123456789012345678901234567890',
'9aea63518f0040c6b4518d8d2242911c9aea63518f0040c6b45']
for explicit_domain_id in bad_ids:
ref['explicit_domain_id'] = explicit_domain_id
self.post('/domains', body={'domain': {}},
expected_status=http_client.BAD_REQUEST)
def test_list_head_domains(self):
"""Call ``GET & HEAD /domains``."""
resource_url = '/domains'

View File

@ -0,0 +1,21 @@
---
features:
- |
Allow the creating of a domain with the additional, optional
parameter of `explicit_domain_id` instead of auto-creating a
domain_id from a uuid.
When keeping two Keystone servers in sync, but avoiding Database
replication, it was often necessary to hack the database to update
the Domain ID so that entries match. Domain ID is then used for
LDAP mapped IDs, and if they don't match, the user IDs are
different. It should be possible to add a domain with an explicit
ID, so that the two servers can match User IDs.
The reason that the variable name is not simple `domain_id` is
twofold: First to keep people from thinking that this is a required, or
at least suggested field. Second, to prevent copy errors when
creating a new domain, where the domain_id would be copied in from
the old one, and having spurious failures, or undesirecd domain_id
matching.
https://specs.openstack.org/openstack/keystone-specs/specs/keystone/stein/explicit-domains-ids.html