Merge "Implement federated auto-provisioning"
This commit is contained in:
commit
5dc7af8fb5
@ -11,7 +11,9 @@
|
||||
# under the License.
|
||||
|
||||
import functools
|
||||
import uuid
|
||||
|
||||
from oslo_log import log
|
||||
from pycadf import cadftaxonomy as taxonomy
|
||||
from six.moves.urllib import parse
|
||||
|
||||
@ -21,16 +23,17 @@ from keystone.common import dependency
|
||||
from keystone import exception
|
||||
from keystone.federation import constants as federation_constants
|
||||
from keystone.federation import utils
|
||||
from keystone.i18n import _
|
||||
from keystone.i18n import _, _LE, _LI
|
||||
from keystone.models import token_model
|
||||
from keystone import notifications
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
METHOD_NAME = 'mapped'
|
||||
|
||||
|
||||
@dependency.requires('federation_api', 'identity_api',
|
||||
'resource_api', 'token_provider_api')
|
||||
@dependency.requires('assignment_api', 'federation_api', 'identity_api',
|
||||
'resource_api', 'token_provider_api', 'role_api')
|
||||
class Mapped(base.AuthMethodHandler):
|
||||
|
||||
def _get_token_ref(self, auth_payload):
|
||||
@ -66,7 +69,9 @@ class Mapped(base.AuthMethodHandler):
|
||||
auth_context,
|
||||
self.resource_api,
|
||||
self.federation_api,
|
||||
self.identity_api)
|
||||
self.identity_api,
|
||||
self.assignment_api,
|
||||
self.role_api)
|
||||
|
||||
|
||||
def handle_scoped_token(request, auth_context, token_ref,
|
||||
@ -105,7 +110,75 @@ def handle_scoped_token(request, auth_context, token_ref,
|
||||
|
||||
|
||||
def handle_unscoped_token(request, auth_payload, auth_context,
|
||||
resource_api, federation_api, identity_api):
|
||||
resource_api, federation_api, identity_api,
|
||||
assignment_api, role_api):
|
||||
|
||||
def validate_shadow_mapping(shadow_projects, existing_roles, idp_domain_id,
|
||||
idp_id):
|
||||
# Validate that the roles in the shadow mapping actually exist. If
|
||||
# they don't we should bail early before creating anything.
|
||||
for shadow_project in shadow_projects:
|
||||
for shadow_role in shadow_project['roles']:
|
||||
# The role in the project mapping must exist in order for it to
|
||||
# be useful.
|
||||
if shadow_role['name'] not in existing_roles:
|
||||
LOG.error(
|
||||
_LE('Role %s was specified in the mapping but does '
|
||||
'not exist. All roles specified in a mapping must '
|
||||
'exist before assignment.'),
|
||||
shadow_role['name']
|
||||
)
|
||||
# NOTE(lbragstad): The RoleNotFound exception usually
|
||||
# expects a role_id as the parameter, but in this case we
|
||||
# only have a name so we'll pass that instead.
|
||||
raise exception.RoleNotFound(shadow_role['name'])
|
||||
role = existing_roles[shadow_role['name']]
|
||||
if (role['domain_id'] is not None and
|
||||
role['domain_id'] != idp_domain_id):
|
||||
LOG.error(
|
||||
_LE('Role %(role)s is a domain-specific role and '
|
||||
'cannot be assigned within %(domain)s.'),
|
||||
{'role': shadow_role['name'], 'domain': idp_domain_id}
|
||||
)
|
||||
raise exception.DomainSpecificRoleNotWithinIdPDomain(
|
||||
role_name=shadow_role['name'],
|
||||
identity_provider=idp_id
|
||||
)
|
||||
|
||||
def create_projects_from_mapping(shadow_projects, idp_domain_id,
|
||||
existing_roles, user, assignment_api,
|
||||
resource_api):
|
||||
for shadow_project in shadow_projects:
|
||||
try:
|
||||
# Check and see if the project already exists and if it
|
||||
# does not, try to create it.
|
||||
project = resource_api.get_project_by_name(
|
||||
shadow_project['name'], idp_domain_id
|
||||
)
|
||||
except exception.ProjectNotFound:
|
||||
LOG.info(
|
||||
_LI('Project %(project_name)s does not exist. It will be '
|
||||
'automatically provisioning for user %(user_id)s.'),
|
||||
{'project_name': shadow_project['name'],
|
||||
'user_id': user['id']}
|
||||
)
|
||||
project_ref = {
|
||||
'id': uuid.uuid4().hex,
|
||||
'name': shadow_project['name'],
|
||||
'domain_id': idp_domain_id
|
||||
}
|
||||
project = resource_api.create_project(
|
||||
project_ref['id'],
|
||||
project_ref
|
||||
)
|
||||
|
||||
shadow_roles = shadow_project['roles']
|
||||
for shadow_role in shadow_roles:
|
||||
assignment_api.create_grant(
|
||||
existing_roles[shadow_role['name']]['id'],
|
||||
user_id=user['id'],
|
||||
project_id=project['id']
|
||||
)
|
||||
|
||||
def is_ephemeral_user(mapped_properties):
|
||||
return mapped_properties['user']['type'] == utils.UserType.EPHEMERAL
|
||||
@ -155,6 +228,34 @@ def handle_unscoped_token(request, auth_payload, auth_context,
|
||||
user = identity_api.shadow_federated_user(identity_provider,
|
||||
protocol, unique_id,
|
||||
display_name)
|
||||
|
||||
if 'projects' in mapped_properties:
|
||||
idp_domain_id = federation_api.get_idp(
|
||||
identity_provider
|
||||
)['domain_id']
|
||||
existing_roles = {
|
||||
role['name']: role for role in role_api.list_roles()
|
||||
}
|
||||
# NOTE(lbragstad): If we are dealing with a shadow mapping,
|
||||
# then we need to make sure we validate all pieces of the
|
||||
# mapping and what it's saying to create. If there is something
|
||||
# wrong with how the mapping is, we should bail early before we
|
||||
# create anything.
|
||||
validate_shadow_mapping(
|
||||
mapped_properties['projects'],
|
||||
existing_roles,
|
||||
idp_domain_id,
|
||||
identity_provider
|
||||
)
|
||||
create_projects_from_mapping(
|
||||
mapped_properties['projects'],
|
||||
idp_domain_id,
|
||||
existing_roles,
|
||||
user,
|
||||
assignment_api,
|
||||
resource_api
|
||||
)
|
||||
|
||||
user_id = user['id']
|
||||
group_ids = mapped_properties['group_ids']
|
||||
build_ephemeral_user_context(auth_context, user,
|
||||
|
@ -348,6 +348,11 @@ class DomainSpecificRoleMismatch(Forbidden):
|
||||
"as the role %(role_id)s being assigned.")
|
||||
|
||||
|
||||
class DomainSpecificRoleNotWithinIdPDomain(Forbidden):
|
||||
message_format = _("role: %(role_name)s must be within the same domain as "
|
||||
"the identity provider: %(identity_provider)s.")
|
||||
|
||||
|
||||
class RoleAssignmentNotFound(NotFound):
|
||||
message_format = _("Could not find role assignment with role: "
|
||||
"%(role_id)s, user or group: %(actor_id)s, "
|
||||
|
@ -36,6 +36,20 @@ class UserType(object):
|
||||
EPHEMERAL = 'ephemeral'
|
||||
LOCAL = 'local'
|
||||
|
||||
ROLE_PROPERTIES = {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["name"],
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"additionalProperties": False
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
MAPPING_SCHEMA = {
|
||||
"type": "object",
|
||||
@ -72,6 +86,18 @@ MAPPING_SCHEMA = {
|
||||
},
|
||||
"additionalProperties": False
|
||||
},
|
||||
"projects": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["name", "roles"],
|
||||
"additionalProperties": False,
|
||||
"properties": {
|
||||
"name": {"type": "string"},
|
||||
"roles": ROLE_PROPERTIES
|
||||
}
|
||||
}
|
||||
},
|
||||
"group": {
|
||||
"type": "object",
|
||||
"oneOf": [
|
||||
|
@ -742,9 +742,18 @@ class MappingRuleEngineTests(unit.BaseTestCase):
|
||||
self.assertEqual(expected_username, values['user']['name'])
|
||||
|
||||
expected_projects = [
|
||||
{"name": "a"},
|
||||
{"name": "b"},
|
||||
{"name": "project for %s" % expected_username},
|
||||
{
|
||||
"name": "Production",
|
||||
"roles": [{"name": "observer"}]
|
||||
},
|
||||
{
|
||||
"name": "Staging",
|
||||
"roles": [{"name": "member"}]
|
||||
},
|
||||
{
|
||||
"name": "Project for %s" % expected_username,
|
||||
"roles": [{"name": "admin"}]
|
||||
}
|
||||
]
|
||||
self.assertEqual(expected_projects, values['projects'])
|
||||
|
||||
|
@ -1590,6 +1590,44 @@ MAPPING_UNICODE = {
|
||||
}
|
||||
|
||||
MAPPING_PROJECTS = {
|
||||
"rules": [
|
||||
{
|
||||
"local": [
|
||||
{
|
||||
"user": {
|
||||
"name": "{0}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"projects": [
|
||||
{"name": "Production",
|
||||
"roles": [{"name": "observer"}]},
|
||||
{"name": "Staging",
|
||||
"roles": [{"name": "member"}]},
|
||||
{"name": "Project for {0}",
|
||||
"roles": [{"name": "admin"}]},
|
||||
],
|
||||
}
|
||||
],
|
||||
"remote": [
|
||||
{
|
||||
"type": "UserName"
|
||||
},
|
||||
{
|
||||
"type": "Email",
|
||||
},
|
||||
{
|
||||
"type": "orgPersonType",
|
||||
"any_one_of": [
|
||||
"Employee"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
MAPPING_PROJECTS_WITHOUT_ROLES = {
|
||||
"rules": [
|
||||
{
|
||||
"local": [
|
||||
@ -1600,7 +1638,7 @@ MAPPING_PROJECTS = {
|
||||
"projects": [
|
||||
{"name": "a"},
|
||||
{"name": "b"},
|
||||
{"name": "project for {0}"},
|
||||
{"name": "Project for {0}"},
|
||||
],
|
||||
}
|
||||
],
|
||||
@ -1612,3 +1650,29 @@ MAPPING_PROJECTS = {
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
MAPPING_PROJECTS_WITHOUT_NAME = {
|
||||
"rules": [
|
||||
{
|
||||
"local": [
|
||||
{
|
||||
"user": {
|
||||
"name": "{0}"
|
||||
},
|
||||
"projects": [
|
||||
{"roles": [{"name": "observer"}]},
|
||||
{"name": "Staging",
|
||||
"roles": [{"name": "member"}]},
|
||||
{"name": "Project for {0}",
|
||||
"roles": [{"name": "admin"}]},
|
||||
]
|
||||
}
|
||||
],
|
||||
"remote": [
|
||||
{
|
||||
"type": "UserName"
|
||||
}
|
||||
]
|
||||
},
|
||||
]
|
||||
}
|
||||
|
@ -1700,6 +1700,58 @@ class MappingCRUDTests(test_v3.RestfulTestCase):
|
||||
self.put(url, expected_status=http_client.BAD_REQUEST,
|
||||
body={'mapping': bad_mapping})
|
||||
|
||||
def test_create_shadow_mapping_without_roles_fails(self):
|
||||
"""Validate that mappings with projects contain roles when created."""
|
||||
url = self.MAPPING_URL + uuid.uuid4().hex
|
||||
self.put(
|
||||
url,
|
||||
body={'mapping': mapping_fixtures.MAPPING_PROJECTS_WITHOUT_ROLES},
|
||||
expected_status=http_client.BAD_REQUEST
|
||||
)
|
||||
|
||||
def test_update_shadow_mapping_without_roles_fails(self):
|
||||
"""Validate that mappings with projects contain roles when updated."""
|
||||
url = self.MAPPING_URL + uuid.uuid4().hex
|
||||
resp = self.put(
|
||||
url,
|
||||
body={'mapping': mapping_fixtures.MAPPING_PROJECTS},
|
||||
expected_status=http_client.CREATED
|
||||
)
|
||||
self.assertValidMappingResponse(
|
||||
resp, mapping_fixtures.MAPPING_PROJECTS
|
||||
)
|
||||
self.patch(
|
||||
url,
|
||||
body={'mapping': mapping_fixtures.MAPPING_PROJECTS_WITHOUT_ROLES},
|
||||
expected_status=http_client.BAD_REQUEST
|
||||
)
|
||||
|
||||
def test_create_shadow_mapping_without_name_fails(self):
|
||||
"""Validate project mappings contain the project name when created."""
|
||||
url = self.MAPPING_URL + uuid.uuid4().hex
|
||||
self.put(
|
||||
url,
|
||||
body={'mapping': mapping_fixtures.MAPPING_PROJECTS_WITHOUT_NAME},
|
||||
expected_status=http_client.BAD_REQUEST
|
||||
)
|
||||
|
||||
def test_update_shadow_mapping_without_name_fails(self):
|
||||
"""Validate project mappings contain the project name when updated."""
|
||||
url = self.MAPPING_URL + uuid.uuid4().hex
|
||||
resp = self.put(
|
||||
url,
|
||||
body={'mapping': mapping_fixtures.MAPPING_PROJECTS},
|
||||
expected_status=http_client.CREATED
|
||||
)
|
||||
self.assertValidMappingResponse(
|
||||
resp, mapping_fixtures.MAPPING_PROJECTS
|
||||
)
|
||||
self.patch(
|
||||
url,
|
||||
body={'mapping': mapping_fixtures.MAPPING_PROJECTS_WITHOUT_NAME},
|
||||
expected_status=http_client.BAD_REQUEST
|
||||
)
|
||||
|
||||
|
||||
class FederatedTokenTests(test_v3.RestfulTestCase, FederatedSetupMixin):
|
||||
|
||||
@ -3030,6 +3082,244 @@ class FederatedUserTests(test_v3.RestfulTestCase, FederatedSetupMixin):
|
||||
return token_resp['user']['id'], unscoped_token
|
||||
|
||||
|
||||
class ShadowMappingTests(test_v3.RestfulTestCase, FederatedSetupMixin):
|
||||
"""Test class dedicated to auto-provisioning resources at login.
|
||||
|
||||
A shadow mapping is a mapping that contains extra properties about that
|
||||
specific federated user's situation based on attributes from the assertion.
|
||||
For example, a shadow mapping can tell us that a user should have specific
|
||||
role assignments on certain projects within a domain. When a federated user
|
||||
authenticates, the shadow mapping will create these entities before
|
||||
returning the authenticated response to the user. This test class is
|
||||
dedicated to testing specific aspects of shadow mapping when performing
|
||||
federated authentication.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super(ShadowMappingTests, self).setUp()
|
||||
# update the mapping we have already setup to have specific projects
|
||||
# and roles.
|
||||
self.federation_api.update_mapping(
|
||||
self.mapping['id'],
|
||||
mapping_fixtures.MAPPING_PROJECTS
|
||||
)
|
||||
|
||||
# The shadow mapping we're using in these tests contain a role named
|
||||
# `member` and `observer` for the sake of using something other than
|
||||
# `admin`. We'll need to create those before hand, otherwise the
|
||||
# mapping will fail during authentication because the roles defined in
|
||||
# the mapping do not exist yet. The shadow mapping mechanism currently
|
||||
# doesn't support creating roles on-the-fly, but this could change in
|
||||
# the future after we get some feedback from shadow mapping being used
|
||||
# in real deployments. We also want to make sure we are dealing with
|
||||
# global roles and not domain-scoped roles. We have specific tests
|
||||
# below that test that behavior and the setup is done in the test.
|
||||
member_role_ref = unit.new_role_ref(name='member')
|
||||
assert member_role_ref['domain_id'] is None
|
||||
self.member_role = self.role_api.create_role(
|
||||
member_role_ref['id'], member_role_ref
|
||||
)
|
||||
observer_role_ref = unit.new_role_ref(name='observer')
|
||||
assert observer_role_ref['domain_id'] is None
|
||||
self.observer_role = self.role_api.create_role(
|
||||
observer_role_ref['id'], observer_role_ref
|
||||
)
|
||||
|
||||
# This is a mapping of the project name to the role that is supposed to
|
||||
# be assigned to the user on that project from the shadow mapping.
|
||||
self.expected_results = {
|
||||
'Production': 'observer',
|
||||
'Staging': 'member',
|
||||
'Project for tbo': 'admin'
|
||||
}
|
||||
|
||||
def auth_plugin_config_override(self):
|
||||
methods = ['saml2']
|
||||
super(ShadowMappingTests, self).auth_plugin_config_override(methods)
|
||||
|
||||
def load_fixtures(self, fixtures):
|
||||
super(ShadowMappingTests, self).load_fixtures(fixtures)
|
||||
self.load_federation_sample_data()
|
||||
|
||||
def test_shadow_mapping_creates_projects(self):
|
||||
projects = self.resource_api.list_projects()
|
||||
for project in projects:
|
||||
self.assertNotIn(project['name'], self.expected_results)
|
||||
|
||||
response = self._issue_unscoped_token()
|
||||
self.assertValidMappedUser(response.json_body['token'])
|
||||
unscoped_token = response.headers.get('X-Subject-Token')
|
||||
response = self.get('/auth/projects', token=unscoped_token)
|
||||
projects = response.json_body['projects']
|
||||
for project in projects:
|
||||
project = self.resource_api.get_project_by_name(
|
||||
project['name'],
|
||||
self.idp['domain_id']
|
||||
)
|
||||
self.assertIn(project['name'], self.expected_results)
|
||||
|
||||
def test_shadow_mapping_create_projects_role_assignments(self):
|
||||
response = self._issue_unscoped_token()
|
||||
self.assertValidMappedUser(response.json_body['token'])
|
||||
unscoped_token = response.headers.get('X-Subject-Token')
|
||||
response = self.get('/auth/projects', token=unscoped_token)
|
||||
projects = response.json_body['projects']
|
||||
for project in projects:
|
||||
# Ask for a scope token to each project in the mapping. Each token
|
||||
# should contain a different role so let's check that is right,
|
||||
# too.
|
||||
scope = self._scope_request(
|
||||
unscoped_token, 'project', project['id']
|
||||
)
|
||||
response = self.v3_create_token(scope)
|
||||
project_name = response.json_body['token']['project']['name']
|
||||
roles = response.json_body['token']['roles']
|
||||
self.assertEqual(
|
||||
self.expected_results[project_name], roles[0]['name']
|
||||
)
|
||||
|
||||
def test_shadow_mapping_does_not_create_roles(self):
|
||||
# If a role required by the mapping does not exist, then we should fail
|
||||
# the mapping since shadow mapping currently does not support creating
|
||||
# mappings on-the-fly.
|
||||
self.role_api.delete_role(self.observer_role['id'])
|
||||
self.assertRaises(exception.RoleNotFound, self._issue_unscoped_token)
|
||||
|
||||
def test_shadow_mapping_creates_project_in_identity_provider_domain(self):
|
||||
response = self._issue_unscoped_token()
|
||||
self.assertValidMappedUser(response.json_body['token'])
|
||||
unscoped_token = response.headers.get('X-Subject-Token')
|
||||
response = self.get('/auth/projects', token=unscoped_token)
|
||||
projects = response.json_body['projects']
|
||||
for project in projects:
|
||||
self.assertEqual(project['domain_id'], self.idp['domain_id'])
|
||||
|
||||
def test_shadow_mapping_is_idempotent(self):
|
||||
"""Test that projects remain idempotent for every federated auth."""
|
||||
response = self._issue_unscoped_token()
|
||||
self.assertValidMappedUser(response.json_body['token'])
|
||||
unscoped_token = response.headers.get('X-Subject-Token')
|
||||
response = self.get('/auth/projects', token=unscoped_token)
|
||||
project_ids = [p['id'] for p in response.json_body['projects']]
|
||||
response = self._issue_unscoped_token()
|
||||
unscoped_token = response.headers.get('X-Subject-Token')
|
||||
response = self.get('/auth/projects', token=unscoped_token)
|
||||
projects = response.json_body['projects']
|
||||
for project in projects:
|
||||
self.assertIn(project['id'], project_ids)
|
||||
|
||||
def test_roles_outside_idp_domain_fail_mapping(self):
|
||||
# Create a new domain
|
||||
d = unit.new_domain_ref()
|
||||
new_domain = self.resource_api.create_domain(d['id'], d)
|
||||
|
||||
# Delete the member role and recreate it in a different domain
|
||||
self.role_api.delete_role(self.member_role['id'])
|
||||
member_role_ref = unit.new_role_ref(
|
||||
name='member',
|
||||
domain_id=new_domain['id']
|
||||
)
|
||||
self.role_api.create_role(member_role_ref['id'], member_role_ref)
|
||||
self.assertRaises(
|
||||
exception.DomainSpecificRoleNotWithinIdPDomain,
|
||||
self._issue_unscoped_token
|
||||
)
|
||||
|
||||
def test_roles_in_idp_domain_can_be_assigned_from_mapping(self):
|
||||
# Delete the member role and recreate it in the domain of the idp
|
||||
self.role_api.delete_role(self.member_role['id'])
|
||||
member_role_ref = unit.new_role_ref(
|
||||
name='member',
|
||||
domain_id=self.idp['domain_id']
|
||||
)
|
||||
self.role_api.create_role(member_role_ref['id'], member_role_ref)
|
||||
response = self._issue_unscoped_token()
|
||||
user_id = response.json_body['token']['user']['id']
|
||||
unscoped_token = response.headers.get('X-Subject-Token')
|
||||
response = self.get('/auth/projects', token=unscoped_token)
|
||||
projects = response.json_body['projects']
|
||||
staging_project = self.resource_api.get_project_by_name(
|
||||
'Staging', self.idp['domain_id']
|
||||
)
|
||||
for project in projects:
|
||||
# Even though the mapping successfully assigned the Staging project
|
||||
# a member role for our user, the /auth/projects response doesn't
|
||||
# include projects with only domain-specific role assignments.
|
||||
self.assertNotEqual(project['name'], 'Staging')
|
||||
domain_role_assignments = self.assignment_api.list_role_assignments(
|
||||
user_id=user_id,
|
||||
project_id=staging_project['id'],
|
||||
strip_domain_roles=False
|
||||
)
|
||||
self.assertEqual(
|
||||
staging_project['id'], domain_role_assignments[0]['project_id']
|
||||
)
|
||||
self.assertEqual(
|
||||
user_id, domain_role_assignments[0]['user_id']
|
||||
)
|
||||
|
||||
def test_mapping_with_groups_includes_projects_with_group_assignment(self):
|
||||
# create a group called Observers
|
||||
observer_group = unit.new_group_ref(
|
||||
domain_id=self.idp['domain_id'],
|
||||
name='Observers'
|
||||
)
|
||||
observer_group = self.identity_api.create_group(observer_group)
|
||||
# make sure the Observers group has a role on the finance project
|
||||
finance_project = unit.new_project_ref(
|
||||
domain_id=self.idp['domain_id'],
|
||||
name='Finance'
|
||||
)
|
||||
finance_project = self.resource_api.create_project(
|
||||
finance_project['id'], finance_project
|
||||
)
|
||||
self.assignment_api.create_grant(
|
||||
self.observer_role['id'],
|
||||
group_id=observer_group['id'],
|
||||
project_id=finance_project['id']
|
||||
)
|
||||
# update the mapping
|
||||
group_rule = {
|
||||
'group': {
|
||||
'name': 'Observers',
|
||||
'domain': {
|
||||
'id': self.idp['domain_id']
|
||||
}
|
||||
}
|
||||
}
|
||||
updated_mapping = copy.deepcopy(mapping_fixtures.MAPPING_PROJECTS)
|
||||
updated_mapping['rules'][0]['local'].append(group_rule)
|
||||
self.federation_api.update_mapping(self.mapping['id'], updated_mapping)
|
||||
response = self._issue_unscoped_token()
|
||||
# user_id = response.json_body['token']['user']['id']
|
||||
unscoped_token = response.headers.get('X-Subject-Token')
|
||||
response = self.get('/auth/projects', token=unscoped_token)
|
||||
projects = response.json_body['projects']
|
||||
self.expected_results = {
|
||||
# These assignments are all a result of a direct mapping from the
|
||||
# shadow user to the newly created project.
|
||||
'Production': 'observer',
|
||||
'Staging': 'member',
|
||||
'Project for tbo': 'admin',
|
||||
# This is a result of the mapping engine maintaining its old
|
||||
# behavior.
|
||||
'Finance': 'observer'
|
||||
}
|
||||
for project in projects:
|
||||
# Ask for a scope token to each project in the mapping. Each token
|
||||
# should contain a different role so let's check that is right,
|
||||
# too.
|
||||
scope = self._scope_request(
|
||||
unscoped_token, 'project', project['id']
|
||||
)
|
||||
response = self.v3_create_token(scope)
|
||||
project_name = response.json_body['token']['project']['name']
|
||||
roles = response.json_body['token']['roles']
|
||||
self.assertEqual(
|
||||
self.expected_results[project_name], roles[0]['name']
|
||||
)
|
||||
|
||||
|
||||
class JsonHomeTests(test_v3.RestfulTestCase, test_v3.JsonHomeTestMixin):
|
||||
JSON_HOME_DATA = {
|
||||
'http://docs.openstack.org/api/openstack-identity/3/ext/OS-FEDERATION/'
|
||||
|
@ -0,0 +1,9 @@
|
||||
---
|
||||
features:
|
||||
- The federated mapping engine now supports the ability
|
||||
to automatically provision federated users into projects
|
||||
specified in the mapping rules at federated login time. If the
|
||||
project within the mapping does not exist, it will be
|
||||
automatically created in the domain of the Identity Provider.
|
||||
This behavior can be done using a specific syntax within
|
||||
the ``local`` rules section of a mapping.
|
Loading…
Reference in New Issue
Block a user