Add keystone oidc tests
This adds tests to test getting a token (scoped and unscoped) when keystone is configured to use oidc for authentication. The oidc provider is keycloak. This is based in very large part on Kristi's work in [1] and [2]. [1] https://github.com/knikolla/devstack-plugin-oidc [2] https://github.com/CCI-MOC/onboarding-tools Co-Authored-By: David Wilde <dwilde@redhat.com> Change-Id: I1772b65f1cc3830ac293a800a79d044a6ab69d65
This commit is contained in:
parent
7f43a20380
commit
47a5e98ae9
@ -47,6 +47,14 @@ FedScenarioGroup = [
|
|||||||
help='Password used to login in the Identity Provider'),
|
help='Password used to login in the Identity Provider'),
|
||||||
cfg.StrOpt('idp_ecp_url',
|
cfg.StrOpt('idp_ecp_url',
|
||||||
help='Identity Provider SAML2/ECP URL'),
|
help='Identity Provider SAML2/ECP URL'),
|
||||||
|
cfg.StrOpt('idp_oidc_url',
|
||||||
|
help='Identity Provider OIDC URL'),
|
||||||
|
|
||||||
|
# client id (oidc)
|
||||||
|
cfg.StrOpt('idp_client_id',
|
||||||
|
help='Identity Provider Client ID'),
|
||||||
|
cfg.StrOpt('idp_client_secret',
|
||||||
|
help='Identity Provider Client Secret'),
|
||||||
|
|
||||||
# Mapping rules
|
# Mapping rules
|
||||||
cfg.StrOpt('mapping_remote_type',
|
cfg.StrOpt('mapping_remote_type',
|
||||||
@ -72,5 +80,6 @@ FedScenarioGroup = [
|
|||||||
# Protocol
|
# Protocol
|
||||||
cfg.StrOpt('protocol_id',
|
cfg.StrOpt('protocol_id',
|
||||||
default='mapped',
|
default='mapped',
|
||||||
help='The Protocol ID')
|
help='The Protocol ID'),
|
||||||
|
|
||||||
]
|
]
|
||||||
|
90
keystone_tempest_plugin/tests/scenario/keycloak.py
Normal file
90
keystone_tempest_plugin/tests/scenario/keycloak.py
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
# 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 requests
|
||||||
|
|
||||||
|
|
||||||
|
class KeycloakClient(object):
|
||||||
|
def __init__(self, keycloak_url, keycloak_username, keycloak_password,
|
||||||
|
realm='master', ca_certs_file=False):
|
||||||
|
self.keycloak_url = keycloak_url
|
||||||
|
self.keycloak_username = keycloak_username
|
||||||
|
self.keycloak_password = keycloak_password
|
||||||
|
self.session = requests.session()
|
||||||
|
self.realm = realm
|
||||||
|
self.ca_certs_file = ca_certs_file
|
||||||
|
self._admin_auth()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def url_base(self):
|
||||||
|
return self.keycloak_url + f'/admin/realms'
|
||||||
|
|
||||||
|
@property
|
||||||
|
def token_endpoint(self):
|
||||||
|
return self.keycloak_url + \
|
||||||
|
f'/realms/{self.realm}/protocol/openid-connect/token'
|
||||||
|
|
||||||
|
@property
|
||||||
|
def discovery_endpoint(self):
|
||||||
|
return self.keycloak_url + \
|
||||||
|
f'/realms/{self.realm}/.well-known/openid-configuration'
|
||||||
|
|
||||||
|
def _construct_url(self, path):
|
||||||
|
return self.url_base + f'/{self.realm}/{path}'
|
||||||
|
|
||||||
|
def _admin_auth(self):
|
||||||
|
params = {
|
||||||
|
'grant_type': 'password',
|
||||||
|
'client_id': 'admin-cli',
|
||||||
|
'username': self.keycloak_username,
|
||||||
|
'password': self.keycloak_password,
|
||||||
|
'scope': 'openid',
|
||||||
|
}
|
||||||
|
r = requests.post(
|
||||||
|
self.token_endpoint,
|
||||||
|
data=params,
|
||||||
|
verify=self.ca_certs_file).json()
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
'Authorization': ("Bearer %s" % r['access_token']),
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
self.session.headers.update(headers)
|
||||||
|
return r
|
||||||
|
|
||||||
|
def create_user(self, email, first_name, last_name):
|
||||||
|
self._admin_auth()
|
||||||
|
data = {
|
||||||
|
'username': email,
|
||||||
|
'email': email,
|
||||||
|
'firstName': first_name,
|
||||||
|
'lastName': last_name,
|
||||||
|
'enabled': True,
|
||||||
|
'emailVerified': True,
|
||||||
|
'credentials': [{
|
||||||
|
'value': 'secret',
|
||||||
|
'type': 'password',
|
||||||
|
}],
|
||||||
|
'requiredActions': []
|
||||||
|
}
|
||||||
|
return self.session.post(
|
||||||
|
self._construct_url('users'),
|
||||||
|
json=data, verify=self.ca_certs_file)
|
||||||
|
|
||||||
|
def delete_user(self, username):
|
||||||
|
self._admin_auth()
|
||||||
|
data = {
|
||||||
|
'id': username,
|
||||||
|
}
|
||||||
|
return self.session.delete(
|
||||||
|
self._construct_url('users'),
|
||||||
|
json=data, verify=self.ca_certs_file)
|
@ -206,6 +206,8 @@ class TestSaml2FederatedExternalAuthentication(
|
|||||||
"Federated Identity feature not enabled")
|
"Federated Identity feature not enabled")
|
||||||
@testtools.skipUnless(CONF.identity_feature_enabled.external_idp,
|
@testtools.skipUnless(CONF.identity_feature_enabled.external_idp,
|
||||||
"External identity provider is not available")
|
"External identity provider is not available")
|
||||||
|
@testtools.skipUnless(CONF.fed_scenario.protocol_id == 'mapped',
|
||||||
|
"Protocol not mapped")
|
||||||
def test_request_unscoped_token(self):
|
def test_request_unscoped_token(self):
|
||||||
self._test_request_unscoped_token()
|
self._test_request_unscoped_token()
|
||||||
|
|
||||||
@ -213,6 +215,8 @@ class TestSaml2FederatedExternalAuthentication(
|
|||||||
"Federated Identity feature not enabled")
|
"Federated Identity feature not enabled")
|
||||||
@testtools.skipUnless(CONF.identity_feature_enabled.external_idp,
|
@testtools.skipUnless(CONF.identity_feature_enabled.external_idp,
|
||||||
"External identity provider is not available")
|
"External identity provider is not available")
|
||||||
|
@testtools.skipUnless(CONF.fed_scenario.protocol_id == 'mapped',
|
||||||
|
"Protocol not mapped")
|
||||||
def test_request_scoped_token(self):
|
def test_request_scoped_token(self):
|
||||||
self._test_request_scoped_token()
|
self._test_request_scoped_token()
|
||||||
|
|
||||||
@ -328,10 +332,14 @@ class TestK2KFederatedAuthentication(TestSaml2EcpFederatedAuthentication):
|
|||||||
|
|
||||||
@testtools.skipUnless(CONF.identity_feature_enabled.federation,
|
@testtools.skipUnless(CONF.identity_feature_enabled.federation,
|
||||||
"Federated Identity feature not enabled")
|
"Federated Identity feature not enabled")
|
||||||
|
@testtools.skipUnless(CONF.fed_scenario.protocol_id == 'mapped',
|
||||||
|
"Protocol not mapped")
|
||||||
def test_request_unscoped_token(self):
|
def test_request_unscoped_token(self):
|
||||||
self._test_request_unscoped_token()
|
self._test_request_unscoped_token()
|
||||||
|
|
||||||
@testtools.skipUnless(CONF.identity_feature_enabled.federation,
|
@testtools.skipUnless(CONF.identity_feature_enabled.federation,
|
||||||
"Federated Identity feature not enabled")
|
"Federated Identity feature not enabled")
|
||||||
|
@testtools.skipUnless(CONF.fed_scenario.protocol_id == 'mapped',
|
||||||
|
"Protocol not mapped")
|
||||||
def test_request_scoped_token(self):
|
def test_request_scoped_token(self):
|
||||||
self._test_request_scoped_token()
|
self._test_request_scoped_token()
|
||||||
|
@ -0,0 +1,151 @@
|
|||||||
|
# Copyright 2022 Red Hat, Inc.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
from keystoneauth1 import identity
|
||||||
|
from keystoneauth1 import session as ks_session
|
||||||
|
from tempest import config
|
||||||
|
from tempest.lib.common.utils import data_utils
|
||||||
|
import testtools
|
||||||
|
|
||||||
|
from .keycloak import KeycloakClient
|
||||||
|
from keystone_tempest_plugin.tests import base
|
||||||
|
|
||||||
|
CONF = config.CONF
|
||||||
|
|
||||||
|
|
||||||
|
class TestOidcFederatedAuthentication(base.BaseIdentityTest):
|
||||||
|
|
||||||
|
def _setup_settings(self):
|
||||||
|
# Keycloak Settings
|
||||||
|
self.idp_id = CONF.fed_scenario.idp_id
|
||||||
|
self.idp_remote_ids = CONF.fed_scenario.idp_remote_ids
|
||||||
|
self.idp_url = CONF.fed_scenario.idp_oidc_url
|
||||||
|
self.idp_client_id = CONF.fed_scenario.idp_client_id
|
||||||
|
self.idp_client_secret = CONF.fed_scenario.idp_client_secret
|
||||||
|
self.idp_password = CONF.fed_scenario.idp_password
|
||||||
|
self.idp_username = CONF.fed_scenario.idp_username
|
||||||
|
|
||||||
|
self.protocol_id = CONF.fed_scenario.protocol_id
|
||||||
|
self.keystone_v3_endpoint = CONF.identity.uri_v3
|
||||||
|
|
||||||
|
# mapping settings
|
||||||
|
self.mapping_remote_type = CONF.fed_scenario.mapping_remote_type
|
||||||
|
self.mapping_user_name = CONF.fed_scenario.mapping_user_name
|
||||||
|
self.mapping_group_name = CONF.fed_scenario.mapping_group_name
|
||||||
|
self.mapping_group_domain_name = \
|
||||||
|
CONF.fed_scenario.mapping_group_domain_name
|
||||||
|
|
||||||
|
# custom CA certificate settings
|
||||||
|
self.ca_certificates_file = CONF.identity.ca_certificates_file
|
||||||
|
|
||||||
|
def _setup_mapping(self):
|
||||||
|
self.mapping_id = data_utils.rand_uuid_hex()
|
||||||
|
rules = [{
|
||||||
|
'local': [
|
||||||
|
{
|
||||||
|
'user': {'name': self.mapping_user_name}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'group': {
|
||||||
|
'domain': {'name': self.mapping_group_domain_name},
|
||||||
|
'name': self.mapping_group_name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'remote': [
|
||||||
|
{
|
||||||
|
'type': self.mapping_remote_type
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}]
|
||||||
|
mapping_ref = {'rules': rules}
|
||||||
|
self.mappings_client.create_mapping_rule(self.mapping_id, mapping_ref)
|
||||||
|
self.addCleanup(
|
||||||
|
self.mappings_client.delete_mapping_rule, self.mapping_id)
|
||||||
|
|
||||||
|
def _setup_protocol(self):
|
||||||
|
self.idps_client.add_protocol_and_mapping(
|
||||||
|
self.idp_id, self.protocol_id, self.mapping_id)
|
||||||
|
self.addCleanup(
|
||||||
|
self.idps_client.delete_protocol_and_mapping,
|
||||||
|
self.idp_id,
|
||||||
|
self.protocol_id)
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(TestOidcFederatedAuthentication, self).setUp()
|
||||||
|
self._setup_settings()
|
||||||
|
|
||||||
|
# Setup mapping and protocol
|
||||||
|
self._setup_mapping()
|
||||||
|
self._setup_protocol()
|
||||||
|
self.keycloak = KeycloakClient(
|
||||||
|
keycloak_url=self.idp_url,
|
||||||
|
keycloak_username=self.idp_username,
|
||||||
|
keycloak_password=self.idp_password,
|
||||||
|
ca_certs_file=self.ca_certificates_file,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _setup_user(self, email=None):
|
||||||
|
email = email if email else f'test-{uuid.uuid4().hex}@example.com'
|
||||||
|
self.keycloak.create_user(email, 'Test', 'User')
|
||||||
|
return email
|
||||||
|
|
||||||
|
def _request_unscoped_token(self, user):
|
||||||
|
auth = identity.v3.OidcPassword(
|
||||||
|
auth_url=self.keystone_v3_endpoint,
|
||||||
|
identity_provider=self.idp_id,
|
||||||
|
protocol=self.protocol_id,
|
||||||
|
client_id=self.idp_client_id,
|
||||||
|
client_secret=self.idp_client_secret,
|
||||||
|
access_token_endpoint=self.keycloak.token_endpoint,
|
||||||
|
discovery_endpoint=self.keycloak.discovery_endpoint,
|
||||||
|
username=user,
|
||||||
|
password='secret'
|
||||||
|
)
|
||||||
|
s = ks_session.Session(auth, verify=self.ca_certificates_file)
|
||||||
|
return s.get_auth_headers()
|
||||||
|
|
||||||
|
@testtools.skipUnless(CONF.identity_feature_enabled.federation,
|
||||||
|
"Federated Identity feature not enabled")
|
||||||
|
@testtools.skipUnless(CONF.identity_feature_enabled.external_idp,
|
||||||
|
"External identity provider is not available")
|
||||||
|
@testtools.skipUnless(CONF.fed_scenario.protocol_id == 'openid',
|
||||||
|
"Protocol not openid")
|
||||||
|
def test_request_unscoped_token(self):
|
||||||
|
user = self._setup_user()
|
||||||
|
token = self._request_unscoped_token(user)
|
||||||
|
self.assertNotEmpty(token)
|
||||||
|
self.keycloak.delete_user(user)
|
||||||
|
|
||||||
|
@testtools.skipUnless(CONF.identity_feature_enabled.federation,
|
||||||
|
"Federated Identity feature not enabled")
|
||||||
|
@testtools.skipUnless(CONF.identity_feature_enabled.external_idp,
|
||||||
|
"External identity provider is not available")
|
||||||
|
@testtools.skipUnless(CONF.fed_scenario.protocol_id == 'openid',
|
||||||
|
"Protocol not openid")
|
||||||
|
def test_request_scoped_token(self):
|
||||||
|
user = self._setup_user()
|
||||||
|
token = self._request_unscoped_token(user)
|
||||||
|
token_id = token['X-Auth-Token']
|
||||||
|
|
||||||
|
projects = self.auth_client.get_available_projects_scopes(
|
||||||
|
self.keystone_v3_endpoint, token_id)['projects']
|
||||||
|
self.assertNotEmpty(projects)
|
||||||
|
|
||||||
|
# Get a scoped token to one of the listed projects
|
||||||
|
self.tokens_client.auth(
|
||||||
|
project_id=projects[0]['id'], token=token_id)
|
||||||
|
self.keycloak.delete_user(user)
|
@ -5,6 +5,7 @@
|
|||||||
pbr!=2.1.0,>=2.0.0 # Apache-2.0
|
pbr!=2.1.0,>=2.0.0 # Apache-2.0
|
||||||
|
|
||||||
# xml parsing
|
# xml parsing
|
||||||
|
keystoneauth1>=5.1.1 # Apache-2.0
|
||||||
lxml!=3.7.0,>=3.4.1 # BSD
|
lxml!=3.7.0,>=3.4.1 # BSD
|
||||||
tempest>=17.1.0 # Apache-2.0
|
tempest>=17.1.0 # Apache-2.0
|
||||||
oslo.config>=5.2.0 # Apache-2.0
|
oslo.config>=5.2.0 # Apache-2.0
|
||||||
|
Loading…
Reference in New Issue
Block a user