OpenStack Identity (Keystone)
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

5073 lines
202 KiB

# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import copy
import os
import random
import re
import subprocess
from testtools import matchers
import uuid
import fixtures
import flask
from lxml import etree
import mock
from oslo_serialization import jsonutils
from oslo_utils import importutils
import saml2
from saml2 import saml
from saml2 import sigver
from six.moves import http_client
from six.moves import range, urllib, zip
xmldsig = importutils.try_import("saml2.xmldsig")
if not xmldsig:
xmldsig = importutils.try_import("xmldsig")
from keystone.api._shared import authentication
from keystone.api import auth as auth_api
from keystone.common import driver_hints
from keystone.common import provider_api
from keystone.common import render_token
import keystone.conf
from keystone import exception
from keystone.federation import idp as keystone_idp
from keystone.models import token_model
from keystone import notifications
from keystone.tests import unit
from keystone.tests.unit import core
from keystone.tests.unit import federation_fixtures
from keystone.tests.unit import ksfixtures
from keystone.tests.unit import mapping_fixtures
from keystone.tests.unit import test_v3
CONF = keystone.conf.CONF
PROVIDERS = provider_api.ProviderAPIs
ROOTDIR = os.path.dirname(os.path.abspath(__file__))
XMLDIR = os.path.join(ROOTDIR, 'saml2/')
def dummy_validator(*args, **kwargs):
pass
class FederatedSetupMixin(object):
ACTION = 'authenticate'
IDP = 'ORG_IDP'
PROTOCOL = 'saml2'
AUTH_METHOD = 'saml2'
USER = 'user@ORGANIZATION'
ASSERTION_PREFIX = 'PREFIX_'
IDP_WITH_REMOTE = 'ORG_IDP_REMOTE'
REMOTE_IDS = ['entityID_IDP1', 'entityID_IDP2']
REMOTE_ID_ATTR = uuid.uuid4().hex
UNSCOPED_V3_SAML2_REQ = {
"identity": {
"methods": [AUTH_METHOD],
AUTH_METHOD: {
"identity_provider": IDP,
"protocol": PROTOCOL
}
}
}
def _check_domains_are_valid(self, token):
domain = PROVIDERS.resource_api.get_domain(self.idp['domain_id'])
self.assertEqual(domain['id'], token['user']['domain']['id'])
self.assertEqual(domain['name'], token['user']['domain']['name'])
def _project(self, project):
return (project['id'], project['name'])
def _roles(self, roles):
return set([(r['id'], r['name']) for r in roles])
def _check_projects_and_roles(self, token, roles, projects):
"""Check whether the projects and the roles match."""
token_roles = token.get('roles')
if token_roles is None:
raise AssertionError('Roles not found in the token')
token_roles = self._roles(token_roles)
roles_ref = self._roles(roles)
self.assertEqual(token_roles, roles_ref)
token_projects = token.get('project')
if token_projects is None:
raise AssertionError('Projects not found in the token')
token_projects = self._project(token_projects)
projects_ref = self._project(projects)
self.assertEqual(token_projects, projects_ref)
def _check_scoped_token_attributes(self, token):
for obj in ('user', 'catalog', 'expires_at', 'issued_at',
'methods', 'roles'):
self.assertIn(obj, token)
os_federation = token['user']['OS-FEDERATION']
self.assertIn('groups', os_federation)
self.assertIn('identity_provider', os_federation)
self.assertIn('protocol', os_federation)
self.assertThat(os_federation, matchers.HasLength(3))
self.assertEqual(self.IDP, os_federation['identity_provider']['id'])
self.assertEqual(self.PROTOCOL, os_federation['protocol']['id'])
def _check_project_scoped_token_attributes(self, token, project_id):
self.assertEqual(project_id, token['project']['id'])
self._check_scoped_token_attributes(token)
def _check_domain_scoped_token_attributes(self, token, domain_id):
self.assertEqual(domain_id, token['domain']['id'])
self._check_scoped_token_attributes(token)
def assertValidMappedUser(self, token):
"""Check if user object meets all the criteria."""
user = token['user']
self.assertIn('id', user)
self.assertIn('name', user)
self.assertIn('domain', user)
self.assertIn('groups', user['OS-FEDERATION'])
self.assertIn('identity_provider', user['OS-FEDERATION'])
self.assertIn('protocol', user['OS-FEDERATION'])
# Make sure user_name is url safe
self.assertEqual(urllib.parse.quote(user['name']), user['name'])
def _issue_unscoped_token(self,
idp=None,
assertion='EMPLOYEE_ASSERTION',
environment=None):
environment = environment or {}
environment.update(getattr(mapping_fixtures, assertion))
with self.make_request(environ=environment):
if idp is None:
idp = self.IDP
r = authentication.federated_authenticate_for_token(
protocol_id=self.PROTOCOL, identity_provider=idp)
return r
def idp_ref(self, id=None):
idp = {
'id': id or uuid.uuid4().hex,
'enabled': True,
'description': uuid.uuid4().hex
}
return idp
def proto_ref(self, mapping_id=None):
proto = {
'id': uuid.uuid4().hex,
'mapping_id': mapping_id or uuid.uuid4().hex
}
return proto
def mapping_ref(self, rules=None):
return {
'id': uuid.uuid4().hex,
'rules': rules or self.rules['rules']
}
def _scope_request(self, unscoped_token_id, scope, scope_id):
return {
'auth': {
'identity': {
'methods': [
'token'
],
'token': {
'id': unscoped_token_id
}
},
'scope': {
scope: {
'id': scope_id
}
}
}
}
def _inject_assertion(self, variant):
assertion = getattr(mapping_fixtures, variant)
flask.request.environ.update(assertion)
def load_federation_sample_data(self):
"""Inject additional data."""
# Create and add domains
self.domainA = unit.new_domain_ref()
PROVIDERS.resource_api.create_domain(
self.domainA['id'], self.domainA
)
self.domainB = unit.new_domain_ref()
PROVIDERS.resource_api.create_domain(
self.domainB['id'], self.domainB
)
self.domainC = unit.new_domain_ref()
PROVIDERS.resource_api.create_domain(
self.domainC['id'], self.domainC
)
self.domainD = unit.new_domain_ref()
PROVIDERS.resource_api.create_domain(
self.domainD['id'], self.domainD
)
# Create and add projects
self.proj_employees = unit.new_project_ref(
domain_id=self.domainA['id'])
PROVIDERS.resource_api.create_project(
self.proj_employees['id'], self.proj_employees
)
self.proj_customers = unit.new_project_ref(
domain_id=self.domainA['id'])
PROVIDERS.resource_api.create_project(
self.proj_customers['id'], self.proj_customers
)
self.project_all = unit.new_project_ref(
domain_id=self.domainA['id'])
PROVIDERS.resource_api.create_project(
self.project_all['id'], self.project_all
)
self.project_inherited = unit.new_project_ref(
domain_id=self.domainD['id'])
PROVIDERS.resource_api.create_project(
self.project_inherited['id'], self.project_inherited
)
# Create and add groups
self.group_employees = unit.new_group_ref(domain_id=self.domainA['id'])
self.group_employees = (
PROVIDERS.identity_api.create_group(self.group_employees))
self.group_customers = unit.new_group_ref(domain_id=self.domainA['id'])
self.group_customers = (
PROVIDERS.identity_api.create_group(self.group_customers))
self.group_admins = unit.new_group_ref(domain_id=self.domainA['id'])
self.group_admins = PROVIDERS.identity_api.create_group(
self.group_admins
)
# Create and add roles
self.role_employee = unit.new_role_ref()
PROVIDERS.role_api.create_role(
self.role_employee['id'], self.role_employee
)
self.role_customer = unit.new_role_ref()
PROVIDERS.role_api.create_role(
self.role_customer['id'], self.role_customer
)
self.role_admin = unit.new_role_ref()
PROVIDERS.role_api.create_role(self.role_admin['id'], self.role_admin)
# Employees can access
# * proj_employees
# * project_all
PROVIDERS.assignment_api.create_grant(
self.role_employee['id'], group_id=self.group_employees['id'],
project_id=self.proj_employees['id']
)
PROVIDERS.assignment_api.create_grant(
self.role_employee['id'], group_id=self.group_employees['id'],
project_id=self.project_all['id']
)
# Customers can access
# * proj_customers
PROVIDERS.assignment_api.create_grant(
self.role_customer['id'], group_id=self.group_customers['id'],
project_id=self.proj_customers['id']
)
# Admins can access:
# * proj_customers
# * proj_employees
# * project_all
PROVIDERS.assignment_api.create_grant(
self.role_admin['id'], group_id=self.group_admins['id'],
project_id=self.proj_customers['id']
)
PROVIDERS.assignment_api.create_grant(
self.role_admin['id'], group_id=self.group_admins['id'],
project_id=self.proj_employees['id']
)
PROVIDERS.assignment_api.create_grant(
self.role_admin['id'], group_id=self.group_admins['id'],
project_id=self.project_all['id']
)
# Customers can access:
# * domain A
PROVIDERS.assignment_api.create_grant(
self.role_customer['id'], group_id=self.group_customers['id'],
domain_id=self.domainA['id']
)
# Customers can access projects via inheritance:
# * domain D
PROVIDERS.assignment_api.create_grant(
self.role_customer['id'], group_id=self.group_customers['id'],
domain_id=self.domainD['id'], inherited_to_projects=True
)
# Employees can access:
# * domain A
# * domain B
PROVIDERS.assignment_api.create_grant(
self.role_employee['id'], group_id=self.group_employees['id'],
domain_id=self.domainA['id']
)
PROVIDERS.assignment_api.create_grant(
self.role_employee['id'], group_id=self.group_employees['id'],
domain_id=self.domainB['id']
)
# Admins can access:
# * domain A
# * domain B
# * domain C
PROVIDERS.assignment_api.create_grant(
self.role_admin['id'], group_id=self.group_admins['id'],
domain_id=self.domainA['id']
)
PROVIDERS.assignment_api.create_grant(
self.role_admin['id'], group_id=self.group_admins['id'],
domain_id=self.domainB['id']
)
PROVIDERS.assignment_api.create_grant(
self.role_admin['id'], group_id=self.group_admins['id'],
domain_id=self.domainC['id']
)
self.rules = {
'rules': [
{
'local': [
{
'group': {
'id': self.group_employees['id']
}
},
{
'user': {
'name': '{0}',
'id': '{1}'
}
}
],
'remote': [
{
'type': 'UserName'
},
{
'type': 'Email',
},
{
'type': 'orgPersonType',
'any_one_of': [
'Employee'
]
}
]
},
{
'local': [
{
'group': {
'id': self.group_employees['id']
}
},
{
'user': {
'name': '{0}',
'id': '{1}'
}
}
],
'remote': [
{
'type': self.ASSERTION_PREFIX + 'UserName'
},
{
'type': self.ASSERTION_PREFIX + 'Email',
},
{
'type': self.ASSERTION_PREFIX + 'orgPersonType',
'any_one_of': [
'SuperEmployee'
]
}
]
},
{
'local': [
{
'group': {
'id': self.group_customers['id']
}
},
{
'user': {
'name': '{0}',
'id': '{1}'
}
}
],
'remote': [
{
'type': 'UserName'
},
{
'type': 'Email'
},
{
'type': 'orgPersonType',
'any_one_of': [
'Customer'
]
}
]
},
{
'local': [
{
'group': {
'id': self.group_admins['id']
}
},
{
'group': {
'id': self.group_employees['id']
}
},
{
'group': {
'id': self.group_customers['id']
}
},
{
'user': {
'name': '{0}',
'id': '{1}'
}
}
],
'remote': [
{
'type': 'UserName'
},
{
'type': 'Email'
},
{
'type': 'orgPersonType',
'any_one_of': [
'Admin',
'Chief'
]
}
]
},
{
'local': [
{
'group': {
'id': uuid.uuid4().hex
}
},
{
'group': {
'id': self.group_customers['id']
}
},
{
'user': {
'name': '{0}',
'id': '{1}'
}
}
],
'remote': [
{
'type': 'UserName',
},
{
'type': 'Email',
},
{
'type': 'FirstName',
'any_one_of': [
'Jill'
]
},
{
'type': 'LastName',
'any_one_of': [
'Smith'
]
}
]
},
{
'local': [
{
'group': {
'id': 'this_group_no_longer_exists'
}
},
{
'user': {
'name': '{0}',
'id': '{1}'
}
}
],
'remote': [
{
'type': 'UserName',
},
{
'type': 'Email',
},
{
'type': 'Email',
'any_one_of': [
'testacct@example.com'
]
},
{
'type': 'orgPersonType',
'any_one_of': [
'Tester'
]
}
]
},
# rules with local group names
{
"local": [
{
'user': {
'name': '{0}',
'id': '{1}'
}
},
{
"group": {
"name": self.group_customers['name'],
"domain": {
"name": self.domainA['name']
}
}
}
],
"remote": [
{
'type': 'UserName',
},
{
'type': 'Email',
},
{
"type": "orgPersonType",
"any_one_of": [
"CEO",
"CTO"
],
}
]
},
{
"local": [
{
'user': {
'name': '{0}',
'id': '{1}'
}
},
{
"group": {
"name": self.group_admins['name'],
"domain": {
"id": self.domainA['id']
}
}
}
],
"remote": [
{
"type": "UserName",
},
{
"type": "Email",
},
{
"type": "orgPersonType",
"any_one_of": [
"Managers"
]
}
]
},
{
"local": [
{
"user": {
"name": "{0}",
"id": "{1}"
}
},
{
"group": {
"name": "NON_EXISTING",
"domain": {
"id": self.domainA['id']
}
}
}
],
"remote": [
{
"type": "UserName",
},
{
"type": "Email",
},
{
"type": "UserName",
"any_one_of": [
"IamTester"
]
}
]
},
{
"local": [
{
"user": {
"type": "local",
"name": self.user['name'],
"domain": {
"id": self.user['domain_id']
}
}
},
{
"group": {
"id": self.group_customers['id']
}
}
],
"remote": [
{
"type": "UserType",
"any_one_of": [
"random"
]
}
]
},
{
"local": [
{
"user": {
"type": "local",
"name": self.user['name'],
"domain": {
"id": uuid.uuid4().hex
}
}
}
],
"remote": [
{
"type": "Position",
"any_one_of": [
"DirectorGeneral"
]
}
]
},
# rules for users with no groups
{
"local": [
{
'user': {
'name': '{0}',
'id': '{1}'
}
}
],
"remote": [
{
'type': 'UserName',
},
{
'type': 'Email',
},
{
'type': 'orgPersonType',
'any_one_of': [
'NoGroupsOrg'
]
}
]
}
]
}
# Add unused IdP first so it is indexed first (#1838592)
self.dummy_idp = self.idp_ref()
PROVIDERS.federation_api.create_idp(
self.dummy_idp['id'], self.dummy_idp
)
# Add IDP
self.idp = self.idp_ref(id=self.IDP)
PROVIDERS.federation_api.create_idp(
self.idp['id'], self.idp
)
# Add IDP with remote
self.idp_with_remote = self.idp_ref(id=self.IDP_WITH_REMOTE)
self.idp_with_remote['remote_ids'] = self.REMOTE_IDS
PROVIDERS.federation_api.create_idp(
self.idp_with_remote['id'], self.idp_with_remote
)
# Add a mapping
self.mapping = self.mapping_ref()
PROVIDERS.federation_api.create_mapping(
self.mapping['id'], self.mapping
)
# Add protocols
self.proto_saml = self.proto_ref(mapping_id=self.mapping['id'])
self.proto_saml['id'] = self.PROTOCOL
PROVIDERS.federation_api.create_protocol(
self.idp['id'], self.proto_saml['id'], self.proto_saml
)
# Add protocols IDP with remote
PROVIDERS.federation_api.create_protocol(
self.idp_with_remote['id'], self.proto_saml['id'], self.proto_saml
)
# Add unused protocol to go with unused IdP (#1838592)
self.proto_dummy = self.proto_ref(mapping_id=self.mapping['id'])
PROVIDERS.federation_api.create_protocol(
self.dummy_idp['id'], self.proto_dummy['id'], self.proto_dummy
)
with self.make_request():
self.tokens = {}
VARIANTS = ('EMPLOYEE_ASSERTION', 'CUSTOMER_ASSERTION',
'ADMIN_ASSERTION')
for variant in VARIANTS:
self._inject_assertion(variant)
r = authentication.authenticate_for_token(
self.UNSCOPED_V3_SAML2_REQ)
self.tokens[variant] = r.id
self.TOKEN_SCOPE_PROJECT_FROM_NONEXISTENT_TOKEN = (
self._scope_request(
uuid.uuid4().hex, 'project', self.proj_customers['id']))
self.TOKEN_SCOPE_PROJECT_EMPLOYEE_FROM_EMPLOYEE = (
self._scope_request(
self.tokens['EMPLOYEE_ASSERTION'], 'project',
self.proj_employees['id']))
self.TOKEN_SCOPE_PROJECT_EMPLOYEE_FROM_ADMIN = self._scope_request(
self.tokens['ADMIN_ASSERTION'], 'project',
self.proj_employees['id'])
self.TOKEN_SCOPE_PROJECT_CUSTOMER_FROM_ADMIN = self._scope_request(
self.tokens['ADMIN_ASSERTION'], 'project',
self.proj_customers['id'])
self.TOKEN_SCOPE_PROJECT_EMPLOYEE_FROM_CUSTOMER = (
self._scope_request(
self.tokens['CUSTOMER_ASSERTION'], 'project',
self.proj_employees['id']))
self.TOKEN_SCOPE_PROJECT_INHERITED_FROM_CUSTOMER = (
self._scope_request(
self.tokens['CUSTOMER_ASSERTION'], 'project',
self.project_inherited['id']))
self.TOKEN_SCOPE_DOMAIN_A_FROM_CUSTOMER = self._scope_request(
self.tokens['CUSTOMER_ASSERTION'], 'domain',
self.domainA['id'])
self.TOKEN_SCOPE_DOMAIN_B_FROM_CUSTOMER = self._scope_request(
self.tokens['CUSTOMER_ASSERTION'], 'domain',
self.domainB['id'])
self.TOKEN_SCOPE_DOMAIN_D_FROM_CUSTOMER = self._scope_request(
self.tokens['CUSTOMER_ASSERTION'], 'domain',
self.domainD['id'])
self.TOKEN_SCOPE_DOMAIN_A_FROM_ADMIN = self._scope_request(
self.tokens['ADMIN_ASSERTION'], 'domain', self.domainA['id'])
self.TOKEN_SCOPE_DOMAIN_B_FROM_ADMIN = self._scope_request(
self.tokens['ADMIN_ASSERTION'], 'domain', self.domainB['id'])
self.TOKEN_SCOPE_DOMAIN_C_FROM_ADMIN = self._scope_request(
self.tokens['ADMIN_ASSERTION'], 'domain',
self.domainC['id'])
class FederatedIdentityProviderTests(test_v3.RestfulTestCase):
"""A test class for Identity Providers."""
idp_keys = ['description', 'enabled']
default_body = {'description': None, 'enabled': True}
def base_url(self, suffix=None):
if suffix is not None:
return '/OS-FEDERATION/identity_providers/' + str(suffix)
return '/OS-FEDERATION/identity_providers'
def _fetch_attribute_from_response(self, resp, parameter,
assert_is_not_none=True):
"""Fetch single attribute from TestResponse object."""
result = resp.result.get(parameter)
if assert_is_not_none:
self.assertIsNotNone(result)
return result
def _create_and_decapsulate_response(self, body=None):
"""Create IdP and fetch it's random id along with entity."""
default_resp = self._create_default_idp(body=body)
idp = self._fetch_attribute_from_response(default_resp,
'identity_provider')
self.assertIsNotNone(idp)
idp_id = idp.get('id')
return (idp_id, idp)
def _get_idp(self, idp_id):
"""Fetch IdP entity based on its id."""
url = self.base_url(suffix=idp_id)
resp = self.get(url)
return resp
def _create_default_idp(self, body=None,
expected_status=http_client.CREATED):
"""Create default IdP."""
url = self.base_url(suffix=uuid.uuid4().hex)
if body is None:
body = self._http_idp_input()
resp = self.put(url, body={'identity_provider': body},
expected_status=expected_status)
return resp
def _http_idp_input(self):
"""Create default input dictionary for IdP data."""
body = self.default_body.copy()
body['description'] = uuid.uuid4().hex
return body
def _assign_protocol_to_idp(self, idp_id=None, proto=None, url=None,
mapping_id=None, validate=True, **kwargs):
if url is None:
url = self.base_url(suffix='%(idp_id)s/protocols/%(protocol_id)s')
if idp_id is None:
idp_id, _ = self._create_and_decapsulate_response()
if proto is None:
proto = uuid.uuid4().hex
if mapping_id is None:
mapping_id = uuid.uuid4().hex
self._create_mapping(mapping_id)
body = {'mapping_id': mapping_id}
url = url % {'idp_id': idp_id, 'protocol_id': proto}
resp = self.put(url, body={'protocol': body}, **kwargs)
if validate:
self.assertValidResponse(resp, 'protocol', dummy_validator,
keys_to_check=['id', 'mapping_id'],
ref={'id': proto,
'mapping_id': mapping_id})
return (resp, idp_id, proto)
def _get_protocol(self, idp_id, protocol_id):
url = '%s/protocols/%s' % (idp_id, protocol_id)
url = self.base_url(suffix=url)
r = self.get(url)
return r
def _create_mapping(self, mapping_id):
mapping = mapping_fixtures.MAPPING_EPHEMERAL_USER
mapping['id'] = mapping_id
url = '/OS-FEDERATION/mappings/%s' % mapping_id
self.put(url,
body={'mapping': mapping},
expected_status=http_client.CREATED)
def assertIdpDomainCreated(self, idp_id, domain_id):
domain = PROVIDERS.resource_api.get_domain(domain_id)
self.assertEqual(domain_id, domain['name'])
self.assertIn(idp_id, domain['description'])
def test_create_idp_without_domain_id(self):
"""Create the IdentityProvider entity associated to remote_ids."""
keys_to_check = list(self.idp_keys)
body = self.default_body.copy()
body['description'] = uuid.uuid4().hex
resp = self._create_default_idp(body=body)
self.assertValidResponse(resp, 'identity_provider', dummy_validator,
keys_to_check=keys_to_check,
ref=body)
attr = self._fetch_attribute_from_response(resp, 'identity_provider')
self.assertIdpDomainCreated(attr['id'], attr['domain_id'])
def test_create_idp_with_domain_id(self):
keys_to_check = list(self.idp_keys)
keys_to_check.append('domain_id')
body = self.default_body.copy()
body['description'] = uuid.uuid4().hex
domain = unit.new_domain_ref()
PROVIDERS.resource_api.create_domain(domain['id'], domain)
body['domain_id'] = domain['id']
resp = self._create_default_idp(body=body)
self.assertValidResponse(resp, 'identity_provider', dummy_validator,
keys_to_check=keys_to_check,
ref=body)
def test_create_idp_domain_id_none(self):
keys_to_check = list(self.idp_keys)
body = self.default_body.copy()
body['description'] = uuid.uuid4().hex
body['domain_id'] = None
resp = self._create_default_idp(body=body)
self.assertValidResponse(resp, 'identity_provider', dummy_validator,
keys_to_check=keys_to_check,
ref=body)
attr = self._fetch_attribute_from_response(resp, 'identity_provider')
self.assertIdpDomainCreated(attr['id'], attr['domain_id'])
def test_conflicting_idp_cleans_up_auto_generated_domain(self):
# NOTE(lbragstad): Create an identity provider, save its ID, and count
# the number of domains.
resp = self._create_default_idp()
idp_id = resp.json_body['identity_provider']['id']
domains = PROVIDERS.resource_api.list_domains()
number_of_domains = len(domains)
# Create an identity provider with the same ID to intentionally cause a
# conflict, this is going to result in a domain getting created for the
# new identity provider. The domain for the new identity provider is
# going to be created before the conflict is raised from the database
# layer. This makes sure the domain is cleaned up after a Conflict is
# detected.
resp = self.put(
self.base_url(suffix=idp_id),
body={'identity_provider': self.default_body.copy()},
expected_status=http_client.CONFLICT
)
domains = PROVIDERS.resource_api.list_domains()
self.assertEqual(number_of_domains, len(domains))
def test_conflicting_idp_does_not_delete_existing_domain(self):
# Create a new domain
domain = unit.new_domain_ref()
PROVIDERS.resource_api.create_domain(domain['id'], domain)
# Create an identity provider and specify the domain
body = self.default_body.copy()
body['description'] = uuid.uuid4().hex
body['domain_id'] = domain['id']
resp = self._create_default_idp(body=body)
idp = resp.json_body['identity_provider']
idp_id = idp['id']
self.assertEqual(idp['domain_id'], domain['id'])
# Create an identity provider with the same domain and ID to ensure a
# Conflict is raised and then to verify the existing domain not deleted
# below
body = self.default_body.copy()
body['domain_id'] = domain['id']
resp = self.put(
self.base_url(suffix=idp_id),
body={'identity_provider': body},
expected_status=http_client.CONFLICT
)
# Make sure the domain specified in the second request was not deleted,
# since it wasn't auto-generated
self.assertIsNotNone(PROVIDERS.resource_api.get_domain(domain['id']))
def test_create_multi_idp_to_one_domain(self):
# create domain and add domain_id to keys to check
domain = unit.new_domain_ref()
PROVIDERS.resource_api.create_domain(domain['id'], domain)
keys_to_check = list(self.idp_keys)
keys_to_check.append('domain_id')
# create idp with the domain_id
body = self.default_body.copy()
body['description'] = uuid.uuid4().hex
body['domain_id'] = domain['id']
idp1 = self._create_default_idp(body=body)
self.assertValidResponse(idp1, 'identity_provider', dummy_validator,
keys_to_check=keys_to_check,
ref=body)
# create a 2nd idp with the same domain_id
url = self.base_url(suffix=uuid.uuid4().hex)
body = self.default_body.copy()
body['description'] = uuid.uuid4().hex
body['domain_id'] = domain['id']
idp2 = self.put(url, body={'identity_provider': body},
expected_status=http_client.CREATED)
self.assertValidResponse(idp2, 'identity_provider', dummy_validator,
keys_to_check=keys_to_check,
ref=body)
self.assertEqual(idp1.result['identity_provider']['domain_id'],
idp2.result['identity_provider']['domain_id'])
def test_cannot_update_idp_domain(self):
# create new idp
body = self.default_body.copy()
default_resp = self._create_default_idp(body=body)
default_idp = self._fetch_attribute_from_response(default_resp,
'identity_provider')
idp_id = default_idp.get('id')
self.assertIsNotNone(idp_id)
# create domain and try to update the idp's domain
domain = unit.new_domain_ref()
PROVIDERS.resource_api.create_domain(domain['id'], domain)
body['domain_id'] = domain['id']
body = {'identity_provider': body}
url = self.base_url(suffix=idp_id)
self.patch(url, body=body, expected_status=http_client.BAD_REQUEST)
def test_create_idp_with_nonexistent_domain_id_fails(self):
body = self.default_body.copy()
body['description'] = uuid.uuid4().hex
body['domain_id'] = uuid.uuid4().hex
self._create_default_idp(body=body,
expected_status=http_client.NOT_FOUND)
def test_create_idp_remote(self):
"""Create the IdentityProvider entity associated to remote_ids."""
keys_to_check = list(self.idp_keys)
keys_to_check.append('remote_ids')
body = self.default_body.copy()
body['description'] = uuid.uuid4().hex
body['remote_ids'] = [uuid.uuid4().hex,
uuid.uuid4().hex,
uuid.uuid4().hex]
resp = self._create_default_idp(body=body)
self.assertValidResponse(resp, 'identity_provider', dummy_validator,
keys_to_check=keys_to_check,
ref=body)
attr = self._fetch_attribute_from_response(resp, 'identity_provider')
self.assertIdpDomainCreated(attr['id'], attr['domain_id'])
def test_create_idp_remote_repeated(self):
"""Create two IdentityProvider entities with some remote_ids.
A remote_id is the same for both so the second IdP is not
created because of the uniqueness of the remote_ids
Expect HTTP 409 Conflict code for the latter call.
"""
body = self.default_body.copy()
repeated_remote_id = uuid.uuid4().hex
body['remote_ids'] = [uuid.uuid4().hex,
uuid.uuid4().hex,
uuid.uuid4().hex,
repeated_remote_id]
self._create_default_idp(body=body)
url = self.base_url(suffix=uuid.uuid4().hex)
body['remote_ids'] = [uuid.uuid4().hex,
repeated_remote_id]
resp = self.put(url, body={'identity_provider': body},
expected_status=http_client.CONFLICT)
resp_data = jsonutils.loads(resp.body)
self.assertIn('Duplicate remote ID',
resp_data.get('error', {}).get('message'))
def test_create_idp_remote_empty(self):
"""Create an IdP with empty remote_ids."""
keys_to_check = list(self.idp_keys)
keys_to_check.append('remote_ids')
body = self.default_body.copy()
body['description'] = uuid.uuid4().hex
body['remote_ids'] = []
resp = self._create_default_idp(body=body)
self.assertValidResponse(resp, 'identity_provider', dummy_validator,
keys_to_check=keys_to_check,
ref=body)
def test_create_idp_remote_none(self):
"""Create an IdP with a None remote_ids."""
keys_to_check = list(self.idp_keys)
keys_to_check.append('remote_ids')
body = self.default_body.copy()
body['description'] = uuid.uuid4().hex
body['remote_ids'] = None
resp = self._create_default_idp(body=body)
expected = body.copy()
expected['remote_ids'] = []
self.assertValidResponse(resp, 'identity_provider', dummy_validator,
keys_to_check=keys_to_check,
ref=expected)
def test_update_idp_remote_ids(self):
"""Update IdP's remote_ids parameter."""
body = self.default_body.copy()
body['remote_ids'] = [uuid.uuid4().hex]
default_resp = self._create_default_idp(body=body)
default_idp = self._fetch_attribute_from_response(default_resp,
'identity_provider')
idp_id = default_idp.get('id')
url = self.base_url(suffix=idp_id)
self.assertIsNotNone(idp_id)
body['remote_ids'] = [uuid.uuid4().hex, uuid.uuid4().hex]
body = {'identity_provider': body}
resp = self.patch(url, body=body)
updated_idp = self._fetch_attribute_from_response(resp,
'identity_provider')
body = body['identity_provider']
self.assertEqual(sorted(body['remote_ids']),
sorted(updated_idp.get('remote_ids')))
resp = self.get(url)
returned_idp = self._fetch_attribute_from_response(resp,
'identity_provider')
self.assertEqual(sorted(body['remote_ids']),
sorted(returned_idp.get('remote_ids')))
def test_update_idp_clean_remote_ids(self):
"""Update IdP's remote_ids parameter with an empty list."""
body = self.default_body.copy()
body['remote_ids'] = [uuid.uuid4().hex]
default_resp = self._create_default_idp(body=body)
default_idp = self._fetch_attribute_from_response(default_resp,
'identity_provider')
idp_id = default_idp.get('id')
url = self.base_url(suffix=idp_id)
self.assertIsNotNone(idp_id)
body['remote_ids'] = []
body = {'identity_provider': body}
resp = self.patch(url, body=body)
updated_idp = self._fetch_attribute_from_response(resp,
'identity_provider')
body = body['identity_provider']
self.assertEqual(sorted(body['remote_ids']),
sorted(updated_idp.get('remote_ids')))
resp = self.get(url)
returned_idp = self._fetch_attribute_from_response(resp,
'identity_provider')
self.assertEqual(sorted(body['remote_ids']),
sorted(returned_idp.get('remote_ids')))
def test_update_idp_remote_repeated(self):
"""Update an IdentityProvider entity reusing a remote_id.
A remote_id is the same for both so the second IdP is not
updated because of the uniqueness of the remote_ids.
Expect HTTP 409 Conflict code for the latter call.
"""
# Create first identity provider
body = self.default_body.copy()
repeated_remote_id = uuid.uuid4().hex
body['remote_ids'] = [uuid.uuid4().hex,
repeated_remote_id]
self._create_default_idp(body=body)
# Create second identity provider (without remote_ids)
body = self.default_body.copy()
default_resp = self._create_default_idp(body=body)
default_idp = self._fetch_attribute_from_response(default_resp,
'identity_provider')
idp_id = default_idp.get('id')
url = self.base_url(suffix=idp_id)
body['remote_ids'] = [repeated_remote_id]
resp = self.patch(url, body={'identity_provider': body},
expected_status=http_client.CONFLICT)
resp_data = jsonutils.loads(resp.body)
self.assertIn('Duplicate remote ID',
resp_data['error']['message'])
def test_list_head_idps(self, iterations=5):
"""List all available IdentityProviders.
This test collects ids of created IdPs and
intersects it with the list of all available IdPs.
List of all IdPs can be a superset of IdPs created in this test,
because other tests also create IdPs.
"""
def get_id(resp):
r = self._fetch_attribute_from_response(resp,
'identity_provider')
return r.get('id')
ids = []
for _ in range(iterations):
id = get_id(self._create_default_idp())
ids.append(id)
ids = set(ids)
keys_to_check = self.idp_keys
keys_to_check.append('domain_id')
url = self.base_url()
resp = self.get(url)
self.assertValidListResponse(resp, 'identity_providers',
dummy_validator,
keys_to_check=keys_to_check)
entities = self._fetch_attribute_from_response(resp,
'identity_providers')
entities_ids = set([e['id'] for e in entities])
ids_intersection = entities_ids.intersection(ids)
self.assertEqual(ids_intersection, ids)
self.head(url, expected_status=http_client.OK)
def test_filter_list_head_idp_by_id(self):
def get_id(resp):
r = self._fetch_attribute_from_response(resp,
'identity_provider')
return r.get('id')
idp1_id = get_id(self._create_default_idp())
idp2_id = get_id(self._create_default_idp())
# list the IdP, should get two IdP.
url = self.base_url()
resp = self.get(url)
entities = self._fetch_attribute_from_response(resp,
'identity_providers')
entities_ids = [e['id'] for e in entities]
self.assertItemsEqual(entities_ids, [idp1_id, idp2_id])
# filter the IdP by ID.
url = self.base_url() + '?id=' + idp1_id
resp = self.get(url)
filtered_service_list = resp.json['identity_providers']
self.assertThat(filtered_service_list, matchers.HasLength(1))
self.assertEqual(idp1_id, filtered_service_list[0].get('id'))
self.head(url, expected_status=http_client.OK)
def test_filter_list_head_idp_by_enabled(self):
def get_id(resp):
r = self._fetch_attribute_from_response(resp,
'identity_provider')
return r.get('id')
idp1_id = get_id(self._create_default_idp())
body = self.default_body.copy()
body['enabled'] = False
idp2_id = get_id(self._create_default_idp(body=body))
# list the IdP, should get two IdP.
url = self.base_url()
resp = self.get(url)
entities = self._fetch_attribute_from_response(resp,
'identity_providers')
entities_ids = [e['id'] for e in entities]
self.assertItemsEqual(entities_ids, [idp1_id, idp2_id])
# filter the IdP by 'enabled'.
url = self.base_url() + '?enabled=True'
resp = self.get(url)
filtered_service_list = resp.json['identity_providers']
self.assertThat(filtered_service_list, matchers.HasLength(1))
self.assertEqual(idp1_id, filtered_service_list[0].get('id'))
self.head(url, expected_status=http_client.OK)
def test_check_idp_uniqueness(self):
"""Add same IdP twice.
Expect HTTP 409 Conflict code for the latter call.
"""
url = self.base_url(suffix=uuid.uuid4().hex)
body = self._http_idp_input()
domain = unit.new_domain_ref()
PROVIDERS.resource_api.create_domain(domain['id'], domain)
body['domain_id'] = domain['id']
self.put(url, body={'identity_provider': body},
expected_status=http_client.CREATED)
resp = self.put(url, body={'identity_provider': body},
expected_status=http_client.CONFLICT)
resp_data = jsonutils.loads(resp.body)
self.assertIn('Duplicate entry',
resp_data.get('error', {}).get('message'))
def test_get_head_idp(self):
"""Create and later fetch IdP."""
body = self._http_idp_input()
domain = unit.new_domain_ref()
PROVIDERS.resource_api.create_domain(domain['id'], domain)
body['domain_id'] = domain['id']
default_resp = self._create_default_idp(body=body)
default_idp = self._fetch_attribute_from_response(default_resp,
'identity_provider')
idp_id = default_idp.get('id')
url = self.base_url(suffix=idp_id)
resp = self.get(url)
# Strip keys out of `body` dictionary. This is done
# to be python 3 compatible
body_keys = list(body)
self.assertValidResponse(resp, 'identity_provider',
dummy_validator, keys_to_check=body_keys,
ref=body)
self.head(url, expected_status=http_client.OK)
def test_get_nonexisting_idp(self):
"""Fetch nonexisting IdP entity.
Expected HTTP 404 Not Found status code.
"""
idp_id = uuid.uuid4().hex
self.assertIsNotNone(idp_id)
url = self.base_url(suffix=idp_id)
self.get(url, expected_status=http_client.NOT_FOUND)
def test_delete_existing_idp(self):
"""Create and later delete IdP.
Expect HTTP 404 Not Found for the GET IdP call.
"""
default_resp = self._create_default_idp()
default_idp = self._fetch_attribute_from_response(default_resp,
'identity_provider')
idp_id = default_idp.get('id')
self.assertIsNotNone(idp_id)
url = self.base_url(suffix=idp_id)
self.delete(url)
self.get(url, expected_status=http_client.NOT_FOUND)
def test_delete_idp_also_deletes_assigned_protocols(self):
"""Deleting an IdP will delete its assigned protocol."""
# create default IdP
default_resp = self._create_default_idp()
default_idp = self._fetch_attribute_from_response(default_resp,
'identity_provider')
idp_id = default_idp['id']
protocol_id = uuid.uuid4().hex
url = self.base_url(suffix='%(idp_id)s/protocols/%(protocol_id)s')
idp_url = self.base_url(suffix=idp_id)
# assign protocol to IdP
kwargs = {'expected_status': http_client.CREATED}
resp, idp_id, proto = self._assign_protocol_to_idp(
url=url,
idp_id=idp_id,
proto=protocol_id,
**kwargs)
# removing IdP will remove the assigned protocol as well
self.assertEqual(
1, len(PROVIDERS.federation_api.list_protocols(idp_id))
)
self.delete(idp_url)
self.get(idp_url, expected_status=http_client.NOT_FOUND)
self.assertEqual(
0, len(PROVIDERS.federation_api.list_protocols(idp_id))
)
def test_delete_nonexisting_idp(self):
"""Delete nonexisting IdP.
Expect HTTP 404 Not Found for the GET IdP call.
"""
idp_id = uuid.uuid4().hex
url = self.base_url(suffix=idp_id)
self.delete(url, expected_status=http_client.NOT_FOUND)
def test_update_idp_mutable_attributes(self):
"""Update IdP's mutable parameters."""
default_resp = self._create_default_idp()
default_idp = self._fetch_attribute_from_response(default_resp,
'identity_provider')
idp_id = default_idp.get('id')
url = self.base_url(suffix=idp_id)
self.assertIsNotNone(idp_id)
_enabled = not default_idp.get('enabled')
body = {'remote_ids': [uuid.uuid4().hex, uuid.uuid4().hex],
'description': uuid.uuid4().hex,
'enabled': _enabled}
body = {'identity_provider': body}
resp = self.patch(url, body=body)
updated_idp = self._fetch_attribute_from_response(resp,
'identity_provider')
body = body['identity_provider']
for key in body.keys():
if isinstance(body[key], list):
self.assertEqual(sorted(body[key]),
sorted(updated_idp.get(key)))
else:
self.assertEqual(body[key], updated_idp.get(key))
resp = self.get(url)
updated_idp = self._fetch_attribute_from_response(resp,
'identity_provider')
for key in body.keys():
if isinstance(body[key], list):
self.assertEqual(sorted(body[key]),
sorted(updated_idp.get(key)))
else:
self.assertEqual(body[key], updated_idp.get(key))
def test_update_idp_immutable_attributes(self):
"""Update IdP's immutable parameters.
Expect HTTP BAD REQUEST.
"""
default_resp = self._create_default_idp()
default_idp = self._fetch_attribute_from_response(default_resp,
'identity_provider')
idp_id = default_idp.get('id')
self.assertIsNotNone(idp_id)
body = self._http_idp_input()
body['id'] = uuid.uuid4().hex
body['protocols'] = [uuid.uuid4().hex, uuid.uuid4().hex]
url = self.base_url(suffix=idp_id)
self.patch(url, body={'identity_provider': body},
expected_status=http_client.BAD_REQUEST)
def test_update_nonexistent_idp(self):
"""Update nonexistent IdP.
Expect HTTP 404 Not Found code.
"""
idp_id = uuid.uuid4().hex
url = self.base_url(suffix=idp_id)
body = self._http_idp_input()
body['enabled'] = False
body = {'identity_provider': body}
self.patch(url, body=body, expected_status=http_client.NOT_FOUND)
def test_assign_protocol_to_idp(self):
"""Assign a protocol to existing IdP."""
self._assign_protocol_to_idp(expected_status=http_client.CREATED)
def test_protocol_composite_pk(self):
"""Test that Keystone can add two entities.
The entities have identical names, however, attached to different
IdPs.
1. Add IdP and assign it protocol with predefined name
2. Add another IdP and assign it a protocol with same name.
Expect HTTP 201 code
"""
url = self.base_url(suffix='%(idp_id)s/protocols/%(protocol_id)s')
kwargs = {'expected_status': http_client.CREATED}
self._assign_protocol_to_idp(proto='saml2',
url=url, **kwargs)
self._assign_protocol_to_idp(proto='saml2',
url=url, **kwargs)
def test_protocol_idp_pk_uniqueness(self):
"""Test whether Keystone checks for unique idp/protocol values.
Add same protocol twice, expect Keystone to reject a latter call and
return HTTP 409 Conflict code.
"""
url = self.base_url(suffix='%(idp_id)s/protocols/%(protocol_id)s')
kwargs = {'expected_status': http_client.CREATED}
resp, idp_id, proto = self._assign_protocol_to_idp(proto='saml2',
url=url, **kwargs)
kwargs = {'expected_status': http_client.CONFLICT}
self._assign_protocol_to_idp(
idp_id=idp_id, proto='saml2', validate=False, url=url, **kwargs
)
def test_assign_protocol_to_nonexistent_idp(self):
"""Assign protocol to IdP that doesn't exist.
Expect HTTP 404 Not Found code.
"""
idp_id = uuid.uuid4().hex
kwargs = {'expected_status': http_client.NOT_FOUND}
self._assign_protocol_to_idp(proto='saml2',
idp_id=idp_id,
validate=False,
**kwargs)
def test_crud_protocol_without_protocol_id_in_url(self):
# NOTE(morgan): This test is redundant but is added to ensure
# the url routing error in bug 1817313 is explicitly covered.
# create a protocol, but do not put the ID in the URL
idp_id, _ = self._create_and_decapsulate_response()
mapping_id = uuid.uuid4().hex
self._create_mapping(mapping_id=mapping_id)
protocol = {
'id': uuid.uuid4().hex,
'mapping_id': mapping_id
}
with self.test_client() as c:
token = self.get_scoped_token()
# DELETE/PATCH/PUT on non-trailing `/` results in
# METHOD_NOT_ALLOWED
c.delete('/v3/OS-FEDERATION/identity_providers/%(idp_id)s'
'/protocols' % {'idp_id': idp_id},
headers={'X-Auth-Token': token},
expected_status_code=http_client.METHOD_NOT_ALLOWED)
c.patch('/v3/OS-FEDERATION/identity_providers/%(idp_id)s'
'/protocols/' % {'idp_id': idp_id},
json={'protocol': protocol},
headers={'X-Auth-Token': token},
expected_status_code=http_client.METHOD_NOT_ALLOWED)
c.put('/v3/OS-FEDERATION/identity_providers/%(idp_id)s'
'/protocols' % {'idp_id': idp_id},
json={'protocol': protocol},
headers={'X-Auth-Token': token},
expected_status_code=http_client.METHOD_NOT_ALLOWED)
# DELETE/PATCH/PUT should raise 405 with trailing '/', it is
# remapped to without the trailing '/' by the normalization
# middleware.
c.delete('/v3/OS-FEDERATION/identity_providers/%(idp_id)s'
'/protocols/' % {'idp_id': idp_id},
headers={'X-Auth-Token': token},
expected_status_code=http_client.METHOD_NOT_ALLOWED)
c.patch('/v3/OS-FEDERATION/identity_providers/%(idp_id)s'
'/protocols/' % {'idp_id': idp_id},
json={'protocol': protocol},
headers={'X-Auth-Token': token},
expected_status_code=http_client.METHOD_NOT_ALLOWED)
c.put('/v3/OS-FEDERATION/identity_providers/%(idp_id)s'
'/protocols/' % {'idp_id': idp_id},
json={'protocol': protocol},
headers={'X-Auth-Token': token},
expected_status_code=http_client.METHOD_NOT_ALLOWED)
def test_get_head_protocol(self):
"""Create and later fetch protocol tied to IdP."""
resp, idp_id, proto = self._assign_protocol_to_idp(
expected_status=http_client.CREATED)
proto_id = self._fetch_attribute_from_response(resp, 'protocol')['id']
url = "%s/protocols/%s" % (idp_id, proto_id)
url = self.base_url(suffix=url)
resp = self.get(url)
reference = {'id': proto_id}
# Strip keys out of `body` dictionary. This is done
# to be python 3 compatible
reference_keys = list(reference)
self.assertValidResponse(resp, 'protocol',
dummy_validator,
keys_to_check=reference_keys,
ref=reference)
self.head(url, expected_status=http_client.OK)
def test_list_head_protocols(self):
"""Create set of protocols and later list them.
Compare input and output id sets.
"""
resp, idp_id, proto = self._assign_protocol_to_idp(
expected_status=http_client.CREATED)
iterations = random.randint(0, 16)
protocol_ids = []
for _ in range(iterations):
resp, _, proto = self._assign_protocol_to_idp(
idp_id=idp_id,
expected_status=http_client.CREATED)
proto_id = self._fetch_attribute_from_response(resp, 'protocol')
proto_id = proto_id['id']
protocol_ids.append(proto_id)
url = "%s/protocols" % idp_id
url = self.base_url(suffix=url)
resp = self.get(url)
self.assertValidListResponse(resp, 'protocols',
dummy_validator,
keys_to_check=['id'])
entities = self._fetch_attribute_from_response(resp, 'protocols')
entities = set([entity['id'] for entity in entities])
protocols_intersection = entities.intersection(protocol_ids)
self.assertEqual(protocols_intersection, set(protocol_ids))
self.head(url, expected_status=http_client.OK)
def test_update_protocols_attribute(self):
"""Update protocol's attribute."""
resp, idp_id, proto = self._assign_protocol_to_idp(
expected_status=http_client.CREATED)
new_mapping_id = uuid.uuid4().hex
self._create_mapping(mapping_id=new_mapping_id)
url = "%s/protocols/%s" % (idp_id, proto)
url = self.base_url(suffix=url)
body = {'mapping_id': new_mapping_id}
resp = self.patch(url, body={'protocol': body})
self.assertValidResponse(resp, 'protocol', dummy_validator,
keys_to_check=['id', 'mapping_id'],
ref={'id': proto,
'mapping_id': new_mapping_id}
)
def test_delete_protocol(self):
"""Delete protocol.
Expect HTTP 404 Not Found code for the GET call after the protocol is
deleted.
"""
url = self.base_url(suffix='%(idp_id)s/'
'protocols/%(protocol_id)s')
resp, idp_id, proto = self._assign_protocol_to_idp(
expected_status=http_client.CREATED)
url = url % {'idp_id': idp_id,
'protocol_id': proto}
self.delete(url)
self.get(url, expected_status=http_client.NOT_FOUND)
class MappingCRUDTests(test_v3.RestfulTestCase):
"""A class for testing CRUD operations for Mappings."""
MAPPING_URL = '/OS-FEDERATION/mappings/'
def assertValidMappingListResponse(self, resp, *args, **kwargs):
return self.assertValidListResponse(
resp,
'mappings',
self.assertValidMapping,
keys_to_check=[],
*args,
**kwargs)
def assertValidMappingResponse(self, resp, *args, **kwargs):
return self.assertValidResponse(
resp,
'mapping',
self.assertValidMapping,
keys_to_check=[],
*args,
**kwargs)
def assertValidMapping(self, entity, ref=None):
self.assertIsNotNone(entity.get('id'))
self.assertIsNotNone(entity.get('rules'))
if ref:
self.assertEqual(entity['rules'], ref['rules'])
return entity
def _create_default_mapping_entry(self):
url = self.MAPPING_URL + uuid.uuid4().hex
resp = self.put(url,
body={'mapping': mapping_fixtures.MAPPING_LARGE},
expected_status=http_client.CREATED)
return resp
def _get_id_from_response(self, resp):
r = resp.result.get('mapping')
return r.get('id')
def test_mapping_create(self):
resp = self._create_default_mapping_entry()
self.assertValidMappingResponse(resp, mapping_fixtures.MAPPING_LARGE)
def test_mapping_list_head(self):
url = self.MAPPING_URL
self._create_default_mapping_entry()
resp = self.get(url)
entities = resp.result.get('mappings')
self.assertIsNotNone(entities)
self.assertResponseStatus(resp, http_client.OK)
self.assertValidListLinks(resp.result.get('links'))
self.assertEqual(1, len(entities))
self.head(url, expected_status=http_client.OK)
def test_mapping_delete(self):
url = self.MAPPING_URL + '%(mapping_id)s'
resp = self._create_default_mapping_entry()
mapping_id = self._get_id_from_response(resp)
url = url % {'mapping_id': str(mapping_id)}
resp = self.delete(url)
self.assertResponseStatus(resp, http_client.NO_CONTENT)
self.get(url, expected_status=http_client.NOT_FOUND)
def test_mapping_get_head(self):
url = self.MAPPING_URL + '%(mapping_id)s'
resp = self._create_default_mapping_entry()
mapping_id = self._get_id_from_response(resp)
url = url % {'mapping_id': mapping_id}
resp = self.get(url)
self.assertValidMappingResponse(resp, mapping_fixtures.MAPPING_LARGE)
self.head(url, expected_status=http_client.OK)
def test_mapping_update(self):
url = self.MAPPING_URL + '%(mapping_id)s'
resp = self._create_default_mapping_entry()
mapping_id = self._get_id_from_response(resp)
url = url % {'mapping_id': mapping_id}
resp = self.patch(url,
body={'mapping': mapping_fixtures.MAPPING_SMALL})
self.assertValidMappingResponse(resp, mapping_fixtures.MAPPING_SMALL)
resp = self.get(url)
self.assertValidMappingResponse(resp, mapping_fixtures.MAPPING_SMALL)
def test_delete_mapping_dne(self):
url = self.MAPPING_URL + uuid.uuid4().hex
self.delete(url, expected_status=http_client.NOT_FOUND)
def test_get_mapping_dne(self):
url = self.MAPPING_URL + uuid.uuid4().hex
self.get(url, expected_status=http_client.NOT_FOUND)
def test_create_mapping_bad_requirements(self):
url = self.MAPPING_URL + uuid.uuid4().hex
self.put(url, expected_status=http_client.BAD_REQUEST,
body={'mapping': mapping_fixtures.MAPPING_BAD_REQ})
def test_create_mapping_no_rules(self):
url = self.MAPPING_URL + uuid.uuid4().hex
self.put(url, expected_status=http_client.BAD_REQUEST,
body={'mapping': mapping_fixtures.MAPPING_NO_RULES})
def test_create_mapping_no_remote_objects(self):
url = self.MAPPING_URL + uuid.uuid4().hex
self.put(url, expected_status=http_client.BAD_REQUEST,
body={'mapping': mapping_fixtures.MAPPING_NO_REMOTE})
def test_create_mapping_bad_value(self):
url = self.MAPPING_URL + uuid.uuid4().hex
self.put(url, expected_status=http_client.BAD_REQUEST,
body={'mapping': mapping_fixtures.MAPPING_BAD_VALUE})
def test_create_mapping_missing_local(self):
url = self.MAPPING_URL + uuid.uuid4().hex
self.put(url, expected_status=http_client.BAD_REQUEST,
body={'mapping': mapping_fixtures.MAPPING_MISSING_LOCAL})
def test_create_mapping_missing_type(self):
url = self.MAPPING_URL + uuid.uuid4().hex
self.put(url, expected_status=http_client.BAD_REQUEST,
body={'mapping': mapping_fixtures.MAPPING_MISSING_TYPE})
def test_create_mapping_wrong_type(self):
url = self.MAPPING_URL + uuid.uuid4().hex
self.put(url, expected_status=http_client.BAD_REQUEST,
body={'mapping': mapping_fixtures.MAPPING_WRONG_TYPE})
def test_create_mapping_extra_remote_properties_not_any_of(self):
url = self.MAPPING_URL + uuid.uuid4().hex
mapping = mapping_fixtures.MAPPING_EXTRA_REMOTE_PROPS_NOT_ANY_OF
self.put(url, expected_status=http_client.BAD_REQUEST,
body={'mapping': mapping})
def test_create_mapping_extra_remote_properties_any_one_of(self):
url = self.MAPPING_URL + uuid.uuid4().hex
mapping = mapping_fixtures.MAPPING_EXTRA_REMOTE_PROPS_ANY_ONE_OF
self.put(url, expected_status=http_client.BAD_REQUEST,
body={'mapping': mapping})
def test_create_mapping_extra_remote_properties_just_type(self):
url = self.MAPPING_URL + uuid.uuid4().hex
mapping = mapping_fixtures.MAPPING_EXTRA_REMOTE_PROPS_JUST_TYPE
self.put(url, expected_status=http_client.BAD_REQUEST,
body={'mapping': mapping})
def test_create_mapping_empty_map(self):
url = self.MAPPING_URL + uuid.uuid4().hex
self.put(url, expected_status=http_client.BAD_REQUEST,
body={'mapping': {}})
def test_create_mapping_extra_rules_properties(self):
url = self.MAPPING_URL + uuid.uuid4().hex
self.put(url, expected_status=http_client.BAD_REQUEST,
body={'mapping': mapping_fixtures.MAPPING_EXTRA_RULES_PROPS})
def test_create_mapping_with_blacklist_and_whitelist(self):
"""Test for adding whitelist and blacklist in the rule.
Server should respond with HTTP 400 Bad Request error upon discovering
both ``whitelist`` and ``blacklist`` keywords in the same rule.
"""
url = self.MAPPING_URL + uuid.uuid4().hex
mapping = mapping_fixtures.MAPPING_GROUPS_WHITELIST_AND_BLACKLIST
self.put(url, expected_status=http_client.BAD_REQUEST,
body={'mapping': mapping})
def test_create_mapping_with_local_user_and_local_domain(self):
url = self.MAPPING_URL + uuid.uuid4().hex
resp = self.put(
url,
body={
'mapping': mapping_fixtures.MAPPING_LOCAL_USER_LOCAL_DOMAIN
},
expected_status=http_client.CREATED)
self.assertValidMappingResponse(
resp, mapping_fixtures.MAPPING_LOCAL_USER_LOCAL_DOMAIN)
def test_create_mapping_with_ephemeral(self):
url = self.MAPPING_URL + uuid.uuid4().hex
resp = self.put(
url,
body={'mapping': mapping_fixtures.MAPPING_EPHEMERAL_USER},
expected_status=http_client.CREATED)
self.assertValidMappingResponse(
resp, mapping_fixtures.MAPPING_EPHEMERAL_USER)
def test_create_mapping_with_bad_user_type(self):
url = self.MAPPING_URL + uuid.uuid4().hex
# get a copy of a known good map
bad_mapping = copy.deepcopy(mapping_fixtures.MAPPING_EPHEMERAL_USER)
# now sabotage the user type
bad_mapping['rules'][0]['local'][0]['user']['type'] = uuid.uuid4().hex
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):
def auth_plugin_config_override(self):
methods = ['saml2', 'token']
super(FederatedTokenTests, self).auth_plugin_config_override(methods)
def setUp(self):
super(FederatedTokenTests, self).setUp()
self._notifications = []
def fake_saml_notify(action, user_id, group_ids,
identity_provider, protocol, token_id, outcome):
note = {
'action': action,
'user_id': user_id,
'identity_provider': identity_provider,
'protocol': protocol,
'send_notification_called': True}
self._notifications.append(note)
self.useFixture(fixtures.MockPatchObject(
notifications,
'send_saml_audit_notification',
fake_saml_notify))
def _assert_last_notify(self, action, identity_provider, protocol,
user_id=None):
self.assertTrue(self._notifications)
note = self._notifications[-1]
if user_id:
self.assertEqual(note['user_id'], user_id)
self.assertEqual(note['action'], action)
self.assertEqual(note['identity_provider'], identity_provider)
self.assertEqual(note['protocol'], protocol)
self.assertTrue(note['send_notification_called'])
def load_fixtures(self, fixtures):
super(FederatedTokenTests, self).load_fixtures(fixtures)
self.load_federation_sample_data()
def test_issue_unscoped_token_notify(self):
self._issue_unscoped_token()
self._assert_last_notify(self.ACTION, self.IDP, self.PROTOCOL)
def test_issue_unscoped_token(self):
r = self._issue_unscoped_token()
token_resp = render_token.render_token_response_from_model(r)['token']
self.assertValidMappedUser(token_resp)
def test_default_domain_scoped_token(self):
# Make sure federated users can get tokens scoped to the default
# domain, which has a non-uuid ID by default (e.g., `default`). We want
# to make sure the token provider handles string types properly if the
# ID isn't compressed into byte format during validation. Turn off
# cache on issue so that we validate the token online right after we
# get it to make sure the token provider is called.
self.config_fixture.config(group='token', cache_on_issue=False)
# Grab an unscoped token to get a domain-scoped token with.
token = self._issue_unscoped_token()
# Give the user a direct role assignment on the default domain, so they
# can get a federated domain-scoped token.
PROVIDERS.assignment_api.create_grant(
self.role_admin['id'], user_id=token.user_id,
domain_id=CONF.identity.default_domain_id
)
# Get a token scoped to the default domain with an ID of `default`,
# which isn't a uuid type, but we should be able to handle it
# accordingly in the token formatters/providers.
auth_request = {
'auth': {
'identity': {
'methods': [
'token'
],
'token': {
'id': token.id
}
},
'scope': {
'domain': {
'id': CONF.identity.default_domain_id
}
}
}
}
r = self.v3_create_token(auth_request)
domain_scoped_token_id = r.headers.get('X-Subject-Token')
# Validate the token to make sure the token providers handle non-uuid
# domain IDs properly.
headers = {'X-Subject-Token': domain_scoped_token_id}
self.get(
'/auth/tokens',
token=domain_scoped_token_id,
headers=headers
)
def test_issue_the_same_unscoped_token_with_user_deleted(self):
r = self._issue_unscoped_token()
token = render_token.render_token_response_from_model(r)['token']
user1 = token['user']
user_id1 = user1.pop('id')
# delete the referenced user, and authenticate again. Keystone should
# create another new shadow user.
PROVIDERS.identity_api.delete_user(user_id1)
r = self._issue_unscoped_token()
token = render_token.render_token_response_from_model(r)['token']
user2 = token['user']
user_id2 = user2.pop('id')
# Only the user_id is different. Other properties include
# identity_provider, protocol, groups and domain are the same.
self.assertIsNot(user_id2, user_id1)
self.assertEqual(user1, user2)
def test_issue_unscoped_token_disabled_idp(self):
"""Check if authentication works with disabled identity providers.
Test plan:
1) Disable default IdP
2) Try issuing unscoped token for that IdP
3) Expect server to forbid authentication
"""
enabled_false = {'enabled': False}
PROVIDERS.federation_api.update_idp(self.IDP, enabled_false)
self.assertRaises(exception.Forbidden,
self._issue_unscoped_token)
def test_issue_unscoped_token_group_names_in_mapping(self):
r = self._issue_unscoped_token(assertion='ANOTHER_CUSTOMER_ASSERTION')
ref_groups = set([self.group_customers['id'], self.group_admins['id']])
token_groups = r.federated_groups
token_groups = set([group['id'] for group in token_groups])
self.assertEqual(ref_groups, token_groups)
def test_issue_unscoped_tokens_nonexisting_group(self):
self._issue_unscoped_token(assertion='ANOTHER_TESTER_ASSERTION')
def test_issue_unscoped_token_with_remote_no_attribute(self):
self._issue_unscoped_token(idp=self.IDP_WITH_REMOTE,
environment={
self.REMOTE_ID_ATTR:
self.REMOTE_IDS[0]
})
def test_issue_unscoped_token_with_remote(self):
self.config_fixture.config(group='federation',
remote_id_attribute=self.REMOTE_ID_ATTR)
self._issue_unscoped_token(idp=self.IDP_WITH_REMOTE,
environment={
self.REMOTE_ID_ATTR:
self.REMOTE_IDS[0]
})
def test_issue_unscoped_token_with_saml2_remote(self):
self.config_fixture.config(group='saml2',
remote_id_attribute=self.REMOTE_ID_ATTR)
self._issue_unscoped_token(idp=self.IDP_WITH_REMOTE,
environment={
self.REMOTE_ID_ATTR:
self.REMOTE_IDS[0]
})
def test_issue_unscoped_token_with_remote_different(self):
self.config_fixture.config(group='federation',
remote_id_attribute=self.REMOTE_ID_ATTR)
self.assertRaises(exception.Forbidden,
self._issue_unscoped_token,
idp=self.IDP_WITH_REMOTE,
environment={
self.REMOTE_ID_ATTR: uuid.uuid4().hex
})
def test_issue_unscoped_token_with_remote_default_overwritten(self):
"""Test that protocol remote_id_attribute has higher priority.
Make sure the parameter stored under ``protocol`` section has higher
priority over parameter from default ``federation`` configuration
section.
"""
self.config_fixture.config(group='saml2',
remote_id_attribute=self.REMOTE_ID_ATTR)
self.config_fixture.config(group='federation',
remote_id_attribute=uuid.uuid4().hex)
self._issue_unscoped_token(idp=self.IDP_WITH_REMOTE,
environment={
self.REMOTE_ID_ATTR:
self.REMOTE_IDS[0]
})
def test_issue_unscoped_token_with_remote_unavailable(self):
self.config_fixture.config(group='federation',
remote_id_attribute=self.REMOTE_ID_ATTR)
self.assertRaises(exception.Unauthorized,
self._issue_unscoped_token,
idp=self.IDP_WITH_REMOTE,
environment={
uuid.uuid4().hex: uuid.uuid4().hex
})
def test_issue_unscoped_token_with_remote_user_as_empty_string(self):
# make sure that REMOTE_USER set as the empty string won't interfere
self._issue_unscoped_token(environment={'REMOTE_USER': ''})
def test_issue_unscoped_token_no_groups(self):
r = self._issue_unscoped_token(assertion='USER_NO_GROUPS_ASSERTION')
token_groups = r.federated_groups
self.assertEqual(0, len(token_groups))
def test_issue_scoped_token_no_groups(self):
"""Verify that token without groups cannot get scoped to project.
This test is required because of bug 1677723.
"""
# issue unscoped token with no groups
r = self._issue_unscoped_token(assertion='USER_NO_GROUPS_ASSERTION')
token_groups = r.federated_groups
self.assertEqual(0, len(token_groups))
unscoped_token = r.id
# let admin get roles in a project
self.proj_employees
admin = unit.new_user_ref(CONF.identity.default_domain_id)
PROVIDERS.identity_api.create_user(admin)
PROVIDERS.assignment_api.create_grant(
self.role_admin['id'], user_id=admin['id'],
project_id=self.proj_employees['id']
)
# try to scope the token. It should fail
scope = self._scope_request(
unscoped_token, 'project', self.proj_employees['id']
)
self.v3_create_token(
scope, expected_status=http_client.UNAUTHORIZED)
def test_issue_unscoped_token_malformed_environment(self):
"""Test whether non string objects are filtered out.
Put non string objects into the environment, inject
correct assertion and try to get an unscoped token.
Expect server not to fail on using split() method on
non string objects and return token id in the HTTP header.
"""
environ = {
'malformed_object': object(),
'another_bad_idea': tuple(range(10)),
'yet_another_bad_param': dict(zip(uuid.uuid4().hex, range(32)))
}
environ.update(mapping_fixtures.EMPLOYEE_ASSERTION)
with self.make_request(environ=environ):
authentication.authenticate_for_token(self.UNSCOPED_V3_SAML2_REQ)
def test_scope_to_project_once_notify(self):
r = self.v3_create_token(
self.TOKEN_SCOPE_PROJECT_EMPLOYEE_FROM_EMPLOYEE)
user_id = r.json['token']['user']['id']
self._assert_last_notify(self.ACTION, self.IDP, self.PROTOCOL, user_id)
def test_scope_to_project_once(self):
r = self.v3_create_token(
self.TOKEN_SCOPE_PROJECT_EMPLOYEE_FROM_EMPLOYEE)
token_resp = r.result['token']
project_id = token_resp['project']['id']
self._check_project_scoped_token_attributes(token_resp, project_id)
roles_ref = [self.role_employee]
projects_ref = self.proj_employees
self._check_projects_and_roles(token_resp, roles_ref, projects_ref)
self.assertValidMappedUser(token_resp)
def test_scope_token_with_idp_disabled(self):
"""Scope token issued by disabled IdP.
Try scoping the token issued by an IdP which is disabled now. Expect
server to refuse scoping operation.
This test confirms correct behaviour when IdP was enabled and unscoped
token was issued, but disabled before user tries to scope the token.
Here we assume the unscoped token was already issued and start from
the moment where IdP is being disabled and unscoped token is being
used.
Test plan:
1) Disable IdP
2) Try scoping unscoped token
"""
enabled_false = {'enabled': False}
PROVIDERS.federation_api.update_idp(self.IDP, enabled_false)
self.v3_create_token(
self.TOKEN_SCOPE_PROJECT_EMPLOYEE_FROM_CUSTOMER,
expected_status=http_client.FORBIDDEN)
def test_validate_token_after_deleting_idp_raises_not_found(self):
token = self.v3_create_token(
self.TOKEN_SCOPE_PROJECT_EMPLOYEE_FROM_ADMIN
)
token_id = token.headers.get('X-Subject-Token')
federated_info = token.json_body['token']['user']['OS-FEDERATION']
idp_id = federated_info['identity_provider']['id']
PROVIDERS.federation_api.delete_idp(idp_id)
headers = {
'X-Subject-Token': token_id
}
# NOTE(lbragstad): This raises a 404 NOT FOUND because the identity
# provider is no longer present. We raise 404 NOT FOUND when we
# validate a token and a project or domain no longer exists.
self.get(
'/auth/tokens/',
token=token_id,
headers=headers,
expected_status=http_client.NOT_FOUND
)
def test_deleting_idp_cascade_deleting_fed_user(self):
token = self.v3_create_token(
self.TOKEN_SCOPE_PROJECT_EMPLOYEE_FROM_ADMIN
)
federated_info = token.json_body['token']['user']['OS-FEDERATION']
idp_id = federated_info['identity_provider']['id']
# There are three fed users (from 'EMPLOYEE_ASSERTION',
# 'CUSTOMER_ASSERTION', 'ADMIN_ASSERTION') with the specified idp.
hints = driver_hints.Hints()
hints.add_filter('idp_id', idp_id)
fed_users = PROVIDERS.shadow_users_api.get_federated_users(hints)
self.assertEqual(3, len(fed_users))
idp_domain_id = PROVIDERS.federation_api.get_idp(idp_id)['domain_id']
for fed_user in fed_users:
self.assertEqual(idp_domain_id, fed_user['domain_id'])
# Delete the idp
PROVIDERS.federation_api.delete_idp(idp_id)
# The related federated user should be deleted as well.
hints = driver_hints.Hints()
hints.add_filter('idp_id', idp_id)
fed_users = PROVIDERS.shadow_users_api.get_federated_users(hints)
self.assertEqual([], fed_users)
def test_scope_to_bad_project(self):
"""Scope unscoped token with a project we don't have access to."""
self.v3_create_token(
self.TOKEN_SCOPE_PROJECT_EMPLOYEE_FROM_CUSTOMER,
expected_status=http_client.UNAUTHORIZED)
def test_scope_to_project_multiple_times(self):
"""Try to scope the unscoped token multiple times.
The new tokens should be scoped to:
* Customers' project
* Employees' project
"""
bodies = (self.TOKEN_SCOPE_PROJECT_EMPLOYEE_FROM_ADMIN,
self.TOKEN_SCOPE_PROJECT_CUSTOMER_FROM_ADMIN)
project_ids = (self.proj_employees['id'],
self.proj_customers['id'])
for body, project_id_ref in zip(bodies, project_ids):
r = self.v3_create_token(body)
token_resp = r.result['token']
self._check_project_scoped_token_attributes(token_resp,
project_id_ref)
def test_scope_to_project_with_duplicate_roles_returns_single_role(self):
r = self.v3_create_token(self.TOKEN_SCOPE_PROJECT_EMPLOYEE_FROM_ADMIN)
# Even though the process of obtaining a token shows that there is a
# role assignment on a project, we should attempt to create a duplicate
# assignment somewhere. Do this by creating a direct role assignment
# with each role against the project the token was scoped to.
user_id = r.json_body['token']['user']['id']
project_id = r.json_body['token']['project']['id']
for role in r.json_body['token']['roles']:
PROVIDERS.assignment_api.create_grant(
role_id=role['id'], user_id=user_id, project_id=project_id
)
# Ensure all roles in the token are unique even though we know there
# should be duplicate role assignment from the assertions and the
# direct role assignments we just created.
r = self.v3_create_token(self.TOKEN_SCOPE_PROJECT_EMPLOYEE_FROM_ADMIN)